From 64d0131ca13e95b71299f0bdbfcfae589fb7c0ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Barto=C5=A1?= <bartos@cesnet.cz>
Date: Thu, 11 Jun 2020 16:09:47 +0200
Subject: [PATCH] NERD analyzer added

---
 README.md                       |  3 ++
 analyzers/NERD/nerd.json        | 28 ++++++++++
 analyzers/NERD/nerd_analyzer.py | 95 +++++++++++++++++++++++++++++++++
 analyzers/NERD/requirements.txt |  2 +
 4 files changed, 128 insertions(+)
 create mode 100644 analyzers/NERD/nerd.json
 create mode 100644 analyzers/NERD/nerd_analyzer.py
 create mode 100644 analyzers/NERD/requirements.txt

diff --git a/README.md b/README.md
index 75707de..f89df05 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 0000000..76bf20e
--- /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 0000000..f297d17
--- /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 0000000..5802388
--- /dev/null
+++ b/analyzers/NERD/requirements.txt
@@ -0,0 +1,2 @@
+requests
+cortexutils
\ No newline at end of file
-- 
GitLab