fullcourtdefense-cli 1.21.21 → 1.21.23

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.
@@ -413,6 +413,33 @@ function isSqlContext(toolName, candidate) {
413
413
  const lastKey = candidate.keyPath.split('.').pop() || '';
414
414
  return SQL_TOOL_HINT.test(toolName) || SQL_KEYS.has(lastKey) || SQL_KEYS.has(candidate.keyPath);
415
415
  }
416
+ /**
417
+ * Metadata SSRF only matters where the string can actually BECOME a request: a shell
418
+ * command, an outbound/HTTP tool, or a URL-typed argument. Source code or file contents
419
+ * that merely MENTION the endpoint (e.g. editing a file that implements an SSRF guard)
420
+ * must not block — the unconditional match over-blocked ordinary development work.
421
+ */
422
+ function isMetadataRequestContext(toolName, candidate) {
423
+ if (isCommandContext(toolName, candidate))
424
+ return true;
425
+ if (OUTBOUND_TOOL_HINT.test(toolName))
426
+ return true;
427
+ const lastKey = candidate.keyPath.split('.').pop() || '';
428
+ return /(?:url|uri|endpoint|host|address|webhook)/i.test(lastKey);
429
+ }
430
+ /**
431
+ * Sensitive credential PATHS only matter where the string is used AS a path (a file access,
432
+ * a path-typed argument) or inside a command. File CONTENTS being written/edited routinely
433
+ * mention such paths legitimately (security tooling source, tests, docs) and must not block.
434
+ */
435
+ function isSensitivePathContext(toolName, candidate) {
436
+ if (isCommandContext(toolName, candidate))
437
+ return true;
438
+ if (/(?:read|open|cat|copy|download|upload|access|stat|list|glob|search|find|grep)/i.test(toolName))
439
+ return true;
440
+ const lastKey = candidate.keyPath.split('.').pop() || '';
441
+ return /(?:path|file|filename|dir|directory|target|source|src|dest|location|glob|pattern|uri|url)/i.test(lastKey);
442
+ }
416
443
  function scanTextValue(toolName, value, options) {
417
444
  const custom = customBlock(value, options);
418
445
  if (custom)
@@ -443,17 +470,42 @@ function scanTextValue(toolName, value, options) {
443
470
  }
444
471
  return undefined;
445
472
  }
473
+ /**
474
+ * A string can only DO something in an actionable context: a shell command, an outbound
475
+ * request, or a path/target argument. File CONTENTS being written or edited are inert —
476
+ * mentioning a blocked path/endpoint/pattern in source code, tests, or docs is normal
477
+ * development work and must never block (see isMetadataRequestContext / isSensitivePathContext).
478
+ */
479
+ function isActionableContext(toolName, candidate) {
480
+ return isCommandContext(toolName, candidate)
481
+ || isSensitivePathContext(toolName, candidate)
482
+ || OUTBOUND_TOOL_HINT.test(toolName);
483
+ }
446
484
  function scanDeterministicToolCall(toolName, toolArgs, options) {
447
485
  const candidates = collectStrings(toolArgs);
448
486
  const outbound = isOutboundContext(toolName, candidates);
449
487
  for (const candidate of candidates) {
488
+ // Custom rules ("block AI agents from touching this path/pattern") follow the same
489
+ // context principle as built-ins: enforce on access/execution/egress, not on editing
490
+ // file contents that merely mention the pattern.
491
+ if (!isActionableContext(toolName, candidate))
492
+ continue;
450
493
  const custom = customBlock(candidate.value, options);
451
494
  if (custom)
452
495
  return custom;
453
496
  }
454
497
  for (const candidate of candidates) {
455
498
  const finding = scanTextValue(toolName, candidate.value, options);
456
- if (finding && (isCommandContext(toolName, candidate) || finding.category === 'sensitive_file' || finding.category === 'metadata_ssrf'))
499
+ if (!finding)
500
+ continue;
501
+ // Context gates: a pattern is only dangerous where it can act — a command, a request
502
+ // target, or a path being accessed. A bare mention inside file contents / source code
503
+ // being written (e.g. editing security tooling or its tests) must not block.
504
+ if (finding.category === 'metadata_ssrf' && !isMetadataRequestContext(toolName, candidate))
505
+ continue;
506
+ if (finding.category === 'sensitive_file' && !isSensitivePathContext(toolName, candidate))
507
+ continue;
508
+ if (isCommandContext(toolName, candidate) || finding.category === 'sensitive_file' || finding.category === 'metadata_ssrf')
457
509
  return finding;
458
510
  }
459
511
  for (const candidate of candidates) {
@@ -876,8 +876,19 @@ class McpGatewayServer {
876
876
  }
877
877
  }
878
878
  else {
879
- const reason = preflight.actionPolicy?.reason || preflight.intentEvaluation?.reasons?.[0] || `Tool call ${preflight.decision}.`;
880
- throw new Error(reason);
879
+ // Detailed, self-explanatory block message: WHICH policy fired, WHAT rule matched,
880
+ // and where to review it — so the developer (and the AI agent relaying the error)
881
+ // can tell exactly why the call was refused instead of guessing.
882
+ const policy = preflight.actionPolicy;
883
+ const parts = [];
884
+ parts.push(policy?.reason || preflight.intentEvaluation?.reasons?.[0] || `Tool call ${preflight.decision} by FullCourtDefense.`);
885
+ if (policy?.policyName)
886
+ parts.push(`Policy: "${policy.policyName}"`);
887
+ if (policy?.matchedRule)
888
+ parts.push(`Matched rule: ${policy.matchedRule}`);
889
+ parts.push(`Tool: ${toolName}${operation && operation !== toolName ? ` (operation: ${operation})` : ''}`);
890
+ parts.push(`Review or change this policy in the FullCourtDefense console → Agent Security → Policies.`);
891
+ throw new Error(parts.join(' | '));
881
892
  }
882
893
  }
883
894
  const rawResult = await this.downstream.callTool(toolName, toolArgs);
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.21.21"
2
+ "version": "1.21.23"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.21.21",
3
+ "version": "1.21.23",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "scripts": {
16
16
  "build": "tsc && node scripts/copy-attack-corpus.js",
17
17
  "test:deterministic-guard": "npm run build && node scripts/test-deterministic-guard.js",
18
+ "test:guard-content-context": "npm run build && node scripts/test-guard-content-context.js",
18
19
  "test:browser-credentials-rule": "npm run build && node scripts/test-browser-credentials-rule.js",
19
20
  "test:taint-ledger": "npm run build && node scripts/test-taint-ledger.js",
20
21
  "test:shell-audit": "npm run build && node scripts/test-shell-audit.js",