fix(sentinelone): count-agents can be asked about the whole fleet again

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.
This commit is contained in:
2026-08-14 23:48:40 +02:00
parent 93309ac74d
commit 1d475a6cf5
2 changed files with 16 additions and 7 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
id: sentinelone id: sentinelone
name: 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." 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 category: endpoint
# Per-instance configuration. The scripts build the API base as <url>/web/api/v2.1. # Per-instance configuration. The scripts build the API base as <url>/web/api/v2.1.
@@ -192,7 +192,7 @@ commands:
networkStatuses: { type: string, description: "Comma-separated network statuses: connected | connecting | disconnected | disconnecting" } 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" } 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" } 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: [] required: []
outputs_schema: { properties: {} } outputs_schema: { properties: {} }
- id: get_agent_mac - id: get_agent_mac
@@ -13,6 +13,10 @@ def csv(v):
return [x.strip() for x in str(v or "").split(",") if x.strip()] 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(): def main():
secrets = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) secrets = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
@@ -40,10 +44,15 @@ def main():
"operationalStatesNin": ",".join(csv(inputs.get("operationalStatesNin"))), "operationalStatesNin": ",".join(csv(inputs.get("operationalStatesNin"))),
"machineTypes": ",".join(csv(inputs.get("machineTypes"))), "machineTypes": ",".join(csv(inputs.get("machineTypes"))),
} }
# Only sent when the caller actually set it: an unset boolean must not become # Text rather than a checkbox, which has no empty state: left blank the filter
# isActive=false and quietly count the inactive agents instead of all of them. # is not sent at all and the count covers active and inactive agents alike.
if inputs.get("isActive") not in (None, ""): # A value we cannot read is refused rather than folded into false, which would
qs["isActive"] = "true" if inputs["isActive"] in (True, "true", "True", 1, "1") else "false" # 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, "")}) 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))) print(json.dumps(request("GET", url, headers)))
# === END === # === END ===