fullcourtdefense-cli 1.15.0 → 1.15.1
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 +15 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +22 -0
- package/dist/commands/onboard.d.ts +10 -0
- package/dist/commands/onboard.js +182 -12
- package/dist/commands/onboardingJournal.d.ts +20 -0
- package/dist/commands/onboardingJournal.js +100 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +44 -1
- package/dist/index.js +11 -0
- package/dist/integrity.d.ts +21 -0
- package/dist/integrity.js +115 -0
- package/dist/telemetry.d.ts +3 -0
- package/dist/telemetry.js +2 -0
- package/dist/version.json +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -26,6 +26,20 @@ fullcourtdefense onboard --token <fleet-enrollment-token>
|
|
|
26
26
|
|
|
27
27
|
Then restart your AI clients (Cursor, Claude, VS Code, ...) so they pick up the wrapped configs. The machine reports to your org's AI Fleet in monitor-first mode until an admin enables enforcement.
|
|
28
28
|
|
|
29
|
+
Onboarding is a resumable local transaction. Its non-secret status journal is
|
|
30
|
+
stored at `~/.fullcourtdefense/onboarding.json`; use `--resume` after an
|
|
31
|
+
interruption and `--repair` to re-run completed protection installation steps.
|
|
32
|
+
Discovery upload and its daily schedule are recorded as optional telemetry,
|
|
33
|
+
separate from required protection. Useful MDM options:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
fullcourtdefense onboard --dry-run true --json true # preflight + final JSON status
|
|
37
|
+
fullcourtdefense onboard --resume # continue a failed transaction
|
|
38
|
+
fullcourtdefense onboard --resume --repair # reinstall protection surfaces
|
|
39
|
+
fullcourtdefense onboard --schedule false # do not install daily discovery
|
|
40
|
+
fullcourtdefense onboard --no-daemon true # do not enable optional self-healing
|
|
41
|
+
```
|
|
42
|
+
|
|
29
43
|
Manual step-by-step equivalent:
|
|
30
44
|
|
|
31
45
|
```bash
|
|
@@ -57,7 +71,7 @@ fullcourtdefense init
|
|
|
57
71
|
## Command Guide
|
|
58
72
|
|
|
59
73
|
- `fullcourtdefense help` — shows the full onboarding flow and command reference.
|
|
60
|
-
- `fullcourtdefense onboard --token <token>` —
|
|
74
|
+
- `fullcourtdefense onboard --token <token>` — resumable machine transaction: preflight + login + protection + optional discovery + verification. `--json true` prints final machine-readable status; exits non-zero only when a critical step fails.
|
|
61
75
|
- `fullcourtdefense doctor` — confirms outbound HTTPS to FullCourtDefense is open before scanning.
|
|
62
76
|
- `fullcourtdefense login --token <token>` — enrolls this machine with a fleet token and saves per-machine Shield credentials (no copy/paste).
|
|
63
77
|
- `fullcourtdefense install-all` — wraps every configured MCP server, installs IDE hooks and terminal guards, uploads discovery, schedules daily rescans.
|
|
@@ -7,4 +7,6 @@ export interface DaemonArgs extends ProtectAllArgs {
|
|
|
7
7
|
/** Suppress OS toasts (still logs). */
|
|
8
8
|
quiet?: string;
|
|
9
9
|
}
|
|
10
|
+
/** Whether the daemon has been registered to start automatically. */
|
|
11
|
+
export declare function isDaemonAutostartInstalled(): boolean;
|
|
10
12
|
export declare function daemonCommand(args: DaemonArgs, config: BotGuardConfig): Promise<void>;
|
package/dist/commands/daemon.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isDaemonAutostartInstalled = isDaemonAutostartInstalled;
|
|
36
37
|
exports.daemonCommand = daemonCommand;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const os = __importStar(require("os"));
|
|
@@ -43,6 +44,7 @@ const mcpGateway_1 = require("./mcpGateway");
|
|
|
43
44
|
const runtimeConfig_1 = require("../runtimeConfig");
|
|
44
45
|
const telemetry_1 = require("../telemetry");
|
|
45
46
|
const notify_1 = require("../notify");
|
|
47
|
+
const integrity_1 = require("../integrity");
|
|
46
48
|
const COLOR = {
|
|
47
49
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
48
50
|
red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
@@ -266,14 +268,20 @@ async function runDaemon(args, config) {
|
|
|
266
268
|
if (!creds.shieldId)
|
|
267
269
|
return;
|
|
268
270
|
try {
|
|
271
|
+
const integrity = (0, integrity_1.getLocalIntegrityReport)({ requireDaemon: true });
|
|
269
272
|
const result = await (0, telemetry_1.flushSpool)({
|
|
270
273
|
apiUrl: creds.apiUrl,
|
|
271
274
|
shieldId: creds.shieldId,
|
|
272
275
|
shieldKey: creds.shieldKey,
|
|
273
276
|
heartbeat: true,
|
|
277
|
+
integrityOk: integrity.ok,
|
|
278
|
+
integrityReasons: integrity.reasons,
|
|
279
|
+
integrityCheckedAt: integrity.checkedAt,
|
|
274
280
|
});
|
|
275
281
|
if (result && result.accepted > 0)
|
|
276
282
|
log(`Heartbeat: flushed ${result.accepted} spooled event(s).`);
|
|
283
|
+
if (!integrity.ok)
|
|
284
|
+
log(`Integrity warning: ${integrity.reasons.join(', ')}.`);
|
|
277
285
|
}
|
|
278
286
|
catch { /* spool stays on disk for the next tick */ }
|
|
279
287
|
};
|
|
@@ -387,6 +395,20 @@ function systemdUnitPath() {
|
|
|
387
395
|
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
388
396
|
return path.join(base, 'systemd', 'user', SYSTEMD_UNIT);
|
|
389
397
|
}
|
|
398
|
+
/** Whether the daemon has been registered to start automatically. */
|
|
399
|
+
function isDaemonAutostartInstalled() {
|
|
400
|
+
if (process.platform === 'win32') {
|
|
401
|
+
const result = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', TASK_NAME], {
|
|
402
|
+
stdio: 'ignore',
|
|
403
|
+
timeout: 5_000,
|
|
404
|
+
windowsHide: true,
|
|
405
|
+
});
|
|
406
|
+
return result.status === 0;
|
|
407
|
+
}
|
|
408
|
+
if (process.platform === 'darwin')
|
|
409
|
+
return fs.existsSync(launchdPlistPath());
|
|
410
|
+
return fs.existsSync(systemdUnitPath());
|
|
411
|
+
}
|
|
390
412
|
function installLinux() {
|
|
391
413
|
const unit = `[Unit]
|
|
392
414
|
Description=FullCourtDefense resident daemon (config watch + heartbeat)
|
|
@@ -3,6 +3,16 @@ import { InstallAllArgs } from './installAll';
|
|
|
3
3
|
export interface OnboardArgs extends InstallAllArgs {
|
|
4
4
|
/** Fleet enrollment token (or FCD_ENROLL_TOKEN env). Optional when already enrolled. */
|
|
5
5
|
token?: string;
|
|
6
|
+
/** Continue a persisted onboarding transaction. */
|
|
7
|
+
resume?: string;
|
|
8
|
+
/** Re-run completed install steps to repair changed machine state. */
|
|
9
|
+
repair?: string;
|
|
10
|
+
/** Report the transaction plan without changing local configuration. */
|
|
11
|
+
dryRun?: string;
|
|
12
|
+
/** Print the final transaction status as JSON. */
|
|
13
|
+
json?: string;
|
|
14
|
+
/** Do not enable optional self-healing protection. */
|
|
15
|
+
noDaemon?: string;
|
|
6
16
|
}
|
|
7
17
|
/**
|
|
8
18
|
* `fullcourtdefense onboard` — the one-command path from a fresh machine to a
|
package/dist/commands/onboard.js
CHANGED
|
@@ -40,6 +40,7 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const config_1 = require("../config");
|
|
41
41
|
const login_1 = require("./login");
|
|
42
42
|
const installAll_1 = require("./installAll");
|
|
43
|
+
const discover_1 = require("./discover");
|
|
43
44
|
const discoverPaths_1 = require("./discoverPaths");
|
|
44
45
|
const discoverSchedule_1 = require("./discoverSchedule");
|
|
45
46
|
const windowsAudit_1 = require("./windowsAudit");
|
|
@@ -47,6 +48,9 @@ const shellGuard_1 = require("./shellGuard");
|
|
|
47
48
|
const cmdGuard_1 = require("./cmdGuard");
|
|
48
49
|
const posixShellGuard_1 = require("./posixShellGuard");
|
|
49
50
|
const notify_1 = require("../notify");
|
|
51
|
+
const daemon_1 = require("./daemon");
|
|
52
|
+
const machineIdentity_1 = require("../machineIdentity");
|
|
53
|
+
const onboardingJournal_1 = require("./onboardingJournal");
|
|
50
54
|
const GREEN = '\x1b[32m';
|
|
51
55
|
const RED = '\x1b[31m';
|
|
52
56
|
const YELLOW = '\x1b[33m';
|
|
@@ -70,6 +74,41 @@ async function checkBackendReachable(apiUrl) {
|
|
|
70
74
|
return { label: 'Backend reachable', ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
71
75
|
}
|
|
72
76
|
}
|
|
77
|
+
async function checkEnrollmentCredential(apiUrl, token) {
|
|
78
|
+
try {
|
|
79
|
+
const identity = (0, machineIdentity_1.getMachineIdentity)();
|
|
80
|
+
const resp = await fetch(`${apiUrl}/api/cli/enroll/preflight`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json', 'x-fleet-token': token },
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
machineId: identity.machineId,
|
|
85
|
+
hostname: identity.hostname,
|
|
86
|
+
developerName: identity.developerName,
|
|
87
|
+
platform: identity.platform,
|
|
88
|
+
osFriendly: identity.osFriendly,
|
|
89
|
+
}),
|
|
90
|
+
signal: AbortSignal.timeout(10_000),
|
|
91
|
+
});
|
|
92
|
+
const body = await resp.json().catch(() => ({}));
|
|
93
|
+
if (resp.ok && body.success) {
|
|
94
|
+
const status = body.data?.enrollmentStatus === 'already_enrolled' ? 'existing device can be re-enrolled safely' : 'new device is eligible';
|
|
95
|
+
return { label: 'Enrollment credential', ok: true, detail: status };
|
|
96
|
+
}
|
|
97
|
+
return { label: 'Enrollment credential', ok: false, detail: body.error || `HTTP ${resp.status}` };
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return { label: 'Enrollment credential', ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function checkCompatibility() {
|
|
104
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] || '0', 10);
|
|
105
|
+
const supportedPlatform = ['win32', 'darwin', 'linux'].includes(process.platform);
|
|
106
|
+
if (nodeMajor < 18)
|
|
107
|
+
return { label: 'CLI compatibility', ok: false, detail: `Node.js ${process.versions.node}; Node 18+ is required` };
|
|
108
|
+
if (!supportedPlatform)
|
|
109
|
+
return { label: 'CLI compatibility', ok: false, detail: `${process.platform} is not supported for local protection` };
|
|
110
|
+
return { label: 'CLI compatibility', ok: true, detail: `Node ${process.versions.node} on ${process.platform}` };
|
|
111
|
+
}
|
|
73
112
|
function readTextSafe(file) {
|
|
74
113
|
try {
|
|
75
114
|
return fs.readFileSync(file, 'utf8');
|
|
@@ -154,40 +193,161 @@ function checkTerminalGuards() {
|
|
|
154
193
|
* Exits non-zero when a required surface failed, so MDM scripts can gate on it.
|
|
155
194
|
*/
|
|
156
195
|
async function onboardCommand(args, config) {
|
|
196
|
+
const journal = (0, onboardingJournal_1.loadOnboardingJournal)();
|
|
197
|
+
const dryRun = args.dryRun === 'true';
|
|
198
|
+
const repair = args.repair === 'true';
|
|
199
|
+
const json = args.json === 'true';
|
|
200
|
+
const journalFile = (0, onboardingJournal_1.onboardingJournalPath)();
|
|
201
|
+
const persist = () => (0, onboardingJournal_1.saveOnboardingJournal)(journal, journalFile);
|
|
202
|
+
const mark = (name, status, detail, error) => {
|
|
203
|
+
(0, onboardingJournal_1.updateOnboardingStep)(journal, name, status, detail, error);
|
|
204
|
+
persist();
|
|
205
|
+
};
|
|
206
|
+
const report = (nextAction) => {
|
|
207
|
+
journal.nextAction = nextAction;
|
|
208
|
+
persist();
|
|
209
|
+
if (json)
|
|
210
|
+
console.log(JSON.stringify({ ok: !Object.values(journal.steps).some(step => step.critical && step.status === 'failed'), journalPath: journalFile, ...journal }));
|
|
211
|
+
};
|
|
157
212
|
console.log(`\n${BOLD}\x1b[36mFullCourtDefense — machine onboarding${RESET}`);
|
|
158
213
|
console.log(`${DIM}Connectivity -> enrollment -> protection -> verification. One command.${RESET}\n`);
|
|
159
214
|
const creds = (0, config_1.resolveCliCredentials)(config, { apiUrl: args.apiUrl });
|
|
160
215
|
const apiUrl = creds.apiUrl;
|
|
216
|
+
const token = (args.token || process.env.FCD_ENROLL_TOKEN || '').trim();
|
|
161
217
|
// 1. Connectivity.
|
|
162
|
-
console.log(`${BOLD}[1/
|
|
218
|
+
console.log(`${BOLD}[1/6] Checking compatibility and connectivity…${RESET}`);
|
|
219
|
+
mark('preflight', 'running');
|
|
163
220
|
const reachable = await checkBackendReachable(apiUrl);
|
|
221
|
+
const compatible = checkCompatibility();
|
|
222
|
+
printCheck(compatible);
|
|
164
223
|
printCheck(reachable);
|
|
165
|
-
if (!reachable.ok) {
|
|
166
|
-
|
|
167
|
-
|
|
224
|
+
if (!reachable.ok || !compatible.ok) {
|
|
225
|
+
const detail = !reachable.ok ? reachable.detail : compatible.detail;
|
|
226
|
+
mark('preflight', 'failed', undefined, detail);
|
|
227
|
+
report(!reachable.ok
|
|
228
|
+
? `Restore connectivity to ${apiUrl}, then run fullcourtdefense onboard --resume.`
|
|
229
|
+
: 'Install Node.js 18 or later, then run fullcourtdefense onboard --resume.');
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (token) {
|
|
234
|
+
const enrollmentCredential = await checkEnrollmentCredential(apiUrl, token);
|
|
235
|
+
printCheck(enrollmentCredential);
|
|
236
|
+
if (!enrollmentCredential.ok) {
|
|
237
|
+
mark('preflight', 'failed', undefined, enrollmentCredential.detail);
|
|
238
|
+
report('Request a valid enrollment code from your administrator, then run fullcourtdefense onboard --resume.');
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
168
242
|
}
|
|
243
|
+
mark('preflight', 'completed');
|
|
169
244
|
// 2. Enrollment.
|
|
170
|
-
console.log(`\n${BOLD}[2/
|
|
171
|
-
|
|
245
|
+
console.log(`\n${BOLD}[2/6] Enrolling this machine…${RESET}`);
|
|
246
|
+
mark('enrollment', 'running');
|
|
172
247
|
const alreadyEnrolled = Boolean(creds.shieldId && creds.shieldKey);
|
|
173
|
-
if (
|
|
248
|
+
if (dryRun) {
|
|
249
|
+
mark('enrollment', 'skipped', alreadyEnrolled ? 'would reuse existing per-machine Shield credentials' : 'would enroll with supplied token');
|
|
250
|
+
}
|
|
251
|
+
else if (token) {
|
|
174
252
|
await (0, login_1.loginCommand)({ token, apiUrl: args.apiUrl }, config);
|
|
253
|
+
mark('enrollment', 'completed', 'machine enrolled');
|
|
175
254
|
}
|
|
176
255
|
else if (alreadyEnrolled) {
|
|
177
256
|
console.log(` ${GREEN}✓${RESET} Already enrolled ${DIM}(shield ${creds.shieldId})${RESET} — pass --token to re-enroll.`);
|
|
257
|
+
mark('enrollment', 'completed', `reused shield ${creds.shieldId}`);
|
|
178
258
|
}
|
|
179
259
|
else {
|
|
180
260
|
console.log(`\n${RED}No fleet enrollment token.${RESET} Ask an org admin for one (AI Fleet -> Settings -> Fleet enrollment token), then run:`);
|
|
181
261
|
console.log(` ${BOLD}fullcourtdefense onboard --token <fleet-enrollment-token>${RESET}`);
|
|
182
|
-
|
|
262
|
+
mark('enrollment', 'failed', undefined, 'no fleet enrollment token or saved machine credentials');
|
|
263
|
+
report('Get a fleet enrollment token, then run fullcourtdefense onboard --resume --token <token>.');
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
return;
|
|
183
266
|
}
|
|
184
267
|
// Re-read config: login just wrote fresh shield credentials to ~/.fullcourtdefense.yml.
|
|
185
268
|
const freshConfig = (0, config_1.loadConfig)();
|
|
186
269
|
// 3. Protection.
|
|
187
|
-
console.log(`\n${BOLD}[3/
|
|
188
|
-
|
|
270
|
+
console.log(`\n${BOLD}[3/6] Installing protection (install-all)…${RESET}`);
|
|
271
|
+
if (dryRun) {
|
|
272
|
+
mark('protection', 'skipped', 'would install MCP gateways, IDE hooks, and terminal guards');
|
|
273
|
+
}
|
|
274
|
+
else if (journal.steps.protection.status === 'completed' && !repair) {
|
|
275
|
+
console.log(` ${GREEN}✓${RESET} Reusing completed protection step ${DIM}(use --repair to reinstall)${RESET}`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
mark('protection', 'running');
|
|
279
|
+
try {
|
|
280
|
+
// Discovery and scheduling are deliberately separate optional transaction
|
|
281
|
+
// steps. install-all remains the compatibility-preserving protection owner.
|
|
282
|
+
await (0, installAll_1.installAllCommand)({
|
|
283
|
+
...args,
|
|
284
|
+
apiUrl,
|
|
285
|
+
discover: 'false',
|
|
286
|
+
schedule: 'false',
|
|
287
|
+
autoProtect: args.noDaemon === 'true' ? 'false' : args.autoProtect,
|
|
288
|
+
}, freshConfig);
|
|
289
|
+
mark('protection', 'completed');
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
293
|
+
mark('protection', 'failed', undefined, detail);
|
|
294
|
+
report('Fix the protection error, then run fullcourtdefense onboard --resume --repair.');
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
console.log(`\n${BOLD}[4/6] Installing background protection…${RESET}`);
|
|
300
|
+
if (dryRun || args.noDaemon === 'true') {
|
|
301
|
+
mark('daemon', 'skipped', dryRun ? 'would register the resident daemon' : 'disabled by --no-daemon true');
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
mark('daemon', 'running');
|
|
305
|
+
try {
|
|
306
|
+
await (0, daemon_1.daemonCommand)({ ...args, install: 'true', quiet: 'true' }, freshConfig);
|
|
307
|
+
if ((0, daemon_1.isDaemonAutostartInstalled)()) {
|
|
308
|
+
mark('daemon', 'completed', 'resident daemon autostart registered');
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
mark('daemon', 'failed', undefined, 'daemon autostart was not registered; run an elevated terminal and retry `fullcourtdefense daemon --install true`');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
mark('daemon', 'failed', undefined, error instanceof Error ? error.message : String(error));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Discovery and scheduling are useful fleet telemetry, but must never mark a
|
|
319
|
+
// device as unprotected or prevent a repair of the enforcement surfaces.
|
|
320
|
+
console.log(`\n${BOLD}[5/6] Recording optional discovery and schedule…${RESET}`);
|
|
321
|
+
if (dryRun || args.discover === 'false') {
|
|
322
|
+
mark('discovery', 'skipped', dryRun ? 'would upload desktop discovery' : 'disabled by --discover false');
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
mark('discovery', 'running');
|
|
326
|
+
try {
|
|
327
|
+
await (0, discover_1.discoverCommand)({ surface: 'all', upload: 'true', apiUrl, userEmail: args.developerName }, freshConfig);
|
|
328
|
+
mark('discovery', 'completed');
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
mark('discovery', 'failed', undefined, error instanceof Error ? error.message : String(error));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (dryRun || args.schedule === 'false') {
|
|
335
|
+
mark('schedule', 'skipped', dryRun ? 'would install daily discovery schedule' : 'disabled by --schedule false');
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
mark('schedule', 'running');
|
|
339
|
+
try {
|
|
340
|
+
const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
|
|
341
|
+
(0, discoverSchedule_1.installDailyDiscoverSchedule)({ userEmail: args.developerName, hour: Number.isFinite(hour) ? hour : 9, surface: 'all', trigger: 'daily' });
|
|
342
|
+
mark('schedule', 'completed');
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
mark('schedule', 'failed', undefined, error instanceof Error ? error.message : String(error));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
189
348
|
// 4. Verification.
|
|
190
|
-
console.log(`\n${BOLD}[
|
|
349
|
+
console.log(`\n${BOLD}[6/6] Verifying protection surfaces…${RESET}`);
|
|
350
|
+
mark('verification', 'running');
|
|
191
351
|
const postCreds = (0, config_1.resolveCliCredentials)((0, config_1.loadConfig)(), { apiUrl });
|
|
192
352
|
const checks = [
|
|
193
353
|
reachable,
|
|
@@ -200,6 +360,12 @@ async function onboardCommand(args, config) {
|
|
|
200
360
|
checkClaudeHooks(),
|
|
201
361
|
checkMcpGateways(),
|
|
202
362
|
...checkTerminalGuards(),
|
|
363
|
+
{
|
|
364
|
+
label: 'Resident daemon autostart',
|
|
365
|
+
ok: (0, daemon_1.isDaemonAutostartInstalled)(),
|
|
366
|
+
optional: true,
|
|
367
|
+
detail: (0, daemon_1.isDaemonAutostartInstalled)() ? undefined : 'run fullcourtdefense daemon --install true from an elevated terminal',
|
|
368
|
+
},
|
|
203
369
|
{
|
|
204
370
|
label: 'Daily discovery schedule',
|
|
205
371
|
ok: (0, discoverSchedule_1.isDiscoverScheduleInstalled)(),
|
|
@@ -217,9 +383,13 @@ async function onboardCommand(args, config) {
|
|
|
217
383
|
console.log(`${GREEN}${BOLD}This machine is protected.${RESET} It reports to your org's AI Fleet (monitor-first until an admin enables enforcement).`);
|
|
218
384
|
console.log(`${DIM}Fleet view: ${dashboard}${RESET}`);
|
|
219
385
|
console.log(`${DIM}Restart your AI clients (Cursor, Claude, VS Code, …) so they pick up the wrapped configs.${RESET}`);
|
|
386
|
+
mark('verification', 'completed');
|
|
387
|
+
report('Onboarding complete. Restart AI clients to load protected configurations.');
|
|
220
388
|
}
|
|
221
389
|
else {
|
|
222
390
|
console.log(`${YELLOW}${BOLD}Onboarding finished with ${requiredFailures.length} unresolved surface(s).${RESET} Fix the ✗ lines above and re-run ${BOLD}fullcourtdefense onboard${RESET}.`);
|
|
223
|
-
|
|
391
|
+
mark('verification', 'failed', undefined, `${requiredFailures.length} critical protection surfaces unresolved`);
|
|
392
|
+
report('Fix the failed required surfaces, then run fullcourtdefense onboard --resume --repair.');
|
|
393
|
+
process.exitCode = 1;
|
|
224
394
|
}
|
|
225
395
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type OnboardingStepName = 'preflight' | 'enrollment' | 'protection' | 'daemon' | 'discovery' | 'schedule' | 'verification';
|
|
2
|
+
export type OnboardingStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
3
|
+
export interface OnboardingStep {
|
|
4
|
+
status: OnboardingStepStatus;
|
|
5
|
+
critical: boolean;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
detail?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface OnboardingJournal {
|
|
11
|
+
version: 1;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
nextAction: string;
|
|
14
|
+
steps: Record<OnboardingStepName, OnboardingStep>;
|
|
15
|
+
}
|
|
16
|
+
export declare function onboardingJournalPath(home?: string): string;
|
|
17
|
+
export declare function createOnboardingJournal(): OnboardingJournal;
|
|
18
|
+
export declare function loadOnboardingJournal(file?: string): OnboardingJournal;
|
|
19
|
+
export declare function saveOnboardingJournal(journal: OnboardingJournal, file?: string): void;
|
|
20
|
+
export declare function updateOnboardingStep(journal: OnboardingJournal, name: OnboardingStepName, status: OnboardingStepStatus, detail?: string, error?: string): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
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.onboardingJournalPath = onboardingJournalPath;
|
|
37
|
+
exports.createOnboardingJournal = createOnboardingJournal;
|
|
38
|
+
exports.loadOnboardingJournal = loadOnboardingJournal;
|
|
39
|
+
exports.saveOnboardingJournal = saveOnboardingJournal;
|
|
40
|
+
exports.updateOnboardingStep = updateOnboardingStep;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const STEP_DEFINITIONS = [
|
|
45
|
+
['preflight', true],
|
|
46
|
+
['enrollment', true],
|
|
47
|
+
['protection', true],
|
|
48
|
+
['daemon', false],
|
|
49
|
+
['discovery', false],
|
|
50
|
+
['schedule', false],
|
|
51
|
+
['verification', true],
|
|
52
|
+
];
|
|
53
|
+
function onboardingJournalPath(home = os.homedir()) {
|
|
54
|
+
return path.join(home, '.fullcourtdefense', 'onboarding.json');
|
|
55
|
+
}
|
|
56
|
+
function createOnboardingJournal() {
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
return {
|
|
59
|
+
version: 1,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
nextAction: 'Run fullcourtdefense onboard --resume to begin.',
|
|
62
|
+
steps: Object.fromEntries(STEP_DEFINITIONS.map(([name, critical]) => [
|
|
63
|
+
name,
|
|
64
|
+
{ status: 'pending', critical, updatedAt: now },
|
|
65
|
+
])),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function loadOnboardingJournal(file = onboardingJournalPath()) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
71
|
+
if (parsed.version === 1 && parsed.steps) {
|
|
72
|
+
const fallback = createOnboardingJournal();
|
|
73
|
+
return {
|
|
74
|
+
...fallback,
|
|
75
|
+
...parsed,
|
|
76
|
+
steps: { ...fallback.steps, ...parsed.steps },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// A missing or malformed journal must never prevent repair.
|
|
82
|
+
}
|
|
83
|
+
return createOnboardingJournal();
|
|
84
|
+
}
|
|
85
|
+
function saveOnboardingJournal(journal, file = onboardingJournalPath()) {
|
|
86
|
+
journal.updatedAt = new Date().toISOString();
|
|
87
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
88
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
89
|
+
fs.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
90
|
+
fs.renameSync(temporary, file);
|
|
91
|
+
}
|
|
92
|
+
function updateOnboardingStep(journal, name, status, detail, error) {
|
|
93
|
+
journal.steps[name] = {
|
|
94
|
+
...journal.steps[name],
|
|
95
|
+
status,
|
|
96
|
+
updatedAt: new Date().toISOString(),
|
|
97
|
+
...(detail ? { detail } : {}),
|
|
98
|
+
...(error ? { error } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -45,6 +45,7 @@ exports.resolveSystemPrompt = resolveSystemPrompt;
|
|
|
45
45
|
const fs = __importStar(require("fs"));
|
|
46
46
|
const os = __importStar(require("os"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
48
49
|
const CONFIG_FILENAMES = [
|
|
49
50
|
'.fullcourtdefense.yml',
|
|
50
51
|
'.fullcourtdefense.yaml',
|
|
@@ -137,6 +138,7 @@ function readConfigFile(filePath) {
|
|
|
137
138
|
apiUrl: raw.apiUrl ? interpolateEnv(String(raw.apiUrl)) : undefined,
|
|
138
139
|
shieldId: raw.shieldId ? interpolateEnv(String(raw.shieldId)) : undefined,
|
|
139
140
|
shieldKey: raw.shieldKey ? interpolateEnv(String(raw.shieldKey)) : undefined,
|
|
141
|
+
shieldKeyDpapi: raw.shieldKeyDpapi ? String(raw.shieldKeyDpapi) : undefined,
|
|
140
142
|
scan: raw.scan
|
|
141
143
|
? {
|
|
142
144
|
endpoint: raw.scan.endpoint,
|
|
@@ -152,6 +154,33 @@ function readConfigFile(filePath) {
|
|
|
152
154
|
: undefined,
|
|
153
155
|
};
|
|
154
156
|
}
|
|
157
|
+
function powershellDpapi(script, value) {
|
|
158
|
+
if (process.platform !== 'win32' || !value)
|
|
159
|
+
return undefined;
|
|
160
|
+
try {
|
|
161
|
+
return (0, child_process_1.execFileSync)('powershell.exe', [
|
|
162
|
+
'-NoProfile',
|
|
163
|
+
'-NonInteractive',
|
|
164
|
+
'-ExecutionPolicy', 'Bypass',
|
|
165
|
+
'-Command', script,
|
|
166
|
+
], {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
169
|
+
env: { ...process.env, FCD_DPAPI_VALUE: value },
|
|
170
|
+
timeout: 5_000,
|
|
171
|
+
windowsHide: true,
|
|
172
|
+
}).trim() || undefined;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function protectShieldKeyForCurrentWindowsUser(value) {
|
|
179
|
+
return powershellDpapi('$secure=ConvertTo-SecureString -String $env:FCD_DPAPI_VALUE -AsPlainText -Force; ConvertFrom-SecureString -SecureString $secure', value);
|
|
180
|
+
}
|
|
181
|
+
function unprotectShieldKeyForCurrentWindowsUser(value) {
|
|
182
|
+
return powershellDpapi('$secure=ConvertTo-SecureString -String $env:FCD_DPAPI_VALUE; $ptr=[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure); try {[Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)} finally {[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)}', value);
|
|
183
|
+
}
|
|
155
184
|
function mergeConfig(base, override) {
|
|
156
185
|
return {
|
|
157
186
|
...base,
|
|
@@ -213,6 +242,7 @@ function resolveCliCredentials(config, overrides = {}) {
|
|
|
213
242
|
|| process.env.FULLCOURTDEFENSE_SHIELD_ID
|
|
214
243
|
|| process.env.AGENTGUARD_SHIELD_ID,
|
|
215
244
|
shieldKey: overrides.shieldKey
|
|
245
|
+
|| unprotectShieldKeyForCurrentWindowsUser(config.shieldKeyDpapi || '')
|
|
216
246
|
|| config.shieldKey
|
|
217
247
|
|| process.env.FCD_SHIELD_KEY
|
|
218
248
|
|| process.env.FULLCOURTDEFENSE_SHIELD_KEY
|
|
@@ -266,8 +296,21 @@ function writeSetupConfig(target, input) {
|
|
|
266
296
|
setTopLevel('apiKey', input.apiKey);
|
|
267
297
|
setTopLevel('organizationId', input.organizationId);
|
|
268
298
|
setTopLevel('shieldId', input.shieldId);
|
|
269
|
-
|
|
299
|
+
const protectedShieldKey = input.shieldKey && process.platform === 'win32'
|
|
300
|
+
? protectShieldKeyForCurrentWindowsUser(input.shieldKey)
|
|
301
|
+
: undefined;
|
|
302
|
+
if (protectedShieldKey) {
|
|
303
|
+
setTopLevel('shieldKeyDpapi', protectedShieldKey);
|
|
304
|
+
// Remove legacy plaintext key on a successful Windows DPAPI migration.
|
|
305
|
+
const plaintextIndex = lines.findIndex(line => /^shieldKey:/.test(line.trim()));
|
|
306
|
+
if (plaintextIndex >= 0)
|
|
307
|
+
lines.splice(plaintextIndex, 1);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
setTopLevel('shieldKey', input.shieldKey);
|
|
311
|
+
}
|
|
270
312
|
setTopLevel('apiUrl', input.apiUrl);
|
|
313
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
271
314
|
fs.writeFileSync(target, `${lines.filter((line, idx, arr) => !(line === '' && arr[idx - 1] === '')).join('\n').trim()}\n`, 'utf-8');
|
|
272
315
|
return target;
|
|
273
316
|
}
|
package/dist/index.js
CHANGED
|
@@ -359,6 +359,8 @@ function printHelp() {
|
|
|
359
359
|
$ fullcourtdefense login --token <fleet-enrollment-token>
|
|
360
360
|
$ fullcourtdefense install-all
|
|
361
361
|
$ fullcourtdefense install-all --auto-protect true --upload
|
|
362
|
+
$ fullcourtdefense onboard --token <fleet-enrollment-token> --resume
|
|
363
|
+
$ fullcourtdefense onboard --dry-run true --json true
|
|
362
364
|
$ fullcourtdefense discover --surface all --upload --schedule logon
|
|
363
365
|
$ fullcourtdefense install-mcp-gateway --clients cursor,codex --mcp-command npm --mcp-args "run mcp"
|
|
364
366
|
$ fullcourtdefense install-codex-mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
@@ -634,6 +636,8 @@ async function main() {
|
|
|
634
636
|
shellGuard: flags['shell-guard'],
|
|
635
637
|
cmdGuard: flags['cmd-guard'],
|
|
636
638
|
posixGuard: flags['posix-guard'],
|
|
639
|
+
schedule: flags.schedule,
|
|
640
|
+
scheduleHour: flags['schedule-hour'],
|
|
637
641
|
};
|
|
638
642
|
await (0, installAll_1.installAllCommand)(args, config);
|
|
639
643
|
break;
|
|
@@ -650,6 +654,13 @@ async function main() {
|
|
|
650
654
|
shellGuard: flags['shell-guard'],
|
|
651
655
|
cmdGuard: flags['cmd-guard'],
|
|
652
656
|
posixGuard: flags['posix-guard'],
|
|
657
|
+
schedule: flags.schedule,
|
|
658
|
+
scheduleHour: flags['schedule-hour'],
|
|
659
|
+
resume: flags.resume,
|
|
660
|
+
repair: flags.repair,
|
|
661
|
+
dryRun: flags['dry-run'],
|
|
662
|
+
json: flags.json,
|
|
663
|
+
noDaemon: flags['no-daemon'],
|
|
653
664
|
};
|
|
654
665
|
await (0, onboard_1.onboardCommand)(args, config);
|
|
655
666
|
break;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type IntegrityReason = 'mcp_gateway_missing' | 'cursor_hook_changed' | 'claude_hook_changed' | 'daemon_not_running' | 'runtime_bundle_stale';
|
|
2
|
+
export interface LocalIntegrityReport {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
reasons: IntegrityReason[];
|
|
5
|
+
checkedAt: string;
|
|
6
|
+
protectedMcpConfigs: number;
|
|
7
|
+
discoveredMcpConfigs: number;
|
|
8
|
+
daemonRunning: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function isDaemonRunning(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Local-only verification of the protection surfaces that a user-mode agent can
|
|
13
|
+
* observe. It deliberately reports gaps instead of claiming EDR-grade tamper
|
|
14
|
+
* resistance: unsupported clients and non-interactive shells remain visible
|
|
15
|
+
* product limitations.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getLocalIntegrityReport(options?: {
|
|
18
|
+
requireDaemon?: boolean;
|
|
19
|
+
runtimeBundleFetchedAt?: number;
|
|
20
|
+
runtimeBundleMaxAgeMs?: number;
|
|
21
|
+
}): LocalIntegrityReport;
|
|
@@ -0,0 +1,115 @@
|
|
|
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.isDaemonRunning = isDaemonRunning;
|
|
37
|
+
exports.getLocalIntegrityReport = getLocalIntegrityReport;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const discoverPaths_1 = require("./commands/discoverPaths");
|
|
42
|
+
function readText(file) {
|
|
43
|
+
try {
|
|
44
|
+
return fs.readFileSync(file, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function hasMcpServers(text) {
|
|
51
|
+
return /"mcpServers"|\[mcp_servers\]|"servers"\s*:/.test(text)
|
|
52
|
+
&& /"command"|command\s*=|"url"\s*:/.test(text);
|
|
53
|
+
}
|
|
54
|
+
function isPidAlive(pid) {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, 0);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return error.code === 'EPERM';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function isDaemonRunning() {
|
|
64
|
+
try {
|
|
65
|
+
const pid = Number(readText(path.join(os.homedir(), '.fullcourtdefense', 'daemon.pid')).trim());
|
|
66
|
+
return Number.isFinite(pid) && pid > 0 && isPidAlive(pid);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Local-only verification of the protection surfaces that a user-mode agent can
|
|
74
|
+
* observe. It deliberately reports gaps instead of claiming EDR-grade tamper
|
|
75
|
+
* resistance: unsupported clients and non-interactive shells remain visible
|
|
76
|
+
* product limitations.
|
|
77
|
+
*/
|
|
78
|
+
function getLocalIntegrityReport(options = {}) {
|
|
79
|
+
const reasons = [];
|
|
80
|
+
let discoveredMcpConfigs = 0;
|
|
81
|
+
let protectedMcpConfigs = 0;
|
|
82
|
+
for (const candidate of (0, discoverPaths_1.candidateConfigPaths)(process.cwd())) {
|
|
83
|
+
const text = readText(candidate.path);
|
|
84
|
+
if (!text || !hasMcpServers(text))
|
|
85
|
+
continue;
|
|
86
|
+
discoveredMcpConfigs++;
|
|
87
|
+
if (text.includes('mcp-gateway'))
|
|
88
|
+
protectedMcpConfigs++;
|
|
89
|
+
}
|
|
90
|
+
if (protectedMcpConfigs < discoveredMcpConfigs)
|
|
91
|
+
reasons.push('mcp_gateway_missing');
|
|
92
|
+
const home = os.homedir();
|
|
93
|
+
const cursorHooks = readText(path.join(home, '.cursor', 'hooks.json'));
|
|
94
|
+
if (cursorHooks && !(/fcd-managed|fullcourtdefense|botguard/i.test(cursorHooks))) {
|
|
95
|
+
reasons.push('cursor_hook_changed');
|
|
96
|
+
}
|
|
97
|
+
const claudeHooks = readText(path.join(home, '.claude', 'settings.json'));
|
|
98
|
+
if (claudeHooks && !(/fcd-managed|fullcourtdefense|botguard/i.test(claudeHooks))) {
|
|
99
|
+
reasons.push('claude_hook_changed');
|
|
100
|
+
}
|
|
101
|
+
const daemonRunning = isDaemonRunning();
|
|
102
|
+
if (options.requireDaemon && !daemonRunning)
|
|
103
|
+
reasons.push('daemon_not_running');
|
|
104
|
+
if (options.runtimeBundleFetchedAt && Date.now() - options.runtimeBundleFetchedAt > (options.runtimeBundleMaxAgeMs ?? 5 * 60_000)) {
|
|
105
|
+
reasons.push('runtime_bundle_stale');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: reasons.length === 0,
|
|
109
|
+
reasons,
|
|
110
|
+
checkedAt: new Date().toISOString(),
|
|
111
|
+
protectedMcpConfigs,
|
|
112
|
+
discoveredMcpConfigs,
|
|
113
|
+
daemonRunning,
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ export interface FlushInput {
|
|
|
29
29
|
agentVersion?: string;
|
|
30
30
|
/** Whether protection points still look intact (tamper signal). */
|
|
31
31
|
integrityOk?: boolean;
|
|
32
|
+
/** Machine-readable reasons for partial protection, never prompt/tool content. */
|
|
33
|
+
integrityReasons?: string[];
|
|
34
|
+
integrityCheckedAt?: string;
|
|
32
35
|
timeoutMs?: number;
|
|
33
36
|
}
|
|
34
37
|
/** Drain the spool to the backend in one batch (+ optional heartbeat). Returns accepted count. */
|
package/dist/telemetry.js
CHANGED
|
@@ -138,6 +138,8 @@ async function flushSpool(input) {
|
|
|
138
138
|
? {
|
|
139
139
|
agentVersion: input.agentVersion,
|
|
140
140
|
integrityOk: input.integrityOk,
|
|
141
|
+
integrityReasons: input.integrityReasons,
|
|
142
|
+
integrityCheckedAt: input.integrityCheckedAt,
|
|
141
143
|
coverage: 'hooks',
|
|
142
144
|
hostname: identity.hostname,
|
|
143
145
|
// Windows-only: current PowerShell audit coverage (ScriptBlock
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,13 @@
|
|
|
27
27
|
"test:per-server-agent-name": "npm run build && node scripts/test-per-server-agent-name.js",
|
|
28
28
|
"test:remote-mcp-gateway": "npm run build && node scripts/test-remote-mcp-gateway.js",
|
|
29
29
|
"test:e2e-onboarding-personas": "npm run build && node scripts/test-e2e-onboarding-personas.js",
|
|
30
|
+
"test:onboarding-transaction": "npm run build && node scripts/test-onboarding-transaction.js",
|
|
30
31
|
"test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
|
|
31
32
|
"test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
|
|
32
33
|
"test:daemon-e2e": "npm run build && node scripts/test-daemon-e2e.js",
|
|
34
|
+
"test:integrity": "npm run build && node scripts/test-integrity.js",
|
|
35
|
+
"test:dpapi-config": "npm run build && node scripts/test-dpapi-config.js",
|
|
36
|
+
"build:msi": "powershell -NoProfile -ExecutionPolicy Bypass -File installer/windows/Build-Msi.ps1",
|
|
33
37
|
"prepublishOnly": "npm run build"
|
|
34
38
|
},
|
|
35
39
|
"keywords": [
|