Correlate failed login activity before alerting
This commit is contained in:
@@ -53,8 +53,9 @@ internal sealed class AttackScanner
|
||||
.ToList();
|
||||
|
||||
int uniqueIpCount = attacks.Select(static attack => attack.SourceIp).Distinct(StringComparer.OrdinalIgnoreCase).Count();
|
||||
string baseAlertState = GetAlertState(attacks.Count, uniqueIpCount, configuration);
|
||||
string baseAlertReason = GetAlertReason(attacks.Count, uniqueIpCount, configuration, baseAlertState);
|
||||
AlertAssessment baseAssessment = AssessAttackActivity(attacks, uniqueIpCount, configuration);
|
||||
string baseAlertState = baseAssessment.State;
|
||||
string baseAlertReason = baseAssessment.Reason;
|
||||
VulnerabilityCorrelationSummary vulnerabilityCorrelation = string.IsNullOrWhiteSpace(options.VulnerabilityCsvPath)
|
||||
? VulnerabilityCorrelationSummary.Empty()
|
||||
: VulnerabilityCorrelation.LoadForMachine(Environment.MachineName, options.VulnerabilityCsvPath, errors);
|
||||
@@ -469,28 +470,90 @@ internal sealed class AttackScanner
|
||||
return IPAddress.TryParse(input, out _);
|
||||
}
|
||||
|
||||
private static string GetAlertState(int totalEvents, int uniqueIpCount, ScannerConfiguration configuration)
|
||||
private static AlertAssessment AssessAttackActivity(IReadOnlyList<AttackEvent> attacks, int uniqueIpCount, ScannerConfiguration configuration)
|
||||
{
|
||||
if (totalEvents >= configuration.CriticalEventThreshold || uniqueIpCount >= configuration.CriticalUniqueIpThreshold)
|
||||
if (attacks.Count == 0)
|
||||
{
|
||||
return "critical";
|
||||
return new AlertAssessment("ok", "No failed login activity observed.");
|
||||
}
|
||||
|
||||
if (totalEvents >= configuration.WarningEventThreshold || uniqueIpCount >= configuration.WarningUniqueIpThreshold)
|
||||
TimeSpan window = TimeSpan.FromMinutes(configuration.LoginBurstWindowMinutes);
|
||||
int largestBurst = attacks
|
||||
.GroupBy(attack => (attack.SourceIp, attack.Username, attack.Target))
|
||||
.Select(group => GetPeakEventCount(group.OrderBy(attack => attack.Timestamp).ToList(), window))
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
int largestSpray = attacks
|
||||
.GroupBy(attack => attack.SourceIp)
|
||||
.Select(group => GetPeakDistinctAccountCount(group.OrderBy(attack => attack.Timestamp).ToList(), window))
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
|
||||
if (largestBurst >= configuration.CriticalLoginBurstCount || largestSpray >= configuration.CriticalSprayAccountCount)
|
||||
{
|
||||
return "warning";
|
||||
return new AlertAssessment("critical", $"High-confidence login attack pattern: burst={largestBurst}, sprayed accounts={largestSpray}, window={configuration.LoginBurstWindowMinutes}m.");
|
||||
}
|
||||
|
||||
return "ok";
|
||||
if (largestBurst >= configuration.WarningLoginBurstCount || largestSpray >= configuration.WarningSprayAccountCount)
|
||||
{
|
||||
return new AlertAssessment("warning", $"Suspicious login pattern: burst={largestBurst}, sprayed accounts={largestSpray}, window={configuration.LoginBurstWindowMinutes}m.");
|
||||
}
|
||||
|
||||
int criticalEventThreshold = Math.Max(configuration.CriticalEventThreshold, configuration.CriticalLoginBurstCount);
|
||||
int criticalIpThreshold = Math.Max(configuration.CriticalUniqueIpThreshold, configuration.CriticalSprayAccountCount);
|
||||
int warningEventThreshold = Math.Max(configuration.WarningEventThreshold, configuration.WarningLoginBurstCount * 2);
|
||||
int warningIpThreshold = Math.Max(configuration.WarningUniqueIpThreshold, configuration.WarningSprayAccountCount);
|
||||
|
||||
if (attacks.Count >= criticalEventThreshold || uniqueIpCount >= criticalIpThreshold)
|
||||
{
|
||||
return new AlertAssessment("critical", $"Critical volume threshold reached: events={attacks.Count}, unique IPs={uniqueIpCount}.");
|
||||
}
|
||||
|
||||
if (attacks.Count >= warningEventThreshold || uniqueIpCount >= warningIpThreshold)
|
||||
{
|
||||
return new AlertAssessment("warning", $"Elevated failed-login volume: events={attacks.Count}, unique IPs={uniqueIpCount}.");
|
||||
}
|
||||
|
||||
return new AlertAssessment("ok", $"Low-volume login errors observed: events={attacks.Count}, unique IPs={uniqueIpCount}; no burst or password-spraying pattern detected.");
|
||||
}
|
||||
|
||||
private static string GetAlertReason(int totalEvents, int uniqueIpCount, ScannerConfiguration configuration, string alertState)
|
||||
private static int GetPeakEventCount(IReadOnlyList<AttackEvent> events, TimeSpan window)
|
||||
{
|
||||
return alertState switch
|
||||
int start = 0;
|
||||
int peak = 0;
|
||||
for (int end = 0; end < events.Count; end++)
|
||||
{
|
||||
"critical" => $"Critical threshold reached. Events={totalEvents}/{configuration.CriticalEventThreshold}, UniqueIPs={uniqueIpCount}/{configuration.CriticalUniqueIpThreshold}.",
|
||||
"warning" => $"Warning threshold reached. Events={totalEvents}/{configuration.WarningEventThreshold}, UniqueIPs={uniqueIpCount}/{configuration.WarningUniqueIpThreshold}.",
|
||||
_ => "No thresholds exceeded."
|
||||
};
|
||||
while (events[end].Timestamp - events[start].Timestamp > window)
|
||||
{
|
||||
start++;
|
||||
}
|
||||
peak = Math.Max(peak, end - start + 1);
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
private static int GetPeakDistinctAccountCount(IReadOnlyList<AttackEvent> events, TimeSpan window)
|
||||
{
|
||||
var accounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
int start = 0;
|
||||
int peak = 0;
|
||||
for (int end = 0; end < events.Count; end++)
|
||||
{
|
||||
accounts[events[end].Username] = accounts.GetValueOrDefault(events[end].Username) + 1;
|
||||
while (events[end].Timestamp - events[start].Timestamp > window)
|
||||
{
|
||||
string account = events[start].Username;
|
||||
accounts[account]--;
|
||||
if (accounts[account] == 0)
|
||||
{
|
||||
accounts.Remove(account);
|
||||
}
|
||||
start++;
|
||||
}
|
||||
peak = Math.Max(peak, accounts.Count);
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
private sealed record AlertAssessment(string State, string Reason);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user