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
14 changes: 13 additions & 1 deletion scripts/forbidden-content.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,17 @@
// 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.
//
// #7433: extended with 12 additional concrete secret formats (aws_access_key, slack_token, google_api_key,
// gitlab_token, npm_token, stripe_secret_key, sendgrid_key, huggingface_token, voyage_api_key, firecrawl_api_key,
// openai_api_key, anthropic_api_key), hand-copied byte-for-byte from src/review/secret-patterns.ts's
// SECRET_PATTERNS rather than imported: check-miner-package.mjs/check-mcp-package.mjs both run via plain
// `node scripts/*.mjs` (package.json's test:miner-pack/test:mcp-pack), and plain Node cannot resolve a `.ts`
// import ("Unknown file extension \".ts\"" against this repo's Node 22 runtime) without a TS loader neither
// script registers -- unlike scripts/check-engine-parity.ts, which runs via `tsx`. jwt, seed_or_mnemonic, and
// bittensor_key were deliberately left out: jwt is out of scope for this issue, and seed_or_mnemonic/
// bittensor_key are documented in secret-patterns.ts as weak, false-positive-prone heuristics (a
// `coldkey:`/`hotkey =` line or the word "mnemonic" in ordinary Bittensor docs is not a leaked credential)
// excluded from HARD_SECRET_KINDS there for the same reason.
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)/;
43 changes: 43 additions & 0 deletions test/unit/forbidden-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,46 @@ describe("FORBIDDEN_CONTENT is the single source of truth (#6290)", () => {
expect(FORBIDDEN_CONTENT.test(SECRET_SHAPED_PROBE)).toBe(true);
});
});

// #7433: FORBIDDEN_CONTENT only matched 4 shapes (private-key block, github_pat_, gh[pousr]_, gts_, and a
// generic TOKEN/SECRET/PRIVATE_KEY= assignment) even though this same repo already ships a materially larger,
// individually-verified-precise set of concrete secret-format patterns in src/review/secret-patterns.ts's
// SECRET_PATTERNS. Each fixture below is assembled from fragments (never a contiguous credential-shaped literal
// in this file's own source) and uses the same fake bodies the repo's own secrets-scan.test.ts and
// content-lane-security-scan.test.ts already use for these exact formats.
describe("FORBIDDEN_CONTENT matches the repo's other known-precise secret formats (#7433)", () => {
it.each([
["aws_access_key", "AKIA" + "ABCDEFGHIJKLMNOP"],
["slack_token", "xoxb-" + "123456789012-ABCDEFabcdef"],
["google_api_key", "AIza" + "SyABCDEFGHIJKLMNOPQRSTUVWXYZ0123456"],
["gitlab_token", "glpat-" + "aBcDeFgHiJkLmNoPqRsT"],
["npm_token", "npm_" + "a".repeat(36)],
["stripe_secret_key", "sk_live_" + "a".repeat(24)],
["sendgrid_key", "SG." + "a".repeat(22) + "." + "b".repeat(43)],
["huggingface_token", "hf_" + "a".repeat(34)],
["voyage_api_key", "pa-" + "aK9xQ2mZw7Ln4Rv8Pt3B"],
["firecrawl_api_key", "fc-" + "aK9xQ2mZw7Ln4Rv8"],
["openai_api_key", "sk-" + "a".repeat(20) + "T3BlbkFJ" + "b".repeat(20)],
["anthropic_api_key", "sk-ant-api03-" + "a".repeat(93) + "AA"],
])("matches a %s-shaped value", (_kind, fixture) => {
expect(FORBIDDEN_CONTENT.test(fixture)).toBe(true);
});

it("still matches the 4 pre-existing formats unchanged", () => {
expect(FORBIDDEN_CONTENT.test("-----BEGIN RSA PRIVATE KEY-----")).toBe(true);
expect(FORBIDDEN_CONTENT.test("github_pat_" + "a".repeat(20))).toBe(true);
expect(FORBIDDEN_CONTENT.test("ghp_" + "a".repeat(20))).toBe(true);
expect(FORBIDDEN_CONTENT.test("gts_" + "0123456789abcdef".repeat(4))).toBe(true);
expect(FORBIDDEN_CONTENT.test(SECRET_SHAPED_PROBE)).toBe(true);
});

// jwt/seed_or_mnemonic/bittensor_key were deliberately left out of this widening (see the code comment on
// FORBIDDEN_CONTENT) -- pinned here so a future edit can't silently reintroduce them without this test
// flagging the scope change.
it("does NOT match jwt/seed_or_mnemonic/bittensor_key shapes (deliberately out of scope)", () => {
const jwtShaped = "eyJ" + "a".repeat(10) + "." + "b".repeat(10) + "." + "c".repeat(10);
expect(FORBIDDEN_CONTENT.test(jwtShaped)).toBe(false);
expect(FORBIDDEN_CONTENT.test("our seed phrase backup process")).toBe(false);
expect(FORBIDDEN_CONTENT.test("hotkey = ss58addresshere")).toBe(false);
});
});
5 changes: 5 additions & 0 deletions test/unit/miner-mcp-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ describe("contract assertions catch violations (canary)", () => {
expect(() => assertNoSecretShapedValue(withText(JSON.stringify({ token: PLANTED_SECRET })))).toThrow();
});

it("assertNoSecretShapedValue also throws on a newly-widened shape (#7433: aws_access_key)", () => {
const awsShapedKey = "AKIA" + "ABCDEFGHIJKLMNOP";
expect(() => assertNoSecretShapedValue(withText(JSON.stringify({ token: awsShapedKey })))).toThrow();
});

it("assertNoExcludedColumn throws when an excluded column is present", () => {
expect(() => assertNoExcludedColumn(withText(JSON.stringify({ payload_json: "x" })), ["payload_json"])).toThrow();
});
Expand Down