Skip to content

Commit

Permalink
Tor project analyzer (#138)
Browse files Browse the repository at this point in the history
* First draft for the tor analyzer

* Moving HTTP request to private method

* Add a TTL parameter to ignore to old nodes

* Add cache mechhanism to avoid always downloading the same file

* Add documentation and fix default value for client.

If ttl is set to 0 then the last update status of the node won't be used.
If cache.duration is set to 0 then no cache will be kept.

* Add report templates

* Bumping version to 1.0

* Add author to TorProject config file

* Add license to analyzer

* Set correct rights on .py files

* use correct version number

* Change report structure

* Slight changes on reports

* Fix boolean display in short report

* Replace pyfscache by diskcache to ensure Python3 compatibility

* Replace getParam() fucntion by get_param()

* Respect contribution policy

* Force usage of python3 when executing analyzer
  • Loading branch information
srilumpa authored and nadouani committed Dec 18, 2017
1 parent cf66ed2 commit 37dcc16
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 0 deletions.
15 changes: 15 additions & 0 deletions analyzers/TorProject/TorProject.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "TorProject",
"author": "Marc-André DOLL, STARC by EXAPROBE",
"license": "AGPL-V3",
"url": "https://github.com/CERT-BDF/Cortex-Analyzers",
"version": "1.0",
"baseConfig": "TorProject",
"config": {
"check_tlp": false,
"max_tlp": 3
},
"description": "Query https://check.torproject.org/exit-addresses for TOR exit nodes IP addresses.",
"dataTypeList": ["ip"],
"command": "TorProject/tor_project_analyzer.py"
}
6 changes: 6 additions & 0 deletions analyzers/TorProject/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
cortexutils
requests
datetime
python-dateutil
pytz
diskcache
80 changes: 80 additions & 0 deletions analyzers/TorProject/tor_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import requests
from datetime import datetime, timedelta
from dateutil.parser import parse
import pytz
from diskcache import Cache


class TorProjectClient:
"""Simple client to query torproject.org for exit nodes.
The client will download https://check.torproject.org/exit-addresses
and check if a specified IP address is present in it. If that IP address
is found it will check for its last update time and return a description
of the node if its last update time is less than `ttl` seconds ago.
:param ttl: Tor node will be kept only if its last update was
less than `ttl` seconds ago. Ignored if `ttl` is 0
:param cache_duration: Duration before refreshing the cache (in seconds).
Ignored if `cache_duration` is 0.
:param cache_root: Path where to store the cached file
downloaded from torproject.org
:type ttl: int
:type cache_duration: int
:type cache_root: str
"""
def __init__(self, ttl=86400, cache_duration=3600,
cache_root='/tmp/cortex/tor_project'):
self.session = requests.Session()
self.delta = None
self.cache = None
if ttl > 0:
self.delta = timedelta(seconds=ttl)
if cache_duration > 0:
self.cache = Cache(cache_root)
self.cache_duration = cache_duration
self.url = 'https://check.torproject.org/exit-addresses'

__cache_key = __name__ + ':raw_data'

def _get_raw_data(self):
try:
return self.cache['raw_data']
except(AttributeError, TypeError):
return self.session.get(self.url).text
except KeyError:
self.cache.set(
'raw_data',
self.session.get(self.url).text,
expire=self.cache_duration)
return self.cache['raw_data']

def search_tor_node(self, ip):
"""Lookup an IP address to check if it is a known tor exit node.
:param ip: The IP address to lookup
:type ip: str
:return: Data relative to the tor node. If `ip`is a tor exit node
it will contain a `node` key with the hash of the node and
a `last_status` key with the last update time of the node.
If `ip` is not a tor exit node, the function will return an
empty dictionary.
:rtype: dict
"""
data = {}
tmp = {}
present = datetime.utcnow().replace(tzinfo=pytz.utc)
for line in self._get_raw_data().splitlines():
params = line.split(' ')
if params[0] == 'ExitNode':
tmp['node'] = params[1]
elif params[0] == 'ExitAddress':
tmp['last_status'] = params[2] + 'T' + params[3] + '+0000'
last_status = parse(tmp['last_status'])
if (self.delta is None or
(present - last_status) < self.delta):
data[params[1]] = tmp
tmp = {}
else:
pass
return data.get(ip, {})
41 changes: 41 additions & 0 deletions analyzers/TorProject/tor_project_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
from cortexutils.analyzer import Analyzer
import tor_project


class TorProjectAnalyzer(Analyzer):
"""Cortex analyzer to query TorProject for exit nodes IP addresses"""
def __init__(self):
Analyzer.__init__(self)
self.ttl = self.get_param('config.ttl', 86400)
self.cache_duration = self.get_param('config.cache.duration', 3600)
self.cache_root = self.get_param(
'config.cache.root', '/tmp/cortex/tor_project'
)

self.client = tor_project.TorProjectClient(
ttl=self.ttl,
cache_duration=self.cache_duration,
cache_root=self.cache_root
)

def summary(self, raw):
taxonomies = []
level = 'info'
value = False
if ("node" in raw):
level = 'suspicious'
value = True
taxonomies.append(
self.build_taxonomy(level, 'TorProject', 'Node', value))
return {"taxonomies": taxonomies}

def run(self):
if self.data_type != 'ip':
return self.error('Not an IP address')
report = self.client.search_tor_node(self.get_data())
self.report(report)


if __name__ == '__main__':
TorProjectAnalyzer().run()
29 changes: 29 additions & 0 deletions thehive-templates/TorProject_1_0/long.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- Node found -->
<div class="panel panel-warning" ng-if="success && content.node">
<div class="panel-heading">
Tor Project - <strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong>
</div>
<div class="panel-body">
Exit node found: {{content.node}} updated at {{content.last_status}}
</div>
</div>

<!-- Empty result -->
<div class="panel panel-success" ng-if="success && !content.node">
<div class="panel-heading">
Tor Project - <strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong>
</div>
<div class="panel-body">
<b>Not a known exit node.</b>
</div>
</div>

<!-- General error -->
<div class="panel panel-danger" ng-if="!success">
<div class="panel-heading">
Tor Project - <strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong>
</div>
<div class="panel-body">
{{content.errorMessage}}
</div>
</div>
3 changes: 3 additions & 0 deletions thehive-templates/TorProject_1_0/short.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<span class="label" ng-repeat="t in content.taxonomies" ng-class="{'info': 'label-info', 'safe': 'label-success', 'suspicious': 'label-warning', 'malicious':'label-danger'}[t.level]">
{{t.namespace}}:{{t.predicate}}={{t.value?'yes':'no'}}
</span>

0 comments on commit 37dcc16

Please sign in to comment.