diff --git a/src/review/secrets-scan.ts b/src/review/secrets-scan.ts index 4d51b4819a..b961e34de5 100644 --- a/src/review/secrets-scan.ts +++ b/src/review/secrets-scan.ts @@ -42,7 +42,7 @@ const SECRET_PATTERNS: Array<{ name: string; re: RegExp }> = [ const GENERIC_SECRET_ASSIGNMENT_PATTERN = /((?:api[_-]?key|secret|token|password|passwd|access[_-]?key|client[_-]?secret))["']?\s*[:=]\s*["']([A-Za-z0-9+/=_-]{16,})["']/gi; -const PLACEHOLDER_VALUE_PATTERN = /placeholder|change[_-]?me|your[_-]|<[^>]*>|\bexample\b|redacted|dummy|\bsample\b|\btodo\b|\bfixme\b|\binsert\b|replace[_-]?me|\bfake\b|\bmock\b/i; +const PLACEHOLDER_VALUE_PATTERN = /placeholder|change[_-]?me|your[_-]|<[^>]*>|\bexample\b|redacted|dummy|\bsample\b|\btodo\b|\bfixme\b|\binsert\b|replace[_-]?me|\bfake\b/i; // #2553 gate review finding: a string with NO repeated characters (e.g. "abcdefghijklmnop123") has HIGH // Shannon entropy by raw character-frequency counting, but is obviously not a real secret -- entropy alone @@ -67,6 +67,9 @@ function hasLongSequentialRun(value: string): boolean { // generic token-assignment heuristic. Keep that carve-out key-aware and two-word-only: lowercase // hyphenated values assigned to password/passwd/client_secret remain plausible passphrase-style credentials. const LOWERCASE_HYPHENATED_TOKEN_FIXTURE_PATTERN = /^[a-z]+-[a-z]+$/; +// Lowercase hyphenated mock names are fixtures; mixed-case/digit-bearing values containing "mock" remain +// plausible credentials and must still be reported by the generic assignment scanner. +const LOWERCASE_HYPHENATED_MOCK_FIXTURE_PATTERN = /^(?:[a-z]+-)*mock(?:-[a-z]+)*$/; /** True for an obvious non-secret filler value: a known placeholder phrase, a string built from at most 2 * distinct characters (e.g. "xxxxxxxxxxxxxxxx", "----------------"), a long monotonic character-code run @@ -74,6 +77,7 @@ const LOWERCASE_HYPHENATED_TOKEN_FIXTURE_PATTERN = /^[a-z]+-[a-z]+$/; function isPlaceholderSecretValue(key: string, value: string): boolean { if (PLACEHOLDER_VALUE_PATTERN.test(value)) return true; if (new Set(value.toLowerCase()).size <= 2) return true; + if (LOWERCASE_HYPHENATED_MOCK_FIXTURE_PATTERN.test(value)) return true; if (key.toLowerCase() === "token" && LOWERCASE_HYPHENATED_TOKEN_FIXTURE_PATTERN.test(value)) return true; return hasLongSequentialRun(value); } diff --git a/test/unit/safety-wiring.test.ts b/test/unit/safety-wiring.test.ts index ce32a36f05..40097ed548 100644 --- a/test/unit/safety-wiring.test.ts +++ b/test/unit/safety-wiring.test.ts @@ -318,6 +318,18 @@ describe("secret-leak finding in the advisory build", () => { expect(finding?.detail).toContain("config/prod.env:1"); }); + it("blocks mixed-case mock-tokenized generic credentials", () => { + const diff = [ + "### config/prod.env (modified) +1/-0", + "@@ -0,0 +1 @@", + '+password = "prod-mock-aK9xQ2mZw7Ln4Rv8Pt3Bh6"', + ].join("\n"); + const finding = secretLeakFinding(diff); + expect(finding?.code).toBe("secret_leak"); + expect(finding?.title).toContain("generic_secret_assignment"); + expect(finding?.detail).toContain("config/prod.env:1"); + }); + it("FLAG-OFF: a concrete leaked secret STILL produces the secret_leak finding (unconditional, #audit-3.4)", async () => { const env = createTestEnv({ GITTENSORY_REVIEW_SAFETY: "false" }); const adv = advisory(); diff --git a/test/unit/secrets-scan.test.ts b/test/unit/secrets-scan.test.ts index 4405a07c72..008407ec05 100644 --- a/test/unit/secrets-scan.test.ts +++ b/test/unit/secrets-scan.test.ts @@ -167,6 +167,13 @@ describe("scanForSecrets — deterministic secret-pattern scanner", () => { expect(scanForSecrets(`fakeSecret = "${fakeSecret}"`).kinds).toContain("generic_secret_assignment"); }); + it.each([ + ["mock prefix with mixed-case suffix", 'password = "mock-aK9xQ2mZw7Ln4Rv8Pt3Bh6"'], + ["embedded mock with mixed-case suffix", 'secret = "prod-mock-aK9xQ2mZw7Ln4Rv8Pt3Bh6"'], + ])("flags mock-tokenized generic credentials unless they are lowercase fixtures: %s", (_name, snippet) => { + expect(scanForSecrets(snippet).kinds).toContain("generic_secret_assignment"); + }); + it("a single lowercase word with no hyphen is unaffected by the token-fixture exclusion (#3041)", () => { // 20 lowercase letters, no repeats and no sequential run, so it isn't already caught by the entropy/ // placeholder checks either -- proves the token-fixture exclusion specifically requires one two-word