# Suspicious activity triggers additional step-up

- **Pattern:** `ab-001475` (`finserv-session-security.session-integrity.suspicious-activity-step-up`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001475
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001475)

## Why it matters

Impossible travel, rapid transaction sequences, and unusually large transfer amounts are established signals of account compromise or fraud. Without anomaly detection, an attacker who obtains a valid session token — through phishing, session fixation, or XSS — can execute transactions that would immediately appear suspicious to any human reviewer but proceed unchallenged by the application. CWE-307 (Improper Restriction of Excessive Authentication Attempts) and NIST SI-4 (System Monitoring) address active threat detection requirements. PCI-DSS Req 10.7 requires that anomalies trigger alerts. Suspicious activity step-up provides a dynamic second factor that activates precisely when risk is highest, without adding friction to normal user behavior.

## Severity rationale

Low because suspicious activity detection is a defense-in-depth layer — its absence does not enable initial access, but it removes the runtime signal that would detect and interrupt an active attack on an already-compromised session.

## Remediation

Add at least two anomaly detection rules in `src/lib/fraudDetection.ts` — impossible travel is the highest-signal, lowest-false-positive starting point:

```js
export async function checkSuspiciousActivity(
  userId: string,
  currentLocation: { lat: number; lng: number }
): Promise<boolean> {
  const lastSession = await db.sessions.findFirst({
    where: { userId },
    orderBy: { loginAt: 'desc' },
    skip: 1 // skip current
  });
  if (!lastSession?.location) return false;

  const elapsedMin =
    (Date.now() - lastSession.loginAt.getTime()) / 60000;
  const distanceKm = geoDistance(lastSession.location, currentLocation);
  const speedKmMin = distanceKm / elapsedMin;

  return speedKmMin > 15; // >15 km/min is physically impossible
}
```

When `checkSuspiciousActivity` returns `true`, call `requireStepUp()` before allowing any financial operation to proceed. Add a second rule (e.g., transactions above a threshold amount) to meet the two-rule minimum.

## Detection

- **ID:** `suspicious-activity-step-up`
- **Severity:** `low`
- **What to look for:** Count all anomaly detection rules (impossible travel, unusual amounts, rapid sequences, failed login threshold). Enumerate each rule and its trigger threshold. Quote the actual detection logic found. Verify at least 2 suspicious activity patterns are monitored. Count all step-up triggers for suspicious activity.
- **Pass criteria:** At least 2 suspicious activity patterns are detected (e.g., impossible travel, unusual transaction amount). When detected, at least 1 step-up challenge is triggered. Report the count even on pass (e.g., "3 detection rules: impossible travel (>15 km/min), amount >$10000, >5 failed logins — all trigger step-up").
- **Fail criteria:** No suspicious activity detection (0 rules), or detection exists but does not trigger step-up (0 step-up triggers for anomalies).
- **Skip (N/A) when:** The application does not perform financial transactions — cite the actual project type found.
- **Detail on fail:** `"0 anomaly detection rules — impossible-travel logins not detected, no fraud monitoring"`
- **Remediation:** Add simple anomaly detection (in `src/lib/fraudDetection.ts`):
  ```js
  // lib/fraudDetection.ts
  export async function checkSuspiciousActivity(userId: string, action: string) {
    const recentSessions = await db.sessions.findMany({
      where: { userId, expiresAt: { gt: new Date() } },
      orderBy: { loginAt: 'desc' },
      take: 1
    });

    const lastSession = recentSessions[0];
    if (!lastSession) return false; // First login, not suspicious

    const timeSinceLastLogin = (Date.now() - lastSession.loginAt.getTime()) / 1000 / 60; // minutes
    const distance = geoDistance(lastSession.location, currentLocation); // km
    const speedOfTravel = distance / timeSinceLastLogin; // km/min

    if (speedOfTravel > 15) { // Impossible travel (>15 km/min)
      return true; // Flag as suspicious
    }
    return false;
  }
  ```

## External references

- cwe CWE-307
- owasp A07
- nist SI-4
- pci-dss Req 10.7

Taxons: access-control, observability

HTML version: https://auditbuffet.com/patterns/ab-001475
