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
|
|
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
|
-
|
|
880
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.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",
|