Two signals
Most production fraud-detection stacks have hundreds of signals. Two of them produce most of the actual catches:
-
Impossible travel. Login from Pune at 10:00 UTC, another from London at 10:15 UTC. 7,400 km in 15 minutes is impossible by aircraft, let alone in-flight WiFi. The session is compromised or shared.
-
Credential-stuffing density. A single IP making login attempts against many accounts in a short window. Either a stuffer trying leaked creds or a poorly-configured corporate proxy. Either way, throttle.
Impossible travel in Go
type Login struct {
UserID string
Latitude float64
Longitude float64
At time.Time
}
func haversineKm(a, b Login) float64 {
const R = 6371.0 // earth radius in km
lat1, lat2 := a.Latitude*math.Pi/180, b.Latitude*math.Pi/180
dLat := (b.Latitude - a.Latitude) * math.Pi / 180
dLon := (b.Longitude - a.Longitude) * math.Pi / 180
h := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2)
return 2 * R * math.Asin(math.Sqrt(h))
}
func impossibleTravel(prev, curr Login, maxKmH float64) bool {
distKm := haversineKm(prev, curr)
hours := curr.At.Sub(prev.At).Hours()
if hours <= 0 { return false }
return distKm / hours > maxKmH
}
maxKmH set to 1000 (faster than any commercial flight) catches the obvious cases. Set to 200 for very paranoid configurations (catches train + car combos that are physically possible but unusual for the user’s pattern).
Credential-stuffing density
type StufferScore struct {
AttemptsLastMinute int
UniqueUsersLast5Min int
FailureRateLast5Min float64
}
func suspicious(score StufferScore) bool {
return score.AttemptsLastMinute > 30 ||
score.UniqueUsersLast5Min > 10 ||
score.FailureRateLast5Min > 0.7
}
Three thresholds. Tuned per workload. Tracked per IP in Redis with sliding-window counters.
Genie’s implementation
agents/cyber_guardian runs both checks plus a couple more (unknown device, fingerprint churn) on every login event. The output is a score 0-1; thresholds (configured per-tenant in the FREE-AI policy YAML) trigger:
- Score 0.0-0.3: log + continue
- Score 0.3-0.6: log + soft step-up (require recent password re-enter)
- Score 0.6-1.0: log + hard block + incident + alert to security team
The thresholds map to FREE-AI Rec 19 (Cybersecurity). Each block writes an Annexure VI incident with FailureMode = FailureUnintendedAction so the regulator can see the cumulative rate.
What this misses
Two attack classes need other signals:
- Slow-burn stuffing (1 attempt per IP, but 10K IPs over hours). Density-by-IP fails; need ASN-level density.
- Targeted account takeover with the right device fingerprint and the right location. Impossible-travel doesn’t trigger; need behavioural signals (typing cadence, mouse patterns).
For production, these two are the foundation. The advanced signals layer on top. Starting with these catches most volume; the long-tail attackers are a quarter-on-quarter investment.