feat(cortex-xdr): incident write-back and external alert push

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 <noreply@anthropic.com>
This commit is contained in:
Guillaume BOURGEOIS
2026-06-29 22:32:47 +02:00
parent b99de4af19
commit 0263619e30
4 changed files with 190 additions and 3 deletions
+33 -3
View File
@@ -1,8 +1,8 @@
id: cortex_xdr id: cortex_xdr
name: Cortex XDR name: Cortex XDR
version: 1.1.0 version: 1.2.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." 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.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." 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 category: endpoint
# Per-instance configuration. The base URL is the tenant API root, e.g. # 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)" } alerts_limit: { type: number, description: "Max alerts to return (default 1000)" }
required: [incident_id] required: [incident_id]
outputs_schema: { properties: {} } 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 - id: get_alerts
name: cortex-xdr-get-alerts name: cortex-xdr-get-alerts
description: "Retrieve alerts using a custom filter (get_alerts_by_filter_data). Returns rich alert objects." 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)" } limit: { type: number, description: "Maximum alerts to fetch (default 100)" }
required: [] required: []
outputs_schema: { properties: {} } 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 ───────────────────────────────────────────────────────────── # ── Endpoints ─────────────────────────────────────────────────────────────
- id: get_endpoints - id: get_endpoints
@@ -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)
@@ -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)
@@ -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)