Regex scanner
The RegexScanner (guardrails/scanners.py) is the deterministic pattern
layer — hidden ASCII, instruction-format markers, secrets and PII. It has zero
ML dependencies and runs on both the request side (tools/call params) and
the response side (tool output / tool descriptions).
Matching model
First-match wins. Patterns are evaluated in list order, so the built-in
list is a priority chain: BLOCK patterns first, then HUMAN_REVIEW, then
ALLOW. A single scan returns at most one result per pass.
Two-pass matching. Pass 1 evaluates every pattern against the original
text. Unless pass 1 already produced a BLOCK (the strongest verdict) or the
text is pure ASCII, pass 2 re-evaluates every pattern against a
detection-only normalized view of the text, and the more severe of the
two hits wins. The severity comparison closes a downgrade evasion: an
attacker cannot neutralise an obfuscated BLOCK marker by adding a benign
string that trips a low-grade pass-1 pattern.
Normalized view
normalized_view(text) is a matching-only transform — it never rewrites
the payload (redaction, forwarding and payload_sha256 all operate on the
original text):
- NFKC fold — collapses full-width forms, compatibility ligatures, circled/squared letters, etc.
- Cf strip — removes Unicode format characters (category Cf): zero-width spaces/joiners, bidi controls, soft hyphens, word joiners.
- Homoglyph fold — maps common Cyrillic and Greek lookalikes of ASCII
letters (e.g. Cyrillic
Ѕ/Ε/Μin a### SYSTEMmarker) to their ASCII equivalents. The map is deliberately limited to unambiguous lookalikes, not a full Unicode confusables table.
Pass 2 is skipped when it cannot change the verdict: pass 1 already BLOCKed,
the text is pure ASCII (the view is provably identical), or the computed view
equals the original. A normalized-view hit's audit reason is suffixed
[normalized-view match] and fingerprints the original chunk
(orig_sha256 / orig_hmac); match_len is the match length in the view.
Built-in pattern set
From default_patterns() in guardrails/scanners.py, in evaluation order:
| # | Name | Outcome | Detects | Example shape | Notes |
|---|---|---|---|---|---|
| 1 | hidden_ascii |
BLOCK | Control characters (except tab/newline/CR), Unicode control pictures, RTL overrides, zero-width chars used to hide instructions | \x00–\x1f, U+200B–U+200F, U+202A–U+202E, U+2060–U+206F, U+FEFF |
Score 0.9. Hidden-instruction carrier. |
| 2 | format_injection |
BLOCK | ChatML / instruction-format tokens used to escape the conversation role | [SYSTEM], [INST], [/INST], [ASSISTANT], <|im_start|>, <|im_end|>, <|endoftext|>, ### system/instruction/override/ignore |
Score 0.98. Case-insensitive (re.IGNORECASE) — casing variants like ### System or <|IM_START|> still hit. PromptGuard-2 (trained on the Llama tokenizer) does not recognise ChatML tokens, so this is the deterministic backstop. |
| 3 | private_key |
BLOCK | PEM private key material | -----BEGIN (RSA|EC|OPENSSH|DSA)? PRIVATE KEY----- |
Score 0.99. |
| 4 | aws_access_key |
BLOCK | AWS access key id | AKIA[0-9A-Z]{16} |
Score 0.95. |
| 5 | aws_temp_key |
BLOCK | AWS temporary security credential (STS / IAM role) | ASIA[0-9A-Z]{16} |
Score 0.95. |
| 6 | google_api_key |
BLOCK | Google API key | AIza + 35 chars (39 total) |
Score 0.95. |
| 7 | github_pat |
BLOCK | GitHub personal access token | gh[pousr]_ + ≥36 chars |
Score 0.95. |
| 8 | gitlab_pat |
BLOCK | GitLab personal access token | glpat- + 20 chars |
Score 0.95. |
| 9 | slack_token |
BLOCK | Slack token | xox[baprs]- + ≥10 chars |
Score 0.95. |
| 10 | llm_api_key |
BLOCK | OpenAI / Anthropic / common LLM API keys — sk-<key>, sk-proj-, sk-svcacct-, sk-ant-api03- |
sk- + ≥20 chars |
Score 0.95. The ≥20-char requirement avoids false positives. |
| 11 | jwt |
HUMAN_REVIEW | JSON Web Token (three base64url segments, header starts eyJ) |
eyJ….…. (signature ≥4 chars) |
Score 0.80. Review grade — JWTs appear in benign contexts too. |
| 12 | connection_string |
HUMAN_REVIEW | Connection strings with embedded credentials | mongodb(+srv)://…, postgres(ql)://…, mysql://…, redis://…, amqp(s)://… (≥10 chars after scheme) |
Score 0.75. Low-entropy fingerprint tier: audit records match_len only (no digest) because the match embeds the password itself. |
| 13 | key_value_credential |
HUMAN_REVIEW | Inline key=value credentials |
password=…, passwd:, secret=, token=, api_key=, access-key:, private_key=, bearer= + ≥8 chars |
Score 0.70. Review grade because "password"/"secret" appear in benign docs and code. Low-entropy tier (length only). |
| 14 | md_image_exfil |
HUMAN_REVIEW | Markdown image exfiltration:  with a single query value of ≥32 chars from a base64/url-ish alphabet |
 |
Score 0.65. Review grade, not BLOCK: signed CDN image URLs with long token params (?sig=…, ?token=…) are a legitimate false-positive source. Pure-exfil payloads usually also trip high_entropy_blob or a key pattern, which still BLOCKs. |
| 15 | high_entropy_blob |
HUMAN_REVIEW | Generic high-entropy blob (possible secret) | [A-Za-z0-9+/]{40,}={0,2} |
Score 0.60. |
| 16 | credit_card |
HUMAN_REVIEW | Possible credit-card number | 13–16 digits with optional spaces/dashes | Score 0.70. Low-entropy tier: length only, so the audit log is not an offline enumeration oracle. |
| 17 | email |
ALLOW | Email address (PII — redact downstream) | user@example.com |
Score 0.20. ALLOW verdict; masking is the redaction stage's job. Low-entropy tier. |
Audit fingerprints
Scanner reasons never embed raw match text (they are copied verbatim into audit logs and OTel spans). Each reason carries a non-reversible fingerprint:
- High-entropy patterns (tokens, keys):
match_lenplus a 12-hex-char digest — HMAC-SHA256 keyed withAUDIT_HMAC_KEYwhen set (recommended), or plain SHA-256 otherwise. - Low-entropy patterns (
email,credit_card,connection_string,key_value_credential):match_lenonly, no digest — a plain hash of a brute-forceable value would make the audit log an offline enumeration oracle.
Configuration
| Knob | Default | Effect |
|---|---|---|
ENABLE_REGEX_SCANNER |
true |
Master switch for the scanner. |
MAX_CONTENT_BYTES |
32768 |
Head window fed to the scanner (see Scan coverage). |
SCAN_TAIL_BYTES |
8192 |
Mid/tail windows on over-budget payloads. |
Custom pattern lists can be injected by constructing RegexScanner(patterns=[...])
with your own Pattern(name, regex, outcome, reason, score) entries —
remember the list is a priority chain (first match wins).