diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index 90d91e071..330db6130 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -475,23 +475,29 @@ export function isDotenvFilename(match: RegExpExecArray): boolean { return spanStart > 0 && input[spanStart - 1] === "."; } -/** File extensions that are not top-level domains (checked against the IANA - * root zone list, 2026-10-28). Extensions that are also TLDs (`.md`, `.py`, - * `.sh`, `.rs`, `.so`, `.zip`, `.mov`, `.map`) are left out on purpose. */ -const NON_TLD_EXTENSION = /\.(?:json|jsonc|jsonl|ndjson|ya?ml|toml|lock|txt|csv|tsv|log|ini|cfg|conf|xml|html?|[mc]?js|jsx|tsx?|tgz|gz|tar|bz2|xz|zst|whl|jar|gem|nupkg|deb|rpm|wasm|svg|png|jpe?g|pdf|patch|diff)$/i; +/** A trailing run of file extensions that are not top-level domains (checked + * against the IANA root zone list, 2026-10-28). Extensions that are also TLDs + * (`.md`, `.py`, `.sh`, `.rs`, `.so`, `.zip`, `.mov`, `.map`) are left out on + * purpose. */ +const NON_TLD_EXTENSIONS = /(?:\.(?:json|jsonc|jsonl|ndjson|ya?ml|toml|lock|txt|csv|tsv|log|ini|cfg|conf|xml|html?|[mc]?js|jsx|tsx?|tgz|gz|tar|bz2|xz|zst|whl|jar|gem|nupkg|deb|rpm|wasm|svg|png|jpe?g|pdf|patch|diff))+$/i; /** * True when a pii.email span is a filename with an `@` in it rather than an * address: `terminal-bench@2.0.json`, `lodash@4.17.21.tgz`, - * `eslint-config@next.yaml`. The package@version convention puts a version - * after the `@`, so either sign decides: the "domain" ends in a file extension - * no TLD uses, or it is a dotted version (two or more all-digit labels) with - * one label after it. `user@163.com` (one numeric label) and an address at a - * TLD that doubles as an extension (`dev@acme.sh`, `ops@acme.md`) still report. + * `eslint-config@next.yaml`. The package@version convention puts a version or + * tag after the `@`, so either sign decides: the "domain" ends in file + * extensions no TLD uses and what precedes them cannot be a domain (one dotless + * label or a dotted version), or it is a dotted version (two or more all-digit + * labels) followed by an extension that doubles as a TLD (`a@1.2.md`). A file + * named after an address (`jane@acme.com.json`), `user@163.com`, + * `alice@1.2.com` and an address at a TLD that doubles as an extension + * (`dev@acme.sh`, `ops@acme.md`) still report. */ export function looksLikeAtFilename(span: string): boolean { const domain = span.slice(span.lastIndexOf("@") + 1); - return NON_TLD_EXTENSION.test(domain) || /^\d+(?:\.\d+)+\.[A-Za-z]{2,}$/.test(domain); + const stem = domain.replace(NON_TLD_EXTENSIONS, ""); + if (stem !== domain) return !/\.[A-Za-z]{2,}$/.test(stem); + return /^\d+(?:\.\d+)+\.(?:md|py|sh|rs|so|zip|mov|map)$/i.test(domain); } /** Extensions that mark a `.local.` span as a file. Closed on diff --git a/test/redact-constant-and-at-filename.test.ts b/test/redact-constant-and-at-filename.test.ts index 0658a0043..794e6c778 100644 --- a/test/redact-constant-and-at-filename.test.ts +++ b/test/redact-constant-and-at-filename.test.ts @@ -73,9 +73,9 @@ describe("pii.email and package@version filenames", () => { expect(looksLikeAtFilename("terminal-bench@2.0.json")).toBe(true); }); - // Value: protects=real addresses still report, including a numeric domain label (163.com) and TLDs that double as file extensions (.sh, .md, .py, - // .zip); fails_when=the guard treats any digit label or any extension-like TLD as a filename; why_new=pins the narrowness of the new guard; - // seam=none (scan); no_claim=every IANA TLD + // Value: protects=real addresses still report, including numeric domain labels (163.com, 1.2.com), TLDs that double as file extensions (.sh, .md, + // .py, .zip) and a file named after an address; fails_when=the guard treats any digit label, any extension-like TLD or any trailing extension as a + // filename; why_new=pins the narrowness of the new guard; seam=none (scan); no_claim=every IANA TLD test("real addresses still report, numeric domains and extension-shaped TLDs included", () => { for (const s of [ "mail alex@acme-corp.com", @@ -85,6 +85,11 @@ describe("pii.email and package@version filenames", () => { "py team@acme.py", "files sales@acme.zip", "contact jo@mail.acme.io", + "reach alice@1.2.com", + "export jane.doe@acme-corp.com.json", + "log at ops-team@acme.io.log", + "uploaded jdoe@acme-corp.com.PDF", + "archive li.wei@163.com.tar.gz", ]) { expect([s, flags("pii.email", s)]).toEqual([s, true]); }