·2 min read·← All posts
Go Security Anomaly Detection Fraud

Two signals

Most production fraud-detection stacks have hundreds of signals. Two of them produce most of the actual catches:

  1. 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.

  2. 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:

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:

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.

← Back to all posts