Repository navigation
feat(pii): add UK NINO detection and redaction to Tier 1 - #11
Conversation
|
Merged current
Verification at the merge head
Tier 1 overhead, since CONTRIBUTING.md sets a sub-millisecond bar for compiled Tier 1 regex. Measured over 2000 rounds on an 800-character payload containing an email, a phone number, an SSN, a card number, an IP and a NINO, Tier 1 only, comparing
Separately, timing Synthetic-mode placeholder for In synthetic mode,
Which of those you want is a call about your product rather than about the pattern, so I have left it to you. |
|
@snowyukitty Before the findings, an apology that is squarely mine. main was force-pushed after you merged it. Your
Same messages, different SHAs, so your merge base no longer exists and the diff reads as 144 files. You will need to rebase onto current main, and you will hit conflicts that are the result of the rewrite rather than anything you wrote. Sorry for that. Shout if it gets messy and I will help untangle it. Two actual findings. 1. The pattern matches ordinary English prose This is the main one. The pattern is case insensitive and treats each separator as independently optional, so any two letter word followed by three 2-digit groups and a letter a-d matches: 12 of the 23 most common two letter English words are valid NINO prefixes under the HMRC rules: Your verification would not have surfaced this, and I do not think that reflects on the work. The test suite only asserts that invalid NINOs are not matched, so there is no assertion that ordinary prose passes through untouched. Could you add a couple of cases in that direction? Honestly that class of assertion is missing for the other Tier 1 patterns too, so it would be a useful thing to establish here. 2. This one landed after your snapshot of main, so there was no way for you to pick it up. That is a leak, so the NINO pattern should use the helpers once you rebase. Your "76 lines byte-identical, nothing removed" note is exactly right, and it is why the A suggested pattern Requiring uppercase and one consistent separator (via a backreference) fixes both issues at once. I tested this against all five of your positive samples and your full invalid list, and it keeps every positive and rejects every negative: (
"UK_NINO",
re.compile(
_ASCII_LEFT_BOUNDARY
+ r"(?!(?:GB|BG|NK|KN|TN|NT|ZZ))[A-CEGHJ-PR-TW-Z][A-CEGHJ-NPR-TW-Z]([ -]?)\d{2}\1\d{2}\1\d{2}\1[A-D]"
+ _ASCII_RIGHT_BOUNDARY
),
),Two known trade-offs, both of which I think are the right call, but say so if you disagree:
Use something different if you see a better approach. The two things I care about are that ordinary prose survives and that the boundary handling matches the rest of Tier 1. On synthetic mode Thanks for laying the three options out so clearly, and for measuring what Let's go with option 3 and leave the generic word fallback, and keep it out of this PR. Option 2 manufactures strings that could collide with a real person's identifier, and as you found, option 1 produces placeholders the engine cannot re-detect. Redaction and rehydration both work correctly today, which is what matters. I will open a separate issue to track the placeholder shape. Happy to look again as soon as you have rebased. Thanks again for the care you put into this one, and sorry again about the force push. |
Adds a UK_NINO entry to TIER1_PATTERNS for HMRC National Insurance numbers. The prefix excludes D, F, I, Q, U and V in both positions and O in the second, and rejects the seven pairs HMRC never issues. The pattern requires upper case and captures the separator once, backreferencing it for each following group. A case-insensitive pattern whose separators are independently optional matches any two-letter word followed by three 2-digit groups and a letter a-d; twelve of the twenty-three most common two-letter English words are valid NINO prefixes, so that shape rewrites ordinary conversation rather than redacting an identifier. Boundaries use _ASCII_LEFT_BOUNDARY / _ASCII_RIGHT_BOUNDARY like every other Tier 1 pattern, so a NINO glued to non-Latin script still matches; Python's \b treats CJK characters as word characters and would let it through. Tests cover the accepted formats, the reserved pairs, both position-specific character sets, invalid suffixes and lengths, mixed separators, lower case, and adjacency to non-Latin script. They also assert that ordinary prose comes back from the whole Tier 1 cascade byte-for-byte unchanged, which is the assertion this suite did not previously have.
459d8ca to
008f79e
Compare
|
Thanks for the review — both findings were real, and the second one I had no way to see. Rebased onto current The 144-file diff. The conflicts were the ones you predicted, and they turned out to be avoidable: the three commits I had merged have trees identical to their rewritten counterparts — 1. Prose false positives. Taken as you wrote it — uppercase, ASCII boundary helpers, one captured separator backreferenced for each group. Your three sentences pass through untouched, as do the other nine two-letter prefixes ( The test is stronger than a NINO check: it asserts 2. Trade-offs, so they are yours rather than hidden. Lower case and mixed separators no longer match, which you called. Beyond those, requiring the separator before the suffix also misses I checked whether making just that separator optional ( Synthetic mode is untouched — option 3, generic word fallback, as you decided. The diff is Validation at
I also checked the character classes against HMRC's NIM39110 letter by letter: |
|
/review |
|
Re-reviewed at the rebased head ( Both findings from the earlier review round are fixed in this diff:
I independently re-derived the two character classes against HMRC NIM39110 ( No new findings in the personal-data/secrets, fail-open, hot-path, or measurement-claim classes. Verdict: MERGEABLE |
|
@greptileai please review |
|
@snowyukitty thank you, and sorry this sat for five weeks after you fixed everything. That delay was on me. I checked both findings against the current code:
I merged current One small thing for a possible follow-up, not a blocker: a number written with spaces in the digits but none before the letter ( Thanks again for the careful work, especially the prose test. That kind of assertion was missing for the other Tier 1 patterns too. |
Description
Adds Tier 1 detection and redaction for UK National Insurance numbers (NINO) to
llm_shield_proxy/engines/pii_engine.py.Body refreshed at
008f79e. It described the pre-review pattern and the pre-rebase diff, both of which changed; every claim below is anchored to that commit so it ages visibly rather than silently.The pattern
Added to
TIER1_PATTERNSbetween the existing ASCII boundary assertions:D F I Q U V; the second excludes those andO, per HMRC NIM39110. Checked letter by letter.GB BG NK KN TN NT ZZ— are rejected by the lookahead.AB123456C,AB 12 34 56 C,AB-12-34-56-C. Mixed or partial separators (AB 12-34 56C,AB 12 34 56C) do not match.[UK_NINO_N]through the existing entity-type naming. No shape-preserving synthetic token is added; synthetic mode falls back to the generic word, as decided in review.Why upper case and one consistent separator. A case-insensitive pattern with independently optional separators matches ordinary English: twelve of the twenty-three most common two-letter words are valid NINO prefixes, so
the meeting is on 10 29 38 cwould be rewritten in flight by a proxy that edits prompts as they pass. The prose test asserts the whole Tier 1 cascade returns thirteen such sentences byte-identical, so it fails if any pattern rewrites conversation, not only this one.Scope
2 files, +102/-0 at
008f79e: 14 lines inllm_shield_proxy/engines/pii_engine.pyand 88 intests/test_pii_engine.py. Nothing is removed and no existing pattern is touched.Tests cover valid NINOs in all three separator styles, the seven reserved prefix pairs, every forbidden prefix letter in both positions, invalid suffixes, wrong digit counts, mixed separators, lower case, and a match glued to non-Latin script — the case the ASCII boundary assertions exist for and
\bwould miss.Verification
Measured at
008f79eand recorded in full on 2026-09-03: #11 (comment) — the suite,ruff,bandit, the Tier 1 latency delta against the sub-millisecond bar in CONTRIBUTING.md, and a backtracking probe over adversarial inputs up to 80,000 characters.Re-run at
008f79eon 2026-09-04:pytest tests/test_pii_engine.py— 19 passed.pytest --ignore=tests/k8s --ignore=tests/k8s_ootb --ignore=tests/ootb— 677 passed, 44 skipped, 13 xfailed, 0 failed. (Six fewer tests collect in this environment than in the 2026-09-03 run, which reported 684 passed and 43 skipped; nothing fails in either.)Closes #7