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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
name: "Mock EDR Incident"
|
||||||
|
color: "#00a8a8"
|
||||||
|
icon: "alert"
|
||||||
@@ -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/<instance>/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/<instance>/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"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: "Mock EDR Incidents → OCSF"
|
||||||
|
description: "Maps a Mock EDR incident (/api/<instance>/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" }
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user