mirror of https://github.com/MISP/misp-modules
Merge pull request #514 from GreyNoise-Intelligence/greynoise-add-cve-enhance-ip
Add CVE Lookup and Enhance IP Lookuppull/515/head
commit
e57393a71d
|
@ -608,16 +608,22 @@ Module to query a local copy of Maxmind's Geolite database.
|
||||||
|
|
||||||
Module to access GreyNoise.io API
|
Module to access GreyNoise.io API
|
||||||
- **features**:
|
- **features**:
|
||||||
>The module takes an IP address as input and queries Greynoise for some additional information about it: basically it checks whether a given IP address is “Internet background noise”, or has been observed scanning or attacking devices across the Internet. The result is returned as text.
|
> - Query an IP from GreyNoise to see if it is internet background noise or a common business service
|
||||||
|
> - Query a CVE from GreyNoise to see the total number of internet scanners looking for the CVE in the last 7 days
|
||||||
|
> - Supports Enterprise (Paid) and Community API for IP lookup
|
||||||
|
> - CVE Lookup is only supported with an Enterprise API Key
|
||||||
- **input**:
|
- **input**:
|
||||||
>An IP address.
|
>An IP address or CVE ID.
|
||||||
- **output**:
|
- **output**:
|
||||||
>Additional information about the IP fetched from Greynoise API.
|
> - For IPs: IP Lookup Details
|
||||||
|
> - FOR CVEs: Scanner Count for last 7 days
|
||||||
- **references**:
|
- **references**:
|
||||||
> - https://greynoise.io/
|
> - https://greynoise.io/
|
||||||
> - https://github.com/GreyNoise-Intelligence/api.greynoise.io
|
> - https://docs.greyniose.io/
|
||||||
|
> - https://www.greynoise.io/viz/account/
|
||||||
- **requirements**:
|
- **requirements**:
|
||||||
>A Greynoise API key.
|
> - A Greynoise API key.
|
||||||
|
> - Selection of API Key type: `enterprise` (for Paid users) or `community` (for Free users)
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 90 KiB |
|
@ -1,10 +1,12 @@
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from pymisp import MISPEvent, MISPObject
|
||||||
|
|
||||||
misperrors = {"error": "Error"}
|
misperrors = {"error": "Error"}
|
||||||
mispattributes = {"input": ["ip-dst", "ip-src"], "output": ["text"]}
|
mispattributes = {"input": ["ip-dst", "ip-src", "vulnerability"], "output": ["text"]}
|
||||||
moduleinfo = {
|
moduleinfo = {
|
||||||
"version": "1.0",
|
"version": "1.1",
|
||||||
"author": "Brad Chiappetta <brad@greynoise.io>",
|
"author": "Brad Chiappetta <brad@greynoise.io>",
|
||||||
"description": "Module to access GreyNoise.io API.",
|
"description": "Module to access GreyNoise.io API.",
|
||||||
"module-type": ["hover"],
|
"module-type": ["hover"],
|
||||||
|
@ -15,16 +17,71 @@ codes_mapping = {
|
||||||
"0x01": "The IP has been observed by the GreyNoise sensor network",
|
"0x01": "The IP has been observed by the GreyNoise sensor network",
|
||||||
"0x02": "The IP has been observed scanning 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",
|
"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 "
|
"0x03": "The IP is adjacent to another host that has been directly observed by the GreyNoise sensor network",
|
||||||
"the GreyNoise sensor network",
|
|
||||||
"0x04": "Reserved",
|
"0x04": "Reserved",
|
||||||
"0x05": "This IP is commonly spoofed in Internet-scan activity",
|
"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 "
|
"0x06": "This IP has been observed as noise, but this host belongs to a cloud provider where IPs can be "
|
||||||
"provider where IPs can be cycled frequently",
|
"cycled frequently",
|
||||||
"0x07": "This IP is invalid",
|
"0x07": "This IP is invalid",
|
||||||
"0x08": "This IP was classified as noise, but has not been observed engaging in "
|
"0x08": "This IP was classified as noise, but has not been observed engaging in Internet-wide scans or "
|
||||||
"Internet-wide scans or attacks in over 60 days",
|
"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
|
def handler(q=False): # noqa: C901
|
||||||
|
@ -33,66 +90,153 @@ def handler(q=False): # noqa: C901
|
||||||
request = json.loads(q)
|
request = json.loads(q)
|
||||||
if not request.get("config") or not request["config"].get("api_key"):
|
if not request.get("config") or not request["config"].get("api_key"):
|
||||||
return {"error": "Missing Greynoise 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 = {
|
headers = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"key": request["config"]["api_key"],
|
"key": request["config"]["api_key"],
|
||||||
"User-Agent": "greynoise-misp-module-{}".format(moduleinfo["version"]),
|
"User-Agent": "greynoise-misp-module-{}".format(moduleinfo["version"]),
|
||||||
}
|
}
|
||||||
for input_type in mispattributes["input"]:
|
|
||||||
if input_type in request:
|
if not (request.get("vulnerability") or request.get("ip-dst") or request.get("ip-src")):
|
||||||
ip = request[input_type]
|
misperrors["error"] = "Vulnerability id missing"
|
||||||
break
|
|
||||||
else:
|
|
||||||
misperrors["error"] = "Unsupported attributes type."
|
|
||||||
return misperrors
|
return misperrors
|
||||||
response = requests.get(f"{greynoise_api_url}{ip}", headers=headers) # Real request
|
|
||||||
if response.status_code == 200:
|
ip = ""
|
||||||
if request["config"]["api_type"] == "enterprise":
|
vulnerability = ""
|
||||||
return {
|
|
||||||
"results": [
|
if request.get("ip-dst"):
|
||||||
{
|
ip = request.get("ip-dst")
|
||||||
"types": ["text"],
|
elif request.get("ip-src"):
|
||||||
"values": codes_mapping[response.json()["code"]],
|
ip = request.get("ip-src")
|
||||||
}
|
else:
|
||||||
]
|
vulnerability = request.get("vulnerability")
|
||||||
}
|
|
||||||
elif response.json()["noise"]:
|
if ip:
|
||||||
return {
|
if request["config"]["api_type"] and request["config"]["api_type"] == "enterprise":
|
||||||
"results": [
|
greynoise_api_url = "https://api.greynoise.io/v2/noise/quick/"
|
||||||
{
|
else:
|
||||||
"types": ["text"],
|
greynoise_api_url = "https://api.greynoise.io/v3/community/"
|
||||||
"values": "IP Address ({}) has been observed by GreyNoise "
|
|
||||||
"scanning the internet in the last 90 days. GreyNoise has "
|
response = requests.get(f"{greynoise_api_url}{ip}", headers=headers) # Real request for IP Query
|
||||||
"classified it as {} and it was last seen on {}. For more "
|
if response.status_code == 200:
|
||||||
"information visit {}".format(
|
if request["config"]["api_type"] == "enterprise":
|
||||||
response.json()["ip"],
|
response = response.json()
|
||||||
response.json()["classification"],
|
enterprise_context_object = MISPObject("greynoise-ip-context")
|
||||||
response.json()["last_seen"],
|
for feature in ("ip", "code_message"):
|
||||||
response.json()["link"],
|
if feature == "code_message":
|
||||||
),
|
value = codes_mapping[response.get("code")]
|
||||||
}
|
else:
|
||||||
]
|
value = response.get(feature)
|
||||||
}
|
if value:
|
||||||
elif response.json()["riot"]:
|
attribute_type, relation = enterprise_context_basic_mapping[feature]
|
||||||
return {
|
enterprise_context_object.add_attribute(relation, **{"type": attribute_type, "value": value})
|
||||||
"results": [
|
if response["noise"]:
|
||||||
{
|
greynoise_api_url = "https://api.greynoise.io/v2/noise/context/"
|
||||||
"types": ["text"],
|
context_response = requests.get(f"{greynoise_api_url}{ip}", headers=headers)
|
||||||
"values": "IP Address ({}) is part of GreyNoise Project RIOT "
|
context_response = context_response.json()
|
||||||
"and likely belongs to a benign service from {}. For more "
|
context_response["link"] = "https://www.greynoise.io/viz/ip/" + ip
|
||||||
"information visit {}".format(
|
if "tags" in context_response:
|
||||||
response.json()["ip"],
|
context_response["tags"] = ",".join(context_response["tags"])
|
||||||
response.json()["name"],
|
if "cve" in context_response:
|
||||||
response.json()["link"],
|
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
|
# There is an error
|
||||||
errors = {
|
errors = {
|
||||||
400: "Bad request.",
|
400: "Bad request.",
|
||||||
|
@ -103,9 +247,7 @@ def handler(q=False): # noqa: C901
|
||||||
try:
|
try:
|
||||||
misperrors["error"] = errors[response.status_code]
|
misperrors["error"] = errors[response.status_code]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
misperrors[
|
misperrors["error"] = f"GreyNoise API not accessible (HTTP {response.status_code})"
|
||||||
"error"
|
|
||||||
] = f"GreyNoise API not accessible (HTTP {response.status_code})"
|
|
||||||
return misperrors
|
return misperrors
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue