From 0263619e30ccbe0b93caf005175cf95b12755f50 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Mon, 29 Jun 2026 22:32:47 +0200 Subject: [PATCH] feat(cortex-xdr): incident write-back and external alert push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three commands to the Cortex XDR integration (v1.2.0): - update_incident: change status, severity, assignment and resolve comment on an incident (closes the SOAR ingest→action loop). - insert_parsed_alerts: push external parsed alerts into Cortex XDR. - insert_cef_alerts: push external CEF-format alerts. Scripts follow the existing self-contained standard/advanced auth pattern. Co-Authored-By: Claude Opus 4.8 --- integrations/cortex-xdr/manifest.yaml | 36 +++++++++++- .../cortex-xdr/scripts/insert_cef_alerts.py | 54 ++++++++++++++++++ .../scripts/insert_parsed_alerts.py | 46 +++++++++++++++ .../cortex-xdr/scripts/update_incident.py | 57 +++++++++++++++++++ 4 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 integrations/cortex-xdr/scripts/insert_cef_alerts.py create mode 100644 integrations/cortex-xdr/scripts/insert_parsed_alerts.py create mode 100644 integrations/cortex-xdr/scripts/update_incident.py diff --git a/integrations/cortex-xdr/manifest.yaml b/integrations/cortex-xdr/manifest.yaml index 521b116..fe48fb4 100644 --- a/integrations/cortex-xdr/manifest.yaml +++ b/integrations/cortex-xdr/manifest.yaml @@ -1,8 +1,8 @@ id: cortex_xdr name: Cortex XDR -version: 1.1.0 -description: "Palo Alto Cortex XDR (public API v1) — incident ingestion, endpoint isolation/scan/delete/tagging, RTR scripts, hash block/allow lists, file quarantine/restore/retrieval, alert exclusions, device-control violations, audits, distributions and RBAC/risk." -changelog: "1.1.0 — Full command coverage: added delete/alias/tag endpoints, abort scan, original alerts, script metadata/code/snippet/exec-status, file retrieval (+details), alert exclusions, device-control violations, audits, distribution url/status/create and RBAC (users, roles, groups, risk score, risky users/hosts). 1.0.0 — Initial release: incident ingestion (get_incidents) with OCSF mapper, endpoints, isolate/unisolate, scan, hash blocklist/allowlist, quarantine/restore, run script + results, alerts retrieval, distributions and action status. Standard or Advanced API authentication." +version: 1.2.0 +description: "Palo Alto Cortex XDR (public API v1) — incident ingestion + write-back, endpoint isolation/scan/delete/tagging, RTR scripts, hash block/allow lists, file quarantine/restore/retrieval, alert exclusions, external alert push (parsed/CEF), device-control violations, audits, distributions and RBAC/risk." +changelog: "1.2.0 — Incident write-back (update_incident: status/severity/assignment/resolve comment) and external alert push (insert_parsed_alerts, insert_cef_alerts). 1.1.0 — Full command coverage: added delete/alias/tag endpoints, abort scan, original alerts, script metadata/code/snippet/exec-status, file retrieval (+details), alert exclusions, device-control violations, audits, distribution url/status/create and RBAC (users, roles, groups, risk score, risky users/hosts). 1.0.0 — Initial release: incident ingestion (get_incidents) with OCSF mapper, endpoints, isolate/unisolate, scan, hash blocklist/allowlist, quarantine/restore, run script + results, alerts retrieval, distributions and action status. Standard or Advanced API authentication." category: endpoint # Per-instance configuration. The base URL is the tenant API root, e.g. @@ -67,6 +67,20 @@ commands: alerts_limit: { type: number, description: "Max alerts to return (default 1000)" } required: [incident_id] outputs_schema: { properties: {} } + - id: update_incident + name: cortex-xdr-update-incident + description: "Update an incident's status, severity, assignment and resolve comment (write-back / close the loop)." + inputs_schema: + properties: + incident_id: { type: string, description: "Incident ID to update" } + status: { type: string, description: "New status (new, under_investigation, resolved_threat_handled, resolved_known_issue, resolved_duplicate, resolved_false_positive, resolved_true_positive, resolved_security_testing, resolved_auto)" } + manual_severity: { type: string, description: "Override severity (low, medium, high, critical)" } + assigned_user_mail: { type: string, description: "Assignee email" } + assigned_user_pretty_name: { type: string, description: "Assignee display name" } + unassign: { type: boolean, description: "Clear the assignment when true" } + resolve_comment: { type: string, description: "Resolution comment (shown when resolving)" } + required: [incident_id] + outputs_schema: { properties: {} } - id: get_alerts name: cortex-xdr-get-alerts description: "Retrieve alerts using a custom filter (get_alerts_by_filter_data). Returns rich alert objects." @@ -78,6 +92,22 @@ commands: limit: { type: number, description: "Maximum alerts to fetch (default 100)" } required: [] outputs_schema: { properties: {} } + - id: insert_parsed_alerts + name: cortex-xdr-insert-parsed-alerts + description: "Push external alerts (parsed JSON objects) into Cortex XDR for correlation." + inputs_schema: + properties: + alerts: { type: string, description: "JSON array of parsed alert objects (each with product, vendor, severity, alert_name, etc.)" } + required: [alerts] + outputs_schema: { properties: {} } + - id: insert_cef_alerts + name: cortex-xdr-insert-cef-alerts + description: "Push external alerts in CEF format into Cortex XDR for correlation." + inputs_schema: + properties: + alerts: { type: string, description: "CEF alert lines: a JSON array of CEF strings, or newline-separated CEF lines" } + required: [alerts] + outputs_schema: { properties: {} } # ── Endpoints ───────────────────────────────────────────────────────────── - id: get_endpoints diff --git a/integrations/cortex-xdr/scripts/insert_cef_alerts.py b/integrations/cortex-xdr/scripts/insert_cef_alerts.py new file mode 100644 index 0000000..324b68b --- /dev/null +++ b/integrations/cortex-xdr/scripts/insert_cef_alerts.py @@ -0,0 +1,54 @@ +import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.error + + +def _client(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = s.get("url", "").rstrip("/") + "/public_api/v1" + key = s.get("api_key", "") + kid = str(s.get("api_key_id", "")) + headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"} + if str(s.get("auth_type") or "standard").lower() == "advanced": + nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) + ts = str(int(time.time()) * 1000) + headers["x-xdr-nonce"] = nonce + headers["x-xdr-timestamp"] = ts + headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() + else: + headers["Authorization"] = key + return base, headers + + +def post(path, request_data): + base, headers = _client() + data = json.dumps({"request_data": request_data}).encode("utf-8") + req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + raw = inputs.get("alerts") + if isinstance(raw, list): + alerts = raw + else: + text = str(raw or "").strip() + try: + # A JSON array of CEF strings, when provided that way. + parsed = json.loads(text) + alerts = parsed if isinstance(parsed, list) else [text] + except Exception: + # Otherwise treat the input as newline-separated CEF lines. + alerts = [ln.strip() for ln in text.splitlines() if ln.strip()] + print(json.dumps(post("/alerts/insert_cef_alerts/", {"alerts": alerts}))) + + +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/cortex-xdr/scripts/insert_parsed_alerts.py b/integrations/cortex-xdr/scripts/insert_parsed_alerts.py new file mode 100644 index 0000000..891ba97 --- /dev/null +++ b/integrations/cortex-xdr/scripts/insert_parsed_alerts.py @@ -0,0 +1,46 @@ +import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.error + + +def _client(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = s.get("url", "").rstrip("/") + "/public_api/v1" + key = s.get("api_key", "") + kid = str(s.get("api_key_id", "")) + headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"} + if str(s.get("auth_type") or "standard").lower() == "advanced": + nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) + ts = str(int(time.time()) * 1000) + headers["x-xdr-nonce"] = nonce + headers["x-xdr-timestamp"] = ts + headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() + else: + headers["Authorization"] = key + return base, headers + + +def post(path, request_data): + base, headers = _client() + data = json.dumps({"request_data": request_data}).encode("utf-8") + req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + raw = inputs.get("alerts") + alerts = raw if isinstance(raw, list) else json.loads(raw or "[]") + if not isinstance(alerts, list): + alerts = [alerts] + print(json.dumps(post("/alerts/insert_parsed_alerts/", {"alerts": alerts}))) + + +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/cortex-xdr/scripts/update_incident.py b/integrations/cortex-xdr/scripts/update_incident.py new file mode 100644 index 0000000..eb2f547 --- /dev/null +++ b/integrations/cortex-xdr/scripts/update_incident.py @@ -0,0 +1,57 @@ +import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.error + + +def _client(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = s.get("url", "").rstrip("/") + "/public_api/v1" + key = s.get("api_key", "") + kid = str(s.get("api_key_id", "")) + headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"} + if str(s.get("auth_type") or "standard").lower() == "advanced": + nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) + ts = str(int(time.time()) * 1000) + headers["x-xdr-nonce"] = nonce + headers["x-xdr-timestamp"] = ts + headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() + else: + headers["Authorization"] = key + return base, headers + + +def post(path, request_data): + base, headers = _client() + data = json.dumps({"request_data": request_data}).encode("utf-8") + req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + update_data = {} + if inputs.get("status"): + update_data["status"] = inputs["status"] + if inputs.get("manual_severity"): + update_data["manual_severity"] = inputs["manual_severity"] + if inputs.get("assigned_user_mail"): + update_data["assigned_user_mail"] = inputs["assigned_user_mail"] + if inputs.get("assigned_user_pretty_name"): + update_data["assigned_user_pretty_name"] = inputs["assigned_user_pretty_name"] + if str(inputs.get("unassign")).lower() in ("true", "1", "yes"): + update_data["assigned_user_mail"] = None + update_data["assigned_user_pretty_name"] = None + if inputs.get("resolve_comment"): + update_data["resolve_comment"] = inputs["resolve_comment"] + rd = {"incident_id": inputs.get("incident_id"), "update_data": update_data} + print(json.dumps(post("/incidents/update_incident/", rd))) + + +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)