Behavioral detection
Pattern-of-behavior rules - catching the slow-burn attacks that single-request detection misses.
Per-request detection catches obvious payloads. It doesn't catch attacks where each individual request looks fine, but the pattern across many requests doesn't.
The slow-burn attack problem
Single-request catches: SQLi (' OR 1=1 --), cmd injection (; rm -rf /), path traversal (../../etc/passwd), SSRF (http://localhost). One request, one match, fires immediately.
Single-request misses: credential stuffing (5,000 /login POSTs, all valid-looking), path scanning (200+ 404s as the attacker maps your app), slow exfil (a few GETs per minute, each individually small), reconnaissance (probing one path family at a time). No single request is suspicious. The shape of the traffic is.
Behavioral rules close that gap by counting things, not pattern-matching individual requests.
Three flavors of behavioral rule
The rule_type field picks the counting mechanism.
usage - request-pattern rules. Counts request activity per IP within a window. Useful for catching high-volume probing behavior that isn't tripping any single-request detection.
return_pattern - response-pattern rules. Counts responses matching a pattern (e.g., 404 status codes) per IP per window. Catches scanning behavior by what your app SAID, not what was sent. This is the type the example uses.
frequency - frequency-pattern rules. Heuristics on request cadence. Watches for unusually regular or unusually sparse patterns over time (e.g., precisely-timed requests suggesting automation).
How return_pattern counts
Every response your app emits passes through the agent's behavioral evaluator. Responses matching the rule's pattern field increment a per-IP counter; the counter is windowed.
- Your handler (or FastAPI itself) returns a status code, e.g., 404.
- Does the response match the rule's
patternfield? Forpattern="404", a 404 status matches; a 200 doesn't. - If matched:
counter[ip] += 1. The counter is bounded by the rule's window. Older hits age out. - If
counter > thresholdwithin the window, the rule trips. The configured action fires (log / alert / throttle / ban).
A path-scanning attacker generates a stream of 404s as they probe URLs that don't exist. No single 404 is suspicious. A legitimate misspelled link looks the same. But 20 in 5 minutes from one IP is reconnaissance, and a return_pattern rule will see that signal.
correlate_with_detection - drop the bar when there's smoke
When True, the rule's threshold is halved for any IP that also has a recent threat detection on its record. The reasoning: an IP behaving suspiciously AND tripping detection is doubly suspicious. Be quicker to act.
| Clean IP | IP with active detection | |
|---|---|---|
| Threshold | Configured value | Halved |
| Example | 20 404s needed | 10 404s trigger |
In practice: leave correlate_with_detection=True on most behavioral rules. Legitimate users don't carry detections; suspicious IPs often do.
Four actions
When the rule trips, the action field decides what happens. Pick by confidence: how sure are you that a trip means an attacker, not a noisy real user?
| Action | When | Impact |
|---|---|---|
log | New rules in validation phase | No production impact, dashboard visibility |
alert | Signals you want to know about but not act on automatically | Notification fires (if alerting is wired) |
throttle | Suspect malicious behavior but worry about FPs | Soft cap on the IP's traffic |
ban | High-confidence rules | IP banned for auto_ban_duration - identical to threat-based ban |
The example uses log - appropriate for a rule still being validated against real traffic.
Wiring
global_behavior_rules=[
{
"rule_type": "return_pattern",
"threshold": 20,
"window": 300,
"pattern": "404",
"action": "log",
"correlate_with_detection": True,
}
]Reads as: "If an IP gets more than 20 404s in 5 minutes, log it. If that IP also has a recent threat detection, drop the threshold to 10."
| Field | Purpose |
|---|---|
rule_type | Which counting mechanism - usage, return_pattern, frequency |
threshold | Count of matches that trips the rule. Halved by correlation. |
window | Seconds. 300 = 5-minute rolling window. |
pattern | What return_pattern counts. "404" matches the status code. |
action | What happens on trip |
correlate_with_detection | Halve threshold when the IP has an active detection |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Rule is defined but never trips | Window too short for your traffic volume. Counter never accumulates before old hits age out. Or pattern doesn't match what your app returns. | Widen the window (try 600 or 900 first). Verify the pattern field. |
| Rule trips on legitimate users | Real users hit 404s too (broken bookmarks, stale links). Threshold too tight. Or correlate_with_detection is False. | Raise threshold AND set correlate_with_detection=True. Legit users won't have active detections; effective threshold stays higher. |
| Want to tune the rule from the dashboard | Behavioral rules are configured in SecurityConfig at boot - NOT in the dynamic-rules schema. | Edit app.py, deploy. Start in log mode for low-risk iteration. |
Behavioral rules count per IP. When an attacker spreads the load across many residential-proxy IPs so that no per-IP counter ever trips, that is a coordinated distributed attack, not a behavioral one. See Coordinated attack detection for the SaaS-only cross-IP correlation that catches it.