From a03f571147f853843b40cb5e0e57d4e4a4ba1851 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 19:23:58 -0700 Subject: [PATCH] fix(redact): stop the E.164 phone pattern flagging compact timestamps Bare 14-digit runs like 20260727202423 (YYYYMMDDHHMMSS backup/log stamps) matched the phone regex and produced MEDIUM PII findings. Reject a separator-free 14-digit span whose fields parse as a plausible date-time; real numbers carry a + or spacing, so phone coverage is unchanged. Contributed by @abkrim (PR #2428). Co-Authored-By: Claude Fable 5 --- lib/redact-patterns.ts | 35 ++++++++++++++++++++++++++++++++++- test/redact-engine.test.ts | 3 ++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index ba85c0de3..060d543f0 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -138,6 +138,36 @@ function looksLikeWallet(span: string): boolean { return span.length >= 26 && span.length <= 62; } +// Compact log/backup stamps (`20260727202423` = YYYYMMDDHHMMSS) are bare digit +// runs that the phone regex happily eats. Only a SEPARATOR-FREE 14-digit span +// qualifies: E.164 tops out at 15 digits and real numbers carry a + or spacing, +// so rejecting this shape costs no phone coverage. +function looksLikeCompactTimestamp(span: string): boolean { + if (!/^\d{14}$/.test(span)) { + return false; + } + const n = (from: number, to: number) => Number(span.slice(from, to)); + const [year, month, day, hour, minute, second] = [ + n(0, 4), + n(4, 6), + n(6, 8), + n(8, 10), + n(10, 12), + n(12, 14), + ]; + return ( + year >= 1900 && + year <= 2999 && + month >= 1 && + month <= 12 && + day >= 1 && + day <= 31 && + hour <= 23 && + minute <= 59 && + second <= 59 + ); +} + // ── Placeholder suppression (per-matched-span, NOT per-line) ───────────────── /** @@ -498,7 +528,10 @@ export const PATTERNS: RedactPattern[] = [ autoRedactable: true, redactToken: "", // A digit-only UUID's hyphen groups read as national phone formatting. - validate: (span, match) => !insideUuid(match) && span.replace(/\D/g, "").length >= 10, + validate: (span, match) => + !insideUuid(match) && + span.replace(/\D/g, "").length >= 10 && + !looksLikeCompactTimestamp(span), }, { id: "pii.ssn", diff --git a/test/redact-engine.test.ts b/test/redact-engine.test.ts index d86693e19..47d6c1be7 100644 --- a/test/redact-engine.test.ts +++ b/test/redact-engine.test.ts @@ -203,8 +203,9 @@ describe("PII patterns", () => { scan("bob@acme.co", { repoVisibility: "private", repoPublicEmails: ["bob@acme.co"] }).findings, ).toHaveLength(0); }); - test("phone E.164", () => { + test("phone E.164 flags, skips compact timestamps", () => { expect(ids("call +14155550123 now")).toContain("pii.phone.e164"); + expect(ids("backup stamp 20260727202423 ran late")).not.toContain("pii.phone.e164"); }); test("ssn flags valid, skips 000 octet", () => { expect(ids("ssn 123-45-6789")).toContain("pii.ssn");