mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 15:09:00 +02:00
fix(redact): parcel IDs are not phone numbers
A county tax-map parcel ID (APN) reads as a national-format phone number to `pii.phone.e164` — the same collision class as the digit-only UUID that `insideUuid` already guards. `12-3456789.000` matches, and so does its normalized `123456789000`. This is not a rare edge. Land, title and property-tax repos carry APNs by the hundred; a single title branch pushed 2 MEDIUM findings, and the same shape recurs in every fixture, mart and smoke in the domain. A guardrail that cries wolf on the domain's primary identifier is one people learn to wave through, which is how a real HIGH finding eventually gets ignored. The guard is deliberately narrow, in two tiers: 1. The DOTTED form is exempt on its own shape. No phone convention puts a dot before a trailing 3-4 digit group after a 4-8 digit middle. Hyphen-only variants (22-0001-000) are NOT shape-exempted — those genuinely are phone-shaped. 2. A DIGITS-ONLY span is phone-shaped in isolation, so it earns the exemption only by evidence: it must be the exact digit-normalization of a punctuated APN within the surrounding window. Fixtures and marts carry the pair; a real phone number has no such twin. This reads the document's own evidence instead of guessing from digits. Verified against the unmodified engine over inputs spanning every rule family (AWS, PEM, GitHub PAT, email, IP, credit card, SSN, timestamp, UUID, nine phone formats): exactly one behavior changed, the APN pair. The new test pins both directions and was proven red under mutation — stubbing the guard to `return true` (the dangerous blanket-exemption failure) fails 12 of 15; `return false` fails 3. Absorbs PR #2591 by @Two-Six-Alpha-1115 (applied via git am -3; 96 tests pass across test/redact-parcel-id-false-positive.test.ts + test/redact-engine.test.ts, and the pattern-lint / CLI / prepush-hook / autoredact suites stay green). Co-authored-by: Scott <scott@peninsulaminerals.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Scott
Claude Fable 5
parent
4e055ca202
commit
4cc19e4712
+56
-2
@@ -168,6 +168,58 @@ function looksLikeCompactTimestamp(span: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Context window for pairing a normalized parcel ID with its punctuated form. */
|
||||
const PARCEL_CONTEXT_CHARS = 400;
|
||||
|
||||
/**
|
||||
* County tax-map parcel ID (APN). Ohio's dominant form is NN-NNNNNNN.NNN, and
|
||||
* some counties use a hyphen before the 3-4 digit suffix instead of a dot.
|
||||
*/
|
||||
const PARCEL_PUNCT_RE = /\b\d{2}-\d{4,8}[.\-]\d{3,4}\b/g;
|
||||
|
||||
/**
|
||||
* A parcel ID reads as a national-format phone number to pii.phone.e164 — the
|
||||
* same collision class as the digit-only UUID that `insideUuid` already guards.
|
||||
* Land/title repos carry these by the hundred, so the false positives are not
|
||||
* incidental: they arrive on every branch that touches title, and a guardrail
|
||||
* that cries wolf on the domain's primary identifier trains people to wave the
|
||||
* warning through, which is how a real HIGH finding eventually gets ignored.
|
||||
*
|
||||
* Deliberately narrow, in two tiers:
|
||||
*
|
||||
* 1. The DOTTED form is exempt on its own shape. No phone convention places a
|
||||
* dot before a trailing 3-4 digit group after a 4-8 digit middle, so this
|
||||
* cannot swallow a real number. The hyphen-only variants (22-0001-000) are
|
||||
* NOT exempted by shape — those genuinely are phone-shaped.
|
||||
*
|
||||
* 2. A DIGITS-ONLY span is phone-shaped in isolation, so it earns the exemption
|
||||
* only by evidence: it must be the exact digit-normalization of a punctuated
|
||||
* parcel ID within the surrounding window. Fixtures and marts always carry
|
||||
* the pair ({parcel_id: "12-3456789.000", norm: "123456789000"}), and a bare
|
||||
* phone number has no such twin nearby — so this reads the document's own
|
||||
* evidence rather than guessing from digits.
|
||||
*/
|
||||
export function looksLikeParcelId(span: string, match: RegExpExecArray): boolean {
|
||||
if (/^\d{2}-\d{4,8}\.\d{3,4}$/.test(span)) return true;
|
||||
if (!/^\d{10,14}$/.test(span)) return false;
|
||||
|
||||
const input = match.input ?? "";
|
||||
const spanStartInMatch = match[1] !== undefined ? match[0].indexOf(match[1]) : 0;
|
||||
const spanStart = match.index + Math.max(0, spanStartInMatch);
|
||||
const spanEnd = spanStart + span.length;
|
||||
const window = input.slice(
|
||||
Math.max(0, spanStart - PARCEL_CONTEXT_CHARS),
|
||||
spanEnd + PARCEL_CONTEXT_CHARS,
|
||||
);
|
||||
|
||||
PARCEL_PUNCT_RE.lastIndex = 0;
|
||||
let p: RegExpExecArray | null;
|
||||
while ((p = PARCEL_PUNCT_RE.exec(window)) !== null) {
|
||||
if (p[0].replace(/\D/g, "") === span) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Placeholder suppression (per-matched-span, NOT per-line) ─────────────────
|
||||
|
||||
/**
|
||||
@@ -558,11 +610,13 @@ export const PATTERNS: RedactPattern[] = [
|
||||
regex: /(?<![\w.])(\+?[1-9]\d{0,2}[ \-.]?\(?\d{2,4}\)?[ \-.]?\d{3,4}[ \-.]?\d{3,4})(?![\w.])/,
|
||||
autoRedactable: true,
|
||||
redactToken: "<REDACTED-PHONE>",
|
||||
// A digit-only UUID's hyphen groups read as national phone formatting.
|
||||
// A digit-only UUID's hyphen groups read as national phone formatting, and
|
||||
// so does a county tax-map parcel ID (see looksLikeParcelId).
|
||||
validate: (span, match) =>
|
||||
!insideUuid(match) &&
|
||||
span.replace(/\D/g, "").length >= 10 &&
|
||||
!looksLikeCompactTimestamp(span),
|
||||
!looksLikeCompactTimestamp(span) &&
|
||||
!looksLikeParcelId(span, match),
|
||||
},
|
||||
{
|
||||
id: "pii.ssn",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* pii.phone.e164 vs county tax-map parcel IDs (APNs).
|
||||
*
|
||||
* A parcel ID reads as a national-format phone number to the e164 pattern —
|
||||
* the same collision class as the digit-only UUID `insideUuid` already guards.
|
||||
* Land, title and property-tax repos carry these by the hundred, so the noise
|
||||
* is not incidental; it arrives on every branch that touches the domain.
|
||||
*
|
||||
* The guard has to be narrow, so this file pins BOTH directions: parcels stay
|
||||
* clean, and every real phone shape stays flagged. The negative controls are
|
||||
* the point — a guard that exempted long digit runs wholesale would pass the
|
||||
* "parcels clean" half and quietly gut the pattern.
|
||||
*/
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { scan } from "../lib/redact-engine";
|
||||
import { looksLikeParcelId } from "../lib/redact-patterns";
|
||||
|
||||
const flagsPhone = (s: string): boolean =>
|
||||
scan(s, { repoVisibility: "private" }).findings.some((f) => f.id === "pii.phone.e164");
|
||||
|
||||
describe("pii.phone.e164 — real phone numbers stay flagged", () => {
|
||||
const REAL_PHONES: [string, string][] = [
|
||||
["US dashed", "call 415-555-0123 now"],
|
||||
["US parens", "phone: (415) 555-0123"],
|
||||
["US dotted", "p 415.555.0123"],
|
||||
["E.164 US", "tel: +14155550123"],
|
||||
["E.164 US spaced", "contact +1 415 555 0123"],
|
||||
["E.164 UK", "ring +44 20 7946 0958"],
|
||||
["E.164 DE", "fon +49 30 901820"],
|
||||
["E.164 PT 12-digit", "reach +351912345678"],
|
||||
["bare 11-digit", "operator 14155550123 ext"],
|
||||
];
|
||||
for (const [label, input] of REAL_PHONES) {
|
||||
test(label, () => {
|
||||
expect(flagsPhone(input)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("pii.phone.e164 — parcel IDs are not phone numbers", () => {
|
||||
test("dotted APN is exempt on shape alone", () => {
|
||||
expect(flagsPhone(' parcel_id: "12-3456789.000",')).toBe(false);
|
||||
});
|
||||
|
||||
test("a normalized APN is exempt when paired with its punctuated form", () => {
|
||||
expect(
|
||||
flagsPhone(' parcel_id: "12-3456789.000",\n norm: "123456789000",'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("8-digit middle is still an APN", () => {
|
||||
expect(flagsPhone('apn "30-00414123.0001"')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pii.phone.e164 — the guard stays narrow", () => {
|
||||
/**
|
||||
* The load-bearing control. A bare digit run is phone-shaped in isolation, so
|
||||
* it may only be exempted by EVIDENCE — a punctuated APN in the surrounding
|
||||
* window. With no such twin, the finding must survive. If this ever goes
|
||||
* green-by-exemption, the guard has become a blanket hole in the pattern.
|
||||
*/
|
||||
test("a bare digit run with no punctuated APN nearby is still flagged", () => {
|
||||
expect(flagsPhone('norm: "123456789000",')).toBe(true);
|
||||
});
|
||||
|
||||
test("hyphen-only APN variants are NOT exempted by shape", () => {
|
||||
// 22-0001-000 is genuinely phone-shaped; only the dotted form earns a
|
||||
// shape-based pass. This one may only be cleared by the evidence tier.
|
||||
expect(looksLikeParcelId("22-0001-000", /(.*)/.exec("22-0001-000")!)).toBe(false);
|
||||
});
|
||||
|
||||
test("evidence pairing requires an exact digit match, not a prefix", () => {
|
||||
// A near-miss APN in the window must not clear a different digit run.
|
||||
expect(flagsPhone(' parcel_id: "12-3456789.000",\n other: "999888777666",')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user