Skip to content

feat(guardrails): per-type PII selection, custom masking patterns and detection counters - #16902

Merged
diegosouzapw merged 7 commits into
release/v3.8.52from
feat/port-pr-3958-pii-per-type-custom-patterns
Oct 17, 2026
Merged

diegosouzapw merged 7 commits into
release/v3.8.52from
feat/port-pr-3958-pii-per-type-custom-patterns

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Summary

  • PII masking stays opt-in (PII_REDACTION_ENABLED / PII_RESPONSE_SANITIZATION, both still defaultValue: "false"; tests/unit/pii-opt-in-default.test.ts untouched and green). This PR only adds control over what is masked once an operator turns it on.
  • Per-type selection of the built-in patterns, up to 20 operator-defined regex/wildcard patterns gated against ReDoS, and in-memory per-type detection counters (counts only, never values).
  • GET/PUT /api/settings/pii, POST /api/settings/pii/test, and a PII masking card under Settings → Security with presets (Minimal / Standard / Financial / Brazilian / USA / Full).

Attribution

Thanks to @lsilveirab39 for the original implementation.

Changes

  • src/shared/constants/piiTypes.ts — catalog of built-in type ids (the piiSanitizer pattern names), request-side alias ssn_us → ssn, UI presets, limits.
  • src/shared/utils/piiCustomPatterns.ts — the single gate every custom pattern goes through: ≤ 500 chars, flags g i m s u only, must compile, must pass safe-regex (catastrophic backtracking), must not match "" or a redaction placeholder (would re-redact its own output in the SSE window). Wildcards are literal except * (bounded to 64 non-whitespace chars, consecutive * collapsed) and ?. Uses the safe-regex dependency already used by evalRunner / ccrQuery — no new dependency.
  • src/lib/db/piiSettings.ts — key_value namespace pii_settings, 1 s read cache (sanitizers call it per string leaf / per chunk). No migration.
  • src/lib/pii/piiRuleConfig.ts — resolves settings into enabled types + compiled rules (recompiled only when the content changes); falls back to "all built-ins, no custom" if settings cannot be read.
  • src/lib/piiSanitizer.ts, src/shared/utils/inputSanitizer.ts — filter built-ins by the selection and append custom rules (new optional argument; no-arg calls behave as before). Zero-length-match guard in the exec loop as defense in depth.
  • src/lib/guardrails/piiMasker.ts — applies selection + custom rules only while request masking is on (flag off = the exact built-in scan as before); records request counters.
  • src/lib/streamingPiiTransform.ts, sanitizePIIResponse — record response counters (log-sanitization callers of sanitizePII do not count).
  • src/lib/pii/piiDetectionStats.ts — in-memory counters, reset on restart.
  • Routes: Zod (src/shared/validation/schemas/piiSettings.ts, kept out of the schemas barrel so safe-regex is not pulled into every importer), requireManagementAuth, createErrorResponse (sanitized), sample capped at 8192 UTF-8 bytes, unsafe pattern refused with 400 on save and on test, audit event settings.pii.updated with counts only.
  • UI: PiiMaskingCard.tsx rendered in SecurityTab; 39 new settings.piiMasking.* keys translated in all 66 locales (common keys reused for Save/Delete/Name/Enabled/Disabled/Responses).
  • Docs: docs/security/GUARDRAILS.md section, docs/openapi.yaml entries.

Out of scope on purpose: reversible pseudonymisation, a mapping API, a consent-log table.

Test plan

TDD — tests/unit/pii-per-type-selection.test.ts run against the base versions of piiSanitizer.ts / inputSanitizer.ts / piiMasker.ts / streamingPiiTransform.ts:

✖ response sanitizer honours the per-type selection
✖ request masker honours the per-type selection (ssn_us maps to ssn)
✖ custom patterns are redacted on both sides when masking is on
✖ detection counters count types (never values) while masking is on
ℹ pass 4 ℹ fail 4

With the port:

$ node --import tsx/esm --test tests/unit/pii-per-type-selection.test.ts tests/unit/pii-settings-route.test.ts tests/unit/pii-opt-in-default.test.ts
ℹ tests 19 ℹ pass 19 ℹ fail 0

# every existing test that cites piiSanitizer / inputSanitizer / piiMasker / streamingPiiTransform / processPII / sanitizePII / SecurityTab (27 files) + log-payload-pii-walk-opt-in + i18n-vi-completeness + the 2 new files
ℹ tests 280 ℹ pass 280 ℹ fail 0

Gates (run locally, isolated DATA_DIR, OMNIROUTE_API_KEY unset):

  • npm run typecheck:core → 0 errors
  • npm run check:dashboard-typecheck → OK (195 pre-existing, within baseline) · check-api-typecheck → OK (255 pre-existing)
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> → clean · npx prettier --check <changed files> → clean
  • npm run i18n:check-keys → 66 locales carry all 13796 keys · i18n:check-ratio → OK · i18n:check-new-keys → PASS · check-ui-icu → PASS · i18n:check-ui-coverage → PASS · i18n:check-value-drift → PASS
  • npm run check:docs-all → rc 0 · check:openapi-routes / check:openapi-coverage (96.8%) / check:openapi-security-tiers → PASS · check:route-validation:t06 → PASS
  • check:file-size / check:complexity / check:cognitive-complexity / check:cycles:ratchet → OK
  • check-pr-test-policy → PASS · check:test-masking → 0 modified, no weakening
  • check:boundary:ratchet → FAIL 773 > 771, identical on the untouched base snapshot (this PR adds no open-sse → src import)
1. Settings → Security → "PII masking" card renders; both flag chips read Disabled on a default install.
2. Click "Brazilian", Save → GET /api/settings/pii returns enabledTypes ["cpf","cnpj","phone_br","email"].
3. Add regex "SK-[a-z0-9]{20}", Test with "use SK-abcdefghij0123456789" → preview "use [CUSTOM_REDACTED]"; Save.
4. Test "(a+)+$" → red "Pattern rejected: ... catastrophic backtracking".
5. Enable PII_REDACTION_ENABLED in Feature flags, send a chat request containing the SK- token and a CPF → upstream body has [CUSTOM_REDACTED] and [CPF_REDACTED]; the card's counters increase.
6. Disable the flag again → the same request reaches upstream untouched.

⚠️ base-red inherited: #15306 — unit/vitest/integration/package-artifact gates on release/v3.8.52

… detection counters

PII masking stays opt-in (PII_REDACTION_ENABLED / PII_RESPONSE_SANITIZATION,
both default off). This adds control over WHAT gets masked once it is on:

- Per-type selection of the built-in patterns (unset = every type, so the
  behaviour is unchanged until an operator picks types). The request engine's
  ssn_us maps to the shared ssn id.
- Up to 20 custom regex/wildcard patterns, replaced with [CUSTOM_REDACTED].
  Each one is gated by a length cap, a flag allow-list, a compile check,
  safe-regex (catastrophic backtracking) and a refusal of patterns that match
  "" or a redaction placeholder. Unsafe patterns are refused on save and on
  test, and a stored row that fails the gate is skipped at runtime. Custom
  rules only run while masking is on.
- In-memory per-type detection counters (counts only, never values) that only
  move while masking is on.
- GET/PUT /api/settings/pii and POST /api/settings/pii/test (Zod, management
  auth, 8 KB sample cap, sanitized errors), a PII masking card under
  Settings > Security with presets, and docs.

Reversible pseudonymisation, a mapping API and a consent log are out of scope.

Co-authored-by: Leandro Barbosa <leandro.silveirabarbosa@gmail.com>
Inspired-by: decolua/9router#3958
@diegosouzapw diegosouzapw added the hold-vps PR verde, merge aguardando validação live (release-drain) label Oct 16, 2026
id: "evil",
name: "evil",
type: "regex" as const,
pattern: "(a+)+$",
assert.deepEqual(stats.request, { email: 2 });
assert.deepEqual(stats.response, { cpf: 1 });
assert.equal(stats.total, 3);
assert.ok(!JSON.stringify(stats).includes("example.com"), "no matched value is kept");
…rom PII test; regenerate omni-settings skill

Builds the deliberately catastrophic custom pattern at runtime so it is test
data rather than a regex literal (CodeQL js/redos), and replaces the
"example.com" substring check with an exact assertion on the stats snapshot
keys (CodeQL js/incomplete-url-substring-sanitization) - request/response
are already compared exactly, so no matched value can hide in it.
Regenerates skills/omni-settings for the new /api/settings/pii routes.
Nothing imports it; it was the one new dead export the dead-code gate counted.
@diegosouzapw
diegosouzapw merged commit 0ab2f0d into release/v3.8.52 Oct 17, 2026
9 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

CI Coverage Report

  • Coverage job: skipped
  • PR test policy: success

Coverage artifact was not available for this run.

@diegosouzapw diegosouzapw mentioned this pull request Oct 18, 2026
@diegosouzapw
diegosouzapw deleted the feat/port-pr-3958-pii-per-type-custom-patterns branch October 18, 2026 23:48
diegosouzapw added a commit to zerotraceio/OmniRoute that referenced this pull request Oct 22, 2026
…e, per-type ids

Merges release/v3.8.53 into the branch (conflicts in piiSanitizer.ts and
inputSanitizer.ts resolved onto the base per-type selection from cheaper-inference#16902) and
applies the pre-merge items for cheaper-inference#16461:

- Request side: processPII now runs each GDPR/POPIA validator before redacting,
  so order numbers, timestamps and identifiers such as gpt4o / gh12abc are left
  alone (red: 6 of 6 probes redacted at the previous head).
- src/shared/utils/piiPatterns.ts is the single table consumed by both
  sanitizers; the duplicated regexes and the dead PII_PATTERNS export are gone,
  and the existing patterns keep their original relative order.
- Validators tightened: known IBAN country + length, EU VAT per member state
  (unknown prefixes no longer match any uppercase word), NIE is X/Y/Z + 7
  digits, standard Luhn for the ZA ID (the previous variant rejected real IDs),
  PESEL/SE birth-date checks, Steuer-ID digit structure, ZA phone national form
  needs separators, ZA VAT / tax reference need a VAT / tax cue.
- Ids registered in PII_BUILTIN_TYPES with gdpr and popia templates, so the
  settings schema, dashboard card and per-type selection control them; UI
  strings added (non-English catalogs carry the English fallback, my and phi
  translated to stay within the translation-ratio baseline).
- Tests: checksum positives/negatives per pattern, false-positive corpus on
  both sides, per-type selection, catalog/i18n consistency, ReDoS bounds;
  sample ids in piiSanitizer.test.ts corrected to genuinely valid ones.
- docs/security/GUARDRAILS.md section and a changelog fragment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold-vps PR verde, merge aguardando validação live (release-drain)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants