Files
riposte-marketplace/integrations/sentinelone/scripts/update_uam_alert_assignee.py
T
f3nris 232fa2f0a6 fix(sentinelone): type the assignee mutation as the console does (1.17.1)
update_uam_alert_assignee declared the action id as String! and the user
as ID!; the console types them ID! and Long and refused the mutation at
validation (VariableTypeMismatch), so no alert was ever assigned.

The user id now goes out as a JSON number, and a value that is not the
numeric console id is refused with an error naming assignee_user_id.

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

78 lines
3.2 KiB
Python

import json, os, sys, urllib.request, urllib.parse, urllib.error
# Assignment rides the same alertTriggerActions mutation as the status and
# verdict updates, with the assignUser payload. The action id is the console's,
# and can be overridden for a tenant that names it differently.
#
# The variable types are the console's, not a guess: the action id is an ID!
# and assignUser.value a Long. Declared String! and ID!, the console rejected
# the mutation at validation (VariableTypeMismatch) before assigning anything.
DEFAULT_ACTION_ID = "S1/alert/assignUser"
MUTATION = """mutation AlertTriggerActions($id: String!, $actionId: ID!, $user: Long) {
alertTriggerActions(
filter: { or: [ { and: [ { fieldId: "id", stringEqual: { value: $id } } ] } ] },
actions: [ { id: $actionId, payload: { assignUser: { value: $user } } } ]
) {
__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",
}
alert_id = str(inputs.get("alert_id") or "").strip()
user_id = str(inputs.get("assignee_user_id") or "").strip()
if not alert_id or not user_id:
raise ValueError("alert_id and assignee_user_id are required")
# A Long is a number: an email or a display name would only be refused by
# the console with a type error that does not say which input was wrong.
if not user_id.isdigit():
raise ValueError("assignee_user_id must be the console's numeric user id "
"(assignee.userId, or the id from list users), got " + repr(user_id))
variables = {
"id": alert_id,
"actionId": str(inputs.get("action_id") or "").strip() or DEFAULT_ACTION_ID,
# Sent as a JSON number. Python's int keeps the 19 digits of a console
# id exactly; it fits the Long the console declares.
"user": int(user_id),
}
resp = request("POST", base + "/unifiedalerts/graphql", headers, {"query": MUTATION, "variables": variables})
if resp.get("errors"):
print(json.dumps({"error": "GraphQL error", "detail": resp["errors"]}))
sys.exit(1)
print(json.dumps({"alert_id": alert_id, "assignee_user_id": user_id, "result": resp.get("data") or {}}))
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)