diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index 7007d49ef..b496bb5e3 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -320,6 +320,28 @@ export function isNumericMetadataValue(span: string, match: RegExpExecArray): bo return NUMERIC_METADATA_KEY.test(input.slice(Math.max(lineStart, start - 80), start)); } +/** + * A four-part version (MAJOR.MINOR.PATCH.BUILD: .NET assembly versions, + * gstack's own VERSION) is byte-for-byte a dotted quad, and `1.128.1.0` is a + * public address, so `"version": "1.128.1.0"` raised pii.ip_public on every + * release (#2784). The value cannot decide it, so only the declaration + * immediately before it on the same line can: a `version` key or word + * (`"version": "`, `app_version = `, ``, `AssemblyVersion("`, + * `version `), or a Keep a Changelog heading `## [1.2.3.4]`. The same digits + * after `host:`, `server = ` or in prose that merely mentions a version still + * report, and the pre-push hook's VERSION-file rule (#2856) is unchanged. + */ +const VERSION_DECLARATION_BEFORE = + /(?:\b[Vv]ersion|\bVERSION|[a-z0-9_]Version|[_-][Vv]ersion|_VERSION)["'\]]?(?:[ \t]*[:=(>][ \t]*|[ \t]+)["']?$/; +const CHANGELOG_HEADING_BEFORE = /^#{1,6}[ \t]+\[$/; +export function isDeclaredVersion(match: RegExpExecArray): boolean { + const input = match.input ?? ""; + const { start, end } = spanBounds(match); + const lineStart = input.lastIndexOf("\n", start - 1) + 1; + if (VERSION_DECLARATION_BEFORE.test(input.slice(Math.max(lineStart, start - 48), start))) return true; + return CHANGELOG_HEADING_BEFORE.test(input.slice(lineStart, start)) && input[end] === "]"; +} + // ── Placeholder suppression (per-matched-span, NOT per-line) ───────────────── /** @@ -898,7 +920,7 @@ export const PATTERNS: RedactPattern[] = [ category: "pii", description: "Public IPv4 address", regex: /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/, - validate: (span) => isPublicIPv4(span), + validate: (span, match) => isPublicIPv4(span) && !isDeclaredVersion(match), }, { id: "pii.wallet", diff --git a/test/redact-context-exemptions.test.ts b/test/redact-context-exemptions.test.ts index 895d259f3..874a08279 100644 --- a/test/redact-context-exemptions.test.ts +++ b/test/redact-context-exemptions.test.ts @@ -130,3 +130,33 @@ describe("internal.hostname: per-machine config filenames are not hosts (#2962, ], ); }); + +describe("pii.ip_public: a four-part version needs a version declaration (#2784)", () => { + table( + "pii.ip_public", + "MEDIUM", + [ + ' "version": "1.128.1.0",', + "app_version = 1.128.1.0", + "schema-version: 1.128.1.0", + "APP_VERSION=1.128.1.0", + "Released version 1.128.1.0 today", + "## [1.128.1.0] - 2026-09-02", + "1.128.1.0", + '[assembly: AssemblyVersion("1.0.0.0")]', + "released v1.128.1.0 today", + ], + [ + "1.128.1.0", + "host: 1.128.1.0", + "server = 1.128.1.0", + '"upstream": "1.128.1.0"', + "resolver = 8.8.8.8", + "version: 1.2.3.4 via 8.8.8.8", + "the version endpoint moved to 1.128.1.0", + "conversion 1.128.1.0", + "## 1.128.1.0 is our egress", + "## [1.128.1.0 mirror]", + ], + ); +});