Eine stumme Regel fällt im Betrieb nicht auf

Eine Erkennungsregel in Microsoft Sentinel ist eine KQL-Abfrage, die regelmäßig läuft und einen Vorfall meldet, sobald sie Zeilen liefert. Liefert sie keine, wertet der Betrieb das als Ruhe. Eine Regel, die aus logischen Gründen niemals eine Zeile liefern kann, lässt sich im Betrieb deshalb nicht von einer Regel unterscheiden, die gerade nichts zu melden hat.

Die folgenden drei Abfragen sind gültiges KQL. Microsofts eigene Parser-Bibliothek Kusto.Language nimmt alle drei an, und jede bleibt im Betrieb stumm. Am 10. Oktober 2026 schickten wir sie durch die Prüfung unserer Sentinel-Prüfschicht. Die Abfragen stehen hier so, wie sie liefen, mitsamt der Kommentarzeile am Anfang, von der an die Zeilennummern zählen. Die Befunde darunter geben die Ausgabe im Wortlaut wieder; nur die Hinweiszeile, die die Prüfung jedem Befund anfügt, fehlt.

1. Die leere Einschlussliste

KQL
// Detection: sign-ins by privileged accounts from outside the allowed countries
let Admins = dynamic([]);
SigninLogs
| where TimeGenerated > ago(1d)
| where UserPrincipalName in (Admins)
| where Location !in ("DE", "AT", "CH")
| project TimeGenerated, UserPrincipalName, IPAddress, Location
Befund
[ERROR] line 5: 'Admins' is empty (line 2) and is used as a positive set test —
        this query cannot return a single row. (empty_filter_list)

Die Liste der privilegierten Konten ist leer, vermutlich als Platzhalter gedacht, den jemand später füllt. Ein positiver Mengentest gegen eine leere Liste trifft nie. Als Ausschlussliste mit !in wäre dieselbe leere Liste korrekt, und deshalb lässt sich der Fall an der Struktur der Abfrage entscheiden.

2. Ticks statt Sekunden

KQL
// Detection: more than one failed sign-in every two seconds from one address
let lookback = 1d;
SigninLogs
| where TimeGenerated > ago(lookback)
| where ResultType != "0"
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), Attempts = count() by UserPrincipalName, IPAddress
| extend DurationSeconds = todouble(LastSeen - FirstSeen)
| where Attempts > 10 and Attempts / DurationSeconds > 0.5
| project UserPrincipalName, IPAddress, Attempts, FirstSeen, LastSeen
Befund
[ERROR] line 7: 'todouble()' converts 'LastSeen - FirstSeen' to a number —
        the result is in ticks, not seconds. (timespan_without_unit)

Die Regel soll melden, wenn eine Adresse mehr als einen fehlgeschlagenen Anmeldeversuch alle zwei Sekunden erzeugt. Zwei Zeitstempel ergeben voneinander abgezogen eine Zeitspanne, und todouble() wandelt sie in eine Zahl in Ticks um, zehn Millionen pro Sekunde. Die berechnete Rate fällt deshalb um sieben Größenordnungen zu klein aus und überschreitet den Schwellwert von 0,5 praktisch nie. Ein Sprachmodell schrieb diese Regel und nannte die Variable „Sekunden“.

3. Die vertauschte Differenz

KQL
// Detection: more than one failed sign-in every two seconds from one address
let lookback = 1d;
SigninLogs
| where TimeGenerated > ago(lookback)
| where ResultType != "0"
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), Attempts = count() by UserPrincipalName, IPAddress
| extend DurationSeconds = datetime_diff("second", FirstSeen, LastSeen)
| where Attempts > 10 and Attempts / DurationSeconds > 0.5
| project UserPrincipalName, IPAddress, Attempts, FirstSeen, LastSeen
Befund
[ERROR] line 7: 'datetime_diff(..., FirstSeen, LastSeen)' is never positive —
        'FirstSeen' is the minimum and 'LastSeen' the maximum of 'timegenerated',
        so the subtraction runs backwards. (negative_duration)

datetime_diff rechnet das zweite Argument minus das dritte. Steht das Minimum links und das Maximum rechts, wird die Dauer nie positiv und die Rate nie größer als null. Dieser Fehler entstand, als dasselbe Modell den zweiten reparierte. Es korrigierte die Einheit und vertauschte dabei die Argumente.

Auch mit richtiger Reihenfolge bliebe ein Fehler. datetime_diff liefert eine ganze Zahl, die Zahl der Versuche ist ebenfalls ganzzahlig, und Kusto schneidet bei der Division zweier ganzer Zahlen den Nachkommateil ab. Fünfzehn Versuche in zwanzig Sekunden ergeben dann null statt 0,75, und die Regel feuert erst ab einem Versuch pro Sekunde statt ab einem halben. Korrekt rechnet todouble(Attempts) / DurationSeconds.

Welche Prüfung findet was

PrüfungWas sie prüftFindet die drei Fälle
Kusto.Language (Microsofts Parser)Syntax, bekannte Tabellen und Spalten, Funktionsargumentekeinen; alle drei sind gültig
kql-guard (Microsoft, 2026)Syntax, mit Schema unbekannte Namen, zwölf Kostenregeln von fehlendem Zeitfilter bis ungebremstem mv-expandlaut Dokumentation keinen; von uns nicht ausgeführt
Prüfkatalog der PrüfschichtMuster, bei denen eine gültige Abfrage keine Zeile liefern kannalle drei als Fehler
Testlauf gegen Angriffsdatenob die Regel einen echten Angriff erkenntja, sofern passende Daten vorliegen

Seit April 2026 liegt im GitHub-Konto von Microsoft kql-guard, ein schneller, offline laufender Analysator für „Detection as Code“. Er fängt Syntaxfehler, meldet mit einem Schema unbekannte Tabellen und Spalten und bewertet teure Abfrageformen. Damit deckt er eine andere Fehlerklasse ab, denn kql-guard fragt, ob eine Abfrage läuft und was sie kostet. Die drei Fälle oben laufen und kosten wenig, nur können sie nichts finden.

Die Werkzeuge ergänzen sich also. Wer Erkennungsregeln im Repository pflegt, sollte kql-guard in die Pipeline hängen, weil es Kostenfallen findet, die unser Katalog nicht kennt. Wer Regeln von einem Sprachmodell schreiben lässt, braucht zusätzlich eine Prüfung auf stille Fehler, weil Modelle solche Fehler flüssig schreiben.

Grenzen

Der Katalog kennt die Muster, die wir gefunden haben, und weitere werden folgen. Er findet Regeln, die nicht feuern können. Dass eine Regel ohne Befund richtig rechnet oder die richtige Bedrohung beschreibt, beweist er nicht. Der dritte Fall zeigt diese Grenze, denn die Prüfung bemerkte die Ganzzahldivision nicht. Die Fassung mit richtiger Argumentreihenfolge und ohne todouble() besteht sie mit „No findings.“, genauso wie die korrigierte Fassung mit todouble(Attempts) / DurationSeconds. Die Aussage zu kql-guard beruht auf dessen README vom Oktober 2026; wir haben das Werkzeug nicht selbst gegen die drei Abfragen laufen lassen, und sein Regelsatz kann wachsen. Die Branchenzahl, nach der rund 13 % der Regeln in produktiven SIEM-Umgebungen nie feuern können, stammt aus dem Report von CardinalOps aus dem Jahr 2025; wir haben sie nicht gemessen.

Quellen

  • microsoft/kql-guard, README, github.com
  • Microsoft Learn, datetime_diff(), learn.microsoft.com
  • Microsoft Learn, Numerical operators, Typregeln für die Division, learn.microsoft.com
  • Zahlen von CardinalOps nach Help Net Security, 9. Juni 2025, helpnetsecurity.com
  • Eigene Läufe vom 10. Oktober 2026 gegen die Tabelle SigninLogs aus dem mitgelieferten Schemakatalog