diff --git a/src/review/safety.ts b/src/review/safety.ts index e1f6e567ab..4be608fe91 100644 --- a/src/review/safety.ts +++ b/src/review/safety.ts @@ -34,6 +34,8 @@ const HARD_SECRET_KINDS = new Set([ "stripe_secret_key", "sendgrid_key", "huggingface_token", + "voyage_api_key", + "firecrawl_api_key", "jwt", "generic_secret_assignment", ]); diff --git a/src/review/secrets-scan.ts b/src/review/secrets-scan.ts index b961e34de5..442051a23e 100644 --- a/src/review/secrets-scan.ts +++ b/src/review/secrets-scan.ts @@ -28,6 +28,10 @@ const SECRET_PATTERNS: Array<{ name: string; re: RegExp }> = [ { name: "sendgrid_key", re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])/ }, // Hugging Face user access token: `hf_` + 34 base62 chars. { name: "huggingface_token", re: /\bhf_[A-Za-z0-9]{34}\b/ }, + // Voyage AI API key: `pa-` (platform) or `al-` (MongoDB Atlas) + base62 body. + { name: "voyage_api_key", re: /\b(?:pa|al)-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/ }, + // Firecrawl API key: `fc-` + base62 body (alnum only; reject hyphen-continued identifiers). + { name: "firecrawl_api_key", re: /\bfc-[A-Za-z0-9]{16,}(?![A-Za-z0-9_-])/ }, { name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ }, { name: "seed_or_mnemonic", re: /\b(?:seed phrase|mnemonic)\b/i }, { name: "bittensor_key", re: /\b(?:hot|cold)key\b\s*[:=]/i }, diff --git a/test/unit/safety-wiring.test.ts b/test/unit/safety-wiring.test.ts index ab14e510a3..55971bae14 100644 --- a/test/unit/safety-wiring.test.ts +++ b/test/unit/safety-wiring.test.ts @@ -475,6 +475,8 @@ describe("gate treats secret_leak as a hard blocker", () => { `### src/config.ts (modified) +1/-0\n@@\n+const jwt = "${"eyJhbGciOiJIUzI1NiJ9" + "." + "eyJzdWIiOiIxMjM0NTY3ODkwIn0" + "." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}";`, ], ["generic_secret_assignment", `### src/config.ts (modified) +1/-0\n@@\n+secret = "${"sk_live_" + "aK9xQ2mZw7Ln4Rv8Pt3Bh6"}"`], + ["voyage_api_key", `### src/config.ts (modified) +1/-0\n@@\n+const voyage = "${"pa-" + "aK9xQ2mZw7Ln4Rv8Pt3B"}";`], + ["firecrawl_api_key", `### src/config.ts (modified) +1/-0\n@@\n+const firecrawl = "${"fc-" + "aK9xQ2mZw7Ln4Rv8"}";`], ])("hard-blocks a %s finding", (kind, diff) => { const finding = secretLeakFinding(diff); expect(finding?.code).toBe("secret_leak"); diff --git a/test/unit/secrets-scan.test.ts b/test/unit/secrets-scan.test.ts index 008407ec05..52d166fdbb 100644 --- a/test/unit/secrets-scan.test.ts +++ b/test/unit/secrets-scan.test.ts @@ -97,6 +97,26 @@ describe("scanForSecrets — deterministic secret-pattern scanner", () => { expect(scanForSecrets(fakeToken).kinds).toContain("huggingface_token"); }); + it("flags Voyage AI API keys", () => { + expect(scanForSecrets("pa-" + "aK9xQ2mZw7Ln4Rv8Pt3B").kinds).toContain("voyage_api_key"); + expect(scanForSecrets("al-" + "mN4pL8sT2vW6xY0A1qZ5").kinds).toContain("voyage_api_key"); + }); + + it("does not flag Voyage AI-shaped values below the length floor or with identifier continuation", () => { + expect(scanForSecrets("pa-" + "a".repeat(19)).kinds).not.toContain("voyage_api_key"); + expect(scanForSecrets("pa-" + "a".repeat(20) + "-suffix").kinds).not.toContain("voyage_api_key"); + expect(scanForSecrets("al-" + "b".repeat(20) + "_suffix").kinds).not.toContain("voyage_api_key"); + }); + + it("flags a Firecrawl API key", () => { + expect(scanForSecrets("fc-" + "aK9xQ2mZw7Ln4Rv8").kinds).toContain("firecrawl_api_key"); + }); + + it("does not flag Firecrawl-shaped values below the length floor or with identifier continuation", () => { + expect(scanForSecrets("fc-" + "c".repeat(15)).kinds).not.toContain("firecrawl_api_key"); + expect(scanForSecrets("fc-" + "c".repeat(16) + "-suffix").kinds).not.toContain("firecrawl_api_key"); + }); + it("flags a JWT", () => { const fakeJwt = "eyJhbGciOiJIUzI1NiJ9" + "." + "eyJzdWIiOiIxMjM0NTY3ODkwIn0" + "." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; expect(scanForSecrets(fakeJwt).kinds).toContain("jwt");