Detection exclusions

Taming false positives, telling the regex engine that your tracing IDs aren't SQLi.

6 min read · verified against [email protected]

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 payload
  • X-Request-ID: a3e9-c1f2-... - UUID, random characters, base64-shaped
  • ?_t=1684827341 - cache-bust timestamp, looks like SQL int
  • Authorization: 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

ListWhatMatch style
excluded_detection_headersHTTP header namesCase-insensitive
excluded_detection_paramsURL query parameter keysCase-insensitive
excluded_detection_body_fieldsBody field names - JSON keys at any depth, plus form and multipart field namesCase-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_id at the root - excluded
  • user.trace_id - excluded
  • metadata.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_bodybool

Scan 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.

default: True

Exclusions 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_thresholdfloat

Anomaly score required to flag a request as a threat

default: 1.0

Penetration 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:

ToolScopeReach for it when
excluded_detection_*Removes specific fields from scanning entirelyA known-safe field (trace ID, JWT) trips matchers
detection_scan_bodyRemoves the entire request body from scanningThe body is machine-generated or binary and FPs concentrate there
detection_threat_score_thresholdGlobal sensitivity - how much evidence flags a requestFPs are spread across many fields, or lone weak signals dominate the noise

Troubleshooting

SymptomLikely causeFix
Excluded a body field but still getting false positivesThe app runs guard-core ≤3.3.x, where exclusion matched top-level JSON keys onlyConfirm the installed guard-core version - any-depth matching landed in 3.4.0.
Excluded X-Correlation-ID but it still tripsThe header doesn't reach the middleware under that name - proxies sometimes strip or rename headers in transitConfirm 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 changedExclusion 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 thresholdThreshold is now higher than a single-signal attack scoresLower it back toward 1.0; add corroborating behavioral rules rather than leaning on a high threshold