fullcourtdefense-cli 1.7.7 → 1.7.9
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.
|
@@ -61,7 +61,10 @@ async function installAllCommand(args, config) {
|
|
|
61
61
|
const discoverArgs = {
|
|
62
62
|
surface: 'all',
|
|
63
63
|
upload: 'true',
|
|
64
|
-
|
|
64
|
+
// Do not forward the saved org API key by default. `discover` treats a
|
|
65
|
+
// present apiKey as an explicit opt-in to the CICD API-key path, while
|
|
66
|
+
// the zero-paste customer flow should upload via the enrolled Shield key.
|
|
67
|
+
apiKey: args.apiKey,
|
|
65
68
|
apiUrl: creds.apiUrl,
|
|
66
69
|
userEmail: args.developerName,
|
|
67
70
|
};
|
|
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
|
|
|
43
43
|
const yaml_1 = require("yaml");
|
|
44
44
|
const output_1 = require("../output");
|
|
45
45
|
const config_1 = require("../config");
|
|
46
|
+
const discoverPaths_1 = require("./discoverPaths");
|
|
46
47
|
const DEFAULT_ATTACKS = [
|
|
47
48
|
{
|
|
48
49
|
id: 'local_prompt_injection_1',
|
|
@@ -954,6 +955,128 @@ function currentShieldConfig(args) {
|
|
|
954
955
|
apiUrl: (args.apiUrl || args.configApiUrl || config.apiUrl || process.env.FULLCOURTDEFENSE_API_URL || DEFAULT_API_URL).replace(/\/$/, ''),
|
|
955
956
|
};
|
|
956
957
|
}
|
|
958
|
+
function parseTomlString(value) {
|
|
959
|
+
const trimmed = value.trim();
|
|
960
|
+
if (!trimmed)
|
|
961
|
+
return undefined;
|
|
962
|
+
try {
|
|
963
|
+
return JSON.parse(trimmed);
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
return trimmed.replace(/^"|"$/g, '').replace(/^'|'$/g, '');
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function parseTomlStringArray(value) {
|
|
970
|
+
const trimmed = value.trim();
|
|
971
|
+
if (!trimmed)
|
|
972
|
+
return [];
|
|
973
|
+
try {
|
|
974
|
+
const parsed = JSON.parse(trimmed);
|
|
975
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
return trimmed.replace(/^\[|\]$/g, '').split(',').map(part => parseTomlString(part) || '').filter(Boolean);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function parseCodexMcpServers(raw) {
|
|
982
|
+
const servers = {};
|
|
983
|
+
let currentName = '';
|
|
984
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
985
|
+
const section = line.match(/^\s*\[mcp_servers\.([^\]]+)\]\s*$/);
|
|
986
|
+
if (section) {
|
|
987
|
+
currentName = section[1].replace(/^"|"$/g, '');
|
|
988
|
+
servers[currentName] = servers[currentName] || {};
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (!currentName)
|
|
992
|
+
continue;
|
|
993
|
+
const trimmed = line.trim();
|
|
994
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
995
|
+
continue;
|
|
996
|
+
const eq = trimmed.indexOf('=');
|
|
997
|
+
if (eq < 0)
|
|
998
|
+
continue;
|
|
999
|
+
const key = trimmed.slice(0, eq).trim();
|
|
1000
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
1001
|
+
if (key === 'command')
|
|
1002
|
+
servers[currentName].command = parseTomlString(value);
|
|
1003
|
+
else if (key === 'args')
|
|
1004
|
+
servers[currentName].args = parseTomlStringArray(value);
|
|
1005
|
+
else if (key === 'url' || key === 'serverUrl')
|
|
1006
|
+
servers[currentName][key] = parseTomlString(value);
|
|
1007
|
+
}
|
|
1008
|
+
return servers;
|
|
1009
|
+
}
|
|
1010
|
+
function extractMcpServerMap(parsed) {
|
|
1011
|
+
if (!parsed || typeof parsed !== 'object')
|
|
1012
|
+
return {};
|
|
1013
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === 'object')
|
|
1014
|
+
return parsed.mcpServers;
|
|
1015
|
+
if (parsed.servers && typeof parsed.servers === 'object')
|
|
1016
|
+
return parsed.servers;
|
|
1017
|
+
if (parsed.mcp?.servers && typeof parsed.mcp.servers === 'object')
|
|
1018
|
+
return parsed.mcp.servers;
|
|
1019
|
+
if (parsed['mcp.servers'] && typeof parsed['mcp.servers'] === 'object')
|
|
1020
|
+
return parsed['mcp.servers'];
|
|
1021
|
+
return {};
|
|
1022
|
+
}
|
|
1023
|
+
function gatewayArgValue(args, flag) {
|
|
1024
|
+
const index = args.indexOf(flag);
|
|
1025
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
1026
|
+
}
|
|
1027
|
+
function localMcpServerOptions() {
|
|
1028
|
+
const out = [];
|
|
1029
|
+
const seen = new Set();
|
|
1030
|
+
for (const target of (0, discoverPaths_1.discoverScanTargets)(process.cwd())) {
|
|
1031
|
+
if (!fs.existsSync(target.path))
|
|
1032
|
+
continue;
|
|
1033
|
+
let raw = '';
|
|
1034
|
+
try {
|
|
1035
|
+
raw = fs.readFileSync(target.path, 'utf8');
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
let parsed;
|
|
1041
|
+
try {
|
|
1042
|
+
parsed = JSON.parse(raw);
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
parsed = /\.toml$/i.test(target.path) || /codex/i.test(target.source)
|
|
1046
|
+
? { mcpServers: parseCodexMcpServers(raw) }
|
|
1047
|
+
: null;
|
|
1048
|
+
}
|
|
1049
|
+
const maps = [{ servers: extractMcpServerMap(parsed) }];
|
|
1050
|
+
if (/claude code/i.test(target.source) && parsed?.projects && typeof parsed.projects === 'object') {
|
|
1051
|
+
for (const [projectPath, projectConfig] of Object.entries(parsed.projects)) {
|
|
1052
|
+
maps.push({ label: path.basename(projectPath), servers: extractMcpServerMap(projectConfig) });
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
for (const item of maps) {
|
|
1056
|
+
for (const [name, cfg] of Object.entries(item.servers)) {
|
|
1057
|
+
const command = typeof cfg?.command === 'string' ? cfg.command : '';
|
|
1058
|
+
const args = Array.isArray(cfg?.args) ? cfg.args.map(String) : [];
|
|
1059
|
+
if (!command)
|
|
1060
|
+
continue;
|
|
1061
|
+
const protectedServer = args.includes('mcp-gateway') || `${command} ${args.join(' ')}`.toLowerCase().includes('mcp-gateway');
|
|
1062
|
+
const id = `${target.path}::${item.label || ''}::${name}`;
|
|
1063
|
+
if (seen.has(id))
|
|
1064
|
+
continue;
|
|
1065
|
+
seen.add(id);
|
|
1066
|
+
out.push({
|
|
1067
|
+
id,
|
|
1068
|
+
name: item.label ? `${name} (${item.label})` : name,
|
|
1069
|
+
source: target.source,
|
|
1070
|
+
command,
|
|
1071
|
+
args,
|
|
1072
|
+
protected: protectedServer,
|
|
1073
|
+
agentName: protectedServer ? gatewayArgValue(args, '--agent-name') : undefined,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return out.sort((a, b) => Number(b.protected) - Number(a.protected) || a.name.localeCompare(b.name));
|
|
1079
|
+
}
|
|
957
1080
|
function compactScanHtml(args) {
|
|
958
1081
|
const auth = currentShieldConfig(args);
|
|
959
1082
|
const isConfigured = Boolean(auth.shieldId && auth.shieldKey);
|
|
@@ -1078,6 +1201,10 @@ function compactScanHtml(args) {
|
|
|
1078
1201
|
</div>
|
|
1079
1202
|
<div id="runner-fields">
|
|
1080
1203
|
<div class="note">Run one MCP tool call from this machine. Use a protected proxy/gateway command or local MCP URL. Results stay local in this browser page.</div>
|
|
1204
|
+
<label>Saved/protected MCP server</label>
|
|
1205
|
+
<select id="runnerServer">
|
|
1206
|
+
<option value="">Manual connection...</option>
|
|
1207
|
+
</select>
|
|
1081
1208
|
<div class="two">
|
|
1082
1209
|
<div>
|
|
1083
1210
|
<label>Protected MCP HTTP/SSE URL</label>
|
|
@@ -1088,6 +1215,16 @@ function compactScanHtml(args) {
|
|
|
1088
1215
|
<input id="runnerTool" placeholder="lookup_customer" />
|
|
1089
1216
|
</div>
|
|
1090
1217
|
</div>
|
|
1218
|
+
<div class="two">
|
|
1219
|
+
<div>
|
|
1220
|
+
<label>Operation/action override</label>
|
|
1221
|
+
<input id="runnerOperation" placeholder="optional, e.g. read / write / delete / payment:write" />
|
|
1222
|
+
</div>
|
|
1223
|
+
<div>
|
|
1224
|
+
<label>Why use this?</label>
|
|
1225
|
+
<input value="Use when one tool supports multiple actions" readonly />
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1091
1228
|
<div class="two">
|
|
1092
1229
|
<div><label>Or stdio command</label><input id="runnerMcpCommand" placeholder="fullcourtdefense" /></div>
|
|
1093
1230
|
<div><label>Command args</label><input id="runnerMcpArgs" placeholder='mcp-gateway --mcp-command node --mcp-args "["./server.js"]"' /></div>
|
|
@@ -1249,6 +1386,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1249
1386
|
byId('runnerMcpCommand').value = qs.get('mcpCommand') || 'fullcourtdefense';
|
|
1250
1387
|
byId('runnerMcpArgs').value = qs.get('mcpArgs') || '';
|
|
1251
1388
|
byId('runnerTool').value = qs.get('mcpTool') || '';
|
|
1389
|
+
byId('runnerOperation').value = qs.get('operation') || '';
|
|
1252
1390
|
const payload = () => ({
|
|
1253
1391
|
target, mode: byId('mode').value, format: byId('format').value,
|
|
1254
1392
|
endpoint: byId('endpoint').value, inputField: byId('inputField').value, outputField: byId('outputField').value,
|
|
@@ -1260,8 +1398,30 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1260
1398
|
mcpCommand: byId('runnerMcpCommand').value,
|
|
1261
1399
|
mcpArgs: byId('runnerMcpArgs').value,
|
|
1262
1400
|
toolName: byId('runnerTool').value,
|
|
1401
|
+
operation: byId('runnerOperation').value,
|
|
1263
1402
|
toolArgs: byId('runnerToolArgs').value
|
|
1264
1403
|
});
|
|
1404
|
+
async function loadMcpServers() {
|
|
1405
|
+
try {
|
|
1406
|
+
const res = await fetch('/mcp/servers');
|
|
1407
|
+
const data = await res.json();
|
|
1408
|
+
const select = byId('runnerServer');
|
|
1409
|
+
const options = data.servers || [];
|
|
1410
|
+
select.innerHTML = '<option value="">Manual connection...</option>' + options.map((server, index) =>
|
|
1411
|
+
'<option value="' + index + '">' + htmlEscape((server.protected ? 'Protected: ' : 'Direct: ') + server.name + ' - ' + server.source + (server.agentName ? ' - ' + server.agentName : '')) + '</option>'
|
|
1412
|
+
).join('');
|
|
1413
|
+
select.onchange = () => {
|
|
1414
|
+
const server = options[Number(select.value)];
|
|
1415
|
+
if (!server) return;
|
|
1416
|
+
byId('runnerMcpUrl').value = '';
|
|
1417
|
+
byId('runnerMcpCommand').value = server.command || '';
|
|
1418
|
+
byId('runnerMcpArgs').value = Array.isArray(server.args) ? server.args.map(part => /\\s/.test(part) ? JSON.stringify(part) : part).join(' ') : '';
|
|
1419
|
+
setConsole('Loaded saved MCP server: ' + server.name + '\\n' + (server.protected ? 'This entry is already routed through FullCourtDefense gateway.' : 'Direct MCP entry. Wrap it with protect-all for policy enforcement.'), 'Server loaded');
|
|
1420
|
+
};
|
|
1421
|
+
} catch {
|
|
1422
|
+
// Local server picker is convenience only; manual fields still work.
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1265
1425
|
const renderCommand = p => {
|
|
1266
1426
|
const parts = ['fullcourtdefense scan --local --type', p.target, '--mode', p.mode, '--format', p.format, '--no-open'];
|
|
1267
1427
|
if (p.target === 'endpoint') parts.push('--endpoint', JSON.stringify(p.endpoint), '--method POST --request-format custom --input-field', p.inputField || 'message', '--output-field', p.outputField || 'response');
|
|
@@ -1334,6 +1494,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1334
1494
|
byId('runnerMcpCommand').value = 'fullcourtdefense';
|
|
1335
1495
|
byId('runnerMcpArgs').value = '';
|
|
1336
1496
|
byId('runnerTool').value = '';
|
|
1497
|
+
byId('runnerOperation').value = '';
|
|
1337
1498
|
byId('runnerToolArgs').value = '{\\n "customerId": "cus_123"\\n}';
|
|
1338
1499
|
byId('mcpResultCard').classList.add('hidden');
|
|
1339
1500
|
byId('mcpResultJson').value = '';
|
|
@@ -1399,6 +1560,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
|
|
|
1399
1560
|
return data.ok;
|
|
1400
1561
|
};
|
|
1401
1562
|
byId('checkBackend').onclick = checkBackend;
|
|
1563
|
+
loadMcpServers();
|
|
1402
1564
|
checkBackend().catch(() => {});
|
|
1403
1565
|
function mcpRows(value) {
|
|
1404
1566
|
if (Array.isArray(value)) return value;
|
|
@@ -1674,6 +1836,7 @@ async function runMcpToolFromBrowser(body) {
|
|
|
1674
1836
|
const rawCommand = typeof body.mcpCommand === 'string' ? body.mcpCommand.trim() : '';
|
|
1675
1837
|
const rawArgs = typeof body.mcpArgs === 'string' ? body.mcpArgs.trim() : '';
|
|
1676
1838
|
const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
|
|
1839
|
+
const operation = typeof body.operation === 'string' && body.operation.trim() ? body.operation.trim() : undefined;
|
|
1677
1840
|
if (!toolName)
|
|
1678
1841
|
throw new Error('Tool name is required.');
|
|
1679
1842
|
if (!rawUrl && !rawCommand)
|
|
@@ -1695,7 +1858,7 @@ async function runMcpToolFromBrowser(body) {
|
|
|
1695
1858
|
const startedAt = Date.now();
|
|
1696
1859
|
try {
|
|
1697
1860
|
await client.initialize();
|
|
1698
|
-
const result = await client.callTool(toolName, toolArgs);
|
|
1861
|
+
const result = await client.callTool(toolName, toolArgs, operation);
|
|
1699
1862
|
const errorResult = parseMcpErrorResult(result);
|
|
1700
1863
|
if (errorResult) {
|
|
1701
1864
|
return {
|
|
@@ -1703,6 +1866,7 @@ async function runMcpToolFromBrowser(body) {
|
|
|
1703
1866
|
blocked: errorResult.blocked,
|
|
1704
1867
|
toolName,
|
|
1705
1868
|
transport,
|
|
1869
|
+
operation,
|
|
1706
1870
|
destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
|
|
1707
1871
|
elapsedMs: Date.now() - startedAt,
|
|
1708
1872
|
reason: errorResult.reason,
|
|
@@ -1714,6 +1878,7 @@ async function runMcpToolFromBrowser(body) {
|
|
|
1714
1878
|
success: true,
|
|
1715
1879
|
toolName,
|
|
1716
1880
|
transport,
|
|
1881
|
+
operation,
|
|
1717
1882
|
destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
|
|
1718
1883
|
elapsedMs: Date.now() - startedAt,
|
|
1719
1884
|
result,
|
|
@@ -1725,6 +1890,7 @@ async function runMcpToolFromBrowser(body) {
|
|
|
1725
1890
|
blocked: /policy|blocked|approval|not allowed|denied/i.test(error instanceof Error ? error.message : String(error)),
|
|
1726
1891
|
toolName,
|
|
1727
1892
|
transport,
|
|
1893
|
+
operation,
|
|
1728
1894
|
destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
|
|
1729
1895
|
elapsedMs: Date.now() - startedAt,
|
|
1730
1896
|
reason: error instanceof Error ? error.message : String(error),
|
|
@@ -1754,6 +1920,11 @@ function startCompactWebServer(args) {
|
|
|
1754
1920
|
}));
|
|
1755
1921
|
return;
|
|
1756
1922
|
}
|
|
1923
|
+
if (req.method === 'GET' && url.pathname === '/mcp/servers') {
|
|
1924
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
1925
|
+
res.end(JSON.stringify({ servers: localMcpServerOptions() }));
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1757
1928
|
if (req.method === 'POST' && url.pathname === '/auth/save') {
|
|
1758
1929
|
const body = await readRequestBody(req);
|
|
1759
1930
|
const shieldId = typeof body.shieldId === 'string' ? body.shieldId.trim() : '';
|
|
@@ -2012,8 +2183,8 @@ class StdioMcpClient {
|
|
|
2012
2183
|
});
|
|
2013
2184
|
this.notify('notifications/initialized', {});
|
|
2014
2185
|
}
|
|
2015
|
-
async callTool(name, args) {
|
|
2016
|
-
return this.request('tools/call', { name, arguments: args });
|
|
2186
|
+
async callTool(name, args, operation) {
|
|
2187
|
+
return this.request('tools/call', { name, arguments: args, ...(operation ? { operation } : {}) });
|
|
2017
2188
|
}
|
|
2018
2189
|
async listTools() {
|
|
2019
2190
|
const result = await this.request('tools/list', {});
|
|
@@ -2150,8 +2321,8 @@ class HttpMcpClient {
|
|
|
2150
2321
|
});
|
|
2151
2322
|
await this.notify('notifications/initialized', {});
|
|
2152
2323
|
}
|
|
2153
|
-
async callTool(name, args) {
|
|
2154
|
-
return this.request('tools/call', { name, arguments: args });
|
|
2324
|
+
async callTool(name, args, operation) {
|
|
2325
|
+
return this.request('tools/call', { name, arguments: args, ...(operation ? { operation } : {}) });
|
|
2155
2326
|
}
|
|
2156
2327
|
async listTools() {
|
|
2157
2328
|
const result = await this.request('tools/list', {});
|
|
@@ -2244,8 +2415,8 @@ class SseMcpClient {
|
|
|
2244
2415
|
});
|
|
2245
2416
|
await this.notify('notifications/initialized', {});
|
|
2246
2417
|
}
|
|
2247
|
-
async callTool(name, args) {
|
|
2248
|
-
return this.request('tools/call', { name, arguments: args });
|
|
2418
|
+
async callTool(name, args, operation) {
|
|
2419
|
+
return this.request('tools/call', { name, arguments: args, ...(operation ? { operation } : {}) });
|
|
2249
2420
|
}
|
|
2250
2421
|
async listTools() {
|
|
2251
2422
|
const result = await this.request('tools/list', {});
|
|
@@ -588,12 +588,12 @@ class McpGatewayServer {
|
|
|
588
588
|
throw new Error('Missing MCP tool name.');
|
|
589
589
|
const toolArgs = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
590
590
|
let approvalActionId;
|
|
591
|
-
let operation;
|
|
591
|
+
let operation = typeof params.operation === 'string' && params.operation.trim() ? params.operation.trim() : undefined;
|
|
592
592
|
try {
|
|
593
593
|
const localBlock = (0, deterministicGuard_1.scanDeterministicToolCall)(toolName, toolArgs);
|
|
594
594
|
if (localBlock)
|
|
595
595
|
throw new Error(`${localBlock.reason} (${localBlock.ruleId}: ${localBlock.evidence})`);
|
|
596
|
-
const preflight = await this.api.checkToolCall({ toolName, toolArgs });
|
|
596
|
+
const preflight = await this.api.checkToolCall({ toolName, operation, toolArgs });
|
|
597
597
|
operation = preflight.operation;
|
|
598
598
|
if (!preflight.allowed) {
|
|
599
599
|
if (preflight.decision === 'approval' && this.gatewayConfig.approvalMode === 'wait') {
|
package/dist/version.json
CHANGED