diff --git a/scripts/forbidden-content.mjs b/scripts/forbidden-content.mjs index 039a6d2d9..d01e54045 100644 --- a/scripts/forbidden-content.mjs +++ b/scripts/forbidden-content.mjs @@ -4,5 +4,15 @@ // the AMS MCP contract test (test/unit/miner-mcp-contract.test.ts) reuses the SAME pattern to assert no MCP tool // response ever leaks one — importing it here rather than hand-duplicating the regex keeps the two byte-for-byte in // sync instead of relying on manual vigilance. +// +// The concrete-format branches below are hand-copied — byte-for-byte — from the `re` bodies of +// src/review/secret-patterns.ts's SECRET_PATTERNS (the subset in its HARD_SECRET_KINDS, documented there as +// near-zero-false-positive and safe for an unconditional hard block). They are NOT imported from that file: its +// consumers here (check-miner-package.mjs / check-mcp-package.mjs) run under plain `node` — `node +// scripts/check-*.mjs` in ci.yml and publish-miner.yml — not `tsx`, so this .mjs cannot import a .ts source at +// runtime (check-engine-parity.ts can only because it is itself run via `tsx`). If secret-patterns.ts changes a +// shared body, update it here too. Deliberately excluded from that set: `jwt` (out of scope for this detector), +// and `seed_or_mnemonic` / `bittensor_key` (documented there as weak, false-positive-prone heuristics kept out of +// HARD_SECRET_KINDS — a `hotkey =` line or the word "mnemonic" in ordinary Bittensor docs is not a leaked secret). export const FORBIDDEN_CONTENT = - /(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+|gts_[0-9a-f]{64}|[A-Z0-9_]*(TOKEN|SECRET|PRIVATE_KEY)=)/; + /(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+|gts_[0-9a-f]{64}|[A-Z0-9_]*(TOKEN|SECRET|PRIVATE_KEY)=|\bAKIA[0-9A-Z]{16}\b|\bxox[baprs]-[A-Za-z0-9-]{10,}\b|\bAIza[0-9A-Za-z_-]{35}\b|\bglpat-[0-9A-Za-z_-]{20}(?![0-9A-Za-z_-])|\bnpm_[A-Za-z0-9]{36}\b|\b(?:sk|rk)_live_[0-9A-Za-z]{24,}\b|\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])|\bhf_[A-Za-z0-9]{34}\b|\b(?:pa|al)-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])|\bfc-[A-Za-z0-9]{16,}(?![A-Za-z0-9_-])|\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}T3BlbkFJ[A-Za-z0-9_-]{20,}\b|\bsk-ant-api03-[A-Za-z0-9_-]{93}AA\b)/; diff --git a/test/unit/forbidden-content.test.ts b/test/unit/forbidden-content.test.ts index 853de1594..b99e844a1 100644 --- a/test/unit/forbidden-content.test.ts +++ b/test/unit/forbidden-content.test.ts @@ -8,12 +8,19 @@ import { FORBIDDEN_CONTENT } from "../../scripts/forbidden-content.mjs"; // drift apart unnoticed (#6290). These assertions pin both halves of the claim -- the structural one (each // checker imports the constant rather than owning a copy) and the behavioral one (each checker actually rejects // what the shared detector matches). -const PACKAGE_CHECKERS = ["scripts/check-miner-package.mjs", "scripts/check-mcp-package.mjs"]; +const PACKAGE_CHECKERS = [ + "scripts/check-miner-package.mjs", + "scripts/check-mcp-package.mjs", +]; // A minimal file list that passes each checker's path/allowlist/required-file guards, so the run reaches the // shared secret-content read. Mirrors the file lists each checker's own "rejects secret-like content" test uses. const REACHABLE_FILES: Record = { - "scripts/check-miner-package.mjs": ["package.json", "bin/loopover-miner.js", "lib/cli.js"], + "scripts/check-miner-package.mjs": [ + "package.json", + "bin/loopover-miner.js", + "lib/cli.js", + ], "scripts/check-mcp-package.mjs": ["package.json", "bin/loopover-mcp.js"], }; @@ -32,11 +39,16 @@ function runChecker( const isMiner = checker.includes("miner"); const env = { ...process.env, - [isMiner ? "CHECK_MINER_PACK_TEST_FILES" : "CHECK_MCP_PACK_TEST_FILES"]: JSON.stringify(files), - [isMiner ? "CHECK_MINER_PACK_TEST_CONTENT" : "CHECK_MCP_PACK_TEST_CONTENT"]: content, + [isMiner ? "CHECK_MINER_PACK_TEST_FILES" : "CHECK_MCP_PACK_TEST_FILES"]: + JSON.stringify(files), + [isMiner ? "CHECK_MINER_PACK_TEST_CONTENT" : "CHECK_MCP_PACK_TEST_CONTENT"]: + content, }; try { - return { status: 0, out: execFileSync(process.execPath, [checker], { encoding: "utf8", env }) }; + return { + status: 0, + out: execFileSync(process.execPath, [checker], { encoding: "utf8", env }), + }; } catch (err) { const e = err as { status?: number; stdout?: string; stderr?: string }; return { status: e.status ?? 1, out: `${e.stdout ?? ""}${e.stderr ?? ""}` }; @@ -44,21 +56,33 @@ function runChecker( } describe("FORBIDDEN_CONTENT is the single source of truth (#6290)", () => { - it.each(PACKAGE_CHECKERS)("%s imports the shared constant instead of re-declaring it", (checker) => { - const source = readFileSync(checker, "utf8"); - expect(source).toContain('import { FORBIDDEN_CONTENT } from "./forbidden-content.mjs";'); - expect(source).toContain("FORBIDDEN_CONTENT.test("); - // The drift this guards against: a checker owning its own copy of the detector. - expect(source).not.toMatch(/const\s+FORBIDDEN_CONTENT\s*=/); - }); + it.each(PACKAGE_CHECKERS)( + "%s imports the shared constant instead of re-declaring it", + (checker) => { + const source = readFileSync(checker, "utf8"); + expect(source).toContain( + 'import { FORBIDDEN_CONTENT } from "./forbidden-content.mjs";', + ); + expect(source).toContain("FORBIDDEN_CONTENT.test("); + // The drift this guards against: a checker owning its own copy of the detector. + expect(source).not.toMatch(/const\s+FORBIDDEN_CONTENT\s*=/); + }, + ); - it.each(PACKAGE_CHECKERS)("%s rejects content the shared detector matches", (checker) => { - // Sanity-check the probe really is what the shared detector flags, then that the checker enforces it. - expect(FORBIDDEN_CONTENT.test(SECRET_SHAPED_PROBE)).toBe(true); - const result = runChecker(checker, REACHABLE_FILES[checker]!, SECRET_SHAPED_PROBE); - expect(result.status).toBe(1); - expect(result.out).toContain("Secret-like content found in"); - }); + it.each(PACKAGE_CHECKERS)( + "%s rejects content the shared detector matches", + (checker) => { + // Sanity-check the probe really is what the shared detector flags, then that the checker enforces it. + expect(FORBIDDEN_CONTENT.test(SECRET_SHAPED_PROBE)).toBe(true); + const result = runChecker( + checker, + REACHABLE_FILES[checker]!, + SECRET_SHAPED_PROBE, + ); + expect(result.status).toBe(1); + expect(result.out).toContain("Secret-like content found in"); + }, + ); // Scoped to the MCP checker: the miner one layers required-file / lib-artifact / docs guards on top of a // minimal file list, so a clean-content pass there would be asserting its allowlist rather than the shared @@ -81,3 +105,67 @@ describe("FORBIDDEN_CONTENT is the single source of truth (#6290)", () => { expect(FORBIDDEN_CONTENT.test(SECRET_SHAPED_PROBE)).toBe(true); }); }); + +// #7433: FORBIDDEN_CONTENT was widened beyond its original 5 shapes to also match the 12 concrete secret formats +// that src/review/secret-patterns.ts's HARD_SECRET_KINDS already hard-block, so a packed miner/mcp file (or an +// MCP tool response) embedding an AWS/Slack/OpenAI/Anthropic/etc. key is caught here too. Every probe is assembled +// from fragments so this file never itself contains a contiguous credential-shaped literal -- the same convention +// secret-patterns.test.ts / check-*-package.test.ts already use so the repo's own secret scanners don't flag it. +describe("FORBIDDEN_CONTENT matches the concrete secret formats it hard-blocks (#7433)", () => { + const MATCHES: Record = { + "AWS access key": "AKIA" + "IOSFODNN7EXAMPLE", + "Slack token": "xox" + "b-" + "1234567890-AbCdEfGhIj", + "Google API key": "AIza" + "B".repeat(35), + "GitLab token": "glp" + "at-" + "abcDEF1234567890ghij", + "npm token": "npm" + "_" + "a".repeat(36), + "Stripe live key": "sk" + "_live_" + "a".repeat(24), + "SendGrid key": "SG." + "a".repeat(22) + "." + "b".repeat(43), + "Hugging Face token": "hf" + "_" + "a".repeat(34), + "Voyage key": "pa-" + "abcdefghij1234567890AB", + "Firecrawl key": "fc-" + "abcdef1234567890", + "OpenAI key": + "sk-" + + "proj-" + + "abcdefghij1234567890" + + "T3Blbk" + + "FJ" + + "abcdefghij1234567890", + "Anthropic key": "sk-" + "ant-api03-" + "a".repeat(93) + "AA", + // The 4 shapes it already covered, re-pinned so the widening can't silently regress one. + "PEM private-key header": "-----BEGIN RSA PRIVATE KEY-----", + "GitHub PAT": "github" + "_pat_" + "A".repeat(22), + "gh*_ token": "gh" + "p_" + "A".repeat(30), + "generic _SECRET= assignment": [ + "PROBE", + "_", + "SECRET", + "=", + "value", + ].join(""), + }; + it.each(Object.entries(MATCHES))("flags a %s", (_name, probe) => { + expect(FORBIDDEN_CONTENT.test(probe)).toBe(true); + }); + + // Deliberately kept OUT of the hard-block set (see forbidden-content.mjs's own comment): `jwt` is out of scope + // for this packaged-secret detector, and `seed_or_mnemonic` / `bittensor_key` are documented in + // secret-patterns.ts as weak, false-positive-prone heuristics -- a JWT-shaped string, a doc sentence mentioning + // a mnemonic/seed phrase, or a Bittensor hotkey/coldkey line must NOT be flagged as a leaked packaged secret. + const NON_MATCHES: Record = { + JWT: + "ey" + + "JhbGciOiJIUzI1NiJ9" + + "." + + "eyJzdWIiOiIxMjM0In0" + + "." + + "abcDEF1234567890", + "mnemonic / seed-phrase prose": + "restore it from your mnemonic or seed phrase if lost", + "Bittensor hotkey/coldkey line": + "hot" + "key = default\ncold" + "key: main", + "ordinary text": "just a normal sentence, nothing secret here at all", + }; + it.each(Object.entries(NON_MATCHES))("does not flag %s", (_name, probe) => { + expect(FORBIDDEN_CONTENT.test(probe)).toBe(false); + }); +});