fix(crowdstrike): NG-SIEM detections are filed under thirdparty (1.6.2)

On a real tenant the Unified Detections raised from ingested FortiGate
logs are product 'thirdparty'; 'ngsiem' matched nothing. The command now
reads thirdparty and ngsiem by default (products input), and its mapper
is rewritten from real payloads: source product/vendor, matched host,
connection endpoints and ports, vendor signature id, Falcon priority,
console link, and a description built from what fired on which connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBnLYwQbUCaf5vpv77N14Q
This commit is contained in:
2026-10-15 08:32:36 +00:00
parent f34c87f0c8
commit 8eed7ed9fb
3 changed files with 43 additions and 13 deletions
File diff suppressed because one or more lines are too long
@@ -1,10 +1,12 @@
name: "CrowdStrike Next-Gen SIEM Detections → OCSF" name: "CrowdStrike Next-Gen SIEM Detections → OCSF"
description: "First cut of the map of a Falcon Next-Gen SIEM detection (alerts/entities/alerts/v2 filtered on product 'ngsiem', results_path = resources) to OCSF. An NG-SIEM detection is raised by a correlation rule over ingested logs, not by the sensor: it carries no device, process or file block, so none of the EDR paths of search_detections apply. Only the fields every unified alert carries are mapped here; hosts, users, IPs and the source log come with the first real payload." description: "Map of a Falcon Next-Gen SIEM detection (alerts/entities/alerts/v2 on products 'thirdparty' and 'ngsiem', results_path = resources) to OCSF. Read from real FortiGate IPS detections: a third-party detection carries no sensor process or file block, but names its source product and vendor, the source and target endpoints with ports and protocol, the host Falcon matched it to (enriched_entities) and the vendor's own signature id. It has no description: the incident's is built from the source product, the signature and the connection."
field_mappings: field_mappings:
title: "name" title: "name"
# Plain path: the ingestion engine reads informational/low/medium/high/critical natively. # Plain path: the ingestion engine reads informational/low/medium/high/critical natively.
severity: "severity_name" severity: "severity_name"
description: "description" # Third-party detections carry no description of their own; say what fired,
# from which product, on which connection.
description: "jsonata: description ? description : $join(source_products, ', ') & ' — ' & name & (source_endpoint_address_ip4 ? ' — ' & source_endpoint_address_ip4 & (local_port ? ':' & local_port : '') & ' → ' & target_endpoint_address_ip4 & (remote_port ? ':' & remote_port : '') & (protocol ? ' (' & protocol & ')' : '') : '')"
# results_path = resources; source_path is JSONata over ONE alert object. # results_path = resources; source_path is JSONata over ONE alert object.
# Paths absent from a given alert are skipped at ingestion, so extra entries are safe. # Paths absent from a given alert are skipped at ingestion, so extra entries are safe.
ocsf: ocsf:
@@ -12,15 +14,38 @@ ocsf:
- { source_path: "composite_id", ocsf_field: "finding_info.uid" } - { source_path: "composite_id", ocsf_field: "finding_info.uid" }
- { source_path: "name", ocsf_field: "finding_info.title" } - { source_path: "name", ocsf_field: "finding_info.title" }
- { source_path: "description", ocsf_field: "finding_info.desc" } - { source_path: "description", ocsf_field: "finding_info.desc" }
- { source_path: "categorization", ocsf_field: "finding_info.types" }
- { source_path: "data_domains", ocsf_field: "finding_info.data_sources" }
- { source_path: "falcon_host_link", ocsf_field: "finding_info.src_url" }
- { source_path: "created_timestamp", ocsf_field: "finding_info.created_time" } - { source_path: "created_timestamp", ocsf_field: "finding_info.created_time" }
- { source_path: "created_timestamp", ocsf_field: "time" }
- { source_path: "updated_timestamp", ocsf_field: "finding_info.modified_time" } - { source_path: "updated_timestamp", ocsf_field: "finding_info.modified_time" }
- { source_path: "type", ocsf_field: "finding_info.types" } # `time` is when the source product saw the event (timestamp), ahead of
# Falcon's own record of the detection (created_timestamp, ~20 s later).
- { source_path: "timestamp", ocsf_field: "time" }
# The vendor's signature id (FortiGate IPS 100012) identifies the rule;
# pattern_id is Falcon's generic third-party bucket (300000 for all of them).
- { source_path: "name", ocsf_field: "finding_info.analytic.name" }
- { source_path: "vendor_pattern_id", ocsf_field: "finding_info.analytic.uid" }
- { source_path: "confidence", ocsf_field: "confidence" } - { source_path: "confidence", ocsf_field: "confidence" }
- { source_path: "priority_value", ocsf_field: "risk_score" }
- { source_path: "status", ocsf_field: "status" } - { source_path: "status", ocsf_field: "status" }
# ── Source product — the log the detection came from ──────────────
- { source_path: "source_products[0]", ocsf_field: "metadata.product.name" }
- { source_path: "source_vendors[0]", ocsf_field: "metadata.product.vendor_name" }
- { source_path: "product", ocsf_field: "metadata.product.feature.name" } - { source_path: "product", ocsf_field: "metadata.product.feature.name" }
- { source_path: "cid", ocsf_field: "cloud.account.uid" } - { source_path: "cid", ocsf_field: "cloud.account.uid" }
# ── MITRE ATT&CK ────────────────────────────────────────────────── # ── Host Falcon matched the detection to ──────────────────────────
- { source_path: "agent_id", ocsf_field: "device.uid" }
- { source_path: "enriched_entities.hostnames[0]", ocsf_field: "device.hostname" }
# ── Connection ────────────────────────────────────────────────────
- { source_path: "source_endpoint_address_ip4 ? source_endpoint_address_ip4 : source_ips[0]", ocsf_field: "src_endpoint.ip" }
- { source_path: "source_hosts[0]", ocsf_field: "src_endpoint.hostname" }
- { source_path: "$number(local_port)", ocsf_field: "src_endpoint.port" }
- { source_path: "target_endpoint_address_ip4 ? target_endpoint_address_ip4 : destination_ips[0]", ocsf_field: "dst_endpoint.ip" }
- { source_path: "$number(remote_port)", ocsf_field: "dst_endpoint.port" }
- { source_path: "protocol", ocsf_field: "connection_info.protocol_name" }
- { source_path: "users[0]", ocsf_field: "actor.user.name" }
# ── MITRE ATT&CK (correlation rules that declare it) ──────────────
- { source_path: "tactic", ocsf_field: "attacks.tactic.name" } - { source_path: "tactic", ocsf_field: "attacks.tactic.name" }
- { source_path: "tactic_id", ocsf_field: "attacks.tactic.uid" } - { source_path: "tactic_id", ocsf_field: "attacks.tactic.uid" }
- { source_path: "technique", ocsf_field: "attacks.technique.name" } - { source_path: "technique", ocsf_field: "attacks.technique.name" }
@@ -33,10 +33,14 @@ def csv(v):
def main(): def main():
tok = token() tok = token()
# Next-Gen SIEM correlation rules raise their detections in the same unified # Next-Gen SIEM files its detections in the same unified Alerts API as the
# Alerts API as the EDR, under product 'ngsiem'. The product clause is always # EDR, under two products: 'thirdparty' for the alerts it raises from ingested
# kept, so a filter narrows the NG-SIEM detections and never widens past them. # third-party logs (a FortiGate IPS hit, say) and 'ngsiem' for those of the
fql = "product:'ngsiem'" # tenant's own correlation rules. The product clause is always kept, so a
# filter narrows these detections and never widens past them.
products = [p.strip().strip("'\"") for p in str(INPUTS.get("products") or "thirdparty,ngsiem").split(",") if p.strip()]
# ',' is OR in FQL; parenthesised so the group binds before the '+' ANDs.
fql = "(" + ",".join("product:'" + p.replace("'", "\\'") + "'" for p in products) + ")"
extra = str(INPUTS.get("filter") or "").strip() extra = str(INPUTS.get("filter") or "").strip()
if extra: if extra:
fql += "+(" + extra + ")" fql += "+(" + extra + ")"