From c85d581c9eb1ae3bbb24861f992051bdf604fa2f Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 16 Sep 2026 20:34:35 -0400 Subject: [PATCH] fix: align MikroTik filter and rule contracts --- filters/audits/mikrotik.md | 40 +++++++++++ filters/mikrotik/mikrotik-fw.yml | 71 ++++++++++++++++++- .../testdata/filter-contracts/mikrotik.json | 58 +++++++++++++++ .../mikrotik_fw/ssh_brute_force_attempts.yml | 6 +- 4 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 filters/audits/mikrotik.md create mode 100644 plugins/alerts/testdata/filter-contracts/mikrotik.json diff --git a/filters/audits/mikrotik.md b/filters/audits/mikrotik.md new file mode 100644 index 000000000..822a9a349 --- /dev/null +++ b/filters/audits/mikrotik.md @@ -0,0 +1,40 @@ +# MikroTik normalization and rule review + +Normalize denial/IP/protocol values and align SSH predicates/history fields with surviving filter output. + +This draft targets UTMStack `v11`. It contains 1 filter changes +and 1 rule changes for this technology only. Review covered +1 filter configurations and 6 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. +- 3 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/mikrotik.json`. Apply that support before running `go test ./...` in `plugins/alerts`. + +The model starts from synthetic extraction results. It does not run complex grok, +JSON/KV/XML/CSV extraction, time conversion, dynamic plugins, historical OpenSearch +queries, or the closed EventProcessor. Raw vendor logs and resulting alerts must +still be checked in staging before rollout. No customer false-positive reduction +has been measured and no production rollout is included. + + + +## References + +- [SDK schema](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) +- [Filter steps](https://github.com/threatwinds/go-sdk/wiki/Filter-Steps-Reference) +- [Standard event schema](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) +- [Rule implementation](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) + +`afterEvents`, empty noncapturing grok names, supported numeric strings, and custom +`log.*` fields are accepted. Existing textual protocol casing and vendor action names +are preserved unless a concrete consumer mismatch requires correction. diff --git a/filters/mikrotik/mikrotik-fw.yml b/filters/mikrotik/mikrotik-fw.yml index 411cd1242..f9c2b0058 100644 --- a/filters/mikrotik/mikrotik-fw.yml +++ b/filters/mikrotik/mikrotik-fw.yml @@ -210,8 +210,8 @@ pipeline: function: string params: key: actionResult - value: "blocked" - where: 'exists("log.action") && equals("log.action", "drop")' + value: denied + where: exists("log.action") && equals("log.action", "drop") # Removing unused fields - delete: @@ -221,4 +221,69 @@ pipeline: - log.trash3 - log.trash4 - log.trash5 - - log.restData \ No newline at end of file + - log.restData + + # 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/mikrotik.json b/plugins/alerts/testdata/filter-contracts/mikrotik.json new file mode 100644 index 000000000..42d5356e1 --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/mikrotik.json @@ -0,0 +1,58 @@ +{ + "technology": "MikroTik", + "filters": [ + "filters/mikrotik/mikrotik-fw.yml" + ], + "rules": [ + "rules/mikrotik/mikrotik_fw/dns_cache_poisoning.yml", + "rules/mikrotik/mikrotik_fw/mikrotik_dns_redirection.yml", + "rules/mikrotik/mikrotik_fw/mikrotik_socks_proxy.yml", + "rules/mikrotik/mikrotik_fw/mikrotik_user_creation.yml", + "rules/mikrotik/mikrotik_fw/routeros_brute_force_attempts.yml", + "rules/mikrotik/mikrotik_fw/ssh_brute_force_attempts.yml" + ], + "fixtures": [ + { + "name": "mikrotik_valid_source_ip", + "filter": "mikrotik/mikrotik-fw.yml", + "input": { + "origin": { + "ip": "2001:db8::7" + } + }, + "expected": { + "origin.ip": "2001:db8::7" + }, + "absent": [], + "rules": {} + }, + { + "name": "mikrotik_hostname_not_ip", + "filter": "mikrotik/mikrotik-fw.yml", + "input": { + "origin": { + "ip": "lab-host" + } + }, + "expected": { + "log.unparsedOriginIp": "lab-host" + }, + "absent": [ + "origin.ip" + ], + "rules": {} + }, + { + "name": "mikrotik_numeric_protocol", + "filter": "mikrotik/mikrotik-fw.yml", + "input": { + "protocol": 6 + }, + "expected": { + "protocol": "TCP" + }, + "absent": [], + "rules": {} + } + ] +} diff --git a/rules/mikrotik/mikrotik_fw/ssh_brute_force_attempts.yml b/rules/mikrotik/mikrotik_fw/ssh_brute_force_attempts.yml index 28eb4ac9a..43eca7e4d 100644 --- a/rules/mikrotik/mikrotik_fw/ssh_brute_force_attempts.yml +++ b/rules/mikrotik/mikrotik_fw/ssh_brute_force_attempts.yml @@ -24,9 +24,9 @@ description: | 5. Ensure SSH access is restricted to authorized management networks only 6. Review MikroTik firewall rules and consider disabling SSH access from untrusted networks where: | - equals("target.port", 22) && equals("protocol", "tcp") && exists("origin.ip") && + equals("target.port", 22) && equalsIgnoreCase("protocol", "tcp") && exists("origin.ip") && (contains("log.chain", "input") || contains("log.action", "drop") - || (contains("log.topics", "system") && contains("log.kvMessage", "ssh"))) + || (contains("log.topics", "system") && contains("raw", "ssh"))) afterEvents: - indexPattern: v11-log-firewall-mikrotik-* with: @@ -38,7 +38,7 @@ afterEvents: value: '22' - field: protocol operator: filter_term - value: 'tcp' + value: '{{.protocol}}' within: 15m count: 10 groupBy: