Skip to content

Commit

Permalink
Merge pull request #816 from vaclavbartos/master
Browse files Browse the repository at this point in the history
New analyzer: NERD
  • Loading branch information
dadokkio authored Aug 10, 2020
2 parents 61641d4 + 33d8439 commit 4450424
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 0 deletions.
Binary file added analyzers/NERD/assets/NERD_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added analyzers/NERD/assets/NERD_long.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added analyzers/NERD/assets/NERD_short.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions analyzers/NERD/nerd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "NERD",
"version": "1.0",
"author": "Vaclav Bartos, CESNET",
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
"license": "AGPL-V3",
"description": "Get Reputation score and other basic information from Network Entity Reputation Database (NERD)",
"dataTypeList": ["ip"],
"baseConfig": "NERD",
"command": "NERD/nerd_analyzer.py",
"configurationItems": [
{
"name": "key",
"description": "API key",
"type": "string",
"multi": false,
"required": true
},
{
"name": "url",
"description": "Base URL of the NERD instance",
"type": "string",
"multi": false,
"required": false,
"defaultValue": "https://nerd.cesnet.cz/nerd/"
}
],
"registration_required": true,
"subscription_required": false,
"service_homepage": "https://nerd.cesnet.cz/",
"service_logo": {
"path": "assets/NERD_logo.png",
"caption": "logo"
},
"screenshots": [
{
"path": "assets/NERD_long.png",
"caption": "NERD long report sample"
},
{
"path": "assets/NERD_short.png",
"caption:": "NERD mini report sample"
}
]
}
125 changes: 125 additions & 0 deletions analyzers/NERD/nerd_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
# encoding: utf-8

import requests
from cortexutils.analyzer import Analyzer

# Map of tag IDs to (name,level)-tuple used in summary (tags not listed here are not shown)
tag_map = {
'reconscanning': ('Scanner', 'suspicious'),
'attemptexploit': ('Exploit', 'malicious'),
'attemptlogin': ('Login', 'malicious'),
'malware': ('Malware', 'malicious'),
'availabilitydos': ('DDoS', 'malicious'),
'researchscanners': ('Research scanner', 'safe'),
'vpn': ('VPN', 'info'),
'nat': ('NAT', 'info'),
'dsl': ('DSL', 'info'),
'dynamicIP': ('Dynamic IP', 'info'),
'tor': ('Tor exit node', 'info'),
'spam': ('Spam', 'malicious'),
'reserved_ip': ('Reserved IP', 'info'),
}


class NERDAnalyzer(Analyzer):
def __init__(self):
Analyzer.__init__(self)
self.base_url = self.get_param('config.url', None, 'Config error: Missing URL of NERD.')
if not self.base_url.endswith('/'):
self.base_url += '/'
self.api_key = self.get_param('config.key', None, 'Config error: Missing API key for NERD.')

def summary(self, raw):
"""Returns a summary, needed for 'short.html' template, based on the full report"""

taxonomies = []
if 'message' in raw:
# IP wasn't found
taxonomies.append(self.build_taxonomy('safe', 'NERD', 'Rep', 'no-data'))
else:
# Reputation score (set level/color according to the score)
rep = round(raw['rep'], 3)
rep_level = 'safe' if rep < 0.02 else ('suspicious' if rep <= 0.5 else 'malicious')
taxonomies.append(self.build_taxonomy(rep_level, 'NERD', 'Rep', rep))

# Number of blacklists
if raw['blacklists']:
taxonomies.append(self.build_taxonomy('malicious', 'NERD', 'Blacklists', len(raw['blacklists'])))

# Tags
for tag_name,level in raw['translated_tags']:
taxonomies.append(self.build_taxonomy(level, 'NERD', 'Tag', tag_name))

return {'taxonomies': taxonomies}

def artifacts(self, raw):
"""Returns a list of indicators extracted from reply (only "hostname" in this case)"""
if raw.get('hostname'):
return [{'dataType': 'fqdn', 'data': raw['hostname']}]
return []

def run(self):
"""Main function run by Cortex to analyze an observable."""
if self.data_type != 'ip':
self.error("Invalid data type, only IP addresses are supported")
return
ip = self.get_data()

# Get data from server
url = '{}api/v1/ip/{}'.format(self.base_url, ip)
headers = {'Authorization': self.api_key}
try:
resp = requests.get(url, headers=headers)
except Exception as e:
self.error("Error when trying to contact server: {}".format(e))
return

# Parse received data
try:
data = resp.json()
except ValueError:
self.error("Unexpected or invalid response received from server (can't parse as JSON). A possible reason can be wrong URL.")
return

if resp.status_code == 404:
# IP not found in NERD's DB (i.e. it wasn't reported as malicious)
self.report({
'rep': 0.0,
'message': '{} not found in NERD, i.e. there are no recent reports of malicious activity.'.format(ip),
'nerd_url': '{}ip/{}'.format(self.base_url, ip), # Link to IP's page at NERD web
})
return
elif resp.status_code == 200:
# Success, IP data received - format as output for Cortex
try:
# Translate tags
translated_tags = []
tag_ids = [t['n'] for t in data['tags'] if t.get('c', 1.0) >= 0.5] # List of tags with confidence >= 50%
for tag in tag_ids:
try:
tag_name, level = tag_map[tag]
except KeyError:
continue
translated_tags.append([tag_name, level])
# Create report
self.report({
'rep': data['rep'], # reputation score (number between 0.0 to 1.0)
'hostname': data['hostname'], # result of DNS PTR qeury
'asn': data['asn'], # list of ASNs announcing the IP (usually just one)
'country': data['geo'].get('ctry', ''), # Geolocation - two-letter country code
'blacklists': data['bl'], # List of blacklists the IP is listed on
'tags': tag_ids, # Original Tags as in NERD
'translated_tags': translated_tags, # Tags filtered and translated to nicer names
'nerd_url': '{}ip/{}'.format(self.base_url, ip), # Link to IP's page at NERD web
})
except KeyError as e:
self.error("Invalid response received from server, missing field: {}".format(e))
else:
# Unexpected status code, there should be an 'error' field with error message
self.error("Error: {} {}".format(resp.status_code, data.get('error', '(no error message)')))
return


if __name__ == '__main__':
NERDAnalyzer().run()
2 changes: 2 additions & 0 deletions analyzers/NERD/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests
cortexutils
52 changes: 52 additions & 0 deletions thehive-templates/NERD_1_0/long.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<div class="panel panel-info" ng-if="success">
<div class="panel-heading">
NERD information for <strong>{{artifact.data}}</strong>
</div>
<div class="panel-body" ng-if="content.message">
<p>{{content.message}}</p>
<p>Current info at: <a href="{{content.nerd_url}}" target="_blank">{{content.nerd_url}}</a></p>
</div>
<div class="panel-body" ng-if="!content.message">
<dl class="dl-horizontal">
<dt class="text-bold">
Reputation score
<sup style="color: #999" title="A score computed from the number of recent alerts, their age, and the number of distinct sources (0.0 = no alerts, 1.0 = lots of alerts from multiple sources)">(?)</sup>
</dt>
<dd><span style="color: hsl({{ (100 - 100*content.rep | number: 2) }}, 85%, 50%); margin-right: 5px">&#x2B24;</span>{{content.rep | number: 3}}</dd>
</dl>
<dl class="dl-horizontal">
<dt class="text-bold">Hostname</dt>
<dd>{{content.hostname || "&mdash;"}}</dd>
</dl>
<dl class="dl-horizontal">
<dt class="text-bold">Blacklists</dt>
<dd>{{content.blacklists.join(", ") || "&mdash;"}}</dd>
</dl>
<dl class="dl-horizontal">
<dt class="text-bold">Tags</dt>
<dd>
<span ng-repeat="t in content.translated_tags" class="label" style="margin-right: 5px" ng-class="{'info': 'label-info', 'safe': 'label-success', 'suspicious': 'label-warning', 'malicious':'label-danger'}[t[1]]">{{t[0]}}</span>
<span ng-if="!content.translated_tags.length">&mdash;</span>
</dd>
</dl>
<dl class="dl-horizontal">
<dt class="text-bold">ASN</dt>
<dd>{{content.asn.join(", ") || "unknown"}}</dd>
</dl>
<dl class="dl-horizontal">
<dt class="text-bold">Country</dt>
<dd>{{content.country || "unknown"}}</dd>
</dl>
<p>Full info at: <a href="{{content.nerd_url}}" target="_blank">{{content.nerd_url}}</a></p>
</div>
</div>

<!-- General error -->
<div class="panel panel-danger" ng-if="!success">
<div class="panel-heading">
<strong>{{artifact.data}}</strong>
</div>
<div class="panel-body">
{{content.errorMessage}}
</div>
</div>
3 changes: 3 additions & 0 deletions thehive-templates/NERD_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}}"
</span>

0 comments on commit 4450424

Please sign in to comment.