diff --git a/analyzers/Malwares/Malwares_GetReport.json b/analyzers/Malwares/Malwares_GetReport.json new file mode 100644 index 000000000..1acf8ff31 --- /dev/null +++ b/analyzers/Malwares/Malwares_GetReport.json @@ -0,0 +1,16 @@ +{ + "name": "Malwares_GetReport", + "version": "1.0", + "author": "CERT-LDO", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Get the latest Malwares report for a file, hash, domain or an IP address.", + "dataTypeList": ["file", "hash", "domain", "ip"], + "baseConfig": "Malwares", + "config": { + "check_tlp": true, + "max_tlp": 3, + "service": "get" + }, + "command": "Malwares/malwares.py" +} diff --git a/analyzers/Malwares/Malwares_Scan.json b/analyzers/Malwares/Malwares_Scan.json new file mode 100644 index 000000000..2bfa6e7f5 --- /dev/null +++ b/analyzers/Malwares/Malwares_Scan.json @@ -0,0 +1,16 @@ +{ + "name": "Malwares_Scan", + "version": "1.0", + "author": "CERT-LDO", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Use Malwares api to scan a file or URL.", + "dataTypeList": ["file", "url"], + "baseConfig": "Malwares", + "config": { + "check_tlp": true, + "service": "scan", + "max_tlp": 1 + }, + "command": "Malwares/malwares.py" +} diff --git a/analyzers/Malwares/malwares.py b/analyzers/Malwares/malwares.py new file mode 100644 index 000000000..a62ca7ee2 --- /dev/null +++ b/analyzers/Malwares/malwares.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +import sys +import os +import json +import codecs +import time +import hashlib + +from malwares_api import Api +from cortexutils.analyzer import Analyzer + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +class MalwaresAnalyzer(Analyzer): + + def __init__(self): + Analyzer.__init__(self) + self.service = self.getParam('config.service', None, 'Service parameter is missing') + self.key = self.getParam('config.key', None, 'Missing Malware API key') + self.polling_interval = self.getParam('config.polling_interval', 60) + + def wait_file_report(self, id): + results = self.check_response(self.m_api.get_file_report(id)) + code = results.get('result_code', None) + if code == 1: + self.report(results) + else: + time.sleep(self.polling_interval) + self.wait_file_report(id) + + def wait_url_report(self, id): + results = self.check_response(self.m_api.get_url_report(id)) + code = results.get('result_code', None) + if code == 1: + self.report(results) + else: + time.sleep(self.polling_interval) + self.wait_url_report(id) + + def check_response(self, response): + if type(response) is not dict: + self.error('Bad response : ' + str(response)) + status = response.get('response_code', -1) + if status in (-14, -15): + self.error('Malwares api rate limit exceeded.') + elif int(status) < 1: + self.error('Bad status : ' + str(status)) + results = response.get('results', {}) + return results + + # 0 => not found + # -2 => in queue + # 1 => ready + + def read_scan_response(self, response, func): + results = self.check_response(response) + code = results.get('result_code', None) + md5 = results.get('md5', None) + url = results.get('url', None) + if code in (1,2) and md5 is not None: + func(md5) + elif code in (1,2) and url is not None: + func(url) + else: + self.error('%d %s %s - Scan not found' % (code, md5, url)) + + def summary(self, raw): + taxonomies = [] + level = "info" + namespace = "Malwares" + predicate = "Score" + value = "\"No info\"" + score = -1 + + result = { + "has_result": True + } + + if "ai_score" in raw.keys(): + score = raw["ai_score"] + if score < 10: + level = "safe" + elif 10 <= score < 30: + level = "suspicious" + else: + level = "malicious" + + result['score'] = score + + value = "\"{}/100\"".format(score) + + else: + if "detected_communicating_file" in raw.keys() or "detected_url" in raw.keys() or "detected_downloaded_file" in raw.keys(): + score = max( + raw.get("detected_communicating_file", {}).get("total", 0), + raw.get("detected_url", {}).get("total", 0), + raw.get("detected_downloaded_file", {}).get("total", 0) + ) + value = "\"{} results\"".format(score) + + elif "virustotal" in raw.keys(): + score = raw.get("virustotal", {}).get("positives", 0) + total = raw.get("virustotal", {}).get("total", 0) + value = "\"{}/{} positives\"".format(score, total) + + if score == 0: + level = "safe" + elif 0 < score <= 5: + level = "suspicious" + elif score > 5: + level = "malicious" + + taxonomies.append(self.build_taxonomy( + level, namespace, predicate, value)) + return {"taxonomies": taxonomies} + + def run(self): + Analyzer.run(self) + self.m_api = Api(self.key) + + if self.service == 'scan': + if self.data_type == 'file': + filename = self.getParam('filename', 'noname.ext') + filepath = self.getParam('file', None, 'File is missing') + self.read_scan_response(self.m_api.scan_file( + open(filepath, 'rb'), filename), self.wait_file_report) + elif self.data_type == 'url': + data = self.getParam('data', None, 'Data is missing') + self.read_scan_response( + self.m_api.scan_url(data), self.wait_url_report) + else: + self.error('Invalid data type') + elif self.service == 'get': + if self.data_type == 'domain': + data = self.getParam('data', None, 'Data is missing') + self.report(self.check_response( + self.m_api.get_domain_report(data))) + elif self.data_type == 'ip': + data = self.getParam('data', None, 'Data is missing') + self.report(self.check_response(self.m_api.get_ip_report(data))) + elif self.data_type == 'file': + + hashes = self.getParam('attachment.hashes', + None) + if hashes is None: + filepath = self.getParam('file', None, 'File is missing') + hash = hashlib.sha256(open(filepath, 'r').read()).hexdigest(); + else: + # find SHA256 hash + hash = next(h for h in hashes if len(h) == 64) + + self.report(self.check_response(self.m_api.get_file_report(hash))) + + elif self.data_type == 'hash': + data = self.getParam('data', None, 'Data is missing') + self.report(self.check_response(self.m_api.get_file_report(data))) + else: + self.error('Invalid data type') + else: + self.error('Invalid service') + + +if __name__ == '__main__': + MalwaresAnalyzer().run() + + diff --git a/analyzers/Malwares/malwares_api.py b/analyzers/Malwares/malwares_api.py new file mode 100644 index 000000000..3e23b6bfb --- /dev/null +++ b/analyzers/Malwares/malwares_api.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +#import StringIO +import requests + +class Api(): + + def __init__(self, api_key=None): + self.api_key = api_key + self.base = 'https://www.malwares.com/api/v2/' + self.version = 2 + if api_key is None: + raise ApiError("You must supply a valid Malwares API key.") + + def scan_file(self, this_file, this_filename): + """ Submit a file to be scanned by Malwares + + :param this_file: File to be scanned (200MB file size limit) + :param this_filename: Filename for scanned file + :return: JSON response that contains scan_id and permalink. + """ + params = { + 'api_key': self.api_key, + 'filename': this_filename + } + try: + files = {'file': (this_file.name, open(this_file.name, 'rb'), 'application/octet-stream')} + except TypeError as e: + return dict(error=e.message) + + try: + response = requests.post(self.base + 'file/upload', files=files, data=params) + except requests.RequestException as e: + return dict(error=e.message) + + return _return_response_and_status_code(response) + + def get_file_report(self, this_hash): + """ Get the scan results for a file. + + :param this_hash: The md5/sha1/sha256/scan_ids hash of the file whose dynamic behavioural report you want to + retrieve or scan_ids from a previous call to scan_file. + :return: + """ + params = {'api_key': self.api_key, 'hash': this_hash} + + try: + response_info = requests.get(self.base + 'file/mwsinfo', params=params) + response_additional = requests.get(self.base + 'file/addinfo', params=params) + except requests.RequestException as e: + return dict(error=e.message) + + ri = _return_response_and_status_code(response_info) + ra = _return_response_and_status_code(response_additional) + + if ri['response_code'] == '1' and ra['response_code'] == '1': # both ok + response = dict(results={**ri['results'], **ra['results']}, response_code=1) + elif ri['response_code'] == '1' and ra['response_code'] == '0': # advance non exists but standard ok + response = ri + elif ri['response_code'] == '2': # main is still loading + response = dict(results={}, response_code=2) + else: # error generic + response = ri + return response + + def scan_url(self, this_url): + """ Submit a URL to be scanned by Malwares. + + :param this_url: The URL that should be scanned. + :return: JSON response that contains scan_id and permalink. + """ + params = {'api_key': self.api_key, 'url': this_url} + + try: + response = requests.post(self.base + 'url/request', data=params) + except requests.RequestException as e: + return dict(error=e.message) + + return _return_response_and_status_code(response) + + def get_url_report(self, this_url): + """ Get the scan results for a URL. + + :param this_url: a URL will retrieve the most recent report on the given URL. + :return: JSON response + """ + params = {'api_key': self.api_key, 'url': this_url} + + try: + response = requests.post(self.base + 'url/info', data=params) + except requests.RequestException as e: + return dict(error=e.message) + + return _return_response_and_status_code(response) + + def get_ip_report(self, this_ip): + """ Get IP address reports. + + :param this_ip: a valid IPv4 address in dotted quad notation, for the time being only IPv4 addresses are + supported. + :return: JSON response + """ + params = {'api_key': self.api_key, 'ip': this_ip} + + try: + response = requests.get(self.base + 'ip/info', params=params) + except requests.RequestException as e: + return dict(error=e.message) + + return _return_response_and_status_code(response) + + def get_domain_report(self, this_domain): + """ Get information about a given domain. + + :param this_domain: a domain name. + :return: JSON response + """ + params = {'api_key': self.api_key, 'hostname': this_domain} + + try: + response = requests.get(self.base + 'hostname/info', params=params) + except requests.RequestException as e: + return dict(error=e.message) + + return _return_response_and_status_code(response) + +class ApiError(Exception): + pass + + +def _return_response_and_status_code(response): + """ Output the requests response JSON and status code + + :rtype : dict + :param response: requests response object + :return: dict containing the JSON response and/or the status code with error string. + """ + + result_codes = { + "-11" : "No matching data to API Key API Key error", + "-12" : "No authority to use No authority to use", + "-13" : "Expired API Key API Key expired", + "-14" : "Over the daily request limit Request limit per daily exceeded", + "-15" : "Over the hourly request limit Request limit per hour exceeded", + "-1" : "Invalid Parameters / Invalid Request", + "-25" : "File Upload Quota Limit Error in file size to upload", + "-2" : "Invalid URL Error in URL type", + "-31" : "Invalid type of hash error in Hash type", + "-400" : "No file attached No file attached", + "-404" : "No result No result", + "-415" : "Ectype of upload form is not multipart/form-data Error in upload form type", + "-41" : "Invalid type of url Error in URL type", + "-500" : "Internal Server Error System error", + "-51" : "Invalid type of ip Error in IP type", + "-61" : "Invalid type of hostname Error in Hostname type", + "0" : "Data is not exist No information found in DB.", + "1" : "Data exists / Analysis request succeeded /Successful upload (new)", + "2" : "Analysis in progress / Successful upload (duplicated)", + "-999": "Error" + + } + + results = response.json() + + result_code = str(response.json().get('result_code', '-999')) + result_message = result_codes[result_code] + return dict(results=results, response_code=result_code, result_message=result_message) + diff --git a/analyzers/Malwares/requirements.txt b/analyzers/Malwares/requirements.txt new file mode 100644 index 000000000..6aabc3cfa --- /dev/null +++ b/analyzers/Malwares/requirements.txt @@ -0,0 +1,2 @@ +cortexutils +requests diff --git a/thehive-templates/Malwares_GetReport_1_0/long.html b/thehive-templates/Malwares_GetReport_1_0/long.html new file mode 100644 index 000000000..add60e4ed --- /dev/null +++ b/thehive-templates/Malwares_GetReport_1_0/long.html @@ -0,0 +1,336 @@ +
+
+ {{(artifact.data || artifact.attachment.name) | fang}} +
+
+ {{content.errorMessage}} +
+
+ +
+ +
+
+ Summary +
+
+
+
AI Score
+
+ + {{content.ai_score || 0}}/100 + + + {{content.ai_score || 0}}/100 + + + {{content.ai_score || 0}}/100 + +
+
+
+
VT Score
+
+ + {{content.virustotal.positives}}/{{content.virustotal.total}} + + + {{content.virustotal.positives}}/{{content.virustotal.total}} + + + {{content.virustotal.positives}}/{{content.virustotal.total}} + +
+
VT Scan Date
+
{{content.virustotal.scan_date}}
+
+
+
MD5
+
{{content.md5}}
+
+
+
SHA-1
+
{{content.sha1}}
+
+
+
SHA-256
+
{{content.sha256}}
+
+
+
SSDEEP
+
{{content.ssdeep}}
+
+
+
Imphash
+
{{content.imphash}}
+
+
+
File Size
+
{{content.filesize}} bytes
+
+
+
File Name
+
{{content.filename.join(', ')}}
+
+
+
File Type
+
{{content.filetype}}
+
+
+
Known Date
+
{{content.first_seen}}
+
+
+
Categories
+
{{content.taglist.join(', ')}}
+
+
+
Resolutions
+
+
+ This domain has been seen to resolve to the following IP addresses. +
+
+ The following domains resolved to the given IP address. +
+
+ {{::resolution.last_resolved | amDateFormat:'DD-MM-YYYY'}}: + {{(resolution.ip_address | fang) || (resolution.hostname | fang)}} +
+
+
+
+
Malwares
+
+ + + + View Full Report + + + + + View Full Report + + + + + View Full Report + +
+
+
+
+ +
+
+ Malicious URL history of this IP + + Show All ({{::content.detected_url.list.length}}) + Show less + +
+
+ + + + + + + + + + + +
ScoreScan DateURL
+ {{::url.positives}}/{{::url.total}} + {{url.date}}{{url.url | fang}}
+
+
+ +
+
+ Hostname usage history of this IP + + Show All ({{::content.hostname_history.list.length}}) + Show less + +
+
+ + + + + + + + + +
HostnameLast Check Date
{{hostname.hostname}}{{hostname.date}}
+
+
+ +
+
+ Malicious sample history downloaded from this IP + + Show All ({{::content.detected_downloaded_file.list.length}}) + Show less + +
+
+ + + + + + + + + + + +
SHA-256VT ScoreKnown Date
{{malicious_sample_downloaded.sha256}}{{::malicious_sample_donwloaded.positives}}/{{::malicious_sample_downloaded.total}}{{malicious_sample_downloaded.date}}
+
+
+ +
+
+ Normal sample history downloaded from this IP + + Show All ({{::content.undetected_downloaded_file.list.length}}) + Show less + +
+
+ + + + + + + + + +
SHA-256Known Date
{{normal_sample_downloaded.sha256}}{{normal_sample_downloaded.date}}
+
+
+ +
+
+ Malicious sample history communicated with this IP + + Show All ({{::content.detected_communicating_file.list.length}}) + Show less + +
+
+ + + + + + + + + + + +
SHA-256VT ScoreKnown Date
{{malicious_sample_communicated.sha256}}{{::malicious_sample_communicated.positives}}/{{::malicious_sample_communicated.total}}{{malicious_sample_communicated.date}}
+
+
+ +
+
+ Normal sample history communicated with this IP + + Show All ({{::content.undetected_communicating_file.list.length}}) + Show less + +
+
+ + + + + + + + + +
SHA-256Known Date
{{normal_sample_communicated.sha256}}{{normal_sample_communicated.date}}
+
+
+ + +
+
+ URL Analysis Engine Detection Details + + Show All ({{::content.virustotal.scans.length}}) + Show less + +
+
+ + + + + + + + + +
URL Analysis Engine NameDetection State
{{vt_n}} + + {{vt_d.result}} + + + {{vt_d.result}} + + + + + + + +
+
+
+ +
+
+ Scans +
+
+ + + + + + + + + + + + + + + + + + +
ScannerDetectedResultDetailsUpdateVersion
+ {{scanner}} + + + {{result.result}} + + + View details + {{result.update}}{{result.version}}
+
+
+ +
\ No newline at end of file diff --git a/thehive-templates/Malwares_GetReport_1_0/short.html b/thehive-templates/Malwares_GetReport_1_0/short.html new file mode 100644 index 000000000..57f9d29cf --- /dev/null +++ b/thehive-templates/Malwares_GetReport_1_0/short.html @@ -0,0 +1,3 @@ + + {{t.namespace}}:{{t.predicate}}={{t.value}} + diff --git a/thehive-templates/Malwares_Scan_1_0/long.html b/thehive-templates/Malwares_Scan_1_0/long.html new file mode 100644 index 000000000..add60e4ed --- /dev/null +++ b/thehive-templates/Malwares_Scan_1_0/long.html @@ -0,0 +1,336 @@ +
+
+ {{(artifact.data || artifact.attachment.name) | fang}} +
+
+ {{content.errorMessage}} +
+
+ +
+ +
+
+ Summary +
+
+
+
AI Score
+
+ + {{content.ai_score || 0}}/100 + + + {{content.ai_score || 0}}/100 + + + {{content.ai_score || 0}}/100 + +
+
+
+
VT Score
+
+ + {{content.virustotal.positives}}/{{content.virustotal.total}} + + + {{content.virustotal.positives}}/{{content.virustotal.total}} + + + {{content.virustotal.positives}}/{{content.virustotal.total}} + +
+
VT Scan Date
+
{{content.virustotal.scan_date}}
+
+
+
MD5
+
{{content.md5}}
+
+
+
SHA-1
+
{{content.sha1}}
+
+
+
SHA-256
+
{{content.sha256}}
+
+
+
SSDEEP
+
{{content.ssdeep}}
+
+
+
Imphash
+
{{content.imphash}}
+
+
+
File Size
+
{{content.filesize}} bytes
+
+
+
File Name
+
{{content.filename.join(', ')}}
+
+
+
File Type
+
{{content.filetype}}
+
+
+
Known Date
+
{{content.first_seen}}
+
+
+
Categories
+
{{content.taglist.join(', ')}}
+
+
+
Resolutions
+
+
+ This domain has been seen to resolve to the following IP addresses. +
+
+ The following domains resolved to the given IP address. +
+
+ {{::resolution.last_resolved | amDateFormat:'DD-MM-YYYY'}}: + {{(resolution.ip_address | fang) || (resolution.hostname | fang)}} +
+
+
+
+
Malwares
+
+ + + + View Full Report + + + + + View Full Report + + + + + View Full Report + +
+
+
+
+ +
+
+ Malicious URL history of this IP + + Show All ({{::content.detected_url.list.length}}) + Show less + +
+
+ + + + + + + + + + + +
ScoreScan DateURL
+ {{::url.positives}}/{{::url.total}} + {{url.date}}{{url.url | fang}}
+
+
+ +
+
+ Hostname usage history of this IP + + Show All ({{::content.hostname_history.list.length}}) + Show less + +
+
+ + + + + + + + + +
HostnameLast Check Date
{{hostname.hostname}}{{hostname.date}}
+
+
+ +
+
+ Malicious sample history downloaded from this IP + + Show All ({{::content.detected_downloaded_file.list.length}}) + Show less + +
+
+ + + + + + + + + + + +
SHA-256VT ScoreKnown Date
{{malicious_sample_downloaded.sha256}}{{::malicious_sample_donwloaded.positives}}/{{::malicious_sample_downloaded.total}}{{malicious_sample_downloaded.date}}
+
+
+ +
+
+ Normal sample history downloaded from this IP + + Show All ({{::content.undetected_downloaded_file.list.length}}) + Show less + +
+
+ + + + + + + + + +
SHA-256Known Date
{{normal_sample_downloaded.sha256}}{{normal_sample_downloaded.date}}
+
+
+ +
+
+ Malicious sample history communicated with this IP + + Show All ({{::content.detected_communicating_file.list.length}}) + Show less + +
+
+ + + + + + + + + + + +
SHA-256VT ScoreKnown Date
{{malicious_sample_communicated.sha256}}{{::malicious_sample_communicated.positives}}/{{::malicious_sample_communicated.total}}{{malicious_sample_communicated.date}}
+
+
+ +
+
+ Normal sample history communicated with this IP + + Show All ({{::content.undetected_communicating_file.list.length}}) + Show less + +
+
+ + + + + + + + + +
SHA-256Known Date
{{normal_sample_communicated.sha256}}{{normal_sample_communicated.date}}
+
+
+ + +
+
+ URL Analysis Engine Detection Details + + Show All ({{::content.virustotal.scans.length}}) + Show less + +
+
+ + + + + + + + + +
URL Analysis Engine NameDetection State
{{vt_n}} + + {{vt_d.result}} + + + {{vt_d.result}} + + + + + + + +
+
+
+ +
+
+ Scans +
+
+ + + + + + + + + + + + + + + + + + +
ScannerDetectedResultDetailsUpdateVersion
+ {{scanner}} + + + {{result.result}} + + + View details + {{result.update}}{{result.version}}
+
+
+ +
\ No newline at end of file diff --git a/thehive-templates/Malwares_Scan_1_0/short.html b/thehive-templates/Malwares_Scan_1_0/short.html new file mode 100644 index 000000000..57f9d29cf --- /dev/null +++ b/thehive-templates/Malwares_Scan_1_0/short.html @@ -0,0 +1,3 @@ + + {{t.namespace}}:{{t.predicate}}={{t.value}} +