diff --git a/misp_modules/modules/expansion/greynoise.py b/misp_modules/modules/expansion/greynoise.py index 19b4653..a2ccf13 100644 --- a/misp_modules/modules/expansion/greynoise.py +++ b/misp_modules/modules/expansion/greynoise.py @@ -1,10 +1,12 @@ -import requests import json +import requests +from pymisp import MISPEvent, MISPObject + misperrors = {"error": "Error"} -mispattributes = {"input": ["ip-dst", "ip-src"], "output": ["text"]} +mispattributes = {"input": ["ip-dst", "ip-src", "vulnerability"], "output": ["text"]} moduleinfo = { - "version": "1.0", + "version": "1.1", "author": "Brad Chiappetta ", "description": "Module to access GreyNoise.io API.", "module-type": ["hover"], @@ -15,16 +17,71 @@ codes_mapping = { "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", + "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", + "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 60 days", + "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 @@ -33,66 +90,153 @@ def handler(q=False): # noqa: C901 request = json.loads(q) if not request.get("config") or not request["config"].get("api_key"): return {"error": "Missing Greynoise API key."} - 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/" headers = { "Accept": "application/json", "key": request["config"]["api_key"], "User-Agent": "greynoise-misp-module-{}".format(moduleinfo["version"]), } - for input_type in mispattributes["input"]: - if input_type in request: - ip = request[input_type] - break - else: - misperrors["error"] = "Unsupported attributes type." + + if not (request.get("vulnerability") or request.get("ip-dst") or request.get("ip-src")): + misperrors["error"] = "Vulnerability id missing" return misperrors - response = requests.get(f"{greynoise_api_url}{ip}", headers=headers) # Real request - if response.status_code == 200: - if request["config"]["api_type"] == "enterprise": - return { - "results": [ - { - "types": ["text"], - "values": codes_mapping[response.json()["code"]], - } - ] - } - elif response.json()["noise"]: - return { - "results": [ - { - "types": ["text"], - "values": "IP Address ({}) has been observed by GreyNoise " - "scanning the internet in the last 90 days. GreyNoise has " - "classified it as {} and it was last seen on {}. For more " - "information visit {}".format( - response.json()["ip"], - response.json()["classification"], - response.json()["last_seen"], - response.json()["link"], - ), - } - ] - } - elif response.json()["riot"]: - return { - "results": [ - { - "types": ["text"], - "values": "IP Address ({}) is part of GreyNoise Project RIOT " - "and likely belongs to a benign service from {}. For more " - "information visit {}".format( - response.json()["ip"], - response.json()["name"], - response.json()["link"], - ), - } - ] - } + + 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} + 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} + + 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}"} + 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 + + if response.status_code == 200: + response = response.json() + vulnerability_object = MISPObject("greynoise-vuln-info") + 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}) + 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}) + if item["classification"] == "unknown": + value = item["count"] + attribute_type, relation = ("text", "Unknown Scanner Count") + vulnerability_object.add_attribute(relation, **{"type": attribute_type, "value": 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} + # There is an error errors = { 400: "Bad request.", @@ -103,9 +247,7 @@ def handler(q=False): # noqa: C901 try: misperrors["error"] = errors[response.status_code] except KeyError: - misperrors[ - "error" - ] = f"GreyNoise API not accessible (HTTP {response.status_code})" + misperrors["error"] = f"GreyNoise API not accessible (HTTP {response.status_code})" return misperrors