diff --git a/integrations/misp/manifest.yaml b/integrations/misp/manifest.yaml new file mode 100644 index 0000000..61e35da --- /dev/null +++ b/integrations/misp/manifest.yaml @@ -0,0 +1,131 @@ +id: misp +name: MISP +version: 1.0.0 +description: "MISP (threat-intelligence platform, REST API) — search events and attributes, read and create events, add attributes, tag events, add sightings, publish and delete events. API-key authentication; stdlib-only, no extra Python dependencies. Works with any MISP instance." +changelog: "1.0.0 — Initial release: event/attribute search, event read/create/publish/delete, attribute add, event tagging and sightings." +category: enrichment + +# Per-instance configuration. The API key (auth key) is sent in the +# 'Authorization' header. Set insecure for MISP instances with a self-signed cert. +config_schema: + properties: + server_url: + type: string + description: "MISP instance URL (e.g. https://misp.example.com)" + api_key: + type: string + description: "MISP API authentication key" + x-soar-sensitive: true + insecure: + type: boolean + description: "Trust any TLS certificate (not secure)" + default: false + required: + - server_url + - api_key + +commands: + - id: search_events + name: misp-search-events + description: "Search events with MISP REST-search filters (returns matching events)." + risk: read + inputs_schema: + properties: + value: { type: string, description: "Attribute value to match" } + type: { type: string, description: "Attribute type filter (e.g. ip-dst, domain, sha256)" } + tags: { type: string, description: "Comma-separated tags" } + limit: { type: number, description: "Maximum events (default 25)" } + filter_json: { type: string, description: "Raw restSearch filter as a JSON object (advanced; merged last)" } + required: [] + outputs_schema: { properties: {} } + - id: search_attributes + name: misp-search-attributes + description: "Search attributes with MISP REST-search filters." + risk: read + inputs_schema: + properties: + value: { type: string, description: "Attribute value to match" } + type: { type: string, description: "Attribute type filter" } + category: { type: string, description: "Attribute category filter" } + tags: { type: string, description: "Comma-separated tags" } + limit: { type: number, description: "Maximum attributes (default 25)" } + filter_json: { type: string, description: "Raw restSearch filter as a JSON object (advanced; merged last)" } + required: [] + outputs_schema: { properties: {} } + - id: get_event + name: misp-get-event + description: "Get a full event by ID or UUID." + risk: read + inputs_schema: + properties: + event_id: { type: string, description: "Event ID or UUID" } + required: [event_id] + outputs_schema: { properties: {} } + - id: create_event + name: misp-create-event + description: "Create a new event." + inputs_schema: + properties: + info: { type: string, description: "Event description/info" } + distribution: { type: number, description: "Distribution level 0-4 (default 0 = your org only)" } + threat_level_id: { type: number, description: "Threat level 1 (high) - 4 (undefined), default 4" } + analysis: { type: number, description: "Analysis state 0 (initial) - 2 (completed), default 0" } + published: { type: boolean, description: "Publish immediately (default false)" } + required: [info] + outputs_schema: { properties: {} } + - id: add_attribute + name: misp-add-attribute + description: "Add an attribute (indicator) to an event." + inputs_schema: + properties: + event_id: { type: string, description: "Event ID" } + type: { type: string, description: "Attribute type (e.g. ip-dst, domain, url, sha256)" } + value: { type: string, description: "Attribute value" } + category: { type: string, description: "Attribute category (e.g. Network activity)" } + to_ids: { type: boolean, description: "Mark the attribute for IDS export (default true)" } + comment: { type: string, description: "Comment" } + required: [event_id, type, value] + outputs_schema: { properties: {} } + - id: add_tag_to_event + name: misp-add-tag-to-event + description: "Attach a tag to an event." + inputs_schema: + properties: + event_id: { type: string, description: "Event ID or UUID" } + tag: { type: string, description: "Tag name (e.g. tlp:amber)" } + required: [event_id, tag] + outputs_schema: { properties: {} } + - id: add_sighting + name: misp-add-sighting + description: "Add a sighting for an attribute value." + inputs_schema: + properties: + value: { type: string, description: "Attribute value that was sighted" } + sighting_type: { type: number, description: "0 = sighting, 1 = false positive, 2 = expiration (default 0)" } + required: [value] + outputs_schema: { properties: {} } + - id: publish_event + name: misp-publish-event + description: "Publish an event." + inputs_schema: + properties: + event_id: { type: string, description: "Event ID" } + required: [event_id] + outputs_schema: { properties: {} } + - id: delete_event + name: misp-delete-event + description: "Delete an event." + inputs_schema: + properties: + event_id: { type: string, description: "Event ID" } + required: [event_id] + outputs_schema: { properties: {} } + + - id: test_connection + name: misp-test-connection + description: "Verify connectivity and the API key (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/misp/scripts/add_attribute.py b/integrations/misp/scripts/add_attribute.py new file mode 100644 index 0000000..3e601c0 --- /dev/null +++ b/integrations/misp/scripts/add_attribute.py @@ -0,0 +1,63 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) 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", "{}")) + event_id = inputs.get("event_id") + if not event_id: + raise Exception("event_id is required") + type_ = inputs.get("type") + if not type_: + raise Exception("type is required") + value = inputs.get("value") + if not value: + raise Exception("value is required") + category = inputs.get("category") + comment = inputs.get("comment") + + body = {"type": type_, "value": value, "to_ids": inputs.get("to_ids", True)} + if category: + body["category"] = category + if comment: + body["comment"] = comment + + res = request("POST", "/attributes/add/" + q(event_id), body) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/add_sighting.py b/integrations/misp/scripts/add_sighting.py new file mode 100644 index 0000000..6f79118 --- /dev/null +++ b/integrations/misp/scripts/add_sighting.py @@ -0,0 +1,49 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + value = inputs.get("value") + if not value: + raise Exception("value is required") + sighting_type = inputs.get("sighting_type") + + body = {"value": value, "type": str(sighting_type if sighting_type not in (None, "") else 0)} + + res = request("POST", "/sightings/add", body) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/add_tag_to_event.py b/integrations/misp/scripts/add_tag_to_event.py new file mode 100644 index 0000000..578da52 --- /dev/null +++ b/integrations/misp/scripts/add_tag_to_event.py @@ -0,0 +1,51 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + event_id = inputs.get("event_id") + if not event_id: + raise Exception("event_id is required") + tag = inputs.get("tag") + if not tag: + raise Exception("tag is required") + + body = {"uuid": event_id, "tag": tag} + + res = request("POST", "/tags/attachTagToObject", body) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/create_event.py b/integrations/misp/scripts/create_event.py new file mode 100644 index 0000000..bd971eb --- /dev/null +++ b/integrations/misp/scripts/create_event.py @@ -0,0 +1,58 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + info = inputs.get("info") + if not info: + raise Exception("info is required") + distribution = inputs.get("distribution") + threat_level_id = inputs.get("threat_level_id") + analysis = inputs.get("analysis") + published = inputs.get("published") + + body = { + "info": info, + "distribution": str(distribution if distribution not in (None, "") else 0), + "threat_level_id": str(threat_level_id or 4), + "analysis": str(analysis if analysis not in (None, "") else 0), + "published": bool(published), + } + + res = request("POST", "/events/add", body) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/delete_event.py b/integrations/misp/scripts/delete_event.py new file mode 100644 index 0000000..4c30b27 --- /dev/null +++ b/integrations/misp/scripts/delete_event.py @@ -0,0 +1,49 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) 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", "{}")) + event_id = inputs.get("event_id") + if not event_id: + raise Exception("event_id is required") + + res = request("POST", "/events/delete/" + q(event_id), {}) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/get_event.py b/integrations/misp/scripts/get_event.py new file mode 100644 index 0000000..21a2efc --- /dev/null +++ b/integrations/misp/scripts/get_event.py @@ -0,0 +1,49 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) 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", "{}")) + event_id = inputs.get("event_id") + if not event_id: + raise Exception("event_id is required") + + res = request("GET", "/events/view/" + q(event_id)) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/publish_event.py b/integrations/misp/scripts/publish_event.py new file mode 100644 index 0000000..2c9c01c --- /dev/null +++ b/integrations/misp/scripts/publish_event.py @@ -0,0 +1,49 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) 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", "{}")) + event_id = inputs.get("event_id") + if not event_id: + raise Exception("event_id is required") + + res = request("POST", "/events/publish/" + q(event_id), {}) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/search_attributes.py b/integrations/misp/scripts/search_attributes.py new file mode 100644 index 0000000..006e099 --- /dev/null +++ b/integrations/misp/scripts/search_attributes.py @@ -0,0 +1,68 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +split = lambda s: [t.strip() for t in str(s or "").split(",") if t.strip()] + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + value = inputs.get("value") + type_ = inputs.get("type") + category = inputs.get("category") + tags = inputs.get("tags") + limit = inputs.get("limit") + filter_json = inputs.get("filter_json") + + body = {"returnFormat": "json", "limit": limit or 25} + if value: + body["value"] = value + if type_: + body["type"] = type_ + if category: + body["category"] = category + tags_list = split(tags) + if tags_list: + body["tags"] = tags_list + if filter_json: + extra = json.loads(filter_json) + if not isinstance(extra, dict): + raise Exception("filter_json must be a JSON object") + body.update(extra) + + res = request("POST", "/attributes/restSearch", body) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/search_events.py b/integrations/misp/scripts/search_events.py new file mode 100644 index 0000000..c43edc0 --- /dev/null +++ b/integrations/misp/scripts/search_events.py @@ -0,0 +1,65 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +split = lambda s: [t.strip() for t in str(s or "").split(",") if t.strip()] + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + value = inputs.get("value") + type_ = inputs.get("type") + tags = inputs.get("tags") + limit = inputs.get("limit") + filter_json = inputs.get("filter_json") + + body = {"returnFormat": "json", "limit": limit or 25} + if value: + body["value"] = value + if type_: + body["type"] = type_ + tags_list = split(tags) + if tags_list: + body["tags"] = tags_list + if filter_json: + extra = json.loads(filter_json) + if not isinstance(extra, dict): + raise Exception("filter_json must be a JSON object") + body.update(extra) + + res = request("POST", "/events/restSearch", body) + print(json.dumps(res)) + + +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) diff --git a/integrations/misp/scripts/test_connection.py b/integrations/misp/scripts/test_connection.py new file mode 100644 index 0000000..ec181cc --- /dev/null +++ b/integrations/misp/scripts/test_connection.py @@ -0,0 +1,43 @@ +import json, os, ssl, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _ctx(): + if _cfg().get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +def request(method, path, body=None): + cfg = _cfg() + url = str(cfg.get("server_url") or "").rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Content-Type": "application/json", + "Authorization": str(cfg.get("api_key") or "")} + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90, context=_ctx()) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + res = request("GET", "/servers/getPyMISPVersion.json") + if not isinstance(res, dict): + raise Exception("unexpected response") + print(json.dumps({"ok": True, "version": res.get("version")})) + + +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)