From 7e960484466ec1e5adb7887ca67cf3e46d8c2603 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sat, 27 Jun 2026 16:00:04 +0200 Subject: [PATCH] feat(mock-edr-s1): EDR incident integration from OpenAPI spec Built from the published OpenAPI spec for mock instance s1 (type: edr). Incident ingestion (list_incidents) with since/after_id paging and an OCSF mapper + 'Mock EDR Incident' default type, plus an acknowledge/resolve/dismiss incident action. X-API-Key auth; the instance path segment is configurable. Co-Authored-By: Claude Opus 4.8 --- .../incident-types/mock-edr-incident.yaml | 3 + integrations/mock-edr-s1/manifest.yaml | 78 +++++++++++++++++++ .../mock-edr-s1/mappers/list_incidents.yaml | 23 ++++++ .../mock-edr-s1/scripts/incident_action.py | 40 ++++++++++ .../mock-edr-s1/scripts/list_incidents.py | 40 ++++++++++ .../mock-edr-s1/scripts/test_connection.py | 37 +++++++++ 6 files changed, 221 insertions(+) create mode 100644 integrations/mock-edr-s1/incident-types/mock-edr-incident.yaml create mode 100644 integrations/mock-edr-s1/manifest.yaml create mode 100644 integrations/mock-edr-s1/mappers/list_incidents.yaml create mode 100644 integrations/mock-edr-s1/scripts/incident_action.py create mode 100644 integrations/mock-edr-s1/scripts/list_incidents.py create mode 100644 integrations/mock-edr-s1/scripts/test_connection.py diff --git a/integrations/mock-edr-s1/incident-types/mock-edr-incident.yaml b/integrations/mock-edr-s1/incident-types/mock-edr-incident.yaml new file mode 100644 index 0000000..4e0c2a8 --- /dev/null +++ b/integrations/mock-edr-s1/incident-types/mock-edr-incident.yaml @@ -0,0 +1,3 @@ +name: "Mock EDR Incident" +color: "#00a8a8" +icon: "alert" diff --git a/integrations/mock-edr-s1/manifest.yaml b/integrations/mock-edr-s1/manifest.yaml new file mode 100644 index 0000000..77c9928 --- /dev/null +++ b/integrations/mock-edr-s1/manifest.yaml @@ -0,0 +1,78 @@ +id: mock_edr_s1 +name: Mock EDR (S1) +version: 1.0.0 +description: "Mock EDR API (instance s1) — incident ingestion with since/after_id pagination and incident actions (acknowledge / resolve / dismiss). Built from the published OpenAPI spec." +changelog: "1.0.0 — Initial release: incident ingestion (list_incidents) with an OCSF mapper, plus an acknowledge/resolve/dismiss action." +category: endpoint + +# Per-instance configuration. The API key is sent in the X-API-Key header. The +# instance segment of the path (/api//incidents) is configurable. +config_schema: + properties: + base_url: + type: string + description: "API base URL" + default: https://mockprod.riposte-labs.com + instance: + type: string + description: "Instance name used in the path (/api//incidents)" + default: s1 + api_key: + type: string + description: "API key (X-API-Key)" + x-soar-sensitive: true + required: + - base_url + - api_key + +auth: + - id: apikey + type: api_key + in: header + name: X-API-Key + value_template: "{{secret}}" + secret_field: api_key + +commands: + # ── Ingestion ─────────────────────────────────────────────────────────────── + - id: list_incidents + name: mock-edr-s1-list-incidents + description: "List incidents. Used for ingestion: results path = items. Supports incremental fetch via 'since' and cursor paging via 'after_id'." + risk: read + inputs_schema: + properties: + since: { type: string, description: "Return incidents created after this ISO-8601 timestamp. Incremental fetch watermark." } + after_id: { type: number, description: "Return incidents with ID greater than this value (cursor paging)" } + limit: { type: number, description: "Maximum number of incidents (default 100, max 1000)" } + required: [] + outputs_schema: { properties: {} } + ingest: + results_path: items + dedup_key: id + incremental_field: since + + - id: incident_action + name: mock-edr-s1-incident-action + description: "Acknowledge, resolve or dismiss an incident." + risk: safe_write + inputs_schema: + properties: + id: { type: number, description: "Incident ID" } + action: { type: string, description: "Action to apply: acknowledge, resolve or dismiss" } + required: [id, action] + outputs_schema: { properties: {} } + + # ── Connectivity test ───────────────────────────────────────────────────── + - id: test_connection + name: mock-edr-s1-test-connection + description: "Verify connectivity and credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + +ingestion: + command: list_incidents + mapper: list_incidents + default_incident_type: "Mock EDR Incident" diff --git a/integrations/mock-edr-s1/mappers/list_incidents.yaml b/integrations/mock-edr-s1/mappers/list_incidents.yaml new file mode 100644 index 0000000..70ffa55 --- /dev/null +++ b/integrations/mock-edr-s1/mappers/list_incidents.yaml @@ -0,0 +1,23 @@ +name: "Mock EDR Incidents → OCSF" +description: "Maps a Mock EDR incident (/api//incidents, results_path = items) to OCSF Detection Finding fields." +field_mappings: + title: "title" + description: "description" + source: "source" + # toSeverity maps critical→5, high→3, medium→2, low→1, informational→1. + severity: "severity" +# results_path = items; source_path is JSONata over ONE incident object. +ocsf: + # ── Finding ─────────────────────────────────────────────────────── + - { source_path: "id", ocsf_field: "finding_info.uid" } + - { source_path: "external_id", ocsf_field: "metadata.uid" } + - { source_path: "title", ocsf_field: "finding_info.title" } + - { source_path: "description", ocsf_field: "finding_info.desc" } + - { source_path: "created_at", ocsf_field: "finding_info.created_time" } + - { source_path: "status", ocsf_field: "status" } + - { source_path: "source", ocsf_field: "metadata.product.name" } + # ── Affected host / artefact ────────────────────────────────────── + - { source_path: "hostname", ocsf_field: "src_endpoint.hostname" } + - { source_path: "ip_address", ocsf_field: "src_endpoint.ip" } + - { source_path: "hostname", ocsf_field: "device.hostname" } + - { source_path: "file_path", ocsf_field: "file.path" } diff --git a/integrations/mock-edr-s1/scripts/incident_action.py b/integrations/mock-edr-s1/scripts/incident_action.py new file mode 100644 index 0000000..a335103 --- /dev/null +++ b/integrations/mock-edr-s1/scripts/incident_action.py @@ -0,0 +1,40 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://mockprod.riposte-labs.com").rstrip("/") + instance = str(s.get("instance") or "s1").strip("/") + headers = {"X-API-Key": s.get("api_key", ""), "Content-Type": "application/json", "Accept": "application/json"} + return base, instance, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path, body=None): + base, _, headers = _cfg() + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(base + "/" + path.lstrip("/"), data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + _, instance, _ = _cfg() + inp = _inputs() + inc_id = urllib.parse.quote(str(inp.get("id", "")), safe="") + body = {"action": inp.get("action")} + print(json.dumps(request("POST", "api/" + instance + "/incidents/" + inc_id + "/actions", body=body))) + + +try: + run() +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/mock-edr-s1/scripts/list_incidents.py b/integrations/mock-edr-s1/scripts/list_incidents.py new file mode 100644 index 0000000..5124dde --- /dev/null +++ b/integrations/mock-edr-s1/scripts/list_incidents.py @@ -0,0 +1,40 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://mockprod.riposte-labs.com").rstrip("/") + instance = str(s.get("instance") or "s1").strip("/") + headers = {"X-API-Key": s.get("api_key", ""), "Accept": "application/json"} + return base, instance, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, _, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + _, instance, _ = _cfg() + inp = _inputs() + params = {"since": inp.get("since"), "after_id": inp.get("after_id"), "limit": min(int(inp.get("limit") or 100), 1000)} + print(json.dumps(request("api/" + instance + "/incidents", params))) + + +try: + run() +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/mock-edr-s1/scripts/test_connection.py b/integrations/mock-edr-s1/scripts/test_connection.py new file mode 100644 index 0000000..fa8ce7a --- /dev/null +++ b/integrations/mock-edr-s1/scripts/test_connection.py @@ -0,0 +1,37 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://mockprod.riposte-labs.com").rstrip("/") + instance = str(s.get("instance") or "s1").strip("/") + headers = {"X-API-Key": s.get("api_key", ""), "Accept": "application/json"} + return base, instance, headers + + +def request(path, params): + base, _, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + _, instance, _ = _cfg() + request("api/" + instance + "/incidents", {"limit": 1}) + print(json.dumps({"ok": True})) + + +try: + run() +except urllib.error.HTTPError as e: + detail = e.read().decode("utf-8", "replace") + msg = "API key is not valid." if e.code in (401, 403) else "HTTP " + str(e.code) + print(json.dumps({"ok": False, "error": msg, "detail": detail})) + sys.exit(1) +except Exception as e: + print(json.dumps({"ok": False, "error": str(e)})) + sys.exit(1)