@srhot/mcp-guard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +22 -0
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/SECURITY.md +11 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +73 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/policy/loadPolicy.d.ts +3 -0
- package/dist/policy/loadPolicy.js +67 -0
- package/dist/policy/loadPolicy.js.map +1 -0
- package/dist/policy/policyEngine.d.ts +3 -0
- package/dist/policy/policyEngine.js +30 -0
- package/dist/policy/policyEngine.js.map +1 -0
- package/dist/reporters/consoleReporter.d.ts +2 -0
- package/dist/reporters/consoleReporter.js +23 -0
- package/dist/reporters/consoleReporter.js.map +1 -0
- package/dist/reporters/jsonReporter.d.ts +2 -0
- package/dist/reporters/jsonReporter.js +4 -0
- package/dist/reporters/jsonReporter.js.map +1 -0
- package/dist/reporters/markdownReporter.d.ts +2 -0
- package/dist/reporters/markdownReporter.js +33 -0
- package/dist/reporters/markdownReporter.js.map +1 -0
- package/dist/rules/MCP01_SECRET_EXPOSURE.d.ts +2 -0
- package/dist/rules/MCP01_SECRET_EXPOSURE.js +48 -0
- package/dist/rules/MCP01_SECRET_EXPOSURE.js.map +1 -0
- package/dist/rules/MCP03_TOOL_POISONING.d.ts +2 -0
- package/dist/rules/MCP03_TOOL_POISONING.js +37 -0
- package/dist/rules/MCP03_TOOL_POISONING.js.map +1 -0
- package/dist/rules/MCP05_COMMAND_EXECUTION.d.ts +2 -0
- package/dist/rules/MCP05_COMMAND_EXECUTION.js +39 -0
- package/dist/rules/MCP05_COMMAND_EXECUTION.js.map +1 -0
- package/dist/rules/MCP07_INSUFFICIENT_AUTH.d.ts +2 -0
- package/dist/rules/MCP07_INSUFFICIENT_AUTH.js +63 -0
- package/dist/rules/MCP07_INSUFFICIENT_AUTH.js.map +1 -0
- package/dist/rules/MCP08_AUDIT_GAP.d.ts +2 -0
- package/dist/rules/MCP08_AUDIT_GAP.js +30 -0
- package/dist/rules/MCP08_AUDIT_GAP.js.map +1 -0
- package/dist/rules/MCP09_SHADOW_SERVER.d.ts +2 -0
- package/dist/rules/MCP09_SHADOW_SERVER.js +64 -0
- package/dist/rules/MCP09_SHADOW_SERVER.js.map +1 -0
- package/dist/rules/MCP10_CONTEXT_OVERSHARING.d.ts +2 -0
- package/dist/rules/MCP10_CONTEXT_OVERSHARING.js +39 -0
- package/dist/rules/MCP10_CONTEXT_OVERSHARING.js.map +1 -0
- package/dist/rules/index.d.ts +10 -0
- package/dist/rules/index.js +18 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/scanner/fileLoader.d.ts +3 -0
- package/dist/scanner/fileLoader.js +48 -0
- package/dist/scanner/fileLoader.js.map +1 -0
- package/dist/scanner/scanTarget.d.ts +2 -0
- package/dist/scanner/scanTarget.js +21 -0
- package/dist/scanner/scanTarget.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.js +8 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/severity.d.ts +4 -0
- package/dist/utils/severity.js +20 -0
- package/dist/utils/severity.js.map +1 -0
- package/docs/architecture.md +11 -0
- package/docs/release.md +42 -0
- package/docs/roadmap.md +34 -0
- package/docs/rules.md +21 -0
- package/examples/bad-mcp-server/.env.example +4 -0
- package/examples/bad-mcp-server/mcp.json +11 -0
- package/examples/bad-mcp-server/server.ts +13 -0
- package/examples/bad-mcp-server/tools.json +17 -0
- package/examples/safe-mcp-server/mcp.json +13 -0
- package/examples/safe-mcp-server/server.ts +9 -0
- package/examples/safe-mcp-server/tools.json +17 -0
- package/mcpguard.config.yml +29 -0
- package/package.json +80 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { lineNumberForIndex } from "../scanner/fileLoader.js";
|
|
2
|
+
const poisoningPatterns = [
|
|
3
|
+
/ignore\s+(?:all\s+)?previous\s+instructions/gi,
|
|
4
|
+
/reveal\s+secrets/gi,
|
|
5
|
+
/send\s+private\s+files/gi,
|
|
6
|
+
/exfiltrate/gi,
|
|
7
|
+
/hidden\s+instruction/gi,
|
|
8
|
+
/system\s+prompt/gi,
|
|
9
|
+
/developer\s+message/gi,
|
|
10
|
+
/bypass\s+policy/gi,
|
|
11
|
+
/do\s+not\s+tell\s+the\s+user/gi
|
|
12
|
+
];
|
|
13
|
+
export const MCP03_TOOL_POISONING = {
|
|
14
|
+
id: "MCP03_TOOL_POISONING",
|
|
15
|
+
scan(context) {
|
|
16
|
+
const findings = [];
|
|
17
|
+
for (const file of context.files) {
|
|
18
|
+
for (const regex of poisoningPatterns) {
|
|
19
|
+
for (const match of file.content.matchAll(regex)) {
|
|
20
|
+
findings.push({
|
|
21
|
+
id: "MCP03_TOOL_POISONING",
|
|
22
|
+
title: "Suspicious tool instruction",
|
|
23
|
+
severity: "HIGH",
|
|
24
|
+
category: "Tool Integrity",
|
|
25
|
+
message: "Tool metadata or source text contains instruction-like content that could poison an agent.",
|
|
26
|
+
filePath: file.filePath,
|
|
27
|
+
lineNumber: lineNumberForIndex(file.content, match.index ?? 0),
|
|
28
|
+
evidence: match[0],
|
|
29
|
+
recommendation: "Remove hidden instructions from tool descriptions and keep tool metadata limited to user-visible behavior."
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=MCP03_TOOL_POISONING.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MCP03_TOOL_POISONING.js","sourceRoot":"","sources":["../../src/rules/MCP03_TOOL_POISONING.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,iBAAiB,GAAG;IACxB,+CAA+C;IAC/C,oBAAoB;IACpB,0BAA0B;IAC1B,cAAc;IACd,wBAAwB;IACxB,mBAAmB;IACnB,uBAAuB;IACvB,mBAAmB;IACnB,gCAAgC;CACjC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAS;IACxC,EAAE,EAAE,sBAAsB;IAC1B,IAAI,CAAC,OAAO;QACV,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,KAAK,IAAI,iBAAiB,EAAE,CAAC;gBACtC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBACjD,QAAQ,CAAC,IAAI,CAAC;wBACZ,EAAE,EAAE,sBAAsB;wBAC1B,KAAK,EAAE,6BAA6B;wBACpC,QAAQ,EAAE,MAAM;wBAChB,QAAQ,EAAE,gBAAgB;wBAC1B,OAAO,EACL,4FAA4F;wBAC9F,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,UAAU,EAAE,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;wBAC9D,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;wBAClB,cAAc,EACZ,4GAA4G;qBAC/G,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { lineNumberForIndex } from "../scanner/fileLoader.js";
|
|
2
|
+
const commandPatterns = [
|
|
3
|
+
{ label: "child_process usage", regex: /\bchild_process\b/gi },
|
|
4
|
+
{ label: "exec call", regex: /\bexec\s*\(/gi },
|
|
5
|
+
{ label: "spawn call", regex: /\bspawn\s*\(/gi },
|
|
6
|
+
{ label: "shell true option", regex: /\bshell\s*:\s*true\b/gi },
|
|
7
|
+
{ label: "arbitrary command input", regex: /arbitrary\s+command\s+input/gi },
|
|
8
|
+
{ label: "run_command tool", regex: /\brun_command\b/gi },
|
|
9
|
+
{ label: "run_shell tool", regex: /\brun_shell\b/gi },
|
|
10
|
+
{ label: "PowerShell command", regex: /\bpowershell\b/gi },
|
|
11
|
+
{ label: "bash -c command", regex: /\bbash\s+-c\b/gi },
|
|
12
|
+
{ label: "recursive deletion", regex: /\brm\s+-rf\b/gi },
|
|
13
|
+
{ label: "pipe to shell", regex: /\bcurl\b\s*\|\s*sh\b/gi }
|
|
14
|
+
];
|
|
15
|
+
export const MCP05_COMMAND_EXECUTION = {
|
|
16
|
+
id: "MCP05_COMMAND_EXECUTION",
|
|
17
|
+
scan(context) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
for (const file of context.files) {
|
|
20
|
+
for (const pattern of commandPatterns) {
|
|
21
|
+
for (const match of file.content.matchAll(pattern.regex)) {
|
|
22
|
+
findings.push({
|
|
23
|
+
id: "MCP05_COMMAND_EXECUTION",
|
|
24
|
+
title: "Risky command execution surface",
|
|
25
|
+
severity: "HIGH",
|
|
26
|
+
category: "Command Execution",
|
|
27
|
+
message: `Detected ${pattern.label}, which can expose the host to command execution risk.`,
|
|
28
|
+
filePath: file.filePath,
|
|
29
|
+
lineNumber: lineNumberForIndex(file.content, match.index ?? 0),
|
|
30
|
+
evidence: match[0],
|
|
31
|
+
recommendation: "Avoid arbitrary shell execution. If command execution is required, use a strict allowlist, fixed arguments, and explicit user consent."
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=MCP05_COMMAND_EXECUTION.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MCP05_COMMAND_EXECUTION.js","sourceRoot":"","sources":["../../src/rules/MCP05_COMMAND_EXECUTION.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,eAAe,GAA4C;IAC/D,EAAE,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,qBAAqB,EAAE;IAC9D,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,EAAE;IAC9C,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE;IAChD,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,wBAAwB,EAAE;IAC/D,EAAE,KAAK,EAAE,yBAAyB,EAAE,KAAK,EAAE,+BAA+B,EAAE;IAC5E,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,mBAAmB,EAAE;IACzD,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACrD,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC1D,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACtD,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACxD,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,wBAAwB,EAAE;CAC5D,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAS;IAC3C,EAAE,EAAE,yBAAyB;IAC7B,IAAI,CAAC,OAAO;QACV,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;gBACtC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBACzD,QAAQ,CAAC,IAAI,CAAC;wBACZ,EAAE,EAAE,yBAAyB;wBAC7B,KAAK,EAAE,iCAAiC;wBACxC,QAAQ,EAAE,MAAM;wBAChB,QAAQ,EAAE,mBAAmB;wBAC7B,OAAO,EAAE,YAAY,OAAO,CAAC,KAAK,wDAAwD;wBAC1F,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,UAAU,EAAE,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;wBAC9D,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;wBAClB,cAAc,EACZ,wIAAwI;qBAC3I,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
const configExtensions = /\.(json|ya?ml)$/i;
|
|
3
|
+
export const MCP07_INSUFFICIENT_AUTH = {
|
|
4
|
+
id: "MCP07_INSUFFICIENT_AUTH",
|
|
5
|
+
scan(context) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
for (const file of context.files.filter((candidate) => configExtensions.test(candidate.filePath))) {
|
|
8
|
+
const parsed = parseConfig(file);
|
|
9
|
+
const contentSuggestsRemote = /\bhttps?:\/\//i.test(file.content) ||
|
|
10
|
+
/"host"\s*:\s*"0\.0\.0\.0"/i.test(file.content) ||
|
|
11
|
+
/\btransport\s*:\s*http/i.test(file.content);
|
|
12
|
+
const explicitlyMissingAuth = /\bauth\s*[:=]\s*false\b/i.test(file.content) ||
|
|
13
|
+
/\bauthentication\s*[:=]\s*["']?none/i.test(file.content);
|
|
14
|
+
if ((contentSuggestsRemote && !hasAuth(parsed)) || explicitlyMissingAuth) {
|
|
15
|
+
findings.push({
|
|
16
|
+
id: "MCP07_INSUFFICIENT_AUTH",
|
|
17
|
+
title: "Remote MCP endpoint without sufficient auth",
|
|
18
|
+
severity: explicitlyMissingAuth ? "HIGH" : "MEDIUM",
|
|
19
|
+
category: "Authentication",
|
|
20
|
+
message: "Configuration exposes a remote HTTP surface without clear authentication controls.",
|
|
21
|
+
filePath: file.filePath,
|
|
22
|
+
lineNumber: 1,
|
|
23
|
+
evidence: explicitlyMissingAuth
|
|
24
|
+
? "auth: false or authentication: none"
|
|
25
|
+
: "remote endpoint without auth",
|
|
26
|
+
recommendation: "Require authentication for remote transports and document the expected credential flow."
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return findings;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
function parseConfig(file) {
|
|
34
|
+
try {
|
|
35
|
+
return file.filePath.endsWith(".json") ? JSON.parse(file.content) : parseYaml(file.content);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function hasAuth(value) {
|
|
42
|
+
if (!value || typeof value !== "object") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.some(hasAuth);
|
|
47
|
+
}
|
|
48
|
+
const record = value;
|
|
49
|
+
for (const [key, child] of Object.entries(record)) {
|
|
50
|
+
const normalizedKey = key.toLowerCase();
|
|
51
|
+
if (["auth", "authentication", "apikey", "tokenrequired", "requireauth"].includes(normalizedKey)) {
|
|
52
|
+
if (child === false || child === "none") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (hasAuth(child)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=MCP07_INSUFFICIENT_AUTH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MCP07_INSUFFICIENT_AUTH.js","sourceRoot":"","sources":["../../src/rules/MCP07_INSUFFICIENT_AUTH.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const MCP08_AUDIT_GAP = {
|
|
2
|
+
id: "MCP08_AUDIT_GAP",
|
|
3
|
+
scan(context) {
|
|
4
|
+
if (!context.policy.require.auditLogging) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const hasAuditLogging = context.files.some((file) => /(?:auditLogging|audit_logging|auditLog)\s*["']?\s*:\s*true/i.test(file.content));
|
|
8
|
+
if (hasAuditLogging) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const anchor = context.files.find((file) => /mcp\.(json|ya?ml)$/i.test(file.filePath)) ?? context.files[0];
|
|
12
|
+
if (!anchor) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
id: "MCP08_AUDIT_GAP",
|
|
18
|
+
title: "Audit logging requirement not satisfied",
|
|
19
|
+
severity: "MEDIUM",
|
|
20
|
+
category: "Audit",
|
|
21
|
+
message: "Policy requires audit logging, but the scanned project does not declare auditLogging: true.",
|
|
22
|
+
filePath: anchor.filePath,
|
|
23
|
+
lineNumber: 1,
|
|
24
|
+
evidence: "require.auditLogging: true",
|
|
25
|
+
recommendation: "Enable audit logging in the MCP server configuration and retain logs for security review."
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=MCP08_AUDIT_GAP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MCP08_AUDIT_GAP.js","sourceRoot":"","sources":["../../src/rules/MCP08_AUDIT_GAP.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAS;IACnC,EAAE,EAAE,iBAAiB;IACrB,IAAI,CAAC,OAAO;QACV,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;YACzC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAClD,6DAA6D,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CACjF,CAAC;QAEF,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,MAAM,GACV,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAE9F,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL;gBACE,EAAE,EAAE,iBAAiB;gBACrB,KAAK,EAAE,yCAAyC;gBAChD,QAAQ,EAAE,QAAQ;gBAClB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EACL,6FAA6F;gBAC/F,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,UAAU,EAAE,CAAC;gBACb,QAAQ,EAAE,4BAA4B;gBACtC,cAAc,EACZ,2FAA2F;aAC9F;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
export const MCP09_SHADOW_SERVER = {
|
|
3
|
+
id: "MCP09_SHADOW_SERVER",
|
|
4
|
+
scan(context) {
|
|
5
|
+
if (context.policy.allowlist.servers.length === 0) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
const allowlist = new Set(context.policy.allowlist.servers);
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const file of context.files.filter((candidate) => /mcp\.(json|ya?ml)$/i.test(candidate.filePath))) {
|
|
11
|
+
for (const serverName of extractServerNames(file)) {
|
|
12
|
+
if (!allowlist.has(serverName)) {
|
|
13
|
+
findings.push({
|
|
14
|
+
id: "MCP09_SHADOW_SERVER",
|
|
15
|
+
title: "Shadow MCP server",
|
|
16
|
+
severity: "MEDIUM",
|
|
17
|
+
category: "Inventory",
|
|
18
|
+
message: `Server "${serverName}" is not present in the policy allowlist.`,
|
|
19
|
+
filePath: file.filePath,
|
|
20
|
+
lineNumber: 1,
|
|
21
|
+
evidence: serverName,
|
|
22
|
+
recommendation: "Add expected MCP servers to the allowlist and investigate unrecognized server definitions."
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function extractServerNames(file) {
|
|
31
|
+
const parsed = parseConfig(file);
|
|
32
|
+
const names = new Set();
|
|
33
|
+
collectNames(parsed, names);
|
|
34
|
+
return [...names];
|
|
35
|
+
}
|
|
36
|
+
function parseConfig(file) {
|
|
37
|
+
try {
|
|
38
|
+
return file.filePath.endsWith(".json") ? JSON.parse(file.content) : parseYaml(file.content);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function collectNames(value, names) {
|
|
45
|
+
if (!value || typeof value !== "object") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
value.forEach((child) => collectNames(child, names));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const record = value;
|
|
53
|
+
if (typeof record.name === "string") {
|
|
54
|
+
names.add(record.name);
|
|
55
|
+
}
|
|
56
|
+
if (record.servers && typeof record.servers === "object" && !Array.isArray(record.servers)) {
|
|
57
|
+
for (const [name, child] of Object.entries(record.servers)) {
|
|
58
|
+
names.add(name);
|
|
59
|
+
collectNames(child, names);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
Object.values(record).forEach((child) => collectNames(child, names));
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=MCP09_SHADOW_SERVER.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MCP09_SHADOW_SERVER.js","sourceRoot":"","sources":["../../src/rules/MCP09_SHADOW_SERVER.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { lineNumberForIndex } from "../scanner/fileLoader.js";
|
|
2
|
+
const broadExposurePatterns = [
|
|
3
|
+
{ label: "filesystem root", regex: /["']\/["']/g },
|
|
4
|
+
{ label: "root glob", regex: /["']\/\*\*?["']/g },
|
|
5
|
+
{ label: "Windows drive root", regex: /["']C:\\\\?["']/gi },
|
|
6
|
+
{ label: "Windows user directory", regex: /C:\\\\?Users\b/gi },
|
|
7
|
+
{ label: "Unix home directory", regex: /["']\/home["']/gi },
|
|
8
|
+
{ label: "macOS users directory", regex: /["']\/Users["']/g },
|
|
9
|
+
{ label: "HOME variable", regex: /\$\{HOME\}/g },
|
|
10
|
+
{
|
|
11
|
+
label: "entire workspace read/write",
|
|
12
|
+
regex: /entire\s+workspace\s+with\s+read\/write\s+access/gi
|
|
13
|
+
}
|
|
14
|
+
];
|
|
15
|
+
export const MCP10_CONTEXT_OVERSHARING = {
|
|
16
|
+
id: "MCP10_CONTEXT_OVERSHARING",
|
|
17
|
+
scan(context) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
for (const file of context.files) {
|
|
20
|
+
for (const pattern of broadExposurePatterns) {
|
|
21
|
+
for (const match of file.content.matchAll(pattern.regex)) {
|
|
22
|
+
findings.push({
|
|
23
|
+
id: "MCP10_CONTEXT_OVERSHARING",
|
|
24
|
+
title: "Overly broad context exposure",
|
|
25
|
+
severity: "HIGH",
|
|
26
|
+
category: "Context Boundaries",
|
|
27
|
+
message: `Detected ${pattern.label}, which can expose more files or resources than an agent needs.`,
|
|
28
|
+
filePath: file.filePath,
|
|
29
|
+
lineNumber: lineNumberForIndex(file.content, match.index ?? 0),
|
|
30
|
+
evidence: match[0],
|
|
31
|
+
recommendation: "Restrict filesystem and resource access to the smallest explicit paths required by the MCP tool."
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=MCP10_CONTEXT_OVERSHARING.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MCP10_CONTEXT_OVERSHARING.js","sourceRoot":"","sources":["../../src/rules/MCP10_CONTEXT_OVERSHARING.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,qBAAqB,GAA4C;IACrE,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,aAAa,EAAE;IAClD,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,kBAAkB,EAAE;IACjD,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,mBAAmB,EAAE;IAC3D,EAAE,KAAK,EAAE,wBAAwB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC9D,EAAE,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC3D,EAAE,KAAK,EAAE,uBAAuB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC7D,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE;IAChD;QACE,KAAK,EAAE,6BAA6B;QACpC,KAAK,EAAE,oDAAoD;KAC5D;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAS;IAC7C,EAAE,EAAE,2BAA2B;IAC/B,IAAI,CAAC,OAAO;QACV,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,OAAO,IAAI,qBAAqB,EAAE,CAAC;gBAC5C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBACzD,QAAQ,CAAC,IAAI,CAAC;wBACZ,EAAE,EAAE,2BAA2B;wBAC/B,KAAK,EAAE,+BAA+B;wBACtC,QAAQ,EAAE,MAAM;wBAChB,QAAQ,EAAE,oBAAoB;wBAC9B,OAAO,EAAE,YAAY,OAAO,CAAC,KAAK,iEAAiE;wBACnG,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,UAAU,EAAE,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;wBAC9D,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;wBAClB,cAAc,EACZ,kGAAkG;qBACrG,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "../types.js";
|
|
2
|
+
import { MCP01_SECRET_EXPOSURE } from "./MCP01_SECRET_EXPOSURE.js";
|
|
3
|
+
import { MCP03_TOOL_POISONING } from "./MCP03_TOOL_POISONING.js";
|
|
4
|
+
import { MCP05_COMMAND_EXECUTION } from "./MCP05_COMMAND_EXECUTION.js";
|
|
5
|
+
import { MCP07_INSUFFICIENT_AUTH } from "./MCP07_INSUFFICIENT_AUTH.js";
|
|
6
|
+
import { MCP08_AUDIT_GAP } from "./MCP08_AUDIT_GAP.js";
|
|
7
|
+
import { MCP09_SHADOW_SERVER } from "./MCP09_SHADOW_SERVER.js";
|
|
8
|
+
import { MCP10_CONTEXT_OVERSHARING } from "./MCP10_CONTEXT_OVERSHARING.js";
|
|
9
|
+
export declare const rules: Rule[];
|
|
10
|
+
export { MCP01_SECRET_EXPOSURE, MCP03_TOOL_POISONING, MCP05_COMMAND_EXECUTION, MCP07_INSUFFICIENT_AUTH, MCP08_AUDIT_GAP, MCP09_SHADOW_SERVER, MCP10_CONTEXT_OVERSHARING };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MCP01_SECRET_EXPOSURE } from "./MCP01_SECRET_EXPOSURE.js";
|
|
2
|
+
import { MCP03_TOOL_POISONING } from "./MCP03_TOOL_POISONING.js";
|
|
3
|
+
import { MCP05_COMMAND_EXECUTION } from "./MCP05_COMMAND_EXECUTION.js";
|
|
4
|
+
import { MCP07_INSUFFICIENT_AUTH } from "./MCP07_INSUFFICIENT_AUTH.js";
|
|
5
|
+
import { MCP08_AUDIT_GAP } from "./MCP08_AUDIT_GAP.js";
|
|
6
|
+
import { MCP09_SHADOW_SERVER } from "./MCP09_SHADOW_SERVER.js";
|
|
7
|
+
import { MCP10_CONTEXT_OVERSHARING } from "./MCP10_CONTEXT_OVERSHARING.js";
|
|
8
|
+
export const rules = [
|
|
9
|
+
MCP01_SECRET_EXPOSURE,
|
|
10
|
+
MCP03_TOOL_POISONING,
|
|
11
|
+
MCP05_COMMAND_EXECUTION,
|
|
12
|
+
MCP07_INSUFFICIENT_AUTH,
|
|
13
|
+
MCP08_AUDIT_GAP,
|
|
14
|
+
MCP09_SHADOW_SERVER,
|
|
15
|
+
MCP10_CONTEXT_OVERSHARING
|
|
16
|
+
];
|
|
17
|
+
export { MCP01_SECRET_EXPOSURE, MCP03_TOOL_POISONING, MCP05_COMMAND_EXECUTION, MCP07_INSUFFICIENT_AUTH, MCP08_AUDIT_GAP, MCP09_SHADOW_SERVER, MCP10_CONTEXT_OVERSHARING };
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,MAAM,CAAC,MAAM,KAAK,GAAW;IAC3B,qBAAqB;IACrB,oBAAoB;IACpB,uBAAuB;IACvB,uBAAuB;IACvB,eAAe;IACf,mBAAmB;IACnB,yBAAyB;CAC1B,CAAC;AAEF,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,EACvB,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EAC1B,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { toPosixPath } from "../utils/paths.js";
|
|
5
|
+
const ignoredDirectories = [
|
|
6
|
+
"**/node_modules/**",
|
|
7
|
+
"**/dist/**",
|
|
8
|
+
"**/.git/**",
|
|
9
|
+
"**/coverage/**",
|
|
10
|
+
"**/.turbo/**"
|
|
11
|
+
];
|
|
12
|
+
export async function loadFiles(targetPath) {
|
|
13
|
+
const stat = await fs.stat(targetPath);
|
|
14
|
+
const root = stat.isDirectory() ? targetPath : path.dirname(targetPath);
|
|
15
|
+
const entries = stat.isDirectory()
|
|
16
|
+
? await fg("**/*", {
|
|
17
|
+
cwd: root,
|
|
18
|
+
dot: true,
|
|
19
|
+
onlyFiles: true,
|
|
20
|
+
ignore: ignoredDirectories,
|
|
21
|
+
absolute: true
|
|
22
|
+
})
|
|
23
|
+
: [targetPath];
|
|
24
|
+
const files = [];
|
|
25
|
+
for (const filePath of entries.sort()) {
|
|
26
|
+
const content = await readTextFile(filePath);
|
|
27
|
+
if (content === undefined) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
files.push({
|
|
31
|
+
filePath,
|
|
32
|
+
relativePath: toPosixPath(path.relative(root, filePath)),
|
|
33
|
+
content
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
async function readTextFile(filePath) {
|
|
39
|
+
const buffer = await fs.readFile(filePath);
|
|
40
|
+
if (buffer.includes(0)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return buffer.toString("utf8");
|
|
44
|
+
}
|
|
45
|
+
export function lineNumberForIndex(content, index) {
|
|
46
|
+
return content.slice(0, Math.max(index, 0)).split(/\r?\n/).length;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=fileLoader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileLoader.js","sourceRoot":"","sources":["../../src/scanner/fileLoader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,WAAW,CAAC;AAE3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,kBAAkB,GAAG;IACzB,oBAAoB;IACpB,YAAY;IACZ,YAAY;IACZ,gBAAgB;IAChB,cAAc;CACf,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAkB;IAChD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE;QAChC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE;YACf,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,IAAI;YACT,SAAS,EAAE,IAAI;YACf,MAAM,EAAE,kBAAkB;YAC1B,QAAQ,EAAE,IAAI;SACf,CAAC;QACJ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAEjB,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,KAAK,CAAC,IAAI,CAAC;YACT,QAAQ;YACR,YAAY,EAAE,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACxD,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,QAAgB;IAC1C,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,KAAa;IAC/D,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;AACpE,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadPolicy } from "../policy/loadPolicy.js";
|
|
3
|
+
import { applyPolicy, summarizeFindings } from "../policy/policyEngine.js";
|
|
4
|
+
import { rules } from "../rules/index.js";
|
|
5
|
+
import { loadFiles } from "./fileLoader.js";
|
|
6
|
+
export async function scanTarget(options) {
|
|
7
|
+
const targetPath = path.resolve(options.target);
|
|
8
|
+
const policy = await loadPolicy(options.policyPath);
|
|
9
|
+
const files = await loadFiles(targetPath);
|
|
10
|
+
const context = { targetPath, files, policy };
|
|
11
|
+
const rawFindings = rules.flatMap((rule) => rule.scan(context));
|
|
12
|
+
const findings = applyPolicy(rawFindings, policy);
|
|
13
|
+
const summary = summarizeFindings(findings);
|
|
14
|
+
return {
|
|
15
|
+
targetPath,
|
|
16
|
+
findings,
|
|
17
|
+
summary,
|
|
18
|
+
shouldFail: summary.failCount > 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=scanTarget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanTarget.js","sourceRoot":"","sources":["../../src/scanner/scanTarget.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAoB;IACnD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC9C,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAE5C,OAAO;QACL,UAAU;QACV,QAAQ;QACR,OAAO;QACP,UAAU,EAAE,OAAO,CAAC,SAAS,GAAG,CAAC;KAClC,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type Severity = "INFO" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
|
2
|
+
export type RuleMode = "off" | "warn" | "error";
|
|
3
|
+
export interface Finding {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
severity: Severity;
|
|
7
|
+
category: string;
|
|
8
|
+
message: string;
|
|
9
|
+
filePath: string;
|
|
10
|
+
lineNumber?: number;
|
|
11
|
+
evidence: string;
|
|
12
|
+
recommendation: string;
|
|
13
|
+
policyLevel?: "warn" | "error";
|
|
14
|
+
failsScan?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface LoadedFile {
|
|
17
|
+
filePath: string;
|
|
18
|
+
relativePath: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
export interface Policy {
|
|
22
|
+
rules: Record<string, RuleMode>;
|
|
23
|
+
allowlist: {
|
|
24
|
+
servers: string[];
|
|
25
|
+
};
|
|
26
|
+
deny: {
|
|
27
|
+
filesystemRoots: string[];
|
|
28
|
+
commands: string[];
|
|
29
|
+
};
|
|
30
|
+
require: {
|
|
31
|
+
auditLogging: boolean;
|
|
32
|
+
explicitToolConsent: boolean;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface RuleContext {
|
|
36
|
+
targetPath: string;
|
|
37
|
+
files: LoadedFile[];
|
|
38
|
+
policy: Policy;
|
|
39
|
+
}
|
|
40
|
+
export interface Rule {
|
|
41
|
+
id: string;
|
|
42
|
+
scan(context: RuleContext): Finding[];
|
|
43
|
+
}
|
|
44
|
+
export interface ScanOptions {
|
|
45
|
+
target: string;
|
|
46
|
+
policyPath?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ScanSummary {
|
|
49
|
+
total: number;
|
|
50
|
+
failCount: number;
|
|
51
|
+
bySeverity: Record<Severity, number>;
|
|
52
|
+
}
|
|
53
|
+
export interface ScanResult {
|
|
54
|
+
targetPath: string;
|
|
55
|
+
findings: Finding[];
|
|
56
|
+
summary: ScanSummary;
|
|
57
|
+
shouldFail: boolean;
|
|
58
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function toPosixPath(filePath) {
|
|
3
|
+
return filePath.split(path.sep).join("/");
|
|
4
|
+
}
|
|
5
|
+
export function displayPath(filePath) {
|
|
6
|
+
return toPosixPath(path.relative(process.cwd(), filePath) || filePath);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,IAAI,QAAQ,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const severityRank = {
|
|
2
|
+
INFO: 0,
|
|
3
|
+
LOW: 1,
|
|
4
|
+
MEDIUM: 2,
|
|
5
|
+
HIGH: 3,
|
|
6
|
+
CRITICAL: 4
|
|
7
|
+
};
|
|
8
|
+
export function shouldFailByDefault(severity) {
|
|
9
|
+
return severityRank[severity] >= severityRank.HIGH;
|
|
10
|
+
}
|
|
11
|
+
export function createSeverityCounts() {
|
|
12
|
+
return {
|
|
13
|
+
INFO: 0,
|
|
14
|
+
LOW: 0,
|
|
15
|
+
MEDIUM: 0,
|
|
16
|
+
HIGH: 0,
|
|
17
|
+
CRITICAL: 0
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=severity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"severity.js","sourceRoot":"","sources":["../../src/utils/severity.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,IAAI,EAAE,CAAC;IACP,GAAG,EAAE,CAAC;IACN,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;IACP,QAAQ,EAAE,CAAC;CACZ,CAAC;AAEF,MAAM,UAAU,mBAAmB,CAAC,QAAkB;IACpD,OAAO,YAAY,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,IAAI,EAAE,CAAC;QACP,GAAG,EAAE,CAAC;QACN,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,CAAC;QACP,QAAQ,EAAE,CAAC;KACZ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
MCP-Guard is intentionally small in v0.1.
|
|
4
|
+
|
|
5
|
+
The CLI in `src/cli.ts` parses commands with Commander and delegates scans to `scanTarget`. The scanner loads target files, loads policy, runs every rule, applies policy, and returns a normalized `ScanResult`.
|
|
6
|
+
|
|
7
|
+
Rules are independent modules under `src/rules`. Each rule receives loaded files and policy context, then returns plain `Finding` objects. The policy engine is the only layer that decides whether a finding fails the scan.
|
|
8
|
+
|
|
9
|
+
Reporters are pure rendering functions. Console, JSON, and Markdown output are generated from the same scan result, which keeps CLI behavior deterministic and easy to test.
|
|
10
|
+
|
|
11
|
+
The examples under `examples/` are the local test laboratory. They are synthetic and safe; they do not require credentials, external APIs, or live MCP servers.
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Release Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist for publishing MCP-Guard to npm and GitHub.
|
|
4
|
+
|
|
5
|
+
## npm
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm login
|
|
9
|
+
npm whoami
|
|
10
|
+
npm run lint
|
|
11
|
+
npm test
|
|
12
|
+
npm run build
|
|
13
|
+
npm pack
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Test the generated tarball locally:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g ./srhot-mcp-guard-0.1.0.tgz
|
|
20
|
+
mcp-guard scan examples/safe-mcp-server
|
|
21
|
+
mcp-guard scan examples/bad-mcp-server --no-fail
|
|
22
|
+
npm uninstall -g @srhot/mcp-guard
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Publish:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm publish --access public
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## GitHub
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git tag v0.1.0
|
|
35
|
+
git push origin v0.1.0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then create a GitHub release for `v0.1.0` with a short summary, install commands, and a link to the README.
|
|
39
|
+
|
|
40
|
+
## GitHub Actions History
|
|
41
|
+
|
|
42
|
+
Old failed GitHub Actions runs remain visible in the Actions tab after fixes are pushed. That is normal. Use the latest workflow run for the current release status.
|