@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.
@@ -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.10",
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.10",
25
- "@panguard-ai/containment": "0.1.0",
26
- "@panguard-ai/panguard-guard": "1.8.10"
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.10"
29
+ "@panguard-ai/atr": "1.8.13"
30
30
  },
31
31
  "files": [
32
32
  "dist",