Wazuh Rule-Priorität richtig verstehen: Warum „Catch-all“-Regeln gewinnen und wie man Audit-Defaults ohne Alert-Flut baut

Einleitung

Beim Bau eigener Wazuh-Rulesets entsteht schnell der Wunsch nach einer „Auffangregel“: Alles, was nicht spezifisch gematcht wird, soll trotzdem klassifiziert, geloggt und später analysierbar sein. Genau hier stolpern viele – weil Wazuh Regeln nicht nach Rule-ID priorisiert, sondern nach Evaluierungslogik (insbesondere Severity/Level, Rule-Chain und Matching). Das Ergebnis: Eine Catch-all-Regel feuert immer, während die eigentlich spezifischen Regeln nie zum Zug kommen. Dieser Artikel erklärt die Mechanik und zeigt ein praxistaugliches Design, das sowohl Korrelationsregeln (<if_matched_sid>) ermöglicht als auch Alert-Spam verhindert.

Ausgangslage / Problemstellung

In einem Custom-Ruleset für MikroTik/Firewall-Logs existieren:

  • Eine Basisregel (Parent) für die Decoder-Klasse firewall-base
  • Drei spezifische Child-Regeln, die über <field name="in"> / <field name="out"> Traffic-Richtungen unterscheiden (LAN→WAN, LAN→LAN, WAN→LAN)
  • Eine Default-/Audit-Regel, die alle übrigen Events auffangen soll

Trotz spezifischer Child-Regeln wird immer die Audit-Regel 120000 ausgelöst. Zusätzlich besteht die Anforderung, später Korrelationsregeln wie Portscan-Detection zu nutzen:

<rule id="100014" level="12" frequency="20" timeframe="60">
  <if_matched_sid>100004</if_matched_sid>
  <same_srcip />
  <same_dstip />
  <different_dstport />
  <description>Port scan detected: IP $(srcip) scan $(dstip).</description>
</rule>

Nebenbedingung:

  • Die „Basis“-Traffic-Regeln sollen möglichst keine Alerts erzeugen (UI nicht „zumüllen“), aber als Grundlage für Korrelation zählen können.

Technische Analyse

1) Rule-ID ist keine Priorität

Die Nummer 120000 ist nicht „stärker“ als 100002. Rule-IDs dienen Identifikation, nicht Sortierung.

2) Warum die Catch-all-Regel gewinnt

Wenn mehrere Regeln denselben Parent haben (<if_sid>100000</if_sid>), hängt das Ergebnis primär von der Rule-Sverity (level) und sekundär von der Lade-/Reihenfolge ab. Entscheidend: Eine Catch-all-Regel ohne weitere Bedingungen matcht alles, was den Parent matched.

In der Praxis bedeutet das:

  • Hat die Catch-all-Regel ein höheres Level als die spezifischen Regeln, wird sie bevorzugt und kann die Verarbeitung „beenden“.
  • Selbst wenn spezifische Regeln existieren, können sie praktisch nie greifen, wenn vorher eine „breite“ Regel mit höherem Level erfolgreich matcht.

3) Das eigentliche Dilemma: „Nicht alerten“ vs. „Korrelierbar sein“

Viele Nutzer setzen Level 0 oder sehr niedrig, um keine Alerts zu bekommen. Gleichzeitig verlangt Korrelation (frequency/timeframe + <if_matched_sid>) eine Regelbasis, die als „match“ im Engine-Kontext nutzbar ist.

Der Fehler ist oft nicht „Level zu hoch“, sondern:

  • Catch-all-Level ist nicht unterhalb der spezifischen Regeln
  • und/oder es fehlt eine zweistufige Default-Kette, um Audit-Klassifizierung und Alert-Triggering sauber zu trennen.

Lösung / Best Practices

Zielarchitektur

  1. Parent-Regel klassifiziert die Decoder-Familie.
  2. Spezifische Child-Regeln haben ein Level, das:
    • hoch genug ist, um gegenüber Catch-all zu gewinnen
    • aber niedrig genug, um bei Standard-Alert-Schwelle keine Alerts zu erzeugen
  3. Catch-all wird zweistufig gebaut:
    • Stage 1: immer matchen, sehr niedriger Level (klassifizieren, gruppieren)
    • Stage 2: optionaler Alert-Trigger, nur wenn gewünscht (oder später per Child-Regeln/Korrelation)

Konkrete Umsetzung: Spezifische Regeln auf Level 2, Catch-all als 2-Stufen-Default

Unter Standard-Konfiguration (<log_alert_level> typischerweise 3) erzeugen Level 2-Regeln keine Alerts, sind aber „echt genug“, um als Basis für Korrelationslogik zu dienen und gegenüber noch niedrigeren Defaults zu gewinnen.

<group name="mikrotik,">

  <!-- Parent: Firewall-Decoder-Klasse -->
  <rule id="100000" level="0">
    <decoded_as>firewall-base</decoded_as>
    <description>Logs Firewall</description>
  </rule>

  <!-- Spezifische Richtungsregeln: nicht alertend (bei log_alert_level=3), aber priorisiert -->
  <rule id="100002" level="2">
    <if_sid>100000</if_sid>
    <field name="in">LAN</field>
    <field name="out">WAN|WAN2|WAN3</field>
    <description>Outbound stream detected: $(in) to $(out).</description>
    <group>firewall_traffic,mikrotik</group>
  </rule>

  <rule id="100003" level="2">
    <if_sid>100000</if_sid>
    <field name="in">LAN</field>
    <field name="out">LAN</field>
    <description>Internal traffic detected at the antenna.</description>
    <group>firewall_traffic,mikrotik</group>
  </rule>

  <rule id="100004" level="2">
    <if_sid>100000</if_sid>
    <field name="in">WAN|WAN2|WAN3</field>
    <field name="out">LAN</field>
    <description>External traffic detected at the antenna.</description>
    <group>firewall_traffic,mikrotik</group>
  </rule>

  <!-- Catch-all Stage 1: immer matchen, aber noch niedriger als die spezifischen Regeln -->
  <rule id="120000" level="1">
    <if_sid>100000</if_sid>
    <description>Firewall audit logs (catch-all)</description>
    <group>audit,mikrotik</group>
  </rule>

  <!-- Catch-all Stage 2: optional alertend, falls man wirklich Alerts für "unclassified" will -->
  <rule id="120001" level="3">
    <if_sid>120000</if_sid>
    <description>Firewall audit logs (unclassified event)</description>
    <group>audit,mikrotik</group>
  </rule>

</group>

Wichtige Details:

  • Trailing comma in <group> vermeiden (<group>audit,</group> ist fehleranfällig). Besser: audit,mikrotik ohne Schlusskomma.
  • Die Catch-all Stage 1 ist bewusst Level 1, damit sie nur greift, wenn Level-2-Regeln nicht greifen.
  • Stage 2 ist optional: Wer gar keine Alerts aus dem Catch-all möchte, lässt 120001 weg oder setzt Level unter die Alert-Schwelle.

Korrelationsregel (Portscan) sauber anbinden

Mit obigem Modell bleibt 100004 Level 2 – und kann damit problemlos als Basis für <if_matched_sid> verwendet werden, ohne die UI zu fluten.

Beispiel (wie gehabt), aber Gruppen sauber:

<rule id="100014" level="12" frequency="20" timeframe="60">
  <if_matched_sid>100004</if_matched_sid>
  <same_srcip />
  <same_dstip />
  <different_dstport />
  <description>Port scan detected: IP $(srcip) scan $(dstip).</description>
  <group>port_scan,network,mikrotik</group>
</rule>

Damit passiert genau das Gewünschte:

  • Einzelne WAN→LAN-Verbindungen (Level 2) erzeugen keine Alerts.
  • Die Korrelation (20 Events/60s) triggert auf Level 12 sehr wohl und ist sauber sichtbar.

Lessons Learned / Best Practices

  • Regelpriorität ist nicht die Rule-ID, sondern ergibt sich aus Evaluierungslogik (Severity/Level und Rule-Chain).
  • Catch-all-Regeln brauchen immer das niedrigste Level innerhalb einer Geschwistergruppe, sonst „erschlagen“ sie Spezifika.
  • Für „zählbare, aber nicht alertende“ Basisevents eignen sich Level 1–2 (abhängig von log_alert_level), nicht Level 0 als Standardlösung.
  • Wenn man sowohl „Audit-Klassifizierung“ als auch „optional alerten“ will, ist ein zweistufiges Default-Pattern (Stage 1/Stage 2) der stabilste Ansatz.
  • <group> ohne Schlusskomma schreiben und konsistente Taxonomie nutzen (z. B. mikrotik, audit, firewall_traffic, port_scan).

Fazit

Dass eine Catch-all-Regel „gewinnt“, liegt nicht an der höheren Rule-ID, sondern daran, dass sie ohne Bedingungen alles matcht und – je nach Level – in der Evaluierung vor den spezifischen Regeln priorisiert wird. Mit Level-2-Spezifikaregeln (nicht alertend bei Standard-Alert-Schwelle) und einer zweistufigen Default-Regel (Level 1 → optional Level 3) erhält man das beste aus beiden Welten: vollständige Audit-Abdeckung ohne Alert-Flut und gleichzeitig eine saubere Basis für Korrelationsregeln wie Portscan-Detection.

Mehr zu Wazuh …
https://wazuh.com/?utm_source=ambassadors&utm_medium=referral&utm_campaign=ambassadors+program

Mehr zum Wazuh Ambassador Program …
https://wazuh.com/ambassadors-program/?utm_source=ambassadors&utm_medium=referral&utm_campaign=ambassadors+program

https://wazuh.slack.com/archives/C07CCCCGHHP/p1769030097566099