diff --git a/analyzers/SpamAssassin/SpamAssassin.json b/analyzers/SpamAssassin/SpamAssassin.json new file mode 100644 index 000000000..fa7ae397c --- /dev/null +++ b/analyzers/SpamAssassin/SpamAssassin.json @@ -0,0 +1,64 @@ +{ + "name": "SpamAssassin", + "author": "Davide Arcuri - LDO-CERT", + "license": "AGPL-V3", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "version": "1.0", + "description": "Get spam score from local SpamAssassin instance", + "dataTypeList": [ + "file" + ], + "baseConfig": "SpamAssassin", + "command": "SpamAssassin/spamassassin.py", + "configurationItems": [ + { + "name": "url", + "description": "SpamAssassin url", + "multi": false, + "required": true, + "type": "string" + }, + { + "name": "port", + "description": "SpamAssassin port", + "type": "number", + "defaultValue": 783, + "multi": false, + "required": true + }, + { + "name": "spam_score", + "description": "Minimum score to consider mail as spam", + "type": "number", + "multi": false, + "required": false, + "defaultValue": 5 + }, + { + "name": "timeout", + "description": "Timout for socket operations in seconds", + "type": "number", + "multi": false, + "required": false, + "defaultValue": 20 + } + ], + "registration_required": false, + "subscription_required": false, + "free_subscription": false, + "service_homepage": "https://spamassassin.apache.org/", + "service_logo": { + "path": "assets/SpamAssassin_logo.png", + "caption": "logo" + }, + "screenshots": [ + { + "path": "assets/SpamAssassin_long.png", + "caption": "SpamAssassin long report sample" + }, + { + "path": "assets/SpamAssassin_short.png", + "caption:": "SpamAssassin mini report sample" + } + ] +} \ No newline at end of file diff --git a/analyzers/SpamAssassin/assets/SpamAssassin_logo.png b/analyzers/SpamAssassin/assets/SpamAssassin_logo.png new file mode 100644 index 000000000..23e779ff6 Binary files /dev/null and b/analyzers/SpamAssassin/assets/SpamAssassin_logo.png differ diff --git a/analyzers/SpamAssassin/assets/SpamAssassin_long.png b/analyzers/SpamAssassin/assets/SpamAssassin_long.png new file mode 100644 index 000000000..7ae63cd4f Binary files /dev/null and b/analyzers/SpamAssassin/assets/SpamAssassin_long.png differ diff --git a/analyzers/SpamAssassin/assets/SpamAssassin_short.png b/analyzers/SpamAssassin/assets/SpamAssassin_short.png new file mode 100644 index 000000000..b9e37937d Binary files /dev/null and b/analyzers/SpamAssassin/assets/SpamAssassin_short.png differ diff --git a/analyzers/SpamAssassin/requirements.txt b/analyzers/SpamAssassin/requirements.txt new file mode 100644 index 000000000..37dfee161 --- /dev/null +++ b/analyzers/SpamAssassin/requirements.txt @@ -0,0 +1 @@ +cortexutils \ No newline at end of file diff --git a/analyzers/SpamAssassin/spamassassin.py b/analyzers/SpamAssassin/spamassassin.py new file mode 100644 index 000000000..c566388a6 --- /dev/null +++ b/analyzers/SpamAssassin/spamassassin.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +import socket +import select +import re +from io import BytesIO +from cortexutils.analyzer import Analyzer + + +divider_pattern = re.compile(br'^(.*?)\r?\n(.*?)\r?\n\r?\n', re.DOTALL) +first_line_pattern = re.compile(br'^SPAMD/[^ ]+ 0 EX_OK$') + + +class SpamAssassinAnalyzer(Analyzer): + def __init__(self): + Analyzer.__init__(self) + url = self.get_param("config.url", None) + port = self.get_param("config.port", None) + self.spam_score = self.get_param("config.spam_score", 5) + self.timeout = self.get_param("config.timeout", 20) + if url and port: + self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client.settimeout(self.timeout) + self.client.connect((url, port)) + + + def _build_message(self, message): + reqfp = BytesIO() + data_len = str(len(message)).encode() + reqfp.write(b'REPORT SPAMC/1.2\r\n') + reqfp.write(b'Content-Length: ' + data_len + b'\r\n') + reqfp.write(b'User: cx42\r\n\r\n') + reqfp.write(message) + return reqfp.getvalue() + + + def _parse_response(self, response): + if response == b'': + return None + + match = divider_pattern.match(response) + if not match: + return None + + first_line = match.group(1) + headers = match.group(2) + body = response[match.end(0):] + + match = first_line_pattern.match(first_line) + if not match: + return None + + report_list = [s.strip() for s in body.decode('utf-8', errors="ignore").strip().split('\n')] + linebreak_num = report_list.index([s for s in report_list if "---" in s][0]) + tablelists = [s for s in report_list[linebreak_num + 1:]] + + tablelists_temp = [] + if tablelists: + for counter, tablelist in enumerate(tablelists): + if len(tablelist)>1: + if (tablelist[0].isnumeric() or tablelist[0] == '-') and (tablelist[1].isnumeric() or tablelist[1] == '.'): + tablelists_temp.append(tablelist) + else: + if tablelists_temp: + tablelists_temp[-1] += " " + tablelist + tablelists = tablelists_temp + + report_json = {"values": []} + for tablelist in tablelists: + wordlist = re.split('\s+', tablelist) + report_json['values'].append({'partscore': float(wordlist[0]), 'description': ' '.join(wordlist[1:]), 'name': wordlist[1]}) + + headers = headers.decode('utf-8').replace(' ', '').replace(':', ';').replace('/', ';').split(';') + report_json['score'] = float(headers[2]) + report_json['is_spam'] = float(headers[2]) > self.spam_score + return report_json + + + def summary(self, raw): + taxonomies = [] + level = "suspicious" if raw.get('is_spam', None) else "info" + taxonomies.append(self.build_taxonomy(level, "Spamassassin", "score", raw.get('score', 0))) + return {"taxonomies": taxonomies} + + + def run(self): + Analyzer.run(self) + + data = self.get_param("file", None, "File is missing") + if self.data_type != "file": + self.error("Invalid data type") + + with open(data, 'rb') as f: + message = f.read() + + self.client.sendall(self._build_message(message)) + self.client.shutdown(socket.SHUT_WR) + + resfp = BytesIO() + while True: + ready = select.select([self.client], [], [], self.timeout) + if ready[0] is None: + self.error("Timeout during socket operation") + + data = self.client.recv(4096) + if data == b'': + break + + resfp.write(data) + + self.client.close() + self.client = None + + + self.report(self._parse_response(resfp.getvalue())) + + +if __name__ == "__main__": + SpamAssassinAnalyzer().run() \ No newline at end of file diff --git a/thehive-templates/SpamAssassin_1_0/long.html b/thehive-templates/SpamAssassin_1_0/long.html new file mode 100755 index 000000000..0746f5e63 --- /dev/null +++ b/thehive-templates/SpamAssassin_1_0/long.html @@ -0,0 +1,36 @@ +
+ +
+ SpamAssassin score for {{(artifact.data || artifact.attachment.name) | fang}} +
+
+

Final Score

+
+
Score
+
{{content.score}}
+
+
+
Is Spam?
+
truefalse
+
+
+

Score Detail

+
+
{{value.name}}
+
{{value.description}} [+{{value.partscore}}]
+
+
+
+ + +
+
+ SpamAssassin score Error +
+
+
+
Error:
+
{{content.errorMessage}}
+
+
+
\ No newline at end of file diff --git a/thehive-templates/SpamAssassin_1_0/short.html b/thehive-templates/SpamAssassin_1_0/short.html new file mode 100755 index 000000000..5fc0dabfb --- /dev/null +++ b/thehive-templates/SpamAssassin_1_0/short.html @@ -0,0 +1,3 @@ + + {{t.namespace}}:{{t.predicate}}="{{t.value}}" +