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),
})
}
}