Detection exclusions
Taming false positives, telling the regex engine that your tracing IDs aren't SQLi.
Modern apps emit headers and fields full of high-entropy strings - trace IDs, JWTs, correlation tokens. To a pattern matcher tuned for attacks, these can look like base64-encoded payloads, SQLi probes, or path-traversal attempts.
Your own infrastructure trips the detectors
Innocuous infrastructure that trips matchers:
traceparent: 00-4bf92f3577b34da6...- OTel context, long hex, looks like payloadX-Request-ID: a3e9-c1f2-...- UUID, random characters, base64-shaped?_t=1684827341- cache-bust timestamp, looks like SQL intAuthorization: Bearer eyJhbGc...- JWT, actual base64, multi-segment
When false positives outnumber real signal, the team learns to dismiss the events log. Detection becomes theater - config that does nothing useful.
Exclusions are the answer: tell the detector "these fields are mine, skip them."
Three lists, three surfaces
| List | What | Match style |
|---|---|---|
excluded_detection_headers | HTTP header names | Case-insensitive |
excluded_detection_params | URL query parameter keys | Case-insensitive |
excluded_detection_body_fields | Body field names - JSON keys at any depth, plus form and multipart field names | Case-insensitive |
Case sensitivity rules
All three surfaces match case-insensitively.
Headers. Header names are lowercased on both sides before comparison, matching HTTP semantics. "X-Correlation-ID" in the set matches x-correlation-id on the wire, and vice versa.
Query params. Both sides are lowercased before comparison, the configured entries and the incoming parameter keys, so _T in the query string matches a _t entry, and vice versa.
Body fields. Same lowercase-both-sides matching: a trace_id entry matches trace_id, Trace_Id, and TRACE_ID in the payload, at any nesting depth.
What exclusion does - and what it doesn't
Does: skip ALL detection rules for the field. The value never reaches the regex engine. Applied during penetration detection scanning, before pattern compilation.
Doesn't: support partial or per-rule exclusion. You can't say "skip SQLi for this field but check SSRF." You can't apply a regex-style filter - only key-name matching. You can't exclude by value.
Nesting - excluded at any depth
Since guard-core 3.4.0 the body-field exclusion list matches key names at every nesting level, and it also applies to x-www-form-urlencoded field names and multipart text-part field names, not just JSON.
excluded_detection_body_fields = {"trace_id"}Against this request body:
{
"trace_id": "abc-123",
"user": {
"id": 42,
"trace_id": "xyz-789"
},
"metadata": {
"trace_id": "def-456"
}
}trace_idat the root - excludeduser.trace_id- excludedmetadata.trace_id- excluded
The match is by key NAME, not path. There is no dot-path syntax. You can't exclude metadata.trace_id while keeping the root trace_id scanned. One name covers every depth, all or nothing.
Wiring
Three small sets in SecurityConfig. They cover OpenTelemetry context propagation, cache-busting hints, and app-level correlation IDs.
excluded_detection_headers = {
"x-correlation-id", "x-request-id", "x-trace-id",
"traceparent", "tracestate",
}
excluded_detection_params = {"_t", "_cache_bust"}
excluded_detection_body_fields = {"correlation_id", "trace_id"}traceparent and tracestate are W3C TraceContext (OTel). The X-* trio is conventional naming for app-level correlation. All lowercased. The header match is case-insensitive.
Don't exclude
User-controlled inputs - search queries, free text, form fields. These ARE the attack surface. SQLi probes arrive in search boxes. Excluding them disables detection on the exact paths attackers target. If a user typed it, it must be scanned.
Auth tokens - JWTs, OAuth bearers, API keys. Tempting because they trip pattern matchers. But excluding them means token-stuffing probes won't register as suspicious behavior on that field. Hash or redact tokens BEFORE the request reaches the middleware. Don't exclude.
Turning body scanning off entirely
detection_scan_bodyboolScan the request body during penetration detection. Set False to restrict detection to the URL path, query params, and headers; the body is then never read or matched, regardless of its shape.
TrueExclusions carve individual fields out of the scan. When the body should never be scanned at all - binary uploads, very large payloads, an API whose body is entirely machine-generated - disable body scanning instead. With detection_scan_body=False the body is never read or matched, regardless of its shape; detection still runs on the URL path, query params, and headers.
The other lever - detection sensitivity
Exclusions remove a field from scanning entirely. When you'd rather keep scanning everything but require more evidence before a request is flagged, tune the anomaly-score threshold instead.
detection_threat_score_thresholdfloatAnomaly score required to flag a request as a threat
1.0Penetration detection accumulates a graduated anomaly score per request: each matched pattern adds weight, and the request is flagged as a threat once the total reaches this threshold. It accepts 0.0-10.0. The default of 1.0 flags a request as soon as one full-confidence pattern matches, identical to the pre-scoring match-anything detector, so raising the guard-core version changes nothing until you move it.
- Raise it (e.g.
2.0) to demand corroboration: a single faint signal no longer flags on its own; it takes multiple matches, or one high-confidence match. Fewer false positives. - Lower it (toward
0.0) to flag on partial signals. More sensitive, more noise.
Which lever to reach for:
| Tool | Scope | Reach for it when |
|---|---|---|
excluded_detection_* | Removes specific fields from scanning entirely | A known-safe field (trace ID, JWT) trips matchers |
detection_scan_body | Removes the entire request body from scanning | The body is machine-generated or binary and FPs concentrate there |
detection_threat_score_threshold | Global sensitivity - how much evidence flags a request | FPs are spread across many fields, or lone weak signals dominate the noise |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Excluded a body field but still getting false positives | The app runs guard-core ≤3.3.x, where exclusion matched top-level JSON keys only | Confirm the installed guard-core version - any-depth matching landed in 3.4.0. |
Excluded X-Correlation-ID but it still trips | The header doesn't reach the middleware under that name - proxies sometimes strip or rename headers in transit | Confirm the header is present on the actual incoming request. Casing is never the cause. Matching is case-insensitive on both sides. |
| Changed exclusions in the dashboard, nothing changed | Exclusion sets are NOT in the dynamic-rules schema. They're code-time only. | Edit app.py, redeploy. Exclusions tend to be set-and-forget anyway. |
| Real attacks stopped registering after raising the threshold | Threshold is now higher than a single-signal attack scores | Lower it back toward 1.0; add corroborating behavioral rules rather than leaning on a high threshold |