From d516a3453604db3e86dbc4d6bdc0beef29f687a1 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 16 Sep 2026 20:34:42 -0400 Subject: [PATCH] fix: align Suricata filter and rule contracts --- filters/audits/suricata.md | 40 ++ filters/suricata/suricata.yml | 151 +++++--- .../testdata/filter-contracts/suricata.json | 365 ++++++++++++++++++ rules/nids/suricata/ddos_attack_patterns.yml | 16 +- ...severity_suricata_alerts_were_detected.yml | 2 +- ...severity_suricata_alerts_were_detected.yml | 2 +- 6 files changed, 522 insertions(+), 54 deletions(-) create mode 100644 filters/audits/suricata.md create mode 100644 plugins/alerts/testdata/filter-contracts/suricata.json diff --git a/filters/audits/suricata.md b/filters/audits/suricata.md new file mode 100644 index 000000000..ac4946747 --- /dev/null +++ b/filters/audits/suricata.md @@ -0,0 +1,40 @@ +# Suricata normalization and rule review + +Respect final packet/flow verdict, require evidence for success, correct priority and repair EVE DDoS rule consumers. + +This draft targets UTMStack `v11`. It contains 1 filter changes +and 3 rule changes for this technology only. Review covered +1 filter configurations and 35 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. +- 18 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/suricata.json`. Apply that support before running `go test ./...` in `plugins/alerts`. + +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/suricata/suricata.yml b/filters/suricata/suricata.yml index 5635ebcaa..484cdcabb 100644 --- a/filters/suricata/suricata.yml +++ b/filters/suricata/suricata.yml @@ -31,6 +31,9 @@ pipeline: - json: source: log.suricataJson where: regexMatch("log.suricataJson", "^\\{.*\\}$") + - json: + source: raw + where: '!exists("log.suricataJson") && regexMatch("raw", "^\\s*\\{")' # Rename fields to conversion v11 schema - rename: @@ -91,24 +94,24 @@ pipeline: # Adding severity field based on log.alert.severity - add: - function: "string" + function: string params: key: severity - value: "low" + value: critical where: equals("log.alert.severity", 1) - add: - function: "string" + function: string params: key: severity - value: "medium" + value: warning where: equals("log.alert.severity", 2) - add: - function: "string" + function: string params: key: severity - value: "high" + value: info where: equals("log.alert.severity", 3) #....................................................................... @@ -124,46 +127,9 @@ pipeline: to: log.fileInfo # Implementing action field used for established connections - - add: - function: "string" - params: - key: action - value: "success" - where: (equals("log.eventType", "tls") && exists("log.tls.sessionresumed")) || - (equals("log.eventType", "dns") && equals("log.dns.type", "answer")) || - (equals("log.eventType", "flow") && exists("origin.ip") && exists("target.ip") && greaterThan("log.flow.bytestoserver", 0) && greaterThan("log.flow.bytestoclient", 0)) || - (equals("log.eventType", "ssh") && exists("log.ssh.server") && exists("log.ssh.client")) || - (equals("log.eventType", "alert") && exists("origin.ip") && exists("target.ip") && equals("log.alert.action", "allowed") && greaterThan("log.flow.bytestoserver", 0) && greaterThan("log.flow.bytestoclient", 0)) || - (equals("log.eventType", "http") && exists("origin.ip") && exists("target.ip") && exists("log.http.status")) || - (equals("log.eventType", "ftp") && exists("origin.ip") && exists("target.ip") && exists("log.ftp.completioncode")) || - (equals("log.eventType", "tftp") && exists("origin.ip") && exists("target.ip") && !equals("log.tftp.packet", "error")) || - (equals("log.eventType", "smb") && exists("origin.ip") && exists("target.ip") && oneOf("log.smb.command", ["NEGOTIATE"]) && oneOf("log.smb.status", ["SUCCESS", "GRANTED", "CONNECTED"])) || - (equals("log.eventType", "rdp") && exists("origin.ip") && exists("target.ip") && (equals("log.rdp.eventType", "connectresponse") || equals("log.rdp.eventType", "tlshandshake"))) || - (equals("log.eventType", "rfb") && exists("origin.ip") && exists("target.ip") && equals("log.rfb.authentication.security-result", "OK")) || - (equals("log.eventType", "mqtt") && exists("origin.ip") && exists("target.ip") && (equals("log.mqtt.connack.returncode", "0") || equals("log.mqtt.connack.returncode", "0x00"))) || - (equals("log.eventType", "pgsql") && exists("origin.ip") && exists("target.ip") && (exists("log.pgsql.request.simplequery") || exists("log.pgsql.response.commandcompleted") || equals("log.pgsql.response.sslaccepted", "true") || equals("log.pgsql.response.accepted", "true") || exists("log.pgsql.response.authenticationmd5password"))) || - ((equals("log.eventType", "ike") || equals("log.eventType", "ikev1") || equals("log.eventType", "ikev2")) && exists("origin.ip") && exists("target.ip")) || - (equals("log.eventType", "modbus") && exists("origin.ip") && exists("target.ip")) || - (equals("log.eventType", "sip") && exists("origin.ip") && exists("target.ip")) || - (equals("log.eventType", "quic") && exists("origin.ip") && exists("target.ip")) || - (equals("log.eventType", "fileinfo") && exists("origin.ip") && exists("target.ip")) || - (equals("log.eventType", "snmp") && exists("origin.ip") && exists("target.ip")) || - (equals("log.eventType", "dhcp") && exists("origin.ip") && exists("target.ip") && !equals("log.dhcp.assignedip", "0")) # Adding action result - - add: - function: string - params: - key: actionResult - value: "allowed" - where: 'exists("log.alert.action") && equals("log.alert.action", "allowed")' - - add: - function: string - params: - key: actionResult - value: "blocked" - where: 'exists("log.alert.action") && equals("log.alert.action", "blocked")' # Adding geolocation to origin.ip - dynamic: @@ -188,4 +154,101 @@ pipeline: - log.syslogPri - log.syslogTimestamp - log.syslogProgram - - log.syslogPid \ No newline at end of file + - log.syslogPid + + # Normalize explicit outcomes; an unknown outcome remains unset. + - add: + function: string + params: + key: actionResult + value: denied + where: oneOf("log.verdict.action",["drop","reject"]) || exists("log.verdict.reject") || equals("log.flow.action","drop") || equals("log.alert.action","blocked") + - add: + function: string + params: + key: actionResult + value: success + where: '!exists("actionResult") && (equals("log.verdict.action","pass") || equals("log.flow.action","pass"))' + - add: + function: string + params: + key: actionResult + value: success + where: '!exists("actionResult") && equals("log.flow.state","established") && greaterThan("log.flow.bytestoserver",0) && greaterThan("log.flow.bytestoclient",0)' + - add: + function: string + params: + key: connectionStatus + value: established + where: equals("log.flow.state","established") && !equals("actionResult","denied") + - add: + function: string + params: + key: connectionStatus + value: closed + where: equals("log.flow.state","closed") + + # 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","::"])) + - add: + function: string + params: + key: protocol + value: ICMP + where: equals("protocol",1) + - add: + function: string + params: + key: protocol + value: TCP + where: equals("protocol",6) + - add: + function: string + params: + key: protocol + value: UDP + where: equals("protocol",17) + - add: + function: string + params: + key: protocol + value: GRE + where: equals("protocol",47) + - add: + function: string + params: + key: protocol + value: ESP + where: equals("protocol",50) + - add: + function: string + params: + key: protocol + value: AH + where: equals("protocol",51) + - add: + function: string + params: + key: protocol + value: ICMPV6 + where: equals("protocol",58) + - add: + function: string + params: + key: protocol + value: SCTP + where: equals("protocol",132) + - rename: + from: + - protocol + to: log.ipProtocolNumber + where: exists("protocol") && greaterOrEqual("protocol",0) diff --git a/plugins/alerts/testdata/filter-contracts/suricata.json b/plugins/alerts/testdata/filter-contracts/suricata.json new file mode 100644 index 000000000..b7bb5e6d0 --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/suricata.json @@ -0,0 +1,365 @@ +{ + "technology": "Suricata", + "filters": [ + "filters/suricata/suricata.yml" + ], + "rules": [ + "rules/nids/suricata/baby_shark_c2.yml", + "rules/nids/suricata/base64_dns_queries.yml", + "rules/nids/suricata/base64_encoded_user_agent.yml", + "rules/nids/suricata/cobalt_strike_dns_beacon.yml", + "rules/nids/suricata/cobalt_strike_malleable_c2.yml", + "rules/nids/suricata/command_and_control_traffic.yml", + "rules/nids/suricata/covert_channel_detection.yml", + "rules/nids/suricata/data_exfiltration_patterns.yml", + "rules/nids/suricata/ddos_attack_patterns.yml", + "rules/nids/suricata/dns_mining_pool_lookups.yml", + "rules/nids/suricata/dns_oob_interaction_domains.yml", + "rules/nids/suricata/dns_tunneling_detection.yml", + "rules/nids/suricata/dns_txt_execution_strings.yml", + "rules/nids/suricata/exploit_attempt_detection.yml", + "rules/nids/suricata/exploit_framework_user_agents.yml", + "rules/nids/suricata/hacktool_user_agents.yml", + "rules/nids/suricata/icmp_tunneling_detection.yml", + "rules/nids/suricata/ipfs_credential_harvesting.yml", + "rules/nids/suricata/lateral_movement_indicators.yml", + "rules/nids/suricata/malware_callbacks.yml", + "rules/nids/suricata/malware_user_agent_strings.yml", + "rules/nids/suricata/nids_arp_spoofing.yml", + "rules/nids/suricata/nids_ja3_threat_matching.yml", + "rules/nids/suricata/nids_malicious_file_downloads.yml", + "rules/nids/suricata/nids_ssh_anomalies.yml", + "rules/nids/suricata/nids_tls_certificate_anomalies.yml", + "rules/nids/suricata/nkn_blockchain_c2.yml", + "rules/nids/suricata/port_scan_detection.yml", + "rules/nids/suricata/pwndrop_file_hosting.yml", + "rules/nids/suricata/rclone_data_exfiltration.yml", + "rules/nids/suricata/threat_intelligence_iocs.yml", + "rules/nids/suricata/tor_proxy_dns_lookups.yml", + "rules/nids/suricata/tunneling_detection.yml", + "rules/suricata/high_severity_suricata_alerts_were_detected.yml", + "rules/suricata/medium_severity_suricata_alerts_were_detected.yml" + ], + "fixtures": [ + { + "name": "Suricata priority 1", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "severity": 1 + } + } + }, + "expected": { + "severity": "critical" + }, + "absent": [], + "rules": {} + }, + { + "name": "Suricata priority 2", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "severity": 2 + } + } + }, + "expected": { + "severity": "warning" + }, + "absent": [], + "rules": {} + }, + { + "name": "Suricata priority 3", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "severity": 3 + } + } + }, + "expected": { + "severity": "info" + }, + "absent": [], + "rules": {} + }, + { + "name": "Suricata final verdict wins", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "action": "allowed" + }, + "verdict": { + "action": "drop" + } + } + }, + "expected": { + "actionResult": "denied" + }, + "absent": [], + "rules": {} + }, + { + "name": "Suricata IDS allowed is not connection success", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "action": "allowed" + } + } + }, + "expected": {}, + "absent": [ + "actionResult", + "action" + ], + "rules": {} + }, + { + "name": "Suricata explicit IPS pass", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "verdict": { + "action": "pass" + } + } + }, + "expected": { + "actionResult": "success" + }, + "absent": [], + "rules": {} + }, + { + "name": "Suricata confirmed established flow", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "eventtype": "flow", + "flow": { + "state": "established", + "bytestoserver": 400, + "bytestoclient": 500 + } + } + }, + "expected": { + "actionResult": "success", + "connectionStatus": "established" + }, + "absent": [], + "rules": {} + }, + { + "name": "Suricata denied established flow", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "verdict": { + "action": "drop" + }, + "flow": { + "state": "established", + "bytestoserver": 400, + "bytestoclient": 500 + } + } + }, + "expected": { + "actionResult": "denied" + }, + "absent": [ + "connectionStatus" + ], + "rules": {} + }, + { + "name": "Suricata priority rule 1", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "severity": 1 + }, + "eventtype": "alert" + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/suricata/high_severity_suricata_alerts_were_detected.yml": true, + "rules/suricata/medium_severity_suricata_alerts_were_detected.yml": false + } + }, + { + "name": "Suricata priority rule 2", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "severity": 2 + }, + "eventtype": "alert" + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/suricata/high_severity_suricata_alerts_were_detected.yml": false, + "rules/suricata/medium_severity_suricata_alerts_were_detected.yml": true + } + }, + { + "name": "Suricata priority rule 3", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "alert": { + "severity": 3 + }, + "eventtype": "alert" + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/suricata/high_severity_suricata_alerts_were_detected.yml": false, + "rules/suricata/medium_severity_suricata_alerts_were_detected.yml": false + } + }, + { + "name": "Suricata NTP ratio 11", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "destport": 123, + "flow": { + "bytestoclient": 1100, + "bytestoserver": 100 + } + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/nids/suricata/ddos_attack_patterns.yml": true + } + }, + { + "name": "Suricata NTP ratio 9", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "destport": 123, + "flow": { + "bytestoclient": 900, + "bytestoserver": 100 + } + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/nids/suricata/ddos_attack_patterns.yml": false + } + }, + { + "name": "Suricata DDoS flow age 4", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "proto": "UDP", + "flow": { + "pktstoserver": 2000, + "age": 4 + } + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/nids/suricata/ddos_attack_patterns.yml": true + } + }, + { + "name": "Suricata DDoS flow age 10", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "proto": "UDP", + "flow": { + "pktstoserver": 2000, + "age": 10 + } + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/nids/suricata/ddos_attack_patterns.yml": false + } + }, + { + "name": "Suricata DDoS missing counters", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "destport": 123 + } + }, + "expected": {}, + "absent": [], + "rules": { + "rules/nids/suricata/ddos_attack_patterns.yml": false + } + }, + { + "name": "Suricata flow drop overrides established state", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "flow": { + "action": "drop", + "state": "established", + "bytestoserver": 500, + "bytestoclient": 200 + } + } + }, + "expected": { + "actionResult": "denied" + }, + "absent": [ + "connectionStatus" + ] + }, + { + "name": "Suricata flow explicit pass", + "filter": "suricata/suricata.yml", + "input": { + "log": { + "flow": { + "action": "pass" + } + } + }, + "expected": { + "actionResult": "success" + }, + "absent": [ + "connectionStatus" + ] + } + ] +} diff --git a/rules/nids/suricata/ddos_attack_patterns.yml b/rules/nids/suricata/ddos_attack_patterns.yml index 05612ee99..7d478446e 100644 --- a/rules/nids/suricata/ddos_attack_patterns.yml +++ b/rules/nids/suricata/ddos_attack_patterns.yml @@ -32,17 +32,17 @@ where: | contains("log.alert.signature", "DDoS") || contains("log.alert.signature", "amplification"))) || (equals("protocol", "TCP") && - equals("log.tcp.flags", "S") && - greaterThan("log.flow.pkts_toserver", 100) && - lessThan("log.flow.duration", 5)) || + (equals("log.tcp.flags", "S") || (equals("log.tcp.syn", true) && !equals("log.tcp.ack", true))) && + (greaterThan("log.flow.pktstoserver", 100) || greaterThan("log.flow.pkts_toserver", 100)) && + (lessThan("log.flow.age", 5) || (!exists("log.flow.age") && lessThan("log.flow.duration", 5)))) || (equals("protocol", "UDP") && - greaterThan("log.flow.pkts_toserver", 1000) && - lessThan("log.flow.duration", 10)) || + (greaterThan("log.flow.pktstoserver", 1000) || greaterThan("log.flow.pkts_toserver", 1000)) && + (lessThan("log.flow.age", 10) || (!exists("log.flow.age") && lessThan("log.flow.duration", 10)))) || (equals("protocol", "ICMP") && - greaterThan("log.flow.pkts_toserver", 500) && - lessThan("log.flow.duration", 5)) || + (greaterThan("log.flow.pktstoserver", 500) || greaterThan("log.flow.pkts_toserver", 500)) && + (lessThan("log.flow.age", 5) || (!exists("log.flow.age") && lessThan("log.flow.duration", 5)))) || (equals("target.port", 123) && - safe("log.flow.bytes_toclient", 0) > safe("log.flow.bytes_toserver", 0) * 10) + safe("log.flow.bytestoclient", safe("log.flow.bytes_toclient", 0.0)) > safe("log.flow.bytestoserver", safe("log.flow.bytes_toserver", 0.0)) * 10.0) ) afterEvents: - indexPattern: v11-log-suricata-* diff --git a/rules/suricata/high_severity_suricata_alerts_were_detected.yml b/rules/suricata/high_severity_suricata_alerts_were_detected.yml index 66cdfc139..ee0dc25b1 100644 --- a/rules/suricata/high_severity_suricata_alerts_were_detected.yml +++ b/rules/suricata/high_severity_suricata_alerts_were_detected.yml @@ -13,7 +13,7 @@ adversary: origin references: - "https://suricata.readthedocs.io/en/latest/" description: "Suricata has detected a high severity alert. This indicates potential malicious activity targeting the network infrastructure, such as exploitation attempts, malware communication, or suspicious network behavior." -where: equals("log.eventType", "alert") && equals("severity", "high") +where: equals("log.eventType", "alert") && equals("log.alert.severity", 1) afterEvents: - indexPattern: v11-log-suricata-* with: diff --git a/rules/suricata/medium_severity_suricata_alerts_were_detected.yml b/rules/suricata/medium_severity_suricata_alerts_were_detected.yml index 277e9c8cf..c87d3ce58 100644 --- a/rules/suricata/medium_severity_suricata_alerts_were_detected.yml +++ b/rules/suricata/medium_severity_suricata_alerts_were_detected.yml @@ -13,7 +13,7 @@ adversary: target description: "Suricata has detected a medium severity alert that may indicate reconnaissance or scanning attempts. This could include port scanning, protocol anomalies, or suspicious network patterns that warrant investigation but may not represent an immediate threat." references: - "https://suricata.readthedocs.io/en/latest/" -where: equals("log.eventType", "alert") && equals("severity", "medium") +where: equals("log.eventType", "alert") && equals("log.alert.severity", 2) afterEvents: - indexPattern: v11-log-suricata-* with: