From 6bf7417fcfcd477657da91da756bfe02217602f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSebastian?= <64795732+slegarraga@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:33:39 -0400 Subject: [PATCH] fix: avoid mcp030 critical from prose --- src/rules/helpers.ts | 10 ++++++++++ src/rules/secrets.ts | 32 ++++++++++++++++++++++++++------ test/rules.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/rules/helpers.ts b/src/rules/helpers.ts index ccb63a9..f1334e8 100644 --- a/src/rules/helpers.ts +++ b/src/rules/helpers.ts @@ -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, diff --git a/src/rules/secrets.ts b/src/rules/secrets.ts index ed7b2f4..8731d31 100644 --- a/src/rules/secrets.ts +++ b/src/rules/secrets.ts @@ -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"]; @@ -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; }, diff --git a/test/rules.test.ts b/test/rules.test.ts index 7f1cde8..c79c572 100644 --- a/test/rules.test.ts +++ b/test/rules.test.ts @@ -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({