fullcourtdefense-cli 1.22.7 → 1.22.8
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/dist/backupManifest.d.ts +7 -0
- package/dist/backupManifest.js +108 -0
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/deterministicGuard.js +18 -3
- package/dist/commands/discover.js +5 -0
- package/dist/commands/hook.js +10 -0
- package/dist/commands/installAll.js +10 -4
- package/dist/commands/installCursorHook.js +6 -2
- package/dist/commands/mcpGateway.js +13 -0
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.js +53 -2
- package/dist/commands/uninstallAll.d.ts +58 -9
- package/dist/commands/uninstallAll.js +359 -37
- package/dist/commands/verifyRemoved.d.ts +24 -0
- package/dist/commands/verifyRemoved.js +293 -0
- package/dist/commands/windowsAudit.d.ts +10 -5
- package/dist/commands/windowsAudit.js +47 -8
- package/dist/index.js +39 -7
- package/dist/localSafetySnapshot.js +6 -3
- package/dist/selfTest.js +31 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,293 @@
|
|
|
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.collectRemnantChecks = collectRemnantChecks;
|
|
37
|
+
exports.verifyRemovedCommand = verifyRemovedCommand;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const uninstallAll_1 = require("./uninstallAll");
|
|
43
|
+
const FCD_TASK_NAMES = [
|
|
44
|
+
'FullCourtDefense Updater',
|
|
45
|
+
'FullCourtDefense Daemon',
|
|
46
|
+
'FullCourtDefense Daemon Watchdog',
|
|
47
|
+
'FullCourtDefense Auto-Protect',
|
|
48
|
+
'FullCourtDefenseDesktopDiscover',
|
|
49
|
+
];
|
|
50
|
+
function taskExists(taskName) {
|
|
51
|
+
const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName], {
|
|
52
|
+
stdio: 'ignore', windowsHide: true, timeout: 10_000,
|
|
53
|
+
});
|
|
54
|
+
return query.status === 0;
|
|
55
|
+
}
|
|
56
|
+
function regQuery(keyPath, valueName) {
|
|
57
|
+
const queryArgs = valueName ? ['query', keyPath, '/v', valueName] : ['query', keyPath];
|
|
58
|
+
const query = (0, child_process_1.spawnSync)('reg', queryArgs, { encoding: 'utf8', windowsHide: true, timeout: 10_000 });
|
|
59
|
+
return query.status === 0 ? query.stdout : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** Well-known GLOBAL client config files that FCD wraps or hooks into. */
|
|
62
|
+
function knownClientConfigFiles() {
|
|
63
|
+
const home = os.homedir();
|
|
64
|
+
const files = [
|
|
65
|
+
path.join(home, '.cursor', 'mcp.json'),
|
|
66
|
+
path.join(home, '.cursor', 'hooks.json'),
|
|
67
|
+
path.join(home, '.claude', 'settings.json'),
|
|
68
|
+
path.join(home, '.claude.json'),
|
|
69
|
+
path.join(home, '.codex', 'config.toml'),
|
|
70
|
+
path.join(home, '.gemini', 'settings.json'),
|
|
71
|
+
path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
72
|
+
];
|
|
73
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
74
|
+
files.push(path.join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json'));
|
|
75
|
+
files.push(path.join(process.env.APPDATA, 'Code', 'User', 'settings.json'));
|
|
76
|
+
}
|
|
77
|
+
else if (process.platform === 'darwin') {
|
|
78
|
+
files.push(path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'));
|
|
79
|
+
files.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
files.push(path.join(home, '.config', 'Claude', 'claude_desktop_config.json'));
|
|
83
|
+
files.push(path.join(home, '.config', 'Code', 'User', 'settings.json'));
|
|
84
|
+
}
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
function fileMentionsFcd(file) {
|
|
88
|
+
try {
|
|
89
|
+
return /fullcourtdefense|fcd-mcp-gateway/i.test(fs.readFileSync(file, 'utf8'));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function collectWindowsChecks() {
|
|
96
|
+
const checks = [];
|
|
97
|
+
const home = os.homedir();
|
|
98
|
+
for (const taskName of FCD_TASK_NAMES) {
|
|
99
|
+
checks.push({
|
|
100
|
+
id: `task:${taskName}`,
|
|
101
|
+
label: `Scheduled task "${taskName}"`,
|
|
102
|
+
present: taskExists(taskName),
|
|
103
|
+
fix: taskName === 'FullCourtDefense Updater'
|
|
104
|
+
? 'removed by the MSI uninstall (Settings → Apps → FullCourtDefense → Uninstall); it runs as SYSTEM, so an elevated `schtasks /Delete /TN "FullCourtDefense Updater" /F` also works'
|
|
105
|
+
: `schtasks /Delete /TN "${taskName}" /F`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// HKLM machine-wide entries. PowerShell audit policies are attributed to
|
|
109
|
+
// FCD via the transcript OutputDirectory — an org GPO pointing elsewhere is
|
|
110
|
+
// NOT flagged (it is not ours to remove).
|
|
111
|
+
const fcdRoot = regQuery('HKLM\\SOFTWARE\\FullCourtDefense');
|
|
112
|
+
checks.push({
|
|
113
|
+
id: 'reg:hklm-fcd',
|
|
114
|
+
label: 'HKLM\\SOFTWARE\\FullCourtDefense registry key',
|
|
115
|
+
present: fcdRoot !== undefined,
|
|
116
|
+
fix: 'removed by the MSI uninstall',
|
|
117
|
+
});
|
|
118
|
+
const transcription = regQuery('HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\Transcription', 'OutputDirectory');
|
|
119
|
+
const fcdTranscription = transcription !== undefined && /FullCourtDefense/i.test(transcription);
|
|
120
|
+
checks.push({
|
|
121
|
+
id: 'reg:transcription',
|
|
122
|
+
label: 'HKLM PowerShell Transcription policy (FCD-enabled)',
|
|
123
|
+
present: fcdTranscription,
|
|
124
|
+
detail: fcdTranscription ? 'OutputDirectory points at a FullCourtDefense folder' : undefined,
|
|
125
|
+
fix: 'fullcourtdefense windows-audit --disable true (or the MSI uninstall)',
|
|
126
|
+
});
|
|
127
|
+
if (fcdTranscription) {
|
|
128
|
+
const sbl = regQuery('HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ScriptBlockLogging', 'EnableScriptBlockLogging');
|
|
129
|
+
checks.push({
|
|
130
|
+
id: 'reg:scriptblocklogging',
|
|
131
|
+
label: 'HKLM PowerShell ScriptBlockLogging policy (FCD-enabled)',
|
|
132
|
+
present: sbl !== undefined,
|
|
133
|
+
fix: 'fullcourtdefense windows-audit --disable true (or the MSI uninstall)',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const runKey = regQuery('HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run', 'FullCourtDefenseDaemon');
|
|
137
|
+
checks.push({
|
|
138
|
+
id: 'reg:run-key',
|
|
139
|
+
label: 'HKCU Run key "FullCourtDefenseDaemon"',
|
|
140
|
+
present: runKey !== undefined,
|
|
141
|
+
fix: 'reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v FullCourtDefenseDaemon /f',
|
|
142
|
+
});
|
|
143
|
+
const autorun = regQuery('HKCU\\Software\\Microsoft\\Command Processor', 'AutoRun');
|
|
144
|
+
const fcdAutorun = autorun !== undefined && /fullcourtdefense/i.test(autorun);
|
|
145
|
+
checks.push({
|
|
146
|
+
id: 'reg:cmd-autorun',
|
|
147
|
+
label: 'cmd.exe AutoRun hook (cmd shell guard)',
|
|
148
|
+
present: fcdAutorun,
|
|
149
|
+
fix: 'fullcourtdefense uninstall-cmd-guard',
|
|
150
|
+
});
|
|
151
|
+
const machinePath = regQuery('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path');
|
|
152
|
+
checks.push({
|
|
153
|
+
id: 'reg:system-path',
|
|
154
|
+
label: 'System PATH entry (FullCourtDefense install dir)',
|
|
155
|
+
present: machinePath !== undefined && /FullCourtDefense/i.test(machinePath),
|
|
156
|
+
fix: 'removed by the MSI uninstall',
|
|
157
|
+
});
|
|
158
|
+
const dirs = [
|
|
159
|
+
['dir:program-files', process.env.ProgramFiles, 'removed by the MSI uninstall'],
|
|
160
|
+
['dir:program-files-x86', process.env['ProgramFiles(x86)'], 'removed by the MSI uninstall'],
|
|
161
|
+
['dir:programdata', process.env.ProgramData, 'removed by the MSI uninstall (transcripts + machine state)'],
|
|
162
|
+
['dir:localappdata', process.env.LOCALAPPDATA, 'fullcourtdefense uninstall (purges local caches)'],
|
|
163
|
+
];
|
|
164
|
+
for (const [id, base, fix] of dirs) {
|
|
165
|
+
if (!base)
|
|
166
|
+
continue;
|
|
167
|
+
const dir = path.join(base, 'FullCourtDefense');
|
|
168
|
+
checks.push({ id, label: dir, present: fs.existsSync(dir), fix });
|
|
169
|
+
}
|
|
170
|
+
const profileFiles = [
|
|
171
|
+
'.fullcourtdefense.yml',
|
|
172
|
+
'.fullcourtdefense-spool.jsonl',
|
|
173
|
+
'.fullcourtdefense-runtime.json',
|
|
174
|
+
'.fullcourtdefense-hook.log',
|
|
175
|
+
'.fullcourtdefense',
|
|
176
|
+
'.fullcourtdefense-shell-guard.json',
|
|
177
|
+
'.fullcourtdefense-shell-guard.ps1',
|
|
178
|
+
'.fullcourtdefense-cmd-guard.js',
|
|
179
|
+
'.fullcourtdefense-cmd-autorun.bat',
|
|
180
|
+
];
|
|
181
|
+
for (const name of profileFiles) {
|
|
182
|
+
const file = path.join(home, name);
|
|
183
|
+
checks.push({
|
|
184
|
+
id: `file:${name}`,
|
|
185
|
+
label: file,
|
|
186
|
+
present: fs.existsSync(file),
|
|
187
|
+
fix: 'fullcourtdefense uninstall (purges profile state)',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return checks;
|
|
191
|
+
}
|
|
192
|
+
function collectPosixChecks() {
|
|
193
|
+
const checks = [];
|
|
194
|
+
const home = os.homedir();
|
|
195
|
+
if (process.platform === 'darwin') {
|
|
196
|
+
const plist = path.join(home, 'Library', 'LaunchAgents', 'ai.fullcourtdefense.daemon.plist');
|
|
197
|
+
checks.push({ id: 'launchd', label: plist, present: fs.existsSync(plist), fix: 'fullcourtdefense daemon --uninstall true' });
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const unit = path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'systemd', 'user', 'fullcourtdefense-daemon.service');
|
|
201
|
+
checks.push({ id: 'systemd', label: unit, present: fs.existsSync(unit), fix: 'fullcourtdefense daemon --uninstall true' });
|
|
202
|
+
const crontab = (0, child_process_1.spawnSync)('crontab', ['-l'], { encoding: 'utf8', timeout: 10_000 });
|
|
203
|
+
checks.push({
|
|
204
|
+
id: 'crontab',
|
|
205
|
+
label: 'crontab entries (# FCD_DAEMON)',
|
|
206
|
+
present: crontab.status === 0 && /FCD_DAEMON|fullcourtdefense/i.test(crontab.stdout || ''),
|
|
207
|
+
fix: 'fullcourtdefense daemon --uninstall true',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
for (const rc of ['.zshrc', '.bashrc', '.bash_profile', '.profile']) {
|
|
211
|
+
const file = path.join(home, rc);
|
|
212
|
+
let mentions = false;
|
|
213
|
+
try {
|
|
214
|
+
mentions = /fullcourtdefense/i.test(fs.readFileSync(file, 'utf8'));
|
|
215
|
+
}
|
|
216
|
+
catch { /* absent */ }
|
|
217
|
+
checks.push({ id: `rc:${rc}`, label: `${file} (shell guard hook)`, present: mentions, fix: 'fullcourtdefense uninstall (removes shell guard wiring)' });
|
|
218
|
+
}
|
|
219
|
+
const profileFiles = [
|
|
220
|
+
'.fullcourtdefense.yml',
|
|
221
|
+
'.fullcourtdefense-spool.jsonl',
|
|
222
|
+
'.fullcourtdefense-runtime.json',
|
|
223
|
+
'.fullcourtdefense-hook.log',
|
|
224
|
+
'.fullcourtdefense',
|
|
225
|
+
'.fullcourtdefense-shell-guard.json',
|
|
226
|
+
'.fullcourtdefense-shell-guard.zsh',
|
|
227
|
+
'.fullcourtdefense-shell-guard.bash',
|
|
228
|
+
];
|
|
229
|
+
for (const name of profileFiles) {
|
|
230
|
+
const file = path.join(home, name);
|
|
231
|
+
checks.push({ id: `file:${name}`, label: file, present: fs.existsSync(file), fix: 'fullcourtdefense uninstall (purges profile state)' });
|
|
232
|
+
}
|
|
233
|
+
return checks;
|
|
234
|
+
}
|
|
235
|
+
/** Shared (all platforms): config backups + still-wrapped client configs. */
|
|
236
|
+
function collectSharedChecks() {
|
|
237
|
+
const checks = [];
|
|
238
|
+
const backups = (0, uninstallAll_1.listFcdConfigBackups)();
|
|
239
|
+
checks.push({
|
|
240
|
+
id: 'config-backups',
|
|
241
|
+
label: 'Config backups (*.fcd-backup-*) — may contain credentials',
|
|
242
|
+
present: backups.length > 0,
|
|
243
|
+
detail: backups.length ? backups.join(', ') : undefined,
|
|
244
|
+
fix: 'fullcourtdefense uninstall (deletes them), or delete the listed files',
|
|
245
|
+
});
|
|
246
|
+
for (const file of knownClientConfigFiles()) {
|
|
247
|
+
if (!fs.existsSync(file))
|
|
248
|
+
continue;
|
|
249
|
+
if (!fileMentionsFcd(file))
|
|
250
|
+
continue;
|
|
251
|
+
checks.push({
|
|
252
|
+
id: `wrapped:${file}`,
|
|
253
|
+
label: `${file} still references FullCourtDefense`,
|
|
254
|
+
present: true,
|
|
255
|
+
fix: 'fullcourtdefense uninstall (restores original client configs)',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return checks;
|
|
259
|
+
}
|
|
260
|
+
/** All known-artifact checks for this platform — reused by `uninstall` for its inline self-verification. */
|
|
261
|
+
function collectRemnantChecks() {
|
|
262
|
+
return [
|
|
263
|
+
...(process.platform === 'win32' ? collectWindowsChecks() : collectPosixChecks()),
|
|
264
|
+
...collectSharedChecks(),
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
async function verifyRemovedCommand(args) {
|
|
268
|
+
const checks = collectRemnantChecks();
|
|
269
|
+
const remnants = checks.filter(check => check.present);
|
|
270
|
+
if (args.json === 'true') {
|
|
271
|
+
console.log(JSON.stringify({ clean: remnants.length === 0, checked: checks.length, remnants }, null, 2));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.log('\n\x1b[1m\x1b[36mFullCourtDefense — verify removal\x1b[0m');
|
|
275
|
+
console.log(`\x1b[2m${checks.length} known artifact locations checked on this machine.\x1b[0m\n`);
|
|
276
|
+
if (remnants.length === 0) {
|
|
277
|
+
console.log('\x1b[32m\x1b[1m✓ Clean — no FullCourtDefense artifacts found. This machine is back to its pre-FCD state.\x1b[0m');
|
|
278
|
+
console.log('\x1b[2mNote: the fleet console record is server-side and separate — an admin deprovisions it there.\x1b[0m');
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.log(`\x1b[33m\x1b[1m${remnants.length} artifact(s) still present:\x1b[0m\n`);
|
|
282
|
+
for (const remnant of remnants) {
|
|
283
|
+
console.log(` \x1b[33m✗\x1b[0m ${remnant.label}`);
|
|
284
|
+
if (remnant.detail)
|
|
285
|
+
console.log(` \x1b[2m${remnant.detail}\x1b[0m`);
|
|
286
|
+
if (remnant.fix)
|
|
287
|
+
console.log(` \x1b[2mfix: ${remnant.fix}\x1b[0m`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (remnants.length > 0)
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
}
|
|
@@ -63,19 +63,24 @@ export declare function disableWindowsAudit(): EnableWindowsAuditResult;
|
|
|
63
63
|
*/
|
|
64
64
|
export declare function enableWindowsAudit(): EnableWindowsAuditResult;
|
|
65
65
|
export interface TranscriptPruneResult {
|
|
66
|
-
/** Files deleted in this run. */
|
|
66
|
+
/** Files deleted in this run (age window). */
|
|
67
67
|
pruned: number;
|
|
68
|
+
/** Files deleted because the folder exceeded the size budget. */
|
|
69
|
+
prunedBySize?: number;
|
|
68
70
|
/** Set when pruning was skipped entirely (not enabled / foreign dir / non-Windows). */
|
|
69
71
|
skipped?: string;
|
|
70
72
|
}
|
|
71
73
|
/** Retention window in days — env override for fleets that want tighter/looser. */
|
|
72
74
|
export declare function transcriptRetentionDays(): number;
|
|
75
|
+
/** Size budget in MB — env override (FCD_TRANSCRIPT_MAX_MB, floor 50MB). */
|
|
76
|
+
export declare function transcriptMaxMb(): number;
|
|
73
77
|
/**
|
|
74
|
-
* Delete transcript files older than the retention window
|
|
75
|
-
*
|
|
76
|
-
*
|
|
78
|
+
* Delete transcript files older than the retention window, then enforce the
|
|
79
|
+
* size budget (oldest-first) on whatever survived. Never throws; locked/
|
|
80
|
+
* in-use files (an open PowerShell session's live transcript) are skipped and
|
|
81
|
+
* picked up on a later run. Empty date folders are removed.
|
|
77
82
|
*/
|
|
78
|
-
export declare function pruneTranscripts(maxAgeDays?: number, dirOverride?: string): TranscriptPruneResult;
|
|
83
|
+
export declare function pruneTranscripts(maxAgeDays?: number, dirOverride?: string, maxTotalMb?: number): TranscriptPruneResult;
|
|
79
84
|
export interface WindowsAuditArgs {
|
|
80
85
|
enable?: string;
|
|
81
86
|
disable?: string;
|
|
@@ -40,6 +40,7 @@ exports.captureAuditPreInstallSnapshot = captureAuditPreInstallSnapshot;
|
|
|
40
40
|
exports.disableWindowsAudit = disableWindowsAudit;
|
|
41
41
|
exports.enableWindowsAudit = enableWindowsAudit;
|
|
42
42
|
exports.transcriptRetentionDays = transcriptRetentionDays;
|
|
43
|
+
exports.transcriptMaxMb = transcriptMaxMb;
|
|
43
44
|
exports.pruneTranscripts = pruneTranscripts;
|
|
44
45
|
exports.windowsAuditCommand = windowsAuditCommand;
|
|
45
46
|
const child_process_1 = require("child_process");
|
|
@@ -353,17 +354,29 @@ function enableWindowsAudit() {
|
|
|
353
354
|
// that FCD itself configured (path contains "FullCourtDefense") — an
|
|
354
355
|
// org-managed transcript share set via GPO is never cleaned by us.
|
|
355
356
|
const DEFAULT_TRANSCRIPT_RETENTION_DAYS = 30;
|
|
357
|
+
// Size budget alongside the age window: heavy PowerShell users produce
|
|
358
|
+
// ~200MB/day of transcripts, so 30 days of age-based retention alone can eat
|
|
359
|
+
// multiple GB of disk (a customer churn finding). Oldest files go first once
|
|
360
|
+
// the folder exceeds the budget, regardless of age.
|
|
361
|
+
const DEFAULT_TRANSCRIPT_MAX_MB = 500;
|
|
362
|
+
const MIN_TRANSCRIPT_MAX_MB = 50;
|
|
356
363
|
/** Retention window in days — env override for fleets that want tighter/looser. */
|
|
357
364
|
function transcriptRetentionDays() {
|
|
358
365
|
const raw = Number(process.env.FCD_TRANSCRIPT_RETENTION_DAYS);
|
|
359
366
|
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : DEFAULT_TRANSCRIPT_RETENTION_DAYS;
|
|
360
367
|
}
|
|
368
|
+
/** Size budget in MB — env override (FCD_TRANSCRIPT_MAX_MB, floor 50MB). */
|
|
369
|
+
function transcriptMaxMb() {
|
|
370
|
+
const raw = Number(process.env.FCD_TRANSCRIPT_MAX_MB);
|
|
371
|
+
return Number.isFinite(raw) && raw >= MIN_TRANSCRIPT_MAX_MB ? Math.floor(raw) : DEFAULT_TRANSCRIPT_MAX_MB;
|
|
372
|
+
}
|
|
361
373
|
/**
|
|
362
|
-
* Delete transcript files older than the retention window
|
|
363
|
-
*
|
|
364
|
-
*
|
|
374
|
+
* Delete transcript files older than the retention window, then enforce the
|
|
375
|
+
* size budget (oldest-first) on whatever survived. Never throws; locked/
|
|
376
|
+
* in-use files (an open PowerShell session's live transcript) are skipped and
|
|
377
|
+
* picked up on a later run. Empty date folders are removed.
|
|
365
378
|
*/
|
|
366
|
-
function pruneTranscripts(maxAgeDays = transcriptRetentionDays(), dirOverride) {
|
|
379
|
+
function pruneTranscripts(maxAgeDays = transcriptRetentionDays(), dirOverride, maxTotalMb = transcriptMaxMb()) {
|
|
367
380
|
if (process.platform !== 'win32' && !dirOverride)
|
|
368
381
|
return { pruned: 0, skipped: 'non-Windows' };
|
|
369
382
|
const dir = dirOverride ?? getWindowsAuditStatus().transcriptionPath;
|
|
@@ -375,6 +388,7 @@ function pruneTranscripts(maxAgeDays = transcriptRetentionDays(), dirOverride) {
|
|
|
375
388
|
return { pruned: 0, skipped: 'transcript dir does not exist' };
|
|
376
389
|
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
377
390
|
let pruned = 0;
|
|
391
|
+
const survivors = [];
|
|
378
392
|
const pruneDir = (target, depth) => {
|
|
379
393
|
let entries;
|
|
380
394
|
try {
|
|
@@ -394,16 +408,41 @@ function pruneTranscripts(maxAgeDays = transcriptRetentionDays(), dirOverride) {
|
|
|
394
408
|
}
|
|
395
409
|
catch { /* not empty — keep */ }
|
|
396
410
|
}
|
|
397
|
-
else
|
|
398
|
-
fs.
|
|
399
|
-
|
|
411
|
+
else {
|
|
412
|
+
const stat = fs.statSync(full);
|
|
413
|
+
if (stat.mtimeMs < cutoff) {
|
|
414
|
+
fs.unlinkSync(full);
|
|
415
|
+
pruned += 1;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
survivors.push({ path: full, mtimeMs: stat.mtimeMs, size: stat.size });
|
|
419
|
+
}
|
|
400
420
|
}
|
|
401
421
|
}
|
|
402
422
|
catch { /* locked (live session transcript) or race — next run */ }
|
|
403
423
|
}
|
|
404
424
|
};
|
|
405
425
|
pruneDir(dir, 0);
|
|
406
|
-
|
|
426
|
+
// Size budget: age retention alone is unbounded on chatty machines. Delete
|
|
427
|
+
// oldest-first until under budget; the newest transcript always survives so
|
|
428
|
+
// a live session's file is never the one we try to delete.
|
|
429
|
+
let prunedBySize = 0;
|
|
430
|
+
const budgetBytes = maxTotalMb * 1024 * 1024;
|
|
431
|
+
let totalBytes = survivors.reduce((sum, file) => sum + file.size, 0);
|
|
432
|
+
if (totalBytes > budgetBytes) {
|
|
433
|
+
survivors.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
434
|
+
for (const file of survivors.slice(0, -1)) {
|
|
435
|
+
if (totalBytes <= budgetBytes)
|
|
436
|
+
break;
|
|
437
|
+
try {
|
|
438
|
+
fs.unlinkSync(file.path);
|
|
439
|
+
totalBytes -= file.size;
|
|
440
|
+
prunedBySize += 1;
|
|
441
|
+
}
|
|
442
|
+
catch { /* locked — skip, next run */ }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { pruned, prunedBySize };
|
|
407
446
|
}
|
|
408
447
|
/**
|
|
409
448
|
* `fullcourtdefense windows-audit` — show (default) or enable (--enable)
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ const mcpGateway_1 = require("./commands/mcpGateway");
|
|
|
51
51
|
const installAll_1 = require("./commands/installAll");
|
|
52
52
|
const onboard_1 = require("./commands/onboard");
|
|
53
53
|
const uninstallAll_1 = require("./commands/uninstallAll");
|
|
54
|
+
const verifyRemoved_1 = require("./commands/verifyRemoved");
|
|
54
55
|
const autoProtect_1 = require("./commands/autoProtect");
|
|
55
56
|
const daemon_1 = require("./commands/daemon");
|
|
56
57
|
const watchdog_1 = require("./commands/watchdog");
|
|
@@ -134,11 +135,21 @@ function printHelp() {
|
|
|
134
135
|
Safe to re-run: completed steps are reused automatically
|
|
135
136
|
(--repair true re-runs them; --dry-run true previews;
|
|
136
137
|
--json true prints the transaction as JSON).
|
|
137
|
-
uninstall
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
uninstall COMPLETE removal in one command: daemon, self-heal schedule,
|
|
139
|
+
MCP gateway wraps, IDE hooks, terminal guards, discovery
|
|
140
|
+
schedule, credentials, caches, and config backups. On Windows
|
|
141
|
+
MSI installs it then launches the Windows uninstaller (one UAC
|
|
142
|
+
prompt) which removes the SYSTEM updater task, HKLM audit
|
|
143
|
+
policies, and the application itself.
|
|
144
|
+
detach Reverse onboard/install-all protection but KEEP machine
|
|
145
|
+
credentials and the installed app, for easy re-onboarding.
|
|
146
|
+
--purge true deletes local state too.
|
|
147
|
+
verify-removed
|
|
148
|
+
Prove this machine is back to its pre-FCD state: checks every
|
|
149
|
+
known FCD artifact (tasks, registry, files, config backups)
|
|
150
|
+
and lists anything left, with the command that removes it.
|
|
151
|
+
Exits 1 when remnants are found. Works via npx after the CLI
|
|
152
|
+
itself is uninstalled.
|
|
142
153
|
doctor First step. Checks outbound HTTPS access to FullCourtDefense.
|
|
143
154
|
login Enrolls this machine with a fleet enrollment token and saves
|
|
144
155
|
per-machine Shield credentials to ~/.fullcourtdefense.yml.
|
|
@@ -150,8 +161,9 @@ function printHelp() {
|
|
|
150
161
|
or --mcp-command needed. Use --hooks false to skip runtime hooks.
|
|
151
162
|
install Alias for install-all.
|
|
152
163
|
windows-audit Show Windows PowerShell audit coverage (ScriptBlock Logging +
|
|
153
|
-
Transcription).
|
|
154
|
-
|
|
164
|
+
Transcription). OFF by default (machine-wide policy + on-disk
|
|
165
|
+
transcripts are opt-in). Use --enable to turn it on, --disable
|
|
166
|
+
to remove both policy keys (one admin prompt either way).
|
|
155
167
|
install-shell-guard Block dangerous commands typed in a terminal (real-time,
|
|
156
168
|
offline rule cache): PowerShell on Windows, bash + zsh on macOS/Linux.
|
|
157
169
|
uninstall-shell-guard removes it; shell-guard-status shows coverage.
|
|
@@ -233,6 +245,9 @@ function printHelp() {
|
|
|
233
245
|
2. Run the one-command onboarding (connectivity + enroll + install + verify):
|
|
234
246
|
fullcourtdefense onboard --token <fleet-enrollment-token>
|
|
235
247
|
Or set FCD_ENROLL_TOKEN and run: fullcourtdefense onboard
|
|
248
|
+
Shows a trust disclosure (what gets installed, what data is sent
|
|
249
|
+
to your org's console) and asks for confirmation; automation/MDM
|
|
250
|
+
passes --accept-disclosure true (or FCD_ACCEPT_DISCLOSURE=1).
|
|
236
251
|
Ends with a green/red checklist per protection surface and exits
|
|
237
252
|
non-zero on failure (safe for MDM scripts).
|
|
238
253
|
|
|
@@ -670,10 +685,26 @@ async function main() {
|
|
|
670
685
|
clients: flags.clients,
|
|
671
686
|
config: flags.config,
|
|
672
687
|
purge: flags.purge,
|
|
688
|
+
localOnly: flags['local-only'],
|
|
689
|
+
reason: flags.reason,
|
|
673
690
|
};
|
|
674
691
|
await (0, uninstallAll_1.uninstallAllCommand)(args, config);
|
|
675
692
|
break;
|
|
676
693
|
}
|
|
694
|
+
case 'detach': {
|
|
695
|
+
const args = {
|
|
696
|
+
...buildGatewayArgs(),
|
|
697
|
+
clients: flags.clients,
|
|
698
|
+
config: flags.config,
|
|
699
|
+
purge: flags.purge,
|
|
700
|
+
};
|
|
701
|
+
await (0, uninstallAll_1.detachCommand)(args, config);
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
case 'verify-removed': {
|
|
705
|
+
await (0, verifyRemoved_1.verifyRemovedCommand)({ json: flags.json });
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
677
708
|
case 'onboard': {
|
|
678
709
|
const args = {
|
|
679
710
|
...buildInstallArgs(),
|
|
@@ -694,6 +725,7 @@ async function main() {
|
|
|
694
725
|
dryRun: flags['dry-run'],
|
|
695
726
|
json: flags.json,
|
|
696
727
|
noDaemon: flags['no-daemon'],
|
|
728
|
+
acceptDisclosure: flags['accept-disclosure'],
|
|
697
729
|
};
|
|
698
730
|
await (0, onboard_1.onboardCommand)(args, config);
|
|
699
731
|
break;
|
|
@@ -68,13 +68,16 @@ function writeCache(file, snapshot) {
|
|
|
68
68
|
catch { /* best-effort */ }
|
|
69
69
|
}
|
|
70
70
|
function toRuleAction(value) {
|
|
71
|
-
|
|
71
|
+
// Explicit 'block' is preserved (not treated as absent): warn-by-default
|
|
72
|
+
// items (e.g. env_workspace) need an org's explicit block override to
|
|
73
|
+
// survive parsing, otherwise the CLI's catalog default would resurrect warn.
|
|
74
|
+
return value === 'warn' || value === 'mask' || value === 'block' ? value : undefined;
|
|
72
75
|
}
|
|
73
76
|
function toSnapshot(data) {
|
|
74
77
|
if (!data || typeof data !== 'object')
|
|
75
78
|
return undefined;
|
|
76
|
-
//
|
|
77
|
-
//
|
|
79
|
+
// Anything unrecognized falls back to 'block' (fail strict); recognized
|
|
80
|
+
// entries — including explicit 'block' — are kept verbatim.
|
|
78
81
|
let itemActions;
|
|
79
82
|
if (data.itemActions && typeof data.itemActions === 'object' && !Array.isArray(data.itemActions)) {
|
|
80
83
|
for (const [itemId, raw] of Object.entries(data.itemActions)) {
|
package/dist/selfTest.js
CHANGED
|
@@ -145,8 +145,39 @@ async function runDeepSelfTest(input) {
|
|
|
145
145
|
add({ id: 'watchdog_task', label: 'Watchdog task registered', ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
// 9. Windowless task principals. FCD tasks must run with the S4U logon type:
|
|
149
|
+
// an InteractiveToken principal running console node.exe flashes a visible
|
|
150
|
+
// console window on every trigger (the "5-minute flicker" complaint class —
|
|
151
|
+
// <Hidden>true</Hidden> does NOT suppress it). Machines where the S4U
|
|
152
|
+
// migration did not converge surface here instead of a user email.
|
|
153
|
+
if (process.platform === 'win32') {
|
|
154
|
+
const interactive = listInteractiveFcdTasks();
|
|
155
|
+
add({
|
|
156
|
+
id: 'windowless_tasks',
|
|
157
|
+
label: 'Scheduled tasks are windowless (S4U)',
|
|
158
|
+
ok: interactive.length === 0,
|
|
159
|
+
detail: interactive.length === 0
|
|
160
|
+
? 'no FCD task runs with an InteractiveToken principal'
|
|
161
|
+
: `InteractiveToken principal (visible console flash) on: ${interactive.join(', ')} — daemon migration to S4U has not converged`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
148
164
|
return { ok: checks.every(check => check.ok), checks, ranAt: new Date().toISOString() };
|
|
149
165
|
}
|
|
166
|
+
/** FCD per-user tasks whose XML still declares an InteractiveToken principal. */
|
|
167
|
+
const WINDOWED_TASK_CANDIDATES = ['FullCourtDefense Daemon', 'FullCourtDefense Daemon Watchdog', 'FullCourtDefense Auto-Protect', 'FullCourtDefenseDesktopDiscover'];
|
|
168
|
+
function listInteractiveFcdTasks() {
|
|
169
|
+
const offenders = [];
|
|
170
|
+
for (const taskName of WINDOWED_TASK_CANDIDATES) {
|
|
171
|
+
const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName, '/XML'], {
|
|
172
|
+
encoding: 'utf8', windowsHide: true, timeout: 10_000,
|
|
173
|
+
});
|
|
174
|
+
if (query.status !== 0 || !query.stdout)
|
|
175
|
+
continue; // absent — nothing to flash
|
|
176
|
+
if (/<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(query.stdout))
|
|
177
|
+
offenders.push(taskName);
|
|
178
|
+
}
|
|
179
|
+
return offenders;
|
|
180
|
+
}
|
|
150
181
|
/** Compact one-line summary for the machine-action result card. */
|
|
151
182
|
function summarizeSelfTest(report) {
|
|
152
183
|
const failed = report.checks.filter(check => !check.ok);
|
package/dist/version.json
CHANGED