Merge branch 'develop' into feature/opencti-v4
4
.gitignore
vendored
@ -16,3 +16,7 @@ lib64
|
||||
pyvenv.cfg
|
||||
share
|
||||
|
||||
analyzers/*/input
|
||||
analyzers/*/output
|
||||
responders/*/input
|
||||
responders/*/output
|
@ -1,11 +1,31 @@
|
||||
{
|
||||
"name": "Abuse_Finder",
|
||||
"version": "3.0",
|
||||
"author": "CERT-BDF",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Find abuse contacts associated with domain names, URLs, IPs and email addresses.",
|
||||
"dataTypeList": ["ip", "domain", "fqdn", "url", "mail"],
|
||||
"baseConfig": "Abuse_Finder",
|
||||
"command": "Abuse_Finder/abusefinder.py"
|
||||
}
|
||||
"name": "Abuse_Finder",
|
||||
"version": "3.0",
|
||||
"author": "CERT-BDF",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Find abuse contacts associated with domain names, URLs, IPs and email addresses.",
|
||||
"dataTypeList": [
|
||||
"ip",
|
||||
"domain",
|
||||
"fqdn",
|
||||
"url",
|
||||
"mail"
|
||||
],
|
||||
"baseConfig": "Abuse_Finder",
|
||||
"command": "Abuse_Finder/abusefinder.py",
|
||||
"registration_required": false,
|
||||
"subscription_required": false,
|
||||
"free_subscription": false,
|
||||
"service_homepage": "https://github.com/certsocietegenerale/abuse_finder",
|
||||
"service_logo": {
|
||||
"path": "",
|
||||
"caption": ""
|
||||
},
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "assets/abuse_finder_longreport.png",
|
||||
"caption": "Abuse_Finder: Long report template"
|
||||
}
|
||||
]
|
||||
}
|
9
analyzers/Abuse_Finder/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
### Abuse_Finder
|
||||
Use CERT-SG's [Abuse Finder](https://github.com/certsocietegenerale/abuse_finder)
|
||||
to find abuse contacts associated with domain names, URLs, IPs and email addresses.
|
||||
|
||||
The analyzer comes in only one flavor.
|
||||
|
||||
No configuration is required. It can be used out of the box.
|
||||
|
||||
This Analyzer can only be run as a docker container or as process with Python <= 3.6.
|
BIN
analyzers/Abuse_Finder/assets/abuse_finder_longreport.png
Normal file
After Width: | Height: | Size: 12 KiB |
@ -16,6 +16,14 @@
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "privacy_type",
|
||||
"description": "Define the privacy setting (Allowed values: public, bylink, owner)",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "bylink"
|
||||
},
|
||||
{
|
||||
"name": "verify_ssl",
|
||||
"description": "Verify SSL certificate",
|
||||
|
@ -12,6 +12,7 @@ class AnyRunAnalyzer(Analyzer):
|
||||
Analyzer.__init__(self)
|
||||
self.url = "https://api.any.run/v1"
|
||||
self.token = self.get_param("config.token", None, "Service token is missing")
|
||||
self.privacy_type = self.get_param("config.privacy_type", None, "Privacy type is missing")
|
||||
self.verify_ssl = self.get_param("config.verify_ssl", True, None)
|
||||
if not self.verify_ssl:
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
@ -49,9 +50,11 @@ class AnyRunAnalyzer(Analyzer):
|
||||
while status_code in (None, 429) and tries <= 15:
|
||||
with open(filepath, "rb") as sample:
|
||||
files = {"file": (filename, sample)}
|
||||
data = {"opt_privacy_type": self.privacy_type}
|
||||
response = requests.post(
|
||||
"{0}/analysis".format(self.url),
|
||||
files=files,
|
||||
data=data,
|
||||
headers=headers,
|
||||
verify=self.verify_ssl,
|
||||
)
|
||||
@ -68,7 +71,7 @@ class AnyRunAnalyzer(Analyzer):
|
||||
self.error(response.json()["message"])
|
||||
elif self.data_type == "url":
|
||||
url = self.get_param("data", None, "Url is missing")
|
||||
data = {"obj_type": "url", "obj_url": url}
|
||||
data = {"obj_type": "url", "obj_url": url, "opt_privacy_type": self.privacy_type}
|
||||
while status_code in (None, 429) and tries <= 15:
|
||||
response = requests.post(
|
||||
"{0}/analysis".format(self.url),
|
||||
@ -127,4 +130,4 @@ class AnyRunAnalyzer(Analyzer):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
AnyRunAnalyzer().run()
|
||||
AnyRunAnalyzer().run()
|
@ -1,27 +1,49 @@
|
||||
{
|
||||
"name": "CIRCLPassiveDNS",
|
||||
"author": "Nils Kuhnert, CERT-Bund",
|
||||
"license": "AGPL-V3",
|
||||
"url": "https://github.com/BSI-CERT-Bund/cortex-analyzers",
|
||||
"version": "2.0",
|
||||
"description": "Check CIRCL's Passive DNS for a given domain or URL.",
|
||||
"dataTypeList": ["domain", "url", "ip"],
|
||||
"baseConfig": "CIRCL",
|
||||
"command": "CIRCLPassiveDNS/circl_passivedns.py",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Username",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
"name": "CIRCLPassiveDNS",
|
||||
"author": "Nils Kuhnert, CERT-Bund",
|
||||
"license": "AGPL-V3",
|
||||
"url": "https://github.com/BSI-CERT-Bund/cortex-analyzers",
|
||||
"version": "2.0",
|
||||
"description": "Check CIRCL's Passive DNS for a given domain or URL.",
|
||||
"dataTypeList": [
|
||||
"domain",
|
||||
"url",
|
||||
"ip"
|
||||
],
|
||||
"baseConfig": "CIRCL",
|
||||
"command": "CIRCLPassiveDNS/circl_passivedns.py",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Username",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "Password",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"registration_required": true,
|
||||
"subscription_required": true,
|
||||
"free_subscription": true,
|
||||
"service_homepage": "https://www.circl.lu/services/passive-dns/",
|
||||
"service_logo": {
|
||||
"path": "assets/passivedns.png",
|
||||
"caption": "logo"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "Password",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "assets/sc-short-circlpassivedns.png",
|
||||
"caption": "CIRCLPassiveDNS: short report"
|
||||
},
|
||||
{
|
||||
"path": "assets/sc-long-circlpassivedns.png",
|
||||
"caption": "CIRCLPassiveDNS: long report"
|
||||
}
|
||||
]
|
||||
}
|
19
analyzers/CIRCLPassiveDNS/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
### CIRCLPassiveDNS
|
||||
|
||||
Check [CIRCL's Passive DNS](https://www.circl.lu/services/passive-dns/) for a
|
||||
given domain.
|
||||
|
||||
This analyzer comes in only one flavor.
|
||||
|
||||
#### Requirements
|
||||
|
||||
Access to CIRCL Passive DNS is only allowed to trusted partners in Luxembourg
|
||||
and abroad. [Contact CIRCL](https://www.circl.lu/contact/) if you would like
|
||||
access. Include your affiliation and the foreseen use of the Passive DNS
|
||||
data.
|
||||
|
||||
If the CIRCL positively answers your access request, you'll obtain a username
|
||||
and password which are needed to make the analyzer work.
|
||||
|
||||
supply your username as the value for the `user` parameter and your password
|
||||
as the value for the `password` parameter.
|
BIN
analyzers/CIRCLPassiveDNS/assets/passivedns.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
analyzers/CIRCLPassiveDNS/assets/sc-long-circlpassivedns.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
analyzers/CIRCLPassiveDNS/assets/sc-short-circlpassivedns.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
@ -1,27 +1,49 @@
|
||||
{
|
||||
"name": "CIRCLPassiveSSL",
|
||||
"author": "Nils Kuhnert, CERT-Bund",
|
||||
"license": "AGPL-V3",
|
||||
"url": "https://github.com/BSI-CERT-Bund/cortex-analyzers",
|
||||
"version": "2.0",
|
||||
"description": "Check CIRCL's Passive SSL for a given IP address or a X509 certificate hash.",
|
||||
"dataTypeList": ["ip", "certificate_hash", "hash"],
|
||||
"baseConfig": "CIRCL",
|
||||
"command": "CIRCLPassiveSSL/circl_passivessl.py",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Username",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
"name": "CIRCLPassiveSSL",
|
||||
"author": "Nils Kuhnert, CERT-Bund",
|
||||
"license": "AGPL-V3",
|
||||
"url": "https://github.com/BSI-CERT-Bund/cortex-analyzers",
|
||||
"version": "2.0",
|
||||
"description": "Check CIRCL's Passive SSL for a given IP address or a X509 certificate hash.",
|
||||
"dataTypeList": [
|
||||
"ip",
|
||||
"certificate_hash",
|
||||
"hash"
|
||||
],
|
||||
"baseConfig": "CIRCL",
|
||||
"command": "CIRCLPassiveSSL/circl_passivessl.py",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Username",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "Password",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"registration_required": true,
|
||||
"subscription_required": true,
|
||||
"free_subscription": true,
|
||||
"service_homepage": "https://www.circl.lu/services/passive-ssl/",
|
||||
"service_logo": {
|
||||
"path": "assets/pssl.png",
|
||||
"caption": "PSSL logo"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "Password",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "assets/sc-short-circlpassivessl.png",
|
||||
"caption": "CIRCLPassiveSSL: short report"
|
||||
},
|
||||
{
|
||||
"path": "assets/sc-long-circlpassivessl.png",
|
||||
"caption": "CIRCLPassiveSSL: long report"
|
||||
}
|
||||
]
|
||||
}
|
18
analyzers/CIRCLPassiveSSL/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
### CIRCLPassiveSSL
|
||||
|
||||
Check [CIRCL's Passive SSL](https://www.circl.lu/services/passive-ssl/)
|
||||
service for a given IP address or certificate hash.
|
||||
|
||||
This analyzer comes in only one flavor.
|
||||
|
||||
#### Requirements
|
||||
|
||||
Access to CIRCL Passive SSL is allowed to partners including security
|
||||
researchers or incident analysts worldwide. [Contact CIRCL](https://www.circl.lu/contact/)
|
||||
if you would like access.
|
||||
|
||||
If the CIRCL positively answers your access request, you'll obtain a username
|
||||
and password which are needed to make the analyzer work.
|
||||
|
||||
Supply your username as the value for the `user` parameter and your password
|
||||
as the value for the `password` parameter.
|
BIN
analyzers/CIRCLPassiveSSL/assets/pssl.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
analyzers/CIRCLPassiveSSL/assets/sc-long-circlpassivessl.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
analyzers/CIRCLPassiveSSL/assets/sc-short-circlpassivessl.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
@ -34,12 +34,15 @@ class CIRCLPassiveSSLAnalyzer(Analyzer):
|
||||
certificates = []
|
||||
else:
|
||||
certificates = list(result.get(ip).get('certificates'))
|
||||
subjects = result.get(ip).get('subjects', dict({}))
|
||||
|
||||
newresult = {'ip': ip,
|
||||
'certificates': []}
|
||||
for cert in certificates:
|
||||
if cert not in subjects:
|
||||
continue
|
||||
newresult['certificates'].append({'fingerprint': cert,
|
||||
'subject': result.get(ip).get('subjects').get(cert).get('values')[0]})
|
||||
'subject': subjects.get(cert).get('values')[0]})
|
||||
return newresult
|
||||
|
||||
def query_certificate(self, cert_hash):
|
||||
@ -86,6 +89,34 @@ class CIRCLPassiveSSLAnalyzer(Analyzer):
|
||||
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
|
||||
def artifacts(self, raw):
|
||||
artifacts = []
|
||||
if 'certificates' in raw:
|
||||
for c in raw.get('certificates'):
|
||||
tags = []
|
||||
tags += ["Certificate:{}".format(a) for a in c.get('subject').split(', ') if a.startswith('CN=')]
|
||||
tags += ["Certificate:{}".format(a) for a in c.get('subject').split(', ') if a.startswith('O=')]
|
||||
artifacts.append(
|
||||
self.build_artifact(
|
||||
'hash',
|
||||
str(c.get('fingerprint')),
|
||||
message=str(c.get('subject')),
|
||||
tags=tags
|
||||
)
|
||||
)
|
||||
|
||||
if 'query' in raw:
|
||||
for ip in raw.get('query').get('seen'):
|
||||
artifacts.append(
|
||||
self.build_artifact(
|
||||
'ip',
|
||||
str(ip)
|
||||
)
|
||||
)
|
||||
return artifacts
|
||||
|
||||
|
||||
def run(self):
|
||||
if self.data_type == 'certificate_hash' or self.data_type == 'hash':
|
||||
data = self.get_data()
|
||||
|
@ -3,7 +3,11 @@ from cortexutils.analyzer import Analyzer
|
||||
from censys.certificates import CensysCertificates
|
||||
from censys.ipv4 import CensysIPv4
|
||||
from censys.websites import CensysWebsites
|
||||
from censys.base import CensysNotFoundException, CensysRateLimitExceededException, CensysUnauthorizedException
|
||||
from censys.exceptions import (
|
||||
CensysNotFoundException,
|
||||
CensysRateLimitExceededException,
|
||||
CensysUnauthorizedException,
|
||||
)
|
||||
|
||||
|
||||
class CensysAnalyzer(Analyzer):
|
||||
@ -11,14 +15,14 @@ class CensysAnalyzer(Analyzer):
|
||||
Analyzer.__init__(self)
|
||||
|
||||
self.__uid = self.get_param(
|
||||
'config.uid',
|
||||
"config.uid",
|
||||
None,
|
||||
'No UID for Censys given. Please add it to the cortex configuration.'
|
||||
"No UID for Censys given. Please add it to the cortex configuration.",
|
||||
)
|
||||
self.__api_key = self.get_param(
|
||||
'config.key',
|
||||
"config.key",
|
||||
None,
|
||||
'No API-Key for Censys given. Please add it to the cortex configuration.'
|
||||
"No API-Key for Censys given. Please add it to the cortex configuration.",
|
||||
)
|
||||
|
||||
def search_hosts(self, ip):
|
||||
@ -55,66 +59,88 @@ class CensysAnalyzer(Analyzer):
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self.data_type == 'ip':
|
||||
self.report({
|
||||
'ip': self.search_hosts(self.get_data())
|
||||
})
|
||||
elif self.data_type == 'hash':
|
||||
self.report({
|
||||
'cert': self.search_certificate(self.get_data())
|
||||
})
|
||||
elif self.data_type == 'domain' or self.data_type == 'fqdn':
|
||||
self.report({
|
||||
'website': self.search_website(self.get_data())
|
||||
})
|
||||
if self.data_type == "ip":
|
||||
self.report({"ip": self.search_hosts(self.get_data())})
|
||||
elif self.data_type == "hash":
|
||||
self.report({"cert": self.search_certificate(self.get_data())})
|
||||
elif self.data_type == "domain" or self.data_type == "fqdn":
|
||||
self.report({"website": self.search_website(self.get_data())})
|
||||
else:
|
||||
self.error('Data type not supported. Please use this analyzer with data types hash, ip or domain.')
|
||||
self.error(
|
||||
"Data type not supported. Please use this analyzer with data types hash, ip or domain."
|
||||
)
|
||||
except CensysNotFoundException:
|
||||
self.report({
|
||||
'message': '{} could not be found.'.format(self.get_data())
|
||||
})
|
||||
self.report({"message": "{} could not be found.".format(self.get_data())})
|
||||
except CensysUnauthorizedException:
|
||||
self.error('Censys raised NotAuthorizedException. Please check your credentials.')
|
||||
self.error(
|
||||
"Censys raised NotAuthorizedException. Please check your credentials."
|
||||
)
|
||||
except CensysRateLimitExceededException:
|
||||
self.error('Rate limit exceeded.')
|
||||
self.error("Rate limit exceeded.")
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
if 'ip' in raw:
|
||||
raw = raw['ip']
|
||||
service_count = len(raw.get('protocols', []))
|
||||
heartbleed = raw.get('443', {}).get('https', {}).get('heartbleed', {}).get('heartbleed_vulnerable', False)
|
||||
if "ip" in raw:
|
||||
raw = raw["ip"]
|
||||
service_count = len(raw.get("protocols", []))
|
||||
heartbleed = (
|
||||
raw.get("443", {})
|
||||
.get("https", {})
|
||||
.get("heartbleed", {})
|
||||
.get("heartbleed_vulnerable", False)
|
||||
)
|
||||
|
||||
taxonomies.append(self.build_taxonomy('info', 'Censys', 'OpenServices', service_count))
|
||||
taxonomies.append(
|
||||
self.build_taxonomy("info", "Censys", "OpenServices", service_count)
|
||||
)
|
||||
if heartbleed:
|
||||
taxonomies.append(self.build_taxonomy('malicious', 'Censys', 'Heartbleed', 'vulnerable'))
|
||||
elif 'website' in raw:
|
||||
raw = raw['website']
|
||||
service_count = len(raw.get('tags', []))
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(
|
||||
"malicious", "Censys", "Heartbleed", "vulnerable"
|
||||
)
|
||||
)
|
||||
elif "website" in raw:
|
||||
raw = raw["website"]
|
||||
service_count = len(raw.get("tags", []))
|
||||
|
||||
taxonomies.append(self.build_taxonomy('info', 'Censys', 'OpenServices', service_count))
|
||||
elif 'cert' in raw:
|
||||
raw = raw['cert']
|
||||
trusted_count = len(raw.get('validation', []))
|
||||
validator_count = len(raw.get('validation', []))
|
||||
taxonomies.append(
|
||||
self.build_taxonomy("info", "Censys", "OpenServices", service_count)
|
||||
)
|
||||
elif "cert" in raw:
|
||||
raw = raw["cert"]
|
||||
trusted_count = len(raw.get("validation", []))
|
||||
validator_count = len(raw.get("validation", []))
|
||||
|
||||
for _, validator in raw.get('validation', []).items():
|
||||
if validator.get('blacklisted', False) or \
|
||||
validator.get('in_revocation_set', False) or \
|
||||
(not validator.get('whitelisted', False) and not validator.get('valid', False)):
|
||||
for _, validator in raw.get("validation", []).items():
|
||||
if (
|
||||
validator.get("blacklisted", False)
|
||||
or validator.get("in_revocation_set", False)
|
||||
or (
|
||||
not validator.get("whitelisted", False)
|
||||
and not validator.get("valid", False)
|
||||
)
|
||||
):
|
||||
trusted_count -= 1
|
||||
if trusted_count < validator_count:
|
||||
taxonomies.append(self.build_taxonomy('suspicious', 'Censys', 'TrustedCount', '{}/{}'.format(
|
||||
trusted_count, validator_count
|
||||
)))
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(
|
||||
"suspicious",
|
||||
"Censys",
|
||||
"TrustedCount",
|
||||
"{}/{}".format(trusted_count, validator_count),
|
||||
)
|
||||
)
|
||||
else:
|
||||
taxonomies.append(self.build_taxonomy('info', 'Censys', 'TrustedCount', '{}/{}'.format(
|
||||
trusted_count, validator_count
|
||||
)))
|
||||
return {
|
||||
'taxonomies': taxonomies
|
||||
}
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(
|
||||
"info",
|
||||
"Censys",
|
||||
"TrustedCount",
|
||||
"{}/{}".format(trusted_count, validator_count),
|
||||
)
|
||||
)
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
CensysAnalyzer().run()
|
||||
|
@ -18,10 +18,20 @@ class DShieldAnalyzer(Analyzer):
|
||||
def artifacts(self, raw):
|
||||
artifacts = []
|
||||
if 'as' in raw:
|
||||
artifacts.append({'type':'autonomous-system','value':str(raw['as'])})
|
||||
|
||||
artifacts.append(
|
||||
self.build_artifact(
|
||||
'autonomous-system',
|
||||
str(raw['as']
|
||||
)
|
||||
)
|
||||
)
|
||||
if 'asabusecontact' in raw:
|
||||
artifacts.append({'type': 'mail', 'value':str(raw['asabusecontact'])})
|
||||
artifacts.append(
|
||||
self.build_artifact(
|
||||
'mail',
|
||||
str(raw['asabusecontact'])
|
||||
)
|
||||
)
|
||||
return artifacts
|
||||
|
||||
def summary(self, raw):
|
||||
|
@ -50,7 +50,7 @@
|
||||
},
|
||||
{
|
||||
"path": "assets/DomainToolsIris_Investigate_short.png",
|
||||
"caption:": "DomainToolsIris_Investigate mini report sample"
|
||||
"caption": "DomainToolsIris_Investigate mini report sample"
|
||||
}
|
||||
]
|
||||
}
|
@ -42,7 +42,7 @@
|
||||
},
|
||||
{
|
||||
"path": "assets/DomainToolsIris_Pivot_short.png",
|
||||
"caption:": "DomainToolsIris_Pivot mini report sample"
|
||||
"caption": "DomainToolsIris_Pivot mini report sample"
|
||||
}
|
||||
]
|
||||
}
|
95
analyzers/Elasticsearch/ElasticSearch.json
Normal file
@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "Elasticsearch_Analysis",
|
||||
"author": "Nick Prokop",
|
||||
"license": "MIT",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"version": "1.0",
|
||||
"description": "Search for IoCs in Elasticsearch",
|
||||
"dataTypeList": ["url", "domain", "ip", "hash", "filename", "fqdn"],
|
||||
"command": "Elasticsearch/elk.py",
|
||||
"baseConfig": "Elasticsearch",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "endpoints",
|
||||
"description": "Define the Elasticsearch endpoints",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": true,
|
||||
"defaultValue": ["http://127.0.0.1:9200"]
|
||||
},
|
||||
{
|
||||
"name": "keys",
|
||||
"description": "Set the Elasticsearch api keys for each endpoint. Note: Use api key or basic auth, but not both.",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "users",
|
||||
"description": "Set the Elasticsearch users for each endpoint. Note: Use api key or basic auth, but not both.",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "passwords",
|
||||
"description": "Set the Elasticsearch passwords for each endpoint. Note: Use api key or basic auth, but not both.",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "kibana",
|
||||
"description": "Define the kibana address",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "dashboard",
|
||||
"description": "Set the kibana dashboard id that will be linked in the report",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "index",
|
||||
"description": "Define the Elasticsearch indices to use",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": true,
|
||||
"defaultValue": ["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"]
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"description": "Define the fields to query",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": true,
|
||||
"defaultValue": ["destination.ip","dll.hash.md5","dll.hash.sha256","dns.question.name","dns.resolved_ip","file.hash.md5","file.hash.sha256","file.name","hash.md5","hash.sha256","process.args","process.hash.md5","process.hash.sha256","process.parent.hash.md5","process.parent.hash.sha256","source.ip","url.domain","url.full"]
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"description": "Define the number of hits per index to return",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "10"
|
||||
},
|
||||
{
|
||||
"name": "verifyssl",
|
||||
"description": "Verify SSL certificate",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "cert_path",
|
||||
"description": "Path to the CA on the system used to check server certificate",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
342
analyzers/Elasticsearch/elk.py
Normal file
@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from elasticsearch import Elasticsearch
|
||||
from cortexutils.analyzer import Analyzer
|
||||
import dateutil.parser
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
# utils
|
||||
import operator
|
||||
|
||||
class Hit:
|
||||
def __init__(self,hitindex,hitid,process_parent_name,process_name,process_args,user_name,host_name,timestamp,time,\
|
||||
destination_ip,destination_port,source_ip,source_port,source_user_name,url_domain,url_path,url_full,\
|
||||
rule_category,dns_question_name,dns_resolvedip):
|
||||
self.hitindex = hitindex
|
||||
self.hitid = hitid
|
||||
self.process_parent_name = process_parent_name
|
||||
self.process_name = process_name
|
||||
self.process_args = process_args
|
||||
self.host_name = host_name
|
||||
self.user_name = user_name
|
||||
self.timestamp = timestamp
|
||||
self.time = time
|
||||
self.url_domain = url_domain
|
||||
self.url_path = url_path
|
||||
self.url_full = url_full
|
||||
self.source_ip = source_ip
|
||||
self.source_port = source_port
|
||||
self.source_user_name = source_user_name
|
||||
self.destination_ip = destination_ip
|
||||
self.destination_port = destination_port
|
||||
self.rule_category = rule_category
|
||||
self.dns_question_name = dns_question_name
|
||||
self.dns_resolvedip = dns_resolvedip
|
||||
|
||||
class ElasticsearchAnalyzer(Analyzer):
|
||||
# Analyzer's constructor
|
||||
def __init__(self):
|
||||
# Call the constructor of the super class
|
||||
Analyzer.__init__(self)
|
||||
|
||||
|
||||
self.endpoints = self.get_param('config.endpoints', None, 'Elasticsearch endpoint is missing')
|
||||
self.kibana = self.get_param('config.kibana', None, None)
|
||||
self.index = self.get_param('config.index', None, 'Elasticsearch index is missing')
|
||||
self.keys = self.get_param('config.keys', None, None)
|
||||
self.users = self.get_param('config.users', None, None)
|
||||
self.passwords = self.get_param('config.passwords', None, None)
|
||||
self.dashboard = self.get_param('config.dashboard', None, None)
|
||||
self.verify = self.get_param('config.verifyssl', True, None)
|
||||
self.cert = self.get_param('config.cert_path', None, None)
|
||||
self.fields = self.get_param('config.field', None, 'Field is missing')
|
||||
self.data = self.get_param('data', None, 'Data is missing')
|
||||
self.size = self.get_param('config.size', None, 'size is missing')
|
||||
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
namespace = "ELK"
|
||||
predicate = "Hit(s)"
|
||||
|
||||
value = "{}".format(raw['info']['hitcount'])
|
||||
if raw['info']['hitcount'] > 0:
|
||||
level = "suspicious"
|
||||
else:
|
||||
level = "safe"
|
||||
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
def artifacts(self, raw):
|
||||
artifacts = []
|
||||
domains = []
|
||||
urls = []
|
||||
ips = []
|
||||
|
||||
for hit in raw['hits']:
|
||||
#domains
|
||||
if 'url_domain' in hit:
|
||||
if isinstance(hit['url_domain'],list):
|
||||
for domain in hit['url_domain']:
|
||||
domains.append(domain)
|
||||
else:
|
||||
domains.append(hit['url_domain'])
|
||||
if 'dns_question_name' in hit:
|
||||
if isinstance(hit['dns_question_name'],list):
|
||||
for domain in hit['dns_question_name']:
|
||||
domains.append(domain)
|
||||
else:
|
||||
domains.append(hit['dns_question_name'])
|
||||
#urls
|
||||
if 'url_full' in hit:
|
||||
if isinstance(hit['url_full'],list):
|
||||
for url in hit['url_full']:
|
||||
urls.append(url)
|
||||
|
||||
#ips
|
||||
if 'source_ip' in hit:
|
||||
if isinstance(hit['source_ip'],list):
|
||||
for ip in hit['source_ip']:
|
||||
ips.append(ip)
|
||||
else:
|
||||
ips.append(hit['source_ip'])
|
||||
if 'destination_ip' in hit:
|
||||
if isinstance(hit['destination_ip'],list):
|
||||
for ip in hit['destination_ip']:
|
||||
ips.append(ip)
|
||||
else:
|
||||
ips.append(hit['destination_ip'])
|
||||
if 'dns_resolvedip' in hit:
|
||||
if isinstance(hit['dns_resolvedip'],list):
|
||||
for ip in hit['dns_resolvedip']:
|
||||
ips.append(ip)
|
||||
else:
|
||||
ips.append(hit['dns_resolvedip'])
|
||||
|
||||
|
||||
domains = list(set(domains))
|
||||
for domain in domains:
|
||||
if domain != "":
|
||||
observable = {'dataType' : 'domain', 'data' : domain, 'message' : 'domain from elastic'}
|
||||
artifacts.append(observable)
|
||||
urls = list(set(urls))
|
||||
for url in urls:
|
||||
if url != "":
|
||||
observable = {'dataType' : 'url', 'data' : url, 'message' : 'url from elastic'}
|
||||
artifacts.append(observable)
|
||||
ips = list(set(ips))
|
||||
for ip in ips:
|
||||
if ip != "":
|
||||
observable = {'dataType' : 'ip', 'data' : ip, 'message' : 'ip from elastic'}
|
||||
artifacts.append(observable)
|
||||
|
||||
return artifacts
|
||||
|
||||
def run(self):
|
||||
Analyzer.run(self)
|
||||
try:
|
||||
for endpoint,key,user,password in zip(self.endpoints,self.keys,self.users,self.passwords):
|
||||
if key:
|
||||
es = Elasticsearch(
|
||||
endpoint,
|
||||
api_key = (key),
|
||||
ca_certs=self.cert,
|
||||
verify_certs=self.verify,
|
||||
timeout=30
|
||||
)
|
||||
elif user:
|
||||
es = Elasticsearch(
|
||||
endpoint,
|
||||
http_auth = (user,password),
|
||||
ca_certs=self.cert,
|
||||
verify_certs=self.verify,
|
||||
timeout=30
|
||||
)
|
||||
else:
|
||||
es = Elasticsearch(
|
||||
endpoint,
|
||||
ca_certs=self.cert,
|
||||
verify_certs=self.verify,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
info = {}
|
||||
hits = []
|
||||
devices = []
|
||||
total = 'eq'
|
||||
#url that links to kibana dashboard
|
||||
info['query'] = ""
|
||||
#query string to show kql search
|
||||
info['querystring'] = ""
|
||||
self.fields = [x.lower() for x in self.fields]
|
||||
#remove all hash fields if not a hash
|
||||
if self.data_type != 'hash':
|
||||
self.fields = list(filter( lambda s: not ("hash" in s), self.fields))
|
||||
#remove all ip fields if not an ip
|
||||
if self.data_type != 'ip':
|
||||
self.fields = list(filter( lambda s: not ("ip" in s), self.fields))
|
||||
#remove all url and domain fields if not a url or domain or fqdn
|
||||
if self.data_type != 'domain' and self.data_type != 'url' and self.data_type != 'fqdn':
|
||||
self.fields = list(filter( lambda s: not ("url" in s or "domain" in s), self.fields))
|
||||
if self.kibana and self.dashboard:
|
||||
#building query
|
||||
info['query'] += self.kibana+"/app/kibana#/dashboard/"+self.dashboard+\
|
||||
"?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-1M,to:now))&_a=(columns:!(_source),interval:auto,query:(language:kuery,query:'"
|
||||
#building query and query string
|
||||
info['query'] += self.fields[0] + "%20:%20%22" + self.data
|
||||
info['querystring'] = self.fields[0] + ':"' + self.data
|
||||
for field in self.fields[1:]:
|
||||
info['query'] += "%22%20or%20" + field + "%20:%20%22" + self.data
|
||||
info['querystring'] += '" or ' + field + ':"' + self.data
|
||||
info['query'] += "%22'),sort:!(!(start_time,desc)))"
|
||||
info['querystring'] += '"'
|
||||
#loop to get hits from each index
|
||||
for index in self.index:
|
||||
#search elastic for fields in each index
|
||||
res = es.search(size=self.size,index=index,body={'sort':[{"@timestamp":{"order":"desc"}}],'query':{'multi_match':{'query':self.data, 'fields':self.fields}}})
|
||||
#if relation is gte then more logs exist than we will display
|
||||
if res['hits']['total']['relation'] == 'gte' or res['hits']['total']['relation'] == 'gt':
|
||||
total = 'gte'
|
||||
#adding results from each query
|
||||
for hit in res['hits']['hits']:
|
||||
hitindex = hit['_index']
|
||||
hitid = hit['_id']
|
||||
#process fields
|
||||
process_parent_name = ""
|
||||
process_name = ""
|
||||
process_args = ""
|
||||
#user fields
|
||||
user_name = ""
|
||||
#host fields
|
||||
host_name = ""
|
||||
#base fields
|
||||
timestamp = ""
|
||||
#destination fields
|
||||
destination_ip = ""
|
||||
destination_port = ""
|
||||
#source fields
|
||||
source_ip = ""
|
||||
source_port = ""
|
||||
source_user_name = ""
|
||||
#event fields
|
||||
event_action = ""
|
||||
#url fields
|
||||
url_domain = ""
|
||||
url_path = ""
|
||||
url_full = ""
|
||||
#dns fields
|
||||
dns_question_name = ""
|
||||
dns_resolvedip = ""
|
||||
#rule fields
|
||||
rule_category = ""
|
||||
|
||||
#base fields
|
||||
if '@timestamp' in hit['_source']:
|
||||
if isinstance(hit['_source']['@timestamp'],str):
|
||||
timestamp = dateutil.parser.parse(hit['_source']['@timestamp'])
|
||||
time = timestamp.astimezone().strftime("%m/%d/%Y %I:%M %p")
|
||||
timestamp = str(timestamp)
|
||||
else:
|
||||
timestamp = dateutil.parser.parse(datetime.fromtimestamp(float(hit['_source']['@timestamp']/1000)).strftime('%c'))
|
||||
time = timestamp.astimezone().strftime("%m/%d/%Y %I:%M %p")
|
||||
timestamp = str(timestamp)
|
||||
#host fields
|
||||
if 'host' in hit['_source']:
|
||||
if 'name' in hit['_source']['host']:
|
||||
host_name = hit['_source']['host']['name']
|
||||
#process fields
|
||||
if 'process' in hit['_source']:
|
||||
if 'parent' in hit['_source']['process']:
|
||||
if 'name' in hit['_source']['process']['parent']:
|
||||
process_parent_name = hit['_source']['process']['parent']['name']
|
||||
if 'name' in hit['_source']['process']:
|
||||
process_name = hit['_source']['process']['name']
|
||||
if 'args' in hit['_source']['process']:
|
||||
process_args = hit['_source']['process']['args']
|
||||
#destination fields
|
||||
if 'destination' in hit['_source']:
|
||||
if 'ip' in hit['_source']['destination']:
|
||||
destination_ip = hit['_source']['destination']['ip']
|
||||
if 'port' in hit['_source']['destination']:
|
||||
destination_port = hit['_source']['destination']['port']
|
||||
#source fields
|
||||
if 'source' in hit['_source']:
|
||||
if 'ip' in hit['_source']['source']:
|
||||
source_ip = hit['_source']['source']['ip']
|
||||
if 'port' in hit['_source']['source']:
|
||||
source_port = hit['_source']['source']['port']
|
||||
if 'user' in hit['_source']['source']:
|
||||
if 'name' in hit['_source']['source']['user']:
|
||||
source_user_name = hit['_source']['source']['user']['name']
|
||||
#event fields
|
||||
if 'event' in hit['_source']:
|
||||
if 'action' in hit['_source']['event']:
|
||||
event_action = hit['_source']['event']['action']
|
||||
#url fields
|
||||
if 'url' in hit['_source']:
|
||||
if 'domain' in hit['_source']['url']:
|
||||
url_domain = hit['_source']['url']['domain']
|
||||
if 'path' in hit['_source']['url']:
|
||||
url_path = hit['_source']['url']['path']
|
||||
if 'full' in hit['_source']['url']:
|
||||
url_full = hit['_source']['url']['full']
|
||||
#user fields
|
||||
if 'user' in hit['_source']:
|
||||
if 'name' in hit['_source']['user']:
|
||||
user_name = hit['_source']['user']['name']
|
||||
#rule fields
|
||||
if 'rule' in hit['_source']:
|
||||
if 'category' in hit['_source']['rule']:
|
||||
rule_category = hit['_source']['rule']['category']
|
||||
#dns fields
|
||||
if 'dns' in hit['_source']:
|
||||
if 'question' in hit['_source']['dns']:
|
||||
if 'name' in hit['_source']['dns']['question']:
|
||||
dns_question_name = hit['_source']['dns']['question']['name']
|
||||
if 'resolved_ip' in hit['_source']['dns']:
|
||||
dns_resolvedip = hit['_source']['dns']['resolved_ip']
|
||||
|
||||
|
||||
hits.append(Hit(hitindex,hitid,process_parent_name,process_name,process_args,user_name,host_name,\
|
||||
timestamp,time,destination_ip,destination_port,source_ip,source_port,source_user_name,\
|
||||
url_domain,url_path,url_full,rule_category,dns_question_name,dns_resolvedip))
|
||||
|
||||
#setup users
|
||||
usernames = [item.user_name for item in hits]
|
||||
source_usernames = [item.source_user_name for item in hits]
|
||||
usernames.extend(source_usernames)
|
||||
info['uniqueusers'] = list(set(usernames))
|
||||
if "" in info['uniqueusers']:
|
||||
info['uniqueusers'].remove("")
|
||||
info['userhitcount'] = len(info['uniqueusers'])
|
||||
|
||||
#setup devices
|
||||
devices = [item.host_name for item in hits]
|
||||
info['uniquedevices'] = list(set(devices))
|
||||
if "" in info['uniquedevices']:
|
||||
info['uniquedevices'].remove("")
|
||||
info['devicehitcount'] = len(info['uniquedevices'])
|
||||
|
||||
#observable that was searched on
|
||||
info['data'] = self.data
|
||||
info['dashboard'] = self.dashboard
|
||||
info['hitcount'] = len(hits)
|
||||
|
||||
#sort the hits based on timestamp
|
||||
hits.sort(key=operator.attrgetter('timestamp'), reverse=True)
|
||||
hits = [ob.__dict__ for ob in hits]
|
||||
|
||||
self.report({'hits' : hits,
|
||||
'info' : info,
|
||||
'total': total})
|
||||
|
||||
except Exception as e:
|
||||
self.unexpectedError(e)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ElasticsearchAnalyzer().run()
|
4
analyzers/Elasticsearch/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
elasticsearch
|
||||
cortexutils
|
||||
pytz
|
||||
requests
|
@ -46,10 +46,11 @@ class EmlParserAnalyzer(Analyzer):
|
||||
|
||||
def artifacts(self, raw):
|
||||
artifacts = []
|
||||
urls = list(iocextract.extract_urls(str(raw)))
|
||||
ipv4s = list(iocextract.extract_ipv4s(str(raw)))
|
||||
mail_addresses = list(iocextract.extract_emails(str(raw)))
|
||||
hashes = list(iocextract.extract_hashes(str(raw)))
|
||||
# report a list of uniq IOCs
|
||||
urls = list(set(list(iocextract.extract_urls(str(raw), strip=True))))
|
||||
ipv4s = list(set(list(iocextract.extract_ipv4s(str(raw)))))
|
||||
mail_addresses = list(set(list(iocextract.extract_emails(str(raw)))))
|
||||
hashes = list(set(list(iocextract.extract_hashes(str(raw)))))
|
||||
|
||||
if urls:
|
||||
for u in urls:
|
||||
|
@ -2,6 +2,18 @@ FROM python:3
|
||||
|
||||
WORKDIR /worker
|
||||
COPY . FileInfo
|
||||
RUN apt update
|
||||
RUN apt install -y -q libboost-regex-dev \
|
||||
libboost-program-options-dev \
|
||||
libboost-system-dev libboost-filesystem-dev libssl-dev \
|
||||
build-essential cmake \
|
||||
git
|
||||
RUN git clone https://github.com/JusticeRage/Manalyze.git && \
|
||||
cd Manalyze && \
|
||||
cmake . && \
|
||||
make -j5 && \
|
||||
cd bin/yara_rules && \
|
||||
python2 update_clamav_signatures.py
|
||||
RUN apt update && \
|
||||
apt install -y -q libfuzzy-dev libimage-exiftool-perl && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
|
@ -14,15 +14,16 @@
|
||||
"description": "Wether to enable manalyze submodule or not.",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"multi": false
|
||||
"multi": false,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "manalyze_enable_docker",
|
||||
"description": "Use docker to run Manalyze.",
|
||||
"description": "Use docker to run Manalyze. Can be used only if not using the docker image of FileInfo",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"multi": false,
|
||||
"default": false
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "manalyze_enable_binary",
|
||||
@ -30,15 +31,15 @@
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"multi": false,
|
||||
"default": true
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "manalyze_binary_path",
|
||||
"description": "Path to the Manalyze binary that was compiled before",
|
||||
"description": "Path to the Manalyze binary that was compiled before. Keep the default value if using the docker image of FileInfo ",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"multi": false,
|
||||
"default": "/opt/Cortex-Analyzers/utils/manalyze/bin/manalyze"
|
||||
"defaultValue": "/worker/Manalyze/bin/manalyze"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
31
analyzers/Inoitsu/Inoitsu.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "Inoitsu",
|
||||
"version": "1.0",
|
||||
"author": "Abdelkader Ben Ali",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "MIT",
|
||||
"description": "Query Inoitsu for a compromised email address.",
|
||||
"dataTypeList": ["mail"],
|
||||
"command": "Inoitsu/inoitsu_analyzer.py",
|
||||
"baseConfig": "Inoitsu",
|
||||
"configurationItems": [
|
||||
|
||||
],
|
||||
"registration_required": false,
|
||||
"subscription_required": false,
|
||||
"service_homepage": "https://www.hotsheet.com/inoitsu/",
|
||||
"service_logo": {
|
||||
"path": "assets/inoitsu_logo.png",
|
||||
"caption": "logo"
|
||||
},
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "assets/Inoitsu_long.png",
|
||||
"caption": "Inoitsu long report sample"
|
||||
},
|
||||
{
|
||||
"path": "assets/Inoitsu_short.png",
|
||||
"caption:": "Inoitsu mini report sample"
|
||||
}
|
||||
]
|
||||
}
|
48
analyzers/Inoitsu/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Inoitsu-analyzer
|
||||
|
||||
This analyzer helps you investigate suspicious emails received from known or unknown senders to ensure that their email addresses aren't compromised.
|
||||
|
||||
No API key required.
|
||||
|
||||
If the email is compromised then it returns:
|
||||
- Total breaches
|
||||
- Most recent breach
|
||||
- Breached data
|
||||
- Critical data
|
||||
- Exposure rating: The comparative data exposure and risk rating assigned to this email address.
|
||||
|
||||
### Testing Inoitsu analyzer (Cortex)
|
||||
|
||||
You need first to enable the analyzer.
|
||||
|
||||
![enable analyzer](https://user-images.githubusercontent.com/37407314/92718622-f4079d00-f359-11ea-8124-0ee9ca565661.PNG)
|
||||
|
||||
Navigate to Analyzers then run Inoitsu analyzer.
|
||||
|
||||
![run analyzer](https://user-images.githubusercontent.com/37407314/92719258-ce2ec800-f35a-11ea-9f82-f4ed9f4ab01e.PNG)
|
||||
|
||||
Test Inoitsu analyzer on a compromised email address.
|
||||
|
||||
![report](https://user-images.githubusercontent.com/37407314/92719758-8d837e80-f35b-11ea-8120-014a389955cd.PNG)
|
||||
|
||||
Test Inoitsu analyzer on an uncompromised email address.
|
||||
|
||||
![uncompromised](https://user-images.githubusercontent.com/37407314/92720556-a9d3eb00-f35c-11ea-8157-911d85149ae4.PNG)
|
||||
|
||||
### Testing Inoitsu analyzer (TheHive)
|
||||
|
||||
In the observables section add emails to test.
|
||||
|
||||
Then select the emails that you want to analyze, select Inoitsu and click on Run selected analyzers.
|
||||
|
||||
![thehive iocs](https://user-images.githubusercontent.com/37407314/92724230-2d440b00-f362-11ea-8115-21c91bf27d2d.PNG)
|
||||
|
||||
![response](https://user-images.githubusercontent.com/37407314/92725358-f2db6d80-f363-11ea-8e59-697e579a75aa.PNG)
|
||||
|
||||
To view the report of the compromised email, click on ```Inoitsu:Compromised="True"```
|
||||
|
||||
![analyzer report](https://user-images.githubusercontent.com/37407314/92727316-d3920f80-f366-11ea-9e29-d2c21d286277.PNG)
|
||||
|
||||
To view the report of the uncompromised email, click on ```Inoitsu:Compromised="False"```
|
||||
|
||||
![analyzer report 2](https://user-images.githubusercontent.com/37407314/92727203-a5accb00-f366-11ea-875a-da30f01b6c4d.PNG)
|
BIN
analyzers/Inoitsu/assets/Inoitsu_long.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
analyzers/Inoitsu/assets/Inoitsu_short.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
analyzers/Inoitsu/assets/inoitsu_logo.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
79
analyzers/Inoitsu/inoitsu_analyzer.py
Executable file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
from cortexutils.analyzer import Analyzer
|
||||
import requests
|
||||
import re
|
||||
|
||||
|
||||
class InoitsuAnalyzer(Analyzer):
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
|
||||
def verify_email_format(self, email):
|
||||
email_regex = '^(?i)[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$'
|
||||
if(re.search(email_regex,email)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def remove_html_tags(self, html):
|
||||
regex = re.compile('<.*?>')
|
||||
cleantext = re.sub(regex, '', html)
|
||||
return cleantext
|
||||
|
||||
def inoitsu_check(self,email):
|
||||
url ="https://www.hotsheet.com/inoitsu/"
|
||||
data = {'act' : email, 'accounthide' : 'test', 'submit' : 'Submit'}
|
||||
r = requests.post(url, data=data, timeout=10)
|
||||
response = r.text
|
||||
if 'BREACH DETECTED!' in response:
|
||||
cleantext = self.remove_html_tags(response)
|
||||
text = cleantext.replace(' ','')
|
||||
Breached_data_finder = re.search('Breached Personal Data(.*)Critical Identity Alerts', text)
|
||||
Breached_data = Breached_data_finder.group(1)[1:]
|
||||
Critical_data_finder = re.search('Critical Identity Alerts(.*)Total Breaches', text)
|
||||
Critical_data = Critical_data_finder.group(1)[1:]
|
||||
Total_breaches_finder = re.search('Total Breaches(.*)Most Recent Breach', text)
|
||||
Total_breaches = Total_breaches_finder.group(1)[1:]
|
||||
Most_recent_breach_finder = re.search('Most Recent Breach(.*)Relative Exposure Rating', text)
|
||||
Most_recent_breach = Most_recent_breach_finder.group(1)[2:]
|
||||
Exposure_rating_finder = re.search('Relative Exposure Rating(.*)breach data from', text)
|
||||
Exposure_rating = Exposure_rating_finder.group(1)[2:]
|
||||
result = dict(Email = email, Leaked = True, Breached_data = Breached_data, Critical_data = Critical_data,
|
||||
Total_breaches = int(Total_breaches), Most_recent_breach = Most_recent_breach,
|
||||
Exposure_rating = Exposure_rating)
|
||||
return result
|
||||
else:
|
||||
return dict(Email = email, Leaked = False)
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
level = "info"
|
||||
namespace = "Inoitsu"
|
||||
predicate = "Compromised"
|
||||
leaked = raw.get("Leaked")
|
||||
if leaked:
|
||||
level = "malicious"
|
||||
value = "True"
|
||||
else:
|
||||
level = "safe"
|
||||
value = "False"
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(level, namespace, predicate, value)
|
||||
)
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
def run(self):
|
||||
email = self.get_data()
|
||||
if not email:
|
||||
self.error('No email given.')
|
||||
try:
|
||||
if self.verify_email_format(email):
|
||||
result = self.inoitsu_check(email)
|
||||
self.report(result)
|
||||
else:
|
||||
self.error('Your input is not an email.')
|
||||
except Exception as e:
|
||||
self.error(str(e))
|
||||
|
||||
if __name__ == "__main__":
|
||||
InoitsuAnalyzer().run()
|
1
analyzers/Inoitsu/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
requests
|
62
analyzers/LdapQuery/LdapQuery.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "Ldap_Query",
|
||||
"version": "2.0",
|
||||
"author": "Florian Perret @cyber_pescadito",
|
||||
"url": "https://github.com/cyberpescadito/Cortex-Analyzers/tree/master/analyzers/LdapQuery",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Query your LDAP server to harvest informations about an user of your organization",
|
||||
"dataTypeList": ["username", "mail"],
|
||||
"command": "LdapQuery/ldapQuery.py",
|
||||
"baseConfig": "LdapQuery",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "LDAP_address",
|
||||
"description": "Should contain the protocol. Eg: ldaps://myldap.myorg.com",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "LDAP_port",
|
||||
"description": "Should contain the ldap port. Eg: 389 or 636",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "LDAP_username",
|
||||
"description": "Usernae of the account that will be used to bind to LDAP server. The Account should have permissions to read ldap objects and attributes.",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "LDAP_password",
|
||||
"description": "Password of the account used to bind to LDAP server.",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "base_DN",
|
||||
"description": "The base DN to use in your LDAP. Eg: dc=myorg,dc=com",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "uid_search_field",
|
||||
"description": "Specify here the field to use when searching by username. Eg: uid or sAMAccountName",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"description": "Specify here the attributes you want to harvest. Eg: mail",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
100
analyzers/LdapQuery/ldapQuery.py
Executable file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
# Author: @cyber_pescadito
|
||||
import json
|
||||
from cortexutils.analyzer import Analyzer
|
||||
import ldap3
|
||||
from ldap3 import Server, Connection, SIMPLE, SYNC, SUBTREE, ALL
|
||||
|
||||
|
||||
class LdapQuery(Analyzer):
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
ldap_address = self.get_param(
|
||||
"config.LDAP_address", None, "ldap_address is missing"
|
||||
)
|
||||
ldap_port = self.get_param("config.LDAP_port", None, "ldap_port is missing")
|
||||
ldap_port = int(ldap_port)
|
||||
|
||||
username = self.get_param("config.LDAP_username", None, "username is missing")
|
||||
password = self.get_param("config.LDAP_password", None, "password is missing")
|
||||
self.base_dn = self.get_param("config.base_DN", None, "base_dn is missing")
|
||||
uid_search_field = self.get_param(
|
||||
"config.search_field", None, "uid_search_field is missing"
|
||||
)
|
||||
if self.data_type == "mail":
|
||||
self.search_field = "mail"
|
||||
else:
|
||||
self.search_field = uid_search_field
|
||||
|
||||
self.attributes = self.get_param(
|
||||
"config.attributes", None, "Missing attributes list to report"
|
||||
)
|
||||
try:
|
||||
s = Server(
|
||||
ldap_address,
|
||||
port=ldap_port,
|
||||
get_info=ALL,
|
||||
use_ssl=True if ldap_port == 636 else False,
|
||||
)
|
||||
self.connection = Connection(
|
||||
s,
|
||||
auto_bind=True,
|
||||
client_strategy=SYNC,
|
||||
user=username,
|
||||
password=password,
|
||||
authentication=SIMPLE,
|
||||
check_names=True,
|
||||
)
|
||||
except Exception:
|
||||
self.error("Error during LDAP connection")
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
level = "info"
|
||||
namespace = "LDAP"
|
||||
predicate = "Query"
|
||||
|
||||
# Find a value to return in value attribute of taxonomies object
|
||||
for user in raw["results"]:
|
||||
if user.get("o", None):
|
||||
value = user["o"]
|
||||
elif user.get("cn", None):
|
||||
value = user["cn"]
|
||||
elif user.get("mail", None):
|
||||
value = user["mail"]
|
||||
else:
|
||||
value = "success"
|
||||
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
def run(self):
|
||||
# Checking connection to LDAP
|
||||
Analyzer.run(self)
|
||||
try:
|
||||
|
||||
data = self.get_param("data", None, "Data is missing")
|
||||
q = "({}={})".format(self.search_field, data)
|
||||
|
||||
self.connection.search(self.base_dn, q, SUBTREE, attributes=self.attributes)
|
||||
responses = self.connection.response
|
||||
|
||||
users = []
|
||||
if responses:
|
||||
for response in responses:
|
||||
dict_response = response.get("attributes", None)
|
||||
user = {}
|
||||
if dict_response:
|
||||
for att in dict_response.keys():
|
||||
user[att] = dict_response[att]
|
||||
users.append(user)
|
||||
|
||||
self.connection.unbind()
|
||||
|
||||
self.report({"results": users})
|
||||
except Exception as e:
|
||||
self.error(str(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
LdapQuery().run()
|
2
analyzers/LdapQuery/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
cortexutils
|
||||
ldap3
|
@ -10,7 +10,7 @@ class PDNSv3(Analyzer):
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
|
||||
self.base_url = "https://portal.mnemonic.no/web/api/pdns/v3"
|
||||
self.base_url = "https://api.mnemonic.no/pdns/v3"
|
||||
self.apikey = self.get_param("config.key", None)
|
||||
self.service = self.get_param('config.service', None, 'Service parameter is missing')
|
||||
|
||||
|
@ -34,7 +34,7 @@ class OTXQueryAnalyzer(Analyzer):
|
||||
try:
|
||||
for section in sections:
|
||||
queryurl = baseurl + section
|
||||
ip_[section] = json.loads(requests.get(queryurl, headers=headers).content)
|
||||
ip_[section] = json.loads(requests.get(queryurl, headers=headers).text)
|
||||
|
||||
ip_general = ip_['general']
|
||||
ip_geo = ip_['geo']
|
||||
@ -64,7 +64,7 @@ class OTXQueryAnalyzer(Analyzer):
|
||||
try:
|
||||
for section in sections:
|
||||
queryurl = baseurl + section
|
||||
ip_[section] = json.loads(requests.get(queryurl, headers=headers).content)
|
||||
ip_[section] = json.loads(requests.get(queryurl, headers=headers).text)
|
||||
|
||||
result = {
|
||||
'pulse_count': ip_.get('general', {}).get('pulse_info', {}).get('count', "0"),
|
||||
@ -98,7 +98,7 @@ class OTXQueryAnalyzer(Analyzer):
|
||||
try:
|
||||
for section in sections:
|
||||
queryurl = baseurl + section
|
||||
ip_[section] = json.loads(requests.get(queryurl, headers=headers).content)
|
||||
ip_[section] = json.loads(requests.get(queryurl, headers=headers).text)
|
||||
|
||||
if ip_['analysis']['analysis']:
|
||||
# file has been analyzed before
|
||||
@ -142,7 +142,7 @@ class OTXQueryAnalyzer(Analyzer):
|
||||
try:
|
||||
for section in sections:
|
||||
queryurl = baseurl + section
|
||||
IP_[section] = json.loads(requests.get(queryurl, headers=headers).content)
|
||||
IP_[section] = json.loads(requests.get(queryurl, headers=headers).text)
|
||||
|
||||
self.report({
|
||||
'pulse_count': IP_.get('general', {}).get('pulse_info', {}).get('count', "0"),
|
||||
|
@ -15,6 +15,7 @@ class OpenCTIAnalyzer(Analyzer):
|
||||
names = self.get_param('config.name', None, 'No OpenCTI instance name given.')
|
||||
urls = self.get_param('config.url', None, 'No OpenCTI url given.')
|
||||
keys = self.get_param('config.key', None, 'No OpenCTI api key given.')
|
||||
proxies = self.get_param('config.proxy', None)
|
||||
|
||||
if len(names) != len(urls) or len(urls) != len(keys):
|
||||
self.error("Config error: please add a name, an url and a key for each OpenCTI instance.")
|
||||
|
@ -5,26 +5,48 @@
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Get the current Patrowl report for a fdqn, a domain or an IP address.",
|
||||
"dataTypeList": ["fqdn", "domain", "ip"],
|
||||
"dataTypeList": [
|
||||
"fqdn",
|
||||
"domain",
|
||||
"ip"
|
||||
],
|
||||
"baseConfig": "Patrowl",
|
||||
"config": {
|
||||
"service": "getreport"
|
||||
"service": "getreport"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "url",
|
||||
"description": "Define the PatrOwl url",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "Define the PatrOwl API Key",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
{
|
||||
"name": "url",
|
||||
"description": "Define the PatrOwl url",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "Define the PatrOwl API Key",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"command": "Patrowl/patrowl.py"
|
||||
}
|
||||
"command": "Patrowl/patrowl.py",
|
||||
"registration_required": false,
|
||||
"subscription_required": false,
|
||||
"free_subscription": false,
|
||||
"service_homepage": "https://patrowl.io/home",
|
||||
"service_logo": {
|
||||
"path": "assets/logo.png",
|
||||
"caption": "logo"
|
||||
},
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "assets/patrowl-minireport.png",
|
||||
"caption": "Patrowl_GetReport: short report template"
|
||||
},
|
||||
{
|
||||
"path": "assets/patrowl-longreport.png",
|
||||
"caption": "Patrowl_GetReport: long report template"
|
||||
}
|
||||
]
|
||||
}
|
11
analyzers/Patrowl/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
### Patrowl
|
||||
|
||||
Get the current [Patrowl](https://github.com/Patrowl/PatrowlManager) report for a fdqn, a domain or an IP address.
|
||||
|
||||
The analyzer comes in only one flavor called **Patrowl_GetReport**.
|
||||
|
||||
#### Requirements
|
||||
You need a running [Patrowl](https://github.com/Patrowl/PatrowlManager) instance or to have access to one to use the analyzer. Supply the following parameters to the analyzer in order to use it:
|
||||
|
||||
- `url`: The PatrowlManager service URL
|
||||
- `api_key`: A valid API Key of a Patrowl user
|
BIN
analyzers/Patrowl/assets/logo.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
analyzers/Patrowl/assets/patrowl-longreport.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
analyzers/Patrowl/assets/patrowl-minireport.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
38
analyzers/SophosIntelix/SophosIntelix_GetReport.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "SophosIntelix_GetReport",
|
||||
"version": "0.3",
|
||||
"author": "SOL",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Fast and easy way to find out if the file is known Good, PUA (Potentially Unwanted Application), or, Malware. For more information or to sign up for SophosLabs Intelix (with a free tier) see https://www.sophos.com/en-us/labs/intelix.aspx",
|
||||
"dataTypeList": ["hash", "domain", "fqdn", "url"],
|
||||
"command": "SophosIntelix/intelix_analyzer.py",
|
||||
"baseConfig": "SophosIntelix",
|
||||
"config": {
|
||||
"service": "get"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "clientID",
|
||||
"description": "Client ID for Sophos Labs Intelix",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "clientSecret",
|
||||
"description": "Client Secret for Sophos Labs Intelix",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "polling_interval",
|
||||
"description": "Define time interval between two requests attempts for the report",
|
||||
"type": "number",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": 60
|
||||
}
|
||||
]
|
||||
}
|
38
analyzers/SophosIntelix/SophosIntelix_Submit_Dynamic.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "SophosIntelix_Submit_Dynamic",
|
||||
"version": "0.1",
|
||||
"author": "SOL",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Detonate your suspicious file in SophosLabs Sandbox and find what behaviours the file has. For more information or to sign up for SophosLabs Intelix (with a free tier) see https://www.sophos.com/en-us/labs/intelix.aspx",
|
||||
"dataTypeList": ["file"],
|
||||
"command": "SophosIntelix/intelix_analyzer.py",
|
||||
"baseConfig": "SophosIntelix",
|
||||
"config": {
|
||||
"service": "submit_dynamic"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "clientID",
|
||||
"description": "Client ID for Sophos Labs Intelix",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "clientSecret",
|
||||
"description": "Client Secret for Sophos Labs Intelix",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "polling_interval",
|
||||
"description": "Define time interval between two requests attempts for the report",
|
||||
"type": "number",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": 60
|
||||
}
|
||||
]
|
||||
}
|
38
analyzers/SophosIntelix/SophosIntelix_Submit_Static.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "SophosIntelix_Submit_Static",
|
||||
"version": "0.1",
|
||||
"author": "SOL",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Use SophosLabs machine learning to understand the characteristics of your suspicious file allowing you to see if the file is similar to known malware. For more information or to sign up for SophosLabs Intelix (with a free tier) see https://www.sophos.com/en-us/labs/intelix.aspx",
|
||||
"dataTypeList": ["file"],
|
||||
"command": "SophosIntelix/intelix_analyzer.py",
|
||||
"baseConfig": "SophosIntelix",
|
||||
"config": {
|
||||
"service": "submit_static"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "clientID",
|
||||
"description": "Client ID for Sophos Labs Intelix",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "clientSecret",
|
||||
"description": "Client Secret for Sophos Labs Intelix",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "polling_interval",
|
||||
"description": "Define time interval between two requests attempts for the report",
|
||||
"type": "number",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": 60
|
||||
}
|
||||
]
|
||||
}
|
159
analyzers/SophosIntelix/intelix_analyzer.py
Executable file
@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
import intelix
|
||||
import time
|
||||
from cortexutils.analyzer import Analyzer
|
||||
|
||||
|
||||
class SophosIntelixAnalyzer(Analyzer):
|
||||
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
self.service = self.get_param('config.service', None, 'Service parameter is missing')
|
||||
self.clientId = self.get_param('config.clientID', None, 'ClientId is Missing')
|
||||
self.clientSecret = self.get_param('config.clientSecret', None, 'Client Secret is Missing')
|
||||
self.polling_interval = self.get_param('config.polling_interval', 60)
|
||||
try:
|
||||
self.ic = intelix.client(self.clientId, self.clientSecret)
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
self.error('Error: {}'.format(error))
|
||||
|
||||
def run(self):
|
||||
if self.service == 'get':
|
||||
if self.data_type == 'hash':
|
||||
try:
|
||||
data = self.get_data()
|
||||
try:
|
||||
self.ic.file_lookup(data)
|
||||
self.report({
|
||||
"file_hash": data,
|
||||
"reputation_score": self.ic.reputationScore,
|
||||
"classification": self.ic.classification
|
||||
})
|
||||
except TypeError:
|
||||
self.report({
|
||||
"file_hash": data,
|
||||
"reputation_score": "None",
|
||||
"classification": "Unknown"
|
||||
})
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
self.error('Error: {}'.format(error))
|
||||
|
||||
elif self.data_type in ('domain', 'fqdn', 'url'):
|
||||
try:
|
||||
data = self.get_data()
|
||||
self.ic.url_lookup(data)
|
||||
self.report({
|
||||
"prod_category": self.ic.productivityCategory,
|
||||
"sec_category": self.ic.securityCategory,
|
||||
"risk_level": self.ic.riskLevel
|
||||
})
|
||||
except:
|
||||
self.error('Error running URL lookup on {}'.format(data))
|
||||
else:
|
||||
self.error('Unsupported Data Type')
|
||||
elif self.service == "submit_static":
|
||||
filepath = self.get_param('file', None, 'File is missing')
|
||||
self.ic.submit_file(filepath, "static")
|
||||
self.ic.file_report_by_jobid(self.ic.jobId, "static")
|
||||
|
||||
while self.ic.report is None:
|
||||
time.sleep(self.polling_interval)
|
||||
self.ic.file_report_by_jobid(self.ic.jobId, "static")
|
||||
else:
|
||||
self.report(self.ic.report)
|
||||
|
||||
elif self.service == "submit_dynamic":
|
||||
filepath = self.get_param('file', None, 'File is missing')
|
||||
self.ic.submit_file(filepath, "dynamic")
|
||||
self.ic.file_report_by_jobid(self.ic.jobId, "dynamic")
|
||||
|
||||
while self.ic.report is None:
|
||||
time.sleep(self.polling_interval)
|
||||
self.ic.file_report_by_jobid(self.ic.jobId, "dynamic")
|
||||
else:
|
||||
self.report(self.ic.report)
|
||||
else:
|
||||
self.error('Invalid Service Type')
|
||||
|
||||
def summary(self, raw):
|
||||
|
||||
taxonomies = []
|
||||
namespace = "Intelix"
|
||||
|
||||
if self.service == 'get':
|
||||
if self.data_type in ('domain', 'fqdn', 'url'):
|
||||
if self.ic.riskLevel == "UNCLASSIFIED":
|
||||
level = "info"
|
||||
elif self.ic.riskLevel == "TRUSTED":
|
||||
level = "safe"
|
||||
elif self.ic.riskLevel == "LOW":
|
||||
level = "info"
|
||||
elif self.ic.riskLevel == "MEDIUM":
|
||||
level = "suspicious"
|
||||
elif self.ic.riskLevel == "HIGH":
|
||||
level = "malicious"
|
||||
else:
|
||||
level = "info"
|
||||
|
||||
result = {
|
||||
"has_result": True
|
||||
}
|
||||
|
||||
predicate = "RiskLevel"
|
||||
value = "{}".format(self.ic.riskLevel)
|
||||
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
elif self.data_type == 'hash':
|
||||
if (self.ic.reputationScore <= 19):
|
||||
level = "malicious"
|
||||
elif (self.ic.reputationScore > 19 and self.ic.reputationScore <= 29):
|
||||
level = "suspicious"
|
||||
elif (self.ic.reputationScore > 29 and self.ic.reputationScore <= 69):
|
||||
level = "suspicious"
|
||||
elif (self.ic.reputationScore > 69 and self.ic.reputationScore <= 100):
|
||||
level = "safe"
|
||||
else:
|
||||
level = "info"
|
||||
|
||||
result = {
|
||||
"has_result": True
|
||||
}
|
||||
|
||||
predicate = "Score"
|
||||
value = "{} - {}".format(self.ic.reputationScore, self.ic.classification)
|
||||
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
elif (self.service == "submit_static") or (self.service == "submit_dynamic"):
|
||||
|
||||
result = {
|
||||
"has_result": True
|
||||
}
|
||||
|
||||
predicate = "Score"
|
||||
value = "{}".format(self.ic.report.get("score"))
|
||||
|
||||
if (self.ic.report.get("score") <= 19):
|
||||
level = "malicious"
|
||||
elif (self.ic.report.get("score") > 19 and self.ic.report.get("score") <= 29):
|
||||
level = "suspicious"
|
||||
elif (self.ic.report.get("score") > 29 and self.ic.report.get("score") <= 69):
|
||||
level = "suspicious"
|
||||
elif (self.ic.report.get("score") > 69 and self.ic.report.get("score") <= 100):
|
||||
level = "safe"
|
||||
else:
|
||||
level = "info"
|
||||
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
SophosIntelixAnalyzer().run()
|
3
analyzers/SophosIntelix/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
cortexutils
|
||||
requests
|
||||
intelix
|
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
|
||||
import splunklib.client as client
|
||||
from time import sleep
|
||||
from cortexutils.analyzer import Analyzer
|
||||
@ -138,7 +139,7 @@ class Splunk(Analyzer):
|
||||
|
||||
|
||||
if jobResult["resultCount"] > self.MAX_COUNT:
|
||||
jobResult["note"] = "Only the first "+str(self.MAX_COUNT)+" results were recovered over "+jobResult["resultCount"]+" to avoid any trouble on TheHive/Cortex. This parameter (max_count) can be changed in the analyzer configuration."
|
||||
jobResult["note"] = "Only the first {} results were recovered over {} to avoid any trouble on TheHive/Cortex. This parameter (max_count) can be changed in the analyzer configuration.".format(self.MAX_COUNT, jobResult["resultCount"])
|
||||
|
||||
jobResult["search"] = job_infos["search"]
|
||||
jobResult["savedsearch"] = saved_search
|
||||
|
@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
import requests
|
||||
import cloudscraper
|
||||
from cortexutils.analyzer import Analyzer
|
||||
import json
|
||||
|
||||
|
||||
class TalosReputation(Analyzer):
|
||||
|
||||
@ -12,7 +12,7 @@ class TalosReputation(Analyzer):
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
level = 'info' #If there's a change of naming, will be presented as info
|
||||
level = 'info' # If there's a change of naming, will be presented as info
|
||||
namespace = 'Talos'
|
||||
predicate = 'Reputation'
|
||||
value = raw.get('email_score_name')
|
||||
@ -31,29 +31,28 @@ class TalosReputation(Analyzer):
|
||||
try:
|
||||
data = self.get_data()
|
||||
|
||||
s = requests.Session()
|
||||
s.get('https://talosintelligence.com/reputation_center/lookup?search={}'.format(data))
|
||||
scraper = cloudscraper.CloudScraper()
|
||||
|
||||
headers={
|
||||
'Host':'talosintelligence.com',
|
||||
'Referer':'https://talosintelligence.com/reputation_center/lookup?search={}'.format(data),
|
||||
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
|
||||
'Accept':'application/json'
|
||||
headers = {
|
||||
'Host': 'talosintelligence.com',
|
||||
'Referer': 'https://talosintelligence.com/reputation_center/lookup?search={}'.format(data),
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
response_details = s.get('https://talosintelligence.com/sb_api/query_lookup',
|
||||
response_details = scraper.get('https://talosintelligence.com/sb_api/query_lookup',
|
||||
headers = headers,
|
||||
params = {
|
||||
'query':'/api/v2/details/ip/',
|
||||
'query_entry':data
|
||||
'query': '/api/v2/details/ip/',
|
||||
'query_entry': data
|
||||
}
|
||||
)
|
||||
|
||||
response_location = s.get('https://talosintelligence.com/sb_api/query_lookup',
|
||||
response_location = scraper.get('https://talosintelligence.com/sb_api/query_lookup',
|
||||
headers = headers,
|
||||
params = {
|
||||
'query':'/api/v2/location/ip/',
|
||||
'query_entry':data
|
||||
'query': '/api/v2/location/ip/',
|
||||
'query_entry': data
|
||||
}
|
||||
)
|
||||
|
||||
@ -69,7 +68,8 @@ class TalosReputation(Analyzer):
|
||||
except Exception as e:
|
||||
self.unexpectedError(e)
|
||||
else:
|
||||
self.notSupported()
|
||||
self.notSupported()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
TalosReputation().run()
|
||||
|
@ -1,2 +1,2 @@
|
||||
cortexutils
|
||||
requests
|
||||
cloudscraper
|
||||
|
@ -45,8 +45,17 @@ class UmbrellaAnalyzer(Analyzer):
|
||||
|
||||
|
||||
def run(self):
|
||||
# Map The Hive observable types to Umbrella observable types
|
||||
observable_mapping = {
|
||||
"domain": "domain",
|
||||
"fqdn": "domain",
|
||||
}
|
||||
|
||||
if self.service == 'get':
|
||||
if self.data_type == 'domain':
|
||||
dataType = self.get_param("dataType")
|
||||
|
||||
# Validate the supplied observable type is supported
|
||||
if dataType in observable_mapping.keys():
|
||||
data = self.get_param('data', None, 'Data is missing')
|
||||
r = self.umbrella_runreport(data)
|
||||
self.report(r)
|
||||
|
@ -5,7 +5,7 @@
|
||||
"url": "https://github.com/arnydo/thehive/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Query the Umbrella Reporting API for recent DNS queries and their status.",
|
||||
"dataTypeList": ["domain"],
|
||||
"dataTypeList": ["domain", "fqdn"],
|
||||
"command": "Umbrella/Umbrella.py",
|
||||
"baseConfig": "Umbrella",
|
||||
"config": {
|
||||
|
@ -3,9 +3,13 @@
|
||||
"license": "AGPL-V3",
|
||||
"author": "Nils Kuhnert, CERT-Bund",
|
||||
"url": "https://github.com/BSI-CERT-Bund/cortex-analyzers",
|
||||
"version": "3.0",
|
||||
"version": "3.1",
|
||||
"description": "VMRay Sandbox file analysis.",
|
||||
"dataTypeList": ["hash", "file"],
|
||||
"dataTypeList": [
|
||||
"hash",
|
||||
"file",
|
||||
"url"
|
||||
],
|
||||
"command": "VMRay/vmray.py",
|
||||
"baseConfig": "VMRay",
|
||||
"configurationItems": [
|
||||
@ -28,7 +32,7 @@
|
||||
"description": "Verify certificates",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"required": false,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
@ -39,12 +43,115 @@
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "disablereanalyze",
|
||||
"description": "If set to true, samples won't get re-analyzed.",
|
||||
"name": "query_retry_wait",
|
||||
"description": "The amount of seconds to wait before trying to fetch the results.",
|
||||
"type": "number",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": 10
|
||||
},
|
||||
{
|
||||
"name": "recursive_sample_limit",
|
||||
"description": "The maximum amount of recursive samples which will be analyzed. 0 disables recursion.",
|
||||
"type": "number",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": 10
|
||||
},
|
||||
{
|
||||
"name": "reanalyze",
|
||||
"description": "If set to true, known samples will be re-analyzed on submission. This is enabled by default.",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "shareable",
|
||||
"description": "If set to true, the hash of the sample will be shared with VirusTotal if the TLP level is white or green.",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "archive_password",
|
||||
"description": "The password that will be used to extract archives.",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "malware"
|
||||
},
|
||||
{
|
||||
"name": "archive_compound_sample",
|
||||
"description": "If set to true, files inside archives are treated as a single, compound sample. Otherwise, each file is treated as its own sample.",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "max_jobs",
|
||||
"description": "Limits the amount of jobs that can be created by jobrules for a submission.",
|
||||
"type": "number",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "enable_reputation",
|
||||
"description": "If set to true, reputation lookups will be performed for submitted samples and analysis artifacts (file hash and URL lookups) by the VMRay cloud reputation service and additional third party services. The user analyzer setting is used as default value for this parameter.",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "enable_whois",
|
||||
"description": "If set to true, domains seen during analyses are queried with external WHOIS service. The user analyzer setting is used as default value for this parameter.",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "analyzer_mode",
|
||||
"description": "Specifies which types of analyzers will be used for analyzing this sample. Supported strings are 'reputation', 'reputation_static', 'reputation_static_dynamic', 'static_dynamic', and 'static'. The user analyzer setting is used as default value for this parameter.",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "known_malicious",
|
||||
"description": "If set to true, triage will be used to pre-filter known malicious samples by results of reputation lookup (if allowed) and static analysis. The user analyzer setting is used as default value for this parameter.",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "known_benign",
|
||||
"description": "If set to true, triage will be used to pre-filter known benign samples by results of reputation lookup (if allowed) and static analysis. The user analyzer setting is used as default value for this parameter.",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"description": "Tags to attach to the sample.",
|
||||
"type": "string",
|
||||
"multi": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"description": "Analysis timeout in seconds.",
|
||||
"type": "number",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "net_scheme_name",
|
||||
"description": "Name of the network schema.",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -7,98 +7,289 @@ from time import sleep
|
||||
class VMRayAnalyzer(Analyzer):
|
||||
"""
|
||||
VMRay analyzer that uses VMRayClient to connect to an VMRay instance. Allows uploading a sample and getting
|
||||
information via hash. More info regarding configuration in the complete documentation.
|
||||
information bac via submission data. More info regarding configuration in the complete documentation.
|
||||
"""
|
||||
|
||||
_namespace = "VMRay"
|
||||
|
||||
_severity_mapping = {
|
||||
"whitelisted": "safe",
|
||||
"suspicious": "suspicious",
|
||||
"malicious": "malicious",
|
||||
"blacklisted": "malicious",
|
||||
}
|
||||
|
||||
_ioc_mapping = {
|
||||
"domains": ("domain", "domain"),
|
||||
"email_addresses": ("email", "mail"),
|
||||
"emails": ("sender", "mail"),
|
||||
"files": ("filename", "filename"),
|
||||
"ips": ("ip_address", "ip"),
|
||||
"mutexes": ("mutex_name", "other"),
|
||||
"registry": ("reg_key_name", "registry"),
|
||||
"urls": ("url", "url"),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
self.url = self.get_param('config.url', None, 'No VMRay url given.').rstrip('/ ')
|
||||
self.disable_reanalyze = self.get_param('config.disablereanalyze', False)
|
||||
self.reanalyze = self.get_param("config.reanalyze", True)
|
||||
self.shareable = self.get_param("config.shareable", False)
|
||||
self.tags = self.get_param("config.tags", ["TheHive"])
|
||||
self.user_config = {
|
||||
"timeout": self.get_param("config.timeout", None),
|
||||
"net_scheme_name": self.get_param("config.net_scheme_name", None),
|
||||
}
|
||||
|
||||
# Check for string and boolean True
|
||||
if self.disable_reanalyze == 'true' or self.disable_reanalyze:
|
||||
reanalyze = False
|
||||
else:
|
||||
reanalyze = True
|
||||
self.query_retry_wait = self.get_param("config.query_retry_wait", 10)
|
||||
self.recursive_sample_limit = self.get_param(
|
||||
"config.recursive_sample_limit", 10
|
||||
)
|
||||
|
||||
verify = self.get_param('config.certverify', None, 'Certificate verification parameter is missing.')
|
||||
certpath = self.get_param('config.certpath', None)
|
||||
verify = self.get_param("config.certverify", True)
|
||||
certpath = self.get_param("config.certpath", None)
|
||||
if verify and certpath:
|
||||
verify = certpath
|
||||
self.vmrc = VMRayClient(url=self.url,
|
||||
key=self.get_param('config.key', None, 'No VMRay API key given.'),
|
||||
cert=verify,
|
||||
reanalyze=reanalyze)
|
||||
|
||||
archive_compound_sample = self.get_param(
|
||||
"config.archive_compound_sample", False
|
||||
)
|
||||
if archive_compound_sample:
|
||||
archive_action = "compound_sample"
|
||||
else:
|
||||
archive_action = "separate_samples"
|
||||
|
||||
self.vmrc = VMRayClient(
|
||||
url=self.get_param("config.url", None, "No VMRay URL given.").rstrip("/ "),
|
||||
key=self.get_param("config.key", None, "No VMRay API key given."),
|
||||
reanalyze=self.reanalyze,
|
||||
verify=verify,
|
||||
archive_password=self.get_param("config.archive_password", "malware"),
|
||||
archive_action=archive_action,
|
||||
max_jobs=self.get_param("config.max_jobs", None),
|
||||
enable_reputation=self.get_param("config.enable_reputation", None),
|
||||
enable_whois=self.get_param("config.enable_whois", None),
|
||||
analyzer_mode=self.get_param("config.analyzer_mode", None),
|
||||
known_malicious=self.get_param("config.known_malicious", None),
|
||||
known_benign=self.get_param("config.known_benign", None),
|
||||
)
|
||||
|
||||
def _build_sample_node(self, sample, current_recursion_level):
|
||||
sample_id = sample["sample_id"]
|
||||
sample["sample_analyses"] = self.vmrc.get_sample_analyses(sample_id)
|
||||
sample["sample_threat_indicators"] = self.vmrc.get_sample_threat_indicators(
|
||||
sample_id
|
||||
)
|
||||
sample["sample_mitre_attack"] = self.vmrc.get_sample_mitre_attack(sample_id)
|
||||
sample["sample_iocs"] = self.vmrc.get_sample_iocs(sample_id)
|
||||
if self.recursive_sample_limit > current_recursion_level:
|
||||
sample["sample_child_samples"] = [
|
||||
self.vmrc.get_sample(child_sample_id)
|
||||
for child_sample_id in sample["sample_child_sample_ids"]
|
||||
]
|
||||
for child_sample in sample["sample_child_samples"]:
|
||||
self._build_sample_node(child_sample, current_recursion_level + 1)
|
||||
|
||||
def _build_report(self, submissions=None, samples=None):
|
||||
if not submissions and not samples:
|
||||
self.error(
|
||||
"Either submissions or samples must be provided in order to build a report"
|
||||
)
|
||||
return
|
||||
sample_ids = (
|
||||
[sample["sample_id"] for sample in samples]
|
||||
if samples
|
||||
else [submission["submission_sample_id"] for submission in submissions]
|
||||
)
|
||||
# note: the dictionary fetched in case the reanalysis is disabled is incomplete. we need to query the samples again in all cases
|
||||
samples = [self.vmrc.get_sample(sample_id) for sample_id in sample_ids]
|
||||
for sample in samples:
|
||||
self._build_sample_node(sample, 0)
|
||||
return {"samples": samples}
|
||||
|
||||
def _wait_for_results(self, submission_result):
|
||||
# Ref: #332: check if job was submitted
|
||||
if not self.reanalyze:
|
||||
if len(submission_result["errors"]) > 0:
|
||||
# Sample has alredy been analyzed and reanalysis is turned off, get the reports
|
||||
self.report(self._build_report(samples=submission_result["samples"]))
|
||||
return # stop waiting for report, because we already have it
|
||||
|
||||
running_submissions = submission_result["submissions"]
|
||||
finished_submissions = []
|
||||
while len(running_submissions) != len(finished_submissions):
|
||||
finished_submissions.extend(
|
||||
[
|
||||
updated_submission
|
||||
for updated_submission in (
|
||||
self.vmrc.update_submission(current_submission["submission_id"])
|
||||
for current_submission in running_submissions
|
||||
)
|
||||
if "submission_finished" in updated_submission
|
||||
and updated_submission["submission_finished"]
|
||||
]
|
||||
)
|
||||
sleep(self.query_retry_wait)
|
||||
|
||||
# Return the results
|
||||
self.report(self._build_report(submissions=finished_submissions))
|
||||
|
||||
def run(self):
|
||||
if self.data_type == 'hash':
|
||||
self.report({'scanreport': self.vmrc.get_sample(self.get_data())})
|
||||
elif self.data_type == 'file':
|
||||
filepath = self.get_param('file')
|
||||
filename = self.get_param('filename')
|
||||
submit_report = self.vmrc.submit_sample(filepath=filepath,
|
||||
filename=filename)
|
||||
# Ref: #332: check if job was submitted
|
||||
if self.disable_reanalyze:
|
||||
if len(submit_report['data']['errors']) > 0:
|
||||
if submit_report['result'] == 'ok':
|
||||
# Sample is already there, get the report
|
||||
self.report({'scanreport': self.vmrc.get_sample(samplehash=submit_report['data']['samples'][0]['sample_sha256hash'])})
|
||||
return
|
||||
else:
|
||||
self.error('Error while submitting sample to VMRay: {}.'
|
||||
.format([error_msg for error_msg in submit_report['data']['errors']]))
|
||||
# Check for completion
|
||||
while not self.vmrc.query_job_status(submissionid=submit_report['data']['submissions'][0]['submission_id']):
|
||||
sleep(10)
|
||||
|
||||
# Return the results
|
||||
self.report({'scanreport': self.vmrc.get_sample(
|
||||
samplehash=submit_report['data']['submissions'][0]['submission_sample_sha256'])
|
||||
})
|
||||
if self.data_type == "hash":
|
||||
# don't run anything, try to build a report using existing results instead
|
||||
samples = self.vmrc.get_samples_by_hash(self.get_data())
|
||||
if samples:
|
||||
self.report(self._build_report(samples=samples))
|
||||
else:
|
||||
self.report({"samples": samples})
|
||||
elif self.data_type == "file":
|
||||
shareable = self.shareable and self.get_param("tlp") in (0, 1)
|
||||
self._wait_for_results(
|
||||
self.vmrc.submit_file_sample(
|
||||
file_path=self.get_param("file"),
|
||||
file_name=self.get_param("filename"),
|
||||
tags=self.tags,
|
||||
shareable=shareable,
|
||||
user_config=self.user_config,
|
||||
)
|
||||
)
|
||||
elif self.data_type == "url":
|
||||
shareable = self.shareable and self.get_param("tlp") in (0, 1)
|
||||
self._wait_for_results(
|
||||
self.vmrc.submit_url_sample(
|
||||
url_sample=self.get_data(),
|
||||
tags=self.tags,
|
||||
shareable=shareable,
|
||||
user_config=self.user_config,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.error('Data type currently not supported')
|
||||
self.error("Data type currently not supported")
|
||||
|
||||
def _taxonomies_for_samples(self, samples):
|
||||
taxonomies = []
|
||||
for sample in samples:
|
||||
level = self._severity_mapping.get(sample["sample_severity"], "info")
|
||||
value = "{}".format(sample["sample_score"])
|
||||
if len(samples) > 1:
|
||||
value += " (from sample {})".format(sample["sample_id"])
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(level, self._namespace, "Score", value)
|
||||
)
|
||||
|
||||
for threat_indicator in sample.get("sample_threat_indicators", {}).get(
|
||||
"threat_indicators", []
|
||||
):
|
||||
predicate = threat_indicator.get("category", None)
|
||||
value = threat_indicator.get("operation", "")
|
||||
if predicate:
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(level, self._namespace, predicate, value)
|
||||
)
|
||||
|
||||
for mitre_technique in sample.get("sample_mitre_attack", {}).get(
|
||||
"mitre_attack_techniques", []
|
||||
):
|
||||
predicate = mitre_technique.get("technique_id", None)
|
||||
value = mitre_technique.get("technique", "Unknown MITRE technique")
|
||||
if "tactics" in mitre_technique:
|
||||
value += " using tactics: {}".format(
|
||||
", ".join(mitre_technique["tactics"])
|
||||
)
|
||||
if predicate:
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(level, self._namespace, predicate, value)
|
||||
)
|
||||
|
||||
# add child sample taxonomies if they have been added
|
||||
taxonomies.extend(
|
||||
self._taxonomies_for_samples(sample.get("sample_child_samples", []))
|
||||
)
|
||||
return taxonomies
|
||||
|
||||
def _sandbox_reports_for_samples(self, samples):
|
||||
sandbox = "vmray"
|
||||
sandbox_type = "on-premise"
|
||||
sandbox_reports = []
|
||||
for sample in samples:
|
||||
permalink = sample.get("sample_webif_url", None)
|
||||
score = sample.get("sample_vti_score", 0)
|
||||
sandbox_report = {
|
||||
"permalink": permalink,
|
||||
"score": score,
|
||||
"sandbox-type": sandbox_type,
|
||||
"{}-sandbox".format(sandbox_type): sandbox,
|
||||
}
|
||||
sandbox_reports.append(sandbox_report)
|
||||
|
||||
# add child sample taxonomies if they have been added
|
||||
sandbox_reports.extend(
|
||||
self._sandbox_reports_for_samples(
|
||||
sample.get("sample_child_samples", [])
|
||||
)
|
||||
)
|
||||
return sandbox_reports
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
namespace = "VMRay"
|
||||
predicate = "Scan"
|
||||
|
||||
r = {
|
||||
'reports': []
|
||||
}
|
||||
|
||||
if raw.get('scanreport', None) and len(raw.get('scanreport').get('data')) > 0:
|
||||
for scan in raw.get('scanreport').get('data'):
|
||||
r['reports'].append({
|
||||
'score': scan.get('sample_score'),
|
||||
'sample_severity': scan.get('sample_severity'),
|
||||
'sample_last_reputation_severity': scan.get('sample_last_reputation_severity'),
|
||||
'url': scan.get('sample_webif_url')
|
||||
})
|
||||
|
||||
if len(r["reports"]) == 0:
|
||||
value = "No Scan"
|
||||
level = "info"
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
sandbox_reports = []
|
||||
samples = raw.get("samples", [])
|
||||
if len(samples) == 0:
|
||||
taxonomies.append(
|
||||
self.build_taxonomy("info", self._namespace, "None", "No Scan")
|
||||
)
|
||||
else:
|
||||
for s in r["reports"]:
|
||||
i = 1
|
||||
if s["sample_severity"] == "not_suspicious":
|
||||
level = "safe"
|
||||
elif s["sample_severity"] == "malicious":
|
||||
level = "malicious"
|
||||
else:
|
||||
level = "info"
|
||||
taxonomies.extend(self._taxonomies_for_samples(samples))
|
||||
sandbox_reports.extend(self._sandbox_reports_for_samples(samples))
|
||||
return {"taxonomies": taxonomies, "sandbox-reports": sandbox_reports}
|
||||
|
||||
if len(r["reports"]) > 1:
|
||||
value = "{}( from scan {})".format(s["score"], i)
|
||||
else:
|
||||
value = "{}".format(s["score"])
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
i += 1
|
||||
def _artifacts_for_samples(self, samples):
|
||||
artifacts = []
|
||||
for sample in samples:
|
||||
link = sample.get("sample_webif_url", None)
|
||||
iocs = sample.get("sample_iocs", {}).get("iocs", {})
|
||||
|
||||
return {"taxonomies": taxonomies}
|
||||
for (
|
||||
ioc_type,
|
||||
(ioc_payload_name, ioc_data_type),
|
||||
) in self._ioc_mapping.items():
|
||||
if ioc_type in iocs:
|
||||
for ioc_node in iocs[ioc_type]:
|
||||
severity = ioc_node.get("severity", "unknown")
|
||||
level = self._severity_mapping.get(severity, "info")
|
||||
tags = list(set((severity, level, ioc_node["type"])))
|
||||
payload = ioc_node[ioc_payload_name]
|
||||
|
||||
context_tags = []
|
||||
if "hashes" in ioc_node:
|
||||
for hash_node in ioc_node["hashes"]:
|
||||
if "sha256_hash" in hash_node:
|
||||
hash_value = hash_node["sha256_hash"]
|
||||
context_tags.append("sha256:{}".format(hash_value))
|
||||
artifacts.append(
|
||||
self.build_artifact(
|
||||
"hash", hash_value, message=link, tags=tags
|
||||
)
|
||||
)
|
||||
elif "operations" in ioc_node:
|
||||
for operation in ioc_node["operations"]:
|
||||
context_tags.append("operation:{}".format(operation))
|
||||
|
||||
tags.extend(set(context_tags))
|
||||
artifacts.append(
|
||||
self.build_artifact(
|
||||
ioc_data_type, payload, message=link, tags=tags
|
||||
)
|
||||
)
|
||||
|
||||
# add child samples if they have been added
|
||||
artifacts.extend(
|
||||
self._artifacts_for_samples(sample.get("sample_child_samples", []))
|
||||
)
|
||||
return artifacts
|
||||
|
||||
def artifacts(self, raw):
|
||||
return self._artifacts_for_samples(raw.get("samples", []))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
VMRayAnalyzer().run()
|
||||
|
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
@ -7,31 +8,40 @@ from requests import sessions
|
||||
|
||||
|
||||
class VMRayClientError(Exception):
|
||||
"""Parent class for all specific errors used by VMRayClient."""
|
||||
""" Parent class for all specific errors used by VMRayClient. """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VMRayAPIError(VMRayClientError):
|
||||
""" Raised in case the VMRay API returns an eror. """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownHashTypeError(VMRayClientError):
|
||||
"""Raised when length of hash as hex-string (or in bits) is not 32 (128 bit), 40 (160 bit) or 64 (256 bit)."""
|
||||
""" Raised when length of hash as hex-string (or in bits) is not 32 (128 bit), 40 (160 bit) or 64 (256 bit). """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BadResponseError(VMRayClientError):
|
||||
"""HTTP return status is not 200."""
|
||||
""" Raised in case the VMRay API returns a non-2xx status code. """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SampleFileNotFoundError(VMRayClientError):
|
||||
"""Sample file was not found under given filepath."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownSubmissionIdError(VMRayClientError):
|
||||
"""Thrown on invalid submission id or if id request fails."""
|
||||
pass
|
||||
def _filter_dict(data):
|
||||
return dict(filter(lambda item: item[1] is not None, data.items()))
|
||||
|
||||
|
||||
class VMRayClient:
|
||||
class VMRayClient(object):
|
||||
"""
|
||||
Client that connects to the VMRay api and allows searching for samples via hash and uploading a new sample to VMRay.
|
||||
|
||||
@ -39,106 +49,287 @@ class VMRayClient:
|
||||
:type url: str
|
||||
:param key: API Key
|
||||
:type key: str
|
||||
:param cert: Certificate for ssl validation in case the server certificate is self-signed. **Default: True**
|
||||
:type cert: [bool, str]
|
||||
:param reanalyze: Force reanalyzation. VMRay does not provide additional information if sample has already been
|
||||
uploaded, so this could be useful to obtain information. **Default: True**
|
||||
:type reanalyze: bool
|
||||
:param verify: Certificate for ssl validation in case the server certificate is self-signed. **Default: True**
|
||||
:type verify: [bool, str]
|
||||
"""
|
||||
def __init__(self, url, key, cert=True, reanalyze=True):
|
||||
|
||||
_submit_endpoint = "/rest/sample/submit"
|
||||
_submission_endpoint = "/rest/submission/{}"
|
||||
_sample_endpoint = "/rest/sample/{}"
|
||||
_sample_hash_endpoint = "/rest/sample/{t}/{h}"
|
||||
_sample_analyses_endpoint = "/rest/analysis/sample/{}"
|
||||
_sample_iocs_endpoint = "/rest/sample/{}/iocs"
|
||||
_sample_mitre_endpoint = "/rest/sample/{}/mitre_attack"
|
||||
_sample_threat_indicators_endpoint = "/rest/sample/{}/threat_indicators"
|
||||
_continuation_endpoint = "/rest/continuation/{}"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url,
|
||||
key,
|
||||
recursive_sample_limit=10,
|
||||
reanalyze=True,
|
||||
verify=True,
|
||||
**optional_parameters
|
||||
):
|
||||
self.url = url
|
||||
self.key = key
|
||||
if cert and os.path.isfile(cert):
|
||||
self.cert = cert
|
||||
else:
|
||||
self.cert = False
|
||||
self.reanalyze = reanalyze
|
||||
self.recursive_sample_limit = recursive_sample_limit
|
||||
self.headers = self._prepare_headers()
|
||||
self.session = sessions.Session()
|
||||
self.session.headers = self.headers
|
||||
self.session.verify = self.cert
|
||||
self.session.verify = verify
|
||||
self.optional_parameters = optional_parameters
|
||||
|
||||
def _prepare_headers(self):
|
||||
"""Prepares connection headers for authorization.
|
||||
|
||||
:returns: Dict with HTTP headers
|
||||
:rtype: dict"""
|
||||
headers = {'Authorization': 'api_key {}'.format(self.key)}
|
||||
headers = {"Authorization": "api_key {}".format(self.key)}
|
||||
return headers
|
||||
|
||||
def get_sample(self, samplehash):
|
||||
def _check_response(self, res):
|
||||
"""
|
||||
Downloads information about a sample using a given hash.
|
||||
Check the response code of the API and either return the results or raise an error.
|
||||
"""
|
||||
if res.status_code < 200 or res.status_code > 299:
|
||||
raise BadResponseError(
|
||||
"HTTP response code from VMRay indicates an error: {c} ({t})".format(
|
||||
c=res.status_code, t=res.text
|
||||
)
|
||||
)
|
||||
else:
|
||||
response_json = res.json()
|
||||
if response_json.get("result") == "ok":
|
||||
data = response_json.get("data", [])
|
||||
if "continuation_id" in response_json:
|
||||
result = self.session.get(
|
||||
url="{}{}".format(
|
||||
self.url,
|
||||
self._continuation_endpoint.format(
|
||||
response_json["continuation_id"]
|
||||
),
|
||||
)
|
||||
)
|
||||
data.extend(self._check_response(result))
|
||||
return data
|
||||
else:
|
||||
error_content = "Error from VMRay via API: {}"
|
||||
if "data" in response_json:
|
||||
error_content = error_content.format(
|
||||
"; ".join(response_json["data"]["errors"])
|
||||
)
|
||||
elif "error_msg" in response_json:
|
||||
error_content = error_content.format(response_json["error_msg"])
|
||||
else:
|
||||
error_content = error_content.format("Unspecified error occurred")
|
||||
raise VMRayAPIError(error_content)
|
||||
|
||||
def submit_url_sample(
|
||||
self, url_sample, tags=["TheHive"], shareable=False, user_config={}
|
||||
):
|
||||
"""
|
||||
Uploads a new URL sample to VMRay api.
|
||||
|
||||
:param url_sample: url to be analyzed
|
||||
:type url_sample: str
|
||||
:param tags: List of tags to apply to the sample
|
||||
:type tags: list(str)
|
||||
:returns: List of submissions and samples
|
||||
:rtype: list(dict)
|
||||
"""
|
||||
params = _filter_dict(self.optional_parameters)
|
||||
params.update(
|
||||
{
|
||||
"sample_url": url_sample,
|
||||
"reanalyze": self.reanalyze,
|
||||
"shareable": shareable,
|
||||
"max_recursive_samples": self.recursive_sample_limit,
|
||||
}
|
||||
)
|
||||
if tags:
|
||||
params["tags"] = ",".join(filter(None, tags))
|
||||
|
||||
user_config = _filter_dict(user_config)
|
||||
if user_config:
|
||||
params["user_config"] = json.dumps(user_config)
|
||||
|
||||
return self._check_response(
|
||||
self.session.post(
|
||||
url="{}{}".format(self.url, self._submit_endpoint),
|
||||
params=params,
|
||||
)
|
||||
)
|
||||
|
||||
def submit_file_sample(
|
||||
self, file_path, file_name, tags=["TheHive"], shareable=False, user_config={}
|
||||
):
|
||||
"""
|
||||
Uploads a new file sample to VMRay API. Filename gets sent base64 encoded.
|
||||
|
||||
:param file_path: path to sample
|
||||
:type file_path: str
|
||||
:param file_name: filename of the original file
|
||||
:type file_name: str
|
||||
:param tags: List of tags to apply to the sample
|
||||
:type tags: list(str)
|
||||
:returns: List of submissions and samples
|
||||
:rtype: list(dict)
|
||||
"""
|
||||
params = _filter_dict(self.optional_parameters)
|
||||
params.update(
|
||||
{
|
||||
"sample_filename_b64enc": base64.b64encode(file_name.encode("utf-8")),
|
||||
"reanalyze": self.reanalyze,
|
||||
"shareable": shareable,
|
||||
"max_recursive_samples": self.recursive_sample_limit,
|
||||
}
|
||||
)
|
||||
if tags:
|
||||
params["tags"] = ",".join(filter(None, tags))
|
||||
|
||||
user_config = _filter_dict(user_config)
|
||||
if user_config:
|
||||
params["user_config"] = json.dumps(user_config)
|
||||
|
||||
if os.path.isfile(file_path):
|
||||
return self._check_response(
|
||||
self.session.post(
|
||||
url="{}{}".format(self.url, self._submit_endpoint),
|
||||
files=[("sample_file", open(file_path, mode="rb"))],
|
||||
params=params,
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise SampleFileNotFoundError("Given sample file was not found.")
|
||||
|
||||
def get_sample_threat_indicators(self, sample_id):
|
||||
"""
|
||||
Download Threat Indicators for a given sample id.
|
||||
|
||||
:param sample_id: ID of the sample
|
||||
:type sample_id: int
|
||||
:returns: Dictionary of Threat Indicators
|
||||
:rtype: dict
|
||||
"""
|
||||
return self._check_response(
|
||||
self.session.get(
|
||||
url="{}{}".format(
|
||||
self.url, self._sample_threat_indicators_endpoint.format(sample_id)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def get_sample_mitre_attack(self, sample_id):
|
||||
"""
|
||||
Download MITRE ATT&CK(tm) information for a given sample id.
|
||||
|
||||
:param sample_id: ID of the sample
|
||||
:type sample_id: int
|
||||
:returns: Dictionary of MITRE ATT&CK(tm) information
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return self._check_response(
|
||||
self.session.get(
|
||||
url="{}{}".format(
|
||||
self.url, self._sample_mitre_endpoint.format(sample_id)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def get_sample(self, sample_id):
|
||||
"""
|
||||
Query sample with a given sample id.
|
||||
|
||||
:param sample_id: ID of the sample
|
||||
:type sample_id: int
|
||||
:returns: Dictionary of Samples
|
||||
:rtype: dict
|
||||
"""
|
||||
return self._check_response(
|
||||
self.session.get(
|
||||
url="{}{}".format(self.url, self._sample_endpoint.format(sample_id)),
|
||||
)
|
||||
)
|
||||
|
||||
def get_samples_by_hash(self, sample_hash):
|
||||
"""
|
||||
Downloads information about a all samplse matching the given hash.
|
||||
|
||||
:param samplehash: hash to search for. Has to be either md5, sha1 or sha256
|
||||
:type samplehash: str
|
||||
:returns: Dictionary of results
|
||||
:returns: List of samples
|
||||
:rtype: list(dict)
|
||||
"""
|
||||
if len(sample_hash) == 32: # MD5
|
||||
hash_type = "md5"
|
||||
elif len(sample_hash) == 40: # SHA1
|
||||
hash_type = "sha1"
|
||||
elif len(sample_hash) == 64: # SHA256
|
||||
hash_type = "sha256"
|
||||
else:
|
||||
raise UnknownHashTypeError("Sample hash has an unknown length.")
|
||||
|
||||
return self._check_response(
|
||||
self.session.get(
|
||||
url="{}{}".format(
|
||||
self.url,
|
||||
self._sample_hash_endpoint.format(t=hash_type, h=sample_hash),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def get_sample_iocs(self, sample_id):
|
||||
"""
|
||||
Query IOCs for a given sample id.
|
||||
|
||||
:param sample_id: ID of the sample
|
||||
:type sample_id: int
|
||||
:returns: Dictionary of IOCs
|
||||
:rtype: dict
|
||||
"""
|
||||
apiurl = '/rest/sample/'
|
||||
if len(samplehash) == 32: # MD5
|
||||
apiurl += 'md5/'
|
||||
elif len(samplehash) == 40: # SHA1
|
||||
apiurl += 'sha1/'
|
||||
elif len(samplehash) == 64: # SHA256
|
||||
apiurl += 'sha256/'
|
||||
else:
|
||||
raise UnknownHashTypeError('Sample hash has an unknown length.')
|
||||
return self._check_response(
|
||||
self.session.get(
|
||||
url="{}{}".format(
|
||||
self.url, self._sample_iocs_endpoint.format(sample_id)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
res = self.session.get(self.url + apiurl + samplehash)
|
||||
if res.status_code == 200:
|
||||
return json.loads(res.text)
|
||||
else:
|
||||
raise BadResponseError('Response from VMRay was not HTTP 200.'
|
||||
' Responsecode: {}; Text: {}'.format(res.status_code, res.text))
|
||||
|
||||
def submit_sample(self, filepath, filename, tags=['TheHive']):
|
||||
def update_submission(self, submission_id):
|
||||
"""
|
||||
Uploads a new sample to VMRay api. Filename gets sent base64 encoded.
|
||||
Queries the current state of a submission.
|
||||
|
||||
:param filepath: path to sample
|
||||
:type filepath: str
|
||||
:param filename: filename of the original file
|
||||
:type filename: str
|
||||
:param tags: List of tags to apply to the sample
|
||||
:type tags: list(str)
|
||||
:returns: Dictionary of results
|
||||
:param submission_id: ID of the submission
|
||||
:type submission_id: int
|
||||
:returns: Dictionary representing the submission
|
||||
:rtype: dict
|
||||
"""
|
||||
apiurl = '/rest/sample/submit?sample_file'
|
||||
params = {'sample_filename_b64enc': base64.b64encode(filename.encode('utf-8')),
|
||||
'reanalyze': self.reanalyze}
|
||||
if tags:
|
||||
params['tags'] = ','.join(tags)
|
||||
return self._check_response(
|
||||
self.session.get(
|
||||
"{}{}".format(self.url, self._submission_endpoint.format(submission_id))
|
||||
)
|
||||
)
|
||||
|
||||
if os.path.isfile(filepath):
|
||||
res = self.session.post(url=self.url + apiurl,
|
||||
files=[('sample_file', open(filepath, mode='rb'))],
|
||||
params=params)
|
||||
if res.status_code == 200:
|
||||
return json.loads(res.text)
|
||||
else:
|
||||
raise BadResponseError('Response from VMRay was not HTTP 200.'
|
||||
' Responsecode: {}; Text: {}'.format(res.status_code, res.text))
|
||||
else:
|
||||
raise SampleFileNotFoundError('Given sample file was not found.')
|
||||
|
||||
def query_job_status(self, submissionid):
|
||||
"""
|
||||
Queries vmray to check id a job was
|
||||
|
||||
:param submissionid: ID of the job/submission
|
||||
:type submissionid: int
|
||||
:returns: True if job finished, false if not
|
||||
:rtype: bool
|
||||
def get_sample_analyses(self, sample_id):
|
||||
"""
|
||||
Queries analyses about a sample using a given sample id.
|
||||
|
||||
apiurl = '/rest/submission/'
|
||||
result = self.session.get('{}{}{}'.format(self.url, apiurl, submissionid))
|
||||
if result.status_code == 200:
|
||||
submission_info = json.loads(result.text)
|
||||
if submission_info.get('data', {}).get('submission_finished', False): # Or something like that
|
||||
return True
|
||||
else:
|
||||
raise UnknownSubmissionIdError('Submission id seems invalid, response was not HTTP 200.')
|
||||
return False
|
||||
:param sample_id: ID of the sample
|
||||
:type sample_id: int
|
||||
:returns: List of analyses
|
||||
:rtype: list(dict)
|
||||
"""
|
||||
return self._check_response(
|
||||
self.session.get(
|
||||
"{}{}".format(
|
||||
self.url, self._sample_analyses_endpoint.format(sample_id)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
50
analyzers/Vulners/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Vulners-analyzer
|
||||
|
||||
This analyzer consists of 2 parts.
|
||||
1. **Vulners_IOC**: As a result of collaboration between Vulners and RST Threat Feed, the idea was to send IOC analysis results through theHive analyzer: blog post
|
||||
2. **Vulners_CVE**: Vulners have a strong vulnerability database. This data is useful if:
|
||||
"if the case (incident) is related to the exploitation of a vulnerability, then the analyst (manually / automatically) can add it to observables and quickly get all the basic information on it in order to continue analyzing the case."
|
||||
|
||||
Vulners API key required.
|
||||
|
||||
## Setting up analyzer
|
||||
|
||||
* copy the folders "Vulners" analyzer & "Vulners" into your Cortex analyzer path
|
||||
* install necessary python modules from the requirements.txt (**pip install -r requirements.txt**)
|
||||
* restart Cortex to initialize the new Responder "**systemctl restart cortex**"
|
||||
|
||||
Get your Vulners api key: ![Vulners API](assets/vulners_api.png)
|
||||
|
||||
Add your Vulners API in Cortex settings: ![API key in Cortex](assets/Cortex_settings.png)
|
||||
|
||||
## Add Observable type in TheHive
|
||||
|
||||
By default theHive does not have a "cve" type to be observables, so we have to add it to Administrator Settings:
|
||||
|
||||
![add observable](assets/theHive_add_cve.PNG)
|
||||
|
||||
## Run the Analyzer in TheHive
|
||||
|
||||
####Network IOCs:
|
||||
|
||||
Short template:
|
||||
|
||||
![Short IOC template](assets/ioc_short_template.png)
|
||||
|
||||
Long template:
|
||||
|
||||
![Long IOC template](assets/ioc_long_template.png)
|
||||
|
||||
|
||||
![Long_IOC_threat_template](assets/ioc_with_malware_family.PNG)
|
||||
|
||||
|
||||
####Vulnerabilities:
|
||||
|
||||
Short template:
|
||||
|
||||
![Short CVE template](assets/cve_short_template.png)
|
||||
|
||||
Long template:
|
||||
|
||||
![Long CVE template](assets/cve_long_template.gif)
|
42
analyzers/Vulners/Vulners_CVE.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "Vulners_CVE",
|
||||
"version": "1.0",
|
||||
"author": "Dmitry Uchakin, Vulners team",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Get information about CVE from powerful Vulners database.",
|
||||
"dataTypeList": ["cve"],
|
||||
"command": "Vulners/vulners_analyzer.py",
|
||||
"baseConfig": "Vulners",
|
||||
"config": {
|
||||
"service": "vulnerability"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "key",
|
||||
"description": "API key for Vulners",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"registration_required": true,
|
||||
"subscription_required": true,
|
||||
"free_subscription": true,
|
||||
"service_homepage": "https://vulners.com",
|
||||
"service_logo": {"path":"assets/vulners_logo.png", "caption": "logo"},
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "assets/theHive_add_cve",
|
||||
"caption": "Add new IOC type in theHive observables"
|
||||
},
|
||||
{
|
||||
"path": "assets/cve_long_template.gif",
|
||||
"caption": "Long template for CVE"
|
||||
},
|
||||
{
|
||||
"path": "assets/cve_short_template",
|
||||
"caption": "Short template for CVE"
|
||||
}
|
||||
]
|
||||
}
|
50
analyzers/Vulners/Vulners_IOC.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Vulners_IOC",
|
||||
"version": "1.0",
|
||||
"author": "Dmitry Uchakin, Vulners team",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Get information from the RST Threat Feed, which integrated with Vulners, for a domain, url or an IP address.",
|
||||
"dataTypeList": ["url", "domain", "ip"],
|
||||
"command": "Vulners/vulners_analyzer.py",
|
||||
"baseConfig": "Vulners",
|
||||
"config": {
|
||||
"service": "ioc"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "key",
|
||||
"description": "API key for Vulners",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"registration_required": true,
|
||||
"subscription_required": true,
|
||||
"free_subscription": true,
|
||||
"service_homepage": "https://vulners.com",
|
||||
"service_logo": {"path":"assets/vulners_logo.png", "caption": "logo"},
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "assets/vulners_api.png",
|
||||
"caption": "Vulners API key for analyzer"
|
||||
},
|
||||
{
|
||||
"path": "assets/Cortex_settings.png",
|
||||
"caption": "Paste Vulners API key in Cortex settings"
|
||||
},
|
||||
{
|
||||
"path": "assets/ioc_long_template.png",
|
||||
"caption": "Long template for network IOCs (ip, url, domain)"
|
||||
},
|
||||
{
|
||||
"path": "assets/ioc_short_template.png",
|
||||
"caption": "Short template for network IOCs (ip, url, domain)"
|
||||
},
|
||||
{
|
||||
"path": "assets/assets/ioc_with_malware_family.png",
|
||||
"caption": "Full template with malware family"
|
||||
}
|
||||
]
|
||||
}
|
BIN
analyzers/Vulners/assets/Cortex_settings.PNG
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
analyzers/Vulners/assets/cve_long_template.gif
Normal file
After Width: | Height: | Size: 12 MiB |
BIN
analyzers/Vulners/assets/cve_short_template.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
analyzers/Vulners/assets/ioc_long_template.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
analyzers/Vulners/assets/ioc_short_template.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
analyzers/Vulners/assets/ioc_with_malware_family.PNG
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
analyzers/Vulners/assets/theHive_add_cve.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
analyzers/Vulners/assets/vulners_api.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
analyzers/Vulners/assets/vulners_logo.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
2
analyzers/Vulners/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
cortexutils
|
||||
vulners
|
135
analyzers/Vulners/vulners_analyzer.py
Normal file
@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
from cortexutils.analyzer import Analyzer
|
||||
import vulners
|
||||
|
||||
|
||||
class VulnersAnalyzer(Analyzer):
|
||||
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
self.service = self.get_param('config.service', None, 'Service parameter is missing')
|
||||
self.api_key = self.get_param('config.key', None, 'Missing vulners api key')
|
||||
proxies = {
|
||||
"https": self.get_param("config.proxy_https"),
|
||||
"http": self.get_param("config.proxy_http"),
|
||||
}
|
||||
self.vulners = vulners.Vulners(api_key=self.api_key, proxies=proxies)
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
namespace = "Vulners"
|
||||
if self.service == 'ioc':
|
||||
predicate = "IOC"
|
||||
if raw['results']:
|
||||
tags = ', '.join(
|
||||
set([', '.join(result['tags']) for result in raw['results']])
|
||||
)
|
||||
level = 'malicious'
|
||||
value = f"Finded IOCs: {len(raw['results'])} / tags: {tags}"
|
||||
else:
|
||||
level = 'info'
|
||||
value = 'No results'
|
||||
|
||||
if self.service == 'vulnerability':
|
||||
predicate = "CVE"
|
||||
if not raw['exploits']:
|
||||
level = 'suspicious'
|
||||
value = f"CVSS score: {raw['cvss']['score']} / Vulners score: {raw['vulners_AI']} / No exploits"
|
||||
else:
|
||||
level = 'malicious'
|
||||
value = f"CVSS score: {raw['cvss']['score']} / Vulners score: {raw['vulners_AI']} / Exploits {len(raw['exploits'])}"
|
||||
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
def run(self):
|
||||
if self.service == 'ioc':
|
||||
if self.data_type in ['ip', 'domain', 'url']:
|
||||
data = self.get_param('data', None, 'Data is missing')
|
||||
all_short_results = self.vulners.search(
|
||||
f'type:rst AND iocType:{self.data_type} AND {self.data_type}:"{data}"')
|
||||
|
||||
results = []
|
||||
|
||||
if all_short_results:
|
||||
if all_short_results[0]['type'] == 'rst' or 'type' in all_short_results[0]:
|
||||
|
||||
full_documents_info = self.vulners.documentList(
|
||||
[doc_id['id'] for doc_id in all_short_results], fields=["*"])
|
||||
|
||||
for document_results in full_documents_info:
|
||||
info_from_document = full_documents_info[f'{document_results}']
|
||||
|
||||
ioc_report = {
|
||||
'service': self.service,
|
||||
'first_seen': info_from_document['published'],
|
||||
'last_seen': info_from_document['lastseen'],
|
||||
'tags': info_from_document['tags'],
|
||||
'ioc_score': info_from_document['iocScore']['ioc_total'],
|
||||
'ioc_url': info_from_document['id'],
|
||||
'fp_descr': info_from_document['fp']['descr']
|
||||
}
|
||||
if self.data_type == 'ip':
|
||||
ioc_report['ioc_result'] = info_from_document['ip']
|
||||
ioc_report['geo_info'] = info_from_document['geodata']
|
||||
ioc_report['asn_info'] = info_from_document['asn']
|
||||
elif self.data_type == 'url':
|
||||
ioc_report['ioc_result'] = info_from_document['url']
|
||||
elif self.data_type == 'domain':
|
||||
ioc_report['ioc_result'] = info_from_document['domain']
|
||||
|
||||
if 'threat' in info_from_document:
|
||||
ioc_report['threat_family'] = info_from_document['threat']
|
||||
|
||||
results.append(ioc_report)
|
||||
|
||||
self.report({'results': results})
|
||||
else:
|
||||
self.report({'results': 'No data found'})
|
||||
else:
|
||||
self.error('Invalid data type')
|
||||
|
||||
if self.service == 'vulnerability':
|
||||
if self.data_type == 'cve':
|
||||
data = self.get_param('data', None, 'Data is missing')
|
||||
cve_info = self.vulners.document(data, fields=["*"])
|
||||
cve_exploits = self.vulners.searchExploit(data)
|
||||
full_cve_info = {}
|
||||
|
||||
if cve_info:
|
||||
full_cve_info = {
|
||||
'service': self.service,
|
||||
'title': cve_info['title'],
|
||||
'published': cve_info['published'],
|
||||
'modified': cve_info['modified'],
|
||||
'cvss3': cve_info['cvss3'],
|
||||
'cvss2': cve_info['cvss2'],
|
||||
'cvss': cve_info['cvss'],
|
||||
'vulners_AI': cve_info['enchantments']['vulnersScore'],
|
||||
'cwe': cve_info['cwe'],
|
||||
'description': cve_info['description'],
|
||||
'affectedSoftware': cve_info['affectedSoftware']
|
||||
}
|
||||
else:
|
||||
self.report({'result': 'No data for specified CVE was found'})
|
||||
|
||||
if cve_exploits:
|
||||
full_exploit_info = []
|
||||
for exploit in cve_exploits:
|
||||
full_exploit_info.append({
|
||||
'title': exploit['title'],
|
||||
'published': exploit['published'],
|
||||
'url': exploit['vhref']
|
||||
})
|
||||
|
||||
full_cve_info['exploits'] = full_exploit_info
|
||||
else:
|
||||
full_cve_info['exploits'] = False
|
||||
|
||||
self.report(full_cve_info)
|
||||
else:
|
||||
self.error('Invalid data type')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
VulnersAnalyzer().run()
|
@ -9,49 +9,93 @@ import traceback
|
||||
from cortexutils.responder import Responder
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
|
||||
class FalconCustomIOC(Responder):
|
||||
def __init__(self):
|
||||
Responder.__init__(self)
|
||||
self.falconapi_url = self.get_param(
|
||||
'config.falconapi_url', None, "Falcon API URL (e.g.:https://falconapi.crowdstrike.com/indicators/entities/iocs/v1)")
|
||||
"config.falconapi_url",
|
||||
None,
|
||||
"Falcon API URL (e.g.:https://falconapi.crowdstrike.com/indicators/entities/iocs/v1)",
|
||||
)
|
||||
self.apiuser = self.get_param(
|
||||
'config.falconapi_user', None, "Falcon query api key missing")
|
||||
"config.falconapi_user", None, "Falcon query api key missing"
|
||||
)
|
||||
self.apikey = self.get_param(
|
||||
'config.falconapi_key', None, "Falcon query api key missing")
|
||||
"config.falconapi_key", None, "Falcon query api key missing"
|
||||
)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
Responder.run(self)
|
||||
ioctypes = {"hash": "sha256", "sha256": "sha256", "md5": "md5", "sha1": "sha1",
|
||||
"ip": "ipv4", "ip6": "ipv6", "ipv6": "ipv6", "domain": "domain", "url": "domain"}
|
||||
data_type = self.get_param('data.dataType')
|
||||
if not data_type in ioctypes:
|
||||
self.error("Unsupported IOC type")
|
||||
Responder.run(self)
|
||||
ioctypes = {
|
||||
"hash": "sha256",
|
||||
"sha256": "sha256",
|
||||
"md5": "md5",
|
||||
"sha1": "sha1",
|
||||
"ip": "ipv4",
|
||||
"ip6": "ipv6",
|
||||
"ipv6": "ipv6",
|
||||
"domain": "domain",
|
||||
"url": "domain",
|
||||
}
|
||||
data_type = self.get_param("data.dataType")
|
||||
if not data_type in ioctypes:
|
||||
self.error("Unsupported IOC type")
|
||||
return
|
||||
ioc = self.get_param("data.data", None, "No IOC provided")
|
||||
if data_type == "url":
|
||||
match = re.match(r"(http:\/\/|https:\/\/)?([\w\d\-\.]{0,256}).*", ioc)
|
||||
if match is None or match.group(2) is None:
|
||||
self.error("Could not parse domain from URL")
|
||||
return
|
||||
ioc = self.get_param('data.data', None, 'No IOC provided')
|
||||
if data_type == "url":
|
||||
match = re.match(r"(http:\/\/|https:\/\/)?([\w\d\-\.]{0,256}).*", ioc)
|
||||
if match is None or match.group(2) is None:
|
||||
self.error("Could not parse domain from URL")
|
||||
return
|
||||
else:
|
||||
ioc=match.group(2)
|
||||
description = self.get_param('data.case.title',None,"Can't get case title")
|
||||
description = str(description).encode('utf-8')[:128]
|
||||
postdata=json.dumps([{"type": ioctypes[data_type], "value": ioc.strip(), "policy": "detect", "description": description, "share_level": "red", "source": "Cortex - FalconCustomIOC ["+description+"]", "expiration_days": 30}])
|
||||
response=requests.post(self.falconapi_url,data=postdata,headers={"Content-Type":"application/json"},auth=HTTPBasicAuth(self.apiuser,self.apikey))
|
||||
json_response = json.loads(response.text)
|
||||
if json_response["errors"]:
|
||||
self.error(str(json_response["errors"]))
|
||||
return
|
||||
else:
|
||||
self.report({'message': ioc+" Submitted to Crowdstrike Falcon custom IOC api","api_response":json_response})
|
||||
except Exception as ex:
|
||||
self.error(traceback.format_exc())
|
||||
|
||||
else:
|
||||
ioc = match.group(2)
|
||||
description = self.get_param(
|
||||
"data.case.title", None, "Can't get case title"
|
||||
)
|
||||
description = str(description).encode("utf-8")[:128]
|
||||
postdata = json.dumps(
|
||||
[
|
||||
{
|
||||
"type": ioctypes[data_type],
|
||||
"value": ioc.strip(),
|
||||
"policy": "detect",
|
||||
"description": description,
|
||||
"share_level": "red",
|
||||
"source": "Cortex - FalconCustomIOC [" + description + "]",
|
||||
"expiration_days": 30,
|
||||
}
|
||||
]
|
||||
)
|
||||
response = requests.post(
|
||||
self.falconapi_url,
|
||||
data=postdata,
|
||||
headers={"Content-Type": "application/json"},
|
||||
auth=HTTPBasicAuth(self.apiuser, self.apikey),
|
||||
)
|
||||
json_response = json.loads(response.text)
|
||||
if json_response["errors"]:
|
||||
self.error(str(json_response["errors"]))
|
||||
return
|
||||
else:
|
||||
self.report(
|
||||
{
|
||||
"message": ioc
|
||||
+ " Submitted to Crowdstrike Falcon custom IOC api",
|
||||
"api_response": json_response,
|
||||
}
|
||||
)
|
||||
except Exception as ex:
|
||||
self.error(traceback.format_exc())
|
||||
|
||||
def operations(self, raw):
|
||||
return [self.build_operation('AddTagToArtifact', tag='CrowdStrike:Custom IOC Uploaded')]
|
||||
if __name__ == '__main__':
|
||||
FalconCustomIOC().run()
|
||||
return [
|
||||
self.build_operation(
|
||||
"AddTagToArtifact", tag="CrowdStrike:Custom IOC Uploaded"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
FalconCustomIOC().run()
|
||||
|
@ -40,7 +40,7 @@ class Mailer(Responder):
|
||||
tags = self.get_param(
|
||||
"data.tags", None, "recipient address not found in tags"
|
||||
)
|
||||
mail_tags = [t[5:] for t in tags if t.startswith("mail:")]
|
||||
mail_tags = [t[5:] for t in tags if t.startswith("mail=") or t.startswith("mail:")]
|
||||
if mail_tags:
|
||||
mail_to = mail_tags.pop()
|
||||
else:
|
||||
@ -51,6 +51,8 @@ class Mailer(Responder):
|
||||
descr_array = description.splitlines()
|
||||
if "mailto:" in descr_array[0]:
|
||||
mail_to = descr_array[0].replace("mailto:", "").strip()
|
||||
elif "mailto=" in descr_array[0]:
|
||||
mail_to = descr_array[0].replace("mailto=", "").strip()
|
||||
else:
|
||||
self.error("recipient address not found in description")
|
||||
# Set rest of description as body
|
||||
|
14
responders/Minemeld/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
### Palo Alto Minemeld
|
||||
|
||||
This responder sends observables you select to a [Palo Alto Minemeld](https://www.paloaltonetworks.com/products/secure-the-network/subscriptions/minemeld) instance.
|
||||
|
||||
#### Requirements
|
||||
The following options are required in the Palo Alto Minemeld Responder configuration:
|
||||
|
||||
- `minemeld_url` : URL of the Minemeld instance to which you will be posting indicators
|
||||
- `minemeld_user`: user accessing the Minemeld instance
|
||||
- `minemeld_password`: password for the user accessing the Minemeld instance
|
||||
- `minemeld_indicator_list`: name of Minemeld indicator list (already created in Minemeld)
|
||||
- `minemeld_share_level`: share level for indicators (defaults to `red`)
|
||||
- `minemeld_confidence`: confidence level for indicators (defaults to `100`)
|
||||
- `minemeld_ttl`: TTL for indicators (defaults to `86400` seconds)
|
BIN
responders/Minemeld/assets/MM-logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
@ -1,69 +1,85 @@
|
||||
{
|
||||
"name": "Minemeld",
|
||||
"version": "1.0",
|
||||
"author": "Wes Lambert, Security Onion Solutions",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Submit indicator to Minemeld",
|
||||
"dataTypeList": ["thehive:case_artifact"],
|
||||
"command": "Minemeld/minemeld.py",
|
||||
"baseConfig": "Minemeld",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "minemeld_url",
|
||||
"description": "URL for Minemeld instance",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "https://x.x.x.x"
|
||||
"name": "Minemeld",
|
||||
"version": "1.0",
|
||||
"author": "Wes Lambert, Security Onion Solutions",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Submit indicator to Minemeld",
|
||||
"dataTypeList": [
|
||||
"thehive:case_artifact"
|
||||
],
|
||||
"command": "Minemeld/minemeld.py",
|
||||
"baseConfig": "Minemeld",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "minemeld_url",
|
||||
"description": "URL for Minemeld instance",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "https://x.x.x.x"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_user",
|
||||
"description": "User for Minemeld",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "apiuser"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_password",
|
||||
"description": "Password for Minemeld",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "password"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_indicator_list",
|
||||
"description": "Name of indicator list to which indicators will be added",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "my_block_list"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_share_level",
|
||||
"description": "Share level for indicator",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "red"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_confidence",
|
||||
"description": "Confidence level for indicator",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "100"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_ttl",
|
||||
"description": "TTL for indicator",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "86400"
|
||||
}
|
||||
],
|
||||
"registration_required": false,
|
||||
"subscription_required": false,
|
||||
"free_subscription": false,
|
||||
"service_homepage": "https://github.com/PaloAltoNetworks/minemeld",
|
||||
"service_logo": {
|
||||
"path": "assets/MM-logo.png",
|
||||
"caption": "logo"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_user",
|
||||
"description": "User for Minemeld",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "apiuser"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_password",
|
||||
"description": "Password for Minemeld",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "password"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_indicator_list",
|
||||
"description": "Name of indicator list to which indicators will be added",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "my_block_list"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_share_level",
|
||||
"description": "Share level for indicator",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "red"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_confidence",
|
||||
"description": "Confidence level for indicator",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "100"
|
||||
},
|
||||
{
|
||||
"name": "minemeld_ttl",
|
||||
"description": "TTL for indicator",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "86400"
|
||||
}
|
||||
]
|
||||
}
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "",
|
||||
"caption": ""
|
||||
}
|
||||
]
|
||||
}
|
@ -40,7 +40,7 @@ class Minemeld(Responder):
|
||||
elif self.observable_type == "url":
|
||||
indicator_type = "URL"
|
||||
elif self.observable_type == "domain":
|
||||
indicator_type = "Domain"
|
||||
indicator_type = "domain"
|
||||
|
||||
# Check for comment
|
||||
if self.observable_description == "":
|
||||
|
2
responders/Shuffle/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
cortexutils
|
||||
requests
|
35
responders/Shuffle/shuffle.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Shuffle",
|
||||
"version": "1.0",
|
||||
"author": "@frikkylikeme",
|
||||
"url": "https://github.com/frikky/shuffle",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Execute a workflow in Shuffle",
|
||||
"dataTypeList": ["thehive:case", "thehive:alert"],
|
||||
"command": "Shuffle/shuffle.py",
|
||||
"baseConfig": "Shuffle",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "url",
|
||||
"description": "The URL to your shuffle instance",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"defaultValue": "https://shuffler.io"
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "The API key to your Shuffle user",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "workflow_id",
|
||||
"description": "The ID of the workflow to execute",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
27
responders/Shuffle/shuffle.py
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
from cortexutils.responder import Responder
|
||||
import requests
|
||||
|
||||
class Shuffle(Responder):
|
||||
def __init__(self):
|
||||
Responder.__init__(self)
|
||||
self.api_key = self.get_param("config.api_key", "")
|
||||
self.url = self.get_param("config.url", "")
|
||||
self.workflow_id = self.get_param("config.workflow_id", "")
|
||||
|
||||
def run(self):
|
||||
Responder.run(self)
|
||||
|
||||
parsed_url = "%s/api/v1/workflows/%s/execute" % (self.url, self.workflow_id)
|
||||
headers = {
|
||||
"Authorization": "Bearer %s" % self.api_key
|
||||
}
|
||||
r = requests.post(parsed_url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
self.report({"Message": "Executed workflow"})
|
||||
else:
|
||||
self.error(r.status_code)
|
||||
|
||||
if __name__ == '__main__':
|
||||
Shuffle().run()
|
||||
|
8
responders/VirustotalDownloader/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
### VirusTotalDownloader
|
||||
|
||||
This responder comes in only 1 flavor that lets you download a sample of malware from VirusTotal by submitting a hash.
|
||||
|
||||
#### Requirements
|
||||
|
||||
This responder need a valid Premium API key from VirusTotal as the `virustotal_apikey` parameter in the configuration.
|
||||
To add the sample in Observables in TheHive, the responder also requires the URL of TheHive as the `thehive_url` paramenter and a valid API key as the `thehive_apikey` parameter.
|
@ -1,34 +1,50 @@
|
||||
{
|
||||
"name": "Virustotal_Downloader",
|
||||
"version": "0.1",
|
||||
"author": "Mario Henkel @hariomenkel",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Download a file from Virustotal by its hash",
|
||||
"dataTypeList": ["thehive:case_artifact"],
|
||||
"command": "VirustotalDownloader/VirustotalDownloader.py",
|
||||
"baseConfig": "VirustotalDownloader",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "virustotal_apikey",
|
||||
"description": "Virustotal API key which should be used to download files",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
"name": "Virustotal_Downloader",
|
||||
"version": "0.1",
|
||||
"author": "Mario Henkel @hariomenkel",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Download a file from Virustotal by its hash",
|
||||
"dataTypeList": [
|
||||
"thehive:case_artifact"
|
||||
],
|
||||
"command": "VirustotalDownloader/VirustotalDownloader.py",
|
||||
"baseConfig": "VirustotalDownloader",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "virustotal_apikey",
|
||||
"description": "Virustotal API key which should be used to download files",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "thehive_url",
|
||||
"description": "URL pointing to your TheHive installation, e.g. 'http://127.0.0.1:9000'",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "thehive_apikey",
|
||||
"description": "TheHive API key which is used to add the downloaded file back to the alert/case",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"registration_required": true,
|
||||
"subscription_required": true,
|
||||
"free_subscription": false,
|
||||
"service_homepage": "https://virustotal.com",
|
||||
"service_logo": {
|
||||
"path": "",
|
||||
"caption": "logo"
|
||||
},
|
||||
{
|
||||
"name": "thehive_url",
|
||||
"description": "URL pointing to your TheHive installation, e.g. 'http://127.0.0.1:9000'",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "thehive_apikey",
|
||||
"description": "TheHive API key which is used to add the downloaded file back to the alert/case",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"screenshots": [
|
||||
{
|
||||
"path": "",
|
||||
"caption": ""
|
||||
}
|
||||
]
|
||||
}
|
@ -11,23 +11,33 @@ import filetype
|
||||
from thehive4py.api import TheHiveApi
|
||||
from thehive4py.models import Case, CaseObservable
|
||||
|
||||
|
||||
class VirustotalDownloader(Responder):
|
||||
def __init__(self):
|
||||
Responder.__init__(self)
|
||||
self.virustotal_apikey = self.get_param('config.virustotal_apikey', None, "Virustotal API key missing!")
|
||||
self.thehive_url = self.get_param('config.thehive_url', None, "TheHive URL missing!")
|
||||
self.thehive_apikey = self.get_param('config.thehive_apikey', None, "TheHive API key missing!")
|
||||
self.virustotal_apikey = self.get_param(
|
||||
"config.virustotal_apikey", None, "Virustotal API key missing!"
|
||||
)
|
||||
self.thehive_url = self.get_param(
|
||||
"config.thehive_url", None, "TheHive URL missing!"
|
||||
)
|
||||
self.thehive_apikey = self.get_param(
|
||||
"config.thehive_apikey", None, "TheHive API key missing!"
|
||||
)
|
||||
|
||||
def run(self):
|
||||
Responder.run(self)
|
||||
|
||||
data_type = self.get_param('data.dataType')
|
||||
case_id = self.get_param('data._parent')
|
||||
data_type = self.get_param("data.dataType")
|
||||
case_id = self.get_param("data.case._id")
|
||||
ioc_types = ["hash"]
|
||||
|
||||
if data_type in ioc_types:
|
||||
url = 'https://www.virustotal.com/vtapi/v2/file/download'
|
||||
params = {'apikey': self.virustotal_apikey, 'hash': self.get_param('data.data')}
|
||||
url = "https://www.virustotal.com/vtapi/v2/file/download"
|
||||
params = {
|
||||
"apikey": self.virustotal_apikey,
|
||||
"hash": self.get_param("data.data"),
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params)
|
||||
|
||||
@ -36,38 +46,64 @@ class VirustotalDownloader(Responder):
|
||||
downloaded_file = response.content
|
||||
|
||||
tempdir = tempfile.gettempdir()
|
||||
f = open(tempdir + "/" + self.get_param('data.data'), 'wb')
|
||||
f = open(tempdir + "/" + self.get_param("data.data"), "wb")
|
||||
f.write(downloaded_file)
|
||||
f.close()
|
||||
filename = f.name
|
||||
|
||||
kind = filetype.guess(f.name)
|
||||
|
||||
if kind.extension != None:
|
||||
api = TheHiveApi(self.thehive_url, self.thehive_apikey)
|
||||
|
||||
if kind and kind.extension != None:
|
||||
os.rename(f.name, f.name + "." + kind.extension)
|
||||
filename = f.name + "." + kind.extension
|
||||
|
||||
api = TheHiveApi(self.thehive_url, self.thehive_apikey)
|
||||
|
||||
file_observable = CaseObservable(dataType='file',
|
||||
file_observable = CaseObservable(
|
||||
dataType="file",
|
||||
data=[filename],
|
||||
tlp=self.get_param('data.tlp'),
|
||||
tlp=self.get_param("data.tlp"),
|
||||
ioc=True,
|
||||
tags=['src:VirusTotal', str(kind.mime), str(kind.extension), 'parent:' + self.get_param('data.data')],
|
||||
message=''
|
||||
)
|
||||
tags=[
|
||||
"src:VirusTotal",
|
||||
str(kind.mime),
|
||||
str(kind.extension),
|
||||
"parent:" + self.get_param("data.data"),
|
||||
],
|
||||
message="",
|
||||
)
|
||||
else:
|
||||
file_observable = CaseObservable(
|
||||
dataType="file",
|
||||
data=[f.name],
|
||||
tlp=self.get_param("data.tlp"),
|
||||
ioc=True,
|
||||
tags=[
|
||||
"src:VirusTotal",
|
||||
"parent:" + self.get_param("data.data"),
|
||||
],
|
||||
message="",
|
||||
)
|
||||
|
||||
response = api.create_case_observable(case_id, file_observable)
|
||||
|
||||
self.report({'message': str(response.status_code) + " " + response.text})
|
||||
self.report(
|
||||
{"message": str(response.status_code) + " " + response.text}
|
||||
)
|
||||
else:
|
||||
self.report({'message': 'Virustotal returned the following error code: ' + str(response.status_code) + ". If you receive 403 this means that you are using a public API key but this responder needs a private Virustotal API key!"})
|
||||
self.report(
|
||||
{
|
||||
"message": "Virustotal returned the following error code: "
|
||||
+ str(response.status_code)
|
||||
+ ". If you receive 403 this means that you are using a public API key but this responder needs a private Virustotal API key!"
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.error('Incorrect dataType. "Hash" expected.')
|
||||
|
||||
def operations(self, raw):
|
||||
return [self.build_operation('AddTagToArtifact', tag='Virustotal:Downloaded')]
|
||||
return [self.build_operation("AddTagToArtifact", tag="Virustotal:Downloaded")]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
VirustotalDownloader().run()
|
||||
|
@ -10,8 +10,8 @@ class Wazuh(Responder):
|
||||
self.wazuh_user = self.get_param('config.wazuh_user', None, 'Username missing!')
|
||||
self.wazuh_password = self.get_param('config.wazuh_password', None, 'Password missing!')
|
||||
self.wazuh_agent_id = self.get_param('data.case.customFields.wazuh_agent_id.string', None, "Agent ID Missing!")
|
||||
self.wazuh_alert_id = self.get_param('data.case.customFields.wazuh_alert_id.string', None, " Missing!")
|
||||
self.wazuh_rule_id = self.get_param('data.case.customFields.wazuh_rule_id.string', None, "Agent ID Missing!")
|
||||
self.wazuh_alert_id = self.get_param('data.case.customFields.wazuh_alert_id.string', None, "Alert ID Missing!")
|
||||
self.wazuh_rule_id = self.get_param('data.case.customFields.wazuh_rule_id.string', None, "Rule ID Missing!")
|
||||
self.observable = self.get_param('data.data', None, "Data is empty")
|
||||
self.observable_type = self.get_param('data.dataType', None, "Data type is empty")
|
||||
|
||||
|
188
thehive-templates/Elasticsearch/long.html
Normal file
@ -0,0 +1,188 @@
|
||||
<div class="report-ELK" ng-if="success">
|
||||
<style>
|
||||
.report-ELK dl {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.backgroundTable tr:nth-child(even) {background-color: #f2f2f2;}
|
||||
.backgroundTableodd tr:nth-child(odd) {background-color: #f2f2f2;}
|
||||
|
||||
.backgroundTable th, .backgroundTableodd th {
|
||||
background: white;
|
||||
position: sticky;
|
||||
top: -2px;
|
||||
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.timelineTable th {
|
||||
background: white;
|
||||
position: sticky;
|
||||
top: -15px;
|
||||
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.4);
|
||||
text-align: center;
|
||||
}
|
||||
.timelineTable tbody tr:hover td, .backgroundTable tbody tr:hover td, .backgroundTableodd tbody tr:hover td {
|
||||
background-color: #d9edf7;
|
||||
}
|
||||
.timelineTable td {
|
||||
max-width:200px;
|
||||
}
|
||||
.timelineTable tr {
|
||||
max-height:100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
var copyText = document.getElementById("querystringtext");
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<strong>Matches</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table style="table-layout:fixed;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h3>{{content.info.hitcount}} Hit(s)</h3>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width:auto;"><button onclick="copyToClipboard()" class="btn btn-success btn-sm"><i class="fa fa-copy"></i> Copy query to clipboard</button></div>
|
||||
<div id="querystring" style="opacity:0;width:0;height:0;"><textarea id="querystringtext">{{content.info.querystring}}</textarea></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td> </td></tr>
|
||||
<tr>
|
||||
<td>
|
||||
<dl class="dl-horizontal">
|
||||
<dt style="width:auto;" ng-if="content.info.dashboard">Kibana Dashboard: </dt>
|
||||
<dd style="margin-left:0;" ng-if="content.info.dashboard"><a href="{{content.info.query}}" target="_blank">{{content.info.dashboard}}</a></dd>
|
||||
<dd style="color:red;margin-left:0;">{{content.info.error}}</dd>
|
||||
<dd style="color:red;margin-left:0;" ng-if="content.total.includes('gte')">*Number of logs exceed maximum. All hits are not shown. See Kibana for more logs...</dd>
|
||||
</dl>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:block;overflow:auto;height:250px;">
|
||||
<table class="table table-hover backgroundTable" style="border: 1px solid #d9edf7;table-layout:fixed;">
|
||||
<thead>
|
||||
<th>{{content.info.userhitcount}} User(s)</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in content.info.uniqueusers track by $index">
|
||||
<td>{{user}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:block;overflow:auto;height:250px;">
|
||||
<table class="table table-hover backgroundTableodd" style="border: 1px solid #d9edf7;table-layout:fixed;">
|
||||
<thead>
|
||||
<th>{{content.info.devicehitcount}} Device(s)</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="device in content.info.uniquedevices track by $index">
|
||||
<td>{{device}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="content.hits[0].hitindex">
|
||||
<div class="panel-heading">
|
||||
<strong>Timeline</strong>
|
||||
</div>
|
||||
<div class="panel-body" style="display:block;height:750px;overflow:auto;">
|
||||
<table class="table table-hover backgroundTable timelineTable">
|
||||
<thead>
|
||||
<th>Time</th>
|
||||
<th>User</th>
|
||||
<th>Device</th>
|
||||
<th>Parent <i class="fa fa-arrow-right"></i> Process</th>
|
||||
<th>Process Args</th>
|
||||
<th>Url</th>
|
||||
<th>Dns Question Name</th>
|
||||
<th>Dns Resolved IP</th>
|
||||
<th>Source IP:Port</th>
|
||||
<th>Destination IP:Port</th>
|
||||
<th>Rule Category</th>
|
||||
<th>Index</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="hit in content.hits track by $index">
|
||||
<td>
|
||||
{{hit.time}}
|
||||
</td>
|
||||
<td style="overflow:auto" ng-if="hit.user_name">
|
||||
{{hit.user_name}}
|
||||
</td>
|
||||
<td style="overflow:auto" ng-if="!hit.user_name">
|
||||
{{hit.source_user_name}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.host_name}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.process_parent_name}} <i class="fa fa-arrow-right" ng-if="hit.process_parent_name && hit.process_name"></i> {{hit.process_name}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.process_args[1]}}
|
||||
</td>
|
||||
<td ng-if="url_full" style="overflow:auto">
|
||||
{{hit.url_full.toString()}}
|
||||
</td>
|
||||
<td ng-if="!url_full" style="overflow:auto">
|
||||
{{hit.url_domain.toString()}}{{hit.url_path.toString()}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.dns_question_name.toString()}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.dns_resolvedip.toString()}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.source_ip}}<i ng-if="hit.source_ip && hit.source_port">:</i>{{hit.source_port}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.destination_ip}}<i ng-if="hit.destination_ip && hit.destination_port">:</i>{{hit.destination_port}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.rule_category}}
|
||||
</td>
|
||||
<td style="overflow:auto">
|
||||
{{hit.hitindex}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<hr>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- General error -->
|
||||
<div class="panel panel-danger" ng-if="!success">
|
||||
<div class="panel-heading">
|
||||
<strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{content.errorMessage}}
|
||||
</div>
|
||||
</div>
|
@ -1,3 +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>
|
||||
<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>
|
63
thehive-templates/Inoitsu_1_0/long.html
Normal file
@ -0,0 +1,63 @@
|
||||
<div class="report-SecurityTrails" ng-if="success">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
Inoitsu lookup ({{ content.Email }})
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div ng-if="(content | json) === '{}'">
|
||||
No data found
|
||||
</div>
|
||||
<div ng-if="(content | json) !== '{}'">
|
||||
|
||||
<div ng-if="(content.Leaked)">
|
||||
<dl class=" dl-horizontal">
|
||||
<dt>Compromised:</dt>
|
||||
<dd class="wrap">{{ content.Leaked }}</dd>
|
||||
</dl>
|
||||
<dl class=" dl-horizontal">
|
||||
<dt>Total breaches:</dt>
|
||||
<dd class="wrap">{{ content.Total_breaches }}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Most recent breach:</dt>
|
||||
<dd class="wrap">{{ content.Most_recent_breach }}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Breached data:</dt>
|
||||
<dd class="wrap">{{ content.Breached_data }}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Critical data:</dt>
|
||||
<dd class="wrap">{{ content.Critical_data }}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Exposure rating:</dt>
|
||||
<dd class="wrap">{{ content.Exposure_rating }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div ng-if="!(content.Leaked)">
|
||||
<dl class=" dl-horizontal">
|
||||
<dt>Compromised:</dt>
|
||||
<dd class="wrap">{{ content.Leaked }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- General error -->
|
||||
<div class="panel panel-danger" ng-if="!success">
|
||||
<div class="panel-heading">
|
||||
<strong>{{ artifact.data | fang }}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal" ng-if="content.errorMessage">
|
||||
<dt>
|
||||
<i class="fa fa-warning"></i>
|
||||
</dt>
|
||||
<dd class="wrap">{{ content.errorMessage }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
3
thehive-templates/Inoitsu_1_0/short.html
Normal file
@ -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>
|
28
thehive-templates/LdapQuery_1_0/long.html
Normal file
@ -0,0 +1,28 @@
|
||||
<div class="report-LdapQuery" ng-if="success">
|
||||
<style>
|
||||
.report-LdapQuery dl {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<strong>LDAP Query Summary</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div ng-if="content.results.length === 0">
|
||||
No records found
|
||||
</div>
|
||||
<div ng-if="content.results.length > 0">
|
||||
<div ng-repeat="results in content.results">
|
||||
<dl class="dl-horizontal" ng-repeat="(key, value) in results">
|
||||
<dt>{{key}}: </dt>
|
||||
<dd class="wrap">{{value}}</dd>
|
||||
</dl>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
6
thehive-templates/LdapQuery_1_0/short.html
Normal file
@ -0,0 +1,6 @@
|
||||
<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>
|
31
thehive-templates/SophosIntelix_GetReport_0_3/long.html
Normal file
@ -0,0 +1,31 @@
|
||||
<div class="panel panel-danger" ng-if="!success">
|
||||
<div class="panel-heading">
|
||||
<strong>{{artifact.data | fang}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{content.errorMessage}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-primary" ng-if="success">
|
||||
<div class="panel-info">
|
||||
<div class="panel-heading">
|
||||
<strong>Report for {{artifact.data | fang}} </strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl ng-if="::['domain', 'fqdn', 'url'].indexOf(artifact.dataType) != -1">
|
||||
<dt>Productivity Category</dt>
|
||||
<dd>{{content.prod_category || "No Data"}}</dd>
|
||||
<dt>Security Category</dt>
|
||||
<dd>{{content.sec_category || "No Data"}}</dd>
|
||||
<dt>Risk Level</dt>
|
||||
<dd>{{content.risk_level || "No Data"}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl ng-if="::artifact.dataType === 'hash'">
|
||||
<dt>File Classification</dt>
|
||||
<dd>{{content.classification || "No Data"}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
3
thehive-templates/SophosIntelix_GetReport_0_3/short.html
Normal file
@ -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>
|
256
thehive-templates/SophosIntelix_Submit_Dynamic_0_1/long.html
Normal file
@ -0,0 +1,256 @@
|
||||
<div class="panel panel-danger" ng-if="!success">
|
||||
<div class="panel-heading">
|
||||
<strong>{{artifact.data | fang}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{content.errorMessage}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-primary" ng-if="success" ng-init="table_limit = 50">
|
||||
<div class="panel-heading">
|
||||
<strong>Sample Details - First Submitted: {{content.submission}} - Analysis Type: {{content.analysis_type}}</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>File Name</th>
|
||||
<th>SHA1</th>
|
||||
<th>SHA256</th>
|
||||
<th>MIME Type</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{artifact.attachment.name | fang}}</td>
|
||||
<td>{{content.analysis_subject.sha1}}</td>
|
||||
<td>{{content.analysis_subject.sha256}}</td>
|
||||
<td>{{content.analysis_subject.mime_type}}</td>
|
||||
<td>{{content.score}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="panel panel-info" ng-if="::content.malicious_activity">
|
||||
<div class="panel-heading">
|
||||
<strong>Malicious Activity</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Activity</th>
|
||||
</tr>
|
||||
<tr ng-repeat="activity in content.malicious_activity.Suspicious | limitTo:table_limit">
|
||||
<td>Suspicious</td>
|
||||
<td>{{activity}}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="activity in content.malicious_activity.Network | limitTo:table_limit">
|
||||
<td>Network</td>
|
||||
<td>{{activity}}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="activity in content.malicious_activity.Signature | limitTo:table_limit">
|
||||
<td>Signature</td>
|
||||
<td>{{activity}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.malicious_classifications">
|
||||
<div class="panel-heading">
|
||||
<strong>Files Written</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Classification</th>
|
||||
<th>Classification Type</th>
|
||||
<th>Artifact</th>
|
||||
<th>Artifact Type</th>
|
||||
<th>Threat Name</th>
|
||||
<th>PID</th>
|
||||
</tr>
|
||||
<tr ng-repeat="mc in content.malicious_classifications | limitTo:table_limit">
|
||||
<td>{{mc.classification}}</td>
|
||||
<td>{{mc.classification_type}}</td>
|
||||
<td>{{mc.artifact}}</td>
|
||||
<td>{{mc.artifact_type}}</td>
|
||||
<td>{{mc.threat_name}}</td>
|
||||
<td>{{mc.pid}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.files.written">
|
||||
<div class="panel-heading">
|
||||
<strong>Files Written</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Process</th>
|
||||
<th>Path</th>
|
||||
<th>PID</th>
|
||||
</tr>
|
||||
<tr ng-repeat="file in content.files.written | limitTo:table_limit">
|
||||
<td>{{file.process}}</td>
|
||||
<td>{{file.path}}</td>
|
||||
<td>{{file.pid}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.files.deleted">
|
||||
<div class="panel-heading">
|
||||
<strong>Files Deleted</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
</tr>
|
||||
<tr ng-repeat="file in content.files.deleted | limitTo:table_limit">
|
||||
<td>{{file.path}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.registry.added">
|
||||
<div class="panel-heading">
|
||||
<strong>Registry Keys Added</strong>
|
||||
</div>
|
||||
<table class="table table-hover" >
|
||||
<tr>
|
||||
<th>TimeStamp</th>
|
||||
<th>Process</th>
|
||||
<th>PID</th>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
<tr ng-repeat="regkey in content.registry.added | limitTo:table_limit">
|
||||
<td>{{regkey.timestamp}}</td>
|
||||
<td>{{regkey.process}}</td>
|
||||
<td>{{regkey.pid}}</td>
|
||||
<td>{{regkey.key}}</td>
|
||||
<td>{{regkey.reg_value}}</td>
|
||||
<td>{{regkey.reg_data}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.registry.deleted">
|
||||
<div class="panel-heading">
|
||||
<strong>Registry Keys Deleted</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>TimeStamp</th>
|
||||
<th>Process</th>
|
||||
<th>PID</th>
|
||||
<th>Key</th>
|
||||
</tr>
|
||||
<tr ng-repeat="regkey in content.registry.deleted | limitTo:table_limit">
|
||||
<td>{{regkey.timestamp}}</td>
|
||||
<td>{{regkey.process}}</td>
|
||||
<td>{{regkey.pid}}</td>
|
||||
<td>{{regkey.key}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.processes">
|
||||
<div class="panel-heading">
|
||||
<strong>Processes</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Start Time</th>
|
||||
<th>Process</th>
|
||||
<th>PID</th>
|
||||
<th>Parent Process</th>
|
||||
<th>Parent PID</th>
|
||||
<th>Command Line</th>
|
||||
</tr>
|
||||
<tr ng-repeat="proc in content.processes | limitTo:table_limit">
|
||||
<td>{{proc.start_time}}</td>
|
||||
<td>{{proc.process}}</td>
|
||||
<td>{{proc.pid}}</td>
|
||||
<td>{{proc.parent_process}}</td>
|
||||
<td>{{proc.ppid}}</td>
|
||||
<td>{{proc.command_line}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.network.connections">
|
||||
<div class="panel-heading">
|
||||
<strong>Network Connections</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Port</th>
|
||||
<th>Protocol</th>
|
||||
<th>Host</th>
|
||||
<th>Process</th>
|
||||
<th>PID</th>
|
||||
</tr>
|
||||
<tr ng-repeat="connection in content.network.connections | limitTo:table_limit">
|
||||
<td>{{connection.ip}}</td>
|
||||
<td>{{connection.port}}</td>
|
||||
<td>{{connection.protocol}}</td>
|
||||
<td>{{connection.host}}</td>
|
||||
<td>{{connection.process}}</td>
|
||||
<td>{{connection.pid}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.network.http_flow">
|
||||
<div class="panel-heading">
|
||||
<strong>HTTP Flow</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>URI</th>
|
||||
<th>HTTP Method</th>
|
||||
<th>HTTP User Agent</th>
|
||||
<th>IP</th>
|
||||
<th>Process</th>
|
||||
<th>URL Classification</th>
|
||||
<th>Response</th>
|
||||
</tr>
|
||||
<tr ng-repeat="http in content.network.http_flow | limitTo:table_limit">
|
||||
<td>{{http.uri}}</td>
|
||||
<td>{{http.http_method}}</td>
|
||||
<td>{{http.http_useragent}}</td>
|
||||
<td>{{http.ip}}</td>
|
||||
<td>{{http.process}}</td>
|
||||
<td>{{http.url_classification}}</td>
|
||||
<td>Code: {{http.response.http_status}}
|
||||
</br>
|
||||
MIME Type: {{http.response.mime_type}}
|
||||
</br>
|
||||
Size: {{http.response.size}}
|
||||
</br>
|
||||
SHA1: {{http.response.sha1}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.network.dns_requests">
|
||||
<div class="panel-heading">
|
||||
<strong>DNS Requests</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Domain</th>
|
||||
</tr>
|
||||
<tr ng-repeat="request in content.network.dns_requests | limitTo:table_limit">
|
||||
<td>{{request.ip}}</td>
|
||||
<td>{{request.domain}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.activity_tree.vis">
|
||||
<div class="panel-heading">
|
||||
<strong>Activity Tree</strong>
|
||||
</div>
|
||||
</br>
|
||||
<img src={{content.activity_tree.vis}} width="1000">
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.screenshots">
|
||||
<div class="panel-heading">
|
||||
<strong>Screenshots</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr ng-repeat="screen in content.screenshots">
|
||||
<td><img src={{screen}} width="1000"></td>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
@ -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>
|
403
thehive-templates/SophosIntelix_Submit_Static_0_1/long.html
Normal file
@ -0,0 +1,403 @@
|
||||
<div class="panel panel-danger" ng-if="!success">
|
||||
<div class="panel-heading">
|
||||
<strong>{{artifact.data | fang}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{content.errorMessage}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-primary" ng-if="success" ng-init="table_limit = 50">
|
||||
<div class="panel-heading">
|
||||
<strong>Sample Details - First Submitted: {{content.submission}} - Analysis Type: {{content.analysis_type}}</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>File Name</th>
|
||||
<th>SHA1</th>
|
||||
<th>SHA256</th>
|
||||
<th>MIME Type</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{artifact.attachment.name | fang}}</td>
|
||||
<td>{{content.analysis_subject.sha1}}</td>
|
||||
<td>{{content.analysis_subject.sha256}}</td>
|
||||
<td>{{content.analysis_subject.mime_type}}</td>
|
||||
<td>{{content.score}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="panel panel-info" ng-if="::content.detection">
|
||||
<div class="panel-heading">
|
||||
<strong>Detections</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Positives</th>
|
||||
<th>VT Link</th>
|
||||
<th>Sophos</th>
|
||||
<th>Sophos ML</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.detection.positives || "0"}}</td>
|
||||
<td><a href={{content.detection.permalink}} target="_blank">{{content.analysis_subject.sha256}}</a></td>
|
||||
<td>{{content.detection.sophos || "0"}}</td>
|
||||
<td>{{content.detection.sophos_ml || "0"}}</td>
|
||||
<td>{{content.detection.total || "0"}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.reputation">
|
||||
<div class="panel-heading">
|
||||
<strong>Reputation</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Reputation Classification</th>
|
||||
<th>Reputation Score</th>
|
||||
<th>Prevalence</th>
|
||||
<th>First Seen</th>
|
||||
<th>Last Seen</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.reputation.score_string}}</td>
|
||||
<td>{{content.reputation.score}}</td>
|
||||
<td>{{content.reputation.prevalence}}</td>
|
||||
<td>{{content.reputation.first_seen}}</td>
|
||||
<td>{{content.reputation.last_seen}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis.versioninfo">
|
||||
<div class="panel-heading">
|
||||
<strong>Version Info</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Legal Copyright</th>
|
||||
<th>Internal Name</th>
|
||||
<th>File Version</th>
|
||||
<th>Special Build</th>
|
||||
<th>Company Name</th>
|
||||
<th>Legal Trademarks</th>
|
||||
<th>Comments</th>
|
||||
<th>Product Name</th>
|
||||
<th>File Description</th>
|
||||
<th>Product Version</th>
|
||||
<th>Private Build</th>
|
||||
<th>Original Filename</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.pe_analysis.versioninfo.legalcopyright}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.internalname}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.fileversion}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.specialbuild}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.companyname}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.legaltrademarks}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.comments}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.productname}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.filedescription}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.productversion}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.privatebuild}}</td>
|
||||
<td>{{content.pe_analysis.versioninfo.originalfilename}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis.digisig">
|
||||
<div class="panel-heading">
|
||||
<strong>Digital Signature</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Signing Date</th>
|
||||
<th>Signer</th>
|
||||
<th>Verfified</th>
|
||||
<th>Certificate Source</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Time Signer</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.pe_analysis.digisig.signing_date}}</td>
|
||||
<td>{{content.pe_analysis.digisig.signer | json}}</td>
|
||||
<td>{{content.pe_analysis.digisig.verified}}</td>
|
||||
<td>{{content.pe_analysis.digisig.certificate_source}}</td>
|
||||
<td>{{content.pe_analysis.digisig.timescert.timestamp}}</td>
|
||||
<td>{{content.pe_analysis.digisig.timescert.timesigner | json}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.ml_file.analyses.genetic_analysis.neighbor_info">
|
||||
<div class="panel-heading">
|
||||
<strong>ML File Analysis - Genetic Analysis - Neighbor Info</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>SHA 265</th>
|
||||
<th>Is Malware</th>
|
||||
<th>Score</th>
|
||||
<th>Match Percentage</th>
|
||||
<th>File Path</th>
|
||||
</tr>
|
||||
<tr ng-repeat="(key, neighbor) in content.ml_file.analyses.genetic_analysis.neighbor_info | limitTo:table_limit">
|
||||
<td><a href=https://www.virustotal.com/gui/file/{{key}}/detection target="_blank">{{key}}</a></td>
|
||||
<td>{{neighbor.is_malware}}</td>
|
||||
<td>{{neighbor.score}}</td>
|
||||
<td>{{neighbor.match_percentage}}</td>
|
||||
<td>{{neighbor.filepath}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.ml_file">
|
||||
<div class="panel-heading">
|
||||
<strong>ML File Analysis - Analyzed Counts</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Benign</th>
|
||||
<th>Malware</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feature Intersections</td>
|
||||
<td>{{content.ml_file.analyzed_counts.feature_intersections.benign}}</td>
|
||||
<td>{{content.ml_file.analyzed_counts.feature_intersections.malware}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feature Maliciousness</td>
|
||||
<td>{{content.ml_file.analyzed_counts.feature_maliciousness.benign}}</td>
|
||||
<td>{{content.ml_file.analyzed_counts.feature_maliciousness.malware}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Genetic Analysis</td>
|
||||
<td>{{content.ml_file.analyzed_counts.genetic_analysis.benign}}</td>
|
||||
<td>{{content.ml_file.analyzed_counts.genetic_analysis.malware}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Black Box</td>
|
||||
<td>{{content.ml_file.analyzed_counts.black_box.benign}}</td>
|
||||
<td>{{content.ml_file.analyzed_counts.black_box.malware}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.ml_file.analyses.feature_intersections">
|
||||
<div class="panel-heading">
|
||||
<strong>ML File Analysis - Feature Intersections</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Indicator</th>
|
||||
<th>Description</th>
|
||||
<th>Probability</th>
|
||||
<th>Malware</th>
|
||||
<th>Benign</th>
|
||||
</tr>
|
||||
<tr ng-repeat="is in content.ml_file.analyses.feature_intersections | limitTo:table_limit">
|
||||
<td>{{is.category}}</td>
|
||||
<td>{{is.indicator}}</td>
|
||||
<td>{{is.description}}</td>
|
||||
<td>{{is.probability}}</td>
|
||||
<td>{{is.malware}}</td>
|
||||
<td>{{is.benign}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.ml_file.analyses.feature_maliciousness">
|
||||
<div class="panel-heading">
|
||||
<strong>ML File Analysis - Feature Maliciousness</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Indicator</th>
|
||||
<th>Description</th>
|
||||
<th>Probability</th>
|
||||
<th>Malware</th>
|
||||
<th>Benign</th>
|
||||
</tr>
|
||||
<tr ng-repeat="fm in content.ml_file.analyses.feature_maliciousness | limitTo:table_limit">
|
||||
<td>{{fm.category}}</td>
|
||||
<td>{{fm.indicator}}</td>
|
||||
<td>{{fm.description}}</td>
|
||||
<td>{{fm.probability}}</td>
|
||||
<td>{{fm.malware}}</td>
|
||||
<td>{{fm.benign}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.ml_file.analyses.black_box">
|
||||
<div class="panel-heading">
|
||||
<strong>ML File Analysis - Black Box</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Model Version</th>
|
||||
<th>Model Name</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.ml_file.analyses.black_box.model_version}}</td>
|
||||
<td>{{content.ml_file.analyses.black_box.model_name}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</br>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Raw</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Benign</td>
|
||||
<td>{{content.ml_file.analyses.black_box.benign.raw}}</td>
|
||||
<td>{{content.ml_file.analyses.black_box.benign.score}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PUA</td>
|
||||
<td>{{content.ml_file.analyses.black_box.pua.raw}}</td>
|
||||
<td>{{content.ml_file.analyses.black_box.pua.score}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.ml_file.overall_scores">
|
||||
<div class="panel-heading">
|
||||
<strong>ML File Analysis - Overall Scores</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Feature Intersections</th>
|
||||
<th>Feature Maliciousness</th>
|
||||
<th>Genetic Analysis</th>
|
||||
<th>Black Box</th>
|
||||
<th>Overall Score</th>
|
||||
<th>ML Aggregate Results</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.ml_file.overall_scores.feature_intersections}}</td>
|
||||
<td>{{content.ml_file.overall_scores.feature_maliciousness}}</td>
|
||||
<td>{{content.ml_file.overall_scores.genetic_analysis}}</td>
|
||||
<td>{{content.ml_file.overall_scores.black_box}}</td>
|
||||
<td>{{content.ml_file.overall_score}}</td>
|
||||
<td>{{content.ml_aggregate_results.overall_score}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</br>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis">
|
||||
<div class="panel-heading">
|
||||
<strong>PE Analysis</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Subsystem</th>
|
||||
<th>Number of Sections</th>
|
||||
<th>File Size</th>
|
||||
<th>Imagebase</th>
|
||||
<th>PDB String</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.pe_analysis.subsystem}}</td>
|
||||
<td>{{content.pe_analysis.numberofsections}}</td>
|
||||
<td>{{content.pe_analysis.filesize}}</td>
|
||||
<td>{{content.pe_analysis.imagebase}}</td>
|
||||
<td>{{content.pe_analysis.pdbstring}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis.sections">
|
||||
<div class="panel-heading">
|
||||
<strong>PE Analysis - Sections</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Number</th>
|
||||
<th>Name</th>
|
||||
<th>Size of Raw Data</th>
|
||||
<th>Entropy</th>
|
||||
<th>Virtual Size</th>
|
||||
<th>Virtual Address</th>
|
||||
<th>Physical Address</th>
|
||||
<th>Characteristics</th>
|
||||
</tr>
|
||||
<tr ng-repeat="section in content.pe_analysis.sections | limitTo:table_limit">
|
||||
<td>{{section.number}}</td>
|
||||
<td>{{section.name}}</td>
|
||||
<td>{{section.sizeofrawdata}}</td>
|
||||
<td>{{section.entropy}}</td>
|
||||
<td>{{section.virtualsize}}</td>
|
||||
<td>{{section.virtualaddress}}</td>
|
||||
<td>{{section.physicaladdress}}</td>
|
||||
<td>{{section.characteristics | json}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis">
|
||||
<div class="panel-heading">
|
||||
<strong>PE Analysis - PE Flags</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Machine ID</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Languages</th>
|
||||
<th>PE Flags</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.pe_analysis.machineid}}</td>
|
||||
<td>{{content.pe_analysis.timedate}}</td>
|
||||
<td>{{content.pe_analysis.languages | json}}</td>
|
||||
<td>{{content.pe_analysis.peflags}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis.export.dllname">
|
||||
<div class="panel-heading">
|
||||
<strong>PE Analysis - Export</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>DLL Name</th>
|
||||
<th>APIs</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{content.pe_analysis.export.dllname}}</td>
|
||||
<td>{{content.pe_analysis.export.apis}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis.import">
|
||||
<div class="panel-heading">
|
||||
<strong>PE Analysis - Imports</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>DLL Name</th>
|
||||
<th>Import By Ordinal</th>
|
||||
<th>APIs</th>
|
||||
</tr>
|
||||
<tr ng-repeat="dll in content.pe_analysis.import | limitTo:table_limit">
|
||||
<td>{{dll.dllname}}</td>
|
||||
<td>{{dll.importbyordinal}}</td>
|
||||
<td>{{dll.apis | json}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info" ng-if="::content.pe_analysis.resources">
|
||||
<div class="panel-heading">
|
||||
<strong>PE Analysis - Resources</strong>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Codepage</th>
|
||||
<th>Type</th>
|
||||
<th>Language</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
<tr ng-repeat="resource in content.pe_analysis.resources | limitTo:table_limit">
|
||||
<td>{{resource.codepage}}</td>
|
||||
<td>{{resource.type}}</td>
|
||||
<td>{{resource.language}}</td>
|
||||
<td>{{resource.size}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
@ -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>
|
@ -1,8 +1,8 @@
|
||||
<div class="panel panel-info" ng-if="success && content.scanreport.data.length > 0">
|
||||
<div class="panel panel-info" ng-if="success && content.samples.length > 0">
|
||||
<div class="panel-heading">
|
||||
VMRay Report
|
||||
</div>
|
||||
<div class="panel-body" ng-repeat="sample in content.scanreport.data">
|
||||
<div class="panel-body" ng-repeat="sample in content.samples">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Score</dt>
|
||||
<dd><a href="{{sample.sample_webif_url}}">{{sample.sample_score}}</a></dd>
|
||||
@ -10,23 +10,21 @@
|
||||
<dl class="dl-horizontal">
|
||||
<dt ng-if="sample.sample_severity">Severity</dt>
|
||||
<dd>
|
||||
<span class="label" ng-class="{'label-success':sample.sample_severity === 'not_suspicious',
|
||||
<span class="label" ng-class="{'label-success':sample.sample_severity === 'not_suspicious',
|
||||
'label-danger': sample.sample_severity==='malicious',
|
||||
'label-info': sample.sample_severity!='not_suspicious' && sample.sample_severity!='malicious'}">
|
||||
{{sample.sample_severity}}
|
||||
</span>
|
||||
<!--<span class="label ">{{sample.sample_last_reputation_severity}}</span>-->
|
||||
|
||||
{{sample.sample_severity}}
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Last reputation</dt>
|
||||
<dd>
|
||||
<span class="label" ng-class="{'label-info':sample.sample_last_reputation_severity === 'unknown',
|
||||
<span class="label" ng-class="{'label-info':sample.sample_last_reputation_severity === 'unknown',
|
||||
'label-danger': sample.sample_last_reputation_severity==='blacklisted',
|
||||
'label-info': sample.sample_last_reputation_severity!='blacklisted'}">
|
||||
{{sample.sample_last_reputation_severity}}
|
||||
</span>
|
||||
{{sample.sample_last_reputation_severity}}
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl ng-if="sample.sample_webif_url" class="dl-horizontal">
|
||||
@ -60,7 +58,9 @@
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-success" ng-if="success && content.scanreport.data.length == 0">
|
||||
|
||||
|
||||
<div class="panel panel-success" ng-if="success && content.samples.length == 0">
|
||||
<div class="panel-heading">
|
||||
VMRay Report
|
||||
</div>
|
||||
@ -81,17 +81,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-danger" ng-if="success && content.data.errors.length > 0">
|
||||
<div class="panel panel-danger" ng-if="success && content.errors.length > 0">
|
||||
<div class="panel-heading">
|
||||
VMRay Upload returned with an error (reanalyzation deactivated?)
|
||||
</div>
|
||||
<div class="panel-body" ng-repeat="e in content.data.errors">
|
||||
<div class="panel-body" ng-repeat="e in content.errors">
|
||||
<dl class="dl-horizontal">
|
||||
<dt><i class="fa fa-warning"></i> Error:</dt>
|
||||
<dd>{{e.error_msg}}</dd>
|
||||
<dt>Filename:</dt>
|
||||
<dd><a href="https://vmray.analyse.certbund/user/sample/list?quick_search_value={{e.submission_filename}}">{{e.submission_filename}}
|
||||
(search)</a></dd>
|
||||
<dd>{{e.submission_filename}}</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
5
thehive-templates/VMRay_3_1/short.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="label" ng-repeat="t in content.taxonomies"
|
||||
ng-class="{'info': 'label-info', 'safe': 'label-success', 'suspicious': 'label-warning', 'malicious':'label-danger'}[t.level]"
|
||||
style="display:inline-block;">
|
||||
{{t.namespace}}:{{t.predicate}}="{{t.value}}"
|
||||
</div>
|
80
thehive-templates/Vulners_CVE_1_0/long.html
Normal file
@ -0,0 +1,80 @@
|
||||
<!-- Success -->
|
||||
<div class="panel panel-info" ng-if="success">
|
||||
<div class="panel-heading">
|
||||
Vulners information for <strong>{{artifact.data | fang}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<h3 align="center"><b>{{artifact.data}}</b></h3>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Published: </dt><dd>{{content.published | limitTo : 10}}</dd>
|
||||
<dt>Modified: </dt><dd>{{content.published | limitTo : 10}}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal" ng-if="content.cvss2">
|
||||
<dt>CVSSv2.0: </dt><dd>{{content.cvss2.cvssV2.baseScore}}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal" ng-if="content.cvss3">
|
||||
<dt>CVSSv3.1: </dt><dd>{{content.cvss3.cvssV3.baseScore}}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal" ng-if="content.vulners_AI">
|
||||
<dt>Vulners Score: </dt> <dd>{{content.vulners_AI}}</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal" ng-if="content.cwe">
|
||||
<dt>CWE: </dt>
|
||||
<dd>
|
||||
<span class="label"
|
||||
ng-style="{'border':'1px solid','color': '#000000', 'background-color': md.color}">
|
||||
{{content.cwe[0]}}
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="dl-horizontal" ng-if="content.exploits">
|
||||
<dt>Exploits: </dt> <dd style="color:#ff0000">Yes</dd>
|
||||
</dl>
|
||||
<hr>
|
||||
<h3>Description</h3>
|
||||
<p>{{content.description}}</p>
|
||||
<hr>
|
||||
<h3>Affected products</h3>
|
||||
<table class="table table-stripped">
|
||||
<tr>
|
||||
<th>Product name</th>
|
||||
<th>Product version</th>
|
||||
</tr>
|
||||
<tr ng-repeat="soft in content.affectedSoftware">
|
||||
<td>{{soft.name}}</td>
|
||||
<td>{{soft.version}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p><br>Source info:
|
||||
<a href="https://vulners.com/cve/{{artifact.data}}" target="_blank">https://vulners.com/cve/{{artifact.data}}</a>
|
||||
</p>
|
||||
</div><br />
|
||||
|
||||
<div class="panel panel-info" ng-if="content.exploits">
|
||||
<div class="panel-heading">
|
||||
<strong>Exploits</strong>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal" ng-repeat="exploit in content.exploits">
|
||||
<dt>Title: </dt> <dd>{{exploit.title}}</dd>
|
||||
<dt>Published: </dt> <dd>{{exploit.published | limitTo : 10}}</dd>
|
||||
<dt>Exploit url: </dt> <dd><a href="{{exploit.url}}" target="_blank">{{exploit.url}}</a></dd>
|
||||
<hr>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General error -->
|
||||
<div class="panel panel-danger" ng-if="!success">
|
||||
<div class="panel-heading">
|
||||
<strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal" ng-if="content.errorMessage">
|
||||
<dt><i class="fa fa-warning"></i> ANALYZERNAME: </dt>
|
||||
<dd class="wrap">{{content.errorMessage}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|