Files
riposte-marketplace/integrations/sentinelone/scripts/update_uam_alert_status.py
T
f3nris 71f8cd970e fix(sentinelone): fail a UAM update the console did not apply (1.17.2)
The assignee mutation came back with the alert under success while the
console kept its previous assignee, and the playbook block passed. The
script now reads the alert back and fails, naming the assignee the
console holds, when it is not the requested user.

The status, verdict and assignee updates also fail when the alert is
listed under failure or skip: the console answers those with a 200 and
no errors array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u8PFdkYuuPiC6JdZBV5x3
2026-09-28 14:43:53 +00:00

58 lines
2.6 KiB
Python

import json, os, sys, urllib.request, urllib.parse, urllib.error
MUTATION = (
"mutation AlertTriggerActions($id: String!, $status: Status!) {"
" alertTriggerActions("
" filter: { or: [ { and: [ { fieldId: \"id\", stringEqual: { value: $id } } ] } ] },"
" actions: [ { id: \"S1/alert/statusUpdate\", payload: { status: { value: $status } } } ]"
" ) { __typename ... on ActionsTriggered { actions { actionId alertCount"
" success { id } failure { id errorType errorMessage } skip { id } } } } }"
)
def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read()
return json.loads(raw) if raw else {}
def main():
secrets = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
base = secrets.get("url", "").rstrip("/") + "/web/api/v2.1"
headers = {
"Authorization": "ApiToken " + secrets.get("api_token", ""),
"Accept": "application/json",
"Content-Type": "application/json",
}
variables = {"id": inputs.get("alert_id"), "status": inputs.get("status")}
body = {"query": MUTATION, "variables": variables}
resp = request("POST", base + "/unifiedalerts/graphql", headers, body)
# GraphQL answers 200 with an errors array: printed as-is, a refused update
# would read as a successful one in the playbook that called it.
if resp.get("errors"):
print(json.dumps({"error": "GraphQL error", "detail": resp["errors"]}))
sys.exit(1)
# A refused or skipped alert comes back in the payload, not in errors: the
# console answers 200, and printed as-is it would read as a done update.
triggered = ((resp.get("data") or {}).get("alertTriggerActions") or {}).get("actions") or []
failures = [f for a in triggered for f in (a.get("failure") or [])]
skipped = [s for a in triggered for s in (a.get("skip") or [])]
if failures or skipped or not any(a.get("success") for a in triggered):
print(json.dumps({"error": "The console did not update the alert",
"failure": failures, "skip": skipped, "result": resp.get("data")}))
sys.exit(1)
print(json.dumps(resp))
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)