mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RadicadeServer.Utils
|
||||
{
|
||||
public static class AuthSecurityMonitor
|
||||
{
|
||||
public static readonly TimeSpan Window = TimeSpan.FromMinutes(10);
|
||||
|
||||
public const int AccountFailureAlertThreshold = 8;
|
||||
|
||||
public const int SourceSprayAccountThreshold = 12;
|
||||
|
||||
public const int SourceFailureAlertThreshold = 40;
|
||||
|
||||
public const int MaxProgressiveDelayMs = 3000;
|
||||
|
||||
private static readonly ConcurrentDictionary<long, Counter> AccountFailures = new();
|
||||
private static readonly ConcurrentDictionary<string, SourceCounter> SourceFailures = new();
|
||||
|
||||
public static int RecordFailedLogin(long? accountId, string? ip)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var accountCount = 0;
|
||||
|
||||
if (accountId is > 0)
|
||||
{
|
||||
var counter = AccountFailures.GetOrAdd(accountId.Value, _ => new Counter());
|
||||
accountCount = counter.Record(now, Window);
|
||||
if (accountCount == AccountFailureAlertThreshold)
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security alert: {accountCount} failed sign-ins for account {accountId.Value} within {Window.TotalMinutes:0} minutes - for staff review only, no automatic action taken");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ip))
|
||||
{
|
||||
var source = SourceFailures.GetOrAdd(ip!, _ => new SourceCounter());
|
||||
var (failures, accounts) = source.Record(now, Window, accountId);
|
||||
if (accounts == SourceSprayAccountThreshold)
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security alert: suspected password spraying from {ip} - failed sign-ins for {accounts} different accounts within {Window.TotalMinutes:0} minutes");
|
||||
else if (failures == SourceFailureAlertThreshold)
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security alert: {failures} failed sign-ins from {ip} within {Window.TotalMinutes:0} minutes");
|
||||
}
|
||||
|
||||
return accountCount;
|
||||
}
|
||||
|
||||
public static int ProgressiveDelayMs(int failureCount) =>
|
||||
failureCount <= 3 ? 0 : Math.Min(MaxProgressiveDelayMs, (failureCount - 3) * 300);
|
||||
|
||||
public static bool IsSourceThrottled(string? ip)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ip)) return false;
|
||||
if (!SourceFailures.TryGetValue(ip!, out var source)) return false;
|
||||
return source.DistinctAccounts(DateTime.UtcNow, Window) >= SourceSprayAccountThreshold;
|
||||
}
|
||||
|
||||
public static void RecordSuccessfulLogin(long accountId, string? ip, bool newDevice, bool newIp)
|
||||
{
|
||||
if (accountId > 0) AccountFailures.TryRemove(accountId, out _);
|
||||
|
||||
if (newDevice || newIp)
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security note: account {accountId} signed in from a {(newDevice ? "new device" : "new IP")} - informational only, this never bans or locks by itself");
|
||||
}
|
||||
|
||||
public static void NoteCredentialChange(long accountId, string kind) =>
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security note: {kind} for account {accountId} - verify it was requested by the account owner");
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
AccountFailures.Clear();
|
||||
SourceFailures.Clear();
|
||||
}
|
||||
|
||||
private sealed class Counter
|
||||
{
|
||||
private readonly object gate = new();
|
||||
private readonly List<DateTime> hits = new();
|
||||
|
||||
public int Record(DateTime now, TimeSpan window)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
hits.RemoveAll(t => now - t > window);
|
||||
hits.Add(now);
|
||||
return hits.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SourceCounter
|
||||
{
|
||||
private readonly object gate = new();
|
||||
private readonly List<(DateTime At, long AccountId)> hits = new();
|
||||
|
||||
public (int Failures, int Accounts) Record(DateTime now, TimeSpan window, long? accountId)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
hits.RemoveAll(h => now - h.At > window);
|
||||
hits.Add((now, accountId ?? 0));
|
||||
return (hits.Count, Distinct());
|
||||
}
|
||||
}
|
||||
|
||||
public int DistinctAccounts(DateTime now, TimeSpan window)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
hits.RemoveAll(h => now - h.At > window);
|
||||
return Distinct();
|
||||
}
|
||||
}
|
||||
|
||||
private int Distinct()
|
||||
{
|
||||
var set = new HashSet<long>();
|
||||
foreach (var hit in hits)
|
||||
if (hit.AccountId > 0) set.Add(hit.AccountId);
|
||||
return set.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user