From db4ae93a194009ca05631245a6316b6c87134f2c Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 16 Sep 2026 20:34:24 -0400 Subject: [PATCH 1/2] fix: align Cisco Meraki filter and rule contracts --- filters/audits/cisco-meraki.md | 42 ++++ filters/cisco/meraki.yml | 224 ++++++++++++------ .../filter-contracts/cisco-meraki.json | 125 ++++++++++ .../advanced_malware_protection_alerts.yml | 14 +- 4 files changed, 320 insertions(+), 85 deletions(-) create mode 100644 filters/audits/cisco-meraki.md create mode 100644 plugins/alerts/testdata/filter-contracts/cisco-meraki.json diff --git a/filters/audits/cisco-meraki.md b/filters/audits/cisco-meraki.md new file mode 100644 index 000000000..83a078614 --- /dev/null +++ b/filters/audits/cisco-meraki.md @@ -0,0 +1,42 @@ +# Cisco Meraki normalization and rule review + +Avoid fabricated success for observations/pending VPN; preserve rule input fields and handle retrospective AMP events without invented source IP. + +This draft targets UTMStack `v11`. It contains 1 filter changes +and 1 rule changes for this technology only. Review covered +1 filter configurations and 7 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. +- 6 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/cisco-meraki.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. + +Current Meraki security events retain their group/message for the AMP consumer. Retrospective malicious dispositions do not report a source IP, so the rule does not invent or require one; grouping includes dataSource. Source: https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples + +## 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/cisco/meraki.yml b/filters/cisco/meraki.yml index 66bb8d610..3990db7e3 100644 --- a/filters/cisco/meraki.yml +++ b/filters/cisco/meraki.yml @@ -103,10 +103,10 @@ pipeline: where: '!equals("log.controlFlag", "Init")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' + value: success where: equals("log.connectivity", "true") # ........................................ # event uplink connectivity change @@ -169,10 +169,10 @@ pipeline: where: '!equals("log.controlFlag", "Init") && contains("action", "Cellular connection")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' + value: success where: equalsIgnoreCase("log.connectionState", "up") # ........................................ # Event: event, dhcp no offers @@ -236,10 +236,10 @@ pipeline: where: '!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp lease")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' + value: success where: contains("log.genericEvent", "dhcp lease") # ........................................ # Event: event, HTTP GET requests in Meraki MX Security Appliance @@ -270,12 +270,6 @@ pipeline: source: log.genericEvent where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && log.merakiGroup=="urls"' # Adding action result - - add: - function: 'string' - params: - key: actionResult - value: 'accepted' - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && log.merakiGroup=="urls"' # ........................................ # Event: flows, IP session initiated in Meraki MX Security Appliance # First variant @@ -342,10 +336,10 @@ pipeline: where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' + value: success where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows") && (startsWith("log.pattern", "0") || startsWith("log.pattern", "allow") || startsWith("log.pattern", "Allow"))' - add: function: 'string' @@ -501,31 +495,31 @@ pipeline: where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events") && (contains("log.genericEvent", "queued due to no phase 1") || contains("log.genericEvent", "queued due to no phase1") || contains("log.genericEvent", "established"))' + value: success + where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "established")' # ........................................ # Event: event spanning-tree guard state change in Meraki MS Switches - grok: patterns: - - fieldName: log.irrelevant - pattern: 'Port' - - fieldName: origin.port - pattern: '{{.integer}}' - - fieldName: log.irrelevant - pattern: 'received an' - - fieldName: protocol - pattern: '{{.data}}BPDU' - - fieldName: log.irrelevant - pattern: 'from' - - fieldName: origin.mac - pattern: '{{.data}}so' - - fieldName: log.irrelevant - pattern: 'the port was' - - fieldName: actionResult - pattern: '{{.greedy}}' + - fieldName: log.irrelevant + pattern: Port + - fieldName: origin.port + pattern: '{{.integer}}' + - fieldName: log.irrelevant + pattern: received an + - fieldName: protocol + pattern: '{{.data}}BPDU' + - fieldName: log.irrelevant + pattern: from + - fieldName: origin.mac + pattern: '{{.data}}so' + - fieldName: log.irrelevant + pattern: the port was + - fieldName: log.merakiResult + pattern: '{{.greedy}}' source: log.genericEvent where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events")' - trim: @@ -853,10 +847,10 @@ pipeline: where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' + value: success where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' # ........................................ # Event: event 802.1X (all events) in Meraki MR Access Points @@ -1008,10 +1002,10 @@ pipeline: where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' + value: success where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' # ........................................ # Event: event wireless packet flood detected in Meraki MR Access Points @@ -1123,32 +1117,32 @@ pipeline: # Event: flows, flow allowed by Layer 3 firewall in Meraki MR Access Points - grok: patterns: - - fieldName: actionResult - pattern: '{{.word}}' - - fieldName: log.irrelevant - pattern: 'src(\s)?=' - - fieldName: origin.ip - pattern: '{{.ipv4}}|{{.ipv6}}' - - fieldName: log.irrelevant - pattern: 'dst(\s)?=' - - fieldName: target.ip - pattern: '{{.ipv4}}|{{.ipv6}}' - - fieldName: log.irrelevant - pattern: 'mac(\s)?=' - - fieldName: origin.mac - pattern: '{{.data}}protocol' - - fieldName: log.irrelevant - pattern: '=' - - fieldName: protocol - pattern: '{{.data}}sport' - - fieldName: log.irrelevant - pattern: '=' - - fieldName: origin.port - pattern: '{{.integer}}' - - fieldName: log.irrelevant - pattern: 'dport(\s)?=' - - fieldName: target.port - pattern: '{{.integer}}' + - fieldName: log.merakiResult + pattern: '{{.word}}' + - fieldName: log.irrelevant + pattern: src(\s)?= + - fieldName: origin.ip + pattern: '{{.ipv4}}|{{.ipv6}}' + - fieldName: log.irrelevant + pattern: dst(\s)?= + - fieldName: target.ip + pattern: '{{.ipv4}}|{{.ipv6}}' + - fieldName: log.irrelevant + pattern: mac(\s)?= + - fieldName: origin.mac + pattern: '{{.data}}protocol' + - fieldName: log.irrelevant + pattern: '=' + - fieldName: protocol + pattern: '{{.data}}sport' + - fieldName: log.irrelevant + pattern: '=' + - fieldName: origin.port + pattern: '{{.integer}}' + - fieldName: log.irrelevant + pattern: dport(\s)?= + - fieldName: target.port + pattern: '{{.integer}}' source: log.genericEvent where: '!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' - trim: @@ -1165,10 +1159,10 @@ pipeline: where: '!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' # Adding action result - add: - function: 'string' + function: string params: key: actionResult - value: 'accepted' + value: success where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "allow") && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' # Adding action result - add: @@ -1388,12 +1382,6 @@ pipeline: - log.vap where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") # Adding action result - - add: - function: 'string' - params: - key: actionResult - value: 'accepted' - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") # ........................................ # Event: security_event ids_alerted, ids signature matched in Meraki MX Security Appliance - grok: @@ -1469,12 +1457,6 @@ pipeline: - protocol where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") # Adding action result - - add: - function: 'string' - params: - key: actionResult - value: 'accepted' - where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") # ............................................................................ # Cleaning common fields - trim: @@ -1541,9 +1523,95 @@ pipeline: - target.port to: int #......................................................................# + # Keep the vendor event group and message available to correlation rules. + - grok: + source: log.merakiGroup + patterns: + - fieldName: log.eventType + pattern: '{{.greedy}}' + where: exists("log.merakiGroup") + - grok: + source: log.genericEvent + patterns: + - fieldName: log.message + pattern: '{{.greedy}}' + where: exists("log.genericEvent") # Removing unused fields - delete: fields: - log.controlFlag - log.irrelevant - - log.genericEvent \ No newline at end of file + - log.genericEvent + + # Normalize explicit outcomes; an unknown outcome remains unset. + - add: + function: string + params: + key: actionResult + value: denied + where: equalsIgnoreCase("log.merakiResult","blocked") + + # 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/cisco-meraki.json b/plugins/alerts/testdata/filter-contracts/cisco-meraki.json new file mode 100644 index 000000000..aa512ec2f --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/cisco-meraki.json @@ -0,0 +1,125 @@ +{ + "technology": "Cisco Meraki", + "filters": [ + "filters/cisco/meraki.yml" + ], + "rules": [ + "rules/cisco/meraki/advanced_malware_protection_alerts.yml", + "rules/cisco/meraki/air_marshal_rogue_ap.yml", + "rules/cisco/meraki/evil_twin_ap_detection.yml", + "rules/cisco/meraki/intrusion_detection_alerts.yml", + "rules/cisco/meraki/meraki_vpn_brute_force.yml", + "rules/cisco/meraki/rogue_ssid_detection.yml", + "rules/cisco/meraki/wireless_intrusion_attempts.yml" + ], + "fixtures": [ + { + "name": "meraki_blocked", + "filter": "cisco/meraki.yml", + "input": { + "log": { + "merakiResult": "blocked" + } + }, + "expected": { + "actionResult": "denied" + }, + "absent": [], + "rules": {} + }, + { + "name": "meraki_allowed", + "filter": "cisco/meraki.yml", + "input": { + "log": { + "merakiResult": "allowed" + } + }, + "expected": {}, + "absent": [ + "actionResult" + ], + "rules": {} + }, + { + "name": "meraki_amp_retrospective", + "filter": "cisco/meraki.yml", + "input": { + "dataSource": "mx-lab", + "log": { + "merakiGroup": "security_event", + "genericEvent": "security_filtering_disposition_change disposition=malicious action=allow" + } + }, + "expected": { + "log.eventType": "security_event", + "log.message": "security_filtering_disposition_change disposition=malicious action=allow" + }, + "absent": [ + "actionResult" + ], + "rules": { + "rules/cisco/meraki/advanced_malware_protection_alerts.yml": true + } + }, + { + "name": "meraki_amp_block", + "filter": "cisco/meraki.yml", + "input": { + "dataSource": "mx-lab", + "log": { + "merakiGroup": "security_event", + "genericEvent": "security_filtering_file_scanned disposition=malicious action=block" + } + }, + "expected": { + "log.eventType": "security_event", + "log.message": "security_filtering_file_scanned disposition=malicious action=block" + }, + "absent": [ + "actionResult" + ], + "rules": { + "rules/cisco/meraki/advanced_malware_protection_alerts.yml": true + } + }, + { + "name": "meraki_amp_clean", + "filter": "cisco/meraki.yml", + "input": { + "dataSource": "mx-lab", + "log": { + "merakiGroup": "security_event", + "genericEvent": "security_filtering_file_scanned disposition=clean action=allow" + } + }, + "expected": { + "log.eventType": "security_event", + "log.message": "security_filtering_file_scanned disposition=clean action=allow" + }, + "absent": [ + "actionResult" + ], + "rules": { + "rules/cisco/meraki/advanced_malware_protection_alerts.yml": false + } + }, + { + "name": "meraki_ids_is_not_amp", + "filter": "cisco/meraki.yml", + "input": { + "dataSource": "mx-lab", + "log": { + "merakiGroup": "security_event", + "genericEvent": "ids_alerted message: malicious traffic blocked" + } + }, + "expected": { + "log.eventType": "security_event" + }, + "rules": { + "rules/cisco/meraki/advanced_malware_protection_alerts.yml": false + } + } + ] +} diff --git a/rules/cisco/meraki/advanced_malware_protection_alerts.yml b/rules/cisco/meraki/advanced_malware_protection_alerts.yml index d134d9c46..5cbe0ce89 100644 --- a/rules/cisco/meraki/advanced_malware_protection_alerts.yml +++ b/rules/cisco/meraki/advanced_malware_protection_alerts.yml @@ -26,13 +26,13 @@ description: | 7. Update endpoint protection signatures and ensure all systems are patched 8. Document the incident and update security policies if needed where: | - equals("log.eventType", "security_event") && - (contains("log.message", "malware") || - contains("log.message", "AMP") || - contains("log.message", "malicious") || + equals("log.eventType", "security_event") && + (((startsWith("log.message", "security_filtering_file_scanned") || + startsWith("log.message", "security_filtering_disposition_change")) && + contains("log.message", "disposition=malicious")) || equals("log.action", "malware_blocked") || - contains("log.eventName", "Advanced Malware Protection")) && - exists("origin.ip") + contains("log.eventName", "Advanced Malware Protection")) groupBy: - - adversary.hostname + - lastEvent.dataSource + - adversary.host - adversary.ip From 7ca5c942a5d63bfdbcbcc703b7515e933dfcd7b5 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Thu, 17 Sep 2026 15:35:10 -0400 Subject: [PATCH 2/2] fix(meraki): parse native events and scope security alerts --- filters/audits/cisco-meraki.md | 167 +- filters/cisco/meraki.yml | 1661 +++++++++++++++-- plugins/alerts/meraki_consumers_test.go | 172 ++ plugins/alerts/meraki_contract_test.go | 486 +++++ plugins/alerts/meraki_history_test.go | 219 +++ .../filter-contracts/cisco-meraki.json | 107 +- plugins/alerts/testdata/meraki_raw.json | 1041 +++++++++++ .../advanced_malware_protection_alerts.yml | 26 +- rules/cisco/meraki/air_marshal_rogue_ap.yml | 5 +- rules/cisco/meraki/evil_twin_ap_detection.yml | 8 +- .../meraki/intrusion_detection_alerts.yml | 9 +- rules/cisco/meraki/meraki_vpn_brute_force.yml | 23 +- rules/cisco/meraki/rogue_ssid_detection.yml | 13 +- .../meraki/wireless_intrusion_attempts.yml | 19 +- 14 files changed, 3621 insertions(+), 335 deletions(-) create mode 100644 plugins/alerts/meraki_consumers_test.go create mode 100644 plugins/alerts/meraki_contract_test.go create mode 100644 plugins/alerts/meraki_history_test.go create mode 100644 plugins/alerts/testdata/meraki_raw.json diff --git a/filters/audits/cisco-meraki.md b/filters/audits/cisco-meraki.md index 83a078614..1fe159456 100644 --- a/filters/audits/cisco-meraki.md +++ b/filters/audits/cisco-meraki.md @@ -1,42 +1,151 @@ -# Cisco Meraki normalization and rule review +# Cisco Meraki parsing and rule contracts -Avoid fabricated success for observations/pending VPN; preserve rule input fields and handle retrospective AMP events without invented source IP. +This source draft targets UTMStack v11 and revises one filter and its seven rules. +The meeting follow-up is based on SDK v1.1.31, pinned by `plugins/alerts/go.mod`, +and the official SDK field and filter/rule documentation. It preserves ingress +`dataSource` and the physical source/destination roles in network events. It does +not deploy configuration, generate customer telemetry or claim that an alert fired. -This draft targets UTMStack `v11`. It contains 1 filter changes -and 1 rule changes for this technology only. Review covered -1 filter configurations and 7 matching shipped rule files. -Unchanged rules are listed in the regression manifest; they are not duplicated in the diff. +## Evidence and its limits -## Contract and validation +The current carrier investigation found nine distinct records without recognizable +Meraki envelopes under `firewall-meraki`: five ISO syslog records from unrelated +programs and four other unrecognized network/syslog records. They lack normalized Meraki fields. The deployed +filter SHA-256 is +`b39216313c9e141d7bb9aef93aafc97f044e482ea7a013bc62ea4bb4ecf64e18`. +Bounded searches over the retained 30-day window found no strong documented +Meraki class signatures. That search is not an exhaustive proof that Meraki +telemetry never arrived. The routing mismatch is a separate ingress-classification issue with its upstream +producer unresolved; +this patch does not reinterpret unrelated syslog as Meraki data. -- 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. -- 6 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. +Private raw records, document IDs and instance provenance remain outside the +repository. All public positive fixtures are fabricated from official Cisco +examples, not customer records. Positive parsing and detection results below are +therefore documented-format compatibility checks, not measured recovery of +customer Meraki events. The reviewed prior source draft is +`db4ae93a194009ca05631245a6316b6c87134f2c`. -The shared alert-contract PR supplies the reusable Go runner for the manifest in -`plugins/alerts/testdata/filter-contracts/cisco-meraki.json`. Apply that support before running `go test ./...` in `plugins/alerts`. +## Producer corrections -The changed rules also require the shared alert-grouping fix to resolve `lastEvent.*` values correctly at runtime. +The old raw header paths require a Cisco calendar-time wrapper. The documented +Meraki native envelope instead starts with fractional epoch, device name and +an event group. The new anchored paths accept that envelope, a bounded RFC3164 +wrapper, and the documented standalone Air Marshal form. Group boundaries are +explicit: device or message text cannot become part of a native event category. +The old greedy group copy was not overwriting an existing subtype; it was the +sole producer of `log.eventType`. -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. +`log.merakiGroup` retains the outer event group and `log.merakiType` retains the +header device name. `log.eventType` exposes the canonical category; nested Air +Marshal messages use `airmarshal_events`. `log.type` holds the events/Air Marshal +classification, while `log.alertType` holds the security-event class. These fields +now match the rule consumers. Class-prefix recognizers are anchored, and quoted +field recovery cannot treat message text as an independent identity or decision. +The full body remains available as `log.message`. -Current Meraki security events retain their group/message for the AMP consumer. Retrospective malicious dispositions do not report a source IP, so the rule does not invent or require one; grouping includes dataSource. Source: https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples +Legacy body handlers remain for event families outside the bounded new handlers; +legacy header/control-field rewrites cannot overwrite a recognized native header. +Representative DHCP, VPN connectivity, cellular status, STP, disassociation and +URL formats have regression fixtures. Not every historical vendor variant has +been exercised, and the model's built-in legacy regex aliases are approximations +of the closed executor's patterns. New native patterns are explicit in YAML. + +Three review controls reproduced false fields before correction: a quoted VPN +fragment produced a peer IP and success, quoted disassociation field names +produced success, and an embedded legacy date header reclassified application +text as a flow. Native VPN/disassociation now consume only their top-level +tokens, and both legacy header patterns require the start of the raw record. +Genuine VPN connectivity fields, full disassociation metadata and both leading +legacy header forms have positive controls. Disassociation timing/radio fields +remain metadata; their presence alone does not establish authentication success. + +| Vendor information | Result | +|---|---| +| Source/destination addresses | Captured and cleaned under vendor fields, validated at promotion, then written to `origin.ip`/`target.ip`. Invalid/unspecified values stay under `log.sourceIp`/`log.destinationIp`; original native `log.src`/`log.dst` also remain. | +| IPv6 and endpoint ports | Valid IPv6 ending in `::` is not truncated by colon cleanup. Semantic zero CIDRs cover alternate and IPv4-mapped unspecified forms. Ports retain numeric bounds before SDK conversion. | +| Flow/firewall decision | Documented `flows`, `firewall`, `cellular_firewall`, `vpn_firewall` and `l7_firewall` groups are recognized. Explicit allow/deny/block becomes success/denied as a policy outcome, not proof a connection completed. | +| IDS / AMP enforcement | IDS `decision=blocked` and AMP scan `action=block` produce denied. Mere IDS observation and retrospective AMP `action=allow` do not manufacture execution success. | +| AnyConnect authentication | Only the documented auth-success/failure classes extract `Peer IP` from their structured message. Generic VPN negotiation or text mentioning failure does not become authentication evidence. | +| AMP hash and URL | Valid 64-hex `log.sha256` is copied to `target.sha256`, describing the file offered by the remote download endpoint; vendor hash is retained. A full URL maps to `target.url`. Threat names such as EICAR classifications are not filenames. | +| 802.1X identity | Documented identity maps to `origin.user`; physical switch interface numbers stay in `log.switchPort`, not transport `origin.port`. | +| Wireless device | Packet-flood device and Air Marshal source/destination MACs retain their documented roles. The observing appliance name is not an attacker hostname. | +| IDS `dhost` | Retained as `log.dhost`; it is no longer unconditionally assigned to the source MAC. Its all-direction endpoint semantics are not established by the available examples. | +| Auxiliary geolocation | Hostnames and zero addresses never enter geolocation. Results use distinct `log.serverGeolocation` / `log.localGeolocation` fields rather than children of scalar address fields. `ip_resp` is response timing and is not geolocated. | + +The SDK uses string fields for outcomes and protocol. This change preserves the +existing protocol conversion policy and uses explicit outcome evidence. Fractional +epoch values remain vendor time metadata; a verified conversion into `deviceTime` +was not established here. Auxiliary geolocation field names and new consumer +aliases require review of saved queries during staging; private dashboards were +not enumerated. + +## Consumers and identities + +- AMP consumes exact documented scan/disposition-change classes and malicious + disposition. Network source remains the downloading client; the rule's + adversary side is the remote target serving the file. This is not proof that + the client executed malware. Retrospective events remain eligible without IPs. +- AMP groups valid hashes using a `hash` namespace. Missing or malformed hashes + retain the vendor value and use the actual ingress `Event.id` in an `event` + namespace, keeping distinct records separate. The filter does not create an + event ID; the input producer supplies it. Neither a usable hash nor an ingress + ID is an explicit completeness negative, not a fabricated file identity. +- IDS accepts high/medium priorities 1 and 2, including documented legacy + `ids-alerts` records containing only a source endpoint. A target IP is not + invented to satisfy a rule. +- Rogue SSID and Air Marshal rules consume the actual Air Marshal class. The + Evil Twin rule requires `ssid_spoofing_detected`, not a generic rogue SSID. + The two rogue rules retain overlapping coverage and the existing RSSI threshold + for team review; primary syslog RSSI units were not established. Cisco documents + removal of these legacy rogue/spoofing messages in MR29 and later. +- Wireless intrusion consumes `device_packet_flood` with `state=start`; an end + event or ordinary message containing attack words is not a new attack trigger. +- VPN uses exact AnyConnect auth failure, valid produced origin IP, and meaningful + ingress/device identities. Its 10-in-15-minute history counts only the filter's + matching failure marker for that source, collector and device. Generic traffic + from the same IP no longer satisfies the history. Input markers are cleared. + +History windows use the SDK's processing-time lower bound on `@timestamp`, not +strict event-time sequencing. Older indexed records have no new candidate marker; +allow the 15-minute history window to warm up when staging the filter and rules +together. Indexed `lastEvent.*` grouping requires the separate alert-foundation +fix; this source draft does not duplicate it. + +## Verification + +`go test ./... -count=1 -v` passes in `plugins/alerts` with the optional private +routing evidence enabled. `git diff --check` also passes. + +- 85 fabricated raw cases exercise all seven rule predicates, native envelopes, + quoted field/class injection, valid and unspecified addresses, documented + legacy event families, decision semantics, hash/ID fallback and new mappings. +- Fourteen isolated auxiliary-geolocation controls test both address fields with + valid IPs, hostnames and alternate unspecified representations. These are + source-step controls, not legacy-envelope parsing proofs. +- Actual SDK CEL and Event/Alert serialization validate consumer identities, + physical endpoint preservation, AMP actor direction and grouping separation. +- Actual SDK history executes against a loopback HTTP mock for the VPN threshold, + count-minus-one/count, inside/expired windows, source/device/collector isolation, + benign raw classes and missing placeholders. It does not query a customer. +- All nine private unrelated/unrecognized records pass as negative routing controls without + invented Meraki identities, outcomes or candidates. +- The shared manifest contains three explicitly isolated normalization/empty + identity controls. It does not prove raw parsing; the standalone raw suite does + that within its stated model boundary. +- The final shared-runner overlay passes 121 test/subtest records, with the + optional private replay skipped there and run separately with its evidence. + +The closed EventProcessor, live OpenSearch, dynamic geolocation lookup and alert +publication are not run by this harness. No production false-positive reduction +has been measured. Before production approval, stage actual native Meraki payloads +through the collector/engine, inspect produced alerts and grouping, check parsing +cost at realistic event rates, and investigate the separate carrier-routing issue. ## References -- [SDK schema](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) +- [Official Meraki syslog formats and examples](https://documentation.meraki.com/Platform_Management/Dashboard_Administration/Operate_and_Maintain/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples) +- [SDK v1.1.31 schema](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) +- [Standard event semantics](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) - [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/cisco/meraki.yml b/filters/cisco/meraki.yml index 3990db7e3..12f8a6539 100644 --- a/filters/cisco/meraki.yml +++ b/filters/cisco/meraki.yml @@ -1,19 +1,98 @@ -# CISCO Meraki filter, version 3.0.3 +# CISCO Meraki filter, version 3.1.0 # Based on https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples # https://documentation.cysiv.com/articles/#!data-source-onboarding-device-configuration-reference/cisco-meraki-reference-information-and-cim-mapping # # Filter Input requirements -> Syslog # # 1. Parsing the message field +patterns: + merakiKVPrefix: ^(?:[A-Za-z][A-Za-z0-9_-]*\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s"']*)\s+)*? + merakiKVValue: (?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s"']+) pipeline: - dataTypes: - firewall-meraki steps: # Header fields + # Documented native envelopes; never infer Meraki from arbitrary message text. - grok: + source: raw + patterns: + - fieldName: '' + pattern: ^(?:<[0-9]{1,3}>)? + - fieldName: log.msgEventTime + pattern: '[0-9]+\.[0-9]+' + - fieldName: '' + pattern: \s+ + - fieldName: log.merakiType + pattern: \S+ + - fieldName: '' + pattern: \s+ + - fieldName: log.merakiGroup + pattern: (?:events|flows|firewall|cellular_firewall|vpn_firewall|l7_firewall|urls|ids-alerts|security_event|airmarshal_events) + - fieldName: '' + pattern: \s+ + - fieldName: log.genericEvent + pattern: (?s:.*) + - grok: + source: raw + patterns: + - fieldName: '' + pattern: ^(?:<[0-9]{1,3}>)?[A-Z][a-z]{2}\s+[0-9]{1,2}\s+(?:[0-9]{4}\s+)?[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?\s+\S+\s+(?:[0-9]+\s+)? + - fieldName: log.msgEventTime + pattern: '[0-9]+\.[0-9]+' + - fieldName: '' + pattern: \s+ + - fieldName: log.merakiType + pattern: \S+ + - fieldName: '' + pattern: \s+ + - fieldName: log.merakiGroup + pattern: (?:events|flows|firewall|cellular_firewall|vpn_firewall|l7_firewall|urls|ids-alerts|security_event|airmarshal_events) + - fieldName: '' + pattern: \s+ + - fieldName: log.genericEvent + pattern: (?s:.*) + - grok: + source: raw + patterns: + - fieldName: '' + pattern: ^(?:<[0-9]{1,3}>)? + - fieldName: log.merakiGroup + pattern: airmarshal_events + - fieldName: '' + pattern: \s+ + - fieldName: log.genericEvent + pattern: (?s:.*) + - add: + function: string + params: + key: log.nativeFormat + value: 'true' + where: exists("log.genericEvent") && exists("log.merakiGroup") + - add: + function: string + params: + key: log.controlFlag + value: Native + where: equals("log.nativeFormat", "true") + - add: + function: string + params: + key: log.nativeHandled + value: 'true' + where: equals("log.nativeFormat", "true") && !(equals("log.merakiGroup", "events") && regexMatch("log.genericEvent", "^(dhcp (lease|no offers)[ + ]|Cellular connection[ ]|failover to[ ]|Site-to-site VPN:|Port[ ]|Blocked DHCP server[ ]|type=(splash_auth)([ ]|$))")) + - grok: + source: log.genericEvent + patterns: + - fieldName: log.nativeMessage + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") + - grok: + where: '!equals("log.nativeFormat", "true")' patterns: - fieldName: log.irrelevant - pattern: '(\<{{.integer}}\>)' + pattern: '^(\<{{.integer}}\>)' - fieldName: log.ciscoTime pattern: '{{.monthName}}\s{{.monthDay}}\s{{.year}}\s{{.time}}' - fieldName: log.serverIp @@ -22,9 +101,10 @@ pipeline: pattern: '{{.greedy}}' source: raw - grok: + where: '!equals("log.nativeFormat", "true")' patterns: - fieldName: log.ciscoTime - pattern: '{{.monthName}}\s{{.monthDay}}\s{{.year}}\s{{.time}}' + pattern: '^{{.monthName}}\s{{.monthDay}}\s{{.year}}\s{{.time}}' - fieldName: log.serverIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}(\s)?\:' - fieldName: log.msg @@ -32,6 +112,7 @@ pipeline: source: raw # Adding control log.controlFlag field (Used to know if the message don't contains the header INT.INT mx_type mx_group) - add: + where: '!equals("log.nativeFormat", "true")' function: 'string' params: key: log.controlFlag @@ -39,6 +120,7 @@ pipeline: # Begin message parsing # First extract the generic message, then parse it - grok: + where: '!equals("log.nativeFormat", "true")' patterns: - fieldName: log.controlFlag pattern: '{{.data}}(\s)(events|flows|urls|ids-alerts|security_event)' @@ -62,7 +144,7 @@ pipeline: - fieldName: log.merakiGroup pattern: '{{.greedy}}' source: log.controlFlag - where: '!equals("log.controlFlag", "Init")' + where: '!equals("log.nativeFormat", "true") && (!equals("log.controlFlag", "Init"))' - grok: patterns: - fieldName: log.msgEventTime @@ -72,7 +154,7 @@ pipeline: - fieldName: log.merakiGroup pattern: '{{.greedy}}' source: log.controlFlag - where: '!equals("log.controlFlag", "Init")' + where: '!equals("log.nativeFormat", "true") && (!equals("log.controlFlag", "Init"))' # ........................................ # event vpn connectivity change - grok: @@ -87,7 +169,7 @@ pipeline: pattern: '''{{.data}}''' - fieldName: log.irrelevant pattern: 'peer_contact(\s)?=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '''{{.data}}\:' - fieldName: origin.port pattern: '{{.integer}}''' @@ -100,21 +182,21 @@ pipeline: - fieldName: log.connectivity pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init"))' # Adding action result - add: function: string params: key: actionResult value: success - where: equals("log.connectivity", "true") + where: '!equals("log.nativeHandled", "true") && (equals("log.connectivity", "true"))' # ........................................ # event uplink connectivity change - rename: from: - log.genericEvent to: action - where: '!equals("log.controlFlag", "Init") && contains("log.genericEvent", "failover to")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("log.genericEvent", "failover to"))' # ........................................ # event uplink connectivity change # Event: urls, HTTP GET requests in MR Access Points @@ -122,7 +204,7 @@ pipeline: from: - log.genericEvent to: action - where: '!equals("log.controlFlag", "Init") && contains("log.genericEvent", "Cellular connection")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("log.genericEvent", "Cellular connection"))' - grok: patterns: - fieldName: action @@ -137,13 +219,13 @@ pipeline: pattern: '(events|flows|urls|ids-alerts|security_event)' - fieldName: log.irrelevant pattern: 'src(\s)?=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.data}}\:' - fieldName: origin.port pattern: '{{.integer}}' - fieldName: log.irrelevant pattern: 'dst(\s)?=' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.data}}\:' - fieldName: target.port pattern: '{{.integer}}' @@ -158,7 +240,7 @@ pipeline: - fieldName: target.url pattern: '{{.greedy}}' source: action - where: '!equals("log.controlFlag", "Init") && contains("action", "Cellular connection")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("action", "Cellular connection"))' - grok: patterns: - fieldName: action @@ -166,14 +248,14 @@ pipeline: - fieldName: log.connectionState pattern: '(up|Up|down|Down)' source: action - where: '!equals("log.controlFlag", "Init") && contains("action", "Cellular connection")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("action", "Cellular connection"))' # Adding action result - add: function: string params: key: actionResult value: success - where: equalsIgnoreCase("log.connectionState", "up") + where: '!equals("log.nativeHandled", "true") && (equalsIgnoreCase("log.connectionState", "up"))' # ........................................ # Event: event, dhcp no offers - grok: @@ -184,23 +266,23 @@ pipeline: pattern: '{{.data}}\s' - fieldName: log.irrelevant pattern: 'host(\s)?=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp no offers")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp no offers"))' - trim: function: suffix substring: 'for mac' fields: - action - where: '!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp no offers")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp no offers"))' # ........................................ # Event: event, dhcp lease - grok: patterns: - fieldName: action pattern: '{{.data}}of\sip' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: log.irrelevant pattern: 'from server mac' @@ -227,33 +309,33 @@ pipeline: - fieldName: log.dns1 pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp lease")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp lease"))' - trim: function: suffix substring: 'of ip' fields: - action - where: '!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp lease")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && contains("log.genericEvent", "dhcp lease"))' # Adding action result - add: function: string params: key: actionResult value: success - where: contains("log.genericEvent", "dhcp lease") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "dhcp lease"))' # ........................................ # Event: event, HTTP GET requests in Meraki MX Security Appliance - grok: patterns: - fieldName: log.irrelevant pattern: 'src(\s)?=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.data}}\:' - fieldName: origin.port pattern: '{{.integer}}' - fieldName: log.irrelevant pattern: 'dst(\s)?=' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.data}}\:' - fieldName: target.port pattern: '{{.integer}}' @@ -268,7 +350,7 @@ pipeline: - fieldName: target.url pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && log.merakiGroup=="urls"' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && log.merakiGroup=="urls")' # Adding action result # ........................................ # Event: flows, IP session initiated in Meraki MX Security Appliance @@ -277,11 +359,11 @@ pipeline: patterns: - fieldName: log.irrelevant pattern: 'src(\s)?=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: log.irrelevant pattern: 'dst(\s)?=' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: log.irrelevant pattern: 'mac(\s)?=' @@ -304,17 +386,17 @@ pipeline: - fieldName: log.pattern pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows"))' # Second variant - grok: patterns: - fieldName: log.irrelevant pattern: 'src(\s)?=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: log.irrelevant pattern: 'dst(\s)?=' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: log.irrelevant pattern: 'protocol(\s)?=' @@ -333,20 +415,20 @@ pipeline: - fieldName: log.pattern pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows"))' # Adding action result - add: function: string params: key: actionResult value: success - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows") && (startsWith("log.pattern", "0") || startsWith("log.pattern", "allow") || startsWith("log.pattern", "Allow"))' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows") && (startsWith("log.pattern", "0") || startsWith("log.pattern", "allow") || startsWith("log.pattern", "Allow")))' - add: function: 'string' params: key: actionResult value: 'denied' - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows") && (startsWith("log.pattern", "deny") || startsWith("log.pattern", "Deny"))' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "src") && equals("log.merakiGroup", "flows") && (startsWith("log.pattern", "deny") || startsWith("log.pattern", "Deny")))' # ........................................ # Event: ids-alerts, ids signature matched in Meraki MX Security Appliance - grok: @@ -373,18 +455,18 @@ pipeline: pattern: '{{.data}}(\s)' - fieldName: log.irrelevant pattern: 'src(\s)?=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.data}}\:' - fieldName: origin.port pattern: '{{.integer}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "signature") && log.merakiGroup=="ids-alerts"' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "signature") && log.merakiGroup=="ids-alerts")' - trim: function: suffix substring: 'priority' fields: - log.signature - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "signature") && log.merakiGroup=="ids-alerts"' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "signature") && log.merakiGroup=="ids-alerts")' # ........................................ # Event: event IPsec-SA request queued due to no phase 1 found, phase2 negotiation failed, initiate new phase 1 negotiation, # ISAKMP-SA established, initiate new phase 2 negotiation, IPsec-SA established, ISAKMP-SA established, and ISAKMP-SA deleted @@ -395,13 +477,13 @@ pipeline: patterns: - fieldName: log.irrelevant pattern: 'Site-to-site VPN(\s)?\:(\s)?{{.greedy}}(\s)(deleted|established)' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: origin.port pattern: '\[(\s)?{{.integer}}(\s)?\]' - fieldName: log.irrelevant pattern: '-' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: target.port pattern: '\[(\s)?{{.integer}}(\s)?\]' @@ -410,18 +492,18 @@ pipeline: - fieldName: log.spi pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events"))' # Second variant - grok: patterns: - fieldName: log.irrelevant pattern: 'Site-to-site VPN(\s)?\:(\s)?{{.greedy}}(\s)request(\s)for' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: log.irrelevant pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events"))' # Third variant - grok: patterns: @@ -429,47 +511,47 @@ pipeline: pattern: 'Site-to-site VPN(\s)?\:(\s)?{{.greedy}}(\s)negotiation(\s)failed' - fieldName: log.irrelevant pattern: '{{.data}}ESP' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: origin.port pattern: '\[(\s)?{{.integer}}(\s)?\]' - fieldName: log.irrelevant pattern: '-\>' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: target.port pattern: '\[(\s)?{{.integer}}(\s)?\]' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events"))' # Fourth variant - grok: patterns: - fieldName: log.irrelevant pattern: 'Site-to-site VPN(\s)?\:(\s)?{{.greedy}}(\s)negotiation(\s)?\:' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: origin.port pattern: '\[(\s)?{{.integer}}(\s)?\]' - fieldName: log.irrelevant pattern: '\<=\>' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: target.port pattern: '\[(\s)?{{.integer}}(\s)?\]' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events"))' # Fifth variant - grok: patterns: - fieldName: log.irrelevant pattern: 'Site-to-site VPN(\s)?\:(\s)?{{.greedy}}(\s)established\:(\s)?ESP/Tunnel' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: origin.port pattern: '\[(\s)?{{.integer}}(\s)?\]' - fieldName: log.irrelevant pattern: '-\>' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}' - fieldName: target.port pattern: '\[(\s)?{{.integer}}(\s)?\]' @@ -478,35 +560,35 @@ pipeline: - fieldName: log.spi pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events"))' - trim: function: prefix substring: '[' fields: - origin.port - target.port - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events"))' - trim: function: suffix substring: ']' fields: - origin.port - target.port - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events"))' # Adding action result - add: function: string params: key: actionResult value: success - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "established")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Site") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "established"))' # ........................................ # Event: event spanning-tree guard state change in Meraki MS Switches - grok: patterns: - fieldName: log.irrelevant pattern: Port - - fieldName: origin.port + - fieldName: log.switchPort pattern: '{{.integer}}' - fieldName: log.irrelevant pattern: received an @@ -521,26 +603,26 @@ pipeline: - fieldName: log.merakiResult pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events"))' - trim: function: suffix substring: 'BPDU' fields: - protocol - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events"))' - trim: function: suffix substring: 'so' fields: - origin.mac - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events"))' # Adding action result - add: function: 'string' params: key: actionResult value: 'denied' - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "blocked")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Port") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "blocked"))' # ........................................ # Event: event blocked DHCP server response in Meraki MS Switches - grok: @@ -558,19 +640,19 @@ pipeline: - fieldName: log.vlan pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Blocked") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Blocked") && equals("log.merakiGroup", "events"))' - trim: function: suffix substring: 'server' fields: - protocol - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Blocked") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Blocked") && equals("log.merakiGroup", "events"))' - trim: function: suffix substring: 'on' fields: - origin.mac - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Blocked") && equals("log.merakiGroup", "events")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "Blocked") && equals("log.merakiGroup", "events"))' # ........................................ # Event: event 802.1X (all events) in Meraki MS Switches - grok: @@ -581,34 +663,34 @@ pipeline: pattern: '{{.data}}port' - fieldName: log.irrelevant pattern: '=' - - fieldName: origin.port + - fieldName: log.switchPort pattern: '{{.data}}identity' - fieldName: log.irrelevant pattern: '=' - fieldName: log.identity pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port"))' - trim: function: suffix substring: 'port' fields: - action - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port"))' - trim: function: prefix substring: '''' fields: - - origin.port + - log.switchPort - log.identity - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port"))' - trim: function: suffix substring: '''' fields: - - origin.port + - log.switchPort - log.identity - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "port"))' # ........................................ # Event: event 802.11 disassociation in Meraki MR Access Points - grok: @@ -663,7 +745,7 @@ pipeline: pattern: '{{.data}}ip_src' - fieldName: log.irrelevant pattern: '=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.data}}arp_resp' - fieldName: log.irrelevant pattern: '=' @@ -690,115 +772,115 @@ pipeline: - fieldName: log.aid pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'radio' fields: - action - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'vap' fields: - log.radio - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'channel' fields: - log.vap - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'reason' fields: - log.channel - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'instigator' fields: - log.reason - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'duration' fields: - log.instigator - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'auth_neg_dur' fields: - log.duration - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'last_auth_ago' fields: - log.authNegDur - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'is_wpa' fields: - log.lastAuthAgo - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'full_conn' fields: - log.isWpa - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'ip_resp' fields: - log.fullConn - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'ip_src' fields: - log.ipResp - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'arp_resp' fields: - - origin.ip - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + - log.sourceIp + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'arp_src' fields: - log.arpResp - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'dns_server' fields: - log.arpSrc - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'dns_req_rtt' fields: - log.dnsServer - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'dns_resp' fields: - log.dnsReqRtt - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: 'aid' fields: - log.dnsResp - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: prefix substring: '''' @@ -814,14 +896,14 @@ pipeline: - log.isWpa - log.fullConn - log.ipResp - - origin.ip + - log.sourceIp - log.arpResp - log.arpSrc - log.dnsServer - log.dnsReqRtt - log.dnsResp - log.aid - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' - trim: function: suffix substring: '''' @@ -837,21 +919,21 @@ pipeline: - log.isWpa - log.fullConn - log.ipResp - - origin.ip + - log.sourceIp - log.arpResp - log.arpSrc - log.dnsServer - log.dnsReqRtt - log.dnsResp - log.aid - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' # Adding action result - add: function: string params: key: actionResult value: success - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "channel") && contains("log.genericEvent", "auth_neg_dur") && contains("log.genericEvent", "last_auth_ago"))' # ........................................ # Event: event 802.1X (all events) in Meraki MR Access Points - grok: @@ -877,31 +959,31 @@ pipeline: - fieldName: log.aid pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid"))' - trim: function: suffix substring: 'radio' fields: - action - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid"))' - trim: function: suffix substring: 'vap' fields: - log.radio - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid"))' - trim: function: suffix substring: 'identity' fields: - log.vap - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid"))' - trim: function: suffix substring: 'aid' fields: - log.identity - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid"))' - trim: function: prefix substring: '''' @@ -910,7 +992,7 @@ pipeline: - log.vap - log.identity - log.aid - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid"))' - trim: function: suffix substring: '''' @@ -919,7 +1001,7 @@ pipeline: - log.vap - log.identity - log.aid - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "radio") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "identity") && contains("log.genericEvent", "aid"))' # ........................................ # Event: event splash authentication in Meraki MR Access Points - grok: @@ -930,7 +1012,7 @@ pipeline: pattern: '{{.data}}ip' - fieldName: log.irrelevant pattern: '=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.data}}duration' - fieldName: log.irrelevant pattern: '=' @@ -949,64 +1031,64 @@ pipeline: - fieldName: log.upload pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' - trim: function: suffix substring: 'ip' fields: - action - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' - trim: function: suffix substring: 'duration' fields: - - origin.ip - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + - log.sourceIp + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' - trim: function: suffix substring: 'vap' fields: - log.duration - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' - trim: function: suffix substring: 'download' fields: - log.vap - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' - trim: function: suffix substring: 'upload' fields: - log.download - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' - trim: function: prefix substring: '''' fields: - - origin.ip + - log.sourceIp - log.vap - log.duration - log.download - log.upload - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' - trim: function: suffix substring: '''' fields: - - origin.ip + - log.sourceIp - log.vap - log.duration - log.download - log.upload - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' # Adding action result - add: function: string params: key: actionResult value: success - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "ip") && contains("log.genericEvent", "vap") && contains("log.genericEvent", "duration") && contains("log.genericEvent", "download") && contains("log.genericEvent", "upload"))' # ........................................ # Event: event wireless packet flood detected in Meraki MR Access Points - grok: @@ -1044,54 +1126,54 @@ pipeline: - fieldName: log.interArrival pattern: '{{.greedy}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: 'packet' fields: - action - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: 'device' fields: - log.packet - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: 'radio' fields: - origin.mac - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: 'state' fields: - log.radio - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: 'alarm_id' fields: - log.state - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: 'dos_count' fields: - log.alarmId - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: 'inter_arrival' fields: - log.dosCount - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: prefix substring: '''' fields: - - origin.ip + - log.sourceIp - origin.mac - log.radio - log.packet @@ -1099,12 +1181,12 @@ pipeline: - log.alarmId - log.state - log.interArrival - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' - trim: function: suffix substring: '''' fields: - - origin.ip + - log.sourceIp - origin.mac - log.radio - log.packet @@ -1112,7 +1194,7 @@ pipeline: - log.alarmId - log.state - log.interArrival - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "type") && equals("log.merakiGroup", "events") && contains("log.genericEvent", "packet") && contains("log.genericEvent", "device") && contains("log.genericEvent", "state") && contains("log.genericEvent", "alarm_id") && contains("log.genericEvent", "dos_count"))' # ........................................ # Event: flows, flow allowed by Layer 3 firewall in Meraki MR Access Points - grok: @@ -1121,11 +1203,11 @@ pipeline: pattern: '{{.word}}' - fieldName: log.irrelevant pattern: src(\s)?= - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}' - fieldName: log.irrelevant pattern: dst(\s)?= - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}' - fieldName: log.irrelevant pattern: mac(\s)?= @@ -1144,33 +1226,33 @@ pipeline: - fieldName: target.port pattern: '{{.integer}}' source: log.genericEvent - where: '!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac"))' - trim: function: suffix substring: 'sport' fields: - protocol - where: '!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac"))' - trim: function: suffix substring: 'protocol' fields: - origin.mac - where: '!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && (startsWith("log.genericEvent", "allow") || startsWith("log.genericEvent", "deny")) && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac"))' # Adding action result - add: function: string params: key: actionResult value: success - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "allow") && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "allow") && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac"))' # Adding action result - add: function: 'string' params: key: actionResult value: 'denied' - where: '!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "deny") && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac")' + where: '!equals("log.nativeHandled", "true") && (!equals("log.controlFlag", "Init") && startsWith("log.genericEvent", "deny") && equals("log.merakiGroup", "flows") && contains("log.genericEvent", "src") && contains("log.genericEvent", "dst") && contains("log.genericEvent", "sport") && contains("log.genericEvent", "dport") && contains("log.genericEvent", "mac"))' # ........................................ # Event: event rogue SSID detected and SSID spoofing detected in Meraki MR Access Points # First variant @@ -1219,7 +1301,7 @@ pipeline: - fieldName: log.fcSubType pattern: '{{.greedy}}' source: log.msg - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' # Second variant - grok: patterns: @@ -1262,91 +1344,91 @@ pipeline: - fieldName: log.fcSubType pattern: '{{.greedy}}' source: log.msg - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: '=' fields: - action - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'ssid' fields: - action - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'bssid' fields: - log.ssid - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'vap' fields: - log.ssid - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'bssid' fields: - log.vap - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'src' fields: - log.bssid - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'dst' fields: - origin.mac - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'wired_mac' fields: - target.mac - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'channel' fields: - target.mac - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'vlan_id' fields: - log.wiredMac - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'channel' fields: - log.vlanId - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'rssi' fields: - log.channel - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'fc_type' fields: - log.rssi - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: 'fc_subtype' fields: - log.fcType - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: prefix substring: '''' @@ -1363,7 +1445,7 @@ pipeline: - log.fcType - log.fcSubType - log.vap - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' - trim: function: suffix substring: '''' @@ -1380,7 +1462,7 @@ pipeline: - log.fcType - log.fcSubType - log.vap - where: startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "airmarshal_events") && contains("log.genericEvent", "type") && contains("log.genericEvent", "ssid") && contains("log.genericEvent", "bssid"))' # Adding action result # ........................................ # Event: security_event ids_alerted, ids signature matched in Meraki MX Security Appliance @@ -1400,7 +1482,7 @@ pipeline: pattern: '{{.data}}dhost' - fieldName: log.irrelevant pattern: '=' - - fieldName: origin.mac + - fieldName: log.dhost pattern: '{{.data}}direction' - fieldName: log.irrelevant pattern: '=' @@ -1412,66 +1494,1207 @@ pipeline: pattern: '{{.data}}src' - fieldName: log.irrelevant pattern: '=' - - fieldName: origin.ip + - fieldName: log.sourceIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}\:' - fieldName: origin.port pattern: '{{.integer}}' - fieldName: log.irrelevant pattern: 'dst(\s)?=' - - fieldName: target.ip + - fieldName: log.destinationIp pattern: '{{.ipv4}}|{{.ipv6}}|{{.hostname}}\:' - fieldName: target.port pattern: '{{.integer}}' - fieldName: log.irrelevant pattern: '{{.greedy}}' source: log.msg - where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost"))' - trim: function: suffix substring: 'priority' fields: - log.signature - where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost"))' - trim: function: suffix substring: 'dhost' fields: - log.msgEventTime - where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost"))' - trim: function: suffix substring: 'direction' fields: - - origin.mac - where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") + - log.dhost + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost"))' - trim: function: suffix substring: 'protocol' fields: - log.direction - where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost"))' - trim: function: suffix substring: 'src' fields: - protocol - where: startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost") + where: '!equals("log.nativeHandled", "true") && (startsWith("log.genericEvent", "signature") && contains("log.genericEvent", "priority") && contains("log.genericEvent", "timestamp") && contains("log.genericEvent", "direction") && contains("log.genericEvent", "dhost"))' # Adding action result # ............................................................................ # Cleaning common fields + # Native authoritative fields, independent of the legacy capture layout. + - grok: + source: log.nativeMessage + patterns: + - fieldName: log.genericEvent + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") + - grok: + source: log.merakiGroup + patterns: + - fieldName: log.eventType + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") + - grok: + source: log.genericEvent + patterns: + - fieldName: log.fields + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") + - grok: + source: log.genericEvent + patterns: + - fieldName: log.message + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") + - grok: + source: log.genericEvent + patterns: + - fieldName: log.alertType + pattern: ^(?:ids_alerted|security_filtering_file_scanned|security_filtering_disposition_change) + - fieldName: '' + pattern: \s+ + - fieldName: log.fields + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "security_event") + - add: + function: string + params: + key: log.alertType + value: ids_alerted + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "ids-alerts") + - grok: + source: log.genericEvent + patterns: + - fieldName: log.eventType + pattern: ^airmarshal_events + - fieldName: '' + pattern: \s+ + - fieldName: log.fields + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") + - grok: + source: log.genericEvent + patterns: + - fieldName: log.merakiResult + pattern: ^(?:allow|deny) + - fieldName: '' + pattern: \s+ + - fieldName: log.fields + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall"]) + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}type\s*=\s*' + - fieldName: log.type + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}src\s*=\s*' + - fieldName: log.src + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}dst\s*=\s*' + - fieldName: log.dst + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}mac\s*=\s*' + - fieldName: log.mac + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}protocol\s*=\s*' + - fieldName: log.protocol + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}sport\s*=\s*' + - fieldName: log.sport + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}dport\s*=\s*' + - fieldName: log.dport + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}signature\s*=\s*' + - fieldName: log.signature + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}priority\s*=\s*' + - fieldName: log.priority + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}direction\s*=\s*' + - fieldName: log.direction + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}dhost\s*=\s*' + - fieldName: log.dhost + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}decision\s*=\s*' + - fieldName: log.decision + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}action\s*=\s*' + - fieldName: log.action + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}url\s*=\s*' + - fieldName: log.url + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}name\s*=\s*' + - fieldName: log.name + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}sha256\s*=\s*' + - fieldName: log.sha256 + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}disposition\s*=\s*' + - fieldName: log.disposition + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}ssid\s*=\s*' + - fieldName: log.ssid + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}bssid\s*=\s*' + - fieldName: log.bssid + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}rssi\s*=\s*' + - fieldName: log.rssi + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}packet\s*=\s*' + - fieldName: log.packet + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}device\s*=\s*' + - fieldName: log.device + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}state\s*=\s*' + - fieldName: log.state + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}alarm_id\s*=\s*' + - fieldName: log.alarmId + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}dos_count\s*=\s*' + - fieldName: log.dosCount + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}msg\s*=\s*' + - fieldName: log.msg + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}identity\s*=\s*' + - fieldName: log.identity + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}port\s*=\s*' + - fieldName: log.port + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}ip_src\s*=\s*' + - fieldName: log.ip_src + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}ip_resp\s*=\s*' + - fieldName: log.ipResp + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}vpn_type\s*=\s*' + - fieldName: log.vpnType + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}peer_contact\s*=\s*' + - fieldName: log.peerContact + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}peer_ident\s*=\s*' + - fieldName: log.peerIdent + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}connectivity\s*=\s*' + - fieldName: log.connectivity + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}radio\s*=\s*' + - fieldName: log.radio + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}vap\s*=\s*' + - fieldName: log.vap + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}channel\s*=\s*' + - fieldName: log.channel + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}reason\s*=\s*' + - fieldName: log.reason + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}instigator\s*=\s*' + - fieldName: log.instigator + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}duration\s*=\s*' + - fieldName: log.duration + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}auth_neg_dur\s*=\s*' + - fieldName: log.authNegDur + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}last_auth_ago\s*=\s*' + - fieldName: log.lastAuthAgo + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}is_wpa\s*=\s*' + - fieldName: log.isWpa + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}full_conn\s*=\s*' + - fieldName: log.fullConn + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}arp_resp\s*=\s*' + - fieldName: log.arpResp + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}arp_src\s*=\s*' + - fieldName: log.arpSrc + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}dns_server\s*=\s*' + - fieldName: log.dnsServer + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}dns_req_rtt\s*=\s*' + - fieldName: log.dnsReqRtt + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}dns_resp\s*=\s*' + - fieldName: log.dnsResp + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}aid\s*=\s*' + - fieldName: log.aid + pattern: '{{.merakiKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + where: equals("log.nativeFormat", "true") + - trim: + function: prefix + substring: '"' + fields: + - log.type + - log.src + - log.dst + - log.mac + - log.protocol + - log.sport + - log.dport + - log.signature + - log.priority + - log.direction + - log.dhost + - log.decision + - log.action + - log.url + - log.name + - log.sha256 + - log.disposition + - log.ssid + - log.bssid + - log.rssi + - log.packet + - log.device + - log.state + - log.alarmId + - log.dosCount + - log.msg + - log.identity + - log.port + - log.ip_src + - log.ipResp + - log.vpnType + - log.peerContact + - log.peerIdent + - log.connectivity + - log.radio + - log.vap + - log.channel + - log.reason + - log.instigator + - log.duration + - log.authNegDur + - log.lastAuthAgo + - log.isWpa + - log.fullConn + - log.arpResp + - log.arpSrc + - log.dnsServer + - log.dnsReqRtt + - log.dnsResp + - log.aid + where: equals("log.nativeFormat", "true") + - trim: + function: suffix + substring: '"' + fields: + - log.type + - log.src + - log.dst + - log.mac + - log.protocol + - log.sport + - log.dport + - log.signature + - log.priority + - log.direction + - log.dhost + - log.decision + - log.action + - log.url + - log.name + - log.sha256 + - log.disposition + - log.ssid + - log.bssid + - log.rssi + - log.packet + - log.device + - log.state + - log.alarmId + - log.dosCount + - log.msg + - log.identity + - log.port + - log.ip_src + - log.ipResp + - log.vpnType + - log.peerContact + - log.peerIdent + - log.connectivity + - log.radio + - log.vap + - log.channel + - log.reason + - log.instigator + - log.duration + - log.authNegDur + - log.lastAuthAgo + - log.isWpa + - log.fullConn + - log.arpResp + - log.arpSrc + - log.dnsServer + - log.dnsReqRtt + - log.dnsResp + - log.aid + where: equals("log.nativeFormat", "true") + - trim: + function: prefix + substring: '''' + fields: + - log.type + - log.src + - log.dst + - log.mac + - log.protocol + - log.sport + - log.dport + - log.signature + - log.priority + - log.direction + - log.dhost + - log.decision + - log.action + - log.url + - log.name + - log.sha256 + - log.disposition + - log.ssid + - log.bssid + - log.rssi + - log.packet + - log.device + - log.state + - log.alarmId + - log.dosCount + - log.msg + - log.identity + - log.port + - log.ip_src + - log.ipResp + - log.vpnType + - log.peerContact + - log.peerIdent + - log.connectivity + - log.radio + - log.vap + - log.channel + - log.reason + - log.instigator + - log.duration + - log.authNegDur + - log.lastAuthAgo + - log.isWpa + - log.fullConn + - log.arpResp + - log.arpSrc + - log.dnsServer + - log.dnsReqRtt + - log.dnsResp + - log.aid + where: equals("log.nativeFormat", "true") + - trim: + function: suffix + substring: '''' + fields: + - log.type + - log.src + - log.dst + - log.mac + - log.protocol + - log.sport + - log.dport + - log.signature + - log.priority + - log.direction + - log.dhost + - log.decision + - log.action + - log.url + - log.name + - log.sha256 + - log.disposition + - log.ssid + - log.bssid + - log.rssi + - log.packet + - log.device + - log.state + - log.alarmId + - log.dosCount + - log.msg + - log.identity + - log.port + - log.ip_src + - log.ipResp + - log.vpnType + - log.peerContact + - log.peerIdent + - log.connectivity + - log.radio + - log.vap + - log.channel + - log.reason + - log.instigator + - log.duration + - log.authNegDur + - log.lastAuthAgo + - log.isWpa + - log.fullConn + - log.arpResp + - log.arpSrc + - log.dnsServer + - log.dnsReqRtt + - log.dnsResp + - log.aid + where: equals("log.nativeFormat", "true") + - grok: + source: log.sha256 + patterns: + - fieldName: target.sha256 + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "security_event") && oneOf("log.alertType", ["security_filtering_file_scanned", + "security_filtering_disposition_change"]) && regexMatch("log.sha256", "(?i)^[0-9a-f]{64}$") + - grok: + source: log.src + patterns: + - fieldName: log.sourceIp + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall", "l7_firewall"]) + && exists("log.src") + - grok: + source: log.src + patterns: + - fieldName: log.sourceIp + pattern: (?:\[[^\]]+\]|[^\s]+) + - fieldName: '' + pattern: ':' + - fieldName: origin.port + pattern: '[0-9]+' + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && (oneOf("log.merakiGroup", ["urls", "ids-alerts"]) || (equals("log.merakiGroup", "security_event") + && oneOf("log.alertType", ["ids_alerted", "security_filtering_file_scanned"]))) && exists("log.src") + - trim: + function: prefix + substring: '[' + fields: + - log.sourceIp + where: equals("log.nativeFormat", "true") + - trim: + function: suffix + substring: ']' + fields: + - log.sourceIp + where: equals("log.nativeFormat", "true") + - grok: + source: log.dst + patterns: + - fieldName: log.destinationIp + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall", "l7_firewall"]) + && exists("log.dst") + - grok: + source: log.dst + patterns: + - fieldName: log.destinationIp + pattern: (?:\[[^\]]+\]|[^\s]+) + - fieldName: '' + pattern: ':' + - fieldName: target.port + pattern: '[0-9]+' + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && (oneOf("log.merakiGroup", ["urls", "ids-alerts"]) || (equals("log.merakiGroup", "security_event") + && oneOf("log.alertType", ["ids_alerted", "security_filtering_file_scanned"]))) && exists("log.dst") + - trim: + function: prefix + substring: '[' + fields: + - log.destinationIp + where: equals("log.nativeFormat", "true") + - trim: + function: suffix + substring: ']' + fields: + - log.destinationIp + where: equals("log.nativeFormat", "true") + - grok: + source: log.sport + patterns: + - fieldName: origin.port + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall", "l7_firewall"]) + && regexMatch("log.sport", "^[0-9]+$") + - grok: + source: log.dport + patterns: + - fieldName: target.port + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall", "l7_firewall"]) + && regexMatch("log.dport", "^[0-9]+$") + - grok: + source: log.protocol + patterns: + - fieldName: protocol + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && exists("log.protocol") + - grok: + source: log.mac + patterns: + - fieldName: origin.mac + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && !equals("log.eventType", "airmarshal_events") && exists("log.mac") + - grok: + source: log.url + patterns: + - fieldName: target.url + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && regexMatch("log.url", "(?i)^https?://[^\\s]+$") + - grok: + source: log.src + patterns: + - fieldName: origin.mac + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.eventType", "airmarshal_events") && exists("log.src") + - grok: + source: log.dst + patterns: + - fieldName: target.mac + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.eventType", "airmarshal_events") && exists("log.dst") + - grok: + source: log.device + patterns: + - fieldName: origin.mac + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && equals("log.type", "device_packet_flood") && exists("log.device") + - grok: + source: log.port + patterns: + - fieldName: log.switchPort + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && startsWith("log.type", "8021x_") && exists("log.port") + - grok: + source: log.identity + patterns: + - fieldName: origin.user + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && startsWith("log.type", "8021x_") && !oneOf("log.identity", + ["", "-"]) && exists("log.identity") + - grok: + source: log.msg + patterns: + - fieldName: '' + pattern: ^Peer IP= + - fieldName: log.sourceIp + pattern: '[^\s]+?' + - fieldName: '' + pattern: \s*Peer port(?:=|\[) + - fieldName: origin.port + pattern: '[0-9]+' + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && oneOf("log.type", ["anyconnect_vpn_auth_failure", "anyconnect_vpn_auth_success"]) + - add: + function: string + params: + key: actionResult + value: failure + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && oneOf("log.type", ["anyconnect_vpn_auth_failure", "anyconnect_vpn_auth_success"]) + && equals("log.type", "anyconnect_vpn_auth_failure") + - add: + function: string + params: + key: actionResult + value: success + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && oneOf("log.type", ["anyconnect_vpn_auth_failure", "anyconnect_vpn_auth_success"]) + && equals("log.type", "anyconnect_vpn_auth_success") + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}pattern:\s*' + - fieldName: log.pattern + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall", "l7_firewall"]) + - add: + function: string + params: + key: actionResult + value: success + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall", "l7_firewall"]) + && (equals("log.merakiResult", "allow") || regexMatch("log.pattern", "(?i)^allow(?:\\s|$)")) + - add: + function: string + params: + key: actionResult + value: denied + where: equals("log.nativeFormat", "true") && oneOf("log.merakiGroup", ["flows", "firewall", "cellular_firewall", "vpn_firewall", "l7_firewall"]) + && (equals("log.merakiResult", "deny") || regexMatch("log.pattern", "(?i)^deny(?:\\s|$)") || equals("log.decision", "blocked")) + - add: + function: string + params: + key: actionResult + value: denied + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "security_event") && ((equals("log.alertType", "ids_alerted") && equals("log.decision", + "blocked")) || (equals("log.alertType", "security_filtering_file_scanned") && equals("log.action", "block"))) + - grok: + source: log.type + patterns: + - fieldName: action + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && oneOf("log.type", ["vpn_connectivity_change", "disassociation"]) + - grok: + source: log.fields + patterns: + - fieldName: '' + pattern: '{{.merakiKVPrefix}}request:\s*' + - fieldName: log.method + pattern: \S+ + - fieldName: '' + pattern: \s+ + - fieldName: target.url + pattern: https?://[^\s]+ + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "urls") + - grok: + source: log.peerContact + patterns: + - fieldName: log.sourceIp + pattern: (?:\[[^\]]+\]|[^\s]+) + - fieldName: '' + pattern: ':' + - fieldName: origin.port + pattern: '[0-9]+' + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && equals("log.type", "vpn_connectivity_change") + - trim: + function: prefix + substring: '[' + fields: + - log.sourceIp + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && equals("log.type", "vpn_connectivity_change") + - trim: + function: suffix + substring: ']' + fields: + - log.sourceIp + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && equals("log.type", "vpn_connectivity_change") + - add: + function: string + params: + key: actionResult + value: success + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && equals("log.type", "vpn_connectivity_change") && equals("log.connectivity", + "true") + - grok: + source: log.genericEvent + patterns: + - fieldName: action + pattern: ^Cellular connection + - fieldName: '' + pattern: \s+ + - fieldName: log.connectionState + pattern: (?:up|down) + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") + - add: + function: string + params: + key: actionResult + value: success + where: equals("log.nativeFormat", "true") && equals("log.connectionState", "up") + - grok: + source: log.genericEvent + patterns: + - fieldName: action + pattern: ^dhcp no offers + - fieldName: '' + pattern: \s+for mac\s+ + - fieldName: origin.mac + pattern: \S+ + - fieldName: '' + pattern: \s+host\s*=\s* + - fieldName: log.sourceIp + pattern: \S+ + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") + - grok: + source: log.genericEvent + patterns: + - fieldName: action + pattern: ^dhcp lease + - fieldName: '' + pattern: \s+of ip\s+ + - fieldName: log.sourceIp + pattern: \S+ + - fieldName: '' + pattern: \s+from server mac\s+ + - fieldName: log.serverMac + pattern: \S+ + - fieldName: '' + pattern: \s+for client mac\s+ + - fieldName: origin.mac + pattern: \S+ + - fieldName: '' + pattern: \s+from router\s+ + - fieldName: log.routerIp + pattern: \S+ + - fieldName: '' + pattern: \s+on subnet\s+ + - fieldName: log.subnet + pattern: \S+ + - fieldName: '' + pattern: \s+with dns\s+ + - fieldName: log.dns + pattern: '[^,]+' + - fieldName: '' + pattern: ',\s*' + - fieldName: log.dns1 + pattern: \S+ + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") + - add: + function: string + params: + key: actionResult + value: success + where: equals("log.nativeFormat", "true") && equals("action", "dhcp lease") + - grok: + source: log.genericEvent + patterns: + - fieldName: '' + pattern: ^Port\s+ + - fieldName: log.switchPort + pattern: '[0-9]+' + - fieldName: '' + pattern: \s+received an\s+ + - fieldName: protocol + pattern: \S+ + - fieldName: '' + pattern: \s+BPDU from\s+ + - fieldName: origin.mac + pattern: \S+ + - fieldName: '' + pattern: \s+so the port was\s+ + - fieldName: log.merakiResult + pattern: blocked + - fieldName: '' + pattern: $ + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") + - grok: + source: log.ip_src + patterns: + - fieldName: log.sourceIp + pattern: (?s:.*) + where: equals("log.nativeFormat", "true") && equals("log.merakiGroup", "events") && equals("log.type", "disassociation") && exists("log.ip_src") + - trim: + function: suffix + substring: ':' + fields: + - log.localIp + where: '!inCIDR("log.localIp", "::/0")' + - trim: + function: suffix + substring: ':' + fields: + - log.sourceIp + where: '!inCIDR("log.sourceIp", "::/0")' - trim: function: suffix substring: ':' fields: - - log.localIp - - origin.ip - - target.ip - - log.serverIp + - log.destinationIp + where: '!inCIDR("log.destinationIp", "::/0")' + - trim: + function: suffix + substring: ':' + fields: + - log.serverIp + where: '!inCIDR("log.serverIp", "::/0")' - trim: function: prefix substring: '''' fields: - - origin.ip + - log.sourceIp - log.peerIdent - log.connectivity - log.vpnType @@ -1485,6 +2708,19 @@ pipeline: - log.vpnType #......................................................................# # Adding geolocation + # Validate vendor captures before standard promotion and geolocation. + - rename: + from: + - log.sourceIp + to: origin.ip + where: (inCIDR("log.sourceIp", "0.0.0.0/0") || inCIDR("log.sourceIp", "::/0")) && !inCIDR("log.sourceIp", "0.0.0.0/32") && !inCIDR("log.sourceIp", + "::/128") + - rename: + from: + - log.destinationIp + to: target.ip + where: (inCIDR("log.destinationIp", "0.0.0.0/0") || inCIDR("log.destinationIp", "::/0")) && !inCIDR("log.destinationIp", "0.0.0.0/32") && + !inCIDR("log.destinationIp", "::/128") - dynamic: plugin: com.utmstack.geolocation params: @@ -1501,22 +2737,24 @@ pipeline: plugin: com.utmstack.geolocation params: source: log.serverIp - destination: log.serverIp.geolocation - where: exists("log.serverIp") + destination: log.serverGeolocation + where: '(inCIDR("log.serverIp", "0.0.0.0/0") || inCIDR("log.serverIp", "::/0")) && !inCIDR("log.serverIp", "0.0.0.0/32") && !inCIDR("log.serverIp", "::/128")' - dynamic: plugin: com.utmstack.geolocation params: source: log.localIp - destination: log.localIp.geolocation - where: exists("log.localIp") - - dynamic: - plugin: com.utmstack.geolocation - params: - source: log.ipResp - destination: log.ipResp.geolocation - where: exists("log.ipResp") - #......................................................................# - # Common field conversions + destination: log.localGeolocation + where: '(inCIDR("log.localIp", "0.0.0.0/0") || inCIDR("log.localIp", "::/0")) && !inCIDR("log.localIp", "0.0.0.0/32") && !inCIDR("log.localIp", "::/128")' + - rename: + from: + - origin.port + to: log.unparsedOriginPort + where: exists("origin.port") && !(regexMatch("origin.port", "^[0-9]{1,5}$") && lessOrEqual("origin.port", 65535)) + - rename: + from: + - target.port + to: log.unparsedTargetPort + where: exists("target.port") && !(regexMatch("target.port", "^[0-9]{1,5}$") && lessOrEqual("target.port", 65535)) - cast: fields: - origin.port @@ -1529,7 +2767,7 @@ pipeline: patterns: - fieldName: log.eventType pattern: '{{.greedy}}' - where: exists("log.merakiGroup") + where: 'exists("log.merakiGroup") && !exists("log.eventType")' - grok: source: log.genericEvent patterns: @@ -1552,16 +2790,6 @@ pipeline: where: equalsIgnoreCase("log.merakiResult","blocked") # 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: @@ -1615,3 +2843,58 @@ pipeline: - protocol to: log.ipProtocolNumber where: exists("protocol") && greaterOrEqual("protocol",0) + - delete: + fields: + - log.vpnAuthenticationFailure + - add: + function: string + params: + key: log.vpnAuthenticationFailure + value: match + where: 'equals("log.eventType", "events") && + + equals("log.type", "anyconnect_vpn_auth_failure") && + + exists("origin.ip") && + + exists("log.merakiType") && !oneOf("log.merakiType", ["", "unknown", "-"]) && + + exists("dataSource") && !oneOf("dataSource", ["", "unknown", "-"])' + - delete: + fields: + - log.malwareGroupingKey + - log.malwareGroupingType + - grok: + source: log.sha256 + patterns: + - fieldName: log.malwareGroupingKey + pattern: (?s:.*) + where: equals("log.eventType", "security_event") && oneOf("log.alertType", ["security_filtering_file_scanned", "security_filtering_disposition_change"]) + && equals("log.disposition", "malicious") && regexMatch("log.sha256", "(?i)^[0-9a-f]{64}$") + - add: + function: string + params: + key: log.malwareGroupingType + value: hash + where: equals("log.eventType", "security_event") && oneOf("log.alertType", ["security_filtering_file_scanned", "security_filtering_disposition_change"]) + && equals("log.disposition", "malicious") && regexMatch("log.sha256", "(?i)^[0-9a-f]{64}$") + - grok: + source: id + patterns: + - fieldName: log.malwareGroupingKey + pattern: (?s:.*) + where: equals("log.eventType", "security_event") && oneOf("log.alertType", ["security_filtering_file_scanned", "security_filtering_disposition_change"]) + && equals("log.disposition", "malicious") && !regexMatch("log.sha256", "(?i)^[0-9a-f]{64}$") && exists("id") && !equals("id", "") + - add: + function: string + params: + key: log.malwareGroupingType + value: event + where: equals("log.eventType", "security_event") && oneOf("log.alertType", ["security_filtering_file_scanned", "security_filtering_disposition_change"]) + && equals("log.disposition", "malicious") && !regexMatch("log.sha256", "(?i)^[0-9a-f]{64}$") && exists("id") && !equals("id", "") + - delete: + fields: + - log.nativeFormat + - log.nativeHandled + - log.nativeMessage + - log.fields diff --git a/plugins/alerts/meraki_consumers_test.go b/plugins/alerts/meraki_consumers_test.go new file mode 100644 index 000000000..8189dded1 --- /dev/null +++ b/plugins/alerts/meraki_consumers_test.go @@ -0,0 +1,172 @@ +package main + +// Verify the SDK Event/Alert wire-field contract and rule role selection using +// synthetic raw events. This does not execute the closed correlation service or +// prove live alert creation; lastEvent is the alert plugin's indexed alias for +// events[0], supplied by the companion alert-field resolution change. +import ( + "strings" + "testing" + + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/utils" + "github.com/tidwall/gjson" +) + +func merakiAlertWire(t *testing.T, rule *plugins.Rule, out string) string { + t.Helper() + event := new(plugins.Event) + if err := utils.StringToProtoMessage(&out, event); err != nil { + t.Fatal(err) + } + alert := &plugins.Alert{Events: []*plugins.Event{event}} + switch rule.Adversary { + case "origin": + alert.Adversary, alert.Target = event.Origin, event.Target + case "target": + alert.Adversary, alert.Target = event.Target, event.Origin + default: + t.Fatalf("unsupported actor side %q", rule.Adversary) + } + wire, err := utils.ProtoMessageToString(alert) + if err != nil { + t.Fatal(err) + } + return *wire +} + +func merakiGroupIdentity(t *testing.T, rule *plugins.Rule, out string) string { + t.Helper() + wire := merakiAlertWire(t, rule, out) + var parts []string + fields := append(append([]string{}, rule.GroupBy...), rule.DeduplicateBy...) + for _, field := range fields { + path := strings.Replace(field, "lastEvent.", "events.0.", 1) + value := gjson.Get(wire, path) + if !value.Exists() || value.String() == "" { + // The documented standalone legacy Air Marshal envelope lacks the + // device header. Collector+BSSID still supplies an actual identity. + if field == "lastEvent.log.merakiType" { + continue + } + t.Errorf("positive raw event cannot resolve alert identity field %s", field) + } + parts = append(parts, field+"="+value.String()) + } + if len(parts) == 0 { + t.Fatal("positive rule has no resolved identity fields") + } + return strings.Join(parts, "\x00") +} + +func TestMerakiAlertFieldContracts(t *testing.T) { + cfg, rules, cache := merakiConfig(t), merakiRules(t), plugins.NewCELCache("meraki-alert-identities") + covered := map[string]bool{} + for _, fixture := range merakiFixtures(t) { + if len(fixture.Matches) == 0 { + continue + } + out := merakiParse(t, cfg, fixture.Raw, fixture.DataSource, cache) + for _, name := range fixture.Matches { + rule := rules[name] + if rule == nil { + t.Fatalf("unknown fixture rule %q", name) + } + if yes, err := cache.Eval(rule.Where, out); err != nil || !yes { + t.Fatalf("%s must match %s before alert identity checks: %v %v", fixture.Name, name, yes, err) + } + merakiGroupIdentity(t, rule, out) + covered[name] = true + } + if fixture.Name == "AMP blocked positive" { + rule := rules["advanced_malware_protection_alerts"] + if rule.Adversary != "target" || gjson.Get(out, "origin.ip").String() != "192.0.2.8" || gjson.Get(out, "target.ip").String() != "198.51.100.9" { + t.Fatal("download must preserve physical client/server roles and choose the remote server as alert adversary") + } + wire := merakiAlertWire(t, rule, out) + if gjson.Get(wire, "adversary.ip").String() != "198.51.100.9" || gjson.Get(wire, "target.ip").String() != "192.0.2.8" { + t.Fatal("SDK alert payload lost the selected remote/client roles") + } + // The previous origin setting selects the downloading client as + // adversary; keep this before/after distinction explicit. + oldRole := &plugins.Rule{Adversary: "origin"} + before := merakiAlertWire(t, oldRole, out) + if gjson.Get(before, "adversary.ip").String() != "192.0.2.8" || gjson.Get(before, "target.ip").String() != "198.51.100.9" { + t.Fatal("before-role control did not select the original network source") + } + } + } + if len(rules) != 7 || len(covered) != len(rules) { + t.Fatalf("every Meraki rule needs a raw positive identity check: rules=%d covered=%v", len(rules), covered) + } +} + +func TestMerakiAMPRecordIdentityFallback(t *testing.T) { + cfg, rules, cache := merakiConfig(t), merakiRules(t), plugins.NewCELCache("meraki-amp-fallback") + rule := rules["advanced_malware_protection_alerts"] + var raw, source string + for _, fixture := range merakiFixtures(t) { + if fixture.Name == "AMP retrospective without endpoint positive" { + raw, source = fixture.Raw, fixture.DataSource + } + } + if raw == "" { + t.Fatal("required synthetic retrospective fixture missing") + } + const firstID = "cb955b9f-744d-4b8e-9b29-d077d6229c36" + const secondID = "e0421218-60d1-4b97-bc7a-5d7cdca619b5" + first := merakiParseEvent(t, cfg, raw, source, firstID, cache) + second := merakiParseEvent(t, cfg, raw, source, secondID, cache) + if merakiGroupIdentity(t, rule, first) != merakiGroupIdentity(t, rule, second) || gjson.Get(first, "log.malwareGroupingType").String() != "hash" { + t.Fatal("same valid file hash on the same collector/device must group independently of event ID") + } + hash := gjson.Get(first, "log.sha256").String() + if len(hash) != 64 || !strings.Contains(raw, "sha256="+hash) { + t.Fatal("retrospective fixture must carry its actual synthetic SHA256") + } + for _, variant := range []struct{ name, raw, source string }{ + {"different hash", strings.Replace(raw, "sha256="+hash, "sha256="+strings.Repeat("b", 64), 1), source}, + {"different collector", raw, "other-meraki-relay"}, + {"different appliance", strings.Replace(raw, " LAB-MX ", " LAB-OTHER ", 1), source}, + } { + other := merakiParseEvent(t, cfg, variant.raw, variant.source, firstID, cache) + if merakiGroupIdentity(t, rule, first) == merakiGroupIdentity(t, rule, other) { + t.Fatalf("%s must not share an AMP alert group", variant.name) + } + } + for _, value := range []string{"", "malformed"} { + badRaw := strings.Replace(raw, "sha256="+hash+" ", "", 1) + if value != "" { + badRaw = strings.Replace(raw, "sha256="+hash, "sha256="+value, 1) + } + one := merakiParseEvent(t, cfg, badRaw, source, firstID, cache) + two := merakiParseEvent(t, cfg, badRaw, source, secondID, cache) + for _, event := range []struct{ out, id string }{{one, firstID}, {two, secondID}} { + if yes, err := cache.Eval(rule.Where, event.out); err != nil || !yes { + t.Fatalf("explicit malicious disposition must remain eligible with actual event ID: %v %v", yes, err) + } + if gjson.Get(event.out, "log.malwareGroupingType").String() != "event" || gjson.Get(event.out, "log.malwareGroupingKey").String() != event.id { + t.Fatal("hashless grouping must use the actual ingress record ID") + } + if gjson.Get(event.out, "origin.ip").Exists() || gjson.Get(event.out, "target.ip").Exists() { + t.Fatal("IP-less retrospective event must not acquire an endpoint") + } + if value != "" && gjson.Get(event.out, "log.sha256").String() != value { + t.Fatal("malformed vendor hash must remain available for investigation") + } + } + if merakiGroupIdentity(t, rule, one) == merakiGroupIdentity(t, rule, two) { + t.Fatal("two incomplete records must not pool under collector-only grouping") + } + // An externally supplied ingress ID could equal a hash string. The + // explicit identity type prevents pooling these different meanings. + collision := merakiParseEvent(t, cfg, badRaw, source, hash, cache) + if merakiGroupIdentity(t, rule, first) == merakiGroupIdentity(t, rule, collision) { + t.Fatal("file hash and record ID namespaces must remain distinct") + } + missing := merakiParseEvent(t, cfg, badRaw, source, "", cache) + if yes, err := cache.Eval(rule.Where, missing); err != nil || yes || gjson.Get(missing, "log.malwareGroupingKey").Exists() { + t.Fatalf("without a valid hash or actual ingress ID no grouping identity may be invented: %v %v", yes, err) + } + } +} diff --git a/plugins/alerts/meraki_contract_test.go b/plugins/alerts/meraki_contract_test.go new file mode 100644 index 000000000..ae0cf147d --- /dev/null +++ b/plugins/alerts/meraki_contract_test.go @@ -0,0 +1,486 @@ +package main + +// Offline Meraki extraction model, not the closed EventProcessor. +// Explicit YAML grok/rename/cast/trim/add/delete and observed KV splitting are +// modeled. CEL, Event serialization, placeholder expansion, query creation and +// history thresholds use SDK v1.1.31. External geolocation is not executed. +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "testing" + "text/template" + + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/utils" + "github.com/tidwall/gjson" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" +) + +type merakiFixture struct { + Name string `json:"name"` + DataSource string `json:"dataSource"` + ID *string `json:"id,omitempty"` + Raw string `json:"raw"` + Expected map[string]any `json:"expected"` + Absent []string `json:"absent"` + Matches []string `json:"matches"` +} + +func merakiPut(m map[string]any, path string, value any, remove bool) { + p := strings.Split(path, ".") + for _, k := range p[:len(p)-1] { + n, ok := m[k].(map[string]any) + if !ok { + if remove { + return + } + n = map[string]any{} + m[k] = n + } + m = n + } + if remove { + delete(m, p[len(p)-1]) + } else { + m[p[len(p)-1]] = value + } +} +func merakiGet(m map[string]any, p string) (any, bool) { + var v any = m + for _, k := range strings.Split(p, ".") { + n, ok := v.(map[string]any) + if !ok { + return nil, false + } + v, ok = n[k] + if !ok { + return nil, false + } + } + return v, true +} +func merakiConfig(t *testing.T) *plugins.Config { + t.Helper() + b, e := utils.ReadPbYaml("../../filters/cisco/meraki.yml") + if e != nil { + t.Fatal(e) + } + c := new(plugins.Config) + if e = protojson.Unmarshal(b, c); e != nil { + t.Fatal(e) + } + return c +} +func merakiRegex(t *testing.T, g *plugins.Grok, cfg *plugins.Config) *regexp.Regexp { + t.Helper() + var pattern strings.Builder + for i, p := range g.Patterns { + if p.FieldName != "" { + fmt.Fprintf(&pattern, "(?P%s)", i, p.Pattern) + } else { + pattern.WriteString("(?:" + p.Pattern + ")") + } + } + // Built-in legacy aliases are approximations, not the closed executor's code. + // New native patterns are explicit in YAML; evidence claims concern that path. + pats := map[string]string{"greedy": ".*", "data": ".*?", "word": "[A-Za-z0-9_-]+", "space": `\s+`, + "integer": `[+-]?[0-9]+`, "ipv4": `(?:[0-9]{1,3}\.){3}[0-9]{1,3}`, + "ipv6": `[0-9A-Fa-f]*:[0-9A-Fa-f:.]+`, "hostname": `[A-Za-z0-9][A-Za-z0-9.-]*`, + "monthName": `[A-Za-z]+`, "monthDay": `[0-9]{1,2}`, "year": `[0-9]{4}`, "time": `[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?`, + } + for k, v := range cfg.Patterns { + pats[k] = v + } + tmpl, e := template.New("grok").Option("missingkey=error").Parse(pattern.String()) + if e != nil { + t.Fatal(e) + } + var b bytes.Buffer + if e = tmpl.Execute(&b, pats); e != nil { + t.Fatal(e) + } + r, e := regexp.Compile(b.String()) + if e != nil { + t.Fatal(e) + } + return r +} +func merakiParse(t *testing.T, cfg *plugins.Config, raw string, dataSource string, cache *plugins.CELCache) string { + return merakiParseEvent(t, cfg, raw, dataSource, "synthetic-ingress-event", cache) +} +func merakiParseEvent(t *testing.T, cfg *plugins.Config, raw, dataSource, id string, cache *plugins.CELCache) string { + t.Helper() + draft := map[string]any{"raw": raw, "dataType": "firewall-meraki", "dataSource": dataSource, "log": map[string]any{}} + if id != "" { + draft["id"] = id + } + for _, stage := range cfg.Pipeline { + matched := false + for _, dataType := range stage.DataTypes { + if dataType == "firewall-meraki" { + matched = true + } + } + if !matched { + continue + } + for _, s := range stage.Steps { + b, e := protojson.Marshal(s) + if e != nil { + t.Fatal(e) + } + var step map[string]map[string]any + if e = json.Unmarshal(b, &step); e != nil { + t.Fatal(e) + } + for kind, body := range step { + if w, ok := body["where"].(string); ok && w != "" { + snapshot, err := json.Marshal(draft) + if err != nil { + t.Fatal(err) + } + match, e := cache.Eval(w, string(snapshot)) + if e != nil { + t.Fatal(e) + } + if !match { + continue + } + } + switch kind { + case "grok": + g := s.Grok + src := g.Source + if src == "" { + src = "raw" + } + v, ok := merakiGet(draft, src) + if !ok { + continue + } + str, ok := v.(string) + if !ok { + t.Fatalf("non-string grok source %s", src) + } + r := merakiRegex(t, g, cfg) + m := r.FindStringSubmatch(str) + if m == nil { + continue + } + for i, p := range g.Patterns { + if p.FieldName != "" { + merakiPut(draft, p.FieldName, m[r.SubexpIndex(fmt.Sprintf("f%d", i))], false) + } + } + case "rename": + for _, p := range s.Rename.From { + if v, ok := merakiGet(draft, p); ok { + merakiPut(draft, s.Rename.To, v, false) + merakiPut(draft, p, nil, true) + break + } + } + case "trim": + for _, p := range s.Trim.Fields { + if v, ok := merakiGet(draft, p); ok { + str, ok := v.(string) + if !ok { + continue + } + switch s.Trim.Function { + case "prefix": + str = strings.TrimPrefix(str, s.Trim.Substring) + case "suffix": + str = strings.TrimSuffix(str, s.Trim.Substring) + default: + t.Fatalf("unsupported trim %s", s.Trim.Function) + } + merakiPut(draft, p, str, false) + } + } + case "add": + if s.Add.Function != "string" { + t.Fatalf("unsupported add function %s", s.Add.Function) + } + merakiPut(draft, s.Add.Params["key"].GetStringValue(), s.Add.Params["value"].AsInterface(), false) + case "delete": + for _, p := range s.Delete.Fields { + merakiPut(draft, p, nil, true) + } + case "kv": + v, ok := merakiGet(draft, s.Kv.Source) + if !ok { + continue + } + // Observed KV output splits quoted multiword values. + // Explicit YAML grok steps rebuild consumed fields afterward. + for _, item := range strings.Split(v.(string), s.Kv.FieldSplit) { + pair := strings.SplitN(item, s.Kv.ValueSplit, 2) + if len(pair) != 2 { + continue + } + key := pair[0] + utils.SanitizeField(&key) + if key != "" { + merakiPut(draft, "log."+key, pair[1], false) + } + } + case "dynamic": + if s.Dynamic.Plugin != "com.utmstack.geolocation" { + t.Fatalf("unsupported dynamic plugin %s", s.Dynamic.Plugin) + } + field := s.Dynamic.Params["source"].GetStringValue() + v, ok := merakiGet(draft, field) + if !ok { + t.Fatalf("missing dynamic source %s", field) + } + ip := net.ParseIP(fmt.Sprint(v)) + if ip == nil || ip.IsUnspecified() { + t.Fatalf("invalid address reaches geolocation: %s", field) + } + // The external geolocation service is not executed. + case "json": + source, ok := merakiGet(draft, s.Json.Source) + if !ok { + continue + } + str, ok := source.(string) + if !ok { + t.Fatalf("JSON source is not a string") + } + var parsed map[string]any + if e := json.Unmarshal([]byte(str), &parsed); e != nil { + t.Fatal(e) + } + for key, value := range merakiSanitizeJSON(parsed) { + merakiPut(draft, "log."+key, value, false) + } + case "cast": + for _, field := range s.Cast.Fields { + if value, ok := merakiGet(draft, field); ok { + switch s.Cast.To { + case "string": + merakiPut(draft, field, utils.CastString(value), false) + case "int": + merakiPut(draft, field, utils.CastInt64(value), false) + default: + t.Fatalf("unsupported cast %s", s.Cast.To) + } + } + } + case "drop": + return "" + default: + t.Fatalf("unsupported filter step %s", kind) + } + } + } + } + b, e := json.Marshal(draft) + if e != nil { + t.Fatal(e) + } + in := string(b) + ev := new(plugins.Event) + if e = utils.StringToProtoMessage(&in, ev); e != nil { + t.Fatal(e) + } + out, e := utils.ProtoMessageToString(ev) + if e != nil { + t.Fatal(e) + } + return *out +} +func merakiRules(t *testing.T) map[string]*plugins.Rule { + t.Helper() + paths, e := filepath.Glob("../../rules/cisco/meraki/*.yml") + if e != nil { + t.Fatal(e) + } + out := map[string]*plugins.Rule{} + for _, p := range paths { + b, e := utils.ReadPbYaml(p) + if e != nil { + t.Fatal(e) + } + r := new(plugins.Rule) + if e = protojson.Unmarshal(b, r); e != nil { + t.Fatal(e) + } + r.Normalize() + out[strings.TrimSuffix(filepath.Base(p), ".yml")] = r + } + return out +} + +func merakiSanitizeJSON(input map[string]any) map[string]any { + out := map[string]any{} + for key, value := range input { + utils.SanitizeField(&key) + if nested, ok := value.(map[string]any); ok { + value = merakiSanitizeJSON(nested) + } + out[key] = value + } + return out +} + +func merakiFixtures(t *testing.T) []merakiFixture { + t.Helper() + b, e := os.ReadFile("testdata/meraki_raw.json") + if e != nil { + t.Fatal(e) + } + var cases []merakiFixture + if e = json.Unmarshal(b, &cases); e != nil { + t.Fatal(e) + } + return cases +} + +func TestMerakiRawContracts(t *testing.T) { + cfg, rules, cache := merakiConfig(t), merakiRules(t), plugins.NewCELCache("meraki-raw") + for _, f := range merakiFixtures(t) { + t.Run(f.Name, func(t *testing.T) { + id := "synthetic-ingress-event" + if f.ID != nil { + id = *f.ID + } + out := merakiParseEvent(t, cfg, f.Raw, f.DataSource, id, cache) + for field, want := range f.Expected { + got := gjson.Get(out, field) + if !got.Exists() || !reflect.DeepEqual(got.Value(), want) { + t.Errorf("%s got %v want %v", field, got.Value(), want) + } + } + for _, field := range f.Absent { + if gjson.Get(out, field).Exists() { + t.Errorf("unexpected %s", field) + } + } + if gjson.Get(out, "raw").String() != f.Raw { + t.Error("raw changed") + } + expected := map[string]bool{} + for _, n := range f.Matches { + expected[n] = true + } + for name, r := range rules { + yes, e := cache.Eval(r.Where, out) + if e != nil { + t.Fatalf("%s CEL %v", name, e) + } + if yes != expected[name] { + t.Errorf("%s match=%v want=%v", name, yes, expected[name]) + } + if name == "meraki_vpn_brute_force" && (gjson.Get(out, "log.vpnAuthenticationFailure").String() == "match") != yes { + t.Error("VPN marker mismatch") + } + if yes { + for _, search := range r.Correlation { + for _, term := range search.With { + v := term.Value.GetStringValue() + if strings.HasPrefix(v, "{{.") { + p := strings.TrimSuffix(strings.TrimPrefix(v, "{{."), "}}") + if !gjson.Get(out, p).Exists() { + t.Errorf("%s missing history %s", name, p) + } + } + } + } + if name == "advanced_malware_protection_alerts" { + ev := new(plugins.Event) + if e := utils.StringToProtoMessage(&out, ev); e != nil { + t.Fatal(e) + } + if r.Adversary != "target" { + t.Error("AMP actor must be remote download endpoint") + } + a := &plugins.Alert{Adversary: ev.Target, Target: ev.Origin} + if a.GetAdversary().GetIp() != ev.GetTarget().GetIp() { + t.Error("AMP endpoint roles changed") + } + } + } + } + }) + } +} + +func TestMerakiAuxiliaryGeoGuards(t *testing.T) { + cfg, cache := merakiConfig(t), plugins.NewCELCache("meraki-auxiliary-geo") + checked := 0 + for _, step := range cfg.Pipeline[0].Steps { + if step.Dynamic == nil { + continue + } + source := step.Dynamic.Params["source"].GetStringValue() + if source != "log.serverIp" && source != "log.localIp" { + continue + } + checked++ + if strings.HasPrefix(step.Dynamic.Params["destination"].GetStringValue(), source+".") { + t.Fatal("geolocation destination nests under its scalar source") + } + for _, value := range []string{"relay.example.invalid", "0.0.0.0", "::0", "0:0:0:0:0:0:0:0", "::ffff:0.0.0.0", "192.0.2.10", "2001:db8::"} { + t.Run(source+"_"+value, func(t *testing.T) { + // Isolated source-step control, not a legacy-envelope replay. + add := &plugins.Step{Add: &plugins.Add{Function: "string", Params: map[string]*structpb.Value{"key": structpb.NewStringValue(source), "value": structpb.NewStringValue(value)}}} + isolated := &plugins.Config{Pipeline: []*plugins.Pipeline{{DataTypes: []string{"firewall-meraki"}, Steps: []*plugins.Step{add, step}}}} + out := merakiParse(t, isolated, "synthetic control", "synthetic-relay", cache) + if gjson.Get(out, source).String() != value { + t.Error("vendor source value changed") + } + }) + } + } + if checked != 2 { + t.Fatal("both auxiliary geolocation sources must be checked") + } +} + +func TestMerakiPrivateRoutingEvidence(t *testing.T) { + path := os.Getenv("UTM_MERAKI_EVIDENCE") + if path == "" { + t.Skip("set UTM_MERAKI_EVIDENCE to the bounded private routing sample") + } + b, e := os.ReadFile(path) + if e != nil { + t.Fatal(e) + } + var hits []struct { + Source map[string]any `json:"_source"` + } + if e = json.Unmarshal(b, &hits); e != nil || len(hits) == 0 { + t.Fatal("invalid or empty private routing evidence") + } + cfg, rules, cache := merakiConfig(t), merakiRules(t), plugins.NewCELCache("meraki-private-routing") + for _, hit := range hits { + raw, ok := hit.Source["raw"].(string) + if !ok { + t.Fatal("private raw missing") + } + source, _ := hit.Source["dataSource"].(string) + id, _ := hit.Source["id"].(string) + out := merakiParseEvent(t, cfg, raw, source, id, cache) + for _, field := range []string{"origin.ip", "target.ip", "log.eventType", "actionResult", "log.vpnAuthenticationFailure", "log.malwareGroupingKey"} { + if gjson.Get(out, field).Exists() { + t.Errorf("unrelated private syslog acquired %s", field) + } + } + for name, rule := range rules { + if yes, e := cache.Eval(rule.Where, out); e != nil || yes { + t.Errorf("private routing negative matched %s or errored: %v", name, e) + } + } + } + t.Logf("private non-Meraki routing controls=%d; no invented Meraki identities, outcomes or rule candidates", len(hits)) +} diff --git a/plugins/alerts/meraki_history_test.go b/plugins/alerts/meraki_history_test.go new file mode 100644 index 000000000..93ac0c0c2 --- /dev/null +++ b/plugins/alerts/meraki_history_test.go @@ -0,0 +1,219 @@ +package main + +// These events are fabricated from the documented AnyConnect syslog format. +// The source-specific offline parser is bounded; CEL, placeholder resolution, +// mapping lookup and the history count decision execute the pinned SDK. The +// OpenSearch endpoint is a local mock, never a customer instance. +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "strings" + "testing" + "time" + + sdkos "github.com/threatwinds/go-sdk/os" + "github.com/threatwinds/go-sdk/plugins" + "github.com/tidwall/gjson" +) + +func TestMerakiSDKHistory(t *testing.T) { + // The SDK client is a process-wide singleton; isolate this mock from other + // technologies' tests when the draft PRs are tested together. + if os.Getenv("UTM_MERAKI_HISTORY_CHILD") != "1" { + command := exec.Command(os.Args[0], "-test.run=^TestMerakiSDKHistory$") + command.Env = append(os.Environ(), "UTM_MERAKI_HISTORY_CHILD=1") + if out, err := command.CombinedOutput(); err != nil { + t.Fatalf("isolated history test: %v\n%s", err, out) + } + return + } + + cfg, rules, cache := merakiConfig(t), merakiRules(t), plugins.NewCELCache("meraki-history") + rule := rules["meraki_vpn_brute_force"] + if rule == nil || len(rule.Correlation) != 1 { + t.Fatal("VPN rule must have exactly one history request") + } + search := rule.Correlation[0] + if search.Count != 10 || search.Within != "15m" || len(search.With) != 4 { + t.Fatal("expected ten failures in fifteen minutes with collector/device/IP/class scope") + } + const raw = "1700000000.123456789 MX-LAB events type=anyconnect_vpn_auth_failure msg= 'Peer IP=198.51.100.10Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '" + const collector = "meraki-test-relay" + const marker = "log.vpnAuthenticationFailure" + parse := func(raw string) string { return merakiParse(t, cfg, raw, collector, cache) } + out := parse(raw) + if yes, err := cache.Eval(rule.Where, out); err != nil || !yes || gjson.Get(out, marker).String() != "match" { + t.Fatalf("documented failure must produce a matching predicate and candidate: %v %v", yes, err) + } + + var history []string + queries := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if strings.HasSuffix(r.URL.Path, "/_mapping") { + // Text-with-keyword and exact IP mappings both exercise SDK lookup. + _, _ = io.WriteString(w, `{"v11-log-firewall-meraki-test":{"mappings":{"properties":{"@timestamp":{"type":"date"},"dataSource":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"origin":{"properties":{"ip":{"type":"ip"}}},"log":{"properties":{"merakiType":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"vpnAuthenticationFailure":{"type":"keyword"}}}}}}`) + return + } + if r.URL.Path != "/v11-log-firewall-meraki-*/_search" { + t.Errorf("unexpected request %s", r.URL.Path) + http.Error(w, "unsupported request", http.StatusBadRequest) + return + } + queries++ + body, err := io.ReadAll(r.Body) + if err != nil { + t.Error(err) + return + } + query := string(body) + clauses := append(gjson.Get(query, "query.bool.must").Array(), gjson.Get(query, "query.bool.filter").Array()...) + if len(clauses) != 5 { + t.Errorf("expected four exact scopes and time range, got %s", query) + } + expected := map[string]string{"dataSource": collector, "log.merakiType": "MX-LAB", "origin.ip": "198.51.100.10", marker: "match"} + for _, clause := range clauses { + if term := clause.Get("term"); term.Exists() { + for field, value := range term.Map() { + path := strings.TrimSuffix(field, ".keyword") + want, exists := expected[path] + if !exists || value.Get("value").String() != want { + t.Errorf("unexpected history term %s", term.Raw) + } + delete(expected, path) + } + } + } + if len(expected) != 0 { + t.Errorf("history omitted scopes: %v", expected) + } + hits := []map[string]any{} + for _, doc := range history { + match := true + for _, clause := range clauses { + if term := clause.Get("term"); term.Exists() { + for field, value := range term.Map() { + actual := gjson.Get(doc, strings.TrimSuffix(field, ".keyword")) + if !actual.Exists() || actual.String() != value.Get("value").String() { + match = false + } + } + } else if span := clause.Get("range"); span.Exists() { + for field, bounds := range span.Map() { + stamp, err := time.Parse(time.RFC3339Nano, gjson.Get(doc, field).String()) + if err != nil { + t.Error(err) + } + cutoff, err := time.Parse(time.RFC3339Nano, bounds.Get("gte").String()) + if err != nil { + t.Error(err) + } + if field != "@timestamp" || !bounds.Get("gte").Exists() || stamp.Before(cutoff) { + match = false + } + } + } else { + t.Errorf("unsupported history clause %s", clause.Raw) + match = false + } + } + if match { + hits = append(hits, map[string]any{"_id": fmt.Sprint(len(hits)), "_index": "v11-log-firewall-meraki-test", "_source": map[string]any{}}) + } + } + _ = json.NewEncoder(w).Encode(map[string]any{"took": 1, "hits": map[string]any{"total": map[string]any{"value": len(hits), "relation": "eq"}, "hits": hits}}) + })) + defer server.Close() + if err := sdkos.Connect([]string{server.URL}, "", ""); err != nil { + t.Fatal(err) + } + mutate := func(doc, field string, value any) string { + var m map[string]any + if err := json.Unmarshal([]byte(doc), &m); err != nil { + t.Fatal(err) + } + parts := strings.Split(field, ".") + node := m + for _, part := range parts[:len(parts)-1] { + next, ok := node[part].(map[string]any) + if !ok { + next = map[string]any{} + node[part] = next + } + node = next + } + if value == nil { + delete(node, parts[len(parts)-1]) + } else { + node[parts[len(parts)-1]] = value + } + b, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + return string(b) + } + stamp := time.Now().Add(-time.Minute).UTC().Format(time.RFC3339Nano) + prior := mutate(out, "@timestamp", stamp) + check := func(name, document string, count int, want bool) { + t.Run(name, func(t *testing.T) { + history = nil + for i := 0; i < count; i++ { + history = append(history, document) + } + match, _, err := search.Execute(&out) + if err != nil || match != want { + t.Fatalf("history result %v, expected %v: %v", match, want, err) + } + }) + } + check("below_threshold", prior, 9, false) + check("at_threshold", prior, 10, true) + check("inside_window", mutate(prior, "@timestamp", time.Now().Add(-15*time.Minute+10*time.Second).UTC().Format(time.RFC3339Nano)), 10, true) + check("expired", mutate(prior, "@timestamp", time.Now().Add(-15*time.Minute-10*time.Second).UTC().Format(time.RFC3339Nano)), 10, false) + check("different_collector", mutate(prior, "dataSource", "other-relay"), 10, false) + check("different_appliance", mutate(prior, "log.merakiType", "MX-OTHER"), 10, false) + check("different_source", mutate(prior, "origin.ip", "198.51.100.99"), 10, false) + check("unmarked_history", mutate(prior, marker, nil), 10, false) + for _, eventType := range []string{"anyconnect_vpn_auth_success", "anyconnect_vpn_general", "anyconnect_vpn_session_manager"} { + // Preserve the same Peer IP and even failure words: the explicit event + // class, not arbitrary message text or other traffic, defines the count. + benign := parse(strings.Replace(raw, "anyconnect_vpn_auth_failure", eventType, 1)) + if yes, err := cache.Eval(rule.Where, benign); err != nil || yes || gjson.Get(benign, marker).Exists() { + t.Fatalf("%s retained an authentication failure: %v %v", eventType, yes, err) + } + check(eventType+"_is_not_failure_history", mutate(benign, "@timestamp", stamp), 10, false) + } + // Nested Air Marshal retains the outer events group; candidate production + // must use the final eventType, exactly as the trigger does. + nested := parse(strings.Replace(raw, "events type=", "events airmarshal_events type=", 1)) + if yes, err := cache.Eval(rule.Where, nested); err != nil || yes || gjson.Get(nested, marker).Exists() { + t.Fatalf("nested non-VPN class retained a candidate: %v %v", yes, err) + } + check("nested_airmarshal_is_not_failure_history", mutate(nested, "@timestamp", stamp), 10, false) + for _, field := range []string{"origin.ip", "log.merakiType", "dataSource"} { + t.Run("missing_"+field+"_preflight", func(t *testing.T) { + missing := mutate(out, field, nil) + if yes, err := cache.Eval(rule.Where, missing); err != nil || yes { + t.Fatalf("missing identity must reject the trigger: %v %v", yes, err) + } + before := queries + if _, _, err := search.Execute(&missing); err == nil || queries != before { + t.Fatal("SDK must abort unresolved identity before a history search") + } + }) + } + for _, field := range []string{"dataSource", "log.merakiType"} { + for _, invalid := range []string{"", "unknown", "-"} { + missing := mutate(out, field, invalid) + if yes, err := cache.Eval(rule.Where, missing); err != nil || yes { + t.Fatalf("placeholder identity %s=%q must reject the trigger: %v %v", field, invalid, yes, err) + } + } + } +} diff --git a/plugins/alerts/testdata/filter-contracts/cisco-meraki.json b/plugins/alerts/testdata/filter-contracts/cisco-meraki.json index aa512ec2f..dfbbf6ff4 100644 --- a/plugins/alerts/testdata/filter-contracts/cisco-meraki.json +++ b/plugins/alerts/testdata/filter-contracts/cisco-meraki.json @@ -14,7 +14,7 @@ ], "fixtures": [ { - "name": "meraki_blocked", + "name": "isolated outcome control: meraki_blocked", "filter": "cisco/meraki.yml", "input": { "log": { @@ -25,100 +25,61 @@ "actionResult": "denied" }, "absent": [], - "rules": {} - }, - { - "name": "meraki_allowed", - "filter": "cisco/meraki.yml", - "input": { - "log": { - "merakiResult": "allowed" - } - }, - "expected": {}, - "absent": [ - "actionResult" - ], - "rules": {} - }, - { - "name": "meraki_amp_retrospective", - "filter": "cisco/meraki.yml", - "input": { - "dataSource": "mx-lab", - "log": { - "merakiGroup": "security_event", - "genericEvent": "security_filtering_disposition_change disposition=malicious action=allow" - } - }, - "expected": { - "log.eventType": "security_event", - "log.message": "security_filtering_disposition_change disposition=malicious action=allow" - }, - "absent": [ - "actionResult" - ], "rules": { - "rules/cisco/meraki/advanced_malware_protection_alerts.yml": true + "rules/cisco/meraki/advanced_malware_protection_alerts.yml": false, + "rules/cisco/meraki/air_marshal_rogue_ap.yml": false, + "rules/cisco/meraki/evil_twin_ap_detection.yml": false, + "rules/cisco/meraki/intrusion_detection_alerts.yml": false, + "rules/cisco/meraki/meraki_vpn_brute_force.yml": false, + "rules/cisco/meraki/rogue_ssid_detection.yml": false, + "rules/cisco/meraki/wireless_intrusion_attempts.yml": false } }, { - "name": "meraki_amp_block", + "name": "isolated outcome control: meraki_allowed", "filter": "cisco/meraki.yml", "input": { - "dataSource": "mx-lab", "log": { - "merakiGroup": "security_event", - "genericEvent": "security_filtering_file_scanned disposition=malicious action=block" + "merakiResult": "allowed" } }, - "expected": { - "log.eventType": "security_event", - "log.message": "security_filtering_file_scanned disposition=malicious action=block" - }, + "expected": {}, "absent": [ "actionResult" ], "rules": { - "rules/cisco/meraki/advanced_malware_protection_alerts.yml": true + "rules/cisco/meraki/advanced_malware_protection_alerts.yml": false, + "rules/cisco/meraki/air_marshal_rogue_ap.yml": false, + "rules/cisco/meraki/evil_twin_ap_detection.yml": false, + "rules/cisco/meraki/intrusion_detection_alerts.yml": false, + "rules/cisco/meraki/meraki_vpn_brute_force.yml": false, + "rules/cisco/meraki/rogue_ssid_detection.yml": false, + "rules/cisco/meraki/wireless_intrusion_attempts.yml": false } }, { - "name": "meraki_amp_clean", + "name": "no extracted Meraki identity negative control", "filter": "cisco/meraki.yml", "input": { - "dataSource": "mx-lab", - "log": { - "merakiGroup": "security_event", - "genericEvent": "security_filtering_file_scanned disposition=clean action=allow" - } - }, - "expected": { - "log.eventType": "security_event", - "log.message": "security_filtering_file_scanned disposition=clean action=allow" + "dataType": "firewall-meraki", + "dataSource": "synthetic-relay", + "log": {} }, + "expected": {}, "absent": [ - "actionResult" + "origin.ip", + "target.ip", + "actionResult", + "log.eventType" ], "rules": { - "rules/cisco/meraki/advanced_malware_protection_alerts.yml": false - } - }, - { - "name": "meraki_ids_is_not_amp", - "filter": "cisco/meraki.yml", - "input": { - "dataSource": "mx-lab", - "log": { - "merakiGroup": "security_event", - "genericEvent": "ids_alerted message: malicious traffic blocked" - } - }, - "expected": { - "log.eventType": "security_event" - }, - "rules": { - "rules/cisco/meraki/advanced_malware_protection_alerts.yml": false + "rules/cisco/meraki/advanced_malware_protection_alerts.yml": false, + "rules/cisco/meraki/air_marshal_rogue_ap.yml": false, + "rules/cisco/meraki/evil_twin_ap_detection.yml": false, + "rules/cisco/meraki/intrusion_detection_alerts.yml": false, + "rules/cisco/meraki/meraki_vpn_brute_force.yml": false, + "rules/cisco/meraki/rogue_ssid_detection.yml": false, + "rules/cisco/meraki/wireless_intrusion_attempts.yml": false } } ] diff --git a/plugins/alerts/testdata/meraki_raw.json b/plugins/alerts/testdata/meraki_raw.json new file mode 100644 index 000000000..1af7eec82 --- /dev/null +++ b/plugins/alerts/testdata/meraki_raw.json @@ -0,0 +1,1041 @@ +[ + { + "name": "AMP blocked positive", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_file_scanned url=https://files.example.invalid/test.bin src=192.0.2.8:53150 dst=198.51.100.9:80 mac=02:00:00:00:00:08 name='Synthetic test malware' sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=block", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "origin.port": 53150, + "target.port": 80, + "log.eventType": "security_event", + "log.alertType": "security_filtering_file_scanned", + "log.disposition": "malicious", + "target.url": "https://files.example.invalid/test.bin", + "actionResult": "denied", + "target.sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ] + }, + { + "name": "AMP retrospective without endpoint positive", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": { + "log.disposition": "malicious", + "target.sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "absent": [ + "origin.ip", + "target.ip", + "actionResult" + ], + "matches": [ + "advanced_malware_protection_alerts" + ] + }, + { + "name": "AMP clean", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_file_scanned url=https://files.example.invalid/test.bin src=192.0.2.8:53150 dst=198.51.100.9:80 mac=02:00:00:00:00:08 name='Synthetic test malware' sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=clean action=block", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "AMP invalid hash", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_file_scanned url=https://files.example.invalid/test.bin src=192.0.2.8:53150 dst=198.51.100.9:80 mac=02:00:00:00:00:08 name='Synthetic test malware' sha256=invalid disposition=malicious action=block", + "dataSource": "synthetic-relay", + "expected": { + "log.sha256": "invalid", + "log.malwareGroupingKey": "event-invalid-hash", + "log.malwareGroupingType": "event" + }, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ], + "id": "event-invalid-hash" + }, + { + "name": "AMP forged disposition", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_file_scanned url=https://files.example.invalid/test.bin src=192.0.2.8:53150 dst=198.51.100.9:80 mac=02:00:00:00:00:08 name='Synthetic disposition=malicious' sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=clean action=block", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "AMP absent source identity", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=allow", + "dataSource": "", + "expected": {}, + "absent": [ + "origin.ip", + "target.ip" + ], + "matches": [] + }, + { + "name": "IDS blocked positive", + "raw": "1720051237.124589040 LAB-MX security_event ids_alerted signature=1:28423:1 priority=1 timestamp=1720051237.1 dhost=02:00:00:00:00:08 direction=ingress protocol=tcp/ip src=198.51.100.9:80 dst=192.0.2.8:53023 decision=blocked action=rst message: Synthetic exploit detection", + "dataSource": "synthetic-relay", + "expected": { + "log.alertType": "ids_alerted", + "origin.ip": "198.51.100.9", + "target.ip": "192.0.2.8", + "log.priority": "1", + "log.dhost": "02:00:00:00:00:08", + "actionResult": "denied" + }, + "absent": [ + "origin.mac" + ], + "matches": [ + "intrusion_detection_alerts" + ] + }, + { + "name": "IDS ineligible priority 0", + "raw": "1720051237.124589040 LAB-MX security_event ids_alerted signature=1:28423:1 priority=0 timestamp=1720051237.1 dhost=02:00:00:00:00:08 direction=ingress protocol=tcp/ip src=198.51.100.9:80 dst=192.0.2.8:53023 decision=blocked action=rst message: Synthetic exploit detection", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "IDS ineligible priority 3", + "raw": "1720051237.124589040 LAB-MX security_event ids_alerted signature=1:28423:1 priority=3 timestamp=1720051237.1 dhost=02:00:00:00:00:08 direction=ingress protocol=tcp/ip src=198.51.100.9:80 dst=192.0.2.8:53023 decision=blocked action=rst message: Synthetic exploit detection", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "IDS ineligible priority bad", + "raw": "1720051237.124589040 LAB-MX security_event ids_alerted signature=1:28423:1 priority=bad timestamp=1720051237.1 dhost=02:00:00:00:00:08 direction=ingress protocol=tcp/ip src=198.51.100.9:80 dst=192.0.2.8:53023 decision=blocked action=rst message: Synthetic exploit detection", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "IDS ineligible priority -1", + "raw": "1720051237.124589040 LAB-MX security_event ids_alerted signature=1:28423:1 priority=-1 timestamp=1720051237.1 dhost=02:00:00:00:00:08 direction=ingress protocol=tcp/ip src=198.51.100.9:80 dst=192.0.2.8:53023 decision=blocked action=rst message: Synthetic exploit detection", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "IDS observed without blocked outcome", + "raw": "1720051237.124589040 LAB-MX security_event ids_alerted signature=1:28423:1 priority=1 timestamp=1720051237.1 dhost=02:00:00:00:00:08 direction=ingress protocol=tcp/ip src=198.51.100.9:80 dst=192.0.2.8:53023 decision=allowed action=rst message: Synthetic exploit detection", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "actionResult" + ], + "matches": [ + "intrusion_detection_alerts" + ] + }, + { + "name": "legacy IDS one endpoint positive", + "raw": "1720051237.124589040 LAB-MX ids-alerts signature=119:15:1 priority=2 timestamp=1720051237.1 direction=egress protocol=tcp/ip src=192.0.2.8:56240", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "log.alertType": "ids_alerted" + }, + "absent": [ + "target.ip", + "actionResult" + ], + "matches": [ + "intrusion_detection_alerts" + ] + }, + { + "name": "VPN explicit authentication failure positive", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=198.51.100.8Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "198.51.100.8", + "origin.port": 8748, + "log.type": "anyconnect_vpn_auth_failure", + "actionResult": "failure", + "log.vpnAuthenticationFailure": "match" + }, + "absent": [], + "matches": [ + "meraki_vpn_brute_force" + ] + }, + { + "name": "VPN explicit authentication success", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_success msg= 'Peer IP=198.51.100.8 Peer port=8748 AAA[8]: AAA authentication successful '", + "dataSource": "synthetic-relay", + "expected": { + "actionResult": "success", + "origin.ip": "198.51.100.8" + }, + "absent": [ + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN generic negotiation failure", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_general msg='VPN authentication failed text in a general notice'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "origin.ip", + "log.vpnAuthenticationFailure", + "actionResult" + ], + "matches": [] + }, + { + "name": "VPN invalid ingress ", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=198.51.100.8Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "", + "expected": {}, + "absent": [ + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid ingress unknown", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=198.51.100.8Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "unknown", + "expected": {}, + "absent": [ + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid ingress -", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=198.51.100.8Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "-", + "expected": {}, + "absent": [ + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid peer 0.0.0.0", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=0.0.0.0Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "0.0.0.0" + }, + "absent": [ + "origin.ip", + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid peer ::", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=::Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "::" + }, + "absent": [ + "origin.ip", + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid peer ::0", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=::0Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "::0" + }, + "absent": [ + "origin.ip", + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid peer 0:0:0:0:0:0:0:0", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=0:0:0:0:0:0:0:0Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "0:0:0:0:0:0:0:0" + }, + "absent": [ + "origin.ip", + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid peer ::ffff:0.0.0.0", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=::ffff:0.0.0.0Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "::ffff:0.0.0.0" + }, + "absent": [ + "origin.ip", + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN invalid peer not-an-ip", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=not-an-ipPeer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "not-an-ip" + }, + "absent": [ + "origin.ip", + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "VPN IPv6 peer positive", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg= 'Peer IP=2001:db8::Peer port[8748] AAA[8]: AAA authenticate failed retval=7 - Authentication failure '", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "2001:db8::" + }, + "absent": [], + "matches": [ + "meraki_vpn_brute_force" + ] + }, + { + "name": "AirMarshal rogue positive", + "raw": "1720051237.124589040 LAB-MX events airmarshal_events type= rogue_ssid_detected ssid='Example Lab' bssid='02:00:00:00:00:01' src='02:00:00:00:00:01' dst='02:00:00:00:00:02' wired_mac='02:00:00:00:00:03' vlan_id='0' channel='157' rssi='21' fc_type='0' fc_subtype='5'", + "dataSource": "synthetic-relay", + "expected": { + "log.eventType": "airmarshal_events", + "log.merakiGroup": "events", + "log.type": "rogue_ssid_detected", + "origin.mac": "02:00:00:00:00:01" + }, + "absent": [ + "origin.ip", + "target.ip", + "actionResult" + ], + "matches": [ + "rogue_ssid_detection", + "air_marshal_rogue_ap" + ] + }, + { + "name": "AirMarshal standalone rogue positive", + "raw": "airmarshal_events type= rogue_ssid_detected ssid='Example Lab' bssid='02:00:00:00:00:01' src='02:00:00:00:00:01' dst='02:00:00:00:00:02' wired_mac='02:00:00:00:00:03' vlan_id='0' channel='157' rssi='21' fc_type='0' fc_subtype='5'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [ + "rogue_ssid_detection", + "air_marshal_rogue_ap" + ] + }, + { + "name": "AirMarshal spoof positive", + "raw": "1720051237.124589040 LAB-MX events airmarshal_events type= ssid_spoofing_detected ssid='Example Lab' bssid='02:00:00:00:00:01' src='02:00:00:00:00:01' dst='02:00:00:00:00:02' wired_mac='02:00:00:00:00:03' vlan_id='0' channel='157' rssi='21' fc_type='0' fc_subtype='5'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [ + "evil_twin_ap_detection" + ] + }, + { + "name": "AirMarshal ordinary SSID text", + "raw": "1720051237.124589040 LAB-MX events airmarshal_events type= association ssid='rogue SSID Evil Twin' bssid='02:00:00:00:00:01' src='02:00:00:00:00:01' dst='02:00:00:00:00:02' wired_mac='02:00:00:00:00:03' vlan_id='0' channel='157' rssi='21' fc_type='0' fc_subtype='5'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "AirMarshal below existing RSSI policy", + "raw": "1720051237.124589040 LAB-MX events airmarshal_events type= rogue_ssid_detected ssid='Example Lab' bssid='02:00:00:00:00:01' src='02:00:00:00:00:01' dst='02:00:00:00:00:02' wired_mac='02:00:00:00:00:03' vlan_id='0' channel='157' rssi='-90' fc_type='0' fc_subtype='5'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [ + "rogue_ssid_detection" + ] + }, + { + "name": "wireless packet flood start positive", + "raw": "1720051237.124589040 LAB-MX events type=device_packet_flood packet='deauth' device='02:00:00:00:00:04' radio='0' state='start' alarm_id='4' dos_count='25' inter_arrival='10000'", + "dataSource": "synthetic-relay", + "expected": { + "origin.mac": "02:00:00:00:00:04", + "log.packet": "deauth", + "log.state": "start" + }, + "absent": [ + "origin.ip", + "actionResult" + ], + "matches": [ + "wireless_intrusion_attempts" + ] + }, + { + "name": "wireless packet flood end", + "raw": "1720051237.124589040 LAB-MX events type=device_packet_flood packet='deauth' device='02:00:00:00:00:04' radio='0' state='end' alarm_id='4' dos_count='25' inter_arrival='10000'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "ordinary wireless notice", + "raw": "1720051237.124589040 LAB-MX events type=association msg=\"wireless intrusion association flood\"", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "flows policy allow", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "origin.port": 55719, + "target.port": 53, + "protocol": "udp", + "actionResult": "success", + "log.eventType": "flows" + }, + "absent": [], + "matches": [] + }, + { + "name": "flows policy deny", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: deny all", + "dataSource": "synthetic-relay", + "expected": { + "actionResult": "denied" + }, + "absent": [], + "matches": [] + }, + { + "name": "firewall policy allow", + "raw": "1720051237.124589040 LAB-MX firewall src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "origin.port": 55719, + "target.port": 53, + "protocol": "udp", + "actionResult": "success", + "log.eventType": "firewall" + }, + "absent": [], + "matches": [] + }, + { + "name": "firewall policy deny", + "raw": "1720051237.124589040 LAB-MX firewall src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: deny all", + "dataSource": "synthetic-relay", + "expected": { + "actionResult": "denied" + }, + "absent": [], + "matches": [] + }, + { + "name": "cellular_firewall policy allow", + "raw": "1720051237.124589040 LAB-MX cellular_firewall src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "origin.port": 55719, + "target.port": 53, + "protocol": "udp", + "actionResult": "success", + "log.eventType": "cellular_firewall" + }, + "absent": [], + "matches": [] + }, + { + "name": "cellular_firewall policy deny", + "raw": "1720051237.124589040 LAB-MX cellular_firewall src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: deny all", + "dataSource": "synthetic-relay", + "expected": { + "actionResult": "denied" + }, + "absent": [], + "matches": [] + }, + { + "name": "vpn_firewall policy allow", + "raw": "1720051237.124589040 LAB-MX vpn_firewall src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "origin.port": 55719, + "target.port": 53, + "protocol": "udp", + "actionResult": "success", + "log.eventType": "vpn_firewall" + }, + "absent": [], + "matches": [] + }, + { + "name": "vpn_firewall policy deny", + "raw": "1720051237.124589040 LAB-MX vpn_firewall src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: deny all", + "dataSource": "synthetic-relay", + "expected": { + "actionResult": "denied" + }, + "absent": [], + "matches": [] + }, + { + "name": "MR flow deny", + "raw": "1720051237.124589040 LAB-MX flows deny src=192.0.2.8 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53", + "dataSource": "synthetic-relay", + "expected": { + "actionResult": "denied" + }, + "absent": [], + "matches": [] + }, + { + "name": "L7 blocked", + "raw": "1720051237.124589040 LAB-MX l7_firewall src=192.0.2.8 dst=198.51.100.9 protocol=tcp sport=59526 dport=443 decision=blocked", + "dataSource": "synthetic-relay", + "expected": { + "actionResult": "denied" + }, + "absent": [], + "matches": [] + }, + { + "name": "flow source invalid not-an-ip", + "raw": "1720051237.124589040 LAB-MX flows src=not-an-ip dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "not-an-ip" + }, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "flow source invalid 0.0.0.0", + "raw": "1720051237.124589040 LAB-MX flows src=0.0.0.0 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "0.0.0.0" + }, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "flow source invalid ::0", + "raw": "1720051237.124589040 LAB-MX flows src=::0 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "::0" + }, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "flow source invalid 0:0:0:0:0:0:0:0", + "raw": "1720051237.124589040 LAB-MX flows src=0:0:0:0:0:0:0:0 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "0:0:0:0:0:0:0:0" + }, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "flow source invalid ::ffff:0.0.0.0", + "raw": "1720051237.124589040 LAB-MX flows src=::ffff:0.0.0.0 dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.sourceIp": "::ffff:0.0.0.0" + }, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "flow source IPv6 unspecified suffix preserved", + "raw": "1720051237.124589040 LAB-MX flows src=2001:db8:: dst=198.51.100.9 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "2001:db8::" + }, + "absent": [], + "matches": [] + }, + { + "name": "flow target invalid not-an-ip", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=not-an-ip mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.destinationIp": "not-an-ip" + }, + "absent": [ + "target.ip" + ], + "matches": [] + }, + { + "name": "flow target invalid 0.0.0.0", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=0.0.0.0 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.destinationIp": "0.0.0.0" + }, + "absent": [ + "target.ip" + ], + "matches": [] + }, + { + "name": "flow target invalid ::0", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=::0 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.destinationIp": "::0" + }, + "absent": [ + "target.ip" + ], + "matches": [] + }, + { + "name": "flow target invalid 0:0:0:0:0:0:0:0", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=0:0:0:0:0:0:0:0 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.destinationIp": "0:0:0:0:0:0:0:0" + }, + "absent": [ + "target.ip" + ], + "matches": [] + }, + { + "name": "flow target invalid ::ffff:0.0.0.0", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=::ffff:0.0.0.0 mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "log.destinationIp": "::ffff:0.0.0.0" + }, + "absent": [ + "target.ip" + ], + "matches": [] + }, + { + "name": "flow target IPv6 unspecified suffix preserved", + "raw": "1720051237.124589040 LAB-MX flows src=192.0.2.8 dst=2001:db8:: mac=02:00:00:00:00:08 protocol=udp sport=55719 dport=53 pattern: allow all", + "dataSource": "synthetic-relay", + "expected": { + "target.ip": "2001:db8::" + }, + "absent": [], + "matches": [] + }, + { + "name": "8021x switch port and identity", + "raw": "1720051237.124589040 LAB-MX events type=8021x_auth port='3' identity='test@example.invalid'", + "dataSource": "synthetic-relay", + "expected": { + "log.switchPort": "3", + "origin.user": "test@example.invalid" + }, + "absent": [ + "origin.port", + "origin.ip", + "actionResult" + ], + "matches": [] + }, + { + "name": "MR response timing not IP", + "raw": "1720051237.124589040 LAB-MX events type=disassociation ip_resp='1.597000' ip_src='192.0.2.8'", + "dataSource": "synthetic-relay", + "expected": { + "log.ipResp": "1.597000", + "origin.ip": "192.0.2.8" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "native RFC3164 wrapper", + "raw": "<134>Sep 17 10:30:00 relay.example 1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ] + }, + { + "name": "native PRI wrapper", + "raw": "<134>1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ] + }, + { + "name": "native RFC3164 relay sequence", + "raw": "Sep 17 10:30:00 192.0.2.200 1 1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ] + }, + { + "name": "unrelated syslog containing embedded Meraki tokens", + "raw": "2026-09-17T10:30:00Z server envoy-access: POST /api?message=1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "origin.ip", + "target.ip", + "log.eventType", + "actionResult" + ], + "matches": [] + }, + { + "name": "quoted field cannot forge remote peer", + "raw": "1720051237.124589040 LAB-MX events type=anyconnect_vpn_auth_failure msg='ordinary src=198.51.100.8 dst=192.0.2.7'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "origin.ip", + "target.ip", + "log.vpnAuthenticationFailure" + ], + "matches": [] + }, + { + "name": "AMP missing hash event-one", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": { + "log.malwareGroupingKey": "event-one", + "log.malwareGroupingType": "event" + }, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ], + "id": "event-one" + }, + { + "name": "AMP missing hash event-two", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": { + "log.malwareGroupingKey": "event-two", + "log.malwareGroupingType": "event" + }, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ], + "id": "event-two" + }, + { + "name": "AMP missing hash and ingress ID", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "log.malwareGroupingKey" + ], + "matches": [], + "id": "" + }, + { + "name": "AMP validated hash grouping", + "raw": "1720051237.124589040 LAB-MX security_event security_filtering_disposition_change name=Synthetic sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa disposition=malicious action=allow", + "dataSource": "synthetic-relay", + "expected": { + "log.malwareGroupingKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "log.malwareGroupingType": "hash", + "target.sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "absent": [], + "matches": [ + "advanced_malware_protection_alerts" + ] + }, + { + "name": "security payload cannot forge AMP class", + "raw": "1720051237.124589040 LAB-MX security_event ids_alerted priority=3 name='text security_filtering_file_scanned disposition=malicious sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa action=block'", + "dataSource": "synthetic-relay", + "expected": { + "log.alertType": "ids_alerted" + }, + "absent": [], + "matches": [] + }, + { + "name": "event payload cannot forge AirMarshal class", + "raw": "1720051237.124589040 LAB-MX events type=ordinary msg='text airmarshal_events type=rogue_ssid_detected bssid=02:00:00:00:00:01'", + "dataSource": "synthetic-relay", + "expected": { + "log.eventType": "events", + "log.type": "ordinary" + }, + "absent": [], + "matches": [] + }, + { + "name": "ordinary message cannot manufacture DHCP success", + "raw": "1720051237.124589040 LAB-MX events type=ordinary msg='dhcp lease of ip 192.0.2.9'", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "origin.ip", + "actionResult" + ], + "matches": [] + }, + { + "name": "URL request positive", + "raw": "1720051237.124589040 LAB-MX urls src=192.0.2.8:63735 dst=198.51.100.9:80 mac=02:00:00:00:00:08 request: GET https://app.example.invalid/a?q=b", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "target.url": "https://app.example.invalid/a?q=b", + "log.method": "GET" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "VPN connectivity up", + "raw": "1720051237.124589040 LAB-MX events type=vpn_connectivity_change vpn_type='site-to-site' peer_contact='198.51.100.9:51856' peer_ident='synthetic-peer' connectivity='true'", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "198.51.100.9", + "origin.port": 51856, + "actionResult": "success" + }, + "absent": [], + "matches": [] + }, + { + "name": "VPN connectivity down", + "raw": "1720051237.124589040 LAB-MX events type=vpn_connectivity_change vpn_type='site-to-site' peer_contact='198.51.100.9:51856' peer_ident='synthetic-peer' connectivity='false'", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "198.51.100.9" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "DHCP no offers", + "raw": "1720051237.124589040 LAB-MX events dhcp no offers for mac 02:00:00:00:00:08 host = 192.0.2.8", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "origin.mac": "02:00:00:00:00:08" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "DHCP lease", + "raw": "1720051237.124589040 LAB-MX events dhcp lease of ip 192.0.2.8 from server mac 02:00:00:00:00:09 for client mac 02:00:00:00:00:08 from router 192.0.2.1 on subnet 255.255.255.0 with dns 192.0.2.53, 198.51.100.53", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "origin.mac": "02:00:00:00:00:08", + "actionResult": "success" + }, + "absent": [], + "matches": [] + }, + { + "name": "Cellular up", + "raw": "1720051237.124589040 LAB-MX events Cellular connection up", + "dataSource": "synthetic-relay", + "expected": { + "log.connectionState": "up", + "actionResult": "success" + }, + "absent": [], + "matches": [] + }, + { + "name": "Cellular down", + "raw": "1720051237.124589040 LAB-MX events Cellular connection down", + "dataSource": "synthetic-relay", + "expected": { + "log.connectionState": "down" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "STP interface blocked", + "raw": "1720051237.124589040 LAB-MX events Port 5 received an STP BPDU from 02:00:00:00:00:09 so the port was blocked", + "dataSource": "synthetic-relay", + "expected": { + "log.switchPort": "5", + "origin.mac": "02:00:00:00:00:09", + "actionResult": "denied" + }, + "absent": [ + "origin.port" + ], + "matches": [] + }, + { + "name": "native MR disassociation source", + "raw": "1720051237.124589040 LAB-MX events type=disassociation ip_resp='1.597000' ip_src='192.0.2.8'", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "log.ipResp": "1.597000" + }, + "absent": [ + "log.ipResp.geolocation" + ], + "matches": [] + }, + { + "name": "VPN quoted payload cannot supply peer or outcome", + "raw": "1720051237.124589040 LAB-MX events type=vpn_connectivity_change msg=\"type=vpn_connectivity_change vpn_type='site-to-site'peer_contact='198.51.100.66:443'peer_ident='x'connectivity='true'\"", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "origin.ip", + "actionResult" + ], + "matches": [] + }, + { + "name": "disassociation quoted names cannot supply success", + "raw": "1720051237.124589040 LAB-MX events type=disassociation msg=\"radio='0' vap='1' channel='6' auth_neg_dur='1' last_auth_ago='1'\"", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "origin.ip", + "actionResult", + "log.radio", + "log.channel" + ], + "matches": [] + }, + { + "name": "disassociation complete metadata remains vendor", + "raw": "1720051237.124589040 LAB-MX events type=disassociation radio='0' vap='1' channel='6' reason='8' instigator='2' duration='11979.728000' auth_neg_dur='24000' last_auth_ago='5.074000' is_wpa='1' full_conn='1.597000' ip_resp='1.597000' ip_src='192.0.2.8' arp_resp='1.265000' arp_src='192.0.2.8' dns_server='192.0.2.53' dns_req_rtt='35000' dns_resp='1.316000' aid='1813578850'", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "log.radio": "0", + "log.channel": "6", + "log.authNegDur": "24000", + "log.lastAuthAgo": "5.074000", + "log.ipResp": "1.597000", + "log.dnsServer": "192.0.2.53" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "embedded legacy header cannot reclassify application data", + "raw": "application data=\"Sep 17 2026 12:00:00192.0.2.44:1720051237.124589040LAB-MX flowssrc=192.0.2.8dst=198.51.100.9protocol=udp sport=55719dport=53pattern:allow all\"", + "dataSource": "synthetic-relay", + "expected": {}, + "absent": [ + "origin.ip", + "target.ip", + "log.eventType", + "actionResult" + ], + "matches": [] + }, + { + "name": "leading legacy header literal compatibility", + "raw": "Sep 17 2026 12:00:00192.0.2.44:1720051237.124589040LAB-MX flowssrc=192.0.2.8dst=198.51.100.9protocol=udp sport=55719dport=53pattern:allow all", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "log.eventType": "flows", + "actionResult": "success" + }, + "absent": [], + "matches": [] + }, + { + "name": "leading legacy PRI header literal compatibility", + "raw": "<134>Sep 17 2026 12:00:00192.0.2.44:1720051237.124589040LAB-MX flowssrc=192.0.2.8dst=198.51.100.9protocol=udp sport=55719dport=53pattern:allow all", + "dataSource": "synthetic-relay", + "expected": { + "origin.ip": "192.0.2.8", + "target.ip": "198.51.100.9", + "log.eventType": "flows", + "actionResult": "success" + }, + "absent": [], + "matches": [] + } +] diff --git a/rules/cisco/meraki/advanced_malware_protection_alerts.yml b/rules/cisco/meraki/advanced_malware_protection_alerts.yml index 5cbe0ce89..f9dfce9a3 100644 --- a/rules/cisco/meraki/advanced_malware_protection_alerts.yml +++ b/rules/cisco/meraki/advanced_malware_protection_alerts.yml @@ -9,30 +9,32 @@ impact: availability: 2 category: Execution technique: "T1204.002 - User Execution: Malicious File" -adversary: origin +adversary: target references: + - https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples - https://documentation.meraki.com/MX/Content_Filtering_and_Threat_Protection/Advanced_Malware_Protection_(AMP) - https://attack.mitre.org/techniques/T1204/002/ description: | - Detects when Meraki Advanced Malware Protection (AMP) identifies malicious files being downloaded or executed on the network. This includes retrospective alerts where files previously considered safe are later identified as malicious. + Detects a malicious file disposition reported by Meraki Advanced Malware Protection (AMP), including retrospective reclassification of a previously downloaded file. A scanned download identifies the requesting client as the event origin and the remote file server as the event target; the alert therefore treats the remote server as the adversary side. Retrospective records can lack both endpoints. A disposition does not establish execution or compromise. A valid SHA-256 groups reports for the same file. When the hash is missing or malformed, the actual ingress event ID keeps that record separate; it is not treated as a file or attacker identity. Next Steps: - 1. Immediately isolate the affected system(s) from the network to prevent lateral movement + 1. Check whether the download was blocked and identify any affected client from the original transfer record 2. Review the malware details including file hash, name, and threat severity in the Meraki dashboard 3. Check if the malicious file was executed or only downloaded - 4. Scan other systems for the same file hash to identify additional infections - 5. Review network traffic logs from the affected IP for suspicious communications + 4. Search other systems for the same file hash and investigate any matches + 5. Review network traffic from any identified client for suspicious communications 6. If file was executed, perform full system scan and consider reimaging the affected device 7. Update endpoint protection signatures and ensure all systems are patched 8. Document the incident and update security policies if needed where: | equals("log.eventType", "security_event") && - (((startsWith("log.message", "security_filtering_file_scanned") || - startsWith("log.message", "security_filtering_disposition_change")) && - contains("log.message", "disposition=malicious")) || - equals("log.action", "malware_blocked") || - contains("log.eventName", "Advanced Malware Protection")) + oneOf("log.alertType", ["security_filtering_file_scanned", "security_filtering_disposition_change"]) && + equals("log.disposition", "malicious") && + oneOf("log.malwareGroupingType", ["hash", "event"]) && + exists("log.malwareGroupingKey") && !equals("log.malwareGroupingKey", "") && + exists("dataSource") && !oneOf("dataSource", ["", "unknown", "-"]) groupBy: - lastEvent.dataSource - - adversary.host - - adversary.ip + - lastEvent.log.merakiType + - lastEvent.log.malwareGroupingType + - lastEvent.log.malwareGroupingKey diff --git a/rules/cisco/meraki/air_marshal_rogue_ap.yml b/rules/cisco/meraki/air_marshal_rogue_ap.yml index 92951d76f..b32eb1534 100644 --- a/rules/cisco/meraki/air_marshal_rogue_ap.yml +++ b/rules/cisco/meraki/air_marshal_rogue_ap.yml @@ -11,6 +11,7 @@ category: Initial Access technique: "T1200 - Hardware Additions" adversary: origin references: + - https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples - https://documentation.meraki.com/MR/Monitoring_and_Reporting/Air_Marshal - https://attack.mitre.org/techniques/T1200/ description: | @@ -30,4 +31,6 @@ where: | exists("log.bssid") && greaterOrEqual("log.rssi", -50) groupBy: - - adversary.host + - lastEvent.dataSource + - lastEvent.log.merakiType + - lastEvent.log.bssid diff --git a/rules/cisco/meraki/evil_twin_ap_detection.yml b/rules/cisco/meraki/evil_twin_ap_detection.yml index 44822c564..de515c2cd 100644 --- a/rules/cisco/meraki/evil_twin_ap_detection.yml +++ b/rules/cisco/meraki/evil_twin_ap_detection.yml @@ -11,6 +11,7 @@ category: Wireless Security technique: "T1557 - Adversary-in-the-Middle" adversary: origin references: + - https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples - https://documentation.meraki.com/MR/Monitoring_and_Reporting/Air_Marshal - https://attack.mitre.org/techniques/T1557/ description: | @@ -25,10 +26,9 @@ description: | 6. Review network traffic from affected clients for signs of credential theft where: | equals("log.eventType", "airmarshal_events") && - (equals("log.type", "ssid_spoofing") || - equals("log.type", "rogue_ssid_detected") || - (contains("log.message", "SSID Spoofing") || contains("log.message", "Evil Twin"))) && + equals("log.type", "ssid_spoofing_detected") && exists("log.bssid") groupBy: + - lastEvent.dataSource + - lastEvent.log.merakiType - lastEvent.log.bssid - - adversary.host diff --git a/rules/cisco/meraki/intrusion_detection_alerts.yml b/rules/cisco/meraki/intrusion_detection_alerts.yml index 7f187618c..769c691c4 100644 --- a/rules/cisco/meraki/intrusion_detection_alerts.yml +++ b/rules/cisco/meraki/intrusion_detection_alerts.yml @@ -25,11 +25,12 @@ description: | 6. Review firewall rules and IPS signatures for potential tuning 7. Document the incident and update threat intelligence feeds if applicable where: | - equals("log.eventType", "security_event") && + oneOf("log.eventType", ["security_event", "ids-alerts"]) && equals("log.alertType", "ids_alerted") && - lessOrEqual("log.priority", 2) && - exists("origin.ip") && - exists("target.ip") + oneOf("log.priority", [1, 2]) && + exists("origin.ip") groupBy: + - lastEvent.dataSource + - lastEvent.log.merakiType - lastEvent.log.signature - adversary.ip diff --git a/rules/cisco/meraki/meraki_vpn_brute_force.yml b/rules/cisco/meraki/meraki_vpn_brute_force.yml index 639d851fa..e8e6ce5e9 100644 --- a/rules/cisco/meraki/meraki_vpn_brute_force.yml +++ b/rules/cisco/meraki/meraki_vpn_brute_force.yml @@ -11,10 +11,11 @@ category: Credential Access technique: "T1110 - Brute Force" adversary: origin references: + - https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples - https://documentation.meraki.com/MX/Client_VPN/Client_VPN_Overview - https://attack.mitre.org/techniques/T1110/ description: | - Detects multiple failed client VPN authentication attempts from the same source IP on Meraki MX appliances, indicating potential brute force attacks against VPN credentials. + Detects at least ten explicit AnyConnect VPN authentication failures within fifteen minutes from the same source IP, collector, and Meraki appliance. Connection teardown and site-to-site tunnel negotiation failures do not establish a credential failure. Next Steps: 1. Review the source IP address and check geographic location @@ -24,17 +25,29 @@ description: | 5. Review VPN authentication settings and ensure MFA is enabled 6. Notify the targeted user if the account is legitimate where: | - (oneOf("log.eventType", ["vpn_auth_failure", "client_vpn_auth_failure"]) || - (contains("log.message", "VPN") && contains("log.message", ["auth fail", "authentication failed", "invalid credentials"]))) && - exists("origin.ip") + equals("log.eventType", "events") && + equals("log.type", "anyconnect_vpn_auth_failure") && + exists("origin.ip") && + exists("log.merakiType") && !oneOf("log.merakiType", ["", "unknown", "-"]) && + exists("dataSource") && !oneOf("dataSource", ["", "unknown", "-"]) afterEvents: - indexPattern: v11-log-firewall-meraki-* with: + - field: dataSource + operator: filter_term + value: '{{.dataSource}}' + - field: log.merakiType + operator: filter_term + value: '{{.log.merakiType}}' - field: origin.ip operator: filter_term value: '{{.origin.ip}}' + - field: log.vpnAuthenticationFailure + operator: filter_term + value: 'match' within: 15m count: 10 groupBy: + - lastEvent.dataSource + - lastEvent.log.merakiType - adversary.ip - - adversary.user diff --git a/rules/cisco/meraki/rogue_ssid_detection.yml b/rules/cisco/meraki/rogue_ssid_detection.yml index 5d3b227cb..bfb4a9d9a 100644 --- a/rules/cisco/meraki/rogue_ssid_detection.yml +++ b/rules/cisco/meraki/rogue_ssid_detection.yml @@ -14,7 +14,7 @@ references: - https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples - https://attack.mitre.org/techniques/T1200/ description: | - Detects when a rogue SSID is identified in the wireless environment. This could indicate an evil twin attack or unauthorized access point deployment attempting to intercept wireless traffic or credentials. + Detects the legacy Air Marshal rogue SSID event, indicating an access point detected on the wired LAN. A rogue SSID does not by itself establish corporate SSID spoofing; the separate spoofing event is used for Evil Twin detection. Next Steps: 1. Immediately investigate the rogue access point's physical location using the MAC address @@ -25,10 +25,9 @@ description: | 6. Update wireless intrusion detection policies if needed 7. Notify security team and facilities management for potential physical security breach where: | - equals("log.eventType", "rogue_ssid_detected") || - (equals("log.type", "airmarshal_events") && - equals("log.subtype", "rogue_ssid_detected")) || - (contains("log.message", "rogue") && - contains("log.message", "SSID")) + equals("log.eventType", "airmarshal_events") && + equals("log.type", "rogue_ssid_detected") groupBy: - - adversary.mac + - lastEvent.dataSource + - lastEvent.log.merakiType + - lastEvent.log.bssid diff --git a/rules/cisco/meraki/wireless_intrusion_attempts.yml b/rules/cisco/meraki/wireless_intrusion_attempts.yml index 2e031e553..dc312abec 100644 --- a/rules/cisco/meraki/wireless_intrusion_attempts.yml +++ b/rules/cisco/meraki/wireless_intrusion_attempts.yml @@ -14,7 +14,7 @@ references: - https://documentation.meraki.com/General_Administration/Monitoring_and_Reporting/Syslog_Event_Types_and_Log_Samples - https://attack.mitre.org/techniques/T1595/002/ description: | - Detects wireless intrusion attempts including deauthentication attacks, association floods, and other wireless-specific attack patterns that could compromise the wireless network integrity. + Detects the start of a wireless packet flood reported by Meraki, including the documented deauthentication packet event. The matching end-of-flood event is recovery telemetry and does not trigger a new alert. Next Steps: 1. Review the wireless access point logs for the affected device @@ -24,14 +24,11 @@ description: | 5. Consider implementing additional wireless monitoring and detection capabilities 6. Document the incident and update security policies if needed where: | - equals("log.eventType", "wids_alerted") || - (equals("log.type", "airmarshal_events") && - (contains("log.subtype", "attack") || - contains("log.subtype", "flood") || - contains("log.subtype", "deauth"))) || - (contains("log.message", "deauthentication attack") || - contains("log.message", "association flood") || - contains("log.message", "wireless intrusion")) + equals("log.eventType", "events") && + equals("log.type", "device_packet_flood") && + equals("log.state", "start") deduplicateBy: - - adversary.ip - - target.mac + - lastEvent.dataSource + - lastEvent.log.merakiType + - adversary.mac + - lastEvent.log.packet