71f8cd970e
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
58 lines
2.6 KiB
Python
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)
|