mirror of https://github.com/garrytan/gstack.git
Merge 07ac6809d6 into 2be6c06ba8
This commit is contained in:
commit
35ab16c29f
|
|
@ -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) ─────────────────
|
// ── Placeholder suppression (per-matched-span, NOT per-line) ─────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -527,11 +579,13 @@ export const PATTERNS: RedactPattern[] = [
|
||||||
regex: /(?<![\w.])(\+?[1-9]\d{0,2}[ \-.]?\(?\d{2,4}\)?[ \-.]?\d{3,4}[ \-.]?\d{3,4})(?![\w.])/,
|
regex: /(?<![\w.])(\+?[1-9]\d{0,2}[ \-.]?\(?\d{2,4}\)?[ \-.]?\d{3,4}[ \-.]?\d{3,4})(?![\w.])/,
|
||||||
autoRedactable: true,
|
autoRedactable: true,
|
||||||
redactToken: "<REDACTED-PHONE>",
|
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) =>
|
validate: (span, match) =>
|
||||||
!insideUuid(match) &&
|
!insideUuid(match) &&
|
||||||
span.replace(/\D/g, "").length >= 10 &&
|
span.replace(/\D/g, "").length >= 10 &&
|
||||||
!looksLikeCompactTimestamp(span),
|
!looksLikeCompactTimestamp(span) &&
|
||||||
|
!looksLikeParcelId(span, match),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pii.ssn",
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue