From d7df536eaf26d5265df29718d029fb0703909848 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Mon, 22 Jun 2026 21:50:56 +0200 Subject: [PATCH] feat(sentinelone): add 13 commands (threat analysis, UAM alerts, remote-script status/results, PowerQuery, tag rule, fetch-file, endpoint logs) Brings the SentinelOne integration to 83 commands. New: threat-analysis, threat-download-from-cloud, abort-endpoint-scan, endpoint-fetch-logs, fetch-file, get-remote-script-task-status, get-remote-script-task-results, get-service-users, list-installed-singularity-marketplace-applications, update-uam-alert-status, update-uam-alert-verdict, run-powerquery (Singularity Data Lake), create-tag-rule. Each command ships a stdlib-only script against API v2.1. --- integrations/sentinelone/manifest.yaml | 169 +++++++++++++++++- .../scripts/abort_endpoint_scan.py | 36 ++++ .../sentinelone/scripts/create_tag_rule.py | 58 ++++++ .../scripts/endpoint_fetch_logs.py | 49 +++++ .../sentinelone/scripts/fetch_file.py | 33 ++++ .../scripts/get_remote_script_task_results.py | 38 ++++ .../scripts/get_remote_script_task_status.py | 41 +++++ .../sentinelone/scripts/get_service_users.py | 39 ++++ ...ed_singularity_marketplace_applications.py | 41 +++++ .../sentinelone/scripts/run_powerquery.py | 52 ++++++ .../sentinelone/scripts/threat_analysis.py | 32 ++++ .../scripts/threat_download_from_cloud.py | 32 ++++ .../scripts/update_uam_alert_status.py | 42 +++++ .../scripts/update_uam_alert_verdict.py | 42 +++++ 14 files changed, 702 insertions(+), 2 deletions(-) create mode 100644 integrations/sentinelone/scripts/abort_endpoint_scan.py create mode 100644 integrations/sentinelone/scripts/create_tag_rule.py create mode 100644 integrations/sentinelone/scripts/endpoint_fetch_logs.py create mode 100644 integrations/sentinelone/scripts/fetch_file.py create mode 100644 integrations/sentinelone/scripts/get_remote_script_task_results.py create mode 100644 integrations/sentinelone/scripts/get_remote_script_task_status.py create mode 100644 integrations/sentinelone/scripts/get_service_users.py create mode 100644 integrations/sentinelone/scripts/list_installed_singularity_marketplace_applications.py create mode 100644 integrations/sentinelone/scripts/run_powerquery.py create mode 100644 integrations/sentinelone/scripts/threat_analysis.py create mode 100644 integrations/sentinelone/scripts/threat_download_from_cloud.py create mode 100644 integrations/sentinelone/scripts/update_uam_alert_status.py create mode 100644 integrations/sentinelone/scripts/update_uam_alert_verdict.py diff --git a/integrations/sentinelone/manifest.yaml b/integrations/sentinelone/manifest.yaml index d13bf12..cac3a1d 100644 --- a/integrations/sentinelone/manifest.yaml +++ b/integrations/sentinelone/manifest.yaml @@ -1,8 +1,8 @@ id: sentinelone name: SentinelOne -version: 1.1.0 +version: 1.2.0 description: "SentinelOne Singularity (API v2.1) — endpoint detection & response: triage threats, enrich, isolate/reconnect hosts, mitigate, scan." -changelog: "1.1.0 — Command names prefixed with 'sentinelone-' (e.g. sentinelone-isolate-agent) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: 70 commands covering agents, threats, alerts, blocklist/exclusions, IOCs, STAR rules, Deep Visibility, remote scripts, tags, firewall and network discovery based on the SentinelOne API v2.1." +changelog: "1.2.0 — Added 13 commands: threat-analysis, threat-download-from-cloud, abort-endpoint-scan, endpoint-fetch-logs, fetch-file, get-remote-script-task-status/results, get-service-users, list-installed-singularity-marketplace-applications, update-uam-alert-status/verdict, run-powerquery and create-tag-rule (83 commands total). 1.1.0 — Command names prefixed with 'sentinelone-' (e.g. sentinelone-isolate-agent) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: 70 commands covering agents, threats, alerts, blocklist/exclusions, IOCs, STAR rules, Deep Visibility, remote scripts, tags, firewall and network discovery based on the SentinelOne API v2.1." category: endpoint # Per-instance configuration. The scripts build the API base as /web/api/v2.1. @@ -790,3 +790,168 @@ commands: limit: { type: number, description: "Max results (default 50)." } required: [] outputs_schema: { properties: {} } + + # ── Threat analysis / sample ────────────────────────────────────────────── + - id: threat_analysis + name: sentinelone-threat-analysis + description: "Return the detailed analysis for a threat (agent, detection and threat info)." + inputs_schema: + properties: + threat_id: { type: string, description: "Threat ID." } + required: [threat_id] + outputs_schema: { properties: {} } + + - id: threat_download_from_cloud + name: sentinelone-threat-download-from-cloud + description: "Get a download URL for a threat's sample from the cloud (BinaryVault)." + inputs_schema: + properties: + threat_id: { type: string, description: "Threat ID." } + required: [threat_id] + outputs_schema: { properties: {} } + + # ── Endpoint actions ────────────────────────────────────────────────────── + - id: abort_endpoint_scan + name: sentinelone-abort-endpoint-scan + description: "Abort the disk scan on the given agents." + inputs_schema: + properties: + agent_ids: { type: string, description: "Comma-separated agent IDs." } + required: [agent_ids] + outputs_schema: { properties: {} } + + - id: endpoint_fetch_logs + name: sentinelone-endpoint-fetch-logs + description: "Fetch agent/endpoint logs from the given agents." + inputs_schema: + properties: + agent_ids: { type: string, description: "Comma-separated agent IDs." } + agents_logs: { type: boolean, description: "Fetch agent logs (default true)." } + customer_facing_logs: { type: boolean, description: "Fetch customer-facing logs (default false)." } + platform_logs: { type: boolean, description: "Fetch platform logs (default false)." } + required: [agent_ids] + outputs_schema: { properties: {} } + + - id: fetch_file + name: sentinelone-fetch-file + description: "Request a file fetch from an agent endpoint (collected as a password-protected zip)." + inputs_schema: + properties: + agent_id: { type: string, description: "Agent ID." } + file_path: { type: string, description: "Absolute file path on the endpoint." } + password: { type: string, description: "Password to protect the resulting zip." } + required: [agent_id, file_path, password] + outputs_schema: { properties: {} } + + # ── Remote scripts (status / results) ───────────────────────────────────── + - id: get_remote_script_task_status + name: sentinelone-get-remote-script-task-status + description: "Get the status of remote-script tasks for a parent task." + inputs_schema: + properties: + parent_task_id: { type: string, description: "Parent task ID." } + ids: { type: string, description: "Comma-separated task IDs." } + status: { type: string, description: "Filter by status (e.g. created, completed)." } + computer_name_contains: { type: string, description: "Filter by computer name substring." } + site_ids: { type: string, description: "Comma-separated site IDs." } + account_ids: { type: string, description: "Comma-separated account IDs." } + limit: { type: number, description: "Max results (default 50)." } + required: [parent_task_id] + outputs_schema: { properties: {} } + + - id: get_remote_script_task_results + name: sentinelone-get-remote-script-task-results + description: "Get download links for the results of remote-script tasks." + inputs_schema: + properties: + task_ids: { type: string, description: "Comma-separated task IDs." } + computer_names: { type: string, description: "Comma-separated computer names." } + required: [task_ids] + outputs_schema: { properties: {} } + + # ── Users / marketplace apps ────────────────────────────────────────────── + - id: get_service_users + name: sentinelone-get-service-users + description: "List service users (API token accounts)." + inputs_schema: + properties: + account_ids: { type: string, description: "Comma-separated account IDs." } + role_ids: { type: string, description: "Comma-separated RBAC role IDs." } + ids: { type: string, description: "Comma-separated service-user IDs." } + site_ids: { type: string, description: "Comma-separated site IDs." } + limit: { type: number, description: "Max results (default 100)." } + required: [] + outputs_schema: { properties: {} } + + - id: list_installed_singularity_marketplace_applications + name: sentinelone-list-installed-singularity-marketplace-applications + description: "List installed Singularity Marketplace applications." + inputs_schema: + properties: + account_ids: { type: string, description: "Comma-separated account IDs." } + application_catalog_id: { type: string, description: "Filter by application catalog ID." } + creator_contains: { type: string, description: "Filter by creator substring." } + ids: { type: string, description: "Comma-separated application IDs." } + name_contains: { type: string, description: "Filter by application name substring." } + site_ids: { type: string, description: "Comma-separated site IDs." } + limit: { type: number, description: "Max results (default 100)." } + required: [] + outputs_schema: { properties: {} } + + # ── Unified (UAM) alerts ────────────────────────────────────────────────── + - id: update_uam_alert_status + name: sentinelone-update-uam-alert-status + description: "Update the status of a unified (UAM) alert." + inputs_schema: + properties: + alert_id: { type: string, description: "UAM alert ID (UUID)." } + status: { type: string, description: "NEW | IN_PROGRESS | RESOLVED" } + required: [alert_id, status] + outputs_schema: { properties: {} } + + - id: update_uam_alert_verdict + name: sentinelone-update-uam-alert-verdict + description: "Update the analyst verdict of a unified (UAM) alert." + inputs_schema: + properties: + alert_id: { type: string, description: "UAM alert ID (UUID)." } + analyst_verdict: { type: string, description: "Verdict enum, e.g. TRUE_POSITIVE_MALWARE, FALSE_POSITIVE_BENIGN, UNDEFINED." } + required: [alert_id, analyst_verdict] + outputs_schema: { properties: {} } + + # ── PowerQuery (Singularity Data Lake) ──────────────────────────────────── + - id: run_powerquery + name: sentinelone-run-powerquery + description: "Run a PowerQuery against the Singularity Data Lake (separate XDR URL + Log Read API key)." + inputs_schema: + properties: + singularity_xdr_url: { type: string, description: "Singularity Data Lake XDR URL (https://...)." } + singularity_xdr_api_key: { type: string, description: "Log Read Access API key." } + query: { type: string, description: "PowerQuery syntax query." } + start_time: { type: string, description: "Query start time." } + end_time: { type: string, description: "Query end time." } + priority: { type: string, description: "Execution priority (default low)." } + recurring: { type: boolean, description: "Create a materialized view (default false)." } + team_emails: { type: string, description: "Comma-separated account emails for cross-team search." } + required: [singularity_xdr_url, singularity_xdr_api_key, query] + outputs_schema: { properties: {} } + + # ── Dynamic asset tag rule (XDR) ────────────────────────────────────────── + - id: create_tag_rule + name: sentinelone-create-tag-rule + description: "Create a dynamic asset tag rule (XDR) that tags assets matching a property filter." + inputs_schema: + properties: + name: { type: string, description: "Rule name." } + account_id: { type: string, description: "Account ID the rule belongs to." } + tag_id: { type: string, description: "Existing tag ID to apply." } + filter_values: { type: string, description: "Comma-separated values (e.g. SRV-,WEB-)." } + filter_name: { type: string, description: "Asset property to filter on (default assetName)." } + filter_operand: { type: string, description: "startsWith | contains | equals | endsWith (default startsWith)." } + conditions_operand: { type: string, description: "and | or (default or)." } + scope_type: { type: string, description: "account | site | global (default account)." } + site_ids: { type: string, description: "Site ID(s); first is used when scope_type=site." } + status: { type: string, description: "enabled | disabled (default enabled)." } + description: { type: string, description: "Rule description." } + required: [name, account_id, tag_id, filter_values] + outputs_schema: { properties: {} } diff --git a/integrations/sentinelone/scripts/abort_endpoint_scan.py b/integrations/sentinelone/scripts/abort_endpoint_scan.py new file mode 100644 index 0000000..4bb7d37 --- /dev/null +++ b/integrations/sentinelone/scripts/abort_endpoint_scan.py @@ -0,0 +1,36 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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 csv(v): + return [x.strip() for x in str(v or "").split(",") if x.strip()] + + +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", + } + body = {"filter": {"ids": csv(inputs.get("agent_ids"))}, "data": {}} + print(json.dumps(request("POST", base + "/agents/actions/abort-scan", headers, body))) + + +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/sentinelone/scripts/create_tag_rule.py b/integrations/sentinelone/scripts/create_tag_rule.py new file mode 100644 index 0000000..b5606fc --- /dev/null +++ b/integrations/sentinelone/scripts/create_tag_rule.py @@ -0,0 +1,58 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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 csv(v): + return [x.strip() for x in str(v or "").split(",") if x.strip()] + + +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", + } + scope_type = inputs.get("scope_type") or "account" + scope_ids = {"accounts": [inputs.get("account_id")]} + sites = csv(inputs.get("site_ids")) + if scope_type == "site" and sites: + scope_ids["sites"] = [sites[0]] + payload = { + "name": inputs.get("name"), + "description": inputs.get("description") or "", + "status": inputs.get("status") or "enabled", + "conditions": { + "operand": inputs.get("conditions_operand") or "or", + "properties": [ + { + "name": inputs.get("filter_name") or "assetName", + "operand": inputs.get("filter_operand") or "startsWith", + "values": csv(inputs.get("filter_values")), + } + ], + }, + "tags": [{"id": inputs.get("tag_id")}], + "scopes": {"scopeType": scope_type, "scopeIds": scope_ids}, + "excludedAssets": [], + } + print(json.dumps(request("POST", base + "/xdr/assets/tags/rules", headers, payload))) + + +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/sentinelone/scripts/endpoint_fetch_logs.py b/integrations/sentinelone/scripts/endpoint_fetch_logs.py new file mode 100644 index 0000000..8801223 --- /dev/null +++ b/integrations/sentinelone/scripts/endpoint_fetch_logs.py @@ -0,0 +1,49 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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 csv(v): + return [x.strip() for x in str(v or "").split(",") if x.strip()] + + +def flag(v, default): + if v is None: + return default + return str(v).lower() in ("true", "1", "yes") + + +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", + } + body = { + "filter": {"ids": csv(inputs.get("agent_ids"))}, + "data": { + "agentLogs": flag(inputs.get("agents_logs"), True), + "customerFacingLogs": flag(inputs.get("customer_facing_logs"), False), + "platformLogs": flag(inputs.get("platform_logs"), False), + }, + } + print(json.dumps(request("POST", base + "/agents/actions/fetch-logs", headers, body))) + + +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/sentinelone/scripts/fetch_file.py b/integrations/sentinelone/scripts/fetch_file.py new file mode 100644 index 0000000..ed1780c --- /dev/null +++ b/integrations/sentinelone/scripts/fetch_file.py @@ -0,0 +1,33 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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", + } + agent_id = urllib.parse.quote(str(inputs.get("agent_id", "")), safe="") + body = {"data": {"password": inputs.get("password"), "files": [inputs.get("file_path")]}} + print(json.dumps(request("POST", base + "/agents/" + agent_id + "/actions/fetch-files", headers, body))) + + +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/sentinelone/scripts/get_remote_script_task_results.py b/integrations/sentinelone/scripts/get_remote_script_task_results.py new file mode 100644 index 0000000..6c05f9d --- /dev/null +++ b/integrations/sentinelone/scripts/get_remote_script_task_results.py @@ -0,0 +1,38 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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 csv(v): + return [x.strip() for x in str(v or "").split(",") if x.strip()] + + +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", + } + data = {"taskIds": csv(inputs.get("task_ids"))} + if inputs.get("computer_names"): + data["computerNames"] = csv(inputs["computer_names"]) + print(json.dumps(request("POST", base + "/remote-scripts/fetch-files", headers, {"data": data}))) + + +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/sentinelone/scripts/get_remote_script_task_status.py b/integrations/sentinelone/scripts/get_remote_script_task_status.py new file mode 100644 index 0000000..5f48962 --- /dev/null +++ b/integrations/sentinelone/scripts/get_remote_script_task_status.py @@ -0,0 +1,41 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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", + } + qs = { + "parentTaskId": inputs.get("parent_task_id"), + "ids": inputs.get("ids"), + "computerName__contains": inputs.get("computer_name_contains"), + "status": inputs.get("status"), + "siteIds": inputs.get("site_ids"), + "accountIds": inputs.get("account_ids"), + "limit": int(inputs.get("limit") or 50), + } + url = base + "/remote-scripts/status?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + + +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/sentinelone/scripts/get_service_users.py b/integrations/sentinelone/scripts/get_service_users.py new file mode 100644 index 0000000..89f4cc9 --- /dev/null +++ b/integrations/sentinelone/scripts/get_service_users.py @@ -0,0 +1,39 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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", + } + qs = { + "accountIds": inputs.get("account_ids"), + "roleIds": inputs.get("role_ids"), + "ids": inputs.get("ids"), + "siteIds": inputs.get("site_ids"), + "limit": int(inputs.get("limit") or 100), + } + url = base + "/service-users?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + + +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/sentinelone/scripts/list_installed_singularity_marketplace_applications.py b/integrations/sentinelone/scripts/list_installed_singularity_marketplace_applications.py new file mode 100644 index 0000000..0773d40 --- /dev/null +++ b/integrations/sentinelone/scripts/list_installed_singularity_marketplace_applications.py @@ -0,0 +1,41 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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", + } + qs = { + "accountIds": inputs.get("account_ids"), + "applicationCatalogId": inputs.get("application_catalog_id"), + "creator__contains": inputs.get("creator_contains"), + "ids": inputs.get("ids"), + "name__contains": inputs.get("name_contains"), + "siteIds": inputs.get("site_ids"), + "limit": int(inputs.get("limit") or 100), + } + url = base + "/singularity-marketplace/applications?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + + +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/sentinelone/scripts/run_powerquery.py b/integrations/sentinelone/scripts/run_powerquery.py new file mode 100644 index 0000000..6660355 --- /dev/null +++ b/integrations/sentinelone/scripts/run_powerquery.py @@ -0,0 +1,52 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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=60) as resp: + raw = resp.read() + return json.loads(raw) if raw else {} + + +def csv(v): + return [x.strip() for x in str(v or "").split(",") if x.strip()] + + +def flag(v): + return str(v).lower() in ("true", "1", "yes") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + # PowerQuery runs against the Singularity Data Lake, not the management console. + sdl_url = str(inputs.get("singularity_xdr_url", "")).rstrip("/") + if not sdl_url.startswith("https://"): + raise ValueError("singularity_xdr_url must start with https://") + headers = { + "Authorization": "Bearer " + str(inputs.get("singularity_xdr_api_key", "")), + "Accept": "application/json", + "Content-Type": "application/json", + } + payload = {"query": inputs.get("query")} + if inputs.get("start_time"): + payload["startTime"] = inputs["start_time"] + if inputs.get("end_time"): + payload["endTime"] = inputs["end_time"] + if inputs.get("priority"): + payload["priority"] = inputs["priority"] + if inputs.get("team_emails"): + payload["teamEmails"] = csv(inputs["team_emails"]) + if inputs.get("recurring") is not None: + payload["recurring"] = flag(inputs["recurring"]) + print(json.dumps(request("POST", sdl_url + "/api/powerQuery", headers, payload))) + + +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/sentinelone/scripts/threat_analysis.py b/integrations/sentinelone/scripts/threat_analysis.py new file mode 100644 index 0000000..26d3562 --- /dev/null +++ b/integrations/sentinelone/scripts/threat_analysis.py @@ -0,0 +1,32 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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", + } + threat_id = urllib.parse.quote(str(inputs.get("threat_id", "")), safe="") + print(json.dumps(request("GET", base + "/private/threats/" + threat_id + "/analysis", headers))) + + +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/sentinelone/scripts/threat_download_from_cloud.py b/integrations/sentinelone/scripts/threat_download_from_cloud.py new file mode 100644 index 0000000..0578c09 --- /dev/null +++ b/integrations/sentinelone/scripts/threat_download_from_cloud.py @@ -0,0 +1,32 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +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", + } + threat_id = urllib.parse.quote(str(inputs.get("threat_id", "")), safe="") + print(json.dumps(request("GET", base + "/threats/" + threat_id + "/download-from-cloud", headers))) + + +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/sentinelone/scripts/update_uam_alert_status.py b/integrations/sentinelone/scripts/update_uam_alert_status.py new file mode 100644 index 0000000..c6d8aae --- /dev/null +++ b/integrations/sentinelone/scripts/update_uam_alert_status.py @@ -0,0 +1,42 @@ +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} + print(json.dumps(request("POST", base + "/unifiedalerts/graphql", headers, body))) + + +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/sentinelone/scripts/update_uam_alert_verdict.py b/integrations/sentinelone/scripts/update_uam_alert_verdict.py new file mode 100644 index 0000000..482be04 --- /dev/null +++ b/integrations/sentinelone/scripts/update_uam_alert_verdict.py @@ -0,0 +1,42 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + +MUTATION = ( + "mutation AlertTriggerActions($id: String!, $verdict: AnalystVerdict!) {" + " alertTriggerActions(" + " filter: { or: [ { and: [ { fieldId: \"id\", stringEqual: { value: $id } } ] } ] }," + " actions: [ { id: \"S1/alert/analystVerdictUpdate\", payload: { analystVerdict: { value: $verdict } } } ]" + " ) { __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"), "verdict": inputs.get("analyst_verdict")} + body = {"query": MUTATION, "variables": variables} + print(json.dumps(request("POST", base + "/unifiedalerts/graphql", headers, body))) + + +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)