[object Object]

← back to Cli Printing Press

fix(skills): extend retro pre-upload scrub with jurisdiction-specific PII patterns (#1302)

42093ad854a0c2505104aba03f92d418c04d3f59 · 2026-05-13 11:56:51 -0700 · Trevin Chow

* fix(skills): extend retro pre-upload scrub with jurisdiction-specific PII patterns

Layer 2 of skills/printing-press-retro/references/secret-scrubbing.md was
US-token-only (sk_live_, ghp_, Bearer, JWT). It missed jurisdiction-specific
identity numbers that flow verbatim from live-API responses captured during
browser-sniff or live-key dogfood, allowing a real Italian-fiscal-data leak to
reach a public upload host before catbox URLs were filed on GitHub issues.

Adds a Jurisdiction-specific PII scanning subsection covering:
- Italian Codice Fiscale (`LLLLLLDDLDDLDDDL`, 16 chars)
- EU IBAN anchored to the SEPA country-code prefix list (so the broad
  body cannot match phones, order IDs, or vendor SKUs)
- US SSN (dashed `DDD-DD-DDDD`)

PII patterns use `~` as the field separator because the IBAN regex
needs `|` for the country-code alternation. Post-scrub verification is
extended in lockstep so anything that slips past Layer 2 still surfaces
before the upload step.

Defense-in-depth only: free-form names, addresses, unenumerated
jurisdictions, and bare 11-digit Partita IVA / VAT numbers stay out of
scope (false-positive rate too high without an allowlist). Direction B
(refuse-upload gate on `discovery/sample-*.json`) and Direction C
(content-summary confirm prompt) from the issue are deferred.

Refs #1291

* fix(skills): case-insensitive PII scrub for mixed-case IBAN and CF

API responses routinely lowercase or mix-case IBANs and Italian Codice
Fiscale values. The initial PII scrub loop and the post-scrub PII
verification both used case-sensitive matching, so a lowercase IBAN
like `it60x0542811101000000123456` would have slipped through both
layers undetected — the same blind spot the credential-pattern Layer 2
loop already had to avoid.

Adds the `-i` grep flag and `/i` perl flag in the PII scrub loop, and
the `-i` grep flag in the post-scrub verification. SSN is digits-only
so the flag is a no-op there; CF and EU-IBAN are where this matters.

Refs #1291

* docs(skills): note PII_REGEX must mirror PII_PATTERNS shapes

If a follow-up extends PII_PATTERNS (e.g. Partita IVA with an
allowlist) without touching PII_REGEX, the post-scrub verification
silently stops checking that shape. The comment co-locates the
synchronization obligation so the two stay in lockstep.

Refs #1291

Files touched

Diff

commit 42093ad854a0c2505104aba03f92d418c04d3f59
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Wed May 13 11:56:51 2026 -0700

    fix(skills): extend retro pre-upload scrub with jurisdiction-specific PII patterns (#1302)
    
    * fix(skills): extend retro pre-upload scrub with jurisdiction-specific PII patterns
    
    Layer 2 of skills/printing-press-retro/references/secret-scrubbing.md was
    US-token-only (sk_live_, ghp_, Bearer, JWT). It missed jurisdiction-specific
    identity numbers that flow verbatim from live-API responses captured during
    browser-sniff or live-key dogfood, allowing a real Italian-fiscal-data leak to
    reach a public upload host before catbox URLs were filed on GitHub issues.
    
    Adds a Jurisdiction-specific PII scanning subsection covering:
    - Italian Codice Fiscale (`LLLLLLDDLDDLDDDL`, 16 chars)
    - EU IBAN anchored to the SEPA country-code prefix list (so the broad
      body cannot match phones, order IDs, or vendor SKUs)
    - US SSN (dashed `DDD-DD-DDDD`)
    
    PII patterns use `~` as the field separator because the IBAN regex
    needs `|` for the country-code alternation. Post-scrub verification is
    extended in lockstep so anything that slips past Layer 2 still surfaces
    before the upload step.
    
    Defense-in-depth only: free-form names, addresses, unenumerated
    jurisdictions, and bare 11-digit Partita IVA / VAT numbers stay out of
    scope (false-positive rate too high without an allowlist). Direction B
    (refuse-upload gate on `discovery/sample-*.json`) and Direction C
    (content-summary confirm prompt) from the issue are deferred.
    
    Refs #1291
    
    * fix(skills): case-insensitive PII scrub for mixed-case IBAN and CF
    
    API responses routinely lowercase or mix-case IBANs and Italian Codice
    Fiscale values. The initial PII scrub loop and the post-scrub PII
    verification both used case-sensitive matching, so a lowercase IBAN
    like `it60x0542811101000000123456` would have slipped through both
    layers undetected — the same blind spot the credential-pattern Layer 2
    loop already had to avoid.
    
    Adds the `-i` grep flag and `/i` perl flag in the PII scrub loop, and
    the `-i` grep flag in the post-scrub verification. SSN is digits-only
    so the flag is a no-op there; CF and EU-IBAN are where this matters.
    
    Refs #1291
    
    * docs(skills): note PII_REGEX must mirror PII_PATTERNS shapes
    
    If a follow-up extends PII_PATTERNS (e.g. Partita IVA with an
    allowlist) without touching PII_REGEX, the post-scrub verification
    silently stops checking that shape. The comment co-locates the
    synchronization obligation so the two stay in lockstep.
    
    Refs #1291
---
 .../references/secret-scrubbing.md                 | 60 ++++++++++++++++++++--
 1 file changed, 56 insertions(+), 4 deletions(-)

diff --git a/skills/printing-press-retro/references/secret-scrubbing.md b/skills/printing-press-retro/references/secret-scrubbing.md
index 1bc4914e..4c5c08e6 100644
--- a/skills/printing-press-retro/references/secret-scrubbing.md
+++ b/skills/printing-press-retro/references/secret-scrubbing.md
@@ -76,6 +76,51 @@ for entry in "${PATTERNS[@]}"; do
 done
 ```
 
+### Jurisdiction-specific PII scanning
+
+Live-API responses captured during browser-sniff or live-key dogfood routinely
+include identifying data of the data subject and any third parties the API
+surfaced. These patterns redact common high-confidence shapes before upload.
+They are defense-in-depth, not bulletproof — free-form names, descriptive
+fields, and unenumerated jurisdictions still slip through.
+
+The `|` field separator collides with the `(IT|DE|...)` country-code
+alternation in the IBAN regex, so PII patterns use `~` as the field separator.
+
+```bash
+PII_PATTERNS=(
+  'codice-fiscale~\b[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]\b~<REDACTED:pii-codice-fiscale>'
+  'eu-iban~\b(AD|AT|BE|BG|CH|CY|CZ|DE|DK|EE|ES|FI|FR|GB|GI|GR|HR|HU|IE|IS|IT|LI|LT|LU|LV|MC|MT|NL|NO|PL|PT|RO|SE|SI|SK|SM|VA)[0-9]{2}[A-Z0-9]{11,30}\b~<REDACTED:pii-eu-iban>'
+  'us-ssn~\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b~<REDACTED:pii-us-ssn>'
+)
+
+for entry in "${PII_PATTERNS[@]}"; do
+  IFS='~' read -r name regex tag <<< "$entry"
+  for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
+    [ -d "$dir" ] || continue
+    find "$dir" -type f -print0 | while IFS= read -r -d '' f; do
+      # Case-insensitive: API JSON routinely lowercases IBANs and other identifiers.
+      if grep -qiE "$regex" "$f" 2>/dev/null; then
+        perl -i -pe "s/$regex/$tag/gi" "$f" 2>/dev/null
+        echo "Redacted $name in $(basename "$f")"
+      fi
+    done
+  done
+done
+```
+
+Pattern notes:
+
+- **Codice Fiscale** (Italian tax code) is a 16-character `LLLLLLDDLDDLDDDL` shape with no plausible collision against ordinary text.
+- **EU IBAN** is anchored to the SEPA country-code prefix list, so the broad `[A-Z0-9]{11,30}` body cannot match a generic phone number, order ID, or vendor SKU.
+- **US SSN** uses the `DDD-DD-DDDD` dashed form, which avoids collisions with bare 9-digit runs in other identifiers.
+
+Out of scope (deferred to follow-up work):
+
+- Bare 11-digit Partita IVA / VAT numbers (false-positive rate against order IDs, phone numbers, and timestamps is too high without an allowlist).
+- Free-form residential addresses (not regex-matchable with acceptable precision).
+- Refusing upload when `discovery/sample-*.json` files are present (a separate gate at the staging-copy step, tracked separately).
+
 ### Env var assignment scanning
 
 Separately scan for hardcoded secret assignments in source code:
@@ -137,16 +182,23 @@ After all layers complete, do a final scan for obvious leaks:
 
 ```bash
 FINAL_CHECK=false
+CRED_REGEX='(sk_live_|sk_test_|ghp_|gho_|Bearer [A-Za-z0-9]{20})'
+# PII_REGEX must mirror the shapes in PII_PATTERNS above; update both together
+# (e.g. when adding Partita IVA with an allowlist) so the verification step
+# does not silently stop checking a shape the scrub loop still redacts.
+PII_REGEX='(\b[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]\b|\b(AD|AT|BE|BG|CH|CY|CZ|DE|DK|EE|ES|FI|FR|GB|GI|GR|HR|HU|IE|IS|IT|LI|LT|LU|LV|MC|MT|NL|NO|PL|PT|RO|SE|SI|SK|SM|VA)[0-9]{2}[A-Z0-9]{11,30}\b|\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b)'
 for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
   [ -d "$dir" ] || continue
-  MATCHES=$(grep -rEi '(sk_live_|sk_test_|ghp_|gho_|Bearer [A-Za-z0-9]{20})' "$dir" 2>/dev/null | grep -v 'REDACTED' | head -5)
-  if [ -n "$MATCHES" ]; then
-    echo "$MATCHES"
+  CRED_MATCHES=$(grep -rEi "$CRED_REGEX" "$dir" 2>/dev/null | grep -v 'REDACTED' | head -5)
+  PII_MATCHES=$(grep -rEi "$PII_REGEX" "$dir" 2>/dev/null | grep -v 'REDACTED' | head -5)
+  if [ -n "$CRED_MATCHES" ] || [ -n "$PII_MATCHES" ]; then
+    [ -n "$CRED_MATCHES" ] && echo "$CRED_MATCHES"
+    [ -n "$PII_MATCHES" ] && echo "$PII_MATCHES"
     FINAL_CHECK=true
   fi
 done
 if [ "$FINAL_CHECK" = true ]; then
-  echo "WARNING: Potential secrets still found after scrubbing. Review the matches above."
+  echo "WARNING: Potential secrets or PII still found after scrubbing. Review the matches above."
   echo "Artifacts will NOT be uploaded until this is resolved."
 fi
 ```

← 8cc7e655 fix(skills): skip publish-validate in mid-pipeline polish (#  ·  back to Cli Printing Press  ·  chore(ci): raise mergify batch_size and add queue safety kno a117a741 →