diff --git a/README.md b/README.md index 2deeefd..d0d2328 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ For more information: [Extending MISP with Python modules](https://www.misp-proj * [GeoIP](misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind. * [GeoIP_City](misp_modules/modules/expansion/geoip_city.py) - a hover and expansion module to get GeoIP City information from geolite/maxmind. * [GeoIP_ASN](misp_modules/modules/expansion/geoip_asn.py) - a hover and expansion module to get GeoIP ASN information from geolite/maxmind. -* [Greynoise](misp_modules/modules/expansion/greynoise.py) - a hover to get information from greynoise. +* [GreyNoise](misp_modules/modules/expansion/greynoise.py) - a hover and expansion module to get IP and CVE information from GreyNoise. * [hashdd](misp_modules/modules/expansion/hashdd.py) - a hover module to check file hashes against [hashdd.com](http://www.hashdd.com) including NSLR dataset. * [hibp](misp_modules/modules/expansion/hibp.py) - a hover module to lookup against Have I Been Pwned? * [html_to_markdown](misp_modules/modules/expansion/html_to_markdown.py) - Simple HTML to markdown converter diff --git a/REQUIREMENTS b/REQUIREMENTS index 0fc7218..ac3fbc6 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -47,6 +47,7 @@ filelock==3.8.0 ; python_version >= '3.7' frozenlist==1.3.1 ; python_version >= '3.7' future==0.18.2 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' geoip2==4.6.0 +greynoise==2.0.0 h11==0.12.0 ; python_version >= '3.6' httpcore==0.15.0 ; python_version >= '3.7' httplib2==0.20.4 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' diff --git a/documentation/logos/crowdsec.png b/documentation/logos/crowdsec.png new file mode 100644 index 0000000..6b2953d Binary files /dev/null and b/documentation/logos/crowdsec.png differ diff --git a/documentation/website/expansion/crowdsec.json b/documentation/website/expansion/crowdsec.json new file mode 100644 index 0000000..750caae --- /dev/null +++ b/documentation/website/expansion/crowdsec.json @@ -0,0 +1,15 @@ +{ + "description": "Hover module to lookup an IP in CrowdSec's CTI", + "logo": "crowdsec.png", + "requirements": [ + "A CrowdSec CTI API key. Get yours by following https://docs.crowdsec.net/docs/cti_api/getting_started/#getting-an-api-key" + ], + "input": "An IP address.", + "output": "IP Lookup information from CrowdSec CTI API", + "references": [ + "https://www.crowdsec.net/", + "https://docs.crowdsec.net/docs/cti_api/getting_started", + "https://app.crowdsec.net/" + ], + "features": "This module enables IP lookup from CrowdSec CTI API. It provides information about the IP, such as what kind of attacks it has been participant of as seen by CrowdSec's network. It also includes enrichment by CrowdSec like background noise score, aggressivity over time etc." +} \ No newline at end of file diff --git a/misp_modules/modules/expansion/greynoise.py b/misp_modules/modules/expansion/greynoise.py index a2ccf13..fbeb88b 100644 --- a/misp_modules/modules/expansion/greynoise.py +++ b/misp_modules/modules/expansion/greynoise.py @@ -1,254 +1,333 @@ +import ipaddress import json +import logging -import requests -from pymisp import MISPEvent, MISPObject + +try: + from greynoise import GreyNoise +except ImportError: + print("greynoise module not installed.") +from pymisp import MISPAttribute, MISPEvent, MISPObject + +from . import check_input_attribute, standard_error_message + +logger = logging.getLogger("greynoise") +logger.setLevel(logging.INFO) misperrors = {"error": "Error"} -mispattributes = {"input": ["ip-dst", "ip-src", "vulnerability"], "output": ["text"]} +mispattributes = {"input": ["ip-src", "ip-dst", "vulnerability"], "format": "misp_standard"} moduleinfo = { - "version": "1.1", + "version": "1.2", "author": "Brad Chiappetta ", - "description": "Module to access GreyNoise.io API.", - "module-type": ["hover"], + "description": "Used to query IP and CVE intel from GreyNoise", + "module-type": ["expansion", "hover"], } moduleconfig = ["api_key", "api_type"] -codes_mapping = { - "0x00": "The IP has never been observed scanning the Internet", - "0x01": "The IP has been observed by the GreyNoise sensor network", - "0x02": "The IP has been observed scanning the GreyNoise sensor network, " - "but has not completed a full connection, meaning this can be spoofed", - "0x03": "The IP is adjacent to another host that has been directly observed by the GreyNoise sensor network", - "0x04": "Reserved", - "0x05": "This IP is commonly spoofed in Internet-scan activity", - "0x06": "This IP has been observed as noise, but this host belongs to a cloud provider where IPs can be " - "cycled frequently", - "0x07": "This IP is invalid", - "0x08": "This IP was classified as noise, but has not been observed engaging in Internet-wide scans or " - "attacks in over 90 days", - "0x09": "IP was found in RIOT", - "0x10": "IP has been observed by the GreyNoise sensor network and is in RIOT", -} -vulnerability_mapping = { - "id": ("vulnerability", "CVE #"), - "details": ("text", "Details"), - "count": ("text", "Total Scanner Count"), -} -enterprise_context_basic_mapping = {"ip": ("text", "IP Address"), "code_message": ("text", "Code Message")} -enterprise_context_advanced_mapping = { - "noise": ("text", "Is Internet Background Noise"), - "link": ("link", "Visualizer Link"), - "classification": ("text", "Classification"), - "actor": ("text", "Actor"), - "tags": ("text", "Tags"), - "cve": ("text", "CVEs"), - "first_seen": ("text", "First Seen Scanning"), - "last_seen": ("text", "Last Seen Scanning"), - "vpn": ("text", "Known VPN Service"), - "vpn_service": ("text", "VPN Service Name"), - "bot": ("text", "Known BOT"), -} -enterprise_context_advanced_metadata_mapping = { - "asn": ("text", "ASN"), - "rdns": ("text", "rDNS"), - "category": ("text", "Category"), - "tor": ("text", "Known Tor Exit Node"), - "region": ("text", "Region"), - "city": ("text", "City"), - "country": ("text", "Country"), - "country_code": ("text", "Country Code"), - "organization": ("text", "Organization"), -} -enterprise_riot_mapping = { - "riot": ("text", "Is Common Business Service"), - "link": ("link", "Visualizer Link"), - "category": ("text", "RIOT Category"), - "name": ("text", "Provider Name"), - "trust_level": ("text", "RIOT Trust Level"), - "last_updated": ("text", "Last Updated"), -} -community_found_mapping = { - "ip": ("text", "IP Address"), - "noise": ("text", "Is Internet Background Noise"), - "riot": ("text", "Is Common Business Service"), - "classification": ("text", "Classification"), - "last_seen": ("text", "Last Seen"), - "name": ("text", "Name"), - "link": ("link", "Visualizer Link"), -} -community_not_found_mapping = { - "ip": ("text", "IP Address"), - "noise": ("text", "Is Internet Background Noise"), - "riot": ("text", "Is Common Business Service"), - "message": ("text", "Message"), -} -misp_event = MISPEvent() -def handler(q=False): # noqa: C901 - if q is False: - return False - request = json.loads(q) - if not request.get("config") or not request["config"].get("api_key"): - return {"error": "Missing Greynoise API key."} +class GreyNoiseParser: + def __init__(self, attribute): + self.misp_event = MISPEvent() + self.attribute = MISPAttribute() + self.attribute.from_dict(**attribute) + self.misp_event.add_attribute(**self.attribute) + self.ip_address_enrich_mapping = { + "noise": {"type": "boolean", "object_relation": "noise"}, + "riot": {"type": "boolean", "object_relation": "riot"}, + "classification": {"type": "text", "object_relation": "classification"}, + "actor": {"type": "text", "object_relation": "actor"}, + "trust_level": {"type": "text", "object_relation": "trust-level"}, + "name": {"type": "text", "object_relation": "provider"}, + "first_seen": {"type": "datetime", "object_relation": "first-seen"}, + "last_seen": {"type": "datetime", "object_relation": "last-seen"}, + "link": {"type": "url", "object_relation": "link"}, + "last_updated": {"type": "datetime", "object_relation": "last-seen"}, + } + self.ip_address_hover_mapping = { + "noise": {"type": "boolean", "object_relation": "noise"}, + "riot": {"type": "boolean", "object_relation": "riot"}, + "classification": {"type": "text", "object_relation": "classification"}, + "actor": {"type": "text", "object_relation": "actor"}, + "tags": {"type": "text", "object_relation": "tags"}, + "cve": {"type": "text", "object_relation": "cve"}, + "vpn": {"type": "text", "object_relation": "vpn"}, + "vpn_service": {"type": "text", "object_relation": "vpn_service"}, + "bot": {"type": "text", "object_relation": "bot"}, + "first_seen": {"type": "datetime", "object_relation": "first-seen"}, + "last_seen": {"type": "datetime", "object_relation": "last-seen"}, + "spoofable": {"type": "datetime", "object_relation": "spoofable"}, + "link": {"type": "url", "object_relation": "link"}, + "category": {"type": "text", "object_relation": "category"}, + "name": {"type": "text", "object_relation": "provider"}, + "trust_level": {"type": "text", "object_relation": "trust-level"}, + "last_updated": {"type": "datetime", "object_relation": "last_updated"}, + } + self.ip_address_metadata_mapping = { + "tor": {"type": "text", "object_relation": "tor"}, + "asn": {"type": "AS", "object_relation": "asn"}, + "city": {"type": "text", "object_relation": "city"}, + "country_code": {"type": "text", "object_relation": "country-code"}, + "country": {"type": "text", "object_relation": "country"}, + "organization": {"type": "text", "object_relation": "organization"}, + "destination_country_codes": {"type": "text", "object_relation": "destination-country-codes"}, + "destination_countries": {"type": "text", "object_relation": "destination-countries"}, + "category": {"type": "text", "object_relation": "category"}, + "rdns": {"type": "text", "object_relation": "rdns"}, + } + self.vulnerability_mapping = { + "id": {"type": "text", "object_relation": "id"}, + "details": {"type": "text", "object_relation": "details"}, + "count": {"type": "text", "object_relation": "total-count"}, + "benign": {"type": "text", "object_relation": "benign-count"}, + "malicious": {"type": "text", "object_relation": "malicious-count"}, + "unknown": {"type": "text", "object_relation": "unknown-count"}, + } - headers = { - "Accept": "application/json", - "key": request["config"]["api_key"], - "User-Agent": "greynoise-misp-module-{}".format(moduleinfo["version"]), - } - - if not (request.get("vulnerability") or request.get("ip-dst") or request.get("ip-src")): - misperrors["error"] = "Vulnerability id missing" - return misperrors - - ip = "" - vulnerability = "" - - if request.get("ip-dst"): - ip = request.get("ip-dst") - elif request.get("ip-src"): - ip = request.get("ip-src") - else: - vulnerability = request.get("vulnerability") - - if ip: - if request["config"]["api_type"] and request["config"]["api_type"] == "enterprise": - greynoise_api_url = "https://api.greynoise.io/v2/noise/quick/" - else: - greynoise_api_url = "https://api.greynoise.io/v3/community/" - - response = requests.get(f"{greynoise_api_url}{ip}", headers=headers) # Real request for IP Query - if response.status_code == 200: - if request["config"]["api_type"] == "enterprise": - response = response.json() - enterprise_context_object = MISPObject("greynoise-ip-context") - for feature in ("ip", "code_message"): - if feature == "code_message": - value = codes_mapping[response.get("code")] - else: - value = response.get(feature) - if value: - attribute_type, relation = enterprise_context_basic_mapping[feature] - enterprise_context_object.add_attribute(relation, **{"type": attribute_type, "value": value}) - if response["noise"]: - greynoise_api_url = "https://api.greynoise.io/v2/noise/context/" - context_response = requests.get(f"{greynoise_api_url}{ip}", headers=headers) - context_response = context_response.json() - context_response["link"] = "https://www.greynoise.io/viz/ip/" + ip - if "tags" in context_response: - context_response["tags"] = ",".join(context_response["tags"]) - if "cve" in context_response: - context_response["cve"] = ",".join(context_response["cve"]) - for feature in enterprise_context_advanced_mapping.keys(): - value = context_response.get(feature) - if value: - attribute_type, relation = enterprise_context_advanced_mapping[feature] - enterprise_context_object.add_attribute( - relation, **{"type": attribute_type, "value": value} - ) - for feature in enterprise_context_advanced_metadata_mapping.keys(): - value = context_response["metadata"].get(feature) - if value: - attribute_type, relation = enterprise_context_advanced_metadata_mapping[feature] - enterprise_context_object.add_attribute( - relation, **{"type": attribute_type, "value": value} - ) - - if response["riot"]: - greynoise_api_url = "https://api.greynoise.io/v2/riot/" - riot_response = requests.get(f"{greynoise_api_url}{ip}", headers=headers) - riot_response = riot_response.json() - riot_response["link"] = "https://www.greynoise.io/viz/riot/" + ip - for feature in enterprise_riot_mapping.keys(): - value = riot_response.get(feature) - if value: - attribute_type, relation = enterprise_riot_mapping[feature] - enterprise_context_object.add_attribute( - relation, **{"type": attribute_type, "value": value} - ) - misp_event.add_object(enterprise_context_object) - event = json.loads(misp_event.to_json()) - results = {key: event[key] for key in ("Attribute", "Object") if (key in event and event[key])} - return {"results": results} + def query_greynoise_ip_hover(self, api_key, api_type): + if api_type == "enterprise": + logger.info(f"Starting hover enrichment for: {self.attribute.value} via GreyNoise ENT API") + integration_name = "greynoise-misp-module-{}".format(moduleinfo["version"]) + session = GreyNoise(api_key=api_key, integration_name=integration_name) + quick_response = session.quick(self.attribute.value) + if len(quick_response) != 1: + misperrors["error"] = "Quick IP lookup returned unexpected response" + return misperrors else: - response = response.json() - community_context_object = MISPObject("greynoise-community-ip-context") - for feature in community_found_mapping.keys(): - value = response.get(feature) - if value: - attribute_type, relation = community_found_mapping[feature] - community_context_object.add_attribute(relation, **{"type": attribute_type, "value": value}) - misp_event.add_object(community_context_object) - event = json.loads(misp_event.to_json()) - results = {key: event[key] for key in ("Attribute", "Object") if (key in event and event[key])} - return {"results": results} - if response.status_code == 404 and request["config"]["api_type"] != "enterprise": - response = response.json() - community_context_object = MISPObject("greynoise-community-ip-context") - for feature in community_not_found_mapping.keys(): - value = response.get(feature) - if value: - attribute_type, relation = community_not_found_mapping[feature] - community_context_object.add_attribute(relation, **{"type": attribute_type, "value": value}) - misp_event.add_object(community_context_object) - event = json.loads(misp_event.to_json()) - results = {key: event[key] for key in ("Attribute", "Object") if (key in event and event[key])} - return {"results": results} + quick_response = quick_response[0] + context_response = session.ip(self.attribute.value) + riot_response = session.riot(self.attribute.value) - if vulnerability: - if request["config"]["api_type"] and request["config"]["api_type"] == "enterprise": - greynoise_api_url = "https://api.greynoise.io/v2/experimental/gnql/stats" - querystring = {"query": f"last_seen:1w cve:{vulnerability}"} + if riot_response and "trust_level" in riot_response: + if riot_response["trust_level"] == "1": + riot_response["trust_level"] = "1 - Reasonably Ignore" + if riot_response["trust_level"] == "2": + riot_response["trust_level"] = "2 - Commonly Seen" + + if context_response and riot_response: + response = context_response.copy() + response.update(riot_response) + response.update(quick_response) + elif context_response: + response = context_response.copy() + response.update(quick_response) + elif riot_response: + response = riot_response.copy() + response.update(quick_response) + + response["link"] = "https://viz.greynoise.io/ip/" + self.attribute.value + + ip_address_attributes = [] + for feature, mapping in self.ip_address_hover_mapping.items(): + logger.debug(f"Checking feature {feature}") + if response.get(feature): + if feature in ["cve", "tags"]: + response[feature] = ", ".join(response[feature]) + if feature == "vpn_service" and response[feature] == "N/A": + continue + if feature == "actor" and response[feature] == "unknown": + continue + attribute = {"value": response[feature]} + logger.debug(f"Adding Feature: {feature}, Attribute: {attribute}") + attribute.update(mapping) + ip_address_attributes.append(attribute) + if "metadata" in context_response: + for feature, mapping in self.ip_address_metadata_mapping.items(): + logger.debug(f"Checking metadata feature {feature}") + if response["metadata"].get(feature): + if feature in ["destination_countries", "destination_country_codes"]: + response["metadata"][feature] = ", ".join(response["metadata"][feature]) + attribute = {"value": response["metadata"][feature]} + logger.debug(f"Adding Feature: {feature}, Attribute: {attribute}") + attribute.update(mapping) + ip_address_attributes.append(attribute) + if ip_address_attributes: + logger.debug("creating greynoise ip object") + gn_ip_object = MISPObject("greynoise-ip-details") + for attribute in ip_address_attributes: + logger.debug(f"adding attribute {attribute}") + gn_ip_object.add_attribute(**attribute) + logger.debug(f"attribute id: {self.attribute.uuid}") + gn_ip_object.add_reference(self.attribute.uuid, "describes") + self.misp_event.add_object(gn_ip_object) + else: + logger.info(f"Starting hover enrichment for: {self.attribute.value} via GreyNoise Community API") + integration_name = "greynoise-community-misp-module-{}".format(moduleinfo["version"]) + session = GreyNoise(api_key=api_key, integration_name=integration_name, offering="community") + community_response = session.ip(self.attribute.value) + + if "noise" in community_response and community_response["noise"]: + community_response["actor"] = community_response["name"] + community_response.pop("name") + + ip_address_attributes = [] + for feature, mapping in self.ip_address_hover_mapping.items(): + if community_response.get(feature): + if feature == "actor" and community_response[feature] == "unknown": + continue + attribute = {"value": community_response[feature]} + attribute.update(mapping) + ip_address_attributes.append(attribute) + if ip_address_attributes: + ip_address_object = MISPObject("greynoise-ip-details") + for attribute in ip_address_attributes: + ip_address_object.add_attribute(**attribute) + ip_address_object.add_reference(self.attribute.uuid, "describes") + self.misp_event.add_object(ip_address_object) + + def query_greynoise_ip_expansion(self, api_key, api_type): + if api_type == "enterprise": + logger.info(f"Starting expansion enrichment for: {self.attribute.value} via GreyNoise ENT API") + integration_name = "greynoise-misp-module-{}".format(moduleinfo["version"]) + session = GreyNoise(api_key=api_key, integration_name=integration_name) + quick_response = session.quick(self.attribute.value) + if len(quick_response) != 1: + misperrors["error"] = "Quick IP lookup returned unexpected response" + return misperrors + else: + quick_response = quick_response[0] + context_response = session.ip(self.attribute.value) + riot_response = session.riot(self.attribute.value) + + if riot_response and "trust_level" in riot_response: + if riot_response["trust_level"] == "1": + riot_response["trust_level"] = "1 - Reasonably Ignore" + if riot_response["trust_level"] == "2": + riot_response["trust_level"] = "2 - Commonly Seen" + + if context_response and riot_response: + response = context_response.copy() + response.update(riot_response) + response.update(quick_response) + elif context_response: + response = context_response.copy() + response.update(quick_response) + elif riot_response: + response = riot_response.copy() + response.update(quick_response) + + response["link"] = "https://viz.greynoise.io/ip/" + self.attribute.value + + ip_address_attributes = [] + for feature, mapping in self.ip_address_enrich_mapping.items(): + logger.debug(f"Checking feature {feature}") + if response.get(feature): + if feature == "actor" and response[feature] == "unknown": + continue + attribute = {"value": response[feature]} + logger.debug(f"Adding Feature: {feature}, Attribute: {attribute}") + attribute.update(mapping) + ip_address_attributes.append(attribute) + if ip_address_attributes: + logger.debug("creating greynoise ip object") + gn_ip_object = MISPObject("greynoise-ip") + for attribute in ip_address_attributes: + logger.debug(f"adding attribute {attribute}") + gn_ip_object.add_attribute(**attribute) + logger.debug(f"attribute id: {self.attribute.uuid}") + gn_ip_object.add_reference(self.attribute.uuid, "describes") + self.misp_event.add_object(gn_ip_object) + else: + logger.info(f"Starting expansion enrichment for: {self.attribute.value} via GreyNoise Community API") + integration_name = "greynoise-community-misp-module-{}".format(moduleinfo["version"]) + session = GreyNoise(api_key=api_key, integration_name=integration_name, offering="community") + community_response = session.ip(self.attribute.value) + + if "noise" in community_response and community_response["noise"]: + community_response["actor"] = community_response["name"] + community_response.pop("name") + + ip_address_attributes = [] + for feature, mapping in self.ip_address_enrich_mapping.items(): + if community_response.get(feature): + if feature == "actor" and community_response[feature] == "unknown": + continue + attribute = {"value": community_response[feature]} + attribute.update(mapping) + ip_address_attributes.append(attribute) + if ip_address_attributes: + ip_address_object = MISPObject("greynoise-ip") + for attribute in ip_address_attributes: + ip_address_object.add_attribute(**attribute) + ip_address_object.add_reference(self.attribute.uuid, "describes") + self.misp_event.add_object(ip_address_object) + + def query_greynoise_vulnerability(self, api_key, api_type): + if api_type == "enterprise": + logger.info(f"Starting expansion enrichment for: {self.attribute.value} via GreyNoise ENT API") + integration_name = "greynoise-misp-module-{}".format(moduleinfo["version"]) + session = GreyNoise(api_key=api_key, integration_name=integration_name) + querystring = f"last_seen:1w cve:{self.attribute.value}" else: misperrors["error"] = "Vulnerability Not Supported with Community API Key" return misperrors - response = requests.get(f"{greynoise_api_url}", headers=headers, params=querystring) # Real request + response = session.stats(querystring) - if response.status_code == 200: - response = response.json() - vulnerability_object = MISPObject("greynoise-vuln-info") + if "stats" in response: response["details"] = ( "The IP count below reflects the number of IPs seen " "by GreyNoise in the last 7 days scanning for this CVE." ) - response["id"] = vulnerability - for feature in ("id", "details", "count"): - value = response.get(feature) - if value: - attribute_type, relation = vulnerability_mapping[feature] - vulnerability_object.add_attribute(relation, **{"type": attribute_type, "value": value}) + response["id"] = self.attribute.value classifications = response["stats"].get("classifications") for item in classifications: if item["classification"] == "benign": value = item["count"] - attribute_type, relation = ("text", "Benign Scanner Count") - vulnerability_object.add_attribute(relation, **{"type": attribute_type, "value": value}) + response["benign"] = value if item["classification"] == "unknown": value = item["count"] - attribute_type, relation = ("text", "Unknown Scanner Count") - vulnerability_object.add_attribute(relation, **{"type": attribute_type, "value": value}) + response["unknown"] = value if item["classification"] == "malicious": value = item["count"] - attribute_type, relation = ("text", "Malicious Scanner Count") - vulnerability_object.add_attribute(relation, **{"type": attribute_type, "value": value}) - misp_event.add_object(vulnerability_object) - event = json.loads(misp_event.to_json()) - results = {key: event[key] for key in ("Attribute", "Object") if (key in event and event[key])} - return {"results": results} + response["malicious"] = value + vulnerability_attributes = [] + for feature, mapping in self.vulnerability_mapping.items(): + if response.get(feature): + attribute = {"value": response[feature]} + attribute.update(mapping) + vulnerability_attributes.append(attribute) + if vulnerability_attributes: + vulnerability_object = MISPObject("greynoise-vuln-info") + for attribute in vulnerability_attributes: + vulnerability_object.add_attribute(**attribute) + vulnerability_object.add_reference(self.attribute.uuid, "describes") + self.misp_event.add_object(vulnerability_object) - # There is an error - errors = { - 400: "Bad request.", - 404: "IP not observed scanning the internet or contained in RIOT data set.", - 401: "Unauthorized. Please check your API key.", - 429: "Too many requests. You've hit the rate-limit.", - } - try: - misperrors["error"] = errors[response.status_code] - except KeyError: - misperrors["error"] = f"GreyNoise API not accessible (HTTP {response.status_code})" - return misperrors + def get_result(self): + event = json.loads(self.misp_event.to_json()) + results = {key: event[key] for key in ("Attribute", "Object") if (key in event and event[key])} + return {"results": results} + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + if not request.get("config", {}).get("api_key"): + return {"error": "GreyNoise API Key required, but missing"} + if not request.get("config", {}).get("api_type"): + return {"error": "GreyNoise API type of enterprise or community required, but missing"} + if not request.get("attribute") or not check_input_attribute(request["attribute"]): + return {"error": f"{standard_error_message}, which should contain at least a type, a value and an uuid."} + attribute = request["attribute"] + if attribute["type"] not in mispattributes["input"]: + return {"error": "Unsupported attribute type."} + greynoise_parser = GreyNoiseParser(attribute) + + if attribute["type"] in ["ip-dst", "ip-src"]: + try: + ipaddress.IPv4Address(attribute["value"]) + if "persistent" in request: + greynoise_parser.query_greynoise_ip_hover(request["config"]["api_key"], request["config"]["api_type"]) + else: + greynoise_parser.query_greynoise_ip_expansion(request["config"]["api_key"], request["config"]["api_type"]) + except ValueError: + return {"error": "Not a valid IPv4 address"} + + if attribute["type"] == "vulnerability": + greynoise_parser.query_greynoise_vulnerability(request["config"]["api_key"], request["config"]["api_type"]) + + return greynoise_parser.get_result() def introspection(): diff --git a/misp_modules/modules/export_mod/defender_endpoint_export.py b/misp_modules/modules/export_mod/defender_endpoint_export.py index 1c36efb..cdab0bf 100755 --- a/misp_modules/modules/export_mod/defender_endpoint_export.py +++ b/misp_modules/modules/export_mod/defender_endpoint_export.py @@ -8,7 +8,7 @@ import json misperrors = {"error": "Error"} -types_to_use = ['sha1', 'md5', 'domain', 'ip', 'url'] +types_to_use = ['sha256', 'sha1', 'md5', 'domain', 'ip', 'url'] userConfig = { @@ -20,11 +20,17 @@ inputSource = ['event'] outputFileExtension = 'kql' responseType = 'application/txt' -moduleinfo = {'version': '1.0', 'author': 'Julien Bachmann, Hacknowledge', +moduleinfo = {'version': '1.1', 'author': 'Julien Bachmann, Hacknowledge, Maik Wuerth', 'description': 'Defender for Endpoint KQL hunting query export module', 'module-type': ['export']} +def handle_sha256(value, period): + query = f"""find in (DeviceAlertEvents, DeviceFileEvents, DeviceImageLoadEvents, DeviceProcessEvents) + where SHA256 == '{value}' or InitiatingProcessSHA1 == '{value}'""" + return query.replace('\n', ' ') + + def handle_sha1(value, period): query = f"""find in (DeviceAlertEvents, DeviceFileEvents, DeviceImageLoadEvents, DeviceProcessEvents) where SHA1 == '{value}' or InitiatingProcessSHA1 == '{value}'""" @@ -56,6 +62,7 @@ def handle_url(value, period): handlers = { + 'sha256': handle_sha256, 'sha1': handle_sha1, 'md5': handle_md5, 'domain': handle_domain, @@ -75,6 +82,10 @@ def handler(q=False): for attribute in event["Attribute"]: if attribute['type'] in types_to_use: output = output + handlers[attribute['type']](attribute['value'], config['Period']) + '\n' + for obj in event["Object"]: + for attribute in obj["Attribute"]: + if attribute['type'] in types_to_use: + output = output + handlers[attribute['type']](attribute['value'], config['Period']) + '\n' r = {"response": [], "data": str(base64.b64encode(bytes(output, 'utf-8')), 'utf-8')} return r diff --git a/mkdocs.yml b/mkdocs.yml index ea06ad7..1b6a4b7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,7 +16,7 @@ edit_uri: "" use_directory_urls: true # Copyright -copyright: "Copyright © 2019-2022 MISP Project" +copyright: "Copyright © 2019-2023 MISP Project" # Options extra: diff --git a/tests/test_expansions.py b/tests/test_expansions.py index 0a7bcf7..914ac2c 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -297,7 +297,7 @@ class TestExpansions(unittest.TestCase): ) else: response = self.misp_modules_post(query) - self.assertEqual(self.get_errors(response), 'Missing Greynoise API key.') + self.assertEqual(self.get_errors(response), 'GreyNoise API Key required, but missing') @unittest.skip("Service doesn't work") def test_ipasn(self):