← 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
M skills/printing-press-retro/references/secret-scrubbing.md
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 →