@panguard-ai/panguard-mcp-proxy 1.8.9 → 1.8.12

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,12 @@ 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
+ };
41
47
  }
42
48
  /**
43
49
  * Whether a single rule match is strong enough to HARD-DENY a live tool call
@@ -56,7 +62,7 @@ interface RuleLike {
56
62
  * Pure + exported so the policy is unit-tested independently of which live rule
57
63
  * happens to match (the rule corpus changes daily; this policy must not).
58
64
  */
59
- export declare function shouldHardDeny(rule: RuleLike): boolean;
65
+ export declare function shouldHardDeny(rule: RuleLike, builtinToolSurface?: boolean): boolean;
60
66
  export declare class ProxyEvaluator {
61
67
  private readonly engine;
62
68
  private rulesLoaded;
@@ -109,7 +115,7 @@ export declare class ProxyEvaluator {
109
115
  * The old hardcoded 'mcp_exchange' both missed the tool_call attack rules
110
116
  * and false-fired multi_agent_comm rules on ordinary commands.
111
117
  */
112
- evaluateToolCall(toolName: string, args: Record<string, unknown>, eventType?: AgentEvent['type']): Promise<EvalResult>;
118
+ evaluateToolCall(toolName: string, args: Record<string, unknown>, eventType?: AgentEvent['type'], builtinToolSurface?: boolean): Promise<EvalResult>;
113
119
  /**
114
120
  * Evaluate a tool response (PostToolUse) — the indirect-prompt-injection path.
115
121
  *
package/dist/evaluator.js CHANGED
@@ -55,9 +55,28 @@ export const EVALUATION_ERROR_SENTINEL = 'evaluation-error';
55
55
  * Pure + exported so the policy is unit-tested independently of which live rule
56
56
  * happens to match (the rule corpus changes daily; this policy must not).
57
57
  */
58
- export function shouldHardDeny(rule) {
58
+ export function shouldHardDeny(rule, builtinToolSurface = false) {
59
59
  if (rule.confirm === 'embedding')
60
60
  return false;
61
+ if (builtinToolSurface && rule.tags?.scan_target === 'mcp') {
62
+ // FALSE-POSITIVE gate for the built-in-tool hook, which guards the agent's
63
+ // OWN native shell (Bash/Edit/Write/WebFetch). Rules scoped scan_target:mcp
64
+ // are MCP-tool-ARGUMENT rules — e.g. ATR-2026-00111 "Shell Metacharacter
65
+ // Injection in Tool Arguments" / ATR-2026-00066 "Parameter Injection" — that
66
+ // key on shell metacharacters (`;` `|` `$()`). Inside an MCP argument those
67
+ // are anomalous and rightly deny; but they are the NORMAL grammar of a real
68
+ // shell, so matched against the agent's own `echo x; curl localhost` they
69
+ // false-block legitimate work (they only reach a tool_call event at all via
70
+ // the engine's mcp-over-tool exception). On this surface an mcp-scoped rule
71
+ // therefore DEGRADES to an 'ask' advisory — the caller warns, never bricks
72
+ // the agent. Semantic exfil/RCE rules scoped to the shell's real domain
73
+ // (tool_args / skill / host / code / any) still hard-block, so credential
74
+ // exfil (`cat ~/.ssh/id_rsa | curl`, `env | curl`) is caught regardless of
75
+ // this gate. The MCP proxy leaves builtinToolSurface=false: on a genuine MCP
76
+ // ARGUMENT, `; curl` really is injection. This keys on scan_target (a rule's
77
+ // intrinsic scope), NOT maturity — so it never drifts with the daily corpus.
78
+ return false;
79
+ }
61
80
  if (rule.severity === 'critical')
62
81
  return true;
63
82
  return rule.severity === 'high' && rule.maturity === 'stable';
@@ -192,7 +211,13 @@ export class ProxyEvaluator {
192
211
  * The old hardcoded 'mcp_exchange' both missed the tool_call attack rules
193
212
  * and false-fired multi_agent_comm rules on ordinary commands.
194
213
  */
195
- async evaluateToolCall(toolName, args, eventType = 'mcp_exchange') {
214
+ async evaluateToolCall(toolName, args, eventType = 'mcp_exchange',
215
+ // The built-in-tool hook (guarding the agent's OWN Bash/Edit/Write) passes
216
+ // true: on that high-FP surface an MCP-argument rule (scan_target:mcp shell-
217
+ // metacharacter rule) advises instead of hard-blocking a real shell command.
218
+ // The MCP proxy leaves it false (default) — on a genuine MCP argument those
219
+ // rules correctly deny.
220
+ builtinToolSurface = false) {
196
221
  const start = Date.now();
197
222
  // Check Guard blocklist first (instant deny, no regex needed)
198
223
  this.refreshBlocklist();
@@ -217,7 +242,7 @@ export class ProxyEvaluator {
217
242
  tool_input: flatContent,
218
243
  },
219
244
  };
220
- return this.evaluate(event, start);
245
+ return this.evaluate(event, start, builtinToolSurface);
221
246
  }
222
247
  /**
223
248
  * Evaluate a tool response (PostToolUse) — the indirect-prompt-injection path.
@@ -251,7 +276,7 @@ export class ProxyEvaluator {
251
276
  };
252
277
  return this.evaluate(event, start);
253
278
  }
254
- async evaluate(event, start) {
279
+ async evaluate(event, start, builtinToolSurface = false) {
255
280
  try {
256
281
  const matches = this.engine.evaluate(event);
257
282
  const durationMs = Date.now() - start;
@@ -284,7 +309,7 @@ export class ProxyEvaluator {
284
309
  // 'ask' advisory below, but it can never silently block the user's tool.
285
310
  // Bundled/trusted rules are unaffected. This is the arm gate for the
286
311
  // enforce path.
287
- const blockMatch = matches.find((m) => shouldHardDeny(m.rule) && !this.adviseOnlyIds.has(m.rule.id));
312
+ const blockMatch = matches.find((m) => shouldHardDeny(m.rule, builtinToolSurface) && !this.adviseOnlyIds.has(m.rule.id));
288
313
  const askMatch = blockMatch ??
289
314
  matches.find((m) => m.rule.severity === 'high' || m.rule.severity === 'critical');
290
315
  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.9",
3
+ "version": "1.8.12",
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.9",
24
+ "@panguard-ai/atr": "1.8.12",
25
25
  "@panguard-ai/containment": "0.1.0",
26
- "@panguard-ai/panguard-guard": "1.8.9"
26
+ "@panguard-ai/panguard-guard": "1.8.12"
27
27
  },
28
28
  "peerDependencies": {
29
- "@panguard-ai/atr": "1.8.9"
29
+ "@panguard-ai/atr": "1.8.12"
30
30
  },
31
31
  "files": [
32
32
  "dist",