Permalink
Browse files

Add new tanner Api function (#166)

* Add new tanner api function

* apply common filter approach

* access code to use api methods

* fix errors

* apply sub-app approach

* add complex statistics

* rename methods

* fix bug

* update method

* fix invalid snare_uuid

* fix possible owner keyerror

* fix and add new tests

* update tests

* fix invalid filter error

* fix snare_uuid key value

* fix apply filters

* fix tests

* fix tests
  • Loading branch information...
rnehra01 authored and afeena committed Jul 7, 2017
1 parent 98fa4fb commit d89834196cc36833cd5f0bbd38a1b3222db2aa6d
Showing with 228 additions and 41 deletions.
  1. +140 −14 tanner/api.py
  2. +18 −13 tanner/server.py
  3. +2 −2 tanner/session.py
  4. +2 −2 tanner/session_analyzer.py
  5. +65 −9 tanner/tests/test_server.py
  6. +1 −1 tanner/tests/test_session_analyzer.py
View
@@ -1,35 +1,104 @@
import json
import logging
import operator
import asyncio_redis
from aiohttp import web
class Api:
def __init__(self):
def __init__(self, redis_client):
self.logger = logging.getLogger('tanner.api.Api')
self.redis_client = redis_client
async def handle_api_request(self, query, params, redis_client):
result = None
@staticmethod
def _make_response(msg):
response_message = dict(
version=1,
response=dict(message=msg)
)
return response_message
if query == 'stats' and not params:
result = await self.return_stats(redis_client)
elif query == 'stats' and 'uuid' in params:
result = await self.return_uuid_stats(params['uuid'], redis_client, 50)
return result
async def handle_index(self, request):
result = 'tanner api'
response_msg = self._make_response(result)
return web.json_response(response_msg)
async def handle_snares(self, request):
result = await self.return_snares()
response_msg = self._make_response(result)
return web.json_response(response_msg)
async def handle_snare_info(self, request):
snare_uuid = request.match_info['snare_uuid']
result = await self.return_snare_info(snare_uuid, 50)
response_msg = self._make_response(result)
return web.json_response(response_msg)
async def return_stats(self, redis_client):
async def handle_snare_stats(self, request):
snare_uuid = request.match_info['snare_uuid']
result = await self.return_snare_stats(snare_uuid)
response_msg = self._make_response(result)
return web.json_response(response_msg)
async def handle_sessions(self, request):
params = request.url.query
applied_filters = {}
try:
if 'filters' in params:
applied_filters = {filt.split(':')[0] : filt.split(':')[1] for filt in params['filters'].split()}
if 'start_time' in applied_filters:
applied_filters['start_time'] = float(applied_filters['start_time'])
if 'end_time' in applied_filters:
applied_filters['end_time'] = float(applied_filters['end_time'])
except Exception as e:
self.logger.error('Filter error : %s' % e)
result = 'Invalid filter definition'
else:
result = await self.return_sessions(applied_filters)
response_msg = self._make_response(result)
return web.json_response(response_msg)
async def handle_session_info(self, request):
sess_uuid = request.match_info['sess_uuid']
result = await self.return_session_info(sess_uuid)
response_msg = self._make_response(result)
return web.json_response(response_msg)
async def return_snares(self):
query_res = []
try:
query_res = await redis_client.smembers('snare_ids')
query_res = await self.redis_client.smembers('snare_ids')
query_res = await query_res.asset()
except asyncio_redis.NotConnectedError as connection_error:
self.logger.error('Can not connect to redis %s', connection_error)
return list(query_res)
async def return_uuid_stats(self, uuid, redis_client, count=-1):
async def return_snare_stats(self, snare_uuid):
result = {}
sessions = await self.return_snare_info(snare_uuid)
if sessions == 'Invalid SNARE UUID':
return result
result['total_sessions'] = len(sessions)
result['total_duration'] = 0
result['attack_frequency'] = {'sqli' : 0,
'lfi' : 0,
'xss' : 0,
'rfi' : 0,
'cmd_exec' : 0}
for sess in sessions:
result['total_duration'] += sess['end_time'] - sess['start_time']
for attack in sess['attack_types']:
if attack in result['attack_frequency']:
result['attack_frequency'][attack] += 1
return result
async def return_snare_info(self, uuid, count=-1):
query_res = []
try:
query_res = await redis_client.lrange_aslist(uuid, 0, count)
query_res = await self.redis_client.lrange_aslist(uuid, 0, count)
except asyncio_redis.NotConnectedError as connection_error:
self.logger.error('Can not connect to redis %s', connection_error)
else:
@@ -38,3 +107,60 @@ def __init__(self):
for (i, val) in enumerate(query_res):
query_res[i] = json.loads(val)
return query_res
async def return_session_info(self, sess_uuid, snare_uuid= None):
query_res = []
if snare_uuid:
snare_uuids = [snare_uuid]
else:
snare_uuids = await self.return_snares()
for snare_id in snare_uuids:
sessions = await self.return_snare_info(snare_id)
if sessions == 'Invalid SNARE UUID':
continue
for sess in sessions:
if sess['sess_uuid'] == sess_uuid:
return sess
async def return_sessions(self, filters):
query_res = []
snare_uuids = await self.return_snares()
matching_sessions = []
for snare_id in snare_uuids:
result = await self.return_snare_info(snare_id)
if result == 'Invalid SNARE UUID':
return 'Invalid filter : SNARE UUID'
sessions = result
for sess in sessions:
match_count = 0
for filter_name, filter_value in filters.items():
try:
if(self.apply_filter(filter_name, filter_value, sess)):
match_count += 1
except KeyError:
return 'Invalid filter : %s' % filter_name
if match_count == len(filters):
matching_sessions.append(sess['sess_uuid'])
return matching_sessions
def apply_filter(self, filter_name, filter_value, sess):
available_filters = {'user_agent' : operator.contains,
'peer_ip' : operator.eq,
'attack_types' : operator.contains,
'possible_owners' : operator.contains,
'start_time' : operator.le,
'end_time': operator.ge,
'snare_uuid' : operator.eq
}
try:
if available_filters[filter_name] is operator.contains:
return available_filters[filter_name](sess[filter_name], filter_value)
else:
return available_filters[filter_name](filter_value, sess[filter_name])
except KeyError:
raise
View
@@ -22,7 +22,7 @@ def __init__(self):
self.session_manager = session_manager.SessionManager()
self.dorks = dorks_manager.DorksManager()
self.api = api.Api()
self.api = None
self.base_handler = base.BaseHandler(base_dir, db_name)
self.logger = logging.getLogger(__name__)
self.redis_client = None
@@ -73,15 +73,6 @@ def _make_response(msg):
lr.create_session(session_data)
return web.json_response(response_msg)
async def handle_api(self, request):
api_query = request.match_info.get("api_query")
if api_query is None:
data = "tanner api"
else:
data = await self.api.handle_api_request(api_query, request.url.query, self.redis_client)
response_msg = self._make_response(data)
return web.json_response(response_msg)
async def handle_dorks(self, request):
dorks = await self.dorks.choose_dorks(self.redis_client)
response_msg = dict(version=1, response=dict(dorks=dorks))
@@ -93,10 +84,21 @@ def _make_response(msg):
def setup_routes(self, app):
app.router.add_route('*', '/', self.default_handler)
app.router.add_post('/event', self.handle_event)
app.router.add_get('/api', self.handle_api)
app.router.add_get('/api/{api_query}', self.handle_api)
app.router.add_get('/dorks', self.handle_dorks)
def setup_api_routes(self, app):
app.router.add_get('/', self.api.handle_index)
app.router.add_get('/snares', self.api.handle_snares)
app.router.add_resource('/snare/{snare_uuid}').add_route('GET', self.api.handle_snare_info)
app.router.add_resource('/snare-stats/{snare_uuid}').add_route('GET', self.api.handle_snare_stats)
app.router.add_resource('/sessions').add_route('GET', self.api.handle_sessions)
app.router.add_resource('/session/{sess_uuid}').add_route('GET', self.api.handle_session_info)
def create_api_app(self, loop):
api_app = web.Application(loop=loop)
self.setup_api_routes(api_app)
return api_app
def create_app(self, loop):
app = web.Application(loop=loop)
app.on_shutdown.append(self.on_shutdown)
@@ -105,8 +107,11 @@ def create_app(self, loop):
def start(self):
loop = asyncio.get_event_loop()
tanner_app = self.create_app(loop)
self.redis_client = loop.run_until_complete(redis_client.RedisClient.get_redis_client())
self.api = api.Api(self.redis_client)
tanner_app = self.create_app(loop)
api_app = self.create_api_app(loop)
tanner_app.add_subapp('/api/', api_app)
host = TannerConfig.get('TANNER', 'host')
port = TannerConfig.get('TANNER', 'port')
web.run_app(tanner_app, host=host, port=port)
View
@@ -17,7 +17,7 @@ def __init__(self, data):
self.ip = data['peer']['ip']
self.port = data['peer']['port']
self.user_agent = data['headers']['user-agent']
self.sensor = data['uuid']
self.snare_uuid = data['uuid']
self.paths = [{'path': data['path'], 'timestamp': time.time(),
'response_status': data['status']}]
self.cookies = data['cookies']
@@ -47,7 +47,7 @@ def is_expired(self):
def to_json(self):
sess = dict(peer=dict(ip=self.ip, port=self.port),
user_agent=self.user_agent,
sensor=self.sensor,
snare_uuid=self.snare_uuid,
sess_uuid=self.sess_uuid.hex,
start_time=self.start_timestamp,
end_time=self.timestamp,
@@ -31,7 +31,7 @@ def __init__(self, loop=None):
async def save_session(self, redis_client):
while not self.queue.empty():
session = await self.queue.get()
s_key = session['sensor_uuid']
s_key = session['snare_uuid']
del_key = session['sess_uuid']
try:
await redis_client.lpush(s_key, [json.dumps(session)])
@@ -52,7 +52,7 @@ def __init__(self, loop=None):
peer_ip=session['peer']['ip'],
peer_port=session['peer']['port'],
user_agent=session['user_agent'],
sensor_uuid=session['sensor'],
snare_uuid=session['snare_uuid'],
start_time=session['start_time'],
end_time=session['end_time'],
requests_in_second=rps,
@@ -4,7 +4,7 @@
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from tanner import server
from tanner import server, api
from tanner.config import TannerConfig
@@ -45,6 +45,7 @@ def setUp(self):
redis.close = mock.Mock()
self.serv.dorks = dorks
self.serv.redis_client = redis
self.serv.api = api.Api(self.serv.redis_client)
super(TestServer, self).setUp()
@@ -56,6 +57,8 @@ def _make_coroutine(self):
def get_app(self):
app = self.serv.create_app(loop=self.loop)
api_app = self.serv.create_api_app(loop=self.loop)
app.add_subapp('/api/', api_app)
return app
@unittest_run_loop
@@ -93,21 +96,74 @@ def test_make_response(self):
self.assertDictEqual(detection, assert_content)
@unittest_run_loop
async def test_api_request(self):
async def test_api_index_request(self):
assert_content = {"version": 1, "response": {"message": "tanner api"}}
request = await self.client.request("GET", "/api")
request = await self.client.request("GET", "/api/")
assert request.status == 200
detection = await request.json()
self.assertDictEqual(detection, assert_content)
@unittest_run_loop
async def test_stats_api_request(self):
async def _make_api_coroutine(*args, **kwargs):
return ["1", "2"]
async def test_api_snares_request(self):
async def mock_return_snares():
return ["8fa6aa98-4283-4085-bfb9-a1cd3a9e56e4"]
assert_content = {"version": 1, "response": {"message": ["1", "2"]}}
self.serv.api.handle_api_request = _make_api_coroutine
request = await self.client.request("GET", "/api/stats")
assert_content = {"version": 1, "response": {"message": ["8fa6aa98-4283-4085-bfb9-a1cd3a9e56e4"]}}
self.serv.api.return_snares = mock_return_snares
request = await self.client.request("GET", "/api/snares")
assert request.status == 200
detection = await request.json()
self.assertDictEqual(detection, assert_content)
@unittest_run_loop
async def test_api_snare_info_request(self):
async def mock_return_snare_info(snare_uuid, count):
if snare_uuid == "8fa6aa98-4283-4085-bfb9-a1cd3a9e56e4" and count == 50:
return [{"test_sess1": "sess1_info"}, {"test_sess1": "sess2_info"}]
assert_content = {"version": 1, "response": {"message": [{"test_sess1": "sess1_info"}, {"test_sess1": "sess2_info"}]}}
self.serv.api.return_snare_info = mock_return_snare_info
request = await self.client.request("GET", "/api/snare/8fa6aa98-4283-4085-bfb9-a1cd3a9e56e4")
assert request.status == 200
detection = await request.json()
self.assertDictEqual(detection, assert_content)
@unittest_run_loop
async def test_api_snare_stats_request(self):
async def mock_return_snare_stats(snare_uuid):
if snare_uuid == "8fa6aa98-4283-4085-bfb9-a1cd3a9e56e4":
return {"total_sessions": 605, "total_duration": 865.560286283493, "attack_frequency": {"sqli": 0, "lfi": 0, "xss": 0, "rfi": 0, "cmd_exec": 0}}
assert_content = {"version": 1, "response": {"message": {"total_sessions": 605, "total_duration": 865.560286283493, "attack_frequency": {"sqli": 0, "lfi": 0, "xss": 0, "rfi": 0, "cmd_exec": 0}}}}
self.serv.api.return_snare_stats = mock_return_snare_stats
request = await self.client.request("GET", "/api/snare-stats/8fa6aa98-4283-4085-bfb9-a1cd3a9e56e4")
assert request.status == 200
detection = await request.json()
self.assertDictEqual(detection, assert_content)
@unittest_run_loop
async def test_api_sessions_request(self):
async def mock_return_sessions(filters):
if type(filters) is dict and filters['peer_ip'] == "127.0.0.1" and \
filters['start_time'] == 1497890400 and filters['user_agent'] == 'ngnix':
return ["f387d46eaeb1454cadf0713a4a55be49", "e85ae767b0bb4b1f91b421b3a28082ef"]
assert_content = {"version": 1, "response": {"message": ["f387d46eaeb1454cadf0713a4a55be49", "e85ae767b0bb4b1f91b421b3a28082ef"]}}
self.serv.api.return_sessions = mock_return_sessions
request = await self.client.request("GET", "/api/sessions?filters=peer_ip:127.0.0.1 start_time:1497890400 user_agent:ngnix")
assert request.status == 200
detection = await request.json()
self.assertDictEqual(detection, assert_content)
@unittest_run_loop
async def test_api_sessions_info_request(self):
async def mock_return_session_info(sess_uuid):
if sess_uuid == "4afd45d61b994d9eb3ba20faa81a45e1":
return {"test_sess1": "sess1_info"}
assert_content = {"version": 1, "response": {"message": {"test_sess1": "sess1_info"}}}
self.serv.api.return_session_info = mock_return_session_info
request = await self.client.request("GET", "/api/session/4afd45d61b994d9eb3ba20faa81a45e1")
assert request.status == 200
detection = await request.json()
self.assertDictEqual(detection, assert_content)
@@ -10,7 +10,7 @@
session = b'{"sess_uuid": "c546114f97f548f982756495f963e280", "start_time": 1466091813.4780173, ' \
b'"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' \
b'Chrome/53.0.2767.4 Safari/537.36", "end_time": 1466091899.9854035, ' \
b'"sensor": "78e51180-bf0d-4757-8a04-f000e5efa179", "count": 24, ' \
b'"snare_uuid": "78e51180-bf0d-4757-8a04-f000e5efa179", "count": 24, ' \
b'"paths": [{"timestamp": 1466091813.4779778, "path": "/", "attack_type": "index", "response_status": 200},' \
b'{"timestamp": 1466091842.7088752, "path": "/fluent-python.html", "attack_type": "index", ' \
b'"response_status": 200}, {"timestamp": 1466091858.214475, "path": "/wow-movie.html?exec=/bin/bash", ' \

0 comments on commit d898341

Please sign in to comment.