Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/spamassassin #811

Merged
merged 3 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions analyzers/SpamAssassin/SpamAssassin.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions analyzers/SpamAssassin/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cortexutils
120 changes: 120 additions & 0 deletions analyzers/SpamAssassin/spamassassin.py
Original file line number Diff line number Diff line change
@@ -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()
36 changes: 36 additions & 0 deletions thehive-templates/SpamAssassin_1_0/long.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="panel panel-info" ng-if="success">

<div class="panel-heading">
SpamAssassin score for <strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong>
</div>
<div class="panel-body">
<h4>Final Score</h4>
<dl class="dl-horizontal">
<dt>Score</dt>
<dd>{{content.score}} </dd>
</dl>
<dl class="dl-horizontal">
<dt>Is Spam?</dt>
<dd><span ng-if="content.is_spam" class="label label-warning">true</span><span ng-if="!content.is_spam" class="label label-primary">false</span></dd>
</dl>
<hr>
<h4>Score Detail</h4>
<dl class="dl-horizontal" ng-if="content.values.length > 0" ng-repeat="value in content.values">
<dt>{{value.name}}</dt>
<dd>{{value.description}} [+{{value.partscore}}]</dd>
</dl>
</div>
</div>

<!-- on error -->
<div class="panel panel-danger" ng-if="!success">
<div class="panel-heading">
SpamAssassin score <b>Error</b>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>Error: </dt>
<dd>{{content.errorMessage}}</dd>
</dl>
</div>
</div>
3 changes: 3 additions & 0 deletions thehive-templates/SpamAssassin_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>