Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.lintliot.com/llms.txt

Use this file to discover all available pages before exploring further.

Individual request inspection catches single malicious requests. Anomaly detection catches the patterns that emerge across many requests over time: a user account accessed from two continents within minutes, a script cycling through thousands of email/password combinations, a user account suddenly pulling 50,000 database records in a 5-minute window. LintLiot’s anomaly detector runs continuously in-process, correlating events across requests to find threats that per-request WAF rules would miss.

Detection capabilities

LintLiot detects seven behavioral anomaly types:

Impossible travel

The same user account authenticates from two geographic locations that cannot be reached in the elapsed time at any commercially available transport speed.

Brute force

A single IP address makes repeated failed login attempts against any account within a rolling time window.

Credential stuffing

Multiple unique IP addresses all target the same user account with failed login attempts — the pattern of automated credential list attacks.

Data exfiltration

A user reads an abnormally large volume of records compared to their historical baseline, suggesting bulk data extraction.

API abuse

An API key or user account makes requests at an inhuman rate — far beyond what a real user could generate manually.

Scraping

An IP address systematically hits a large number of unique endpoints in a short window, suggesting automated content harvesting.

Account takeover

A login from a new device or country that doesn’t match the user’s established session fingerprint.

Impossible travel detection

Impossible travel uses the haversine formula to calculate the great-circle distance between two login coordinates, then divides by the elapsed time to get an implied speed. If that speed exceeds 900 km/h — the maximum speed of commercial aviation — the second login is flagged.
// How it works internally
const distanceKm = haversineDistanceKm(prevLat, prevLon, currentLat, currentLon)
const elapsedHours = elapsedMs / 3_600_000
const impliedSpeedKmh = distanceKm / elapsedHours

if (impliedSpeedKmh > 900) {
  // Impossible travel detected
  // Second session invalidated automatically
  // Event logged as auth.impossible_travel at critical severity
}
Two safety margins prevent false positives:
  • Logins less than 5 minutes apart are ignored (same session reconnect is plausible)
  • Distances under 100 km are ignored (regional IP geolocation variance)
When impossible travel is detected, the second session is invalidated and a critical alert fires immediately. You don’t need to handle this in your code — LintLiot handles it in the middleware layer.

Recording auth failures

Call lintliot.anomaly.recordAuthFailure() in your login route whenever authentication fails. This feeds both brute force and credential stuffing detection:
// In your login handler
async function loginHandler(req, res) {
  const { email, password } = req.body

  const user = await findUserByEmail(email)
  const valid = user && await verifyPassword(password, user.passwordHash)

  if (!valid) {
    // Pass the request and the target userId (if you found one)
    const anomaly = await lintliot.anomaly.recordAuthFailure(req, user?.id)

    if (anomaly.detected) {
      console.log(`Anomaly: ${anomaly.type}`)
      // anomaly.type === 'brute_force' or 'credential_stuffing'
      // anomaly.severity === 'HIGH' or 'CRITICAL'
      // anomaly.autoBlock === true means the IP was already blocked
    }

    return res.status(401).json({ error: 'Invalid credentials' })
  }

  // ... continue with successful login
}

AnomalyResult type

interface AnomalyResult {
  /** Whether an anomaly was detected */
  detected: boolean

  /** The type of anomaly, if detected */
  type?: 'brute_force' | 'credential_stuffing' | 'data_exfiltration'
        | 'account_takeover' | 'api_abuse' | 'privilege_escalation' | 'scraping'

  /** Severity level */
  severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'

  /** Human-readable description of what was detected */
  message?: string

  /** Whether LintLiot automatically blocked the source IP */
  autoBlock?: boolean

  /** Additional context (IP count, record count, distance, etc.) */
  meta?: Record<string, unknown>
}

Recording data reads

Call lintliot.anomaly.recordDataRead() after any query that returns records to track data access volume. LintLiot accumulates totals per user in a rolling 5-minute window and alerts when the volume exceeds 500 records (or your configured threshold):
// After a database query
const { data: orders } = await supabase
  .from('orders')
  .select('*')
  .eq('user_id', userId)

// Tell LintLiot how many records this user just read
await lintliot.anomaly.recordDataRead(userId, orders.length)

// Or instrument it inline
const anomaly = await lintliot.anomaly.recordDataRead(userId, orders.length)
if (anomaly.detected) {
  // anomaly.type === 'data_exfiltration'
  // Log, alert, or throttle the response
}
You don’t need to call recordDataRead on every single query. Focus on endpoints that return lists of records — user lists, export endpoints, search results, and report queries. Single-record reads rarely trigger the exfiltration threshold.

Detection thresholds

Default thresholds are tuned to minimize false positives. You can override them when creating the client:
const lintliot = createLintliot({
  apiKey: process.env.LINTLIOT_API_KEY,
  anomalyThresholds: {
    bruteForceFailures: 5,        // failures before brute force alert (default: 10)
    bruteForceWindowMs: 300_000,  // window for counting failures (default: 5 min)
    stuffingIPs: 3,               // unique IPs before stuffing alert (default: 5)
    stuffingWindowMs: 600_000,    // window for unique IP tracking (default: 10 min)
    exfiltrationReads: 200,       // records before exfiltration alert (default: 500)
    exfiltrationWindowMs: 300_000,// window for record counting (default: 5 min)
  },
})
DetectionDefault thresholdDefault window
Brute force10 failures from same IP5 minutes
Credential stuffing5 unique IPs targeting same account10 minutes
Data exfiltration500 records read by same user5 minutes
Scraping100 unique endpoints from same IP1 minute
API abuse30 requests/second from same key1 minute

Alerts and auto-blocking

When an anomaly is detected, LintLiot:
  1. Returns an AnomalyResult with detected: true from the function you called
  2. Pushes a threat.detected event to the LintLiot dashboard in real time
  3. Fires a push notification to the dashboard (critical events are immediate)
For some anomaly types, autoBlock is set to true — this means LintLiot has already blocked the source IP or invalidated the session at the middleware layer. For others (like credential stuffing), blocking is flagged as recommended but left to your discretion, since blocking all source IPs might collaterally block legitimate users on shared networks.
Anomaly detection is stateful — state is stored in-process per SDK instance. In multi-instance deployments (serverless, horizontal scale), each instance maintains its own counters. For cross-instance correlation, LintLiot pushes events to the central API where the dashboard performs cross-instance analysis. Per-instance thresholds may be lower than they appear in a multi-instance deployment.

Re-alert suppression

To prevent alert fatigue, LintLiot suppresses repeated alerts for the same active anomaly. Once an anomaly fires an alert, it won’t fire again for the same IP or user for 15 minutes, unless the underlying condition escalates in severity.