@panguard-ai/panguard-mcp-proxy 1.8.10 → 1.8.13
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/dist/evaluator.d.ts +9 -2
- package/dist/evaluator.js +45 -5
- package/package.json +5 -5
package/dist/evaluator.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ interface RuleLike {
|
|
|
38
38
|
readonly severity: string;
|
|
39
39
|
readonly maturity?: string;
|
|
40
40
|
readonly confirm?: string;
|
|
41
|
+
/** ATR scan_target lives on the parsed rule under tags (rich vocab: mcp /
|
|
42
|
+
* tool_args / skill / host / code / any / llm_io / ...). Used by the built-in-
|
|
43
|
+
* tool surface gate to skip MCP-argument rules on a native shell command. */
|
|
44
|
+
readonly tags?: {
|
|
45
|
+
readonly scan_target?: string;
|
|
46
|
+
readonly subcategory?: string;
|
|
47
|
+
};
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* Whether a single rule match is strong enough to HARD-DENY a live tool call
|
|
@@ -56,7 +63,7 @@ interface RuleLike {
|
|
|
56
63
|
* Pure + exported so the policy is unit-tested independently of which live rule
|
|
57
64
|
* happens to match (the rule corpus changes daily; this policy must not).
|
|
58
65
|
*/
|
|
59
|
-
export declare function shouldHardDeny(rule: RuleLike): boolean;
|
|
66
|
+
export declare function shouldHardDeny(rule: RuleLike, builtinToolSurface?: boolean): boolean;
|
|
60
67
|
export declare class ProxyEvaluator {
|
|
61
68
|
private readonly engine;
|
|
62
69
|
private rulesLoaded;
|
|
@@ -109,7 +116,7 @@ export declare class ProxyEvaluator {
|
|
|
109
116
|
* The old hardcoded 'mcp_exchange' both missed the tool_call attack rules
|
|
110
117
|
* and false-fired multi_agent_comm rules on ordinary commands.
|
|
111
118
|
*/
|
|
112
|
-
evaluateToolCall(toolName: string, args: Record<string, unknown>, eventType?: AgentEvent['type']): Promise<EvalResult>;
|
|
119
|
+
evaluateToolCall(toolName: string, args: Record<string, unknown>, eventType?: AgentEvent['type'], builtinToolSurface?: boolean): Promise<EvalResult>;
|
|
113
120
|
/**
|
|
114
121
|
* Evaluate a tool response (PostToolUse) — the indirect-prompt-injection path.
|
|
115
122
|
*
|
package/dist/evaluator.js
CHANGED
|
@@ -38,6 +38,20 @@ function findRulesDir() {
|
|
|
38
38
|
* silently break the fail-open contract.
|
|
39
39
|
*/
|
|
40
40
|
export const EVALUATION_ERROR_SENTINEL = 'evaluation-error';
|
|
41
|
+
/**
|
|
42
|
+
* The ONLY rule subcategories whose detection keys on shell METACHARACTERS
|
|
43
|
+
* (`;` `|` `$()` `&&`) — the grammar that is anomalous inside an MCP tool
|
|
44
|
+
* argument but NORMAL in the agent's own shell. Only these may degrade on the
|
|
45
|
+
* built-in-tool surface. Everything else scoped scan_target:mcp (credential-theft,
|
|
46
|
+
* env-var-harvesting, tool-poisoning, ...) detects malicious SEMANTICS that are
|
|
47
|
+
* illegitimate on any shell and MUST keep hard-blocking there. 1.8.12 wrongly
|
|
48
|
+
* gated the whole scan_target:mcp class, which silently degraded credential theft
|
|
49
|
+
* (`cat ~/.ssh/id_rsa`) to an advisory on the agent's own shell — a shipped hole.
|
|
50
|
+
*/
|
|
51
|
+
const SHELL_METACHAR_FP_SUBCATEGORIES = new Set([
|
|
52
|
+
'shell-escape',
|
|
53
|
+
'parameter-injection',
|
|
54
|
+
]);
|
|
41
55
|
/**
|
|
42
56
|
* Whether a single rule match is strong enough to HARD-DENY a live tool call
|
|
43
57
|
* (vs. degrade to 'ask'). This is the proxy's false-positive control point: the
|
|
@@ -55,9 +69,29 @@ export const EVALUATION_ERROR_SENTINEL = 'evaluation-error';
|
|
|
55
69
|
* Pure + exported so the policy is unit-tested independently of which live rule
|
|
56
70
|
* happens to match (the rule corpus changes daily; this policy must not).
|
|
57
71
|
*/
|
|
58
|
-
export function shouldHardDeny(rule) {
|
|
72
|
+
export function shouldHardDeny(rule, builtinToolSurface = false) {
|
|
59
73
|
if (rule.confirm === 'embedding')
|
|
60
74
|
return false;
|
|
75
|
+
if (builtinToolSurface &&
|
|
76
|
+
rule.tags?.scan_target === 'mcp' &&
|
|
77
|
+
SHELL_METACHAR_FP_SUBCATEGORIES.has(rule.tags?.subcategory ?? '')) {
|
|
78
|
+
// FALSE-POSITIVE gate for the built-in-tool hook, which guards the agent's
|
|
79
|
+
// OWN native shell (Bash/Edit/Write/WebFetch). NARROWED (1.8.13) to ONLY the
|
|
80
|
+
// shell-metacharacter-injection subcategories (shell-escape / parameter-
|
|
81
|
+
// injection — ATR-2026-00111 / 00066) that key on `;` `|` `$()` grammar.
|
|
82
|
+
// Inside an MCP argument those are anomalous and rightly deny; but they are the
|
|
83
|
+
// NORMAL grammar of a real shell, so matched against the agent's own
|
|
84
|
+
// `echo x; curl localhost` they false-block legitimate work (they only reach a
|
|
85
|
+
// tool_call event via the engine's mcp-over-tool exception). On this surface
|
|
86
|
+
// they DEGRADE to an 'ask' advisory. CRITICAL: every OTHER scan_target:mcp
|
|
87
|
+
// rule still hard-blocks here — 1.8.12 wrongly gated the whole class, which
|
|
88
|
+
// silently degraded credential theft (`cat ~/.ssh/id_rsa`, matched by the
|
|
89
|
+
// scan_target:mcp credential-theft rule 00113) to a non-blocking advisory on
|
|
90
|
+
// the agent's own shell. The MCP proxy leaves builtinToolSurface=false: on a genuine MCP
|
|
91
|
+
// ARGUMENT, `; curl` really is injection. This keys on scan_target (a rule's
|
|
92
|
+
// intrinsic scope), NOT maturity — so it never drifts with the daily corpus.
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
61
95
|
if (rule.severity === 'critical')
|
|
62
96
|
return true;
|
|
63
97
|
return rule.severity === 'high' && rule.maturity === 'stable';
|
|
@@ -192,7 +226,13 @@ export class ProxyEvaluator {
|
|
|
192
226
|
* The old hardcoded 'mcp_exchange' both missed the tool_call attack rules
|
|
193
227
|
* and false-fired multi_agent_comm rules on ordinary commands.
|
|
194
228
|
*/
|
|
195
|
-
async evaluateToolCall(toolName, args, eventType = 'mcp_exchange'
|
|
229
|
+
async evaluateToolCall(toolName, args, eventType = 'mcp_exchange',
|
|
230
|
+
// The built-in-tool hook (guarding the agent's OWN Bash/Edit/Write) passes
|
|
231
|
+
// true: on that high-FP surface an MCP-argument rule (scan_target:mcp shell-
|
|
232
|
+
// metacharacter rule) advises instead of hard-blocking a real shell command.
|
|
233
|
+
// The MCP proxy leaves it false (default) — on a genuine MCP argument those
|
|
234
|
+
// rules correctly deny.
|
|
235
|
+
builtinToolSurface = false) {
|
|
196
236
|
const start = Date.now();
|
|
197
237
|
// Check Guard blocklist first (instant deny, no regex needed)
|
|
198
238
|
this.refreshBlocklist();
|
|
@@ -217,7 +257,7 @@ export class ProxyEvaluator {
|
|
|
217
257
|
tool_input: flatContent,
|
|
218
258
|
},
|
|
219
259
|
};
|
|
220
|
-
return this.evaluate(event, start);
|
|
260
|
+
return this.evaluate(event, start, builtinToolSurface);
|
|
221
261
|
}
|
|
222
262
|
/**
|
|
223
263
|
* Evaluate a tool response (PostToolUse) — the indirect-prompt-injection path.
|
|
@@ -251,7 +291,7 @@ export class ProxyEvaluator {
|
|
|
251
291
|
};
|
|
252
292
|
return this.evaluate(event, start);
|
|
253
293
|
}
|
|
254
|
-
async evaluate(event, start) {
|
|
294
|
+
async evaluate(event, start, builtinToolSurface = false) {
|
|
255
295
|
try {
|
|
256
296
|
const matches = this.engine.evaluate(event);
|
|
257
297
|
const durationMs = Date.now() - start;
|
|
@@ -284,7 +324,7 @@ export class ProxyEvaluator {
|
|
|
284
324
|
// 'ask' advisory below, but it can never silently block the user's tool.
|
|
285
325
|
// Bundled/trusted rules are unaffected. This is the arm gate for the
|
|
286
326
|
// enforce path.
|
|
287
|
-
const blockMatch = matches.find((m) => shouldHardDeny(m.rule) && !this.adviseOnlyIds.has(m.rule.id));
|
|
327
|
+
const blockMatch = matches.find((m) => shouldHardDeny(m.rule, builtinToolSurface) && !this.adviseOnlyIds.has(m.rule.id));
|
|
288
328
|
const askMatch = blockMatch ??
|
|
289
329
|
matches.find((m) => m.rule.severity === 'high' || m.rule.severity === 'critical');
|
|
290
330
|
const outcome = blockMatch ? 'deny' : askMatch ? 'ask' : 'allow';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panguard-ai/panguard-mcp-proxy",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.13",
|
|
4
4
|
"description": "MCP Proxy — runtime interception for AI agent tool calls using ATR rules",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
23
23
|
"agent-threat-rules": "^3.5.8",
|
|
24
|
-
"@panguard-ai/atr": "1.8.
|
|
25
|
-
"@panguard-ai/
|
|
26
|
-
"@panguard-ai/
|
|
24
|
+
"@panguard-ai/atr": "1.8.13",
|
|
25
|
+
"@panguard-ai/panguard-guard": "1.8.13",
|
|
26
|
+
"@panguard-ai/containment": "0.1.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@panguard-ai/atr": "1.8.
|
|
29
|
+
"@panguard-ai/atr": "1.8.13"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|