Analytics Rules
detect-successful-login-following-distributed-ssh-brute-force.kql
Successful SSH login against a host that just weathered a distributed brute-force campaign. Pair with the Act-I hunt to know if the lightning hit anything.
// Author: Ian D. Hanley (DevSecOpsDad) | linkedin.com/in/ianhanley | devsecopsdad.com | devsecopsdadattack.com
// Correlates a successful SSH login against a host that just weathered a distributed brute-force
// campaign (Act I's shape). The Act-I hunt tells you the storm is happening; this rule tells you
// if the lightning hit anything.
// Source: KQL Detection of the Week: The Attack That Stayed Under the Threshold (2026-06-19) — https://devsecopsdadattack.com/2026-06-19-KQL-of-the-Week_-The-Attack-That-Stayed-Under-the-Threshold/
let lookback = 1h;
let failThreshold = 10;
let failedSSH =
Syslog
| where TimeGenerated > ago(lookback)
| where Facility in ("auth", "authpriv") and SyslogMessage has "Failed password"
| extend SourceIP = extract(@"from ([\d\.]+)", 1, SyslogMessage)
| where isnotempty(SourceIP)
| summarize
FailCount = count(),
FirstFail = min(TimeGenerated),
LastFail = max(TimeGenerated)
by SourceIP, Computer;
let successSSH =
Syslog
| where TimeGenerated > ago(lookback)
| where Facility in ("auth", "authpriv") and SyslogMessage has "Accepted password"
| extend SourceIP = extract(@"from ([\d\.]+)", 1, SyslogMessage)
| extend User = extract(@"for (\S+) from", 1, SyslogMessage)
| extend SSHPort = extract(@"port (\d+)", 1, SyslogMessage)
| where isnotempty(SourceIP);
successSSH
| join kind=inner failedSSH on SourceIP, Computer
| where FailCount >= failThreshold
| where TimeGenerated > LastFail
| project
TimeGenerated,
Computer,
SourceIP,
User,
SSHPort,
FailCount,
FirstFail,
LastFail,
AlertDetail = strcat("SSH success after ", tostring(FailCount), " failures from ", SourceIP)