mirror of
https://github.com/valitydev/Cortex-Analyzers.git
synced 2024-11-06 09:05:19 +00:00
Merge branch 'release/2.5.0'
This commit is contained in:
commit
0fba4d5ae7
13
analyzers/ClamAV/ClamAV_FileInfo.json
Normal file
13
analyzers/ClamAV/ClamAV_FileInfo.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "ClamAV_FileInfo",
|
||||
"version": "1.1",
|
||||
"author": "Brian Laskowski",
|
||||
"url": "https://github.com/Hestat/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Use Clamscan with custom rules",
|
||||
"dataTypeList": ["file"],
|
||||
"command": "ClamAV/pyclam_analyzer.py",
|
||||
"baseConfig": "ClamAV"
|
||||
|
||||
}
|
||||
|
53
analyzers/ClamAV/pyclam_analyzer.py
Normal file
53
analyzers/ClamAV/pyclam_analyzer.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
from cortexutils.analyzer import Analyzer
|
||||
|
||||
import os
|
||||
import pyclamd
|
||||
|
||||
cd = pyclamd.ClamdUnixSocket()
|
||||
|
||||
|
||||
class ClamAnalyzer(Analyzer):
|
||||
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
|
||||
def check(self, file: str) -> list:
|
||||
"""
|
||||
Checks a given file against all available yara rules
|
||||
|
||||
:param file: Path to file
|
||||
:returns: Python dictionary containing the results
|
||||
"""
|
||||
match = cd.scan_file(file)
|
||||
if match:
|
||||
return match[file][1]
|
||||
return None
|
||||
|
||||
# def summary(self, raw):
|
||||
# return raw
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
namespace = "ClamAV"
|
||||
predicate = "Match"
|
||||
|
||||
if raw["results"]:
|
||||
value = "{}".format(raw["results"])
|
||||
level = "malicious"
|
||||
else:
|
||||
value = "No matches"
|
||||
level = "safe"
|
||||
|
||||
taxonomies.append(self.build_taxonomy(level, namespace, predicate, value))
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
def run(self):
|
||||
if self.data_type == "file":
|
||||
self.report({"results": self.check(self.getParam("file"))})
|
||||
else:
|
||||
self.error("Wrong data type.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""This is necessary, because it is called from the CLI."""
|
||||
ClamAnalyzer().run()
|
2
analyzers/ClamAV/requirements.txt
Normal file
2
analyzers/ClamAV/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
cortexutils
|
||||
pyclamd
|
25
analyzers/IPVoid/IPVoid.json
Normal file
25
analyzers/IPVoid/IPVoid.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "IPVoid",
|
||||
"version": "1.0",
|
||||
"author": "Joel Snape @ Nettitude",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-v3",
|
||||
"description": "Determine whether an IP is present on any of the feeds consumed by IPVoid",
|
||||
"dataTypeList": ["ip"],
|
||||
"baseConfig": "IPVoid",
|
||||
"command": "IPVoid/ipvoid.py",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "key",
|
||||
"description": "API key for IPVoid",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"check_tlp": true,
|
||||
"max_tlp": 2,
|
||||
"auto_extract": false
|
||||
}
|
||||
}
|
64
analyzers/IPVoid/ipvoid.py
Executable file
64
analyzers/IPVoid/ipvoid.py
Executable file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
|
||||
from cortexutils.analyzer import Analyzer
|
||||
|
||||
class IPVoid(Analyzer):
|
||||
"""
|
||||
IPVoid API docs - https://app.apivoid.com/dashboard/api/ip-reputation/documentation/
|
||||
"""
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self.data_type == 'ip':
|
||||
api_key = self.get_param('config.key',None, 'Missing API key')
|
||||
ip = self.get_data()
|
||||
|
||||
url = 'https://endpoint.apivoid.com/iprep/v1/pay-as-you-go/?key={}&ip={}'.format(api_key,ip)
|
||||
response = requests.get(url)
|
||||
|
||||
if not (200 <= response.status_code < 300):
|
||||
self.error('Unable to query IPVoid API\n{}'.format(response.text))
|
||||
|
||||
json_response = response.json()
|
||||
|
||||
self.report(json_response)
|
||||
|
||||
else:
|
||||
self.notSupported()
|
||||
except Exception as e:
|
||||
self.unexpectedError(e)
|
||||
|
||||
|
||||
def summary(self, raw):
|
||||
try:
|
||||
taxonomies = list()
|
||||
|
||||
#Parse the information section of the report into a Location taxonomy. Only a subset of keys included for now
|
||||
|
||||
info = raw['data']['report']['information']
|
||||
|
||||
location = info['city_name']+'/'+info['country_name']
|
||||
taxonomies = taxonomies + [self.build_taxonomy('info','IPVoid','Location',location)]
|
||||
|
||||
#Parse blacklists info
|
||||
detections = raw['data']['report']['blacklists']['detections']
|
||||
engines = raw['data']['report']['blacklists']['engines_count']
|
||||
|
||||
if detections > 0:
|
||||
taxonomies = taxonomies + [self.build_taxonomy('suspicious','IPVoid','Blacklists',str(detections)+"/"+str(engines))]
|
||||
else:
|
||||
taxonomies = taxonomies + [self.build_taxonomy('info','IPVoid','Blacklists',str(detections)+"/"+str(engines))]
|
||||
|
||||
return({'taxonomies':taxonomies})
|
||||
|
||||
except Exception as e:
|
||||
if 'error' in raw:
|
||||
self.unexpectedError(raw['error'])
|
||||
else:
|
||||
self.unexpectedError(e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
IPVoid().run()
|
||||
|
2
analyzers/IPVoid/requirements.txt
Normal file
2
analyzers/IPVoid/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
cortexutils
|
||||
requests
|
@ -100,9 +100,6 @@ class InvestigateAnalyzer(Analyzer):
|
||||
except Exception as e:
|
||||
self.unexpectedError(e)
|
||||
|
||||
else:
|
||||
self.error('Unknown Investigate service or invalid data type')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
InvestigateAnalyzer().run()
|
||||
|
27
analyzers/ThreatGrid/ThreatGrid.json
Normal file
27
analyzers/ThreatGrid/ThreatGrid.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "ThreatGrid",
|
||||
"license": "MIT",
|
||||
"author": "Cisco Security",
|
||||
"url": "https://github.com/CiscoSecurity",
|
||||
"version": "1.0",
|
||||
"description": "Threat Grid Sandbox",
|
||||
"dataTypeList": ["file", "url", "hash"],
|
||||
"command": "ThreatGrid/ThreatGrid.py",
|
||||
"baseConfig": "ThreatGrid",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "tg_host",
|
||||
"description": "Threat Grid Host",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "Threat Grid API Key",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
273
analyzers/ThreatGrid/ThreatGrid.py
Normal file
273
analyzers/ThreatGrid/ThreatGrid.py
Normal file
@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
import requests
|
||||
|
||||
from time import sleep
|
||||
from cortexutils.analyzer import Analyzer
|
||||
from simplejson.errors import JSONDecodeError
|
||||
|
||||
|
||||
class ThreatGridAnalyzer(Analyzer):
|
||||
"""
|
||||
Threat Grid analyzer submits a 'file' or 'url' to Threat Grid for dynamic analysis and returns
|
||||
the results. Queryies for a 'hash' and returns the analysis results from the sample with the
|
||||
highest threat score submitted within the last 90 days.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
self.tg_host = self.get_param(
|
||||
"config.tg_host", None, "No Threat Grid host given."
|
||||
)
|
||||
self.api_key = self.get_param(
|
||||
"config.api_key", None, "No Threat Grid API Key given."
|
||||
)
|
||||
|
||||
# Create a Requests Session
|
||||
self.base_url = "https://{}/api/v2".format(self.tg_host)
|
||||
self.tg_session = requests.Session()
|
||||
auth_param = {"api_key": self.api_key}
|
||||
self.tg_session.params.update(auth_param)
|
||||
|
||||
def verify_response(self, response, key):
|
||||
"""Verify the HTTP status code is 200 and the expected key is present in the JSON
|
||||
"""
|
||||
try:
|
||||
return bool(response.status_code == 200 and key in response.json())
|
||||
except JSONDecodeError:
|
||||
self.error(
|
||||
"The server responded with HTTP status code 200 but did not return JSON"
|
||||
)
|
||||
|
||||
def wait_for_completion(self, sample_id):
|
||||
"""Check for sample completion every minute for 10 minutes
|
||||
"""
|
||||
url = self.base_url + "/samples/{}/state".format(sample_id)
|
||||
finished = False
|
||||
tries = 0
|
||||
while not finished and tries <= 20: # wait max 10 min check every 30 seconds
|
||||
if tries == 0:
|
||||
sleep(3) # It takes a second for the respnse to be available
|
||||
else:
|
||||
sleep(30)
|
||||
response = self.tg_session.get(url)
|
||||
state = response.json().get("data", {}).get("state")
|
||||
if state == "succ":
|
||||
finished = True
|
||||
elif state == "fail":
|
||||
self.get_fail_status(sample_id)
|
||||
tries += 1
|
||||
if not finished:
|
||||
self.error(
|
||||
"Timed out waiting for Sample analysis. Sample ID: {}".format(sample_id)
|
||||
)
|
||||
|
||||
def get_fail_status(self, sample_id):
|
||||
"""When a sample fails get the reason for the failure
|
||||
"""
|
||||
url = self.base_url + "/samples/{}".format(sample_id)
|
||||
response = self.tg_session.get(url)
|
||||
|
||||
if self.verify_response(response, "data"):
|
||||
status = response.json().get("data", []).get("status")
|
||||
if status:
|
||||
self.error(
|
||||
"Sample analysis failed with status. Sample ID: {} - {}".format(
|
||||
sample_id, status
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.error("Sample analysis failed. Sample ID: {}".format(sample_id))
|
||||
else:
|
||||
self.error(
|
||||
"Sample analysis failed, error getting fail status. Sample ID: {} recieved {} - {}".format(
|
||||
sample_id, response.status_code, response.text
|
||||
)
|
||||
)
|
||||
|
||||
def get_sample_id(self, submit_response):
|
||||
"""Verify response after submitting a sample and return the Sample ID
|
||||
"""
|
||||
if self.verify_response(submit_response, "data"):
|
||||
sample_id = submit_response.json()["data"]["id"]
|
||||
return sample_id
|
||||
else:
|
||||
self.error(
|
||||
"Error submitting sample, recieved {} - {}".format(
|
||||
submit_response.status_code, submit_response.text
|
||||
)
|
||||
)
|
||||
|
||||
def get_sample_results(self, sample_id):
|
||||
"""Collect the sample analysis results
|
||||
"""
|
||||
# Get Analysis JSON from Threat Grid
|
||||
analysis_response = self.get_analysis_json(sample_id)
|
||||
|
||||
# Get Sample Summary JSON from Threat Grid
|
||||
summary_response = self.get_summary(sample_id)
|
||||
|
||||
# Build report from summary and analyis results
|
||||
self.build_repot(analysis_response, summary_response)
|
||||
|
||||
def get_summary(self, sample_id):
|
||||
"""Get the sample summary information
|
||||
"""
|
||||
# Get Summary about sample from Threat Grid
|
||||
url = self.base_url + "/samples/{}/summary".format(sample_id)
|
||||
response = self.tg_session.get(url)
|
||||
|
||||
if self.verify_response(response, "data"):
|
||||
return response
|
||||
else:
|
||||
self.error(
|
||||
"Fetching sample summary failed. Sample ID: {} recieved {} - {}".format(
|
||||
sample_id, response.status_code, response.text
|
||||
)
|
||||
)
|
||||
|
||||
def get_analysis_json(self, sample_id):
|
||||
"""Get the sample analysis JSON
|
||||
"""
|
||||
url = self.base_url + "/samples/{}/analysis.json".format(sample_id)
|
||||
response = self.tg_session.get(url)
|
||||
|
||||
if self.verify_response(response, "metadata"):
|
||||
return response
|
||||
else:
|
||||
self.error(
|
||||
"Fetching analysis JSON failed. Sample ID: {} recieved {} - {}".format(
|
||||
sample_id, response.status_code, response.text
|
||||
)
|
||||
)
|
||||
|
||||
def build_repot(self, analysis_response, summary_response):
|
||||
"""Reformat elements from the analysis JSON into a custom report structure
|
||||
"""
|
||||
analysis_json = analysis_response.json()
|
||||
summary_json = summary_response.json()
|
||||
|
||||
raw_report = {}
|
||||
raw_report["host"] = self.tg_host
|
||||
raw_report["summary"] = summary_json.get("data")
|
||||
raw_report["summary"]["domains"] = len(analysis_json.get("domains"))
|
||||
raw_report["metadata"] = analysis_json.get("metadata")
|
||||
raw_report["threat"] = analysis_json.get("threat")
|
||||
raw_report["status"] = analysis_json.get("status")
|
||||
raw_report["iocs"] = analysis_json.get("iocs")
|
||||
raw_report["network"] = analysis_json.get("network")
|
||||
raw_report["domains"] = analysis_json.get("domains")
|
||||
|
||||
self.report(raw_report)
|
||||
|
||||
def run(self):
|
||||
|
||||
dataType = self.get_param("dataType")
|
||||
|
||||
if dataType == "file":
|
||||
file = self.get_param("file")
|
||||
filename = self.get_param("filename")
|
||||
|
||||
parameters = {"private": "true", "sample_filename": filename}
|
||||
|
||||
# Read file and submit to Threat Grid
|
||||
with open(file, "rb") as sample:
|
||||
submit_response = self.tg_session.post(
|
||||
self.base_url + "/samples",
|
||||
files={"sample": sample},
|
||||
params=parameters,
|
||||
)
|
||||
|
||||
# Verify response and store Sample ID
|
||||
sample_id = self.get_sample_id(submit_response)
|
||||
|
||||
# Wait for analysis completion
|
||||
self.wait_for_completion(sample_id)
|
||||
|
||||
# Get analysis results
|
||||
self.get_sample_results(sample_id)
|
||||
|
||||
elif dataType == "url":
|
||||
observable_url = self.get_param("data")
|
||||
|
||||
parameters = {"private": "true", "url": observable_url}
|
||||
|
||||
# Submit to Threat Grid
|
||||
submit_response = self.tg_session.post(
|
||||
self.base_url + "/samples", params=parameters
|
||||
)
|
||||
|
||||
# Verify response and store Sample ID
|
||||
sample_id = self.get_sample_id(submit_response)
|
||||
|
||||
# Wait for analysis completion
|
||||
self.wait_for_completion(sample_id)
|
||||
|
||||
# Get analysis results
|
||||
self.get_sample_results(sample_id)
|
||||
|
||||
elif dataType == "hash":
|
||||
observable_hash = self.get_param("data")
|
||||
|
||||
parameters = {
|
||||
"limit": 1,
|
||||
"state": "succ",
|
||||
"term": "sample",
|
||||
"sort_by": "threat",
|
||||
"sort_order": "desc",
|
||||
"after": "90 days ago",
|
||||
"q": observable_hash,
|
||||
}
|
||||
|
||||
query_response = self.tg_session.get(
|
||||
self.base_url + "/search/submissions", params=parameters
|
||||
)
|
||||
|
||||
# Verify response and store Sample ID
|
||||
if self.verify_response(query_response, "data"):
|
||||
query_response_json = query_response.json()
|
||||
current_item_count = query_response_json["data"]["current_item_count"]
|
||||
if current_item_count > 0:
|
||||
sample_id = query_response_json["data"]["items"][0]["item"][
|
||||
"sample"
|
||||
]
|
||||
else:
|
||||
self.error("No samples found in the last 90 days")
|
||||
else:
|
||||
self.error(
|
||||
"Error submitting file, recieved {} - {}".format(
|
||||
query_response.status_code, query_response.text
|
||||
)
|
||||
)
|
||||
|
||||
# Get analysis results
|
||||
self.get_sample_results(sample_id)
|
||||
|
||||
else:
|
||||
self.error("Data type currently not supported")
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
namespace = "TG"
|
||||
predicate = "Analysis"
|
||||
|
||||
threat = raw.get("threat", {})
|
||||
threat_score = threat.get("threat_score")
|
||||
|
||||
# Set level based on Threat Score
|
||||
if threat_score >= 90:
|
||||
level = "malicious"
|
||||
elif 90 > threat_score >= 50:
|
||||
level = "suspicious"
|
||||
elif threat_score < 50:
|
||||
level = "safe"
|
||||
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(level, namespace, predicate, value=threat_score)
|
||||
)
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ThreatGridAnalyzer().run()
|
2
analyzers/ThreatGrid/requirements.txt
Normal file
2
analyzers/ThreatGrid/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
cortexutils
|
||||
requests
|
42
analyzers/ThreatResponse/ThreatResponse.json
Normal file
42
analyzers/ThreatResponse/ThreatResponse.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "ThreatResponse",
|
||||
"license": "MIT",
|
||||
"author": "Cisco Security",
|
||||
"url": "https://github.com/CiscoSecurity",
|
||||
"version": "1.0",
|
||||
"description": "Threat Response",
|
||||
"dataTypeList": ["domain", "filename", "fqdn", "hash", "ip", "url"],
|
||||
"command": "ThreatResponse/ThreatResponse.py",
|
||||
"baseConfig": "ThreatResponse",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "region",
|
||||
"description": "Threat Response Region (us, eu, or apjc). Will default to 'us' region if left blank",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"description": "Threat Response Client ID",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "client_password",
|
||||
"description": "Threat Response API Client Password",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "extract_amp_targets",
|
||||
"description": "Would you like to extract AMP connector GUIDs as artifacts?",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
}
|
231
analyzers/ThreatResponse/ThreatResponse.py
Normal file
231
analyzers/ThreatResponse/ThreatResponse.py
Normal file
@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from cortexutils.analyzer import Analyzer
|
||||
from threatresponse import ThreatResponse
|
||||
|
||||
|
||||
class ThreatResponseAnalyzer(Analyzer):
|
||||
"""
|
||||
Cisco Threat Response analyzer
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Analyzer.__init__(self)
|
||||
self.region = self.get_param("config.region").lower()
|
||||
self.client_id = self.get_param(
|
||||
"config.client_id", None, "No Threat Response client ID given."
|
||||
)
|
||||
self.client_password = self.get_param(
|
||||
"config.client_password", None, "No Threat Response client Password given."
|
||||
)
|
||||
self.extract_amp_targets = self.get_param("config.extract_amp_targets", False)
|
||||
|
||||
# Validate that the supplied region is valid
|
||||
if self.region and self.region not in ("us", "eu", "apjc"):
|
||||
self.error(
|
||||
"{} is not a valid Threat Response region. Must be 'us', 'eu', or 'apjc'".format(
|
||||
self.region
|
||||
)
|
||||
)
|
||||
|
||||
# Set region to '' if 'us' was supplied
|
||||
if self.region == "us":
|
||||
self.region = ""
|
||||
|
||||
# Create Threat Response client
|
||||
self.client = ThreatResponse(
|
||||
client_id=self.client_id,
|
||||
client_password=self.client_password,
|
||||
region=self.region,
|
||||
)
|
||||
|
||||
def run(self):
|
||||
def identify_hash(observable):
|
||||
"""Validate the provided hash is a supported type
|
||||
"""
|
||||
# RegEx for supported checksum types MD5, SHA1, SHA256
|
||||
hash_mapping = {
|
||||
re.compile(r"^[A-Za-z0-9]{32}$"): "md5",
|
||||
re.compile(r"^[A-Za-z0-9]{40}$"): "sha1",
|
||||
re.compile(r"^[A-Za-z0-9]{64}$"): "sha256",
|
||||
}
|
||||
|
||||
for expression in hash_mapping:
|
||||
if expression.match(observable):
|
||||
return hash_mapping[expression]
|
||||
|
||||
def parse_verdicts(response_json):
|
||||
"""Parse response from Threat Response and extract verdicts
|
||||
"""
|
||||
verdicts = []
|
||||
for module in response_json.get("data", []):
|
||||
module_name = module["module"]
|
||||
|
||||
for doc in module.get("data", {}).get("verdicts", {}).get("docs", []):
|
||||
verdicts.append(
|
||||
{
|
||||
"observable_value": doc["observable"]["value"],
|
||||
"observable_type": doc["observable"]["type"],
|
||||
"expiration": doc["valid_time"]["end_time"],
|
||||
"module": module_name,
|
||||
"disposition_name": doc["disposition_name"],
|
||||
}
|
||||
)
|
||||
|
||||
return verdicts
|
||||
|
||||
def parse_targets(response_json):
|
||||
"""Parse response Threat Response and extract targets
|
||||
"""
|
||||
result = []
|
||||
for module in response_json.get("data", []):
|
||||
module_name = module["module"]
|
||||
module_type = module["module-type"]
|
||||
targets = []
|
||||
|
||||
for doc in module.get("data", {}).get("sightings", {}).get("docs", []):
|
||||
|
||||
for target in doc.get("targets", []):
|
||||
element = deepcopy(target)
|
||||
element.pop("observed_time", None)
|
||||
if element not in targets:
|
||||
targets.append(element)
|
||||
|
||||
if targets:
|
||||
result.append(
|
||||
{
|
||||
"module": module_name,
|
||||
"module_type": module_type,
|
||||
"targets": targets,
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
# Map The Hive observable types to Threat Response observable types
|
||||
observable_mapping = {
|
||||
"domain": "domain",
|
||||
"mail": "email",
|
||||
"mail_subject": "email_subject",
|
||||
"filename": "file_name",
|
||||
"fqdn": "domain",
|
||||
"hash": None,
|
||||
"ip": "ip",
|
||||
"url": "url",
|
||||
}
|
||||
|
||||
# Map the provided region to the FQDN
|
||||
host_mapping = {
|
||||
"": "visibility.amp.cisco.com",
|
||||
"us": "visibility.amp.cisco.com",
|
||||
"eu": "visibility.eu.amp.cisco.com",
|
||||
"apjc": "visibility.apjc.amp.cisco.com",
|
||||
}
|
||||
|
||||
dataType = self.get_param("dataType")
|
||||
|
||||
# Validate the supplied observable type is supported
|
||||
if dataType in observable_mapping.keys():
|
||||
observable = self.get_data() # Get the observable data
|
||||
|
||||
# If the observable type is 'hash' determine which type of hash
|
||||
# Threat Response only supports MD5, SHA1, SHA256
|
||||
if dataType == "hash":
|
||||
hash_type = identify_hash(observable)
|
||||
if hash_type:
|
||||
observable_mapping["hash"] = hash_type
|
||||
else:
|
||||
self.error(
|
||||
"{} is not a valid MD5, SHA1, or SHA256".format(observable)
|
||||
)
|
||||
|
||||
# Format the payload to be sent to the Threat Response API
|
||||
payload = [{"value": observable, "type": observable_mapping[dataType]}]
|
||||
|
||||
# Query Threat Response Enrich API
|
||||
response = self.client.enrich.observe.observables(payload)
|
||||
|
||||
# Parse verdicts from response for display
|
||||
verdicts = parse_verdicts(response)
|
||||
|
||||
# Parse targets from response for display
|
||||
targets = parse_targets(response)
|
||||
|
||||
# Build raw report
|
||||
raw_report = {
|
||||
"response": response,
|
||||
"targets": targets,
|
||||
"verdicts": verdicts,
|
||||
"host": host_mapping[self.region],
|
||||
"observable": observable,
|
||||
}
|
||||
|
||||
self.report(raw_report)
|
||||
|
||||
else:
|
||||
self.error("Data type {} not supported".format(dataType))
|
||||
|
||||
def summary(self, raw):
|
||||
taxonomies = []
|
||||
namespace = "TR"
|
||||
|
||||
verdicts = raw.get("verdicts", [])
|
||||
|
||||
# Map Threat Response dispositions to The Hive levels
|
||||
level_mapping = {
|
||||
"Clean": "safe",
|
||||
"Common": "safe",
|
||||
"Malicious": "malicious",
|
||||
"Suspicious": "suspicious",
|
||||
"Unknown": "info",
|
||||
}
|
||||
|
||||
for verdict in verdicts:
|
||||
disposition_name = verdict.get(
|
||||
"disposition_name"
|
||||
) # Clean, Common, Malicious, Suspicious, Unknown
|
||||
module = verdict.get("module")
|
||||
|
||||
taxonomies.append(
|
||||
self.build_taxonomy(
|
||||
level_mapping[disposition_name], namespace, module, disposition_name
|
||||
)
|
||||
)
|
||||
|
||||
# Inform if not module returned a verdict
|
||||
if len(verdicts) < 1:
|
||||
taxonomies.append(
|
||||
self.build_taxonomy("info", namespace, "Enrich", "No Verdicts")
|
||||
)
|
||||
# level, namespace, predicate, value
|
||||
|
||||
return {"taxonomies": taxonomies}
|
||||
|
||||
def artifacts(self, raw):
|
||||
artifacts = []
|
||||
|
||||
if self.extract_amp_targets:
|
||||
for module in raw.get("targets", []):
|
||||
if module.get("module_type") == "AMPInvestigateModule":
|
||||
for target in module.get("targets", []):
|
||||
for observable in target.get("observables", []):
|
||||
if observable.get("type") == "hostname":
|
||||
hostname = observable.get("value")
|
||||
if observable.get("type") == "amp_computer_guid":
|
||||
guid = observable.get("value")
|
||||
if guid:
|
||||
tags = []
|
||||
if hostname:
|
||||
tags.append("AMP Hostname:{}".format(hostname))
|
||||
tags.append("AMP GUID")
|
||||
artifacts.append(
|
||||
self.build_artifact("other", guid, tags=tags)
|
||||
)
|
||||
|
||||
return artifacts
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ThreatResponseAnalyzer().run()
|
3
analyzers/ThreatResponse/requirements.txt
Normal file
3
analyzers/ThreatResponse/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
requests
|
||||
cortexutils
|
||||
threatresponse
|
240
responders/AMPforEndpoints/AMPforEndpoints.py
Normal file
240
responders/AMPforEndpoints/AMPforEndpoints.py
Normal file
@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
from cortexutils.responder import Responder
|
||||
|
||||
|
||||
class AMPforEndpoints(Responder):
|
||||
def __init__(self):
|
||||
Responder.__init__(self)
|
||||
self.service = self.get_param("config.service", None, "Service Missing")
|
||||
self.amp_cloud = self.get_param("config.amp_cloud", None, "AMP HOST Missing")
|
||||
self.client_id = self.get_param("config.client_id", None, "Client ID Missing")
|
||||
self.api_key = self.get_param("config.api_key", None, "API Key Missing")
|
||||
|
||||
if self.service in ("scdadd", "scdremove"):
|
||||
self.scd_guid = self.get_param(
|
||||
"config.scd_guid", None, "Simple Custom Detectoin GUID Missing"
|
||||
)
|
||||
if self.service in ("moveguid"):
|
||||
self.group_guid = self.get_param(
|
||||
"config.group_guid", None, "Group GUID Missing"
|
||||
)
|
||||
if self.service in ("isolationstart"):
|
||||
self.unlock_code = self.get_param("config.unlock_code", None)
|
||||
|
||||
self.amp_session = requests.Session()
|
||||
self.amp_session.auth = (self.client_id, self.api_key)
|
||||
self.amp_session.headers.update(
|
||||
{
|
||||
"User-Agent": "AMPforEndpoints-Cortex-Responder",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
}
|
||||
)
|
||||
|
||||
def run(self):
|
||||
def parse_amp_error(error_response):
|
||||
"""Parse AMP for Endponts error response
|
||||
Return the human readable error message
|
||||
"""
|
||||
try:
|
||||
response = error_response.json()
|
||||
errors = response.get("errors", [])
|
||||
details = errors[0].get("details", [])
|
||||
error = details[0]
|
||||
return error
|
||||
except IndexError:
|
||||
return "Something went wrong! Recieved status code: {}".format(
|
||||
error_response.status_code
|
||||
)
|
||||
|
||||
def validate_guid(guid):
|
||||
"""Validate the provided GUIDs is the correct format
|
||||
"""
|
||||
expression = r"^[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}$"
|
||||
return bool(re.match(expression, guid))
|
||||
|
||||
def scd_add(amp_cloud, scd_guid, sha256, caseId, title):
|
||||
"""Add a SHA256 to a Simple Custom Detection List
|
||||
"""
|
||||
url = "https://{}/v1/file_lists/{}/files/{}".format(
|
||||
amp_cloud, scd_guid, sha256
|
||||
)
|
||||
|
||||
body = {
|
||||
"description": "The Hive Case ID: {} Case Title: {}".format(
|
||||
caseId, title
|
||||
)
|
||||
}
|
||||
body = json.dumps(body)
|
||||
response = self.amp_session.post(url, data=body)
|
||||
|
||||
if response.status_code == 201:
|
||||
self.report({"message": "SHA256 Added to SCD"})
|
||||
if response.status_code == 409:
|
||||
self.error("SHA256 already on SCD list")
|
||||
else:
|
||||
self.error("Failed to add to blacklist.")
|
||||
|
||||
def scd_remove(amp_cloud, scd_guid, sha256):
|
||||
"""Remove a SHA256 from a Simple Custom Detection List
|
||||
"""
|
||||
url = "https://{}/v1/file_lists/{}/files/{}".format(
|
||||
amp_cloud, scd_guid, sha256
|
||||
)
|
||||
|
||||
response = self.amp_session.delete(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.report({"message": "SHA256 removed from SCD"})
|
||||
else:
|
||||
error = parse_amp_error(response)
|
||||
self.error(error)
|
||||
|
||||
def move_guid(amp_cloud, group_guid, connector_guid):
|
||||
"""Move a connector GUID to a new group
|
||||
"""
|
||||
url = "https://{}/v1/computers/{}".format(amp_cloud, connector_guid)
|
||||
|
||||
body = {"group_guid": group_guid}
|
||||
body = json.dumps(body)
|
||||
|
||||
response = self.amp_session.patch(url, data=body)
|
||||
response_json = response.json()
|
||||
data = response_json.get("data", {})
|
||||
hostname = data.get("hostname", "for uknown hostname")
|
||||
|
||||
if response.status_code == 202:
|
||||
self.report({"message": "Connector {} moved".format(hostname)})
|
||||
else:
|
||||
error = parse_amp_error(response)
|
||||
self.error(error)
|
||||
|
||||
def isolation_start(amp_cloud, connector_guid, unlock_code):
|
||||
"""Send request to start host isolation for a connector
|
||||
"""
|
||||
url = "https://{}/v1/computers/{}/isolation".format(
|
||||
amp_cloud, connector_guid
|
||||
)
|
||||
|
||||
if unlock_code:
|
||||
body = {"unlock_code": unlock_code}
|
||||
body = json.dumps(body)
|
||||
response = self.amp_session.put(url, data=body)
|
||||
else:
|
||||
response = self.amp_session.put(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.report(
|
||||
{
|
||||
"message": "Request sent to start isolation for connector: {}".format(
|
||||
connector_guid
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
error = parse_amp_error(response)
|
||||
self.error(error)
|
||||
|
||||
def isolation_stop(amp_cloud, connector_guid):
|
||||
"""Send request to stop host isolation for a connector
|
||||
"""
|
||||
url = "https://{}/v1/computers/{}/isolation".format(
|
||||
amp_cloud, connector_guid
|
||||
)
|
||||
|
||||
response = self.amp_session.delete(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.report(
|
||||
{
|
||||
"message": "Request sent to stop isolation for connector: {}".format(
|
||||
connector_guid
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
error = parse_amp_error(response)
|
||||
self.error(error)
|
||||
|
||||
Responder.run(self)
|
||||
|
||||
dataType = self.get_param("data.dataType")
|
||||
|
||||
if dataType == "hash" and self.service in ("scdadd", "scdremove"):
|
||||
sha256 = self.get_param("data.data", None)
|
||||
caseId = self.get_param("data.case.caseId", None, "caseId is missing")
|
||||
title = self.get_param("data.case.title", None, "title is missing").encode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
# Valide a valid SHA256 was provided
|
||||
if not re.match(r"^[A-Fa-f0-9]{64}$", sha256):
|
||||
self.error("{} is not a SHA256".format(sha256))
|
||||
|
||||
# Add SHA256 to Simple Custom Detection list
|
||||
if self.service == "scdadd":
|
||||
scd_add(self.amp_cloud, self.scd_guid, sha256, caseId, title)
|
||||
|
||||
# Remove SHA256 from Simple Custom Detection list
|
||||
if self.service == "scdremove":
|
||||
scd_remove(self.amp_cloud, self.scd_guid, sha256)
|
||||
|
||||
if dataType == "other" and self.service in (
|
||||
"moveguid",
|
||||
"isolationstart",
|
||||
"isolationstop",
|
||||
):
|
||||
connector_guid = self.get_param("data.data", None)
|
||||
|
||||
# Validate the connector GUID is the right format
|
||||
if not validate_guid(connector_guid):
|
||||
self.error(
|
||||
"{} is not a valid AMP connector GUID".format(connector_guid)
|
||||
)
|
||||
|
||||
# Validate the Group GUID is the right format
|
||||
if self.service in ("moveguid") and not validate_guid(self.group_guid):
|
||||
self.error("{} is not a valid AMP Group GUID".format(self.group_guid))
|
||||
|
||||
# Move the connector GUID to a new group
|
||||
if self.service in ("moveguid"):
|
||||
move_guid(self.amp_cloud, self.group_guid, connector_guid)
|
||||
|
||||
# Start host isolation
|
||||
if self.service in ("isolationstart"):
|
||||
# Check if the unlock_code is less than 24 characters
|
||||
if self.unlock_code and self.unlock_code and len(self.unlock_code) > 24:
|
||||
self.error(
|
||||
"Validation failed: Unlock Code is invalid, Unlock Code is too long. (Maximum 24 characters)"
|
||||
)
|
||||
# Check if the unlock_code contains spaces
|
||||
if self.unlock_code and bool(" " in self.unlock_code):
|
||||
self.error(
|
||||
"Validation failed: Unlock Code is invalid, Unlock Code cannot contain spaces"
|
||||
)
|
||||
isolation_start(self.amp_cloud, connector_guid, self.unlock_code)
|
||||
|
||||
# Stop host isolation
|
||||
if self.service in ("isolationstop"):
|
||||
isolation_stop(self.amp_cloud, connector_guid)
|
||||
|
||||
# Return an error for all other datatypes
|
||||
self.error(
|
||||
"Incorrect dataType received '{}' as '{}'".format(
|
||||
self.get_param("data.data", None), dataType
|
||||
)
|
||||
)
|
||||
|
||||
def operations(self, raw):
|
||||
if self.service == "scdadd":
|
||||
return [self.build_operation("AddTagToArtifact", tag="AMP:blocked")]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
AMPforEndpoints().run()
|
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "AMPforEndpoints_IsolationStart",
|
||||
"version": "1.0",
|
||||
"author": "Cisco Security",
|
||||
"url": "https://github.com/CiscoSecurity",
|
||||
"license": "MIT",
|
||||
"description": "Start host isolation for an AMP for Endpoints connector",
|
||||
"dataTypeList": ["thehive:case_artifact"],
|
||||
"command": "AMPforEndpoints/AMPforEndpoints.py",
|
||||
"baseConfig": "AMPforEndpoints",
|
||||
"config": {
|
||||
"service": "isolationstart"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "amp_cloud",
|
||||
"description": "FQDN of the AMP for Endpoints cloud to interact with",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"description": "Client ID for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "API Key for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "unlock_code",
|
||||
"description": "Custom unlock code used to stop isolation from the endpoint (Maximum 24 characters)",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "AMPforEndpoints_IsolationStop",
|
||||
"version": "1.0",
|
||||
"author": "Cisco Security",
|
||||
"url": "https://github.com/CiscoSecurity",
|
||||
"license": "MIT",
|
||||
"description": "Stop host isolation for an AMP for Endpoints connector",
|
||||
"dataTypeList": ["thehive:case_artifact"],
|
||||
"command": "AMPforEndpoints/AMPforEndpoints.py",
|
||||
"baseConfig": "AMPforEndpoints",
|
||||
"config": {
|
||||
"service": "isolationstop"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "amp_cloud",
|
||||
"description": "FQDN of the AMP for Endpoints cloud to interact with",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"description": "Client ID for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "API Key for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
44
responders/AMPforEndpoints/AMPforEndpoints_MoveGUID.json
Normal file
44
responders/AMPforEndpoints/AMPforEndpoints_MoveGUID.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "AMPforEndpoints_MoveGUID",
|
||||
"version": "1.0",
|
||||
"author": "Cisco Security",
|
||||
"url": "https://github.com/CiscoSecurity",
|
||||
"license": "MIT",
|
||||
"description": "Move an AMP for Endpoints connector GUID to a different Group",
|
||||
"dataTypeList": ["thehive:case_artifact"],
|
||||
"command": "AMPforEndpoints/AMPforEndpoints.py",
|
||||
"baseConfig": "AMPforEndpoints",
|
||||
"config": {
|
||||
"service": "moveguid"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "amp_cloud",
|
||||
"description": "FQDN of the AMP for Endpoints cloud to interact with",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"description": "Client ID for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "API Key for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "group_guid",
|
||||
"description": "AMP for Endpoints Group GUID for the group connectors will be moved to",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
44
responders/AMPforEndpoints/AMPforEndpoints_SCDAdd.json
Normal file
44
responders/AMPforEndpoints/AMPforEndpoints_SCDAdd.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "AMPforEndpoints_SCDAdd",
|
||||
"version": "1.0",
|
||||
"author": "Cisco Security",
|
||||
"url": "https://github.com/CiscoSecurity",
|
||||
"license": "MIT",
|
||||
"description": "Add a SHA256 to an AMP for Endpoints Simple Custom Detection list",
|
||||
"dataTypeList": ["thehive:case_artifact"],
|
||||
"command": "AMPforEndpoints/AMPforEndpoints.py",
|
||||
"baseConfig": "AMPforEndpoints",
|
||||
"config": {
|
||||
"service": "scdadd"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "amp_cloud",
|
||||
"description": "FQDN of the AMP for Endpoints cloud to interact with",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"description": "Client ID for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "API Key for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "scd_guid",
|
||||
"description": "AMP for Endpoints Simple Custom Detection GUID",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
44
responders/AMPforEndpoints/AMPforEndpoints_SCDRemove.json
Normal file
44
responders/AMPforEndpoints/AMPforEndpoints_SCDRemove.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "AMPforEndpoints_SCDRemove",
|
||||
"version": "1.0",
|
||||
"author": "Cisco Security",
|
||||
"url": "https://github.com/CiscoSecurity",
|
||||
"license": "MIT",
|
||||
"description": "Remove a SHA256 to an AMP for Endpoints Simple Custom Detection list",
|
||||
"dataTypeList": ["thehive:case_artifact"],
|
||||
"command": "AMPforEndpoints/AMPforEndpoints.py",
|
||||
"baseConfig": "AMPforEndpoints",
|
||||
"config": {
|
||||
"service": "scdremove"
|
||||
},
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "amp_cloud",
|
||||
"description": "FQDN of the AMP for Endpoints cloud to interact with",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"description": "Client ID for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"description": "API Key for AMP for Endpoints",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "scd_guid",
|
||||
"description": "AMP for Endpoints Simple Custom Detection GUID",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
2
responders/AMPforEndpoints/requirements.txt
Normal file
2
responders/AMPforEndpoints/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
requests
|
||||
cortexutils
|
88
responders/Redmine/Redmine_Issue.json
Normal file
88
responders/Redmine/Redmine_Issue.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "Redmine_Issue",
|
||||
"version": "1.0",
|
||||
"author": "Marc-André DOLL",
|
||||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
|
||||
"license": "AGPL-V3",
|
||||
"description": "Create a redmine issue from a case",
|
||||
"dataTypeList": [
|
||||
"thehive:case",
|
||||
"thehive:case_task"
|
||||
],
|
||||
"command": "Redmine/redmine.py",
|
||||
"baseConfig": "Redmine",
|
||||
"configurationItems": [
|
||||
{
|
||||
"name": "instance_name",
|
||||
"description": "Name of the Redmine instance",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"defaultValue": "redmine"
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"description": "URL where to find the Redmine API",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"description": "Username to log into Redmine",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "Password to log into Redmine",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "project_field",
|
||||
"description": "Name of the custom field containing the Redmine project to use when creating the issue",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "tracker_field",
|
||||
"description": "Name of the custom field containing the Redmine tracker to use when creating the issue",
|
||||
"multi": false,
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "assignee_field",
|
||||
"description": "Name of the custom field containing the Redmine assignee to use when creating the issue",
|
||||
"multi": false,
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "reference_field",
|
||||
"description": "Name of the case custom field in which to store the opened issue. If not defined, this information will not be stored",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"multi": false
|
||||
},
|
||||
{
|
||||
"name": "opening_status",
|
||||
"description": "Status used when opening a Redmine issue (if not defined here, will use the default opening status from the Redmine Workflow)",
|
||||
"type": "string",
|
||||
"multi": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "closing_task",
|
||||
"description": "Closing the task after successfully creating the Redmine issue",
|
||||
"type": "boolean",
|
||||
"multi": false,
|
||||
"defaultValue": false,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
67
responders/Redmine/redmine.py
Executable file
67
responders/Redmine/redmine.py
Executable file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
from cortexutils.responder import Responder
|
||||
import redmine_client
|
||||
|
||||
class Redmine(Responder):
|
||||
def __init__(self):
|
||||
Responder.__init__(self)
|
||||
self.instance_name = self.get_param('config.instance_name', 'redmine')
|
||||
self.instance_url = self.get_param('config.url', None, 'Missing Redmine URL')
|
||||
self.client = redmine_client.RedmineClient(
|
||||
baseurl=self.instance_url,
|
||||
username=self.get_param('config.username', None, 'Missing username'),
|
||||
password=self.get_param('config.password', None, 'Missing password'))
|
||||
self.project_field = self.get_param('config.project_field', None, 'Missing custom field for Redmine project')
|
||||
self.tracker_field = self.get_param('config.tracker_field', None, 'Missing custom field for Redmine tracker')
|
||||
self.assignee_field = self.get_param('config.assignee_field', None, 'Missing custom field for Redmine assignee')
|
||||
self.reference_field = self.get_param('config.reference_field', None)
|
||||
self.closing_task = self.get_param('config.closing_task', False)
|
||||
|
||||
def run(self):
|
||||
issue_data = {}
|
||||
if self.data_type == 'thehive:case':
|
||||
issue_data = self.extract_case_data()
|
||||
elif self.data_type == 'thehive:case_task':
|
||||
issue_data = self.extract_case_data('data.case')
|
||||
else:
|
||||
self.error('Invalid dataType')
|
||||
try:
|
||||
issue = self.client.create_issue(
|
||||
title=issue_data['title'], body=issue_data['description'],
|
||||
project=issue_data['project'], tracker=issue_data['tracker'],
|
||||
status=issue_data['status'], priority=issue_data['severity'],
|
||||
assignee=issue_data['assignee'])
|
||||
self.report({
|
||||
'message': 'issue {} created'.format(issue['issue']['id']),
|
||||
'instance': {'name': self.instance_name, "url": self.instance_url},
|
||||
'issue': issue
|
||||
})
|
||||
except Exception as e:
|
||||
self.error(str(e))
|
||||
|
||||
def operations(self, raw):
|
||||
ops = []
|
||||
if self.reference_field:
|
||||
ops.append(self.build_operation('AddCustomFields', name=self.reference_field, tpe='string', value='{}#{}'.format(self.instance_name, raw['issue']['issue']['id'])))
|
||||
if self.data_type == 'thehive:case_task' and self.closing_task:
|
||||
ops.append(self.build_operation('CloseTask'))
|
||||
return ops
|
||||
|
||||
def extract_case_data(self, data_root='data'):
|
||||
issue_data = {}
|
||||
issue_data['title'] = self.get_param('{}.title'.format(data_root), None, 'Case title is missing')
|
||||
issue_data['description'] = self.get_param('{}.description'.format(data_root), None, 'Case description is missing')
|
||||
issue_data['severity'] = self.get_param('{}.severity'.format(data_root))
|
||||
if self.project_field:
|
||||
issue_data['project'] = self.get_param('{}.customFields.{}.string'.format(data_root, self.project_field), None, 'Project not defined in case')
|
||||
if self.tracker_field:
|
||||
issue_data['tracker'] = self.get_param('{}.customFields.{}.string'.format(data_root, self.tracker_field), None, 'Tracker not defined in case')
|
||||
if self.assignee_field:
|
||||
issue_data['assignee'] = self.get_param('{}.customFields.{}.string'.format(data_root, self.assignee_field), None)
|
||||
issue_data['status'] = self.get_param('config.opening_status')
|
||||
return issue_data
|
||||
|
||||
if __name__ == '__main__':
|
||||
Redmine().run()
|
77
responders/Redmine/redmine_client.py
Normal file
77
responders/Redmine/redmine_client.py
Normal file
@ -0,0 +1,77 @@
|
||||
import requests
|
||||
|
||||
class RedmineClient:
|
||||
def __init__(self, baseurl, username, password):
|
||||
self.base_url = baseurl
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({'content-type': 'application/json'})
|
||||
self.session.auth = (username, password)
|
||||
|
||||
def create_issue(self, title=None, body=None, project=None, tracker=None,
|
||||
priority=None, status=None, assignee=None):
|
||||
payload = {
|
||||
'issue': {
|
||||
'subject': title,
|
||||
'description': body,
|
||||
'project_id': project,
|
||||
'tracker_id': self.get_tracker_id(tracker),
|
||||
'priority_id': priority,
|
||||
'status_id': self.get_status_id(status),
|
||||
'assigned_to_id': self.get_assignee_id(project, assignee)
|
||||
}
|
||||
}
|
||||
url = self.base_url + '/issues.json'
|
||||
response = self.session.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
if 'error' in result:
|
||||
raise RedmineClientError(result['error'])
|
||||
return result
|
||||
|
||||
def get_tracker_id(self, name):
|
||||
url = self.base_url + '/trackers.json'
|
||||
id = None
|
||||
trackers = self.session.get(url)
|
||||
trackers.raise_for_status()
|
||||
for p in trackers.json()['trackers']:
|
||||
if p['name'] == name:
|
||||
id = p['id']
|
||||
break
|
||||
return id
|
||||
|
||||
def get_status_id(self, name):
|
||||
url = self.base_url + '/issue_statuses.json'
|
||||
id = None
|
||||
issue_statuses = self.session.get(url)
|
||||
issue_statuses.raise_for_status()
|
||||
for p in issue_statuses.json()['issue_statuses']:
|
||||
if p['name'] == name:
|
||||
id = p['id']
|
||||
break
|
||||
return id
|
||||
|
||||
def get_assignee_id(self, project, assignee):
|
||||
url = '{}/projects/{}/memberships.json'.format(self.base_url, project)
|
||||
id = None
|
||||
payload = {'offset': 0}
|
||||
total_count = 0
|
||||
while id is None and payload['offset'] <= total_count:
|
||||
response = self.session.get(url, params=payload)
|
||||
response.raise_for_status()
|
||||
for member in response.json()['memberships']:
|
||||
if 'user' in member:
|
||||
if assignee == member['user']['name']:
|
||||
id = member['user']['id']
|
||||
break
|
||||
elif 'group' in member:
|
||||
if assignee == member['group']['name']:
|
||||
id = member['group']['id']
|
||||
break
|
||||
total_count = response.json()['total_count']
|
||||
payload['offset'] += response.json()['limit']
|
||||
return id
|
||||
|
||||
|
||||
class RedmineClientError(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
2
responders/Redmine/requirements.txt
Normal file
2
responders/Redmine/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
requests
|
||||
cortexutils
|
33
thehive-templates/ClamAV_1_0/long.html
Normal file
33
thehive-templates/ClamAV_1_0/long.html
Normal file
@ -0,0 +1,33 @@
|
||||
<div class="panel panel-danger" ng-if="success && content.results.length > 0">
|
||||
<div class="panel-heading">
|
||||
ClamAV Report
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Match</dt>
|
||||
<dd>{{content.results}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-success" ng-if="success && content.results.length == 0">
|
||||
<div class="panel-heading">
|
||||
ClamAV Report
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<span>No matches.</span>
|
||||
</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> ClamAV : </dt>
|
||||
<dd class="wrap">{{content.errorMessage}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
3
thehive-templates/ClamAV_1_0/short.html
Normal file
3
thehive-templates/ClamAV_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>
|
60
thehive-templates/IPVoid_1_0/long.html
Normal file
60
thehive-templates/IPVoid_1_0/long.html
Normal file
@ -0,0 +1,60 @@
|
||||
<div class="panel panel-info" ng-if="success">
|
||||
<div class="panel-heading">
|
||||
IPVoid lookup of <strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div ng-show="content.data.report.anonymity.is_vpn" class="alert alert-warning">VPN</div>
|
||||
<div ng-show="content.data.report.anonymity.is_proxy" class="alert alert-warning">Proxy</div>
|
||||
<div ng-show="content.data.report.anonymity.is_webproxy" class="alert alert-warning">Web-proxy</div>
|
||||
<div ng-show="content.data.report.anonymity.is_hosting" class="alert alert-info">Hosting provider</div>
|
||||
<div ng-show="content.data.report.anonymity.is_tor" class="alert alert-warning">Tor</div>
|
||||
|
||||
<dl class="dl-horizontal" >
|
||||
|
||||
<dt>Location:</dt>
|
||||
<dd>
|
||||
{{[content.data.report.information.city_name, content.data.report.information.region_name, content.data.report.information.country_name, content.data.report.information.continent_name].join(' / ')}}
|
||||
</dd>
|
||||
|
||||
<dt ng-if="content.data.report.information.reverse_dns!=''">
|
||||
RDNS:
|
||||
</dt>
|
||||
<dd ng-if="content.data.report.information.reverse_dns!=''">
|
||||
{{content.data.report.information.reverse_dns}}
|
||||
</dd>
|
||||
|
||||
<dt ng-if="content.data.report.information.isp!=''">
|
||||
ISP:
|
||||
</dt>
|
||||
<dd ng-if="content.data.report.information.isp!=''">
|
||||
{{content.data.report.information.isp}}
|
||||
</dd>
|
||||
|
||||
<dt>Blocklists {{content.data.report.blacklists.detections}}/{{content.data.report.blacklists.engines_count}}:
|
||||
</dt>
|
||||
|
||||
<dd ng-if="content.data.report.blacklists.detections > 0">
|
||||
<div ng-repeat="value in content.data.report.blacklists.engines" ng-if="value.detected==true">
|
||||
|
||||
<a href="{{value.reference}}">{{value.engine}}</a>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
<dd ng-if="content.data.report.blacklists.detections==0">
|
||||
None
|
||||
</dd>
|
||||
|
||||
</dl>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
3
thehive-templates/IPVoid_1_0/short.html
Normal file
3
thehive-templates/IPVoid_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>
|
264
thehive-templates/ThreatGrid_1_0/long.html
Normal file
264
thehive-templates/ThreatGrid_1_0/long.html
Normal file
@ -0,0 +1,264 @@
|
||||
<style>
|
||||
td.critical {
|
||||
color: #fff;
|
||||
background-color: #cf2030 !important;
|
||||
fill: #cf2030;
|
||||
}
|
||||
|
||||
td.high {
|
||||
color: #fff;
|
||||
background-color: #ff7300 !important;
|
||||
fill: #ff7300;
|
||||
}
|
||||
|
||||
td.medium {
|
||||
color: #000;
|
||||
background-color: #fc0 !important;
|
||||
fill: #fc0;
|
||||
}
|
||||
|
||||
td.low {
|
||||
color: #fff;
|
||||
background-color: #b8c5db !important;
|
||||
fill: #b8c5db;
|
||||
}
|
||||
|
||||
.odns-verdict {
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
margin: 1px 5px 1px 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.malicious {
|
||||
color: #fff;
|
||||
background-color: #cf2030;
|
||||
}
|
||||
|
||||
.indeterminate {
|
||||
color: #39393b;
|
||||
background-color: #fc0;
|
||||
}
|
||||
|
||||
.innocuous {
|
||||
color: #fff;
|
||||
background-color: #6cc04a;
|
||||
}
|
||||
|
||||
.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Success -->
|
||||
<div class="panel panel-info" ng-if="success">
|
||||
<div class="panel-heading">
|
||||
Summary
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Threat Score</dt>
|
||||
<dd class="wrap">{{content.threat.threat_score}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.summary.times_seen">
|
||||
<dt>Times Seen</dt>
|
||||
<dd class="wrap">{{content.summary.times_seen}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.status.id">
|
||||
<dt>Sample ID</dt>
|
||||
<dd class="wrap">{{content.status.id}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.summary.first_seen">
|
||||
<dt>First Seen</dt>
|
||||
<dd class="wrap">{{content.summary.first_seen}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.metadata.sandcastle_env.display_name">
|
||||
<dt>OS</dt>
|
||||
<dd class="wrap">{{content.metadata.sandcastle_env.display_name}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.summary.last_seen">
|
||||
<dt>Last Seen</dt>
|
||||
<dd class="wrap">{{content.summary.last_seen}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.summary.run_start">
|
||||
<dt>Started</dt>
|
||||
<dd class="wrap">{{content.summary.run_start}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.summary.magic_type">
|
||||
<dt>Magic Type</dt>
|
||||
<dd class="wrap">{{content.summary.magic_type}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.summary.run_stop">
|
||||
<dt>Ended</dt>
|
||||
<dd class="wrap">{{content.summary.run_stop}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.status.sha256">
|
||||
<dt>SHA256</dt>
|
||||
<dd class="wrap">{{content.status.sha256}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.status.id">
|
||||
<dt>Threat Grid</dt>
|
||||
<dd>
|
||||
<i class="fa fa-search"></i>
|
||||
<a ng-href="https://{{content.host}}/samples/{{content.status.id}}" target="panacea">
|
||||
View Full Report</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.status.sha1">
|
||||
<dt>SHA1</dt>
|
||||
<dd class="wrap">{{content.status.sha1}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<!-- FREE CELL -->
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal" ng-if="content.status.md5">
|
||||
<dt>MD5</dt>
|
||||
<dd class="wrap">{{content.status.md5}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info" ng-if="content.iocs">
|
||||
<div class="panel-heading">
|
||||
<strong>Behavioral Indicators ({{content.iocs.length}})</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Catagories</th>
|
||||
<th>ATT&CK</th>
|
||||
<th>Tags</th>
|
||||
<th>Hits</th>
|
||||
<th style="width: 85px">Score</th>
|
||||
</tr>
|
||||
<tr ng-repeat="ioc_data in content.iocs | orderBy:'-(severity*confidence)/100'" ng-init="score = ((ioc_data.severity * ioc_data.confidence) / 100 | number:0)">
|
||||
<td>
|
||||
<a ng-href="https://{{content.host}}/mask/samples/{{content.status.id}}#indicator-row-{{ioc_data.ioc}}" target="panacea">
|
||||
{{ioc_data.title}}
|
||||
</a>
|
||||
</td>
|
||||
<td><span data-ng-repeat="category in ioc_data.category">{{category}} {{$last ? '' : ', '}}</span></td>
|
||||
<td><span data-ng-repeat="tactic in ioc_data['mitre-tactics']">{{tactic}} {{$last ? '' : ', '}}</span></td>
|
||||
<td><span data-ng-repeat="tag in ioc_data.tags">{{tag}} {{$last ? '' : ', '}}</span></td>
|
||||
<td style="text-align: center;">{{ioc_data.hits}}</td>
|
||||
<td style="text-align: center;" ng-class="{'critical' : score >= 90, 'high' : score < 90 && score >= 75, 'medium' : score < 75 && score >= 50, 'low' : score < 50}">
|
||||
{{score}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info" ng-if="content.domains">
|
||||
<div class="panel-heading">
|
||||
<strong>Domains ({{content.summary.domains}})</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Content Categories</th>
|
||||
<th>Security Categories</th>
|
||||
<th>Umbrella Status</th>
|
||||
</tr>
|
||||
<tr ng-repeat="(domain, domain_data) in content.domains">
|
||||
<td>{{domain}}</td>
|
||||
<td><span data-ng-repeat="category in domain_data.content_categories">{{category}} {{$last ? '' : ', '}}</span></td>
|
||||
<td><span data-ng-repeat="category in domain_data.security_categories">{{category}} {{$last ? '' : ', '}}</span></td>
|
||||
<td><div ng-class="['odns-verdict', domain_data.status]"><span class="capitalize">{{domain_data.status}}</span></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info" ng-if="content.network">
|
||||
<div class="panel-heading">
|
||||
<strong>TCP/IP Streams ({{content.summary.stream_count}})</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Stream</th>
|
||||
<th>Src. IP</th>
|
||||
<th>Src. Port</th>
|
||||
<th>Dest. IP</th>
|
||||
<th>Dest. Port</th>
|
||||
<th>Transport</th>
|
||||
<th>Packets</th>
|
||||
<th>Bytes</th>
|
||||
</tr>
|
||||
<tr ng-repeat="(stream_num, stream) in content.network">
|
||||
<td>
|
||||
<a ng-href="https://{{content.host}}/mask/samples/{{content.status.id}}#tcpIp-row-{{stream_num}}" target="panacea">
|
||||
{{stream_num}}
|
||||
</a>
|
||||
<span ng-if="stream.service">  ({{stream.service | uppercase}})</span>
|
||||
</td>
|
||||
<td>{{stream.src}}</td>
|
||||
<td>{{stream.src_port}}</td>
|
||||
<td>{{stream.dst}}</td>
|
||||
<td>{{stream.dst_port}}</td>
|
||||
<td>{{stream.transport}}</td>
|
||||
<td>{{stream.packets}}</td>
|
||||
<td>{{stream.bytes}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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> Threat Grid: </dt>
|
||||
<dd class="wrap">{{content.errorMessage}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
3
thehive-templates/ThreatGrid_1_0/short.html
Normal file
3
thehive-templates/ThreatGrid_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>
|
131
thehive-templates/ThreatResponse_1_0/long.html
Normal file
131
thehive-templates/ThreatResponse_1_0/long.html
Normal file
@ -0,0 +1,131 @@
|
||||
<style>
|
||||
.dg-relation-wrapper {
|
||||
margin: 2px 0;
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dg-observable-wrapper {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dg-observable-token {
|
||||
background: #dedede;
|
||||
border-radius: 5px;
|
||||
margin: 0 2px 5px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.dg-observable-type {
|
||||
color: #888;
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
text-transform: uppercase;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.dg-observable-value {
|
||||
color: #222;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Success -->
|
||||
<div class="panel panel-info" ng-if="success">
|
||||
<div class="panel-heading">
|
||||
<strong>Summary</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Threat Response</dt>
|
||||
<dd>
|
||||
<i class="fa fa-search"></i>
|
||||
<a ng-href="https://{{content.host}}/investigate?q={{content.observable}}" target="_blank">
|
||||
Investigate
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info" ng-if="content.verdicts.length > 0">
|
||||
<div class="panel-heading">
|
||||
<strong>Verdicts</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Module</th>
|
||||
<th>Observable</th>
|
||||
<th>Observable Type</th>
|
||||
<th>Disposition</th>
|
||||
<th>Expiration</th>
|
||||
</tr>
|
||||
<tr ng-repeat="verdict in content.verdicts | orderBy:'observable_value'">
|
||||
<td>{{verdict.module}}</td>
|
||||
<td>
|
||||
<i class="fa fa-search"></i>
|
||||
<a ng-href="https://{{content.host}}/investigate?q={{verdict.observable_value}}" target="_blank">
|
||||
{{verdict.observable_value}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{verdict.observable_type}}</td>
|
||||
<td>{{verdict.disposition_name}}</td>
|
||||
<td>{{verdict.expiration}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info" ng-if="content.targets.length > 0">
|
||||
<div class="panel-heading">
|
||||
<strong>Targets</strong>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Module</th>
|
||||
<th>Sensor</th>
|
||||
<th>Targets</th>
|
||||
</tr>
|
||||
<tbody ng-repeat="module in content.targets">
|
||||
<tr ng-repeat="target in module.targets">
|
||||
<td>
|
||||
{{module.module}}
|
||||
</td>
|
||||
<td>
|
||||
{{target.type}}
|
||||
</td>
|
||||
<td>
|
||||
<div ng-class="'dg-relation-wrapper'">
|
||||
<div ng-class="['dg-observable-wrapper', 'dg-observable-token']" data-ng-repeat="observable in target.observables">
|
||||
<span ng-class="'dg-observable-type'">{{observable.type}}</span>
|
||||
<span ng-class="'dg-observable-value'">{{observable.value}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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> Threat Response: </dt>
|
||||
<dd class="wrap">{{content.errorMessage}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
3
thehive-templates/ThreatResponse_1_0/short.html
Normal file
3
thehive-templates/ThreatResponse_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>
|
Loading…
Reference in New Issue
Block a user