From 015131a2d140ad9ac3d1cf495a47879c331b8c26 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sun, 12 Jul 2026 14:58:07 +0200 Subject: [PATCH] feat(malwarebazaar): new MalwareBazaar sample-intel integration MalwareBazaar (abuse.ch) API v1, 5 commands: sample info by hash, recent samples, tag info, signature info. Auth-Key auth, stdlib-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- integrations/malwarebazaar/manifest.yaml | 65 +++++++++++++++++++ .../malwarebazaar/scripts/get_info.py | 46 +++++++++++++ .../malwarebazaar/scripts/get_recent.py | 44 +++++++++++++ .../malwarebazaar/scripts/get_siginfo.py | 47 ++++++++++++++ .../malwarebazaar/scripts/get_taginfo.py | 47 ++++++++++++++ .../malwarebazaar/scripts/test_connection.py | 43 ++++++++++++ 6 files changed, 292 insertions(+) create mode 100644 integrations/malwarebazaar/manifest.yaml create mode 100644 integrations/malwarebazaar/scripts/get_info.py create mode 100644 integrations/malwarebazaar/scripts/get_recent.py create mode 100644 integrations/malwarebazaar/scripts/get_siginfo.py create mode 100644 integrations/malwarebazaar/scripts/get_taginfo.py create mode 100644 integrations/malwarebazaar/scripts/test_connection.py diff --git a/integrations/malwarebazaar/manifest.yaml b/integrations/malwarebazaar/manifest.yaml new file mode 100644 index 0000000..c937169 --- /dev/null +++ b/integrations/malwarebazaar/manifest.yaml @@ -0,0 +1,65 @@ +id: malwarebazaar +name: MalwareBazaar +version: 1.0.0 +description: "MalwareBazaar by abuse.ch (API v1) — malware sample intelligence: look up a sample by hash, list recent samples, and query samples by tag or signature. Auth-Key authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: sample info by hash, recent samples, tag info, signature info." +category: enrichment + +# Per-instance configuration. abuse.ch requires an Auth-Key header. +config_schema: + properties: + auth_key: + type: string + description: "abuse.ch Auth-Key" + x-soar-sensitive: true + required: + - auth_key + +commands: + - id: get_info + name: malwarebazaar-get-info + description: "Look up a sample by hash (MD5, SHA-1, or SHA-256)." + risk: read + inputs_schema: + properties: + hash: { type: string, description: "Sample hash" } + required: [hash] + outputs_schema: { properties: {} } + - id: get_recent + name: malwarebazaar-get-recent + description: "List recently submitted samples." + risk: read + inputs_schema: + properties: + selector: { type: string, description: "'time' (last hour) or '100' (last 100) — default 100" } + required: [] + outputs_schema: { properties: {} } + - id: get_taginfo + name: malwarebazaar-get-taginfo + description: "List samples tagged with a given tag." + risk: read + inputs_schema: + properties: + tag: { type: string, description: "Tag (e.g. Emotet)" } + limit: { type: number, description: "Max samples (default 50)" } + required: [tag] + outputs_schema: { properties: {} } + - id: get_siginfo + name: malwarebazaar-get-siginfo + description: "List samples for a given malware signature/family." + risk: read + inputs_schema: + properties: + signature: { type: string, description: "Signature/family (e.g. TrickBot)" } + limit: { type: number, description: "Max samples (default 50)" } + required: [signature] + outputs_schema: { properties: {} } + + - id: test_connection + name: malwarebazaar-test-connection + description: "Verify the Auth-Key (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/malwarebazaar/scripts/get_info.py b/integrations/malwarebazaar/scripts/get_info.py new file mode 100644 index 0000000..51f3fd2 --- /dev/null +++ b/integrations/malwarebazaar/scripts/get_info.py @@ -0,0 +1,46 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://mb-api.abuse.ch/api/v1/" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def query(cfg, fields): + data = urllib.parse.urlencode({k: v for k, v in fields.items() if v not in (None, "")}).encode("utf-8") + headers = { + "Auth-Key": str(cfg.get("auth_key", "")), + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + req = urllib.request.Request(API, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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) + + +def main(cfg, inputs): + hash_ = inputs.get("hash") + if not hash_: + raise Exception("hash is required") + + return query(cfg, {"query": "get_info", "hash": hash_}) + + +_run(main) diff --git a/integrations/malwarebazaar/scripts/get_recent.py b/integrations/malwarebazaar/scripts/get_recent.py new file mode 100644 index 0000000..7dac6d6 --- /dev/null +++ b/integrations/malwarebazaar/scripts/get_recent.py @@ -0,0 +1,44 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://mb-api.abuse.ch/api/v1/" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def query(cfg, fields): + data = urllib.parse.urlencode({k: v for k, v in fields.items() if v not in (None, "")}).encode("utf-8") + headers = { + "Auth-Key": str(cfg.get("auth_key", "")), + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + req = urllib.request.Request(API, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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) + + +def main(cfg, inputs): + selector = inputs.get("selector") + + return query(cfg, {"query": "get_recent", "selector": (selector or "100")}) + + +_run(main) diff --git a/integrations/malwarebazaar/scripts/get_siginfo.py b/integrations/malwarebazaar/scripts/get_siginfo.py new file mode 100644 index 0000000..733cde3 --- /dev/null +++ b/integrations/malwarebazaar/scripts/get_siginfo.py @@ -0,0 +1,47 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://mb-api.abuse.ch/api/v1/" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def query(cfg, fields): + data = urllib.parse.urlencode({k: v for k, v in fields.items() if v not in (None, "")}).encode("utf-8") + headers = { + "Auth-Key": str(cfg.get("auth_key", "")), + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + req = urllib.request.Request(API, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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) + + +def main(cfg, inputs): + signature = inputs.get("signature") + if not signature: + raise Exception("signature is required") + limit = inputs.get("limit") + + return query(cfg, {"query": "get_siginfo", "signature": signature, "limit": int(limit or 50)}) + + +_run(main) diff --git a/integrations/malwarebazaar/scripts/get_taginfo.py b/integrations/malwarebazaar/scripts/get_taginfo.py new file mode 100644 index 0000000..41f3e8c --- /dev/null +++ b/integrations/malwarebazaar/scripts/get_taginfo.py @@ -0,0 +1,47 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://mb-api.abuse.ch/api/v1/" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def query(cfg, fields): + data = urllib.parse.urlencode({k: v for k, v in fields.items() if v not in (None, "")}).encode("utf-8") + headers = { + "Auth-Key": str(cfg.get("auth_key", "")), + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + req = urllib.request.Request(API, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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) + + +def main(cfg, inputs): + tag = inputs.get("tag") + if not tag: + raise Exception("tag is required") + limit = inputs.get("limit") + + return query(cfg, {"query": "get_taginfo", "tag": tag, "limit": int(limit or 50)}) + + +_run(main) diff --git a/integrations/malwarebazaar/scripts/test_connection.py b/integrations/malwarebazaar/scripts/test_connection.py new file mode 100644 index 0000000..1aceafe --- /dev/null +++ b/integrations/malwarebazaar/scripts/test_connection.py @@ -0,0 +1,43 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://mb-api.abuse.ch/api/v1/" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def query(cfg, fields): + data = urllib.parse.urlencode({k: v for k, v in fields.items() if v not in (None, "")}).encode("utf-8") + headers = { + "Auth-Key": str(cfg.get("auth_key", "")), + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + req = urllib.request.Request(API, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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) + + +def main(cfg, inputs): + query(cfg, {"query": "get_recent", "selector": "100"}) + return {"ok": True} + + +_run(main)