diff --git a/README.md b/README.md index 75707dedba7cc7a4987fa002c0a8ed73db49f925..f89df05cd2bcb28d9eb4c679ffd171f8aab19669 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # Cortex analyzers +Custom Cortex analyzers and responders + + diff --git a/analyzers/NERD/nerd.json b/analyzers/NERD/nerd.json new file mode 100644 index 0000000000000000000000000000000000000000..76bf20e998fd05ef935b26ba3bc94aeb37dbfc34 --- /dev/null +++ b/analyzers/NERD/nerd.json @@ -0,0 +1,28 @@ +{ + "name": "NERD", + "version": "1.0", + "author": "Vaclav Bartos, CESNET", + "url": "TODO (not public yet)", + "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/" + } + ] +} \ No newline at end of file diff --git a/analyzers/NERD/nerd_analyzer.py b/analyzers/NERD/nerd_analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..f297d1790708a71f7e93a694f77d7cdb791bd43f --- /dev/null +++ b/analyzers/NERD/nerd_analyzer.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +import requests +from cortexutils.analyzer import Analyzer + +class NERDAnalyzer(Analyzer): + def __init__(self): + Analyzer.__init__(self) + self.base_url = self.get_param('config.url', None, 'Missing URL of NERD.') + if not self.base_url.endswith('/'): + self.base_url += '/' + self.api_key = self.get_param('config.key', None, '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 in raw['tags']: + # TODO: filter tags, set different levels + taxonomies.append(self.build_taxonomy('info', 'NERD', 'Tag', tag)) + + return {'taxonomies': taxonomies} + + def artifacts(self, raw): + """Returns a list of indicators extracted from reply (empty in this case)""" + return [] # TODO add hostname as a new indicator? + + def run(self): + """Main function run by Cortex to analyze an observable.""" + print("RUN") + if self.data_type != 'ip': + self.error("Invalid data type, only IP addresses are supported") + return + ip = self.get_data() + print("ip:", ip) + + # 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("Invalid response received from server.") + 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) + }) + return + elif resp.status_code == 200: + # Success, IP data received - format as output for Cortex + try: + 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']['ctry'], # Geolocation - two-letter country code + 'blacklists': data['bl'], # List of blacklists the IP is listed on + 'tags': [t['n'] for t in data['tags'] if t.get('c', 1.0) >= 0.5] # List of tags (with confidence >= 50%) + }) + 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() diff --git a/analyzers/NERD/requirements.txt b/analyzers/NERD/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..58023889324134d497fc817c65c6607143810207 --- /dev/null +++ b/analyzers/NERD/requirements.txt @@ -0,0 +1,2 @@ +requests +cortexutils \ No newline at end of file