2020-07-23 12:28:56 +02:00
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import requests
|
2021-10-25 17:01:05 +02:00
|
|
|
from requests.exceptions import (
|
|
|
|
HTTPError,
|
|
|
|
ProxyError,
|
|
|
|
InvalidURL,
|
|
|
|
ConnectTimeout,
|
|
|
|
ConnectionError,
|
|
|
|
)
|
|
|
|
from typing import Optional, List, Tuple, Dict
|
2020-07-28 11:47:53 +02:00
|
|
|
from . import check_input_attribute, checking_error, standard_error_message
|
2020-08-24 11:19:15 +02:00
|
|
|
import platform
|
|
|
|
import os
|
|
|
|
from urllib.parse import quote, urlparse
|
2020-07-23 12:28:56 +02:00
|
|
|
from pymisp import MISPAttribute, MISPEvent, MISPTag, MISPObject
|
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
moduleinfo = {
|
2021-10-25 17:01:05 +02:00
|
|
|
"version": "2.0.0",
|
|
|
|
"author": "Recorded Future",
|
|
|
|
"description": "Module to retrieve data from Recorded Future",
|
|
|
|
"module-type": ["expansion", "hover"],
|
2020-08-24 11:19:15 +02:00
|
|
|
}
|
2020-07-23 12:28:56 +02:00
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
moduleconfig = ["token", "proxy_host", "proxy_port", "proxy_username", "proxy_password"]
|
|
|
|
|
|
|
|
misperrors = {"error": "Error"}
|
|
|
|
|
|
|
|
GALAXY_FILE_PATH = "https://raw.githubusercontent.com/MISP/misp-galaxy/main/clusters/"
|
|
|
|
|
|
|
|
ATTRIBUTESTYPES = [
|
|
|
|
"ip",
|
|
|
|
"ip-src",
|
|
|
|
"ip-dst",
|
|
|
|
"ip-src|port",
|
|
|
|
"ip-dst|port",
|
|
|
|
"domain",
|
|
|
|
"hostname",
|
|
|
|
"md5",
|
|
|
|
"sha1",
|
|
|
|
"sha256",
|
|
|
|
"uri",
|
|
|
|
"url",
|
|
|
|
"vulnerability",
|
|
|
|
"weakness",
|
|
|
|
]
|
|
|
|
|
|
|
|
OUTPUTATTRIBUTESTYPES = ATTRIBUTESTYPES + [
|
|
|
|
"email-src",
|
|
|
|
"malware-sample",
|
|
|
|
"text",
|
|
|
|
"target-org",
|
|
|
|
"threat-actor",
|
|
|
|
"target-user",
|
2020-08-24 11:19:15 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
mispattributes = {
|
2021-10-25 17:01:05 +02:00
|
|
|
"input": ATTRIBUTESTYPES,
|
|
|
|
"output": OUTPUTATTRIBUTESTYPES,
|
|
|
|
"format": "misp_standard",
|
2020-08-24 11:19:15 +02:00
|
|
|
}
|
2020-07-23 12:28:56 +02:00
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
LOGGER = logging.getLogger("recorded_future")
|
2020-07-23 12:28:56 +02:00
|
|
|
LOGGER.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
class RequestHandler:
|
|
|
|
"""A class for handling any outbound requests from this module."""
|
2021-10-25 17:01:05 +02:00
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
def __init__(self):
|
|
|
|
self.session = requests.Session()
|
2021-10-25 17:01:05 +02:00
|
|
|
self.app_id = (
|
|
|
|
f'{os.path.basename(__file__)}/{moduleinfo["version"]} ({platform.platform()}) '
|
|
|
|
f'misp_enrichment/{moduleinfo["version"]} python-requests/{requests.__version__}'
|
|
|
|
)
|
2020-08-24 11:19:15 +02:00
|
|
|
self.proxies = None
|
|
|
|
self.rf_token = None
|
|
|
|
|
|
|
|
def get(self, url: str, headers: dict = None) -> requests.Response:
|
|
|
|
"""General get method with proxy error handling."""
|
|
|
|
try:
|
|
|
|
timeout = 7 if self.proxies else None
|
2021-10-25 17:01:05 +02:00
|
|
|
response = self.session.get(
|
|
|
|
url, headers=headers, proxies=self.proxies, timeout=timeout
|
|
|
|
)
|
2020-08-24 11:19:15 +02:00
|
|
|
response.raise_for_status()
|
|
|
|
return response
|
|
|
|
except (ConnectTimeout, ProxyError, InvalidURL) as error:
|
2021-10-25 17:01:05 +02:00
|
|
|
msg = "Error connecting with proxy, please check the Recorded Future app proxy settings."
|
|
|
|
LOGGER.error(f"{msg} Error: {error}")
|
|
|
|
misperrors["error"] = msg
|
2020-08-24 11:19:15 +02:00
|
|
|
raise
|
|
|
|
|
|
|
|
def rf_lookup(self, category: str, ioc: str) -> requests.Response:
|
|
|
|
"""Do a lookup call using Recorded Future's ConnectAPI."""
|
2021-10-25 17:01:05 +02:00
|
|
|
parsed_ioc = quote(ioc, safe="")
|
|
|
|
url = f"https://api.recordedfuture.com/gw/misp/lookup/{category}/{parsed_ioc}"
|
|
|
|
headers = {"X-RFToken": self.rf_token, "User-Agent": self.app_id}
|
2020-08-24 11:19:15 +02:00
|
|
|
try:
|
|
|
|
response = self.get(url, headers)
|
|
|
|
except HTTPError as error:
|
2021-10-25 17:01:05 +02:00
|
|
|
msg = f"Error when requesting data from Recorded Future. {error.response}: {error.response.reason}"
|
2020-08-24 11:19:15 +02:00
|
|
|
LOGGER.error(msg)
|
2021-10-25 17:01:05 +02:00
|
|
|
misperrors["error"] = msg
|
2020-08-24 11:19:15 +02:00
|
|
|
raise
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
GLOBAL_REQUEST_HANDLER = RequestHandler()
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
class GalaxyFinder:
|
|
|
|
"""A class for finding MISP galaxy matches to Recorded Future data."""
|
2021-10-25 17:01:05 +02:00
|
|
|
|
2020-07-23 12:28:56 +02:00
|
|
|
def __init__(self):
|
|
|
|
self.session = requests.Session()
|
2021-10-25 17:01:05 +02:00
|
|
|
# There are duplicates values for different keys because Links entities and Related entities
|
|
|
|
# have have different naming for the same types
|
2020-07-23 12:28:56 +02:00
|
|
|
self.sources = {
|
2021-10-25 17:01:05 +02:00
|
|
|
"RelatedThreatActor": [f"{GALAXY_FILE_PATH}threat-actor.json"],
|
|
|
|
"Threat Actor": [f"{GALAXY_FILE_PATH}threat-actor.json"],
|
|
|
|
"RelatedMalware": [
|
|
|
|
f"{GALAXY_FILE_PATH}banker.json",
|
|
|
|
f"{GALAXY_FILE_PATH}botnet.json",
|
|
|
|
f"{GALAXY_FILE_PATH}exploit-kit.json",
|
|
|
|
f"{GALAXY_FILE_PATH}rat.json",
|
|
|
|
f"{GALAXY_FILE_PATH}ransomware.json",
|
|
|
|
f"{GALAXY_FILE_PATH}malpedia.json",
|
|
|
|
],
|
|
|
|
"Malware": [
|
|
|
|
f"{GALAXY_FILE_PATH}banker.json",
|
|
|
|
f"{GALAXY_FILE_PATH}botnet.json",
|
|
|
|
f"{GALAXY_FILE_PATH}exploit-kit.json",
|
|
|
|
f"{GALAXY_FILE_PATH}rat.json",
|
|
|
|
f"{GALAXY_FILE_PATH}ransomware.json",
|
|
|
|
f"{GALAXY_FILE_PATH}malpedia.json",
|
|
|
|
],
|
|
|
|
"MitreAttackIdentifier": [
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-attack-pattern.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-course-of-action.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-enterprise-attack-attack-pattern.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-enterprise-attack-course-of-action.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-enterprise-attack-intrusion-set.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-enterprise-attack-malware.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-enterprise-attack-tool.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-intrusion-set.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-malware.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-mobile-attack-attack-pattern.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-mobile-attack-course-of-action.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-mobile-attack-intrusion-set.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-mobile-attack-malware.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-mobile-attack-tool.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-pre-attack-attack-pattern.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-pre-attack-intrusion-set.json",
|
|
|
|
f"{GALAXY_FILE_PATH}mitre-tool.json",
|
2020-08-24 11:19:15 +02:00
|
|
|
],
|
2020-07-23 12:28:56 +02:00
|
|
|
}
|
|
|
|
self.galaxy_clusters = {}
|
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
def pull_galaxy_cluster(self, related_type: str) -> None:
|
2020-07-23 12:28:56 +02:00
|
|
|
"""Fetches galaxy clusters for the related_type from the remote json files specified as self.sources."""
|
|
|
|
# Only fetch clusters if not fetched previously
|
|
|
|
if not self.galaxy_clusters.get(related_type):
|
|
|
|
for source in self.sources.get(related_type):
|
2020-08-24 11:19:15 +02:00
|
|
|
try:
|
|
|
|
response = GLOBAL_REQUEST_HANDLER.get(source)
|
2021-10-25 17:01:05 +02:00
|
|
|
name = source.split("/")[-1].split(".")[0]
|
|
|
|
self.galaxy_clusters.setdefault(related_type, {}).update(
|
|
|
|
{name: response.json()}
|
|
|
|
)
|
2020-08-24 11:19:15 +02:00
|
|
|
except ConnectionError as error:
|
2021-10-25 17:01:05 +02:00
|
|
|
LOGGER.warning(
|
|
|
|
f"pull_galaxy_cluster failed for source: {source}, with error: {error}."
|
|
|
|
)
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
def find_galaxy_match(self, indicator: str, related_type: str) -> str:
|
|
|
|
"""Searches the clusters of the related_type for a match with the indicator.
|
2021-10-25 17:01:05 +02:00
|
|
|
:returns the first matching galaxy string or an empty string if no galaxy match is found.
|
2020-07-23 12:28:56 +02:00
|
|
|
"""
|
|
|
|
self.pull_galaxy_cluster(related_type)
|
2020-08-24 11:19:15 +02:00
|
|
|
for cluster_name, cluster in self.galaxy_clusters.get(related_type, {}).items():
|
2021-10-25 17:01:05 +02:00
|
|
|
for value in cluster["values"]:
|
|
|
|
if indicator in value.get("meta", {}).get(
|
|
|
|
"synonyms", ""
|
|
|
|
) or indicator in value.get("value", ""):
|
|
|
|
value = value["value"]
|
|
|
|
return f'misp-galaxy:{cluster_name}="{value}"'
|
|
|
|
return ""
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
class RFColors:
|
|
|
|
"""Class for setting signature RF-colors."""
|
2021-10-25 17:01:05 +02:00
|
|
|
|
2020-07-23 12:28:56 +02:00
|
|
|
def __init__(self):
|
2021-10-25 17:01:05 +02:00
|
|
|
self.rf_white = "#CCCCCC"
|
|
|
|
self.rf_grey = " #CDCDCD"
|
|
|
|
self.rf_yellow = "#FFCF00"
|
|
|
|
self.rf_red = "#D10028"
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
def riskscore_color(self, risk_score: int) -> str:
|
|
|
|
"""Returns appropriate hex-colors according to risk score."""
|
|
|
|
risk_score = int(risk_score)
|
|
|
|
if risk_score < 25:
|
|
|
|
return self.rf_white
|
|
|
|
elif risk_score < 65:
|
|
|
|
return self.rf_yellow
|
|
|
|
else:
|
|
|
|
return self.rf_red
|
|
|
|
|
|
|
|
def riskrule_color(self, risk_rule_criticality: int) -> str:
|
|
|
|
"""Returns appropriate hex-colors according to risk rule criticality."""
|
|
|
|
risk_rule_criticality = int(risk_rule_criticality)
|
|
|
|
if risk_rule_criticality == 1:
|
|
|
|
return self.rf_white
|
|
|
|
elif risk_rule_criticality == 2:
|
|
|
|
return self.rf_yellow
|
|
|
|
else: # risk_rule_criticality == 3 or 4
|
|
|
|
return self.rf_red
|
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
def criticality_color(self, criticality) -> str:
|
|
|
|
mapper = {
|
|
|
|
"None": self.rf_grey,
|
|
|
|
"Low": self.rf_grey,
|
|
|
|
"Unusual": self.rf_grey,
|
|
|
|
"Informational": self.rf_grey,
|
|
|
|
"Medium": self.rf_yellow,
|
|
|
|
"Suspicious": self.rf_yellow,
|
|
|
|
"High": self.rf_red,
|
|
|
|
"Critical": self.rf_red,
|
|
|
|
"Very Critical": self.rf_red,
|
|
|
|
"Malicious": self.rf_red,
|
|
|
|
"Very Malicious": self.rf_red,
|
|
|
|
}
|
|
|
|
return mapper.get(criticality, self.rf_white)
|
|
|
|
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
class RFEnricher:
|
|
|
|
"""Class for enriching an attribute with data from Recorded Future.
|
2021-10-25 17:01:05 +02:00
|
|
|
The enrichment data is returned as a custom MISP object.
|
2020-07-23 12:28:56 +02:00
|
|
|
"""
|
2021-10-25 17:01:05 +02:00
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
def __init__(self, attribute_props: dict):
|
2020-07-23 12:28:56 +02:00
|
|
|
self.event = MISPEvent()
|
2021-10-25 17:01:05 +02:00
|
|
|
self.enrichment_object = MISPObject("Recorded Future Enrichment")
|
2022-01-14 10:23:08 +01:00
|
|
|
self.enrichment_object.template_uuid = "cbe0ffda-75e5-4c49-833f-093f057652ba"
|
|
|
|
self.enrichment_object.template_id = "1"
|
|
|
|
self.enrichment_object.description = "Recorded Future Enrichment"
|
|
|
|
setattr(self.enrichment_object, 'meta-category', 'network')
|
2020-08-24 11:19:15 +02:00
|
|
|
description = (
|
2021-10-25 17:01:05 +02:00
|
|
|
"An object containing the enriched attribute and "
|
|
|
|
"related entities from Recorded Future."
|
|
|
|
)
|
|
|
|
self.enrichment_object.from_dict(
|
|
|
|
**{"meta-category": "misc", "description": description, "distribution": 0}
|
2020-08-24 11:19:15 +02:00
|
|
|
)
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
# Create a copy of enriched attribute to add tags to
|
|
|
|
temp_attr = MISPAttribute()
|
|
|
|
temp_attr.from_dict(**attribute_props)
|
|
|
|
self.enriched_attribute = MISPAttribute()
|
2021-10-25 17:01:05 +02:00
|
|
|
self.enriched_attribute.from_dict(
|
|
|
|
**{"value": temp_attr.value, "type": temp_attr.type, "distribution": 0}
|
|
|
|
)
|
2020-07-23 12:28:56 +02:00
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
self.related_attributes: List[Tuple[str, MISPAttribute]] = []
|
2020-07-23 12:28:56 +02:00
|
|
|
self.color_picker = RFColors()
|
|
|
|
self.galaxy_finder = GalaxyFinder()
|
|
|
|
|
|
|
|
# Mapping from MISP-type to RF-type
|
2020-08-24 11:19:15 +02:00
|
|
|
self.type_to_rf_category = {
|
2021-10-25 17:01:05 +02:00
|
|
|
"ip": "ip",
|
|
|
|
"ip-src": "ip",
|
|
|
|
"ip-dst": "ip",
|
|
|
|
"ip-src|port": "ip",
|
|
|
|
"ip-dst|port": "ip",
|
|
|
|
"domain": "domain",
|
|
|
|
"hostname": "domain",
|
|
|
|
"md5": "hash",
|
|
|
|
"sha1": "hash",
|
|
|
|
"sha256": "hash",
|
|
|
|
"uri": "url",
|
|
|
|
"url": "url",
|
|
|
|
"vulnerability": "vulnerability",
|
|
|
|
"weakness": "vulnerability",
|
2020-08-24 11:19:15 +02:00
|
|
|
}
|
2020-07-23 12:28:56 +02:00
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
# Related entities have 'Related' as part of the word and Links entities from RF
|
|
|
|
# portrayed as related attributes in MISP
|
2020-08-24 11:19:15 +02:00
|
|
|
self.related_attribute_types = [
|
2021-10-25 17:01:05 +02:00
|
|
|
"RelatedIpAddress",
|
|
|
|
"RelatedInternetDomainName",
|
|
|
|
"RelatedHash",
|
|
|
|
"RelatedEmailAddress",
|
|
|
|
"RelatedCyberVulnerability",
|
|
|
|
"IpAddress",
|
|
|
|
"InternetDomainName",
|
|
|
|
"Hash",
|
|
|
|
"EmailAddress",
|
|
|
|
"CyberVulnerability",
|
|
|
|
]
|
|
|
|
# Related entities have 'Related' as part of the word and and Links entities from RF portrayed as tags in MISP
|
|
|
|
self.galaxy_tag_types = [
|
|
|
|
"RelatedMalware",
|
|
|
|
"RelatedThreatActor",
|
|
|
|
"Threat Actor",
|
|
|
|
"MitreAttackIdentifier",
|
|
|
|
"Malware",
|
2020-08-24 11:19:15 +02:00
|
|
|
]
|
2020-07-23 12:28:56 +02:00
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
def enrich(self) -> None:
|
2020-07-23 12:28:56 +02:00
|
|
|
"""Run the enrichment."""
|
2021-10-25 17:01:05 +02:00
|
|
|
category = self.type_to_rf_category.get(self.enriched_attribute.type, "")
|
|
|
|
enriched_attribute_value = self.enriched_attribute.value
|
|
|
|
# If enriched attribute has a port we need to remove that port
|
|
|
|
# since RF do not support enriching ip addresses with port
|
|
|
|
if self.enriched_attribute.type in ["ip-src|port", "ip-dst|port"]:
|
|
|
|
enriched_attribute_value = enriched_attribute_value.split("|")[0]
|
|
|
|
json_response = GLOBAL_REQUEST_HANDLER.rf_lookup(
|
|
|
|
category, enriched_attribute_value
|
|
|
|
)
|
2020-08-24 11:19:15 +02:00
|
|
|
response = json.loads(json_response.content)
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Add risk score and risk rules as tags to the enriched attribute
|
2021-10-25 17:01:05 +02:00
|
|
|
risk_score = response["data"]["risk"]["score"]
|
2020-07-23 12:28:56 +02:00
|
|
|
hex_color = self.color_picker.riskscore_color(risk_score)
|
|
|
|
tag_name = f'recorded-future:risk-score="{risk_score}"'
|
|
|
|
self.add_tag(tag_name, hex_color)
|
2021-10-25 17:01:05 +02:00
|
|
|
risk_criticality = response["data"]["risk"]["criticalityLabel"]
|
|
|
|
hex_color = self.color_picker.criticality_color(risk_criticality)
|
|
|
|
tag_name = f'recorded-future:criticality="{risk_criticality}"'
|
|
|
|
self.add_tag(tag_name, hex_color)
|
|
|
|
|
|
|
|
for evidence in response["data"]["risk"]["evidenceDetails"]:
|
|
|
|
risk_rule = evidence["rule"]
|
|
|
|
criticality = evidence["criticality"]
|
2020-07-23 12:28:56 +02:00
|
|
|
hex_color = self.color_picker.riskrule_color(criticality)
|
|
|
|
tag_name = f'recorded-future:risk-rule="{risk_rule}"'
|
|
|
|
self.add_tag(tag_name, hex_color)
|
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
links_data = response["data"].get("links", {}).get("hits")
|
|
|
|
# Check if we have error in links response. If yes, then user do not have right module enabled in token
|
|
|
|
links_access_error = response["data"].get("links", {}).get("error")
|
|
|
|
galaxy_tags = []
|
|
|
|
if not links_access_error:
|
|
|
|
for hit in links_data:
|
|
|
|
for section in hit["sections"]:
|
|
|
|
for sec_list in section["lists"]:
|
|
|
|
entity_type = sec_list["type"]["name"]
|
|
|
|
for entity in sec_list["entities"]:
|
|
|
|
if entity_type in self.galaxy_tag_types:
|
|
|
|
galaxy = self.galaxy_finder.find_galaxy_match(
|
|
|
|
entity["name"], entity_type
|
|
|
|
)
|
|
|
|
if galaxy and galaxy not in galaxy_tags:
|
|
|
|
galaxy_tags.append(galaxy)
|
|
|
|
else:
|
|
|
|
self.add_attribute(entity["name"], entity_type)
|
|
|
|
|
|
|
|
else:
|
|
|
|
# Retrieve related entities
|
|
|
|
for related_entity in response["data"]["relatedEntities"]:
|
|
|
|
related_type = related_entity["type"]
|
|
|
|
if related_type in self.related_attribute_types:
|
|
|
|
# Related entities returned as additional attributes
|
|
|
|
for related in related_entity["entities"]:
|
|
|
|
# filter those entities that have count bigger than 4, to reduce noise
|
|
|
|
# because there can be a huge list of related entities
|
|
|
|
if int(related["count"]) > 4:
|
|
|
|
indicator = related["entity"]["name"]
|
|
|
|
self.add_attribute(indicator, related_type)
|
|
|
|
elif related_type in self.galaxy_tag_types:
|
|
|
|
# Related entities added as galaxy-tags to the enriched attribute
|
|
|
|
galaxy_tags = []
|
|
|
|
for related in related_entity["entities"]:
|
|
|
|
# filter those entities that have count bigger than 4, to reduce noise
|
|
|
|
# because there can be a huge list of related entities
|
|
|
|
if int(related["count"]) > 4:
|
|
|
|
indicator = related["entity"]["name"]
|
|
|
|
galaxy = self.galaxy_finder.find_galaxy_match(
|
|
|
|
indicator, related_type
|
|
|
|
)
|
|
|
|
# Handle deduplication of galaxy tags
|
|
|
|
if galaxy and galaxy not in galaxy_tags:
|
|
|
|
galaxy_tags.append(galaxy)
|
|
|
|
for galaxy in galaxy_tags:
|
|
|
|
self.add_tag(galaxy)
|
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
except KeyError:
|
2021-10-25 17:01:05 +02:00
|
|
|
misperrors["error"] = "Unexpected format in Recorded Future api response."
|
2020-08-24 11:19:15 +02:00
|
|
|
raise
|
2020-07-23 12:28:56 +02:00
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
def add_attribute(self, indicator: str, indicator_type: str) -> None:
|
|
|
|
"""Helper method for adding an indicator to the attribute list."""
|
|
|
|
out_type = self.get_output_type(indicator_type, indicator)
|
2020-07-23 12:28:56 +02:00
|
|
|
attribute = MISPAttribute()
|
2021-10-25 17:01:05 +02:00
|
|
|
attribute.from_dict(**{"value": indicator, "type": out_type, "distribution": 0})
|
|
|
|
self.related_attributes.append((indicator_type, attribute))
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
def add_tag(self, tag_name: str, hex_color: str = None) -> None:
|
|
|
|
"""Helper method for adding a tag to the enriched attribute."""
|
|
|
|
tag = MISPTag()
|
2021-10-25 17:01:05 +02:00
|
|
|
tag_properties = {"name": tag_name}
|
2020-07-23 12:28:56 +02:00
|
|
|
if hex_color:
|
2021-10-25 17:01:05 +02:00
|
|
|
tag_properties["colour"] = hex_color
|
2020-07-23 12:28:56 +02:00
|
|
|
tag.from_dict(**tag_properties)
|
|
|
|
self.enriched_attribute.add_tag(tag)
|
|
|
|
|
|
|
|
def get_output_type(self, related_type: str, indicator: str) -> str:
|
|
|
|
"""Helper method for translating a Recorded Future related type to a MISP output type."""
|
2021-10-25 17:01:05 +02:00
|
|
|
output_type = "text"
|
|
|
|
if related_type in ["RelatedIpAddress", "IpAddress"]:
|
|
|
|
output_type = "ip-dst"
|
|
|
|
elif related_type in ["RelatedInternetDomainName", "InternetDomainName"]:
|
|
|
|
output_type = "domain"
|
|
|
|
elif related_type in ["RelatedHash", "Hash"]:
|
2020-07-23 12:28:56 +02:00
|
|
|
hash_len = len(indicator)
|
|
|
|
if hash_len == 64:
|
2021-10-25 17:01:05 +02:00
|
|
|
output_type = "sha256"
|
2020-07-23 12:28:56 +02:00
|
|
|
elif hash_len == 40:
|
2021-10-25 17:01:05 +02:00
|
|
|
output_type = "sha1"
|
2020-07-23 12:28:56 +02:00
|
|
|
elif hash_len == 32:
|
2021-10-25 17:01:05 +02:00
|
|
|
output_type = "md5"
|
|
|
|
elif related_type in ["RelatedEmailAddress", "EmailAddress"]:
|
|
|
|
output_type = "email-src"
|
|
|
|
elif related_type in ["RelatedCyberVulnerability", "CyberVulnerability"]:
|
|
|
|
signature = indicator.split("-")[0]
|
|
|
|
if signature == "CVE":
|
|
|
|
output_type = "vulnerability"
|
|
|
|
elif signature == "CWE":
|
|
|
|
output_type = "weakness"
|
|
|
|
elif related_type == "MalwareSignature":
|
|
|
|
output_type = "malware-sample"
|
|
|
|
elif related_type == "Organization":
|
|
|
|
output_type = "target-org"
|
|
|
|
elif related_type == "Username":
|
|
|
|
output_type = "target-user"
|
2020-07-23 12:28:56 +02:00
|
|
|
return output_type
|
|
|
|
|
|
|
|
def get_results(self) -> dict:
|
|
|
|
"""Build and return the enrichment results."""
|
2021-10-25 17:01:05 +02:00
|
|
|
self.enrichment_object.add_attribute(
|
|
|
|
"Enriched attribute", **self.enriched_attribute
|
|
|
|
)
|
2020-07-23 12:28:56 +02:00
|
|
|
for related_type, attribute in self.related_attributes:
|
|
|
|
self.enrichment_object.add_attribute(related_type, **attribute)
|
|
|
|
self.event.add_object(**self.enrichment_object)
|
|
|
|
event = json.loads(self.event.to_json())
|
2021-10-25 17:01:05 +02:00
|
|
|
result = {key: event[key] for key in ["Object"] if key in event}
|
|
|
|
return {"results": result}
|
2020-07-23 12:28:56 +02:00
|
|
|
|
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
def get_proxy_settings(config: dict) -> Optional[Dict[str, str]]:
|
2020-08-24 11:19:15 +02:00
|
|
|
"""Returns proxy settings in the requests format.
|
2021-10-25 17:01:05 +02:00
|
|
|
If no proxy settings are set, return None."""
|
2020-08-24 11:19:15 +02:00
|
|
|
proxies = None
|
2021-10-25 17:01:05 +02:00
|
|
|
host = config.get("proxy_host")
|
|
|
|
port = config.get("proxy_port")
|
|
|
|
username = config.get("proxy_username")
|
|
|
|
password = config.get("proxy_password")
|
2020-08-24 11:19:15 +02:00
|
|
|
|
|
|
|
if host:
|
|
|
|
if not port:
|
2021-10-25 17:01:05 +02:00
|
|
|
misperrors["error"] = (
|
|
|
|
"The recordedfuture_proxy_host config is set, "
|
|
|
|
"please also set the recordedfuture_proxy_port."
|
|
|
|
)
|
2020-08-24 11:19:15 +02:00
|
|
|
raise KeyError
|
|
|
|
parsed = urlparse(host)
|
2021-10-25 17:01:05 +02:00
|
|
|
if "http" in parsed.scheme:
|
|
|
|
scheme = "http"
|
2020-08-24 11:19:15 +02:00
|
|
|
else:
|
|
|
|
scheme = parsed.scheme
|
|
|
|
netloc = parsed.netloc
|
2021-10-25 17:01:05 +02:00
|
|
|
host = f"{netloc}:{port}"
|
2020-08-24 11:19:15 +02:00
|
|
|
|
|
|
|
if username:
|
|
|
|
if not password:
|
2021-10-25 17:01:05 +02:00
|
|
|
misperrors["error"] = (
|
|
|
|
"The recordedfuture_proxy_username config is set, "
|
|
|
|
"please also set the recordedfuture_proxy_password."
|
|
|
|
)
|
2020-08-24 11:19:15 +02:00
|
|
|
raise KeyError
|
2021-10-25 17:01:05 +02:00
|
|
|
auth = f"{username}:{password}"
|
|
|
|
host = auth + "@" + host
|
2020-08-24 11:19:15 +02:00
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
proxies = {"http": f"{scheme}://{host}", "https": f"{scheme}://{host}"}
|
2020-08-24 11:19:15 +02:00
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
LOGGER.info(f"Proxy settings: {proxies}")
|
2020-08-24 11:19:15 +02:00
|
|
|
return proxies
|
|
|
|
|
|
|
|
|
2020-07-23 12:28:56 +02:00
|
|
|
def handler(q=False):
|
|
|
|
"""Handle enrichment."""
|
|
|
|
if q is False:
|
|
|
|
return False
|
|
|
|
request = json.loads(q)
|
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
config = request.get("config")
|
|
|
|
if config and config.get("token"):
|
|
|
|
GLOBAL_REQUEST_HANDLER.rf_token = config.get("token")
|
2020-07-23 12:28:56 +02:00
|
|
|
else:
|
2021-10-25 17:01:05 +02:00
|
|
|
misperrors["error"] = "Missing Recorded Future token."
|
2020-07-23 12:28:56 +02:00
|
|
|
return misperrors
|
2021-10-25 17:01:05 +02:00
|
|
|
if not request.get("attribute") or not check_input_attribute(
|
|
|
|
request["attribute"], requirements=("type", "value")
|
|
|
|
):
|
|
|
|
return {"error": f"{standard_error_message}, {checking_error}."}
|
|
|
|
if request["attribute"]["type"] not in mispattributes["input"]:
|
|
|
|
return {"error": "Unsupported attribute type."}
|
2020-07-23 12:28:56 +02:00
|
|
|
|
2020-08-24 11:19:15 +02:00
|
|
|
try:
|
|
|
|
GLOBAL_REQUEST_HANDLER.proxies = get_proxy_settings(config)
|
|
|
|
except KeyError:
|
|
|
|
return misperrors
|
|
|
|
|
2021-10-25 17:01:05 +02:00
|
|
|
input_attribute = request.get("attribute")
|
2020-08-24 11:19:15 +02:00
|
|
|
rf_enricher = RFEnricher(input_attribute)
|
|
|
|
|
2020-07-23 12:28:56 +02:00
|
|
|
try:
|
|
|
|
rf_enricher.enrich()
|
2020-08-24 11:19:15 +02:00
|
|
|
except (HTTPError, ConnectTimeout, ProxyError, InvalidURL, KeyError):
|
2020-07-23 12:28:56 +02:00
|
|
|
return misperrors
|
|
|
|
|
|
|
|
return rf_enricher.get_results()
|
|
|
|
|
|
|
|
|
|
|
|
def introspection():
|
|
|
|
"""Returns a dict of the supported attributes."""
|
|
|
|
return mispattributes
|
|
|
|
|
|
|
|
|
|
|
|
def version():
|
|
|
|
"""Returns a dict with the version and the associated meta-data
|
|
|
|
including potential configurations required of the module."""
|
2021-10-25 17:01:05 +02:00
|
|
|
moduleinfo["config"] = moduleconfig
|
2020-07-23 12:28:56 +02:00
|
|
|
return moduleinfo
|