From 88bc26147470c862a3527cb30c72dd7c05f040ae Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 16 Sep 2026 20:34:13 -0400 Subject: [PATCH] fix: align Bitdefender GravityZone filter and rule contracts --- filters/antivirus/bitdefender_gz.yml | 59 +++++- filters/audits/bitdefender.md | 42 +++++ .../filter-contracts/bitdefender.json | 170 ++++++++++++++++++ .../bitdefender_gz/apt_detection.yml | 2 +- .../high_severity_threat_detection.yml | 2 +- .../malware_outbreak_multiple_hosts.yml | 2 +- .../multiple_malware_from_single_source.yml | 2 +- .../network_threat_detection.yml | 2 +- .../phishing_access_blocked.yaml | 2 +- .../quarantine_failure_detection.yml | 2 +- 10 files changed, 271 insertions(+), 14 deletions(-) create mode 100644 filters/audits/bitdefender.md create mode 100644 plugins/alerts/testdata/filter-contracts/bitdefender.json diff --git a/filters/antivirus/bitdefender_gz.yml b/filters/antivirus/bitdefender_gz.yml index bc73922ea..ea2df8ed2 100644 --- a/filters/antivirus/bitdefender_gz.yml +++ b/filters/antivirus/bitdefender_gz.yml @@ -504,8 +504,8 @@ pipeline: - rename: from: - - log.severity - to: severity + - log.severity + to: log.cefSeverity - rename: from: @@ -560,23 +560,30 @@ pipeline: - rename: from: - - log.BitdefenderGZEventSourceIP + - log.BitdefenderGZEventSourceIP to: origin.ip + where: '!exists("origin.ip")' # Adding actionResult field to indicate whether the action was successful or failed + - add: + function: string + params: + key: actionResult + value: denied + where: oneOf("action", ["blocked", "block", "aph_blocked", "portscan_blocked", "quarantined"]) - add: function: string params: key: actionResult value: success - where: 'oneOf("action", ["blocked", "block", "aph_blocked", "portscan_blocked", "deleted", "disinfected", "quarantined", "restored"])' + where: oneOf("action", ["deleted", "disinfected", "restored"]) - add: function: string params: key: actionResult - value: failed - where: 'oneOf("action", ["still present", "ignored", "no action", "reportOnly"])' + value: failure + where: oneOf("action", ["still present", "ignored", "no action", "reportOnly"]) # Adding geolocation to origin ip - dynamic: @@ -620,4 +627,42 @@ pipeline: - log.dvc - log.request - log.suser - - log.fname \ No newline at end of file + - log.fname + + # 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","::"])) + + # Normalize the source event severity. + - add: + function: string + params: + key: severity + value: info + where: (greaterOrEqual("log.cefSeverity",0) && lessOrEqual("log.cefSeverity",3)) || oneOf("log.cefSeverity",["Low","low","Unknown"]) + - add: + function: string + params: + key: severity + value: warning + where: (greaterOrEqual("log.cefSeverity",4) && lessOrEqual("log.cefSeverity",6)) || oneOf("log.cefSeverity",["Medium","medium"]) + - add: + function: string + params: + key: severity + value: error + where: (greaterOrEqual("log.cefSeverity",7) && lessOrEqual("log.cefSeverity",8)) || oneOf("log.cefSeverity",["High","high"]) + - add: + function: string + params: + key: severity + value: critical + where: (greaterOrEqual("log.cefSeverity",9) && lessOrEqual("log.cefSeverity",10)) || oneOf("log.cefSeverity",["Very-High","Very High","very-high"]) diff --git a/filters/audits/bitdefender.md b/filters/audits/bitdefender.md new file mode 100644 index 000000000..19096a6ac --- /dev/null +++ b/filters/audits/bitdefender.md @@ -0,0 +1,42 @@ +# Bitdefender GravityZone normalization and rule review + +Preserve attacker IPs, normalize response outcomes and severity, and align phishing/priority rules. + +This draft targets UTMStack `v11`. It contains 1 filter changes +and 7 rule changes for this technology only. Review covered +1 filter configurations and 21 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. +- 8 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/bitdefender.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/plugins/alerts/testdata/filter-contracts/bitdefender.json b/plugins/alerts/testdata/filter-contracts/bitdefender.json new file mode 100644 index 000000000..aeae2463e --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/bitdefender.json @@ -0,0 +1,170 @@ +{ + "technology": "Bitdefender GravityZone", + "filters": [ + "filters/antivirus/bitdefender_gz.yml" + ], + "rules": [ + "rules/antivirus/bitdefender_gz/antivirus_service_stopped.yml", + "rules/antivirus/bitdefender_gz/apt_detection.yml", + "rules/antivirus/bitdefender_gz/av_console_lateral_movement.yml", + "rules/antivirus/bitdefender_gz/av_policy_override.yml", + "rules/antivirus/bitdefender_gz/bootkit_detection.yml", + "rules/antivirus/bitdefender_gz/crypto_mining_detection.yml", + "rules/antivirus/bitdefender_gz/email_threat_spreading.yml", + "rules/antivirus/bitdefender_gz/fileless_malware_detection.yml", + "rules/antivirus/bitdefender_gz/high_severity_threat_detection.yml", + "rules/antivirus/bitdefender_gz/malware_outbreak_multiple_hosts.yml", + "rules/antivirus/bitdefender_gz/memory_threat_detection.yml", + "rules/antivirus/bitdefender_gz/multiple_malware_from_single_source.yml", + "rules/antivirus/bitdefender_gz/network_threat_detection.yml", + "rules/antivirus/bitdefender_gz/phishing_access_blocked.yaml", + "rules/antivirus/bitdefender_gz/quarantine_failure_detection.yml", + "rules/antivirus/bitdefender_gz/ransomware_behavior_detection.yml", + "rules/antivirus/bitdefender_gz/realtime_protection_disabled.yml", + "rules/antivirus/bitdefender_gz/rootkit_detection.yml", + "rules/antivirus/bitdefender_gz/suspicious_exclusions_added.yml", + "rules/antivirus/bitdefender_gz/usb_malware_propagation.yml", + "rules/antivirus/bitdefender_gz/zero_day_malware_detection.yml" + ], + "fixtures": [ + { + "name": "Bitdefender phishing aph_blocked", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "actFull": "aph_blocked", + "BitdefenderGZModule": "aph" + } + }, + "expected": { + "actionResult": "denied" + }, + "absent": [], + "rules": { + "rules/antivirus/bitdefender_gz/phishing_access_blocked.yaml": false + } + }, + { + "name": "Bitdefender phishing reportOnly", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "actFull": "reportOnly", + "BitdefenderGZModule": "aph" + } + }, + "expected": { + "actionResult": "failure" + }, + "absent": [], + "rules": { + "rules/antivirus/bitdefender_gz/phishing_access_blocked.yaml": true + } + }, + { + "name": "Bitdefender attacker priority", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "BitdefenderGZDetectionAttackerIp": "198.51.100.10", + "BitdefenderGZEventSourceIP": "10.0.0.2" + } + }, + "expected": { + "origin.ip": "198.51.100.10" + }, + "absent": [], + "rules": {} + }, + { + "name": "Bitdefender CEF priority 0", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "severity": "0", + "BitdefenderGZModule": "network-monitor" + } + }, + "expected": { + "severity": "info", + "log.cefSeverity": "0" + }, + "absent": [], + "rules": { + "rules/antivirus/bitdefender_gz/network_threat_detection.yml": false + } + }, + { + "name": "Bitdefender CEF priority 3", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "severity": "3", + "BitdefenderGZModule": "network-monitor" + } + }, + "expected": { + "severity": "info", + "log.cefSeverity": "3" + }, + "absent": [], + "rules": { + "rules/antivirus/bitdefender_gz/network_threat_detection.yml": false + } + }, + { + "name": "Bitdefender CEF priority 6", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "severity": "6", + "BitdefenderGZModule": "network-monitor" + } + }, + "expected": { + "severity": "warning", + "log.cefSeverity": "6" + }, + "absent": [], + "rules": { + "rules/antivirus/bitdefender_gz/network_threat_detection.yml": false + } + }, + { + "name": "Bitdefender CEF priority 8", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "severity": "8", + "BitdefenderGZModule": "network-monitor" + } + }, + "expected": { + "severity": "error", + "log.cefSeverity": "8" + }, + "absent": [], + "rules": { + "rules/antivirus/bitdefender_gz/network_threat_detection.yml": true + } + }, + { + "name": "Bitdefender CEF priority 10", + "filter": "antivirus/bitdefender_gz.yml", + "input": { + "log": { + "severity": "10", + "BitdefenderGZModule": "network-monitor" + } + }, + "expected": { + "severity": "critical", + "log.cefSeverity": "10" + }, + "absent": [], + "rules": { + "rules/antivirus/bitdefender_gz/network_threat_detection.yml": true + } + } + ] +} diff --git a/rules/antivirus/bitdefender_gz/apt_detection.yml b/rules/antivirus/bitdefender_gz/apt_detection.yml index 8d91eab8b..5f30c2629 100644 --- a/rules/antivirus/bitdefender_gz/apt_detection.yml +++ b/rules/antivirus/bitdefender_gz/apt_detection.yml @@ -40,7 +40,7 @@ description: | 5. Collect forensic artifacts before remediating - memory image and endpoint logs - since a targeted intrusion warrants attribution work 6. Isolate the endpoint if the detection action shows the threat was not blocked, then hunt for what ran while it was active where: | - greaterOrEqual("severity", 8) && + (greaterOrEqual("log.cefSeverity", 8) || greaterOrEqual("severity", 8)) && ( (equals("log.BitdefenderGZModule", "hd") && regexMatch("log.BitdefenderGZAttackTypes", "(?i)targeted attack")) || diff --git a/rules/antivirus/bitdefender_gz/high_severity_threat_detection.yml b/rules/antivirus/bitdefender_gz/high_severity_threat_detection.yml index d513c6d45..524628e6b 100644 --- a/rules/antivirus/bitdefender_gz/high_severity_threat_detection.yml +++ b/rules/antivirus/bitdefender_gz/high_severity_threat_detection.yml @@ -38,7 +38,7 @@ description: | - Check that signatures were current at deviceTime, using log.BitdefenderGZSignaturesNumber where: | oneOf("log.BitdefenderGZModule", ["av", "avc", "hd"]) && - greaterOrEqual("severity", 8) + (greaterOrEqual("log.cefSeverity", 8) || greaterOrEqual("severity", 8)) groupBy: - target.host - target.malware diff --git a/rules/antivirus/bitdefender_gz/malware_outbreak_multiple_hosts.yml b/rules/antivirus/bitdefender_gz/malware_outbreak_multiple_hosts.yml index 0327d9b5d..e26dcc8ce 100644 --- a/rules/antivirus/bitdefender_gz/malware_outbreak_multiple_hosts.yml +++ b/rules/antivirus/bitdefender_gz/malware_outbreak_multiple_hosts.yml @@ -33,7 +33,7 @@ description: | 7. Keep the incident open until a full day passes with no new host reporting the same malware where: | oneOf("log.BitdefenderGZModule", ["av", "avc", "hd"]) && - greaterOrEqual("severity", 8) && + (greaterOrEqual("log.cefSeverity", 8) || greaterOrEqual("severity", 8)) && exists("target.malware") correlation: - indexPattern: v11-log-antivirus-bitdefender-gz-* diff --git a/rules/antivirus/bitdefender_gz/multiple_malware_from_single_source.yml b/rules/antivirus/bitdefender_gz/multiple_malware_from_single_source.yml index f045cae50..4bb04a2a7 100644 --- a/rules/antivirus/bitdefender_gz/multiple_malware_from_single_source.yml +++ b/rules/antivirus/bitdefender_gz/multiple_malware_from_single_source.yml @@ -35,7 +35,7 @@ description: | 7. Reimage if the same host keeps reappearing in this rule across days where: | oneOf("log.BitdefenderGZModule", ["av", "avc", "hd"]) && - greaterOrEqual("severity", 8) + (greaterOrEqual("log.cefSeverity", 8) || greaterOrEqual("severity", 8)) correlation: - indexPattern: v11-log-antivirus-bitdefender-gz-* within: 1h diff --git a/rules/antivirus/bitdefender_gz/network_threat_detection.yml b/rules/antivirus/bitdefender_gz/network_threat_detection.yml index b452ab552..7153b51ff 100644 --- a/rules/antivirus/bitdefender_gz/network_threat_detection.yml +++ b/rules/antivirus/bitdefender_gz/network_threat_detection.yml @@ -37,7 +37,7 @@ description: | 6. Block the source at the perimeter, and only then close the alert. A blocked attempt means this attack failed, not that the attacker stopped where: | oneOf("log.BitdefenderGZModule", ["network-monitor", "fw"]) && - greaterOrEqual("severity", 8) + (greaterOrEqual("log.cefSeverity", 8) || greaterOrEqual("severity", 8)) correlation: - indexPattern: v11-log-antivirus-bitdefender-gz-* within: 2h diff --git a/rules/antivirus/bitdefender_gz/phishing_access_blocked.yaml b/rules/antivirus/bitdefender_gz/phishing_access_blocked.yaml index 19fe39758..3fa3b1d69 100644 --- a/rules/antivirus/bitdefender_gz/phishing_access_blocked.yaml +++ b/rules/antivirus/bitdefender_gz/phishing_access_blocked.yaml @@ -37,7 +37,7 @@ description: | 7. Submit the URL for blocking at the perimeter so the rest of the estate is covered where: | equals("log.BitdefenderGZModule", "aph") && - equals("actionResult", "success") + equals("action", "reportOnly") groupBy: - target.user - adversary.url diff --git a/rules/antivirus/bitdefender_gz/quarantine_failure_detection.yml b/rules/antivirus/bitdefender_gz/quarantine_failure_detection.yml index d83430101..f5cb7762d 100644 --- a/rules/antivirus/bitdefender_gz/quarantine_failure_detection.yml +++ b/rules/antivirus/bitdefender_gz/quarantine_failure_detection.yml @@ -42,7 +42,7 @@ description: | where: | equals("log.eventType", "AntiMalware") && ( - equals("actionResult", "failed") || + oneOf("actionResult", ["failure", "failed"]) || (greaterOrEqual("log.BitdefenderGZPresentMalwareCnt", 1) && equals("log.BitdefenderGZQuarantinedMalwareCnt", 0) && equals("log.BitdefenderGZCleanedMalwareCnt", 0))