From 1d475a6cf56a7fb7b5b420188e444b35eb9cd6cc Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Fri, 14 Aug 2026 23:48:40 +0200 Subject: [PATCH] fix(sentinelone): count-agents can be asked about the whole fleet again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declared as a boolean, isActive rendered as a checkbox in the toolbox, and a checkbox has no empty state: the count was forced to one half of the fleet or the other, with no way to ask for both. As text, an empty field means the filter is not sent. true/yes/1 and false/no/0 are read in any case, a real JSON boolean from a playbook still works, and anything else is refused rather than folded into false — that would answer a different question than the one asked. --- integrations/sentinelone/manifest.yaml | 6 +++--- .../sentinelone/scripts/count_agents.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/integrations/sentinelone/manifest.yaml b/integrations/sentinelone/manifest.yaml index 17f7f24..e80a0cb 100644 --- a/integrations/sentinelone/manifest.yaml +++ b/integrations/sentinelone/manifest.yaml @@ -1,8 +1,8 @@ id: sentinelone name: SentinelOne -version: 1.7.0 +version: 1.7.1 description: "SentinelOne Singularity (API v2.1) — endpoint detection & response: triage threats, enrich, isolate/reconnect hosts, mitigate, scan." -changelog: "1.7.0 — count_agents takes an accountIds filter, so a multi-tenant console can be counted one account at a time. 1.6.0 — count_agents takes an isActive filter, to count the active agents apart from the inactive ones; left unset, the count still covers both. 1.5.0 — count_agents takes four more filters: agentVersion__gt to count the fleet still below a target build, networkStatuses to count only the endpoints in a given connection state, operationalStatesNin to leave states out of the count, and machineTypes to count servers apart from laptops. 1.4.0 — get_threats and get_alerts accept an optional account_ids filter (accountIds), so a multi-tenant console can be scoped to one or more accounts at fetch time. 1.3.0 — Exhaustive OCSF mappers: get_threats (38 fields) and get_alerts (58 fields) now cover device, finding, malware, actor/target process, file, registry, network, indicators and container. 1.2.4 — Re-modelled the OCSF mappers to OCSF actor/target semantics: the initiating process maps to actor.* (actor.process, actor.user), and the process/file acted upon maps to the target (process.*, file.*, user.*). 1.2.3 — Expanded the OCSF mappers (get_threats, get_alerts) with more fields: file hashes (sha1/md5), file ext/size, finding/rule ids, confidence, status, parent process and device/user identity. 1.2.2 — Added pre-built OCSF mappers (get_threats, get_alerts) for the mapper library. 1.2.1 — Added test_connection for the instance Test button. 1.2.0 — Added 13 commands: threat-analysis, threat-download-from-cloud, abort-endpoint-scan, endpoint-fetch-logs, fetch-file, get-remote-script-task-status/results, get-service-users, list-installed-singularity-marketplace-applications, update-uam-alert-status/verdict, run-powerquery and create-tag-rule (83 commands total). 1.1.0 — Command names prefixed with 'sentinelone-' (e.g. sentinelone-isolate-agent) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: 70 commands covering agents, threats, alerts, blocklist/exclusions, IOCs, STAR rules, Deep Visibility, remote scripts, tags, firewall and network discovery based on the SentinelOne API v2.1." +changelog: "1.7.1 — count_agents' isActive filter is a text field again: declared as a boolean it rendered as a checkbox, which has no empty state, so the count could never be asked for both the active and the inactive agents. Left empty the filter is not sent. 1.7.0 — count_agents takes an accountIds filter, so a multi-tenant console can be counted one account at a time. 1.6.0 — count_agents takes an isActive filter, to count the active agents apart from the inactive ones; left unset, the count still covers both. 1.5.0 — count_agents takes four more filters: agentVersion__gt to count the fleet still below a target build, networkStatuses to count only the endpoints in a given connection state, operationalStatesNin to leave states out of the count, and machineTypes to count servers apart from laptops. 1.4.0 — get_threats and get_alerts accept an optional account_ids filter (accountIds), so a multi-tenant console can be scoped to one or more accounts at fetch time. 1.3.0 — Exhaustive OCSF mappers: get_threats (38 fields) and get_alerts (58 fields) now cover device, finding, malware, actor/target process, file, registry, network, indicators and container. 1.2.4 — Re-modelled the OCSF mappers to OCSF actor/target semantics: the initiating process maps to actor.* (actor.process, actor.user), and the process/file acted upon maps to the target (process.*, file.*, user.*). 1.2.3 — Expanded the OCSF mappers (get_threats, get_alerts) with more fields: file hashes (sha1/md5), file ext/size, finding/rule ids, confidence, status, parent process and device/user identity. 1.2.2 — Added pre-built OCSF mappers (get_threats, get_alerts) for the mapper library. 1.2.1 — Added test_connection for the instance Test button. 1.2.0 — Added 13 commands: threat-analysis, threat-download-from-cloud, abort-endpoint-scan, endpoint-fetch-logs, fetch-file, get-remote-script-task-status/results, get-service-users, list-installed-singularity-marketplace-applications, update-uam-alert-status/verdict, run-powerquery and create-tag-rule (83 commands total). 1.1.0 — Command names prefixed with 'sentinelone-' (e.g. sentinelone-isolate-agent) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: 70 commands covering agents, threats, alerts, blocklist/exclusions, IOCs, STAR rules, Deep Visibility, remote scripts, tags, firewall and network discovery based on the SentinelOne API v2.1." category: endpoint # Per-instance configuration. The scripts build the API base as /web/api/v2.1. @@ -192,7 +192,7 @@ commands: networkStatuses: { type: string, description: "Comma-separated network statuses: connected | connecting | disconnected | disconnecting" } operationalStatesNin: { type: string, description: "Comma-separated operational states to exclude, e.g. na" } machineTypes: { type: string, description: "Comma-separated machine types: desktop | laptop | server | kubernetes node | storage | unknown" } - isActive: { type: boolean, description: "Count only active agents (true) or only inactive ones (false); leave unset to count both" } + isActive: { type: string, description: "true to count only active agents, false for only the inactive ones; leave empty to count both" } required: [] outputs_schema: { properties: {} } - id: get_agent_mac diff --git a/integrations/sentinelone/scripts/count_agents.py b/integrations/sentinelone/scripts/count_agents.py index 3f4eda8..0c045bf 100644 --- a/integrations/sentinelone/scripts/count_agents.py +++ b/integrations/sentinelone/scripts/count_agents.py @@ -13,6 +13,10 @@ def csv(v): return [x.strip() for x in str(v or "").split(",") if x.strip()] +TRUE_WORDS = {"true", "yes", "1"} +FALSE_WORDS = {"false", "no", "0"} + + def main(): secrets = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) @@ -40,10 +44,15 @@ def main(): "operationalStatesNin": ",".join(csv(inputs.get("operationalStatesNin"))), "machineTypes": ",".join(csv(inputs.get("machineTypes"))), } - # Only sent when the caller actually set it: an unset boolean must not become - # isActive=false and quietly count the inactive agents instead of all of them. - if inputs.get("isActive") not in (None, ""): - qs["isActive"] = "true" if inputs["isActive"] in (True, "true", "True", 1, "1") else "false" + # Text rather than a checkbox, which has no empty state: left blank the filter + # is not sent at all and the count covers active and inactive agents alike. + # A value we cannot read is refused rather than folded into false, which would + # answer a different question than the one asked. + active = str(inputs.get("isActive", "")).strip().lower() + if active: + if active not in TRUE_WORDS | FALSE_WORDS: + raise ValueError("isActive must be true or false, got: " + repr(inputs["isActive"])) + qs["isActive"] = "true" if active in TRUE_WORDS else "false" 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 ===