From ff461464fdb40e494c8d4ac4a412b3f7bad98b22 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Thu, 15 Oct 2026 12:08:05 +0000 Subject: [PATCH] fix(crowdstrike): resolve_detection fails when Falcon changed nothing (1.6.3) The alerts PATCH answers 200 to a refused update (errors, resources_affected 0) and the command reported it as a success. It now exits in error with Falcon's reasons and the ids sent; an empty update is refused up front. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VBnLYwQbUCaf5vpv77N14Q --- integrations/crowdstrike/manifest.yaml | 8 ++++---- .../crowdstrike/scripts/resolve_detection.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/integrations/crowdstrike/manifest.yaml b/integrations/crowdstrike/manifest.yaml index 215088e..333aee8 100644 --- a/integrations/crowdstrike/manifest.yaml +++ b/integrations/crowdstrike/manifest.yaml @@ -1,8 +1,8 @@ id: crowdstrike name: CrowdStrike Falcon -version: 1.6.2 +version: 1.6.3 description: "CrowdStrike Falcon (OAuth2 API) — full IR coverage: device/IOC/process enrichment, detections & cases, host groups, Real Time Response, ML/IOA exclusions, quarantine, Spotlight/CVE, ODS scans, CSPM, users, IOA rules, CNAPP, and Fusion workflows." -changelog: "1.6.2 — search_ngsiem_detections reads products thirdparty and ngsiem by default, set by a new products input. On a real tenant the NG-SIEM detections of the Unified Detections page — here FortiGate IPS hits — are filed under product thirdparty, and product ngsiem matched nothing, so 1.6.0 returned an empty list. Its mapper is rewritten from those payloads: source product and vendor, the host Falcon matched the detection to, source and target IPs and ports, protocol, the vendor signature id as the analytic uid, the Falcon priority as risk score, the console link, and the source event time as OCSF time. Third-party detections carry no description, so the incident's is built from the source product, the signature and the connection. | 1.6.1 — search_detections and search_ngsiem_detections return a query block next to resources: the FQL actually sent, the total Falcon counts for it and any errors the query carried. A search that matched nothing used to come back as a bare empty list, which could not be told apart from a wrong region, a filter Falcon read differently or an API client that sees no alerts. | 1.6.0 — New search_ngsiem_detections command for Next-Gen SIEM detections. Falcon files the detections of NG-SIEM correlation rules in the same unified Alerts API as the EDR, under product 'ngsiem', so search_detections' default epp/ldt scope never returned them, and its mapper reads device, process and file blocks an NG-SIEM detection does not carry. The new command always keeps the product:'ngsiem' clause and ANDs any filter to it, takes the same created_from watermark, and ships its own mapper and a CrowdStrike NG-SIEM Detection incident type. The mapper is a first cut on the fields every unified alert carries (identity, name, description, severity, status, timestamps, MITRE); hosts, users and the source log will follow from a real payload. | 1.5.0 — resolve_detection sets the verdict. Falcon has no verdict field on an alert: the console's Resolution column is read from the tags true_positive, false_positive and ignored, and the status only takes new, in_progress, reopened and closed — so no value of the old status box could record a verdict. The new verdict input adds the tag, after removing the other two so an alert never carries two resolutions, and status and verdict are now drop-downs. 1.4.0 — New device_count command: counts devices matching filters without listing them. The device query already answers with meta.pagination.total — the number of matches, independent of the page asked for — so device_count asks for a one-ID page and reads it, where search-device resolves every matching device in full through /devices/entities/devices/v2 to reach the same figure, and only filters on four properties. Filters cover sixteen device FQL properties (hostname, device id, local and external IP, platform, OS version, role, containment status, sensor version, AD domain/OU/site, serial, tags, host groups, RFM), add first_seen/last_seen bounds, and leave a raw FQL `filter` escape hatch for the rest. Several values on one property become an OR group, parenthesised so it binds before the AND that joins it to the others — the reference client omits those parentheses and leaves the precedence to the server. Values are quoted with the single quote escaped, so a hostname carrying an apostrophe cannot end the literal early and change what the query counts. The estate `total` comes back next to `count` so the figure reads as a proportion, and a filter too broad to mean anything shows as matching everything rather than passing for an answer. | 1.3.0 — search_detections takes a created_from timestamp, folded into the FQL as created_timestamp, and declares it as the ingestion watermark input. An alert rule can now fetch incrementally instead of re-reading the whole window every run: this API expresses its time bound inside the FQL, so the watermark used to have nowhere to go but the filter input, where it replaced the query rather than narrowing it. filter now carries the scoping only. 1.2.4 — Fixed two silent defects in the search_detections OCSF mapper: severity was a ternary that resolved to nothing, so every alert kept the alert rule's default severity instead of Falcon's — it is now a plain path to severity_name, whose words the engine reads natively; and no entry fed the OCSF `time` field, so detected_at was never stamped and MTTD measured the collection delay as zero. 1.2.3 — Documentation wording cleanup. 1.2.2 — Corrected the search_detections OCSF mapper against the Falcon Raptor alert schema: domain is device.hostinfo.domain, dropped non-existent fields, added scenario/alleged_filetype/cid. 1.2.1 — Marked search_detections as an ingestion source (ingest hints: results_path=resources, dedup_key=composite_id) so the alert-rule editor pre-fills it. 1.2.0 — Added a pre-built OCSF mapper for search_detections (Falcon unified alerts → OCSF: device, finding, MITRE, actor/parent process, file and IOC). 1.1.1 — Added test_connection for the instance Test button. 1.1.0 — Expanded to 81 commands (host groups, cases, RTR files/scripts/responders, ML/IOA exclusions, quarantine, Spotlight host-by-vuln/CVE, ODS scans, CSPM, users, IOA rules, CNAPP, identity/mobile detection resolve, and workflows). 1.0.0 — Initial release: device/detection enrichment, Spotlight, IOC management, contain/lift, and core RTR." +changelog: "1.6.3 — resolve_detection fails when Falcon changed nothing. The alerts PATCH answers 200 to an update it refused, with the reasons in errors and meta.writes.resources_affected at 0, and the command passed that on as a success: an incident could be closed in Riposte and stay open in the console. It now exits in error with Falcon's errors and the ids it sent (the PATCH takes composite ids, :). A call with nothing to change is refused before reaching Falcon. The command applies to every unified alert, NG-SIEM and third-party detections included. | 1.6.2 — search_ngsiem_detections reads products thirdparty and ngsiem by default, set by a new products input. On a real tenant the NG-SIEM detections of the Unified Detections page — here FortiGate IPS hits — are filed under product thirdparty, and product ngsiem matched nothing, so 1.6.0 returned an empty list. Its mapper is rewritten from those payloads: source product and vendor, the host Falcon matched the detection to, source and target IPs and ports, protocol, the vendor signature id as the analytic uid, the Falcon priority as risk score, the console link, and the source event time as OCSF time. Third-party detections carry no description, so the incident's is built from the source product, the signature and the connection. | 1.6.1 — search_detections and search_ngsiem_detections return a query block next to resources: the FQL actually sent, the total Falcon counts for it and any errors the query carried. A search that matched nothing used to come back as a bare empty list, which could not be told apart from a wrong region, a filter Falcon read differently or an API client that sees no alerts. | 1.6.0 — New search_ngsiem_detections command for Next-Gen SIEM detections. Falcon files the detections of NG-SIEM correlation rules in the same unified Alerts API as the EDR, under product 'ngsiem', so search_detections' default epp/ldt scope never returned them, and its mapper reads device, process and file blocks an NG-SIEM detection does not carry. The new command always keeps the product:'ngsiem' clause and ANDs any filter to it, takes the same created_from watermark, and ships its own mapper and a CrowdStrike NG-SIEM Detection incident type. The mapper is a first cut on the fields every unified alert carries (identity, name, description, severity, status, timestamps, MITRE); hosts, users and the source log will follow from a real payload. | 1.5.0 — resolve_detection sets the verdict. Falcon has no verdict field on an alert: the console's Resolution column is read from the tags true_positive, false_positive and ignored, and the status only takes new, in_progress, reopened and closed — so no value of the old status box could record a verdict. The new verdict input adds the tag, after removing the other two so an alert never carries two resolutions, and status and verdict are now drop-downs. 1.4.0 — New device_count command: counts devices matching filters without listing them. The device query already answers with meta.pagination.total — the number of matches, independent of the page asked for — so device_count asks for a one-ID page and reads it, where search-device resolves every matching device in full through /devices/entities/devices/v2 to reach the same figure, and only filters on four properties. Filters cover sixteen device FQL properties (hostname, device id, local and external IP, platform, OS version, role, containment status, sensor version, AD domain/OU/site, serial, tags, host groups, RFM), add first_seen/last_seen bounds, and leave a raw FQL `filter` escape hatch for the rest. Several values on one property become an OR group, parenthesised so it binds before the AND that joins it to the others — the reference client omits those parentheses and leaves the precedence to the server. Values are quoted with the single quote escaped, so a hostname carrying an apostrophe cannot end the literal early and change what the query counts. The estate `total` comes back next to `count` so the figure reads as a proportion, and a filter too broad to mean anything shows as matching everything rather than passing for an answer. | 1.3.0 — search_detections takes a created_from timestamp, folded into the FQL as created_timestamp, and declares it as the ingestion watermark input. An alert rule can now fetch incrementally instead of re-reading the whole window every run: this API expresses its time bound inside the FQL, so the watermark used to have nowhere to go but the filter input, where it replaced the query rather than narrowing it. filter now carries the scoping only. 1.2.4 — Fixed two silent defects in the search_detections OCSF mapper: severity was a ternary that resolved to nothing, so every alert kept the alert rule's default severity instead of Falcon's — it is now a plain path to severity_name, whose words the engine reads natively; and no entry fed the OCSF `time` field, so detected_at was never stamped and MTTD measured the collection delay as zero. 1.2.3 — Documentation wording cleanup. 1.2.2 — Corrected the search_detections OCSF mapper against the Falcon Raptor alert schema: domain is device.hostinfo.domain, dropped non-existent fields, added scenario/alleged_filetype/cid. 1.2.1 — Marked search_detections as an ingestion source (ingest hints: results_path=resources, dedup_key=composite_id) so the alert-rule editor pre-fills it. 1.2.0 — Added a pre-built OCSF mapper for search_detections (Falcon unified alerts → OCSF: device, finding, MITRE, actor/parent process, file and IOC). 1.1.1 — Added test_connection for the instance Test button. 1.1.0 — Expanded to 81 commands (host groups, cases, RTR files/scripts/responders, ML/IOA exclusions, quarantine, Spotlight host-by-vuln/CVE, ODS scans, CSPM, users, IOA rules, CNAPP, identity/mobile detection resolve, and workflows). 1.0.0 — Initial release: device/detection enrichment, Spotlight, IOC management, contain/lift, and core RTR." category: endpoint # Per-instance configuration. Scripts obtain an OAuth2 bearer token from @@ -188,11 +188,11 @@ commands: - id: resolve_detection name: crowdstrike-resolve-detection - description: "Update detections/alerts: change status, set the verdict, append a comment, and/or assign to an analyst." + description: "Update unified alerts — EDR, NG-SIEM and third-party detections alike: change status, set the verdict, append a comment, and/or assign to an analyst. Takes composite ids. Fails when Falcon updated no alert." risk: destructive inputs_schema: properties: - ids: { type: string, description: "Comma-separated alert composite IDs." } + ids: { type: string, description: "Comma-separated alert composite IDs (composite_id, : — not the bare id)." } status: type: string description: "New status. closed ends the triage; set verdict alongside it so the console shows a resolution." diff --git a/integrations/crowdstrike/scripts/resolve_detection.py b/integrations/crowdstrike/scripts/resolve_detection.py index b49dc34..6789872 100644 --- a/integrations/crowdstrike/scripts/resolve_detection.py +++ b/integrations/crowdstrike/scripts/resolve_detection.py @@ -62,8 +62,23 @@ def main(): action_params.append({"name": "append_comment", "value": str(INPUTS["comment"])}) if INPUTS.get("assign_to_uuid"): action_params.append({"name": "assign_to_uuid", "value": str(INPUTS["assign_to_uuid"])}) + if not action_params: + raise ValueError("nothing to change: give a status, a verdict, a comment or an assignee") body = {"action_parameters": action_params, "composite_ids": ids} result = call("PATCH", "/alerts/entities/alerts/v3", tok, body=body) + # Falcon answers 200 to an update it refused: the reasons sit in errors and + # meta.writes.resources_affected stays at 0. Passing that on as a success + # left an incident closed in Riposte and still open in the console. + affected = ((result.get("meta") or {}).get("writes") or {}).get("resources_affected") + if result.get("errors") or affected == 0: + print(json.dumps({ + "error": "Falcon updated no alert" if affected == 0 else "Falcon refused part of the update", + "resources_affected": affected, + "errors": result.get("errors") or [], + # The PATCH takes composite ids (:), not the alert's bare id. + "composite_ids": ids, + })) + sys.exit(1) print(json.dumps(result))