Frontend Labs
⋯

Day 102 — Session Behavioral Analysis

Learning objective Track session-level behavioral patterns that distinguish bots from humans
Key packages sync, time, net/http
Security context Bots interact with APIs programmatically — no navigation patterns, no referrer chain, too fast

Key Concepts

Human vs bot behavior:

Signal Human Bot
Requests/second 0.1–2 10–1000
Unique paths/session 5–20 1–3
Referer presence 60–80% < 10%
Session depth Grows gradually Constant single-endpoint
Inter-request timing Variable (50ms–5s) Fixed intervals

Exercise — Session Behavioral Tracker

package bot

import (
    "net/http"
    "sync"
    "time"
)

type sessionData struct {
    mu              sync.Mutex
    requestTimes    []time.Time // sliding 60s window
    uniquePaths     map[string]struct{}
    refererCount    int
    requestCount    int
    created         time.Time
    lastSee         time.Time
}

func newSessionData() *sessionData {
    return &sessionData{
        uniquePaths: make(map[string]struct{}),
        created:     time.Now(),
        lastSee:     time.Now(),
    }
}

func (sd *sessionData) record(r *http.Request) {
    sd.mu.Lock()
    defer sd.mu.Unlock()

    now := time.Now()
    sd.requestCount++
    sd.lastSee = now
    sd.uniquePaths[r.URL.Path] = struct{}{}
    if r.Referer() != "" {
        sd.refererCount++
    }

    // Maintain sliding 60s window for velocity
    cutoff := now.Add(-60 * time.Second)
    valid := sd.requestTimes[:0]
    for _, t := range sd.requestTimes {
        if t.After(cutoff) {
            valid = append(valid, t)
        }
    }
    sd.requestTimes = append(valid, now)
}

// velocity returns requests per second over the last 60s
func (sd *sessionData) velocity() float64 {
    sd.mu.Lock()
    defer sd.mu.Unlock()
    if len(sd.requestTimes) == 0 {
        return 0
    }
    return float64(len(sd.requestTimes)) / 60.0
}

func (sd *sessionData) pathDiversity() int {
    sd.mu.Lock()
    defer sd.mu.Unlock()
    return len(sd.uniquePaths)
}

func (sd *sessionData) refererRate() float64 {
    sd.mu.Lock()
    defer sd.mu.Unlock()
    if sd.requestCount == 0 {
        return 0
    }
    return float64(sd.refererCount) / float64(sd.requestCount)
}

// BehavioralDetector implements Detector
type BehavioralDetector struct {
    mu       sync.RWMutex
    sessions map[string]*sessionData // keyed by IP (or session cookie)
}

func NewBehavioralDetector() *BehavioralDetector {
    return &BehavioralDetector{sessions: make(map[string]*sessionData)}
}

func (d *BehavioralDetector) getSession(ip string) *sessionData {
    d.mu.RLock()
    s, ok := d.sessions[ip]
    d.mu.RUnlock()
    if ok {
        return s
    }
    d.mu.Lock()
    defer d.mu.Unlock()
    s, ok = d.sessions[ip]
    if !ok {
        s = newSessionData()
        d.sessions[ip] = s
    }
    return s
}

func (d *BehavioralDetector) Detect(r *http.Request, score *SessionScore) {
    ip := clientIP(r)
    session := d.getSession(ip)
    session.record(r)

    // Only evaluate after enough data
    if session.requestCount < 5 {
        return
    }

    velocity := session.velocity()
    diversity := session.pathDiversity()
    refRate := session.refererRate()

    // High velocity signal
    if velocity > 10 {
        score.AddSignal(Signal{
            "high_velocity", 20, CategoryBehavior,
            fmt.Sprintf("%.1f req/s over last 60s", velocity),
        })
    }

    // Low path diversity
    if diversity < 2 && session.requestCount > 10 {
        score.AddSignal(Signal{
            "low_path_diversity", 10, CategoryBehavior,
            fmt.Sprintf("only %d unique paths in %d requests", diversity, session.requestCount),
        })
    }

    // Missing referer
    if refRate < 0.2 && session.requestCount > 10 {
        score.AddSignal(Signal{
            "low_referer_rate", 10, CategoryBehavior,
            fmt.Sprintf("referer present in only %.0f%% of requests", refRate*100),
        })
    }
}

Generated from week14.md · 8 Oct 2026 16:36