Skip to content
Closed
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
12 changes: 11 additions & 1 deletion scripts/forbidden-content.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)/;
126 changes: 107 additions & 19 deletions test/unit/forbidden-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]> = {
"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"],
};

Expand All @@ -32,33 +39,50 @@ 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 ?? ""}` };
}
}

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
Expand All @@ -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<string, string> = {
"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 <NAME>_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<string, string> = {
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);
});
});