Merge branch 'release/2.5.0'

This commit is contained in:
Jérôme Leonard 2020-02-24 11:57:40 +01:00
commit 0fba4d5ae7
32 changed files with 1926 additions and 3 deletions

View 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"
}

View 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()

View File

@ -0,0 +1,2 @@
cortexutils
pyclamd

View 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
View 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()

View File

@ -0,0 +1,2 @@
cortexutils
requests

View File

@ -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()

View 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
}
]
}

View 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()

View File

@ -0,0 +1,2 @@
cortexutils
requests

View 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
}
]
}

View 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()

View File

@ -0,0 +1,3 @@
requests
cortexutils
threatresponse

View 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()

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View 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
}
]
}

View 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
}
]
}

View 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
}
]
}

View File

@ -0,0 +1,2 @@
requests
cortexutils

View 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
View 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()

View 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

View File

@ -0,0 +1,2 @@
requests
cortexutils

View 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>

View 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>

View 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>

View 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>

View 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">&nbsp&nbsp({{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>

View 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>

View 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>

View 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>