From 768e4a77e381166b84f0bb51a29f6dc082505408 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Thu, 24 Sep 2026 19:18:07 -0400 Subject: [PATCH 1/2] fix(sonicwall): recover addresses, firewall actions and event outcomes The address step splits src and dst only as ip:port:interface. The grok step writes nothing unless every pattern matches and the filter then deletes the value, so ip:port (VPN negotiation events) and ip::interface (user logins) lose their address. When fw_action is the last key, the rescue grok cannot bound it and the fallback rename reads log.fwaction, while go-sdk v1.1.35 and later keep the underscore, so those events get no action and no outcome. Events whose firewall action is NA get no outcome at all, and the CEF header step captures its fields with a lone lazy pattern, which the grok step rejects. Each address shape now has its own step, and the original value is deleted only after an address was read. A fallback reads fw_action under both spellings. Events without a firewall decision get an outcome from the meaning of their message: allowed logins and completed IKEv2 negotiations give success, as does Connection Closed when bytes came back; bad credentials, RADIUS, LDAP, XAUTH and SSO failures and failed IKE negotiations give failure; policy and zone refusals give denied. The CEF header fields use a non-empty pattern. The administrator authentication-failure rule counted "Administrator login allowed" (29) as a failure and, through its message branch, SSO probe failures and policy denials. It now counts logins denied due to bad credentials with a failure outcome, its history counts failures from the same address, and alerts group by address, so a password spray is one alert instead of one per user name. Co-Authored-By: Claude Opus 5.5 --- filters/sonicwall/sonic_wall.yml | 132 +++++++++++++++--- .../sonicwall_admin_auth_failures.yml | 17 +-- 2 files changed, 120 insertions(+), 29 deletions(-) diff --git a/filters/sonicwall/sonic_wall.yml b/filters/sonicwall/sonic_wall.yml index d20d7b247..bf5b5699f 100644 --- a/filters/sonicwall/sonic_wall.yml +++ b/filters/sonicwall/sonic_wall.yml @@ -1,4 +1,4 @@ -# SonicWall Firewall — version 4.0.0 +# SonicWall Firewall — version 4.1.0 # # Formats supported # - SonicOS syslog KV: id=firewall sn=... time="..." fw=... msg="..." src=IP:PORT:IF ... @@ -35,6 +35,8 @@ pipeline: # ------------------------------------------------------------------- # CEF header (only when the log line contains "CEF:") + # The grok step matches each pattern alone and rejects an empty match, + # so header fields use a non-empty pattern instead of a lazy one. # ------------------------------------------------------------------- - grok: source: raw @@ -46,27 +48,27 @@ pipeline: - fieldName: "" pattern: '\|' - fieldName: log.deviceVendor - pattern: '{{.data}}' + pattern: '(?:[^|\\]|\\.)+' - fieldName: "" pattern: '\|' - fieldName: log.deviceProduct - pattern: '{{.data}}' + pattern: '(?:[^|\\]|\\.)+' - fieldName: "" pattern: '\|' - fieldName: log.deviceVersion - pattern: '{{.data}}' + pattern: '(?:[^|\\]|\\.)+' - fieldName: "" pattern: '\|' - fieldName: log.eventCode - pattern: '{{.data}}' + pattern: '(?:[^|\\]|\\.)+' - fieldName: "" pattern: '\|' - fieldName: log.eventName - pattern: '{{.data}}' + pattern: '(?:[^|\\]|\\.)+' - fieldName: "" pattern: '\|' - fieldName: log.cefSeverity - pattern: '{{.data}}' + pattern: '(?:[^|\\]|\\.)+' - fieldName: "" pattern: '\|' where: contains("raw", "CEF:") @@ -202,10 +204,6 @@ pipeline: pattern: '{{.greedy}}' where: contains("raw", "uuid=\"") - - delete: - fields: - - log.fwaction - where: contains("raw", "fw_action=\"") - grok: source: raw patterns: @@ -351,8 +349,10 @@ pipeline: where: exists("log.deviceTimeRaw") # ------------------------------------------------------------------- - # Split src / dst => ip[:port[:iface]] + # Split src / dst => ip[:port[:iface]], ip:port or ip::iface. + # Each shape has its own step: a step writes only when all its patterns match. # ------------------------------------------------------------------- + # ip:port:interface - grok: source: log.src patterns: @@ -366,18 +366,43 @@ pipeline: pattern: ':' - fieldName: log.sourceInterface pattern: '{{.word}}' - where: contains("log.src", ":") + where: regexMatch("log.src", "^[0-9.]+:[0-9]+:.") + # ip:port, used by VPN negotiation events + - grok: + source: log.src + patterns: + - fieldName: origin.ip + pattern: '{{.ipv4}}' + - fieldName: "" + pattern: ':' + - fieldName: origin.port + pattern: '{{.integer}}' + where: regexMatch("log.src", "^[0-9.]+:[0-9]+$") + # ip::interface, used by user login events + - grok: + source: log.src + patterns: + - fieldName: origin.ip + pattern: '{{.ipv4}}' + - fieldName: "" + pattern: '::' + - fieldName: log.sourceInterface + pattern: '{{.word}}' + where: regexMatch("log.src", "^[0-9.]+::.") - delete: fields: - log.src - where: contains("log.src", ":") + where: exists("origin.ip") + # A bare address; any other value stays under its vendor key. - rename: from: - log.src to: origin.ip + where: inCIDR("log.src", "0.0.0.0/0") + # ip:port:interface - grok: source: log.dst patterns: @@ -391,17 +416,41 @@ pipeline: pattern: ':' - fieldName: log.targetInterface pattern: '{{.word}}' - where: contains("log.dst", ":") + where: regexMatch("log.dst", "^[0-9.]+:[0-9]+:.") + # ip:port, used by VPN negotiation events + - grok: + source: log.dst + patterns: + - fieldName: target.ip + pattern: '{{.ipv4}}' + - fieldName: "" + pattern: ':' + - fieldName: target.port + pattern: '{{.integer}}' + where: regexMatch("log.dst", "^[0-9.]+:[0-9]+$") + # ip::interface, used by user login events + - grok: + source: log.dst + patterns: + - fieldName: target.ip + pattern: '{{.ipv4}}' + - fieldName: "" + pattern: '::' + - fieldName: log.targetInterface + pattern: '{{.word}}' + where: regexMatch("log.dst", "^[0-9.]+::.") - delete: fields: - log.dst - where: contains("log.dst", ":") + where: exists("target.ip") + # A bare address; any other value stays under its vendor key. - rename: from: - log.dst to: target.ip + where: inCIDR("log.dst", "0.0.0.0/0") - grok: source: log.natSrc @@ -587,6 +636,15 @@ pipeline: - log.cs6 to: log.threatContext + # fw_action is often the last key, which the rescue above cannot bound. + # Fall back to the KV value; go-sdk v1.1.35 and later keep the underscore. + - rename: + from: + - log.fw_action + - log.fwaction + to: log.actionRaw + where: '!exists("log.actionRaw")' + # ------------------------------------------------------------------- # Strip residual quotes / apostrophes from rescued values. # ------------------------------------------------------------------- @@ -638,11 +696,6 @@ pipeline: - log.userRaw to: origin.user - - rename: - from: - - log.fwaction - to: action - # ------------------------------------------------------------------- # Type casts # ------------------------------------------------------------------- @@ -685,6 +738,42 @@ pipeline: key: actionResult value: denied where: oneOf("action", ["drop", "dropped", "deny", "denied", "block", "blocked"]) + # Events without a firewall decision, by the meaning of their message: + # 29, 31, 236, 237, 238 and 1080 "... login allowed"; 942 "IKEv2 + # Authentication successful"; 978 "IKEv2 negotiation complete". + - add: + function: string + params: + key: actionResult + value: success + where: '!exists("actionResult") && oneOf("log.eventCode", ["29", "31", "236", "237", "238", "1080", "942", "978"])' + # 537 "Connection Closed" follows an allowed connection; only one that + # received data back completed. Without received bytes it keeps no outcome. + - add: + function: string + params: + key: actionResult + value: success + where: '!exists("actionResult") && equals("log.eventCode", "537") && greaterThan("origin.bytesReceived", 0)' + # 32, 33 and 200 "... login denied due to bad credentials"; 34 "Pending + # login timed out"; 140 "XAUTH Failed ... Authentication failure"; 243, 244, + # 746 and 747 RADIUS or LDAP failures; 1035 "password expired"; 1117 "SSO + # probe failed"; 402, 658, 953, 967 and 1305 failed IKE negotiations; and + # any other "... denied due to bad credentials" message. + - add: + function: string + params: + key: actionResult + value: failure + where: '!exists("actionResult") && (oneOf("log.eventCode", ["32", "33", "34", "140", "200", "243", "244", "746", "747", "1035", "1117", "402", "658", "953", "967", "1305"]) || contains("log.message", "denied due to bad credentials"))' + # 986 "User login denied - not allowed by Policy rule"; 1079 "SSLVPN + # service is not allowed on ". + - add: + function: string + params: + key: actionResult + value: denied + where: '!exists("actionResult") && oneOf("log.eventCode", ["986", "1079"])' # ------------------------------------------------------------------- # Group category human label from log.groupCategoryId (1..17) @@ -851,4 +940,5 @@ pipeline: - log.syslogPri - log.cefVersion - log.fwaction + - log.fw_action - log.grokTrash diff --git a/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml b/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml index 1ba922bd0..66a254536 100644 --- a/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml +++ b/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml @@ -1,4 +1,4 @@ -# Rule version v2.1.0 +# Rule version v2.2.0 dataTypes: - firewall-sonicwall @@ -17,6 +17,8 @@ description: | Detects failed authentication attempts against the SonicWall management interface (GUI/CLI/API). Repeated failures from a single source IP suggest a brute-force or credential-stuffing attack targeting admin credentials. + It counts logins denied due to bad credentials (events 32, 33 and 200), + not SSO probe failures, policy denials or allowed logins. Next Steps: 1. Investigate the source IP — geolocation, reputation, previous activity. @@ -26,20 +28,19 @@ description: | 4. Restrict management access to specific IP ranges and enforce MFA. where: | ( - oneOf("log.eventCode", ["29", "32", "33", "1246"]) || - ( - contains("log.message", ["admin", "administrator", "login", "management"]) && - contains("log.message", ["failed", "fail", "denied", "invalid", "incorrect"]) - ) - ) && exists("origin.ip") + oneOf("log.eventCode", ["32", "33", "200"]) || + contains("log.message", "denied due to bad credentials") + ) && equals("actionResult", "failure") && exists("origin.ip") afterEvents: - indexPattern: v11-log-firewall-sonicwall-* with: - field: origin.ip operator: filter_term value: '{{.origin.ip}}' + - field: actionResult + operator: filter_term + value: failure within: 15m count: 5 groupBy: - adversary.ip - - adversary.user From 90f18d050367abaf8def001edc5684345dcd5c22 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Thu, 24 Sep 2026 19:33:51 -0400 Subject: [PATCH 2/2] fix(sonicwall): deduplicate bad-credential alerts by source address Grouping by address only nests repeats: every attempt that passes the history check still creates a child alert. On the busiest day in the last week one deployment had 41,227 bad-credential denials from 41 addresses, which would create 40,756 alerts. Deduplicating by address creates 15 and keeps one alert per source for seven days. Co-Authored-By: Claude Opus 5.5 --- .../sonicwall_firewall/sonicwall_admin_auth_failures.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml b/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml index 66a254536..90666f784 100644 --- a/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml +++ b/rules/sonicwall/sonicwall_firewall/sonicwall_admin_auth_failures.yml @@ -18,7 +18,8 @@ description: | interface (GUI/CLI/API). Repeated failures from a single source IP suggest a brute-force or credential-stuffing attack targeting admin credentials. It counts logins denied due to bad credentials (events 32, 33 and 200), - not SSO probe failures, policy denials or allowed logins. + not SSO probe failures, policy denials or allowed logins, and raises one + alert per source address for seven days. Next Steps: 1. Investigate the source IP — geolocation, reputation, previous activity. @@ -42,5 +43,5 @@ afterEvents: value: failure within: 15m count: 5 -groupBy: +deduplicateBy: - adversary.ip