|
|
|
|
@@ -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.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetAlertReason(int totalEvents, int uniqueIpCount, ScannerConfiguration configuration, string alertState)
|
|
|
|
|
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 alertState switch
|
|
|
|
|
return new AlertAssessment("critical", $"Critical volume threshold reached: events={attacks.Count}, unique IPs={uniqueIpCount}.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (attacks.Count >= warningEventThreshold || uniqueIpCount >= warningIpThreshold)
|
|
|
|
|
{
|
|
|
|
|
"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."
|
|
|
|
|
};
|
|
|
|
|
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 int GetPeakEventCount(IReadOnlyList<AttackEvent> events, TimeSpan window)
|
|
|
|
|
{
|
|
|
|
|
int start = 0;
|
|
|
|
|
int peak = 0;
|
|
|
|
|
for (int end = 0; end < events.Count; end++)
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|