From 9c04290a342431d41af816c0882b3c299aeb7f07 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Mon, 22 Jun 2026 12:48:43 +0200 Subject: [PATCH] feat(sentinelone): full command coverage (70 commands, SentinelOne API v2.1) Expand from 9 to 70 commands covering the official + DEV SentinelOne V2 integrations: agents (actions/info/tags/count), threats & alerts (verdict/status/ notes/mitigate/fetch-file), hash blocklist & exclusions, IOCs, STAR rules, Deep Visibility, remote scripts, endpoint tags, firewall rules and network discovery. All script-based (urllib, INTEGRATION_SECRETS/INPUTS contract); inputs prioritized from the DEV integration. --- integrations/sentinelone/manifest.yaml | 671 +++++++++++++++++- .../scripts/add_hash_to_blocklist.py | 57 ++ .../sentinelone/scripts/broadcast_message.py | 45 ++ .../sentinelone/scripts/count_agents.py | 45 ++ .../scripts/create_endpoint_tag.py | 51 ++ .../scripts/create_firewall_rule.py | 55 ++ .../sentinelone/scripts/create_ioc.py | 50 ++ .../sentinelone/scripts/create_query.py | 43 ++ .../sentinelone/scripts/create_star_rule.py | 58 ++ .../scripts/create_white_list_item.py | 51 ++ .../scripts/delete_endpoint_tag.py | 46 ++ .../sentinelone/scripts/delete_exclusion.py | 38 + .../sentinelone/scripts/delete_group.py | 39 + .../sentinelone/scripts/delete_ioc.py | 38 + .../sentinelone/scripts/delete_star_rule.py | 38 + .../sentinelone/scripts/delete_user.py | 39 + .../sentinelone/scripts/disable_star_rules.py | 38 + .../sentinelone/scripts/enable_agent.py | 41 ++ .../sentinelone/scripts/enable_star_rules.py | 38 + .../sentinelone/scripts/expire_site.py | 39 + .../sentinelone/scripts/fetch_threat_file.py | 38 + .../sentinelone/scripts/get_accounts.py | 42 ++ .../sentinelone/scripts/get_activities.py | 44 ++ .../sentinelone/scripts/get_agent_mac.py | 49 ++ .../sentinelone/scripts/get_alerts.py | 48 ++ .../scripts/get_app_inventory_endpoints.py | 47 ++ .../sentinelone/scripts/get_blocklist.py | 49 ++ .../scripts/get_dv_query_status.py | 41 ++ .../sentinelone/scripts/get_endpoint_tags.py | 42 ++ .../sentinelone/scripts/get_events.py | 43 ++ .../sentinelone/scripts/get_gateways.py | 39 + .../sentinelone/scripts/get_groups.py | 46 ++ .../scripts/get_installed_applications.py | 39 + integrations/sentinelone/scripts/get_iocs.py | 46 ++ .../scripts/get_network_discovery_assets.py | 45 ++ .../scripts/get_network_discovery_table.py | 43 ++ .../sentinelone/scripts/get_processes.py | 42 ++ .../sentinelone/scripts/get_shadow_assets.py | 46 ++ integrations/sentinelone/scripts/get_site.py | 39 + integrations/sentinelone/scripts/get_sites.py | 46 ++ .../sentinelone/scripts/get_star_rules.py | 47 ++ .../sentinelone/scripts/get_threat_notes.py | 38 + .../sentinelone/scripts/get_threat_summary.py | 39 + .../sentinelone/scripts/get_white_list.py | 47 ++ .../list_endpoints_with_application.py | 46 ++ .../sentinelone/scripts/list_users.py | 42 ++ .../sentinelone/scripts/manage_tags.py | 41 ++ .../sentinelone/scripts/mark_as_threat.py | 38 + .../sentinelone/scripts/move_agent.py | 39 + .../sentinelone/scripts/purge_crash_dumps.py | 38 + .../sentinelone/scripts/reactivate_site.py | 40 ++ .../scripts/remove_hash_from_blocklist.py | 44 ++ .../scripts/remove_item_from_whitelist.py | 51 ++ .../sentinelone/scripts/resolve_threat.py | 38 + .../sentinelone/scripts/run_remote_script.py | 51 ++ .../sentinelone/scripts/shutdown_agent.py | 45 ++ .../sentinelone/scripts/uninstall_agent.py | 45 ++ .../scripts/update_alerts_status.py | 38 + .../scripts/update_alerts_verdict.py | 38 + .../sentinelone/scripts/update_star_rule.py | 61 ++ .../scripts/update_threats_status.py | 38 + .../scripts/update_threats_verdict.py | 38 + 62 files changed, 3335 insertions(+), 1 deletion(-) create mode 100644 integrations/sentinelone/scripts/add_hash_to_blocklist.py create mode 100644 integrations/sentinelone/scripts/broadcast_message.py create mode 100644 integrations/sentinelone/scripts/count_agents.py create mode 100644 integrations/sentinelone/scripts/create_endpoint_tag.py create mode 100644 integrations/sentinelone/scripts/create_firewall_rule.py create mode 100644 integrations/sentinelone/scripts/create_ioc.py create mode 100644 integrations/sentinelone/scripts/create_query.py create mode 100644 integrations/sentinelone/scripts/create_star_rule.py create mode 100644 integrations/sentinelone/scripts/create_white_list_item.py create mode 100644 integrations/sentinelone/scripts/delete_endpoint_tag.py create mode 100644 integrations/sentinelone/scripts/delete_exclusion.py create mode 100644 integrations/sentinelone/scripts/delete_group.py create mode 100644 integrations/sentinelone/scripts/delete_ioc.py create mode 100644 integrations/sentinelone/scripts/delete_star_rule.py create mode 100644 integrations/sentinelone/scripts/delete_user.py create mode 100644 integrations/sentinelone/scripts/disable_star_rules.py create mode 100644 integrations/sentinelone/scripts/enable_agent.py create mode 100644 integrations/sentinelone/scripts/enable_star_rules.py create mode 100644 integrations/sentinelone/scripts/expire_site.py create mode 100644 integrations/sentinelone/scripts/fetch_threat_file.py create mode 100644 integrations/sentinelone/scripts/get_accounts.py create mode 100644 integrations/sentinelone/scripts/get_activities.py create mode 100644 integrations/sentinelone/scripts/get_agent_mac.py create mode 100644 integrations/sentinelone/scripts/get_alerts.py create mode 100644 integrations/sentinelone/scripts/get_app_inventory_endpoints.py create mode 100644 integrations/sentinelone/scripts/get_blocklist.py create mode 100644 integrations/sentinelone/scripts/get_dv_query_status.py create mode 100644 integrations/sentinelone/scripts/get_endpoint_tags.py create mode 100644 integrations/sentinelone/scripts/get_events.py create mode 100644 integrations/sentinelone/scripts/get_gateways.py create mode 100644 integrations/sentinelone/scripts/get_groups.py create mode 100644 integrations/sentinelone/scripts/get_installed_applications.py create mode 100644 integrations/sentinelone/scripts/get_iocs.py create mode 100644 integrations/sentinelone/scripts/get_network_discovery_assets.py create mode 100644 integrations/sentinelone/scripts/get_network_discovery_table.py create mode 100644 integrations/sentinelone/scripts/get_processes.py create mode 100644 integrations/sentinelone/scripts/get_shadow_assets.py create mode 100644 integrations/sentinelone/scripts/get_site.py create mode 100644 integrations/sentinelone/scripts/get_sites.py create mode 100644 integrations/sentinelone/scripts/get_star_rules.py create mode 100644 integrations/sentinelone/scripts/get_threat_notes.py create mode 100644 integrations/sentinelone/scripts/get_threat_summary.py create mode 100644 integrations/sentinelone/scripts/get_white_list.py create mode 100644 integrations/sentinelone/scripts/list_endpoints_with_application.py create mode 100644 integrations/sentinelone/scripts/list_users.py create mode 100644 integrations/sentinelone/scripts/manage_tags.py create mode 100644 integrations/sentinelone/scripts/mark_as_threat.py create mode 100644 integrations/sentinelone/scripts/move_agent.py create mode 100644 integrations/sentinelone/scripts/purge_crash_dumps.py create mode 100644 integrations/sentinelone/scripts/reactivate_site.py create mode 100644 integrations/sentinelone/scripts/remove_hash_from_blocklist.py create mode 100644 integrations/sentinelone/scripts/remove_item_from_whitelist.py create mode 100644 integrations/sentinelone/scripts/resolve_threat.py create mode 100644 integrations/sentinelone/scripts/run_remote_script.py create mode 100644 integrations/sentinelone/scripts/shutdown_agent.py create mode 100644 integrations/sentinelone/scripts/uninstall_agent.py create mode 100644 integrations/sentinelone/scripts/update_alerts_status.py create mode 100644 integrations/sentinelone/scripts/update_alerts_verdict.py create mode 100644 integrations/sentinelone/scripts/update_star_rule.py create mode 100644 integrations/sentinelone/scripts/update_threats_status.py create mode 100644 integrations/sentinelone/scripts/update_threats_verdict.py diff --git a/integrations/sentinelone/manifest.yaml b/integrations/sentinelone/manifest.yaml index f6f7f57..f45c9ff 100644 --- a/integrations/sentinelone/manifest.yaml +++ b/integrations/sentinelone/manifest.yaml @@ -2,7 +2,7 @@ id: sentinelone name: SentinelOne version: 1.0.0 description: "SentinelOne Singularity (API v2.1) — endpoint detection & response: triage threats, enrich, isolate/reconnect hosts, mitigate, scan." -changelog: "1.0.0 — Initial release: threats, agents, hash verdict, isolate/reconnect, mitigate, scan, threat notes." +changelog: "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 (parity with the XSOAR SentinelOne V2 integration)." category: endpoint # Per-instance configuration. The scripts build the API base as /web/api/v2.1. @@ -121,3 +121,672 @@ commands: note: { type: string, description: "Note text" } required: [threat_ids, note] outputs_schema: { properties: {} } + + # ── Agents: actions & info ──────────────────────────────────────────────── + - id: shutdown_agent + name: Shutdown agent + description: "Send a shutdown command to agents matching the filter." + inputs_schema: + properties: + agent_id: { type: string, description: "Comma-separated agent IDs" } + group_id: { type: string, description: "Comma-separated group IDs" } + query: { type: string, description: "Free-text agent match" } + required: [] + outputs_schema: { properties: {} } + - id: uninstall_agent + name: Uninstall agent + description: "Send an uninstall command to agents matching the filter." + inputs_schema: + properties: + agent_id: { type: string, description: "Comma-separated agent IDs" } + group_id: { type: string, description: "Comma-separated group IDs" } + query: { type: string, description: "Free-text agent match" } + required: [] + outputs_schema: { properties: {} } + - id: broadcast_message + name: Broadcast message + description: "Broadcast a message to agents matching the filter." + inputs_schema: + properties: + message: { type: string, description: "Message to broadcast to agents" } + agent_id: { type: string, description: "Comma-separated agent IDs" } + group_id: { type: string, description: "Comma-separated group IDs" } + active_agent: { type: boolean, description: "Only target active agents" } + required: [message] + outputs_schema: { properties: {} } + - id: enable_agent + name: Enable agent + description: "Enable agents matching the given IDs, optionally rebooting them." + inputs_schema: + properties: + agent_ids: { type: string, description: "Comma-separated agent IDs" } + shouldReboot: { type: boolean, description: "Reboot agents after enabling" } + required: [agent_ids] + outputs_schema: { properties: {} } + - id: move_agent + name: Move agent + description: "Move agents into the specified group." + inputs_schema: + properties: + group_id: { type: string, description: "Target group ID" } + agents_ids: { type: string, description: "Comma-separated agent IDs to move" } + required: [group_id, agents_ids] + outputs_schema: { properties: {} } + - id: count_agents + name: Count agents + description: "Count agents matching the given filters." + inputs_schema: + properties: + computer_name: { type: string, description: "Filter by computer name" } + os_type: { type: string, description: "Filter by OS type" } + scan_status: { type: string, description: "Filter by scan status" } + siteIds: { type: string, description: "Comma-separated site IDs" } + groupIds: { type: string, description: "Comma-separated group IDs" } + required: [] + outputs_schema: { properties: {} } + - id: get_agent_mac + name: Get agent MAC addresses + description: "Retrieve network interfaces (IP and MAC) for a single agent." + inputs_schema: + properties: + agent_id: { type: string, description: "Agent ID" } + required: [agent_id] + outputs_schema: { properties: {} } + - id: get_installed_applications + name: Get installed applications + description: "Retrieve the applications installed on the given agents." + inputs_schema: + properties: + agent_ids: { type: string, description: "Comma-separated agent IDs" } + required: [agent_ids] + outputs_schema: { properties: {} } + - id: manage_tags + name: Manage tags + description: "Add, override or remove a tag on the given agents." + inputs_schema: + properties: + tagId: { type: string, description: "Tag ID to apply" } + agent_ids: { type: string, description: "Comma-separated agent IDs" } + operation: { type: string, description: "Tag operation: add, override or remove" } + required: [tagId, agent_ids, operation] + outputs_schema: { properties: {} } + + # ── Threats & Alerts ────────────────────────────────────────────────────── + - id: resolve_threat + name: Resolve threat + description: "Mark one or more threats as resolved." + inputs_schema: + properties: + threat_ids: { type: string, description: "Comma-separated threat IDs to resolve." } + required: [threat_ids] + outputs_schema: { properties: {} } + - id: mark_as_threat + name: Mark as threat + description: "Mark detections as a true threat, scoped to a site or the whole tenant." + inputs_schema: + properties: + threat_ids: { type: string, description: "Comma-separated threat IDs to mark." } + target_scope: { type: string, description: "Scope of the action: site or tenant." } + required: [threat_ids, target_scope] + outputs_schema: { properties: {} } + - id: update_threats_verdict + name: Update threats verdict + description: "Set the analyst verdict on one or more threats." + inputs_schema: + properties: + verdict: { type: string, description: "undefined, true_positive, false_positive, or suspicious." } + threat_ids: { type: string, description: "Comma-separated threat IDs to update." } + required: [verdict, threat_ids] + outputs_schema: { properties: {} } + - id: update_threats_status + name: Update threats status + description: "Set the incident status on one or more threats." + inputs_schema: + properties: + status: { type: string, description: "in_progress, resolved, or unresolved." } + threat_ids: { type: string, description: "Comma-separated threat IDs to update." } + required: [status, threat_ids] + outputs_schema: { properties: {} } + - id: get_threat_notes + name: Get threat notes + description: "Retrieve the notes attached to a single threat." + inputs_schema: + properties: + threat_id: { type: string, description: "ID of the threat whose notes to retrieve." } + required: [threat_id] + outputs_schema: { properties: {} } + - id: get_threat_summary + name: Get threat summary + description: "Retrieve a dashboard threat summary, optionally filtered by site and group." + inputs_schema: + properties: + site_ids: { type: string, description: "Comma-separated site IDs." } + group_ids: { type: string, description: "Comma-separated group IDs." } + required: [] + outputs_schema: { properties: {} } + - id: fetch_threat_file + name: Fetch threat file + description: "Request the threat file for download, protected by a ZIP password." + inputs_schema: + properties: + threat_id: { type: string, description: "ID of the threat whose file to fetch." } + password: { type: string, description: "Password used to encrypt the fetched file ZIP." } + required: [threat_id, password] + outputs_schema: { properties: {} } + - id: get_alerts + name: Get alerts + description: "Retrieve cloud-detection alerts filtered by creation window and optional criteria." + inputs_schema: + properties: + created_from: { type: string, description: "Return alerts created at or after this timestamp." } + created_until: { type: string, description: "Return alerts created at or before this timestamp." } + ruleName: { type: string, description: "Filter by rule name substring." } + incidentStatus: { type: string, description: "Filter by incident status." } + analystVerdict: { type: string, description: "Filter by analyst verdict." } + alert_ids: { type: string, description: "Comma-separated alert IDs." } + site_ids: { type: string, description: "Comma-separated site IDs." } + limit: { type: number, description: "Max results (default 100)." } + required: [created_from] + outputs_schema: { properties: {} } + - id: update_alerts_verdict + name: Update alerts verdict + description: "Set the analyst verdict on one or more cloud-detection alerts." + inputs_schema: + properties: + verdict: { type: string, description: "undefined, true_positive, false_positive, or suspicious." } + alert_ids: { type: string, description: "Comma-separated alert IDs to update." } + required: [verdict, alert_ids] + outputs_schema: { properties: {} } + - id: update_alerts_status + name: Update alerts status + description: "Set the incident status on one or more cloud-detection alerts." + inputs_schema: + properties: + status: { type: string, description: "in_progress, resolved, or unresolved." } + alert_ids: { type: string, description: "Comma-separated alert IDs to update." } + required: [status, alert_ids] + outputs_schema: { properties: {} } + + # ── Hash, blocklist, exclusions, IOCs ───────────────────────────────────── + - id: get_blocklist + name: Get blocklist + description: "Retrieve hash blocklist (restriction) entries, optionally filtered by hash, scope, and pagination." + inputs_schema: + properties: + global: { type: boolean, description: "Query the global (tenant) scope. Defaults to true." } + hash: { type: string, description: "Filter by hash value substring." } + offset: { type: number, description: "Records to skip for pagination." } + limit: { type: number, description: "Max records to return (default 100)." } + site_ids: { type: string, description: "Comma-separated site IDs." } + group_ids: { type: string, description: "Comma-separated group IDs." } + account_ids: { type: string, description: "Comma-separated account IDs." } + required: [] + outputs_schema: { properties: {} } + - id: add_hash_to_blocklist + name: Add hash to blocklist + description: "Add a SHA1 or SHA256 hash to the blocklist for the given scope." + inputs_schema: + properties: + os_type: { type: string, description: "Target OS type: windows, linux, or macos." } + sha1: { type: string, description: "SHA1 hash (used if sha256 not provided)." } + sha256: { type: string, description: "SHA256 hash (takes precedence over sha1)." } + description: { type: string, description: "Optional description." } + source: { type: string, description: "Source label (defaults to Riposte)." } + account_ids: { type: string, description: "Comma-separated account IDs." } + site_ids: { type: string, description: "Comma-separated site IDs." } + group_ids: { type: string, description: "Comma-separated group IDs." } + required: [os_type] + outputs_schema: { properties: {} } + - id: remove_hash_from_blocklist + name: Remove hash from blocklist + description: "Find blocklist entries matching a SHA1 hash and delete them." + inputs_schema: + properties: + sha1: { type: string, description: "SHA1 hash to remove." } + os_type: { type: string, description: "Optional OS type filter." } + required: [sha1] + outputs_schema: { properties: {} } + - id: get_white_list + name: Get exclusions (white list) + description: "Retrieve exclusion items, optionally filtered by IDs, type, OS, and scope." + inputs_schema: + properties: + item_ids: { type: string, description: "Comma-separated exclusion item IDs." } + os_types: { type: string, description: "Filter by OS types." } + exclusion_type: { type: string, description: "Filter by exclusion type." } + limit: { type: number, description: "Max records (default 10)." } + include_parent: { type: boolean, description: "Include parent-scope exclusions." } + include_children: { type: boolean, description: "Include child-scope exclusions." } + site_ids: { type: string, description: "Comma-separated site IDs." } + required: [] + outputs_schema: { properties: {} } + - id: create_white_list_item + name: Create exclusion item + description: "Create an exclusion (whitelist) item of the given type and value." + inputs_schema: + properties: + exclusion_type: { type: string, description: "Exclusion type (path, white_hash, certificate, browser, file_type)." } + exclusion_value: { type: string, description: "The value to exclude." } + os_type: { type: string, description: "Target OS type." } + description: { type: string, description: "Optional description." } + exclusion_mode: { type: string, description: "Optional exclusion mode." } + path_exclusion_type: { type: string, description: "Optional path exclusion type." } + group_ids: { type: string, description: "Comma-separated group IDs." } + site_ids: { type: string, description: "Comma-separated site IDs." } + required: [exclusion_type, exclusion_value, os_type] + outputs_schema: { properties: {} } + - id: remove_item_from_whitelist + name: Remove exclusion item + description: "Find exclusion items matching a value and delete them." + inputs_schema: + properties: + item: { type: string, description: "The exclusion value to find and remove." } + os_type: { type: string, description: "Optional OS type filter." } + exclusion_type: { type: string, description: "Optional exclusion type filter." } + required: [item] + outputs_schema: { properties: {} } + - id: delete_exclusion + name: Delete exclusion + description: "Delete exclusion items by their IDs and type." + inputs_schema: + properties: + ids: { type: string, description: "Comma-separated exclusion item IDs to delete." } + type: { type: string, description: "Exclusion type of the items being deleted." } + required: [ids, type] + outputs_schema: { properties: {} } + - id: create_ioc + name: Create IOC + description: "Create a threat-intelligence IOC for the given account scope." + inputs_schema: + properties: + name: { type: string, description: "Name of the IOC." } + source: { type: string, description: "Source of the IOC." } + type: { type: string, description: "IOC type: DNS, IPV4, IPV6, MD5, SHA1, SHA256, or URL." } + method: { type: string, description: "Comparison method (e.g. EQUALS)." } + validUntil: { type: string, description: "Expiration timestamp (ISO 8601)." } + value: { type: string, description: "The IOC value." } + account_ids: { type: string, description: "Comma-separated account IDs." } + externalId: { type: string, description: "Optional external identifier." } + description: { type: string, description: "Optional description." } + required: [name, source, type, method, validUntil, value, account_ids] + outputs_schema: { properties: {} } + - id: delete_ioc + name: Delete IOC + description: "Delete threat-intelligence IOCs by UUID within the given account scope." + inputs_schema: + properties: + account_ids: { type: string, description: "Comma-separated account IDs." } + uuids: { type: string, description: "Comma-separated IOC UUIDs to delete." } + required: [account_ids, uuids] + outputs_schema: { properties: {} } + - id: get_iocs + name: Get IOCs + description: "Retrieve threat-intelligence IOCs, optionally filtered by type, value, source, and name." + inputs_schema: + properties: + account_ids: { type: string, description: "Comma-separated account IDs." } + limit: { type: number, description: "Max records (default 1000)." } + type: { type: string, description: "Filter by IOC type." } + value: { type: string, description: "Filter by IOC value." } + source: { type: string, description: "Filter by IOC source." } + name_contains: { type: string, description: "Filter by IOC name substring." } + required: [account_ids] + outputs_schema: { properties: {} } + + # ── Sites, groups, accounts, users, Deep Visibility ─────────────────────── + - id: get_activities + name: Get activities + description: "Retrieve activities, optionally filtered by date, agents, or threats." + inputs_schema: + properties: + created_after: { type: string, description: "Activities created after this timestamp." } + limit: { type: number, description: "Max results (default 50)." } + agent_ids: { type: string, description: "Comma-separated agent IDs." } + threats_ids: { type: string, description: "Comma-separated threat IDs." } + required: [] + outputs_schema: { properties: {} } + - id: get_groups + name: Get groups + description: "List groups with optional filters." + inputs_schema: + properties: + group_type: { type: string, description: "Group type (static, dynamic)." } + group_ids: { type: string, description: "Comma-separated group IDs." } + is_default: { type: boolean, description: "Filter for default groups only." } + name: { type: string, description: "Exact group name." } + query: { type: string, description: "Free-text search." } + limit: { type: number, description: "Max results (default 50)." } + required: [] + outputs_schema: { properties: {} } + - id: delete_group + name: Delete group + description: "Delete a group by its ID." + inputs_schema: + properties: + group_id: { type: string, description: "ID of the group to delete." } + required: [group_id] + outputs_schema: { properties: {} } + - id: get_sites + name: Get sites + description: "List sites with optional filters." + inputs_schema: + properties: + query: { type: string, description: "Free-text search." } + state: { type: string, description: "Site state (active, expired)." } + site_type: { type: string, description: "Site type." } + site_name: { type: string, description: "Exact site name." } + site_ids: { type: string, description: "Comma-separated site IDs." } + limit: { type: number, description: "Max results (default 50)." } + required: [] + outputs_schema: { properties: {} } + - id: get_site + name: Get site + description: "Retrieve a single site by its ID." + inputs_schema: + properties: + site_id: { type: string, description: "ID of the site." } + required: [site_id] + outputs_schema: { properties: {} } + - id: reactivate_site + name: Reactivate site + description: "Reactivate an expired site." + inputs_schema: + properties: + site_id: { type: string, description: "ID of the site to reactivate." } + unlimited: { type: boolean, description: "Set the site to unlimited duration." } + expiration: { type: string, description: "New expiration date (ISO 8601)." } + required: [site_id] + outputs_schema: { properties: {} } + - id: expire_site + name: Expire site + description: "Expire a site immediately." + inputs_schema: + properties: + site_id: { type: string, description: "ID of the site to expire." } + required: [site_id] + outputs_schema: { properties: {} } + - id: get_accounts + name: Get accounts + description: "List all accounts, or retrieve a single account when an ID is provided." + inputs_schema: + properties: + account_id: { type: string, description: "Optional account ID." } + required: [] + outputs_schema: { properties: {} } + - id: list_users + name: List users + description: "List users, optionally filtered by account IDs." + inputs_schema: + properties: + account_ids: { type: string, description: "Comma-separated account IDs." } + limit: { type: number, description: "Max results (default 100)." } + required: [] + outputs_schema: { properties: {} } + - id: delete_user + name: Delete user + description: "Delete a user by their ID." + inputs_schema: + properties: + user_id: { type: string, description: "ID of the user to delete." } + required: [user_id] + outputs_schema: { properties: {} } + - id: create_query + name: Create DV query + description: "Initialize a Deep Visibility query over a given time range." + inputs_schema: + properties: + query: { type: string, description: "Deep Visibility query string." } + from_date: { type: string, description: "Start of the time range (ISO 8601)." } + to_date: { type: string, description: "End of the time range (ISO 8601)." } + required: [query, from_date, to_date] + outputs_schema: { properties: {} } + - id: get_dv_query_status + name: Get DV query status + description: "Get the status of a Deep Visibility query by ID." + inputs_schema: + properties: + query_id: { type: string, description: "ID of the Deep Visibility query." } + required: [query_id] + outputs_schema: { properties: {} } + - id: get_events + name: Get DV events + description: "Retrieve events for a completed Deep Visibility query." + inputs_schema: + properties: + query_id: { type: string, description: "ID of the Deep Visibility query." } + limit: { type: number, description: "Max events (default 50)." } + cursor: { type: string, description: "Pagination cursor." } + required: [query_id] + outputs_schema: { properties: {} } + - id: get_processes + name: Get DV processes + description: "Retrieve process events for a completed Deep Visibility query." + inputs_schema: + properties: + query_id: { type: string, description: "ID of the Deep Visibility query." } + limit: { type: number, description: "Max process events (default 50)." } + required: [query_id] + outputs_schema: { properties: {} } + + # ── STAR rules & advanced (remote scripts, tags, firewall, discovery) ────── + - id: create_star_rule + name: Create STAR rule + description: "Create a STAR custom detection rule in Draft status." + inputs_schema: + properties: + name: { type: string, description: "Rule name." } + query: { type: string, description: "S1QL detection query." } + rule_severity: { type: string, description: "Low | Medium | High | Critical." } + expiration_mode: { type: string, description: "Permanent | Temporary." } + query_type: { type: string, description: "Query type (defaults to events)." } + network_quarantine: { type: boolean, description: "Network-quarantine matching endpoints." } + treatAsThreat: { type: string, description: "Malicious | Suspicious | UNDEFINED." } + description: { type: string, description: "Rule description." } + expiration_date: { type: string, description: "Expiration date (when Temporary)." } + site_ids: { type: string, description: "Comma-separated site IDs." } + group_ids: { type: string, description: "Comma-separated group IDs." } + account_ids: { type: string, description: "Comma-separated account IDs." } + required: [name, query, rule_severity, expiration_mode, network_quarantine, treatAsThreat] + outputs_schema: { properties: {} } + - id: get_star_rules + name: Get STAR rules + description: "List STAR custom detection rules." + inputs_schema: + properties: + status: { type: string, description: "Filter by rule status." } + queryType: { type: string, description: "Filter by query type." } + name_contains: { type: string, description: "Filter by name substring." } + ruleIds: { type: string, description: "Comma-separated rule IDs." } + accountIds: { type: string, description: "Comma-separated account IDs." } + siteIds: { type: string, description: "Comma-separated site IDs." } + limit: { type: number, description: "Max results (default 1000)." } + required: [] + outputs_schema: { properties: {} } + - id: update_star_rule + name: Update STAR rule + description: "Update an existing STAR custom detection rule by ID." + inputs_schema: + properties: + rule_id: { type: string, description: "ID of the rule to update." } + name: { type: string, description: "Rule name." } + query: { type: string, description: "S1QL detection query." } + rule_severity: { type: string, description: "Low | Medium | High | Critical." } + expiration_mode: { type: string, description: "Permanent | Temporary." } + query_type: { type: string, description: "Query type (defaults to events)." } + network_quarantine: { type: boolean, description: "Network-quarantine matching endpoints." } + treatAsThreat: { type: string, description: "Malicious | Suspicious | UNDEFINED." } + description: { type: string, description: "Rule description." } + expiration_date: { type: string, description: "Expiration date (when Temporary)." } + query_lang: { type: string, description: "Query language version." } + site_ids: { type: string, description: "Comma-separated site IDs." } + group_ids: { type: string, description: "Comma-separated group IDs." } + account_ids: { type: string, description: "Comma-separated account IDs." } + required: [rule_id, name, query, rule_severity, expiration_mode, network_quarantine, treatAsThreat] + outputs_schema: { properties: {} } + - id: enable_star_rules + name: Enable STAR rules + description: "Enable one or more STAR custom detection rules by ID." + inputs_schema: + properties: + rule_ids: { type: string, description: "Comma-separated rule IDs to enable." } + required: [rule_ids] + outputs_schema: { properties: {} } + - id: disable_star_rules + name: Disable STAR rules + description: "Disable one or more STAR custom detection rules by ID." + inputs_schema: + properties: + rule_ids: { type: string, description: "Comma-separated rule IDs to disable." } + required: [rule_ids] + outputs_schema: { properties: {} } + - id: delete_star_rule + name: Delete STAR rule + description: "Delete one or more STAR custom detection rules by ID." + inputs_schema: + properties: + rule_ids: { type: string, description: "Comma-separated rule IDs to delete." } + required: [rule_ids] + outputs_schema: { properties: {} } + - id: run_remote_script + name: Run remote script + description: "Execute a remote script (RSO) on the targeted agents." + inputs_schema: + properties: + account_ids: { type: string, description: "Comma-separated account IDs." } + script_id: { type: string, description: "ID of the remote script to run." } + output_destination: { type: string, description: "Where script output is sent." } + task_description: { type: string, description: "Description of the task." } + agent_ids: { type: string, description: "Comma-separated agent IDs to target." } + site_ids: { type: string, description: "Comma-separated site IDs." } + input_params: { type: string, description: "Input parameters passed to the script." } + password: { type: string, description: "Password for protected scripts." } + requires_approval: { type: boolean, description: "Whether execution requires approval." } + required: [account_ids, script_id, output_destination, task_description] + outputs_schema: { properties: {} } + - id: create_endpoint_tag + name: Create endpoint tag + description: "Create an endpoint tag in the tag manager." + inputs_schema: + properties: + type: { type: string, description: "Tag type." } + key: { type: string, description: "Tag key." } + value: { type: string, description: "Tag value." } + description: { type: string, description: "Tag description." } + groupIds: { type: string, description: "Comma-separated group IDs." } + siteIds: { type: string, description: "Comma-separated site IDs." } + accountIds: { type: string, description: "Comma-separated account IDs." } + tenant: { type: boolean, description: "Apply at tenant scope." } + required: [type, key] + outputs_schema: { properties: {} } + - id: delete_endpoint_tag + name: Delete endpoint tag + description: "Delete endpoint tags matching the given scope filter." + inputs_schema: + properties: + siteIds: { type: string, description: "Comma-separated site IDs." } + groupIds: { type: string, description: "Comma-separated group IDs." } + accountIds: { type: string, description: "Comma-separated account IDs." } + query: { type: string, description: "Free-text query filter." } + required: [] + outputs_schema: { properties: {} } + - id: get_endpoint_tags + name: Get endpoint tags + description: "List endpoint tags, optionally filtered by key or value substring." + inputs_schema: + properties: + key__contains: { type: string, description: "Filter by key substring." } + value__contains: { type: string, description: "Filter by value substring." } + required: [] + outputs_schema: { properties: {} } + - id: create_firewall_rule + name: Create firewall rule + description: "Create a firewall control rule scoped to an account or site." + inputs_schema: + properties: + action: { type: string, description: "Allow | Block." } + name: { type: string, description: "Rule name." } + status: { type: string, description: "Enabled | Disabled." } + description: { type: string, description: "Rule description." } + direction: { type: string, description: "incoming | outgoing | any." } + filter_type: { type: string, description: "Scope filter type: accountIds | siteIds." } + filter_id: { type: string, description: "ID for the chosen scope filter type." } + remote_host: { type: string, description: "Comma-separated remote host addresses." } + required: [action, name, status, description, direction, filter_type, filter_id] + outputs_schema: { properties: {} } + - id: get_network_discovery_table + name: Get network discovery table + description: "Retrieve the Ranger network discovery table view." + inputs_schema: + properties: + hostnames__contains: { type: string, description: "Filter by hostname substring." } + externalIp: { type: string, description: "Filter by exact external IP." } + externalIp__contains: { type: string, description: "Filter by external IP substring." } + required: [] + outputs_schema: { properties: {} } + - id: get_network_discovery_assets + name: Get network discovery assets + description: "Retrieve XDR network discovery surface assets." + inputs_schema: + properties: + limit: { type: number, description: "Max results (default 50)." } + account_id: { type: string, description: "Account ID filter." } + site_id: { type: string, description: "Site ID filter." } + hostname: { type: string, description: "Filter by name substring." } + epp_status: { type: string, description: "Filter by EPP unsupported/unknown status." } + required: [] + outputs_schema: { properties: {} } + - id: get_shadow_assets + name: Get shadow assets + description: "Retrieve XDR shadow assets with optional coverage filters." + inputs_schema: + properties: + limit: { type: number, description: "Max results (default 50)." } + account_id: { type: string, description: "Account ID filter." } + site_id: { type: string, description: "Site ID filter." } + name: { type: string, description: "Filter by name substring." } + active_coverage: { type: string, description: "Filter by active coverage." } + missing_coverage: { type: string, description: "Filter by missing coverage." } + required: [] + outputs_schema: { properties: {} } + - id: get_gateways + name: Get gateways + description: "List Ranger gateways." + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + - id: purge_crash_dumps + name: Purge crash dumps + description: "Purge agent crash dumps for the specified agents." + inputs_schema: + properties: + agent_ids: { type: string, description: "Comma-separated agent IDs." } + required: [agent_ids] + outputs_schema: { properties: {} } + - id: list_endpoints_with_application + name: List endpoints with application + description: "List the application inventory across endpoints." + inputs_schema: + properties: + groupIds: { type: string, description: "Comma-separated group IDs." } + name__contains: { type: string, description: "Filter by application name substring." } + osTypes: { type: string, description: "Filter by OS types." } + vendor__contains: { type: string, description: "Filter by vendor substring." } + siteIds: { type: string, description: "Comma-separated site IDs." } + limit: { type: number, description: "Max results (default 10)." } + required: [] + outputs_schema: { properties: {} } + - id: get_app_inventory_endpoints + name: Get app inventory endpoints + description: "List endpoints that have a given application installed." + inputs_schema: + properties: + applicationName: { type: string, description: "Application name filter." } + applicationVendor: { type: string, description: "Application vendor filter." } + siteIds: { type: string, description: "Comma-separated site IDs." } + groupIds: { type: string, description: "Comma-separated group IDs." } + accountIds: { type: string, description: "Comma-separated account IDs." } + cursor: { type: string, description: "Pagination cursor." } + limit: { type: number, description: "Max results (default 50)." } + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/sentinelone/scripts/add_hash_to_blocklist.py b/integrations/sentinelone/scripts/add_hash_to_blocklist.py new file mode 100644 index 0000000..dad4f4b --- /dev/null +++ b/integrations/sentinelone/scripts/add_hash_to_blocklist.py @@ -0,0 +1,57 @@ +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", + } + # === REQUEST === + data = { + "source": inputs.get("source") or "Riposte", + "osType": inputs.get("os_type"), + "type": "black_hash", + "description": inputs.get("description") or "", + } + if inputs.get("sha256"): + data["sha256Value"] = inputs["sha256"] + elif inputs.get("sha1"): + data["value"] = inputs["sha1"] + filt = {} + if inputs.get("site_ids"): + filt["siteIds"] = csv(inputs["site_ids"]) + if inputs.get("group_ids"): + filt["groupIds"] = csv(inputs["group_ids"]) + if inputs.get("account_ids"): + filt["accountIds"] = csv(inputs["account_ids"]) + if not filt: + filt = {"tenant": True} + body = {"data": data, "filter": filt} + print(json.dumps(request("POST", base + "/restrictions", headers, body))) + # === END === + + +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/broadcast_message.py b/integrations/sentinelone/scripts/broadcast_message.py new file mode 100644 index 0000000..e9d8787 --- /dev/null +++ b/integrations/sentinelone/scripts/broadcast_message.py @@ -0,0 +1,45 @@ +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", + } + # === REQUEST === + flt = {} + if inputs.get("agent_id"): + flt["ids"] = csv(inputs.get("agent_id")) + if inputs.get("group_id"): + flt["groupIds"] = csv(inputs.get("group_id")) + if inputs.get("active_agent") in (True, "true", "True", 1): + flt["isActive"] = True + body = {"data": {"message": str(inputs.get("message", ""))}, "filter": flt} + print(json.dumps(request("POST", base + "/agents/actions/broadcast", headers, body))) + # === END === + + +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/count_agents.py b/integrations/sentinelone/scripts/count_agents.py new file mode 100644 index 0000000..9b74ca9 --- /dev/null +++ b/integrations/sentinelone/scripts/count_agents.py @@ -0,0 +1,45 @@ +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", + } + # === REQUEST === + qs = { + "computerName": inputs.get("computer_name"), + "osTypes": inputs.get("os_type"), + "scan_status": inputs.get("scan_status"), + "siteIds": inputs.get("siteIds"), + "groupIds": inputs.get("groupIds"), + } + url = base + "/agents/count?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_endpoint_tag.py b/integrations/sentinelone/scripts/create_endpoint_tag.py new file mode 100644 index 0000000..4d712ca --- /dev/null +++ b/integrations/sentinelone/scripts/create_endpoint_tag.py @@ -0,0 +1,51 @@ +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", + } + # === REQUEST === + body = { + "data": {"type": inputs.get("type"), "key": inputs.get("key")}, + "filter": {"tenant": inputs.get("tenant") in (True, "true", "True", 1)}, + } + if inputs.get("value"): + body["data"]["value"] = inputs["value"] + if inputs.get("description"): + body["data"]["description"] = inputs["description"] + if inputs.get("groupIds"): + body["filter"]["groupIds"] = csv(inputs["groupIds"]) + if inputs.get("siteIds"): + body["filter"]["siteIds"] = csv(inputs["siteIds"]) + if inputs.get("accountIds"): + body["filter"]["accountIds"] = csv(inputs["accountIds"]) + print(json.dumps(request("POST", base + "/tag-manager", headers, body))) + # === END === + + +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_firewall_rule.py b/integrations/sentinelone/scripts/create_firewall_rule.py new file mode 100644 index 0000000..33f6fa0 --- /dev/null +++ b/integrations/sentinelone/scripts/create_firewall_rule.py @@ -0,0 +1,55 @@ +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", + } + # === REQUEST === + rh = [{"type": "addresses", "values": [h]} for h in csv(inputs.get("remote_host"))] + filt = {"tenant": True} + ft = inputs.get("filter_type") + fid = inputs.get("filter_id") + if ft and fid: + filt[ft] = [fid] + body = { + "data": { + "name": inputs.get("name"), + "description": inputs.get("description"), + "action": inputs.get("action"), + "status": inputs.get("status"), + "osTypes": ["linux", "macos", "windows"], + "direction": inputs.get("direction") or "any", + "remoteHosts": rh, + }, + "filter": filt, + } + print(json.dumps(request("POST", base + "/firewall-control", headers, body))) + # === END === + + +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_ioc.py b/integrations/sentinelone/scripts/create_ioc.py new file mode 100644 index 0000000..7436318 --- /dev/null +++ b/integrations/sentinelone/scripts/create_ioc.py @@ -0,0 +1,50 @@ +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", + } + # === REQUEST === + body = { + "filter": {"accountIds": csv(inputs.get("account_ids"))}, + "data": [{ + "source": inputs.get("source"), + "type": inputs.get("type"), + "method": inputs.get("method") or "EQUALS", + "validUntil": inputs.get("validUntil"), + "name": inputs.get("name"), + "value": inputs.get("value"), + "externalId": inputs.get("externalId"), + "description": inputs.get("description"), + }], + } + print(json.dumps(request("POST", base + "/threat-intelligence/iocs", headers, body))) + # === END === + + +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_query.py b/integrations/sentinelone/scripts/create_query.py new file mode 100644 index 0000000..6cf516b --- /dev/null +++ b/integrations/sentinelone/scripts/create_query.py @@ -0,0 +1,43 @@ +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", + } + # === REQUEST === + url = base + "/dv/init-query" + body = { + "query": inputs.get("query"), + "fromDate": inputs.get("from_date"), + "toDate": inputs.get("to_date"), + } + print(json.dumps(request("POST", url, headers, body))) + # === END === + + +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_star_rule.py b/integrations/sentinelone/scripts/create_star_rule.py new file mode 100644 index 0000000..f70f932 --- /dev/null +++ b/integrations/sentinelone/scripts/create_star_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", + } + # === REQUEST === + body = { + "data": { + "name": inputs.get("name"), + "description": inputs.get("description"), + "s1ql": inputs.get("query"), + "queryType": inputs.get("query_type") or "events", + "severity": inputs.get("rule_severity"), + "expirationMode": inputs.get("expiration_mode"), + "expiration": inputs.get("expiration_date"), + "status": "Draft", + "networkQuarantine": inputs.get("network_quarantine") in (True, "true", "True", 1), + "treatAsThreat": inputs.get("treatAsThreat"), + }, + "filter": {"tenant": "true"}, + } + if inputs.get("site_ids"): + body["filter"]["siteIds"] = csv(inputs["site_ids"]) + if inputs.get("group_ids"): + body["filter"]["groupIds"] = csv(inputs["group_ids"]) + if inputs.get("account_ids"): + body["filter"]["accountIds"] = csv(inputs["account_ids"]) + print(json.dumps(request("POST", base + "/cloud-detection/rules", headers, body))) + # === END === + + +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_white_list_item.py b/integrations/sentinelone/scripts/create_white_list_item.py new file mode 100644 index 0000000..009d43e --- /dev/null +++ b/integrations/sentinelone/scripts/create_white_list_item.py @@ -0,0 +1,51 @@ +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", + } + # === REQUEST === + body = { + "filter": { + "siteIds": csv(inputs.get("site_ids")), + "groupIds": csv(inputs.get("group_ids")), + }, + "data": { + "type": inputs.get("exclusion_type"), + "value": inputs.get("exclusion_value"), + "osType": inputs.get("os_type"), + "description": inputs.get("description"), + "mode": inputs.get("exclusion_mode"), + "pathExclusionType": inputs.get("path_exclusion_type"), + }, + } + print(json.dumps(request("POST", base + "/exclusions", headers, body))) + # === END === + + +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/delete_endpoint_tag.py b/integrations/sentinelone/scripts/delete_endpoint_tag.py new file mode 100644 index 0000000..1fcc656 --- /dev/null +++ b/integrations/sentinelone/scripts/delete_endpoint_tag.py @@ -0,0 +1,46 @@ +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", + } + # === REQUEST === + body = {"filter": {}} + if inputs.get("groupIds"): + body["filter"]["groupIds"] = csv(inputs["groupIds"]) + if inputs.get("siteIds"): + body["filter"]["siteIds"] = csv(inputs["siteIds"]) + if inputs.get("accountIds"): + body["filter"]["accountIds"] = csv(inputs["accountIds"]) + if inputs.get("query"): + body["filter"]["query"] = inputs["query"] + print(json.dumps(request("DELETE", base + "/tag-manager", headers, body))) + # === END === + + +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/delete_exclusion.py b/integrations/sentinelone/scripts/delete_exclusion.py new file mode 100644 index 0000000..a76a4fd --- /dev/null +++ b/integrations/sentinelone/scripts/delete_exclusion.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", + } + # === REQUEST === + body = {"data": {"ids": csv(inputs.get("ids")), "type": inputs.get("type")}} + print(json.dumps(request("DELETE", base + "/exclusions", headers, body))) + # === END === + + +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/delete_group.py b/integrations/sentinelone/scripts/delete_group.py new file mode 100644 index 0000000..30a6696 --- /dev/null +++ b/integrations/sentinelone/scripts/delete_group.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 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", + } + # === REQUEST === + group_id = urllib.parse.quote(str(inputs.get("group_id", "")), safe="") + url = base + "/groups/" + group_id + print(json.dumps(request("DELETE", url, headers))) + # === END === + + +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/delete_ioc.py b/integrations/sentinelone/scripts/delete_ioc.py new file mode 100644 index 0000000..a11e40e --- /dev/null +++ b/integrations/sentinelone/scripts/delete_ioc.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", + } + # === REQUEST === + body = {"filter": {"accountIds": csv(inputs.get("account_ids")), "uuids": csv(inputs.get("uuids"))}} + print(json.dumps(request("DELETE", base + "/threat-intelligence/iocs", headers, body))) + # === END === + + +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/delete_star_rule.py b/integrations/sentinelone/scripts/delete_star_rule.py new file mode 100644 index 0000000..e21982f --- /dev/null +++ b/integrations/sentinelone/scripts/delete_star_rule.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", + } + # === REQUEST === + body = {"filter": {"ids": csv(inputs.get("rule_ids"))}} + print(json.dumps(request("DELETE", base + "/cloud-detection/rules", headers, body))) + # === END === + + +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/delete_user.py b/integrations/sentinelone/scripts/delete_user.py new file mode 100644 index 0000000..bb6ccd7 --- /dev/null +++ b/integrations/sentinelone/scripts/delete_user.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 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", + } + # === REQUEST === + user_id = urllib.parse.quote(str(inputs.get("user_id", "")), safe="") + url = base + "/users/" + user_id + print(json.dumps(request("DELETE", url, headers))) + # === END === + + +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/disable_star_rules.py b/integrations/sentinelone/scripts/disable_star_rules.py new file mode 100644 index 0000000..48c1d2e --- /dev/null +++ b/integrations/sentinelone/scripts/disable_star_rules.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", + } + # === REQUEST === + body = {"filter": {"ids": csv(inputs.get("rule_ids"))}} + print(json.dumps(request("PUT", base + "/cloud-detection/rules/disable", headers, body))) + # === END === + + +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/enable_agent.py b/integrations/sentinelone/scripts/enable_agent.py new file mode 100644 index 0000000..427126c --- /dev/null +++ b/integrations/sentinelone/scripts/enable_agent.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 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", + } + # === REQUEST === + body = { + "data": {"shouldReboot": inputs.get("shouldReboot") in (True, "true", "True", 1)}, + "filter": {"ids": csv(inputs.get("agent_ids"))}, + } + print(json.dumps(request("POST", base + "/agents/actions/enable-agent", headers, body))) + # === END === + + +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/enable_star_rules.py b/integrations/sentinelone/scripts/enable_star_rules.py new file mode 100644 index 0000000..03c5829 --- /dev/null +++ b/integrations/sentinelone/scripts/enable_star_rules.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", + } + # === REQUEST === + body = {"filter": {"ids": csv(inputs.get("rule_ids"))}} + print(json.dumps(request("PUT", base + "/cloud-detection/rules/enable", headers, body))) + # === END === + + +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/expire_site.py b/integrations/sentinelone/scripts/expire_site.py new file mode 100644 index 0000000..8d0b878 --- /dev/null +++ b/integrations/sentinelone/scripts/expire_site.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 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", + } + # === REQUEST === + site_id = urllib.parse.quote(str(inputs.get("site_id", "")), safe="") + url = base + "/sites/" + site_id + "/expire-now" + print(json.dumps(request("POST", url, headers, None))) + # === END === + + +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_threat_file.py b/integrations/sentinelone/scripts/fetch_threat_file.py new file mode 100644 index 0000000..8886e43 --- /dev/null +++ b/integrations/sentinelone/scripts/fetch_threat_file.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", + } + # === REQUEST === + body = {"data": {"password": inputs.get("password")}, "filter": {"ids": csv(inputs.get("threat_id"))}} + print(json.dumps(request("POST", base + "/threats/fetch-file", headers, body))) + # === END === + + +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_accounts.py b/integrations/sentinelone/scripts/get_accounts.py new file mode 100644 index 0000000..da8de87 --- /dev/null +++ b/integrations/sentinelone/scripts/get_accounts.py @@ -0,0 +1,42 @@ +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", + } + # === REQUEST === + account_id = inputs.get("account_id") + if account_id: + url = base + "/accounts/" + urllib.parse.quote(str(account_id), safe="") + else: + url = base + "/accounts" + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_activities.py b/integrations/sentinelone/scripts/get_activities.py new file mode 100644 index 0000000..fc13b7f --- /dev/null +++ b/integrations/sentinelone/scripts/get_activities.py @@ -0,0 +1,44 @@ +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", + } + # === REQUEST === + qs = { + "created_at__gt": inputs.get("created_after"), + "limit": int(inputs.get("limit") or 50), + "agentIds": inputs.get("agent_ids"), + "threatIds": inputs.get("threats_ids"), + } + url = base + "/activities?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_agent_mac.py b/integrations/sentinelone/scripts/get_agent_mac.py new file mode 100644 index 0000000..2a5f299 --- /dev/null +++ b/integrations/sentinelone/scripts/get_agent_mac.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 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", + } + # === REQUEST === + agent_id = urllib.parse.quote(str(inputs.get("agent_id", "")), safe="") + resp = request("GET", base + "/agents?ids=" + agent_id, headers) + interfaces = [] + for agent in resp.get("data", []): + for i in agent.get("networkInterfaces", []): + interfaces.append({ + "hostname": agent.get("computerName"), + "agent_id": agent.get("id"), + "int_name": i.get("name"), + "ip": i.get("inet"), + "mac": i.get("physical"), + }) + print(json.dumps({"interfaces": interfaces})) + # === END === + + +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_alerts.py b/integrations/sentinelone/scripts/get_alerts.py new file mode 100644 index 0000000..958f1d8 --- /dev/null +++ b/integrations/sentinelone/scripts/get_alerts.py @@ -0,0 +1,48 @@ +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", + } + # === REQUEST === + qs = { + "createdAt__gte": inputs.get("created_from"), + "createdAt__lte": inputs.get("created_until"), + "ruleName__contains": inputs.get("ruleName"), + "incidentStatus": inputs.get("incidentStatus"), + "analystVerdict": inputs.get("analystVerdict"), + "ids": inputs.get("alert_ids"), + "siteIds": inputs.get("site_ids"), + "limit": int(inputs.get("limit") or 100), + } + url = base + "/cloud-detection/alerts?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_app_inventory_endpoints.py b/integrations/sentinelone/scripts/get_app_inventory_endpoints.py new file mode 100644 index 0000000..614feae --- /dev/null +++ b/integrations/sentinelone/scripts/get_app_inventory_endpoints.py @@ -0,0 +1,47 @@ +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", + } + # === REQUEST === + qs = { + "applicationName": inputs.get("applicationName"), + "applicationVendor": inputs.get("applicationVendor"), + "siteIds": inputs.get("siteIds"), + "groupIds": inputs.get("groupIds"), + "accountIds": inputs.get("accountIds"), + "cursor": inputs.get("cursor"), + "limit": int(inputs.get("limit") or 50), + } + url = base + "/application-management/inventory/endpoints?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_blocklist.py b/integrations/sentinelone/scripts/get_blocklist.py new file mode 100644 index 0000000..cd1f1cf --- /dev/null +++ b/integrations/sentinelone/scripts/get_blocklist.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 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", + } + # === REQUEST === + qs = { + "tenant": "true" if inputs.get("global") in (True, "true", "True", 1, None) else "false", + "value__contains": inputs.get("hash"), + "siteIds": inputs.get("site_ids"), + "groupIds": inputs.get("group_ids"), + "accountIds": inputs.get("account_ids"), + "skip": int(inputs.get("offset") or 0), + "limit": int(inputs.get("limit") or 100), + "sortBy": "updatedAt", + "sortOrder": "desc", + } + url = base + "/restrictions?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_dv_query_status.py b/integrations/sentinelone/scripts/get_dv_query_status.py new file mode 100644 index 0000000..80550ac --- /dev/null +++ b/integrations/sentinelone/scripts/get_dv_query_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 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", + } + # === REQUEST === + qs = { + "query_id": inputs.get("query_id"), + } + url = base + "/dv/query-status?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_endpoint_tags.py b/integrations/sentinelone/scripts/get_endpoint_tags.py new file mode 100644 index 0000000..41798ee --- /dev/null +++ b/integrations/sentinelone/scripts/get_endpoint_tags.py @@ -0,0 +1,42 @@ +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", + } + # === REQUEST === + qs = { + "key__contains": inputs.get("key__contains"), + "value__contains": inputs.get("value__contains"), + } + url = base + "/agents/tags?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_events.py b/integrations/sentinelone/scripts/get_events.py new file mode 100644 index 0000000..94adedf --- /dev/null +++ b/integrations/sentinelone/scripts/get_events.py @@ -0,0 +1,43 @@ +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", + } + # === REQUEST === + qs = { + "query_id": inputs.get("query_id"), + "limit": int(inputs.get("limit") or 50), + "cursor": inputs.get("cursor"), + } + url = base + "/dv/events?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_gateways.py b/integrations/sentinelone/scripts/get_gateways.py new file mode 100644 index 0000000..2490c02 --- /dev/null +++ b/integrations/sentinelone/scripts/get_gateways.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 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", + } + # === REQUEST === + qs = {"limit": 1000} + url = base + "/ranger/gateways?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_groups.py b/integrations/sentinelone/scripts/get_groups.py new file mode 100644 index 0000000..9f62a9d --- /dev/null +++ b/integrations/sentinelone/scripts/get_groups.py @@ -0,0 +1,46 @@ +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", + } + # === REQUEST === + qs = { + "type": inputs.get("group_type"), + "groupIds": inputs.get("group_ids"), + "isDefault": inputs.get("is_default"), + "name": inputs.get("name"), + "query": inputs.get("query"), + "limit": int(inputs.get("limit") or 50), + } + url = base + "/groups?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_installed_applications.py b/integrations/sentinelone/scripts/get_installed_applications.py new file mode 100644 index 0000000..da7872f --- /dev/null +++ b/integrations/sentinelone/scripts/get_installed_applications.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 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", + } + # === REQUEST === + qs = {"ids": ",".join(csv(inputs.get("agent_ids")))} + url = base + "/agents/applications?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_iocs.py b/integrations/sentinelone/scripts/get_iocs.py new file mode 100644 index 0000000..5a09efd --- /dev/null +++ b/integrations/sentinelone/scripts/get_iocs.py @@ -0,0 +1,46 @@ +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", + } + # === REQUEST === + qs = { + "accountIds": inputs.get("account_ids"), + "limit": int(inputs.get("limit") or 1000), + "type": inputs.get("type"), + "value": inputs.get("value"), + "source": inputs.get("source"), + "name__contains": inputs.get("name_contains"), + } + url = base + "/threat-intelligence/iocs?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_network_discovery_assets.py b/integrations/sentinelone/scripts/get_network_discovery_assets.py new file mode 100644 index 0000000..769afad --- /dev/null +++ b/integrations/sentinelone/scripts/get_network_discovery_assets.py @@ -0,0 +1,45 @@ +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", + } + # === REQUEST === + qs = { + "limit": int(inputs.get("limit") or 50), + "accountIds": inputs.get("account_id"), + "siteIds": inputs.get("site_id"), + "name__contains": inputs.get("hostname"), + "eppUnsupportedUnknown": inputs.get("epp_status"), + } + url = base + "/xdr/assets/surface/networkDiscovery?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_network_discovery_table.py b/integrations/sentinelone/scripts/get_network_discovery_table.py new file mode 100644 index 0000000..67ee74a --- /dev/null +++ b/integrations/sentinelone/scripts/get_network_discovery_table.py @@ -0,0 +1,43 @@ +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", + } + # === REQUEST === + qs = { + "hostnames__contains": inputs.get("hostnames__contains"), + "externalIp": inputs.get("externalIp"), + "externalIp__contains": inputs.get("externalIp__contains"), + } + url = base + "/ranger/table-view?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_processes.py b/integrations/sentinelone/scripts/get_processes.py new file mode 100644 index 0000000..c27e6b5 --- /dev/null +++ b/integrations/sentinelone/scripts/get_processes.py @@ -0,0 +1,42 @@ +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", + } + # === REQUEST === + qs = { + "query_id": inputs.get("query_id"), + "limit": int(inputs.get("limit") or 50), + } + url = base + "/dv/events/process?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_shadow_assets.py b/integrations/sentinelone/scripts/get_shadow_assets.py new file mode 100644 index 0000000..42563b7 --- /dev/null +++ b/integrations/sentinelone/scripts/get_shadow_assets.py @@ -0,0 +1,46 @@ +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", + } + # === REQUEST === + qs = { + "limit": int(inputs.get("limit") or 50), + "accountIds": inputs.get("account_id"), + "siteIds": inputs.get("site_id"), + "name__contains": inputs.get("name"), + "activeCoverage": inputs.get("active_coverage"), + "missingCoverage": inputs.get("missing_coverage"), + } + url = base + "/xdr/assets?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_site.py b/integrations/sentinelone/scripts/get_site.py new file mode 100644 index 0000000..85c074c --- /dev/null +++ b/integrations/sentinelone/scripts/get_site.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 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", + } + # === REQUEST === + site_id = urllib.parse.quote(str(inputs.get("site_id", "")), safe="") + url = base + "/sites/" + site_id + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_sites.py b/integrations/sentinelone/scripts/get_sites.py new file mode 100644 index 0000000..8ce275b --- /dev/null +++ b/integrations/sentinelone/scripts/get_sites.py @@ -0,0 +1,46 @@ +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", + } + # === REQUEST === + qs = { + "query": inputs.get("query"), + "state": inputs.get("state"), + "siteType": inputs.get("site_type"), + "name": inputs.get("site_name"), + "siteIds": inputs.get("site_ids"), + "limit": int(inputs.get("limit") or 50), + } + url = base + "/sites?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_star_rules.py b/integrations/sentinelone/scripts/get_star_rules.py new file mode 100644 index 0000000..5403ce9 --- /dev/null +++ b/integrations/sentinelone/scripts/get_star_rules.py @@ -0,0 +1,47 @@ +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", + } + # === REQUEST === + qs = { + "status": inputs.get("status"), + "queryType": inputs.get("queryType"), + "name__contains": inputs.get("name_contains"), + "ids": inputs.get("ruleIds"), + "accountIds": inputs.get("accountIds"), + "siteIds": inputs.get("siteIds"), + "limit": int(inputs.get("limit") or 1000), + } + url = base + "/cloud-detection/rules?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_threat_notes.py b/integrations/sentinelone/scripts/get_threat_notes.py new file mode 100644 index 0000000..c8cc835 --- /dev/null +++ b/integrations/sentinelone/scripts/get_threat_notes.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", + } + # === REQUEST === + threat_id = urllib.parse.quote(str(inputs.get("threat_id", "")), safe="") + print(json.dumps(request("GET", base + "/threats/" + threat_id + "/notes", headers))) + # === END === + + +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_threat_summary.py b/integrations/sentinelone/scripts/get_threat_summary.py new file mode 100644 index 0000000..308ee1f --- /dev/null +++ b/integrations/sentinelone/scripts/get_threat_summary.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 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", + } + # === REQUEST === + qs = {"siteIds": inputs.get("site_ids"), "groupIds": inputs.get("group_ids")} + url = base + "/private/threats/summary?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_white_list.py b/integrations/sentinelone/scripts/get_white_list.py new file mode 100644 index 0000000..25f81a0 --- /dev/null +++ b/integrations/sentinelone/scripts/get_white_list.py @@ -0,0 +1,47 @@ +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", + } + # === REQUEST === + qs = { + "ids": inputs.get("item_ids"), + "osTypes": inputs.get("os_types"), + "type": inputs.get("exclusion_type"), + "limit": int(inputs.get("limit") or 10), + "includeChildren": inputs.get("include_children"), + "includeParents": inputs.get("include_parent"), + "siteIds": inputs.get("site_ids"), + } + url = base + "/exclusions?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_endpoints_with_application.py b/integrations/sentinelone/scripts/list_endpoints_with_application.py new file mode 100644 index 0000000..e68d0e8 --- /dev/null +++ b/integrations/sentinelone/scripts/list_endpoints_with_application.py @@ -0,0 +1,46 @@ +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", + } + # === REQUEST === + qs = { + "groupIds": inputs.get("groupIds"), + "name__contains": inputs.get("name__contains"), + "osTypes": inputs.get("osTypes"), + "vendor__contains": inputs.get("vendor__contains"), + "siteIds": inputs.get("siteIds"), + "limit": int(inputs.get("limit") or 10), + } + url = base + "/application-management/inventory?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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_users.py b/integrations/sentinelone/scripts/list_users.py new file mode 100644 index 0000000..0ea1c79 --- /dev/null +++ b/integrations/sentinelone/scripts/list_users.py @@ -0,0 +1,42 @@ +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", + } + # === REQUEST === + qs = { + "accountIds": inputs.get("account_ids"), + "limit": int(inputs.get("limit") or 100), + } + url = base + "/users?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + print(json.dumps(request("GET", url, headers))) + # === END === + + +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/manage_tags.py b/integrations/sentinelone/scripts/manage_tags.py new file mode 100644 index 0000000..d2f7cbc --- /dev/null +++ b/integrations/sentinelone/scripts/manage_tags.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 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", + } + # === REQUEST === + body = { + "data": [{"tagId": inputs.get("tagId"), "operation": inputs.get("operation")}], + "filter": {"ids": csv(inputs.get("agent_ids"))}, + } + print(json.dumps(request("POST", base + "/agents/actions/manage-tags", headers, body))) + # === END === + + +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/mark_as_threat.py b/integrations/sentinelone/scripts/mark_as_threat.py new file mode 100644 index 0000000..0e73b86 --- /dev/null +++ b/integrations/sentinelone/scripts/mark_as_threat.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", + } + # === REQUEST === + body = {"filter": {"ids": csv(inputs.get("threat_ids"))}, "data": {"targetScope": inputs.get("target_scope")}} + print(json.dumps(request("POST", base + "/threats/mark-as-threat", headers, body))) + # === END === + + +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/move_agent.py b/integrations/sentinelone/scripts/move_agent.py new file mode 100644 index 0000000..f1ab828 --- /dev/null +++ b/integrations/sentinelone/scripts/move_agent.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 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", + } + # === REQUEST === + group_id = urllib.parse.quote(str(inputs.get("group_id", "")), safe="") + body = {"filter": {"ids": csv(inputs.get("agents_ids"))}} + print(json.dumps(request("PUT", base + "/groups/" + group_id + "/move-agents", headers, body))) + # === END === + + +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/purge_crash_dumps.py b/integrations/sentinelone/scripts/purge_crash_dumps.py new file mode 100644 index 0000000..01f3158 --- /dev/null +++ b/integrations/sentinelone/scripts/purge_crash_dumps.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", + } + # === REQUEST === + body = {"data": {}, "filter": {"ids": csv(inputs.get("agent_ids"))}} + print(json.dumps(request("POST", base + "/private/agents/support-actions/purge-crash-dumps", headers, body))) + # === END === + + +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/reactivate_site.py b/integrations/sentinelone/scripts/reactivate_site.py new file mode 100644 index 0000000..d643162 --- /dev/null +++ b/integrations/sentinelone/scripts/reactivate_site.py @@ -0,0 +1,40 @@ +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", + } + # === REQUEST === + site_id = urllib.parse.quote(str(inputs.get("site_id", "")), safe="") + url = base + "/sites/" + site_id + "/reactivate" + body = {"data": {"expiration": inputs.get("expiration"), "unlimited": inputs.get("unlimited") in (True, "true", "True", 1)}} + print(json.dumps(request("PUT", url, headers, body))) + # === END === + + +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/remove_hash_from_blocklist.py b/integrations/sentinelone/scripts/remove_hash_from_blocklist.py new file mode 100644 index 0000000..0cc255a --- /dev/null +++ b/integrations/sentinelone/scripts/remove_hash_from_blocklist.py @@ -0,0 +1,44 @@ +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", + } + # === REQUEST === + sha1 = inputs.get("sha1") + qs = {"tenant": "true", "value__contains": sha1, "limit": 4} + url = base + "/restrictions?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + found = request("GET", url, headers) + ids = [e.get("id") for e in (found.get("data") or []) if str(e.get("value", "")).lower() == str(sha1 or "").lower()] + for rid in ids: + request("DELETE", base + "/restrictions", headers, {"data": {"ids": [rid]}}) + print(json.dumps({"removed": len(ids), "ids": ids})) + # === END === + + +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/remove_item_from_whitelist.py b/integrations/sentinelone/scripts/remove_item_from_whitelist.py new file mode 100644 index 0000000..80cba8a --- /dev/null +++ b/integrations/sentinelone/scripts/remove_item_from_whitelist.py @@ -0,0 +1,51 @@ +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", + } + # === REQUEST === + item = inputs.get("item") + qs = { + "value__contains": item, + "type": inputs.get("exclusion_type"), + "osTypes": inputs.get("os_type"), + "includeChildren": "true", + "includeParents": "true", + "limit": 5, + } + url = base + "/exclusions?" + urllib.parse.urlencode({k: v for k, v in qs.items() if v not in (None, "")}) + found = request("GET", url, headers) + ids = [e.get("id") for e in (found.get("data") or []) if str(e.get("value", "")).lower() == str(item or "").lower()] + for eid in ids: + request("DELETE", base + "/exclusions", headers, {"data": {"ids": [eid]}}) + print(json.dumps({"removed": len(ids), "ids": ids})) + # === END === + + +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/resolve_threat.py b/integrations/sentinelone/scripts/resolve_threat.py new file mode 100644 index 0000000..3546c5a --- /dev/null +++ b/integrations/sentinelone/scripts/resolve_threat.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", + } + # === REQUEST === + body = {"filter": {"ids": csv(inputs.get("threat_ids"))}} + print(json.dumps(request("POST", base + "/threats/mark-as-resolved", headers, body))) + # === END === + + +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_remote_script.py b/integrations/sentinelone/scripts/run_remote_script.py new file mode 100644 index 0000000..30cc5bb --- /dev/null +++ b/integrations/sentinelone/scripts/run_remote_script.py @@ -0,0 +1,51 @@ +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", + } + # === REQUEST === + filt = {"accountIds": csv(inputs.get("account_ids"))} + if inputs.get("agent_ids"): + filt["ids"] = csv(inputs["agent_ids"]) + if inputs.get("site_ids"): + filt["siteIds"] = csv(inputs["site_ids"]) + data = { + "taskDescription": inputs.get("task_description"), + "outputDestination": inputs.get("output_destination"), + "scriptId": inputs.get("script_id"), + "inputParams": inputs.get("input_params"), + "password": inputs.get("password"), + "requiresApproval": inputs.get("requires_approval") in (True, "true", "True", 1), + } + body = {"filter": filt, "data": {k: v for k, v in data.items() if v not in (None, "")}} + print(json.dumps(request("POST", base + "/remote-scripts/execute", headers, body))) + # === END === + + +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/shutdown_agent.py b/integrations/sentinelone/scripts/shutdown_agent.py new file mode 100644 index 0000000..dae9dc3 --- /dev/null +++ b/integrations/sentinelone/scripts/shutdown_agent.py @@ -0,0 +1,45 @@ +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", + } + # === REQUEST === + flt = {} + if inputs.get("agent_id"): + flt["ids"] = csv(inputs.get("agent_id")) + if inputs.get("group_id"): + flt["groupIds"] = csv(inputs.get("group_id")) + if inputs.get("query"): + flt["query"] = inputs.get("query") + body = {"filter": flt} + print(json.dumps(request("POST", base + "/agents/actions/shutdown", headers, body))) + # === END === + + +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/uninstall_agent.py b/integrations/sentinelone/scripts/uninstall_agent.py new file mode 100644 index 0000000..947394b --- /dev/null +++ b/integrations/sentinelone/scripts/uninstall_agent.py @@ -0,0 +1,45 @@ +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", + } + # === REQUEST === + flt = {} + if inputs.get("agent_id"): + flt["ids"] = csv(inputs.get("agent_id")) + if inputs.get("group_id"): + flt["groupIds"] = csv(inputs.get("group_id")) + if inputs.get("query"): + flt["query"] = inputs.get("query") + body = {"filter": flt} + print(json.dumps(request("POST", base + "/agents/actions/uninstall", headers, body))) + # === END === + + +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_alerts_status.py b/integrations/sentinelone/scripts/update_alerts_status.py new file mode 100644 index 0000000..b32c7b4 --- /dev/null +++ b/integrations/sentinelone/scripts/update_alerts_status.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", + } + # === REQUEST === + body = {"data": {"incidentStatus": inputs.get("status")}, "filter": {"ids": csv(inputs.get("alert_ids"))}} + print(json.dumps(request("POST", base + "/cloud-detection/alerts/incident", headers, body))) + # === END === + + +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_alerts_verdict.py b/integrations/sentinelone/scripts/update_alerts_verdict.py new file mode 100644 index 0000000..dcc09c0 --- /dev/null +++ b/integrations/sentinelone/scripts/update_alerts_verdict.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", + } + # === REQUEST === + body = {"data": {"analystVerdict": inputs.get("verdict")}, "filter": {"ids": csv(inputs.get("alert_ids")), "tenant": "true"}} + print(json.dumps(request("POST", base + "/cloud-detection/alerts/analyst-verdict", headers, body))) + # === END === + + +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_star_rule.py b/integrations/sentinelone/scripts/update_star_rule.py new file mode 100644 index 0000000..92c50c2 --- /dev/null +++ b/integrations/sentinelone/scripts/update_star_rule.py @@ -0,0 +1,61 @@ +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", + } + # === REQUEST === + body = { + "data": { + "name": inputs.get("name"), + "description": inputs.get("description"), + "s1ql": inputs.get("query"), + "queryType": inputs.get("query_type") or "events", + "severity": inputs.get("rule_severity"), + "expirationMode": inputs.get("expiration_mode"), + "expiration": inputs.get("expiration_date"), + "status": "Draft", + "networkQuarantine": inputs.get("network_quarantine") in (True, "true", "True", 1), + "treatAsThreat": inputs.get("treatAsThreat"), + }, + "filter": {"tenant": "true"}, + } + if inputs.get("query_lang") is not None: + body["data"]["queryLang"] = inputs.get("query_lang") + if inputs.get("site_ids"): + body["filter"]["siteIds"] = csv(inputs["site_ids"]) + if inputs.get("group_ids"): + body["filter"]["groupIds"] = csv(inputs["group_ids"]) + if inputs.get("account_ids"): + body["filter"]["accountIds"] = csv(inputs["account_ids"]) + rule_id = urllib.parse.quote(str(inputs.get("rule_id", "")), safe="") + print(json.dumps(request("PUT", base + "/cloud-detection/rules/" + rule_id, headers, body))) + # === END === + + +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_threats_status.py b/integrations/sentinelone/scripts/update_threats_status.py new file mode 100644 index 0000000..8a1ab06 --- /dev/null +++ b/integrations/sentinelone/scripts/update_threats_status.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", + } + # === REQUEST === + body = {"data": {"incidentStatus": inputs.get("status")}, "filter": {"ids": csv(inputs.get("threat_ids"))}} + print(json.dumps(request("POST", base + "/threats/incident", headers, body))) + # === END === + + +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_threats_verdict.py b/integrations/sentinelone/scripts/update_threats_verdict.py new file mode 100644 index 0000000..683b2ed --- /dev/null +++ b/integrations/sentinelone/scripts/update_threats_verdict.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", + } + # === REQUEST === + body = {"data": {"analystVerdict": inputs.get("verdict")}, "filter": {"ids": csv(inputs.get("threat_ids")), "tenant": "true"}} + print(json.dumps(request("POST", base + "/threats/analyst-verdict", headers, body))) + # === END === + + +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)