From f3eb9592744bdbf196d18b9e8673a1d478fb4622 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 16 Sep 2026 20:34:25 -0400 Subject: [PATCH] fix: align CrowdStrike filter and rule contracts --- filters/audits/crowdstrike.md | 42 ++++++++++++ filters/crowdstrike/crowdstrike.yml | 28 ++++++-- .../filter-contracts/crowdstrike.json | 64 +++++++++++++++++++ rules/crowdstrike/inhibit_system_recovery.yml | 4 +- ...failures_(possible_brute_force_attack).yml | 2 +- .../os_credential_dumping_activity.yml | 4 +- ...y_defenses_impaired_or_policy_disabled.yml | 2 +- ...cious_downloader_execution_linux_macos.yml | 2 +- ...uspicious_encoded_powershell_execution.yml | 2 +- .../suspicious_native_downloaders.yml | 2 +- .../windows_event_log_clearing.yml | 4 +- 11 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 filters/audits/crowdstrike.md create mode 100644 plugins/alerts/testdata/filter-contracts/crowdstrike.json diff --git a/filters/audits/crowdstrike.md b/filters/audits/crowdstrike.md new file mode 100644 index 000000000..a2fc9c148 --- /dev/null +++ b/filters/audits/crowdstrike.md @@ -0,0 +1,42 @@ +# CrowdStrike normalization and rule review + +Preserve explicit SourceIp over LocalIP, normalize addresses/outcomes and correct alert grouping. + +This draft targets UTMStack `v11`. It contains 1 filter changes +and 8 rule changes for this technology only. Review covered +1 filter configurations and 17 matching shipped rule files. +Unchanged rules are listed in the regression manifest; they are not duplicated in the diff. + +## Contract and validation + +- Compared exact standard names/types with go-sdk v1.1.31 and the supplied UTMStack dictionaries. +- Checked documented pipeline ordering, rename/move behavior, open vendor log fields, + event-side versus alert-side fields, and surviving fields used by affected rule predicates/history/grouping. +- Strict SDK configuration decoding and actual CEL compilation pass for this scope. +- 2 synthetic normalization cases pass, including SDK Event conversion and any + trigger predicate assertions recorded in the manifest. +- The scoped alerts module tests and `git diff --check` pass with the shared contract runner applied. + +The shared alert-contract PR supplies the reusable Go runner for the manifest in +`plugins/alerts/testdata/filter-contracts/crowdstrike.json`. Apply that support before running `go test ./...` in `plugins/alerts`. + +The changed rules also require the shared alert-grouping fix to resolve `lastEvent.*` values correctly at runtime. + +The model starts from synthetic extraction results. It does not run complex grok, +JSON/KV/XML/CSV extraction, time conversion, dynamic plugins, historical OpenSearch +queries, or the closed EventProcessor. Raw vendor logs and resulting alerts must +still be checked in staging before rollout. No customer false-positive reduction +has been measured and no production rollout is included. + + + +## References + +- [SDK schema](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) +- [Filter steps](https://github.com/threatwinds/go-sdk/wiki/Filter-Steps-Reference) +- [Standard event schema](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) +- [Rule implementation](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) + +`afterEvents`, empty noncapturing grok names, supported numeric strings, and custom +`log.*` fields are accepted. Existing textual protocol casing and vendor action names +are preserved unless a concrete consumer mismatch requires correction. diff --git a/filters/crowdstrike/crowdstrike.yml b/filters/crowdstrike/crowdstrike.yml index 560140d6e..3cffe91dc 100644 --- a/filters/crowdstrike/crowdstrike.yml +++ b/filters/crowdstrike/crowdstrike.yml @@ -343,6 +343,12 @@ pipeline: from: - log.event.LocalIP to: origin.ip + where: '!exists("origin.ip")' + + - rename: + from: + - log.event.LocalIP + to: log.eventLocalIP - rename: from: @@ -680,8 +686,8 @@ pipeline: function: string params: key: actionResult - value: "failed" - where: 'exists("log.eventSuccess") && oneOf("log.eventSuccess", [false, "false"])' + value: failure + where: exists("log.eventSuccess") && oneOf("log.eventSuccess", [false, "false"]) - add: function: string params: @@ -692,8 +698,8 @@ pipeline: function: string params: key: actionResult - value: "failed" - where: 'exists("statusCode") && greaterOrEqual("statusCode", 400)' + value: failure + where: exists("statusCode") && greaterOrEqual("statusCode", 400) # .......................................................................# # Normalizing request method and renaming to action @@ -741,4 +747,16 @@ pipeline: - log.statusCode - log.event.UserIp - log.event.Attributes.user_ip - - log.event.Attributes.action_target_name \ No newline at end of file + - log.event.Attributes.action_target_name + + # Keep addresses in IP fields and retain other source values under log. + - rename: + from: + - origin.ip + to: log.unparsedOriginIp + where: exists("origin.ip") && (!(inCIDR("origin.ip","0.0.0.0/0") || inCIDR("origin.ip","::/0")) || oneOf("origin.ip",["0.0.0.0","::"])) + - rename: + from: + - target.ip + to: log.unparsedTargetIp + where: exists("target.ip") && (!(inCIDR("target.ip","0.0.0.0/0") || inCIDR("target.ip","::/0")) || oneOf("target.ip",["0.0.0.0","::"])) diff --git a/plugins/alerts/testdata/filter-contracts/crowdstrike.json b/plugins/alerts/testdata/filter-contracts/crowdstrike.json new file mode 100644 index 000000000..257082bcc --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/crowdstrike.json @@ -0,0 +1,64 @@ +{ + "technology": "CrowdStrike", + "filters": [ + "filters/crowdstrike/crowdstrike.yml" + ], + "rules": [ + "rules/crowdstrike/critical_role_modification.yml", + "rules/crowdstrike/custom_indicator_of_compromise_(IoC)_detected.yml", + "rules/crowdstrike/deletion_or_deactivation_of_user_account.yml", + "rules/crowdstrike/endpoint_network_containment_action.yml", + "rules/crowdstrike/endpoint_or_XDR_detection_alert.yml", + "rules/crowdstrike/inhibit_system_recovery.yml", + "rules/crowdstrike/ip_whitelisting_modification.yml", + "rules/crowdstrike/major_incident_generated.yml", + "rules/crowdstrike/multiple_authentication_failures_(possible_brute_force_attack).yml", + "rules/crowdstrike/os_credential_dumping_activity.yml", + "rules/crowdstrike/real_time_response_rtr_session_execution.yml", + "rules/crowdstrike/security_defenses_impaired_or_policy_disabled.yml", + "rules/crowdstrike/security_policy_disabled_or_deleted.yml", + "rules/crowdstrike/suspicious_downloader_execution_linux_macos.yml", + "rules/crowdstrike/suspicious_encoded_powershell_execution.yml", + "rules/crowdstrike/suspicious_native_downloaders.yml", + "rules/crowdstrike/windows_event_log_clearing.yml" + ], + "fixtures": [ + { + "name": "CrowdStrike remote/local split", + "filter": "crowdstrike/crowdstrike.yml", + "input": { + "log": { + "event": { + "SourceIp": "198.51.100.10", + "LocalIP": "10.0.0.8" + } + } + }, + "expected": { + "origin.ip": "198.51.100.10", + "log.eventLocalIP": "10.0.0.8" + }, + "absent": [ + "target.ip" + ], + "rules": {} + }, + { + "name": "Crowdstrike endpoint source fallback", + "filter": "crowdstrike/crowdstrike.yml", + "input": { + "log": { + "event": { + "LocalIP": "10.0.0.10" + } + } + }, + "expected": { + "origin.ip": "10.0.0.10" + }, + "absent": [ + "target.ip" + ] + } + ] +} diff --git a/rules/crowdstrike/inhibit_system_recovery.yml b/rules/crowdstrike/inhibit_system_recovery.yml index 2031b0c39..b4efb374c 100644 --- a/rules/crowdstrike/inhibit_system_recovery.yml +++ b/rules/crowdstrike/inhibit_system_recovery.yml @@ -16,5 +16,5 @@ where: > exists("log.eventCommandLine") && regexMatch("log.eventCommandLine", "(?i).*(vssadmin.*delete shadows|wmic.*shadowcopy.*delete|bcdedit.*recoveryenabled.*no).*") groupBy: - - origin.host - - origin.user \ No newline at end of file + - adversary.host + - adversary.user \ No newline at end of file diff --git a/rules/crowdstrike/multiple_authentication_failures_(possible_brute_force_attack).yml b/rules/crowdstrike/multiple_authentication_failures_(possible_brute_force_attack).yml index 1c7ab31cf..287b98cca 100644 --- a/rules/crowdstrike/multiple_authentication_failures_(possible_brute_force_attack).yml +++ b/rules/crowdstrike/multiple_authentication_failures_(possible_brute_force_attack).yml @@ -27,4 +27,4 @@ afterEvents: operator: filter_term value: 'false' deduplicateBy: - - origin.ip \ No newline at end of file + - adversary.ip \ No newline at end of file diff --git a/rules/crowdstrike/os_credential_dumping_activity.yml b/rules/crowdstrike/os_credential_dumping_activity.yml index 2a9d35238..77a2bd787 100644 --- a/rules/crowdstrike/os_credential_dumping_activity.yml +++ b/rules/crowdstrike/os_credential_dumping_activity.yml @@ -16,5 +16,5 @@ where: > exists("log.eventCommandLine") && regexMatch("log.eventCommandLine", "(?i).*(procdump.*lsass|mimikatz|sekurlsa|lsass\\.dmp).*") groupBy: - - origin.host - - origin.user \ No newline at end of file + - adversary.host + - adversary.user \ No newline at end of file diff --git a/rules/crowdstrike/security_defenses_impaired_or_policy_disabled.yml b/rules/crowdstrike/security_defenses_impaired_or_policy_disabled.yml index fb727dae2..031d0cef3 100644 --- a/rules/crowdstrike/security_defenses_impaired_or_policy_disabled.yml +++ b/rules/crowdstrike/security_defenses_impaired_or_policy_disabled.yml @@ -15,5 +15,5 @@ where: > equals("log.eventPatternDispositionFlagsPolicyDisabled", true) || oneOf("log.eventPatternDispositionValue", [8192, 8208, 8320, 8704, 9216, 10240, 12304, 73728, 73744]) groupBy: - - origin.host + - adversary.host - lastEvent.log.eventPatternDispositionDescription \ No newline at end of file diff --git a/rules/crowdstrike/suspicious_downloader_execution_linux_macos.yml b/rules/crowdstrike/suspicious_downloader_execution_linux_macos.yml index d3ae34193..7fa3fc904 100644 --- a/rules/crowdstrike/suspicious_downloader_execution_linux_macos.yml +++ b/rules/crowdstrike/suspicious_downloader_execution_linux_macos.yml @@ -17,5 +17,5 @@ where: > exists("log.eventCommandLine") && regexMatch("log.eventCommandLine", "(?i).*(curl|wget).*http.*") groupBy: - - origin.host + - adversary.host - lastEvent.log.eventCommandLine \ No newline at end of file diff --git a/rules/crowdstrike/suspicious_encoded_powershell_execution.yml b/rules/crowdstrike/suspicious_encoded_powershell_execution.yml index b7e043dee..881f657cc 100644 --- a/rules/crowdstrike/suspicious_encoded_powershell_execution.yml +++ b/rules/crowdstrike/suspicious_encoded_powershell_execution.yml @@ -16,5 +16,5 @@ where: > exists("log.eventCommandLine") && regexMatch("log.eventCommandLine", "(?i).*(powershell|pwsh).*-(e|en|enc|encodedcommand|ec)\\s+.*") groupBy: - - origin.host + - adversary.host - lastEvent.log.eventCommandLine \ No newline at end of file diff --git a/rules/crowdstrike/suspicious_native_downloaders.yml b/rules/crowdstrike/suspicious_native_downloaders.yml index 6b05dd819..0b103089e 100644 --- a/rules/crowdstrike/suspicious_native_downloaders.yml +++ b/rules/crowdstrike/suspicious_native_downloaders.yml @@ -16,5 +16,5 @@ where: > exists("log.eventCommandLine") && regexMatch("log.eventCommandLine", "(?i).*(certutil.*-urlcache|bitsadmin.*-transfer|curl.*http|wget.*http).*") groupBy: - - origin.host + - adversary.host - lastEvent.log.eventCommandLine \ No newline at end of file diff --git a/rules/crowdstrike/windows_event_log_clearing.yml b/rules/crowdstrike/windows_event_log_clearing.yml index c5f50d002..b2e479729 100644 --- a/rules/crowdstrike/windows_event_log_clearing.yml +++ b/rules/crowdstrike/windows_event_log_clearing.yml @@ -16,5 +16,5 @@ where: > exists("log.eventCommandLine") && regexMatch("log.eventCommandLine", "(?i).*(wevtutil\\s+cl.*|Clear-EventLog.*|Remove-EventLog.*).*") groupBy: - - origin.host - - origin.user \ No newline at end of file + - adversary.host + - adversary.user \ No newline at end of file