Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/review/secrets-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -67,13 +67,17 @@ 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
* (e.g. "abcdefghijklmnop123"), or a narrow token fixture name (e.g. "installation-token"). */
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);
}
Expand Down
12 changes: 12 additions & 0 deletions test/unit/safety-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions test/unit/secrets-scan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down