fullcourtdefense-cli 1.7.17 → 1.7.19

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.
@@ -0,0 +1,13 @@
1
+ import { BotGuardConfig } from '../config';
2
+ export interface AgentCiArgs {
3
+ failOn?: string;
4
+ format?: string;
5
+ upload?: string;
6
+ requireUpload?: string;
7
+ requireGateway?: string;
8
+ requireApprovalFor?: string;
9
+ extraPath?: string;
10
+ apiKey?: string;
11
+ apiUrl?: string;
12
+ }
13
+ export declare function agentCiCommand(args: AgentCiArgs, config: BotGuardConfig): Promise<void>;
@@ -0,0 +1,311 @@
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.agentCiCommand = agentCiCommand;
37
+ const crypto = __importStar(require("crypto"));
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const config_1 = require("../config");
41
+ const discoverPaths_1 = require("./discoverPaths");
42
+ const discoverAgentFiles_1 = require("./discoverAgentFiles");
43
+ const discoverSecrets_1 = require("./discoverSecrets");
44
+ const discover_1 = require("./discover");
45
+ const SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
46
+ function stableId(parts) {
47
+ return crypto.createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
48
+ }
49
+ function parseJsonFile(filePath) {
50
+ try {
51
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function parseTomlMcpServers(content) {
58
+ const servers = {};
59
+ const lines = content.split(/\r?\n/);
60
+ let current = null;
61
+ for (const raw of lines) {
62
+ const line = raw.trim();
63
+ const section = line.match(/^\[mcp_servers\.([^\]]+)\]$/);
64
+ if (section) {
65
+ current = section[1].replace(/^["']|["']$/g, '');
66
+ servers[current] = {};
67
+ continue;
68
+ }
69
+ if (!current || !line || line.startsWith('#'))
70
+ continue;
71
+ const match = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
72
+ if (!match)
73
+ continue;
74
+ const key = match[1];
75
+ const rawValue = match[2].trim().replace(/,$/, '');
76
+ if (rawValue.startsWith('[')) {
77
+ servers[current][key] = rawValue
78
+ .replace(/^\[|\]$/g, '')
79
+ .split(',')
80
+ .map(part => part.trim().replace(/^["']|["']$/g, ''))
81
+ .filter(Boolean);
82
+ }
83
+ else {
84
+ servers[current][key] = rawValue.replace(/^["']|["']$/g, '');
85
+ }
86
+ }
87
+ return servers;
88
+ }
89
+ function extractServerMap(parsed) {
90
+ if (!parsed || typeof parsed !== 'object')
91
+ return {};
92
+ if (parsed.mcpServers && typeof parsed.mcpServers === 'object')
93
+ return parsed.mcpServers;
94
+ if (parsed.servers && typeof parsed.servers === 'object')
95
+ return parsed.servers;
96
+ if (parsed.mcp?.servers && typeof parsed.mcp.servers === 'object')
97
+ return parsed.mcp.servers;
98
+ if (parsed['mcp.servers'] && typeof parsed['mcp.servers'] === 'object')
99
+ return parsed['mcp.servers'];
100
+ return {};
101
+ }
102
+ function riskSignalsForServer(name, server) {
103
+ const text = [
104
+ name,
105
+ server?.command,
106
+ ...(Array.isArray(server?.args) ? server.args : []),
107
+ server?.url,
108
+ ].filter(Boolean).join(' ').toLowerCase();
109
+ const signals = [
110
+ [/\b(shell|exec|command|terminal|powershell|bash|cmd\.exe)\b/, 'shell'],
111
+ [/\b(file|filesystem|readfile|writefile|directory|path)\b/, 'filesystem'],
112
+ [/\b(database|postgres|mysql|mongo|sql|db)\b/, 'database'],
113
+ [/\b(secret|token|key|password|credential|env)\b/, 'secrets'],
114
+ [/\b(payment|wire|transfer|bank|iban|invoice|billing)\b/, 'payment'],
115
+ [/\b(customer|user|pii|ssn|email|phone|record)\b/, 'pii'],
116
+ [/\b(browser|web|http|fetch|request|webhook)\b/, 'network'],
117
+ [/\b(delete|remove|destroy|drop|wipe|purge|write|update|create)\b/, 'write'],
118
+ ];
119
+ return signals.filter(([re]) => re.test(text)).map(([, signal]) => signal);
120
+ }
121
+ function discoverMcpServers(cwd, extraPath) {
122
+ const out = [];
123
+ for (const candidate of (0, discoverPaths_1.discoverScanTargets)(cwd, extraPath)) {
124
+ if (!fs.existsSync(candidate.path))
125
+ continue;
126
+ const isToml = candidate.path.endsWith('.toml');
127
+ const parsed = isToml
128
+ ? parseTomlMcpServers(fs.readFileSync(candidate.path, 'utf8'))
129
+ : parseJsonFile(candidate.path);
130
+ const map = extractServerMap(parsed);
131
+ for (const [name, server] of Object.entries(map)) {
132
+ const cfg = server;
133
+ const command = typeof cfg.command === 'string' ? cfg.command : undefined;
134
+ const args = Array.isArray(cfg.args) ? cfg.args.map(String) : [];
135
+ const url = typeof cfg.url === 'string' ? cfg.url : typeof cfg.endpoint === 'string' ? cfg.endpoint : undefined;
136
+ const targetText = [command, ...args, url].filter(Boolean).join(' ');
137
+ const protectedGateway = /fullcourtdefense|agentguard|mcp-gateway|5066|fcd/i.test(targetText);
138
+ out.push({
139
+ name,
140
+ source: candidate.source,
141
+ filePath: candidate.path,
142
+ command,
143
+ args,
144
+ url,
145
+ direct: !protectedGateway,
146
+ protectedGateway,
147
+ riskSignals: riskSignalsForServer(name, cfg),
148
+ });
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+ function parseRequiredControls(value) {
154
+ return (value || 'payment,delete,shell,secrets')
155
+ .split(',')
156
+ .map(part => part.trim().toLowerCase())
157
+ .filter(Boolean);
158
+ }
159
+ function finding(input) {
160
+ return { ...input, id: stableId([input.category, input.title, input.filePath || '', input.detail]) };
161
+ }
162
+ function evaluateAgentGate(args) {
163
+ const cwd = process.cwd();
164
+ const mcpServers = discoverMcpServers(cwd, args.extraPath);
165
+ const agentFiles = (0, discoverAgentFiles_1.scanAgentFiles)({ cwd });
166
+ const secrets = (0, discoverSecrets_1.scanSecrets)({ cwd, maxEnvDepth: 3 });
167
+ const requireGateway = args.requireGateway !== 'false';
168
+ const requiredControls = parseRequiredControls(args.requireApprovalFor);
169
+ const findings = [];
170
+ for (const server of mcpServers) {
171
+ if (requireGateway && server.direct && server.riskSignals.length > 0) {
172
+ findings.push(finding({
173
+ severity: server.riskSignals.includes('payment') || server.riskSignals.includes('shell') || server.riskSignals.includes('secrets') ? 'critical' : 'high',
174
+ category: 'mcp',
175
+ title: `Unprotected MCP server: ${server.name}`,
176
+ detail: `${server.source} exposes ${server.riskSignals.join(', ')} without Full Court Defense gateway wrapping.`,
177
+ filePath: server.filePath,
178
+ remediation: 'Wrap this MCP server with fullcourtdefense protect-all or install-mcp-gateway before merging.',
179
+ }));
180
+ }
181
+ const missingControls = server.riskSignals.filter(signal => requiredControls.includes(signal));
182
+ if (missingControls.length > 0 && server.direct) {
183
+ findings.push(finding({
184
+ severity: 'high',
185
+ category: 'policy',
186
+ title: `Missing approval policy evidence for ${server.name}`,
187
+ detail: `Risky tool signals need approval/block policy before merge: ${missingControls.join(', ')}.`,
188
+ filePath: server.filePath,
189
+ remediation: 'Add an AgentGuard action policy requiring approval/block for this tool class, then route through the gateway.',
190
+ }));
191
+ }
192
+ }
193
+ for (const file of agentFiles.filter(item => item.riskTags.length > 0)) {
194
+ findings.push(finding({
195
+ severity: file.riskTags.includes('allows_shell') || file.riskTags.includes('broad_filesystem') ? 'high' : 'medium',
196
+ category: 'agent-file',
197
+ title: `Risky agent instruction: ${file.title}`,
198
+ detail: `${file.filePath} contains agent risk tags: ${file.riskTags.join(', ')}.`,
199
+ filePath: file.filePath,
200
+ remediation: 'Review the instruction/rule and add explicit least-privilege boundaries.',
201
+ }));
202
+ }
203
+ for (const secret of secrets.findings.filter(item => item.severity === 'critical' || item.severity === 'high')) {
204
+ findings.push(finding({
205
+ severity: secret.severity === 'critical' ? 'critical' : 'high',
206
+ category: 'secret',
207
+ title: secret.title,
208
+ detail: secret.detail,
209
+ filePath: secret.filePath,
210
+ remediation: secret.remediation,
211
+ }));
212
+ }
213
+ let score = 100;
214
+ for (const item of findings) {
215
+ score -= item.severity === 'critical' ? 25 : item.severity === 'high' ? 12 : item.severity === 'medium' ? 5 : 2;
216
+ }
217
+ return { findings: findings.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]), score: Math.max(0, score), mcpServers };
218
+ }
219
+ function shouldFail(findings, failOn) {
220
+ return findings.some(item => SEVERITY_RANK[item.severity] >= SEVERITY_RANK[failOn]);
221
+ }
222
+ function toSarif(findings) {
223
+ return {
224
+ version: '2.1.0',
225
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
226
+ runs: [{
227
+ tool: {
228
+ driver: {
229
+ name: 'Full Court Defense Agent CI Gate',
230
+ informationUri: 'https://fullcourtdefense.ai',
231
+ rules: findings.map(item => ({
232
+ id: item.id,
233
+ name: item.title,
234
+ shortDescription: { text: item.title },
235
+ fullDescription: { text: item.detail },
236
+ help: { text: item.remediation },
237
+ defaultConfiguration: {
238
+ level: item.severity === 'critical' || item.severity === 'high' ? 'error' : item.severity === 'medium' ? 'warning' : 'note',
239
+ },
240
+ })),
241
+ },
242
+ },
243
+ results: findings.map(item => ({
244
+ ruleId: item.id,
245
+ level: item.severity === 'critical' || item.severity === 'high' ? 'error' : item.severity === 'medium' ? 'warning' : 'note',
246
+ message: { text: `${item.title}: ${item.detail}` },
247
+ locations: item.filePath ? [{
248
+ physicalLocation: {
249
+ artifactLocation: { uri: path.relative(process.cwd(), item.filePath).replace(/\\/g, '/') },
250
+ region: { startLine: 1 },
251
+ },
252
+ }] : [],
253
+ })),
254
+ }],
255
+ };
256
+ }
257
+ function printSummary(report, failed, failOn) {
258
+ console.log(`Agent CI Gate: ${failed ? 'FAIL' : 'PASS'}`);
259
+ console.log(`Score: ${report.score}/100`);
260
+ console.log(`MCP servers: ${report.mcpServers.length}`);
261
+ console.log(`Findings: ${report.findings.length} (fail-on: ${failOn})`);
262
+ for (const item of report.findings.slice(0, 20)) {
263
+ console.log(`- [${item.severity.toUpperCase()}] ${item.title}`);
264
+ console.log(` ${item.detail}`);
265
+ if (item.filePath)
266
+ console.log(` ${path.relative(process.cwd(), item.filePath)}`);
267
+ }
268
+ if (report.findings.length > 20)
269
+ console.log(`... and ${report.findings.length - 20} more findings`);
270
+ }
271
+ async function agentCiCommand(args, config) {
272
+ const failOn = (['critical', 'high', 'medium', 'low'].includes(String(args.failOn || '').toLowerCase())
273
+ ? String(args.failOn).toLowerCase()
274
+ : 'high');
275
+ const format = (['summary', 'json', 'sarif'].includes(String(args.format || '').toLowerCase())
276
+ ? String(args.format).toLowerCase()
277
+ : 'summary');
278
+ const report = evaluateAgentGate(args);
279
+ const failed = shouldFail(report.findings, failOn);
280
+ if (format === 'json') {
281
+ console.log(JSON.stringify({ ...report, failed, failOn }, null, 2));
282
+ }
283
+ else if (format === 'sarif') {
284
+ console.log(JSON.stringify(toSarif(report.findings), null, 2));
285
+ }
286
+ else {
287
+ printSummary(report, failed, failOn);
288
+ }
289
+ const uploadRequested = args.upload === 'true' || args.requireUpload === 'true';
290
+ if (uploadRequested) {
291
+ const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
292
+ if (creds.apiKey || (creds.shieldId && creds.shieldKey)) {
293
+ try {
294
+ await (0, discover_1.runDiscoverUpload)({ apiKey: args.apiKey, apiUrl: args.apiUrl, silent: true }, config);
295
+ console.error('AgentGuard evidence upload: uploaded to AI Fleet / discovery posture inventory.');
296
+ }
297
+ catch (error) {
298
+ console.error(`AgentGuard evidence upload: failed (${error instanceof Error ? error.message : String(error)})`);
299
+ if (args.requireUpload === 'true')
300
+ process.exit(1);
301
+ }
302
+ }
303
+ else {
304
+ console.error('AgentGuard evidence upload: missing credentials. Run fullcourtdefense login or pass FULLCOURTDEFENSE_API_KEY.');
305
+ if (args.requireUpload === 'true')
306
+ process.exit(1);
307
+ }
308
+ }
309
+ if (failed)
310
+ process.exit(1);
311
+ }
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ const configure_1 = require("./commands/configure");
43
43
  const login_1 = require("./commands/login");
44
44
  const flushSpool_1 = require("./commands/flushSpool");
45
45
  const discover_1 = require("./commands/discover");
46
+ const agentCi_1 = require("./commands/agentCi");
46
47
  const hook_1 = require("./commands/hook");
47
48
  const installCursorHook_1 = require("./commands/installCursorHook");
48
49
  const mcpGateway_1 = require("./commands/mcpGateway");
@@ -125,6 +126,8 @@ function printHelp() {
125
126
  discover Finds MCP servers, local secrets, agent rules/skills, and machine
126
127
  blast radius on this laptop. Use --surface all --upload for AI Fleet.
127
128
  Use --schedule logon to upload inventory/posture at machine login.
129
+ agent-ci CI/CD gate for agents and tools only. Fails builds on risky MCP,
130
+ agent instruction, gateway, policy, or secret drift. Not a bot scan.
128
131
  install-cursor-hook
129
132
  Installs a Cursor hook so EVERY agent action on this machine is
130
133
  checked against your org's Action Policies — in any repo/folder.
@@ -218,6 +221,8 @@ function printHelp() {
218
221
  \x1b[1mRecommended Commands:\x1b[0m
219
222
  fullcourtdefense doctor
220
223
  fullcourtdefense configure
224
+ fullcourtdefense agent-ci --fail-on high --require-gateway --format summary
225
+ fullcourtdefense agent-ci --fail-on high --format sarif --upload
221
226
  fullcourtdefense scan --local
222
227
  fullcourtdefense scan --local --type mcp --mcp-command node --mcp-args ./server.js --mcp-tool all --mode full --format report
223
228
  fullcourtdefense scan --local --type mcp --mcp-url https://internal.company.com/mcp --mcp-auth-type bearer --mcp-token TOKEN --mcp-tool all --mode full --format report
@@ -256,6 +261,10 @@ function printHelp() {
256
261
  --rag-url <url> Live RAG HTTP endpoint to scan
257
262
  --shield-id <id> Shield ID for local outbound Shield verdicts
258
263
  --shield-key <key> Optional Shield key for locked Shields
264
+ --fail-on <severity> agent-ci fail threshold: critical, high, medium, low
265
+ --require-gateway agent-ci requires risky MCP servers to use FCD gateway
266
+ --require-approval-for agent-ci risky controls list, e.g. payment,delete,shell,secrets
267
+ --require-upload agent-ci fails if evidence cannot upload to AgentGuard UI
259
268
  --method <GET|POST> Local endpoint method
260
269
  --request-format <fmt> Local endpoint request format: custom or openai
261
270
  --auth-type <type> none, bearer, basic, api-key
@@ -489,6 +498,21 @@ async function main() {
489
498
  await (0, discover_1.discoverCommand)(args, config);
490
499
  break;
491
500
  }
501
+ case 'agent-ci': {
502
+ const args = {
503
+ failOn: flags['fail-on'],
504
+ format: flags.format,
505
+ upload: flags.upload,
506
+ requireUpload: flags['require-upload'],
507
+ requireGateway: flags['require-gateway'],
508
+ requireApprovalFor: flags['require-approval-for'],
509
+ extraPath: flags.path,
510
+ apiKey: flags['api-key'],
511
+ apiUrl: flags['api-url'],
512
+ };
513
+ await (0, agentCi_1.agentCiCommand)(args, config);
514
+ break;
515
+ }
492
516
  case 'install-cursor-hook': {
493
517
  const args = {
494
518
  project: flags.project,
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.7.17"
2
+ "version": "1.7.19"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.7.17",
3
+ "version": "1.7.19",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {