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
10 changes: 10 additions & 0 deletions src/rules/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ export const SECRET_PATH_PATTERNS = [
".kube/config",
];

/** File patterns that are still strong signals when they appear in prose. */
export const PROSE_SECRET_PATH_PATTERNS = [
".env",
"id_rsa",
"id_ed25519",
"/etc/passwd",
"/etc/shadow",
".pem",
];

/** Case-insensitive "does haystack contain any needle". */
export function containsAny(
haystack: string | undefined,
Expand Down
32 changes: 26 additions & 6 deletions src/rules/secrets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Finding, Rule } from "../types.js";
import { containsAny, SECRET_PATH_PATTERNS } from "./helpers.js";
import {
containsAny,
PROSE_SECRET_PATH_PATTERNS,
SECRET_PATH_PATTERNS,
} from "./helpers.js";

const PATH_ARG_NAMES = ["path", "file", "filename", "filepath", "dir", "directory", "location"];

Expand All @@ -12,25 +16,41 @@ export const resourceExposesSecrets: Rule = {
id: "MCP030",
title: "Resource exposes secrets or sensitive paths",
description:
"Resources should never surface credential files, private keys, or sensitive system paths.",
"Resources should never surface credential files, private keys, or sensitive system paths. URI evidence is critical; metadata prose is reported at high only for unambiguous file patterns.",
severity: "critical",
category: "secrets",
evaluate(target, ctx): Finding[] {
const findings: Finding[] = [];
for (const res of target.resources) {
const haystack = `${res.uri} ${res.name ?? ""} ${res.description ?? ""}`;
const hit = containsAny(haystack, SECRET_PATH_PATTERNS);
if (hit) {
const uriHit = containsAny(res.uri, SECRET_PATH_PATTERNS);
if (uriHit) {
findings.push(
ctx.report({
title: "Resource surfaces sensitive material",
message: `Resource "${res.uri}" references "${hit}", which commonly holds secrets or system credentials.`,
message: `Resource "${res.uri}" references "${uriHit}", which commonly holds secrets or system credentials.`,
remediation:
"Remove the resource or restrict it to non-sensitive content. Never expose credential files or system paths over MCP.",
location: res.uri,
}),
);
}

const proseHit = containsAny(
`${res.name ?? ""} ${res.description ?? ""}`,
PROSE_SECRET_PATH_PATTERNS,
);
if (proseHit) {
findings.push(
ctx.report({
title: "Resource metadata references sensitive material",
message: `Resource "${res.uri}" metadata references "${proseHit}", which is an unambiguous sensitive file pattern.`,
remediation:
"Review the resource name and description. Only a resource that actually exposes the file should be removed or restricted.",
location: res.uri,
severity: "high",
}),
);
}
}
return findings;
},
Expand Down
41 changes: 41 additions & 0 deletions test/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,47 @@ describe("MCP040 http auth", () => {
});
});

describe("MCP030 resource secrets", () => {
it("does not flag generic credentials wording in a description", () => {
const target = makeTarget({
resources: [
{
uri: "https://example.com/notes",
name: "Project notes",
description: "Project notes. Contains no credentials or secrets.",
},
],
});
const findings = audit(target).findings.filter(
(f) => f.ruleId === "MCP030",
);
expect(findings).toHaveLength(0);
});

it("reports URI evidence as critical and prose evidence as high", () => {
const target = makeTarget({
resources: [
{
uri: "file:///home/me/.env",
name: "Environment backup",
description: "Local environment backup",
},
{
uri: "https://example.com/resource",
name: "server.pem",
description: "TLS certificate material",
},
],
});
const findings = audit(target).findings.filter(
(f) => f.ruleId === "MCP030",
);
expect(findings).toHaveLength(2);
expect(findings.some((f) => f.severity === "critical")).toBe(true);
expect(findings.some((f) => f.severity === "high")).toBe(true);
});
});

describe("MCP060 tool name collision", () => {
it("detects duplicate tool names", () => {
const target = makeTarget({
Expand Down
Loading