fullcourtdefense-cli 1.0.2 → 1.1.0
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.
- package/README.md +4 -4
- package/dist/api.d.ts +1 -0
- package/dist/commands/credits.js +2 -2
- package/dist/commands/discover.d.ts +11 -0
- package/dist/commands/discover.js +283 -0
- package/dist/commands/hook.d.ts +22 -0
- package/dist/commands/hook.js +312 -0
- package/dist/commands/init.js +14 -14
- package/dist/commands/installCursorHook.d.ts +12 -0
- package/dist/commands/installCursorHook.js +186 -0
- package/dist/commands/local-scan.d.ts +3 -0
- package/dist/commands/local-scan.js +908 -8
- package/dist/commands/scan.d.ts +3 -0
- package/dist/commands/scan.js +5 -2
- package/dist/index.js +80 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ fullcourtdefense init
|
|
|
57
57
|
Create a `.fullcourtdefense.yml` to avoid passing flags every time:
|
|
58
58
|
|
|
59
59
|
```yaml
|
|
60
|
-
apiKey: ${
|
|
60
|
+
apiKey: ${FULLCOURTDEFENSE_API_KEY}
|
|
61
61
|
apiUrl: https://api.fullcourtdefense.ai
|
|
62
62
|
shieldId: sh_your_shield_id
|
|
63
63
|
# shieldKey: shsk_optional_if_locked
|
|
@@ -108,7 +108,7 @@ Local scans run from the customer machine or VPN and only send captured text out
|
|
|
108
108
|
|
|
109
109
|
| Flag | Applies To | Description | Default |
|
|
110
110
|
|---|---|---|---|
|
|
111
|
-
| `--api-key <key>` | hosted scan/credits | Hosted scan API key. Can also use `BOTGUARD_API_KEY`. | config/env |
|
|
111
|
+
| `--api-key <key>` | hosted scan/credits | Hosted scan API key. Can also use `FULLCOURTDEFENSE_API_KEY` or legacy `BOTGUARD_API_KEY`. | config/env |
|
|
112
112
|
| `--shield-id <id>` | local scan | Shield ID from the Shield Integrate tab. Can also use `FULLCOURTDEFENSE_SHIELD_ID`, `FCD_SHIELD_ID`, or `AGENTGUARD_SHIELD_ID`. | config/env/prompt |
|
|
113
113
|
| `--shield-key <key>` | local scan | Optional Shield key for locked Shields. Can also use `FULLCOURTDEFENSE_SHIELD_KEY`, `FCD_SHIELD_KEY`, or `AGENTGUARD_SHIELD_KEY`. | config/env/prompt |
|
|
114
114
|
|
|
@@ -206,7 +206,7 @@ Checked: https://api.fullcourtdefense.ai/api/health/ping
|
|
|
206
206
|
Use hosted scans when the agent endpoint is reachable by FullCourtDefense and you have a CI/CD API key.
|
|
207
207
|
|
|
208
208
|
```powershell
|
|
209
|
-
$env:
|
|
209
|
+
$env:FULLCOURTDEFENSE_API_KEY = "bg_live_..."
|
|
210
210
|
fullcourtdefense scan --endpoint "https://support-bot.example.com/chat" --description "Customer support chatbot" --mode sync --format summary --fail-threshold 80
|
|
211
211
|
```
|
|
212
212
|
|
|
@@ -345,7 +345,7 @@ fullcourtdefense scan --local --type endpoint --endpoint "https://agent.internal
|
|
|
345
345
|
Fail the build if score is below 80:
|
|
346
346
|
|
|
347
347
|
```powershell
|
|
348
|
-
fullcourtdefense scan --api-key "$env:
|
|
348
|
+
fullcourtdefense scan --api-key "$env:FULLCOURTDEFENSE_API_KEY" --endpoint "https://agent.example.com/chat" --description "Production support agent" --mode sync --format summary --fail-threshold 80
|
|
349
349
|
```
|
|
350
350
|
|
|
351
351
|
Local CI against a service started earlier in the job:
|
package/dist/api.d.ts
CHANGED
package/dist/commands/credits.js
CHANGED
|
@@ -4,9 +4,9 @@ exports.creditsCommand = creditsCommand;
|
|
|
4
4
|
const api_1 = require("../api");
|
|
5
5
|
const output_1 = require("../output");
|
|
6
6
|
async function creditsCommand(args, config) {
|
|
7
|
-
const apiKey = args.apiKey || config.apiKey || process.env.BOTGUARD_API_KEY;
|
|
7
|
+
const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY || process.env.BOTGUARD_API_KEY;
|
|
8
8
|
if (!apiKey) {
|
|
9
|
-
console.error('Error: API key required. Use --api-key,
|
|
9
|
+
console.error('Error: API key required. Use --api-key, FULLCOURTDEFENSE_API_KEY env var, or .fullcourtdefense.yml');
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
const api = new api_1.BotGuardApi(apiKey, args.apiUrl || config.apiUrl);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BotGuardConfig } from '../config';
|
|
2
|
+
export interface DiscoverArgs {
|
|
3
|
+
type?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
json?: string;
|
|
7
|
+
upload?: string;
|
|
8
|
+
connectorName?: string;
|
|
9
|
+
extraPath?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function discoverCommand(args: DiscoverArgs, config: BotGuardConfig): Promise<void>;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.discoverCommand = discoverCommand;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
|
|
41
|
+
const SECRET_VALUE = /sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{20,}/;
|
|
42
|
+
const SECRET_KEY = /key|token|secret|password|credential|api[_-]?key/i;
|
|
43
|
+
/** Where MCP client configs live, by client + OS. */
|
|
44
|
+
function candidateConfigPaths(cwd, extra) {
|
|
45
|
+
const home = os.homedir();
|
|
46
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
47
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
48
|
+
const list = [
|
|
49
|
+
// Cursor
|
|
50
|
+
{ path: path.join(home, '.cursor', 'mcp.json'), source: 'Cursor (global)' },
|
|
51
|
+
{ path: path.join(cwd, '.cursor', 'mcp.json'), source: 'Cursor (project)' },
|
|
52
|
+
// Claude Desktop
|
|
53
|
+
{ path: path.join(appData, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (win)' },
|
|
54
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (mac)' },
|
|
55
|
+
{ path: path.join(xdg, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (linux)' },
|
|
56
|
+
// VS Code (project + user settings)
|
|
57
|
+
{ path: path.join(cwd, '.vscode', 'mcp.json'), source: 'VS Code (project)' },
|
|
58
|
+
{ path: path.join(appData, 'Code', 'User', 'settings.json'), source: 'VS Code (user, win)' },
|
|
59
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), source: 'VS Code (user, mac)' },
|
|
60
|
+
{ path: path.join(xdg, 'Code', 'User', 'settings.json'), source: 'VS Code (user, linux)' },
|
|
61
|
+
// Windsurf
|
|
62
|
+
{ path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), source: 'Windsurf' },
|
|
63
|
+
// Generic / repo-root
|
|
64
|
+
{ path: path.join(cwd, '.mcp.json'), source: 'Repo (.mcp.json)' },
|
|
65
|
+
{ path: path.join(cwd, 'mcp.json'), source: 'Repo (mcp.json)' },
|
|
66
|
+
];
|
|
67
|
+
if (extra)
|
|
68
|
+
list.push({ path: path.resolve(extra), source: 'Custom path' });
|
|
69
|
+
return list;
|
|
70
|
+
}
|
|
71
|
+
/** MCP servers can be declared under several keys across clients. */
|
|
72
|
+
function extractServerMap(parsed) {
|
|
73
|
+
if (!parsed || typeof parsed !== 'object')
|
|
74
|
+
return {};
|
|
75
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === 'object')
|
|
76
|
+
return parsed.mcpServers;
|
|
77
|
+
if (parsed.servers && typeof parsed.servers === 'object')
|
|
78
|
+
return parsed.servers;
|
|
79
|
+
if (parsed.mcp?.servers && typeof parsed.mcp.servers === 'object')
|
|
80
|
+
return parsed.mcp.servers;
|
|
81
|
+
if (parsed['mcp.servers'] && typeof parsed['mcp.servers'] === 'object')
|
|
82
|
+
return parsed['mcp.servers'];
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
const RISK_RULES = [
|
|
86
|
+
{ test: /shell|bash|\bexec\b|command[-_]?runner|terminal|subprocess|run[-_]?admin/i, level: 'critical', tag: 'shell/exec' },
|
|
87
|
+
{ test: /postgres|mysql|mongo|sqlite|\bsql\b|database|\bdb\b|redis|snowflake|bigquery/i, level: 'critical', tag: 'database' },
|
|
88
|
+
{ test: /stripe|payment|refund|payout|invoice|billing/i, level: 'critical', tag: 'payments' },
|
|
89
|
+
{ test: /filesystem|file[-_]?system|\bfs\b|read[-_]?file|write[-_]?file/i, level: 'high', tag: 'filesystem' },
|
|
90
|
+
{ test: /github|gitlab|bitbucket/i, level: 'high', tag: 'source-control' },
|
|
91
|
+
{ test: /aws|gcp|azure|cloud|kubernetes|k8s|terraform/i, level: 'high', tag: 'cloud-infra' },
|
|
92
|
+
{ test: /slack|email|gmail|smtp|sendgrid|twilio|notification/i, level: 'high', tag: 'messaging' },
|
|
93
|
+
{ test: /browser|puppeteer|playwright|fetch|http[-_]?request|web[-_]?search/i, level: 'medium', tag: 'web/network' },
|
|
94
|
+
{ test: /drive|gdrive|dropbox|s3|storage|notion|confluence/i, level: 'medium', tag: 'data-store' },
|
|
95
|
+
];
|
|
96
|
+
function scoreRisk(s) {
|
|
97
|
+
const hay = [s.serverName, s.command || '', ...(s.args || []), s.url || ''].join(' ');
|
|
98
|
+
const tags = [];
|
|
99
|
+
let level = 'low';
|
|
100
|
+
const order = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
101
|
+
for (const rule of RISK_RULES) {
|
|
102
|
+
if (rule.test.test(hay)) {
|
|
103
|
+
tags.push(rule.tag);
|
|
104
|
+
if (order[rule.level] > order[level])
|
|
105
|
+
level = rule.level;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { level, tags };
|
|
109
|
+
}
|
|
110
|
+
function parseConfigFile(filePath, source) {
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return []; // settings.json with comments/JSONC — best-effort skip
|
|
124
|
+
}
|
|
125
|
+
const servers = extractServerMap(parsed);
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const [name, cfgRaw] of Object.entries(servers)) {
|
|
128
|
+
const cfg = (cfgRaw || {});
|
|
129
|
+
const command = typeof cfg.command === 'string' ? cfg.command : undefined;
|
|
130
|
+
const args = Array.isArray(cfg.args) ? cfg.args.map(String) : undefined;
|
|
131
|
+
const url = typeof cfg.url === 'string' ? cfg.url : (typeof cfg.serverUrl === 'string' ? cfg.serverUrl : undefined);
|
|
132
|
+
const env = (cfg.env && typeof cfg.env === 'object') ? cfg.env : {};
|
|
133
|
+
const envKeys = Object.keys(env);
|
|
134
|
+
const transport = url
|
|
135
|
+
? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
|
|
136
|
+
: command ? 'stdio' : 'unknown';
|
|
137
|
+
const tools = Array.isArray(cfg.tools)
|
|
138
|
+
? cfg.tools.map((t) => (typeof t === 'string' ? t : t?.name)).filter(Boolean).map(String)
|
|
139
|
+
: [];
|
|
140
|
+
const { level, tags } = scoreRisk({ serverName: name, command, args, url });
|
|
141
|
+
const warnings = [];
|
|
142
|
+
const blob = `${command || ''} ${(args || []).join(' ')} ${JSON.stringify(env)}`;
|
|
143
|
+
if (SECRET_VALUE.test(blob))
|
|
144
|
+
warnings.push('Hardcoded secret/API token detected in config (rotate + use env var).');
|
|
145
|
+
for (const [k, v] of Object.entries(env)) {
|
|
146
|
+
const val = String(v ?? '');
|
|
147
|
+
if (SECRET_KEY.test(k) && val && !/^\$\{?[A-Za-z0-9_]+\}?$/.test(val)) {
|
|
148
|
+
warnings.push(`Env "${k}" appears to contain a literal secret value.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
out.push({
|
|
152
|
+
serverName: name,
|
|
153
|
+
command,
|
|
154
|
+
args,
|
|
155
|
+
url,
|
|
156
|
+
transport,
|
|
157
|
+
envKeys,
|
|
158
|
+
tools,
|
|
159
|
+
source,
|
|
160
|
+
configPath: filePath,
|
|
161
|
+
riskLevel: level,
|
|
162
|
+
riskTags: tags,
|
|
163
|
+
warnings,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
/** Merge servers found in multiple files; keep richest record, union sources. */
|
|
169
|
+
function dedupe(servers) {
|
|
170
|
+
const byName = new Map();
|
|
171
|
+
for (const s of servers) {
|
|
172
|
+
const existing = byName.get(s.serverName);
|
|
173
|
+
if (!existing) {
|
|
174
|
+
byName.set(s.serverName, { ...s, source: s.source });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (!existing.source.includes(s.source))
|
|
178
|
+
existing.source += `, ${s.source}`;
|
|
179
|
+
existing.command = existing.command || s.command;
|
|
180
|
+
existing.url = existing.url || s.url;
|
|
181
|
+
existing.tools = Array.from(new Set([...existing.tools, ...s.tools]));
|
|
182
|
+
existing.warnings = Array.from(new Set([...existing.warnings, ...s.warnings]));
|
|
183
|
+
}
|
|
184
|
+
return [...byName.values()];
|
|
185
|
+
}
|
|
186
|
+
const COLOR = {
|
|
187
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
188
|
+
red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
189
|
+
};
|
|
190
|
+
function riskColor(level) {
|
|
191
|
+
return level === 'critical' || level === 'high' ? COLOR.red : level === 'medium' ? COLOR.yellow : COLOR.green;
|
|
192
|
+
}
|
|
193
|
+
async function upload(servers, apiUrl, apiKey, connectorName) {
|
|
194
|
+
const payload = {
|
|
195
|
+
connectorName: connectorName || 'Local MCP discovery (CLI)',
|
|
196
|
+
servers: servers.map(s => ({
|
|
197
|
+
serverName: s.serverName,
|
|
198
|
+
command: s.command,
|
|
199
|
+
url: s.url,
|
|
200
|
+
tools: s.tools.map(name => ({ name })),
|
|
201
|
+
})),
|
|
202
|
+
};
|
|
203
|
+
const resp = await fetch(`${apiUrl.replace(/\/$/, '')}/api/cicd/discovery/mcp`, {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
|
|
206
|
+
body: JSON.stringify(payload),
|
|
207
|
+
});
|
|
208
|
+
const data = await resp.json().catch(() => ({}));
|
|
209
|
+
if (!resp.ok || data.success === false) {
|
|
210
|
+
throw new Error(data.error || `Upload failed (${resp.status})`);
|
|
211
|
+
}
|
|
212
|
+
const ingested = data.data?.ingested ?? servers.length;
|
|
213
|
+
console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) to your AI Inventory.${COLOR.reset}`);
|
|
214
|
+
}
|
|
215
|
+
async function discoverCommand(args, config) {
|
|
216
|
+
const type = (args.type || 'mcp').toLowerCase();
|
|
217
|
+
if (type !== 'mcp') {
|
|
218
|
+
console.error(`Unsupported discover type "${type}". Supported: mcp`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
const cwd = process.cwd();
|
|
222
|
+
const candidates = candidateConfigPaths(cwd, args.extraPath);
|
|
223
|
+
const scanned = [];
|
|
224
|
+
let found = [];
|
|
225
|
+
for (const c of candidates) {
|
|
226
|
+
if (!fs.existsSync(c.path))
|
|
227
|
+
continue;
|
|
228
|
+
scanned.push(`${c.source} → ${c.path}`);
|
|
229
|
+
found.push(...parseConfigFile(c.path, c.source));
|
|
230
|
+
}
|
|
231
|
+
found = dedupe(found);
|
|
232
|
+
if (args.json === 'true') {
|
|
233
|
+
console.log(JSON.stringify({ scannedFiles: scanned, servers: found }, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
console.log(`\n${COLOR.bold}${COLOR.cyan}MCP server discovery${COLOR.reset}`);
|
|
237
|
+
console.log(`${COLOR.gray}Scanned ${candidates.length} known config locations on this machine.${COLOR.reset}\n`);
|
|
238
|
+
if (scanned.length === 0) {
|
|
239
|
+
console.log(`${COLOR.yellow}No MCP config files found.${COLOR.reset}`);
|
|
240
|
+
console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop, VS Code, Windsurf, and repo-root configs.${COLOR.reset}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
console.log(`${COLOR.bold}Config files found:${COLOR.reset}`);
|
|
244
|
+
for (const s of scanned)
|
|
245
|
+
console.log(` ${COLOR.dim}•${COLOR.reset} ${s}`);
|
|
246
|
+
console.log('');
|
|
247
|
+
if (found.length === 0) {
|
|
248
|
+
console.log(`${COLOR.yellow}Config files exist but declare no MCP servers.${COLOR.reset}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
252
|
+
found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
|
|
253
|
+
console.log(`${COLOR.bold}${found.length} MCP server(s) discovered:${COLOR.reset}\n`);
|
|
254
|
+
for (const s of found) {
|
|
255
|
+
const col = riskColor(s.riskLevel);
|
|
256
|
+
const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
|
|
257
|
+
console.log(` ${col}${COLOR.bold}[${s.riskLevel.toUpperCase()}]${COLOR.reset} ${COLOR.bold}${s.serverName}${COLOR.reset} ${COLOR.gray}(${s.transport})${COLOR.reset}`);
|
|
258
|
+
console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
|
|
259
|
+
if (s.riskTags.length)
|
|
260
|
+
console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
|
|
261
|
+
if (s.tools.length)
|
|
262
|
+
console.log(` ${COLOR.gray}tools:${COLOR.reset} ${s.tools.slice(0, 12).join(', ')}${s.tools.length > 12 ? ' …' : ''}`);
|
|
263
|
+
console.log(` ${COLOR.gray}from:${COLOR.reset} ${s.source}`);
|
|
264
|
+
for (const w of s.warnings)
|
|
265
|
+
console.log(` ${COLOR.red}⚠ ${w}${COLOR.reset}`);
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
|
268
|
+
const counts = found.reduce((acc, s) => { acc[s.riskLevel] = (acc[s.riskLevel] || 0) + 1; return acc; }, {});
|
|
269
|
+
console.log(`${COLOR.bold}Summary:${COLOR.reset} ${counts.critical || 0} critical · ${counts.high || 0} high · ${counts.medium || 0} medium · ${counts.low || 0} low`);
|
|
270
|
+
if (args.upload === 'true') {
|
|
271
|
+
const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
|
|
272
|
+
const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
|
|
273
|
+
if (!apiKey) {
|
|
274
|
+
console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset} Set --api-key, FULLCOURTDEFENSE_API_KEY, or apiKey in config.`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
console.log('');
|
|
278
|
+
await upload(found, apiUrl, apiKey, args.connectorName);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.log(`${COLOR.gray}Run with --upload to push these into your AI Inventory.${COLOR.reset}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BotGuardConfig } from '../config';
|
|
2
|
+
/**
|
|
3
|
+
* `fullcourtdefense hook` — Cursor hook bridge.
|
|
4
|
+
*
|
|
5
|
+
* Cursor invokes this command for agent lifecycle events (beforeSubmitPrompt,
|
|
6
|
+
* beforeShellExecution, beforeMCPExecution, afterFileEdit, ...). Cursor pipes a
|
|
7
|
+
* JSON payload on stdin and reads a JSON verdict on stdout. Exit code 2 also
|
|
8
|
+
* blocks the action, which we use as the cross-event, cross-version-safe block
|
|
9
|
+
* signal (not every event documents a `permission` output field).
|
|
10
|
+
*
|
|
11
|
+
* Verdict source of truth: POST {apiUrl}/api/shield/analyze/{shieldId}.
|
|
12
|
+
*/
|
|
13
|
+
export interface HookArgs {
|
|
14
|
+
event?: string;
|
|
15
|
+
apiUrl?: string;
|
|
16
|
+
shieldId?: string;
|
|
17
|
+
shieldKey?: string;
|
|
18
|
+
shadow?: string;
|
|
19
|
+
failClosed?: string;
|
|
20
|
+
timeout?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function hookCommand(args: HookArgs, config: BotGuardConfig): Promise<void>;
|