feat(greynoise): new GreyNoise enrichment integration

7 commands: IP context, quick check, RIOT, GNQL query + stats, IP
timeline. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Guillaume BOURGEOIS
2026-07-11 22:33:09 +02:00
parent 9504b22e04
commit 9ec84905be
8 changed files with 382 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
id: greynoise
name: GreyNoise
version: 1.0.0
description: "GreyNoise (API v2/v3) — tell internet background-noise scanners from targeted activity: IP context and quick classification, RIOT common-business-service lookup, GNQL query and stats, and IP scan timeline. API-key authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: IP context, quick check, RIOT, GNQL query and stats, and IP timeline."
category: enrichment
config_schema:
properties:
api_key:
type: string
description: "GreyNoise API key"
x-soar-sensitive: true
required:
- api_key
commands:
- id: ip_context
name: greynoise-ip-context
description: "Full GreyNoise context for an IP (classification, actor, tags, metadata)."
risk: read
inputs_schema:
properties:
ip: { type: string, description: "IP address" }
required: [ip]
outputs_schema: { properties: {} }
- id: ip_quick_check
name: greynoise-ip-quick-check
description: "Quick noise/RIOT classification for an IP (lightweight)."
risk: read
inputs_schema:
properties:
ip: { type: string, description: "IP address" }
required: [ip]
outputs_schema: { properties: {} }
- id: riot
name: greynoise-riot
description: "RIOT lookup — is the IP a known common business service (e.g. Google DNS, Office365)?"
risk: read
inputs_schema:
properties:
ip: { type: string, description: "IP address" }
required: [ip]
outputs_schema: { properties: {} }
- id: query
name: greynoise-query
description: "Run a GNQL (GreyNoise Query Language) query and return matching IPs."
risk: read
inputs_schema:
properties:
query: { type: string, description: "GNQL query string" }
size: { type: number, description: "Maximum results (default 50)" }
scroll: { type: string, description: "Scroll token for the next page" }
required: [query]
outputs_schema: { properties: {} }
- id: stats
name: greynoise-stats
description: "Aggregate statistics for a GNQL query (counts by classification, org, tag, …)."
risk: read
inputs_schema:
properties:
query: { type: string, description: "GNQL query string" }
count: { type: number, description: "Number of top aggregations to return" }
required: [query]
outputs_schema: { properties: {} }
- id: timeline
name: greynoise-timeline
description: "Daily scan-activity timeline for an IP."
risk: read
inputs_schema:
properties:
ip: { type: string, description: "IP address" }
days: { type: number, description: "Number of days of history (default 30)" }
required: [ip]
outputs_schema: { properties: {} }
- id: test_connection
name: greynoise-test-connection
description: "Verify the API key (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,42 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
API = "https://api.greynoise.io"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def request(method, path, params=None):
url = API + path
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
headers = {"Accept": "application/json", "key": str(_cfg().get("api_key") or "")}
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
q = lambda v: urllib.parse.quote(str(v), safe="")
def main():
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
ip = inputs.get("ip")
if not ip:
raise Exception("ip is required")
result = request("GET", "/v2/noise/context/" + q(ip))
print(json.dumps(result))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,42 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
API = "https://api.greynoise.io"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def request(method, path, params=None):
url = API + path
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
headers = {"Accept": "application/json", "key": str(_cfg().get("api_key") or "")}
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
q = lambda v: urllib.parse.quote(str(v), safe="")
def main():
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
ip = inputs.get("ip")
if not ip:
raise Exception("ip is required")
result = request("GET", "/v2/noise/quick/" + q(ip))
print(json.dumps(result))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
+46
View File
@@ -0,0 +1,46 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
API = "https://api.greynoise.io"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def request(method, path, params=None):
url = API + path
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
headers = {"Accept": "application/json", "key": str(_cfg().get("api_key") or "")}
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
q = lambda v: urllib.parse.quote(str(v), safe="")
def main():
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
query = inputs.get("query")
if not query:
raise Exception("query is required")
size = inputs.get("size")
scroll = inputs.get("scroll")
params = {"query": query, "size": size or 50, "scroll": scroll or None}
result = request("GET", "/v2/experimental/gnql", params=params)
print(json.dumps(result))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
+42
View File
@@ -0,0 +1,42 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
API = "https://api.greynoise.io"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def request(method, path, params=None):
url = API + path
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
headers = {"Accept": "application/json", "key": str(_cfg().get("api_key") or "")}
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
q = lambda v: urllib.parse.quote(str(v), safe="")
def main():
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
ip = inputs.get("ip")
if not ip:
raise Exception("ip is required")
result = request("GET", "/v2/riot/" + q(ip))
print(json.dumps(result))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
+45
View File
@@ -0,0 +1,45 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
API = "https://api.greynoise.io"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def request(method, path, params=None):
url = API + path
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
headers = {"Accept": "application/json", "key": str(_cfg().get("api_key") or "")}
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
q = lambda v: urllib.parse.quote(str(v), safe="")
def main():
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
query = inputs.get("query")
if not query:
raise Exception("query is required")
count = inputs.get("count")
params = {"query": query, "count": count or None}
result = request("GET", "/v2/experimental/gnql/stats", params=params)
print(json.dumps(result))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,36 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
API = "https://api.greynoise.io"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def request(method, path, params=None):
url = API + path
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
headers = {"Accept": "application/json", "key": str(_cfg().get("api_key") or "")}
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def main():
result = request("GET", "/v2/noise/quick/8.8.8.8")
if not isinstance(result, dict):
raise Exception("unexpected response")
print(json.dumps({"ok": True}))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,45 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
API = "https://api.greynoise.io"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def request(method, path, params=None):
url = API + path
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
headers = {"Accept": "application/json", "key": str(_cfg().get("api_key") or "")}
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
q = lambda v: urllib.parse.quote(str(v), safe="")
def main():
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
ip = inputs.get("ip")
if not ip:
raise Exception("ip is required")
days = inputs.get("days")
params = {"days": days or 30}
result = request("GET", "/v3/noise/ips/" + q(ip) + "/timeline", params=params)
print(json.dumps(result))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)