fullcourtdefense-cli 1.22.3 → 1.22.5
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/commands/daemon.js +18 -0
- package/dist/commands/mcpGateway.js +37 -12
- package/dist/commands/windowsAudit.d.ts +38 -0
- package/dist/commands/windowsAudit.js +193 -0
- package/dist/index.js +3 -2
- package/dist/version.json +1 -1
- package/package.json +3 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -75,6 +75,7 @@ const cmdGuard_1 = require("./cmdGuard");
|
|
|
75
75
|
const machineActionVerify_1 = require("../machineActionVerify");
|
|
76
76
|
const desktopChatGuard_1 = require("./desktopChatGuard");
|
|
77
77
|
const honeypot_1 = require("../honeypot");
|
|
78
|
+
const windowsAudit_1 = require("./windowsAudit");
|
|
78
79
|
const COLOR = {
|
|
79
80
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
80
81
|
red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
@@ -1157,6 +1158,21 @@ async function runDaemon(args, config) {
|
|
|
1157
1158
|
}, RESCAN_INTERVAL_MS);
|
|
1158
1159
|
const bundleTimer = setInterval(() => { void pollBundle(); }, BUNDLE_POLL_MS);
|
|
1159
1160
|
const heartbeatTimer = setInterval(() => { void heartbeat(); }, HEARTBEAT_INTERVAL_MS);
|
|
1161
|
+
// PowerShell transcript retention (Windows): the Transcription policy FCD
|
|
1162
|
+
// enables writes a file per session forever — prune anything older than the
|
|
1163
|
+
// retention window once a day (plus once shortly after boot, so laptops
|
|
1164
|
+
// that never stay up 24h still get cleaned). Only touches FCD-managed
|
|
1165
|
+
// transcript folders; see pruneTranscripts.
|
|
1166
|
+
const runTranscriptPrune = () => {
|
|
1167
|
+
try {
|
|
1168
|
+
const result = (0, windowsAudit_1.pruneTranscripts)();
|
|
1169
|
+
if (result.pruned > 0)
|
|
1170
|
+
log(`Transcript retention: pruned ${result.pruned} transcript file(s) older than ${(0, windowsAudit_1.transcriptRetentionDays)()} day(s).`);
|
|
1171
|
+
}
|
|
1172
|
+
catch { /* never let housekeeping hurt the daemon */ }
|
|
1173
|
+
};
|
|
1174
|
+
const transcriptPruneBootTimer = setTimeout(runTranscriptPrune, 5 * 60_000);
|
|
1175
|
+
const transcriptPruneTimer = setInterval(runTranscriptPrune, 24 * 60 * 60_000);
|
|
1160
1176
|
const shutdown = (signal) => {
|
|
1161
1177
|
if (stopped)
|
|
1162
1178
|
return;
|
|
@@ -1165,6 +1181,8 @@ async function runDaemon(args, config) {
|
|
|
1165
1181
|
clearInterval(rescanTimer);
|
|
1166
1182
|
clearInterval(bundleTimer);
|
|
1167
1183
|
clearInterval(heartbeatTimer);
|
|
1184
|
+
clearTimeout(transcriptPruneBootTimer);
|
|
1185
|
+
clearInterval(transcriptPruneTimer);
|
|
1168
1186
|
clearTimeout(discoverCatchUpBootTimer);
|
|
1169
1187
|
clearInterval(discoverCatchUpTimer);
|
|
1170
1188
|
if (initialDiscoverTimer)
|
|
@@ -84,6 +84,17 @@ const CLIENT_LABELS = {
|
|
|
84
84
|
windsurf: 'Windsurf',
|
|
85
85
|
vscode: 'VS Code',
|
|
86
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Default configured-context template per client. Single source of truth —
|
|
89
|
+
* protect-all wraps configs for EVERY client from one shared GatewayConfig,
|
|
90
|
+
* and deriving the objective from that shared config gave Codex/Claude/etc.
|
|
91
|
+
* entries the Cursor template ("from Cursor" on a Codex event — a customer-
|
|
92
|
+
* reported labeling bug). This is a CONFIGURED template, not observed
|
|
93
|
+
* behavior; the console labels it accordingly.
|
|
94
|
+
*/
|
|
95
|
+
function defaultUserObjective(agentClient) {
|
|
96
|
+
return `Developer requested this action from ${CLIENT_LABELS[agentClient] || 'an MCP client'}.`;
|
|
97
|
+
}
|
|
87
98
|
function parseHeadersFlag(value) {
|
|
88
99
|
if (!value?.trim())
|
|
89
100
|
return undefined;
|
|
@@ -206,7 +217,8 @@ function resolveGatewayConfig(args, config, defaults = {}) {
|
|
|
206
217
|
agentClient,
|
|
207
218
|
developerName,
|
|
208
219
|
environment: args.environment || process.env.FCD_ENVIRONMENT || 'developer-workstation',
|
|
209
|
-
userObjective: args.userObjective || process.env.FCD_USER_OBJECTIVE || defaults.userObjective ||
|
|
220
|
+
userObjective: args.userObjective || process.env.FCD_USER_OBJECTIVE || defaults.userObjective || defaultUserObjective(agentClient),
|
|
221
|
+
userObjectiveExplicit: !!(args.userObjective || process.env.FCD_USER_OBJECTIVE),
|
|
210
222
|
authority: args.authority || process.env.FCD_AUTHORITY || 'developer',
|
|
211
223
|
approvalMode: args.approvalMode === 'block' ? 'block' : 'wait',
|
|
212
224
|
approvalTimeoutMs: Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 900000,
|
|
@@ -606,7 +618,11 @@ class AgentGuardApi {
|
|
|
606
618
|
agentName: this.config.agentName,
|
|
607
619
|
toolName: input.toolName,
|
|
608
620
|
operation: input.operation,
|
|
609
|
-
|
|
621
|
+
// Data minimization: this is a pure audit-record call (the decision was
|
|
622
|
+
// already made by check-tool-call), and the backend persists only
|
|
623
|
+
// argsSummary — so the RAW arguments are redacted/truncated ON THIS
|
|
624
|
+
// MACHINE and never sent here. Raw args still travel on the policy
|
|
625
|
+
// check itself, where the evaluation genuinely needs them.
|
|
610
626
|
argsSummary: summarizeToolArgs(input.toolArgs, this.config.developerName, this.config.agentClient),
|
|
611
627
|
environment: this.config.environment,
|
|
612
628
|
userObjective: this.config.userObjective,
|
|
@@ -1352,7 +1368,6 @@ async function installCursorMcpGatewayCommand(args, config) {
|
|
|
1352
1368
|
const baseGatewayConfig = resolveGatewayConfig(args, config, {
|
|
1353
1369
|
agentClient: 'cursor',
|
|
1354
1370
|
agentNamePrefix: 'cursor',
|
|
1355
|
-
userObjective: 'Developer requested this action from Cursor.',
|
|
1356
1371
|
});
|
|
1357
1372
|
const projectScope = args.project === 'true';
|
|
1358
1373
|
const file = cursorMcpPath(projectScope);
|
|
@@ -1743,6 +1758,22 @@ function applyPerServerAgentName(gatewayConfig, args, serverName) {
|
|
|
1743
1758
|
agentName: perServerAgentName(gatewayConfig.developerName, gatewayConfig.agentClient, serverName),
|
|
1744
1759
|
};
|
|
1745
1760
|
}
|
|
1761
|
+
/**
|
|
1762
|
+
* The per-server config protect-all bakes into each wrapped entry. The shared
|
|
1763
|
+
* base config was resolved ONCE (defaulting to Cursor), so every client-
|
|
1764
|
+
* specific field must be re-derived here: the client itself, the per-server
|
|
1765
|
+
* agent name, and the user-objective template — a Codex entry must say
|
|
1766
|
+
* "from Codex", not inherit the base config's Cursor text. An explicit
|
|
1767
|
+
* --user-objective / FCD_USER_OBJECTIVE still wins for all clients.
|
|
1768
|
+
*/
|
|
1769
|
+
function perServerGatewayConfig(gatewayConfig, agentClient, serverName) {
|
|
1770
|
+
return {
|
|
1771
|
+
...gatewayConfig,
|
|
1772
|
+
agentClient,
|
|
1773
|
+
agentName: perServerAgentName(gatewayConfig.developerName, agentClient, serverName),
|
|
1774
|
+
userObjective: gatewayConfig.userObjectiveExplicit ? gatewayConfig.userObjective : defaultUserObjective(agentClient),
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1746
1777
|
function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
1747
1778
|
const stats = newWrapStats();
|
|
1748
1779
|
let json;
|
|
@@ -1773,7 +1804,7 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
|
1773
1804
|
const credentialPairStale = existingArgs.includes('--shield-id') && !existingArgs.includes('--shield-key');
|
|
1774
1805
|
const downstream = credentialPairStale ? extractFullyUnwrappedDownstream(entry) : null;
|
|
1775
1806
|
if (!credentialPairStale || downstream) {
|
|
1776
|
-
const perServer =
|
|
1807
|
+
const perServer = perServerGatewayConfig(gatewayConfig, agentClient, name);
|
|
1777
1808
|
entry.command = nodeExe;
|
|
1778
1809
|
// Path-only upgrades preserve every downstream/config flag exactly.
|
|
1779
1810
|
// The credential-pair bug requires a safe rebuild from downstream.
|
|
@@ -1788,7 +1819,7 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
|
1788
1819
|
stats.skippedManaged.push(name);
|
|
1789
1820
|
continue;
|
|
1790
1821
|
}
|
|
1791
|
-
const perServer =
|
|
1822
|
+
const perServer = perServerGatewayConfig(gatewayConfig, agentClient, name);
|
|
1792
1823
|
if (typeof entry.command !== 'string' || !entry.command) {
|
|
1793
1824
|
// Remote HTTP/SSE MCP: wrap the URL behind a local stdio gateway that
|
|
1794
1825
|
// enforces policies, then forwards over Streamable HTTP. Original url,
|
|
@@ -2003,7 +2034,7 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
|
|
|
2003
2034
|
}
|
|
2004
2035
|
continue;
|
|
2005
2036
|
}
|
|
2006
|
-
const perServer =
|
|
2037
|
+
const perServer = perServerGatewayConfig(gatewayConfig, 'codex', section.name);
|
|
2007
2038
|
const wrappedArgs = buildGatewayCommandArgs(perServer, { kind: 'stdio', command: section.command, args: section.args || [] }, INSTALL_GATEWAY_ARGS);
|
|
2008
2039
|
const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
|
|
2009
2040
|
lines[section.cmdLine] = `${indent}command = ${JSON.stringify(nodeExe)}`;
|
|
@@ -2187,7 +2218,6 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
2187
2218
|
const baseGatewayConfig = resolveGatewayConfig(args, config, {
|
|
2188
2219
|
agentClient: 'claude-code',
|
|
2189
2220
|
agentNamePrefix: 'claude-code',
|
|
2190
|
-
userObjective: 'Developer requested this action from Claude Code.',
|
|
2191
2221
|
});
|
|
2192
2222
|
const scope = normalizeClaudeCodeScope(args.scope);
|
|
2193
2223
|
const nodeExe = process.execPath;
|
|
@@ -2251,7 +2281,6 @@ async function installClaudeDesktopMcpGatewayCommand(args, config) {
|
|
|
2251
2281
|
const baseGatewayConfig = resolveGatewayConfig(args, config, {
|
|
2252
2282
|
agentClient: 'claude-desktop',
|
|
2253
2283
|
agentNamePrefix: 'claude-desktop',
|
|
2254
|
-
userObjective: 'Developer requested this action from Claude Desktop.',
|
|
2255
2284
|
});
|
|
2256
2285
|
const targets = (0, discoverPaths_1.claudeDesktopInstallTargets)(args.configPath);
|
|
2257
2286
|
const nodeExe = process.execPath;
|
|
@@ -2274,7 +2303,6 @@ async function installCodexMcpGatewayCommand(args, config) {
|
|
|
2274
2303
|
const baseGatewayConfig = resolveGatewayConfig(args, config, {
|
|
2275
2304
|
agentClient: 'codex',
|
|
2276
2305
|
agentNamePrefix: 'codex',
|
|
2277
|
-
userObjective: 'Developer requested this action from Codex.',
|
|
2278
2306
|
});
|
|
2279
2307
|
const projectScope = args.project === 'true';
|
|
2280
2308
|
const file = codexConfigPath(projectScope);
|
|
@@ -2293,7 +2321,6 @@ async function installGeminiMcpGatewayCommand(args, config) {
|
|
|
2293
2321
|
const baseGatewayConfig = resolveGatewayConfig(args, config, {
|
|
2294
2322
|
agentClient: 'gemini-cli',
|
|
2295
2323
|
agentNamePrefix: 'gemini',
|
|
2296
|
-
userObjective: 'Developer requested this action from Gemini CLI.',
|
|
2297
2324
|
});
|
|
2298
2325
|
const projectScope = args.project === 'true';
|
|
2299
2326
|
const file = geminiSettingsPath(projectScope);
|
|
@@ -2312,7 +2339,6 @@ async function installWindsurfMcpGatewayCommand(args, config) {
|
|
|
2312
2339
|
const baseGatewayConfig = resolveGatewayConfig(args, config, {
|
|
2313
2340
|
agentClient: 'windsurf',
|
|
2314
2341
|
agentNamePrefix: 'windsurf',
|
|
2315
|
-
userObjective: 'Developer requested this action from Windsurf.',
|
|
2316
2342
|
});
|
|
2317
2343
|
const file = windsurfConfigPath();
|
|
2318
2344
|
const nodeExe = process.execPath;
|
|
@@ -2330,7 +2356,6 @@ async function installVscodeMcpGatewayCommand(args, config) {
|
|
|
2330
2356
|
const baseGatewayConfig = resolveGatewayConfig(args, config, {
|
|
2331
2357
|
agentClient: 'vscode',
|
|
2332
2358
|
agentNamePrefix: 'vscode',
|
|
2333
|
-
userObjective: 'Developer requested this action from VS Code.',
|
|
2334
2359
|
});
|
|
2335
2360
|
const projectScope = args.project !== 'false';
|
|
2336
2361
|
const nodeExe = process.execPath;
|
|
@@ -24,6 +24,22 @@ export declare function getWindowsAuditStatus(): WindowsAuditStatus;
|
|
|
24
24
|
* Never throws — telemetry/discovery must not break on a probe failure.
|
|
25
25
|
*/
|
|
26
26
|
export declare function getShellAuditReport(): ShellAuditReport | undefined;
|
|
27
|
+
export interface AuditPreInstallSnapshot {
|
|
28
|
+
capturedAt: string;
|
|
29
|
+
scriptBlockLogging: {
|
|
30
|
+
present: boolean;
|
|
31
|
+
enableValue?: number;
|
|
32
|
+
};
|
|
33
|
+
transcription: {
|
|
34
|
+
present: boolean;
|
|
35
|
+
enableValue?: number;
|
|
36
|
+
invocationHeader?: number;
|
|
37
|
+
outputDirectory?: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export declare function auditPreInstallSnapshotPath(): string;
|
|
41
|
+
/** Record the audit keys as they were BEFORE FCD changes them (first contact only). Best-effort, never throws. */
|
|
42
|
+
export declare function captureAuditPreInstallSnapshot(): void;
|
|
27
43
|
export interface EnableWindowsAuditResult {
|
|
28
44
|
ok: boolean;
|
|
29
45
|
status: WindowsAuditStatus;
|
|
@@ -32,6 +48,13 @@ export interface EnableWindowsAuditResult {
|
|
|
32
48
|
/** True when the user saw (and answered) a UAC elevation prompt. */
|
|
33
49
|
promptShown: boolean;
|
|
34
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Disable ScriptBlock Logging + Transcription (removes the HKLM policy keys)
|
|
53
|
+
* via one UAC prompt. Mirrors enableWindowsAudit's fail-open contract. If the
|
|
54
|
+
* keys are enforced by GPO/Intune they will reappear on the next policy sync
|
|
55
|
+
* — the message says so.
|
|
56
|
+
*/
|
|
57
|
+
export declare function disableWindowsAudit(): EnableWindowsAuditResult;
|
|
35
58
|
/**
|
|
36
59
|
* Enable ScriptBlock Logging + Transcription via ONE UAC elevation prompt.
|
|
37
60
|
* Skips silently when already fully enabled. Never throws; on any failure
|
|
@@ -39,8 +62,23 @@ export interface EnableWindowsAuditResult {
|
|
|
39
62
|
* message — callers must treat that as non-blocking.
|
|
40
63
|
*/
|
|
41
64
|
export declare function enableWindowsAudit(): EnableWindowsAuditResult;
|
|
65
|
+
export interface TranscriptPruneResult {
|
|
66
|
+
/** Files deleted in this run. */
|
|
67
|
+
pruned: number;
|
|
68
|
+
/** Set when pruning was skipped entirely (not enabled / foreign dir / non-Windows). */
|
|
69
|
+
skipped?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Retention window in days — env override for fleets that want tighter/looser. */
|
|
72
|
+
export declare function transcriptRetentionDays(): number;
|
|
73
|
+
/**
|
|
74
|
+
* Delete transcript files older than the retention window. Never throws;
|
|
75
|
+
* locked/in-use files (an open PowerShell session's live transcript) are
|
|
76
|
+
* skipped and picked up on a later run. Empty date folders are removed.
|
|
77
|
+
*/
|
|
78
|
+
export declare function pruneTranscripts(maxAgeDays?: number, dirOverride?: string): TranscriptPruneResult;
|
|
42
79
|
export interface WindowsAuditArgs {
|
|
43
80
|
enable?: string;
|
|
81
|
+
disable?: string;
|
|
44
82
|
}
|
|
45
83
|
/**
|
|
46
84
|
* `fullcourtdefense windows-audit` — show (default) or enable (--enable)
|
|
@@ -35,7 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getWindowsAuditStatus = getWindowsAuditStatus;
|
|
37
37
|
exports.getShellAuditReport = getShellAuditReport;
|
|
38
|
+
exports.auditPreInstallSnapshotPath = auditPreInstallSnapshotPath;
|
|
39
|
+
exports.captureAuditPreInstallSnapshot = captureAuditPreInstallSnapshot;
|
|
40
|
+
exports.disableWindowsAudit = disableWindowsAudit;
|
|
38
41
|
exports.enableWindowsAudit = enableWindowsAudit;
|
|
42
|
+
exports.transcriptRetentionDays = transcriptRetentionDays;
|
|
43
|
+
exports.pruneTranscripts = pruneTranscripts;
|
|
39
44
|
exports.windowsAuditCommand = windowsAuditCommand;
|
|
40
45
|
const child_process_1 = require("child_process");
|
|
41
46
|
const fs = __importStar(require("fs"));
|
|
@@ -132,6 +137,56 @@ function defaultTranscriptDir() {
|
|
|
132
137
|
const programData = process.env.ProgramData || 'C:\\ProgramData';
|
|
133
138
|
return path.join(programData, 'FullCourtDefense', 'Transcripts');
|
|
134
139
|
}
|
|
140
|
+
function auditPreInstallSnapshotPath() {
|
|
141
|
+
const programData = process.env.ProgramData || 'C:\\ProgramData';
|
|
142
|
+
return path.join(programData, 'FullCourtDefense', 'audit-preinstall.json');
|
|
143
|
+
}
|
|
144
|
+
/** Record the audit keys as they were BEFORE FCD changes them (first contact only). Best-effort, never throws. */
|
|
145
|
+
function captureAuditPreInstallSnapshot() {
|
|
146
|
+
try {
|
|
147
|
+
const file = auditPreInstallSnapshotPath();
|
|
148
|
+
const trDir = regString(TRANSCRIPTION_KEY, 'OutputDirectory');
|
|
149
|
+
// Upgrade guard: when transcription already points at OUR folder, the
|
|
150
|
+
// current keys are FCD's own configuration written by a pre-snapshot
|
|
151
|
+
// version (<=1.22.3) — NOT a pre-install state. Recording it would make
|
|
152
|
+
// uninstall "restore" FCD's keys instead of removing them. Skip the
|
|
153
|
+
// snapshot; uninstall's conservative heuristic handles FCD-provenance
|
|
154
|
+
// keys correctly.
|
|
155
|
+
const fcdProvenance = typeof trDir === 'string' && /FullCourtDefense/i.test(trDir);
|
|
156
|
+
if (fs.existsSync(file)) {
|
|
157
|
+
// Self-heal snapshots already written by 1.22.4.0 on upgraded machines:
|
|
158
|
+
// a "pre-install" transcription directory inside an FCD folder is
|
|
159
|
+
// provably bogus (FCD did not exist before FCD was installed).
|
|
160
|
+
try {
|
|
161
|
+
const existing = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
162
|
+
const dir = existing?.transcription?.outputDirectory;
|
|
163
|
+
if (typeof dir === 'string' && /FullCourtDefense/i.test(dir))
|
|
164
|
+
fs.unlinkSync(file);
|
|
165
|
+
}
|
|
166
|
+
catch { /* unreadable snapshot: leave it — the uninstall branch parses defensively */ }
|
|
167
|
+
if (fs.existsSync(file))
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (fcdProvenance)
|
|
171
|
+
return;
|
|
172
|
+
const sblValue = regDword(SBL_KEY, 'EnableScriptBlockLogging');
|
|
173
|
+
const trValue = regDword(TRANSCRIPTION_KEY, 'EnableTranscripting');
|
|
174
|
+
const trHeader = regDword(TRANSCRIPTION_KEY, 'EnableInvocationHeader');
|
|
175
|
+
const snapshot = {
|
|
176
|
+
capturedAt: new Date().toISOString(),
|
|
177
|
+
scriptBlockLogging: { present: sblValue !== undefined, enableValue: sblValue },
|
|
178
|
+
transcription: {
|
|
179
|
+
present: trValue !== undefined || trHeader !== undefined || trDir !== undefined,
|
|
180
|
+
enableValue: trValue,
|
|
181
|
+
invocationHeader: trHeader,
|
|
182
|
+
outputDirectory: trDir,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
186
|
+
fs.writeFileSync(file, JSON.stringify(snapshot, null, 2), 'utf8');
|
|
187
|
+
}
|
|
188
|
+
catch { /* best-effort: without a snapshot, uninstall falls back to the conservative heuristic */ }
|
|
189
|
+
}
|
|
135
190
|
/** The elevated payload: sets both policy keys + creates the transcript dir. */
|
|
136
191
|
function buildEnableScript(transcriptDir) {
|
|
137
192
|
return [
|
|
@@ -148,6 +203,74 @@ function buildEnableScript(transcriptDir) {
|
|
|
148
203
|
`New-ItemProperty -Path $tr -Name OutputDirectory -Value $dir -PropertyType String -Force | Out-Null`,
|
|
149
204
|
].join('\r\n') + '\r\n';
|
|
150
205
|
}
|
|
206
|
+
/** The elevated disable payload: removes both policy keys (audit off machine-wide). */
|
|
207
|
+
function buildDisableScript() {
|
|
208
|
+
return [
|
|
209
|
+
`$ErrorActionPreference = 'SilentlyContinue'`,
|
|
210
|
+
`Remove-Item -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ScriptBlockLogging' -Recurse -Force`,
|
|
211
|
+
`Remove-Item -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\Transcription' -Recurse -Force`,
|
|
212
|
+
`exit 0`,
|
|
213
|
+
].join('\r\n') + '\r\n';
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Disable ScriptBlock Logging + Transcription (removes the HKLM policy keys)
|
|
217
|
+
* via one UAC prompt. Mirrors enableWindowsAudit's fail-open contract. If the
|
|
218
|
+
* keys are enforced by GPO/Intune they will reappear on the next policy sync
|
|
219
|
+
* — the message says so.
|
|
220
|
+
*/
|
|
221
|
+
function disableWindowsAudit() {
|
|
222
|
+
const before = getWindowsAuditStatus();
|
|
223
|
+
if (!before.supported) {
|
|
224
|
+
return { ok: false, status: before, promptShown: false, message: 'PowerShell audit logging is a Windows feature — skipped on this OS.' };
|
|
225
|
+
}
|
|
226
|
+
if (!before.scriptBlockLogging && !before.transcription) {
|
|
227
|
+
return { ok: true, status: before, promptShown: false, message: 'PowerShell audit logging is already disabled.' };
|
|
228
|
+
}
|
|
229
|
+
const scriptPath = path.join(os.tmpdir(), `fcd-disable-audit-${process.pid}.ps1`);
|
|
230
|
+
const elevatePath = path.join(os.tmpdir(), `fcd-elevate-disable-audit-${process.pid}.ps1`);
|
|
231
|
+
let promptShown = false;
|
|
232
|
+
try {
|
|
233
|
+
fs.writeFileSync(scriptPath, buildDisableScript(), 'utf8');
|
|
234
|
+
const quotedPayload = `"${scriptPath}"`.replace(/'/g, "''");
|
|
235
|
+
fs.writeFileSync(elevatePath, [
|
|
236
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
237
|
+
`try {`,
|
|
238
|
+
` $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','${quotedPayload}')`,
|
|
239
|
+
` exit $p.ExitCode`,
|
|
240
|
+
`} catch {`,
|
|
241
|
+
` exit 1`,
|
|
242
|
+
`}`,
|
|
243
|
+
].join('\r\n') + '\r\n', 'utf8');
|
|
244
|
+
promptShown = true;
|
|
245
|
+
(0, child_process_1.spawnSync)('powershell.exe', [
|
|
246
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', elevatePath,
|
|
247
|
+
], { encoding: 'utf8', windowsHide: true, timeout: 120000 });
|
|
248
|
+
const after = getWindowsAuditStatus();
|
|
249
|
+
if (!after.scriptBlockLogging && !after.transcription) {
|
|
250
|
+
return { ok: true, status: after, promptShown, message: 'PowerShell audit logging disabled — both policy keys removed. Existing transcripts were NOT deleted.' };
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ok: false, status: after, promptShown,
|
|
254
|
+
message: 'PowerShell audit logging is still enabled — the admin prompt was declined, or the keys are enforced by GPO/Intune (they re-apply on policy sync). Ask IT to disable via GPO in that case.',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false, status: getWindowsAuditStatus(), promptShown,
|
|
260
|
+
message: `PowerShell audit logging could not be disabled (${error instanceof Error ? error.message : String(error)}).`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
try {
|
|
265
|
+
fs.unlinkSync(scriptPath);
|
|
266
|
+
}
|
|
267
|
+
catch { /* best effort */ }
|
|
268
|
+
try {
|
|
269
|
+
fs.unlinkSync(elevatePath);
|
|
270
|
+
}
|
|
271
|
+
catch { /* best effort */ }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
151
274
|
/**
|
|
152
275
|
* Enable ScriptBlock Logging + Transcription via ONE UAC elevation prompt.
|
|
153
276
|
* Skips silently when already fully enabled. Never throws; on any failure
|
|
@@ -159,6 +282,9 @@ function enableWindowsAudit() {
|
|
|
159
282
|
if (!before.supported) {
|
|
160
283
|
return { ok: false, status: before, promptShown: false, message: 'PowerShell audit logging is a Windows feature — skipped on this OS.' };
|
|
161
284
|
}
|
|
285
|
+
// Snapshot BEFORE any change (and before the already-enabled early return:
|
|
286
|
+
// "already enabled by the org" is exactly the state uninstall must restore).
|
|
287
|
+
captureAuditPreInstallSnapshot();
|
|
162
288
|
if (before.scriptBlockLogging && before.transcription) {
|
|
163
289
|
return { ok: true, status: before, promptShown: false, message: 'PowerShell audit logging already enabled (ScriptBlock Logging + Transcription).' };
|
|
164
290
|
}
|
|
@@ -219,6 +345,66 @@ function enableWindowsAudit() {
|
|
|
219
345
|
catch { /* best effort */ }
|
|
220
346
|
}
|
|
221
347
|
}
|
|
348
|
+
// ── Transcript retention ────────────────────────────────────────────────────
|
|
349
|
+
// Transcription writes a file per PowerShell session into date-named
|
|
350
|
+
// subfolders; on a busy developer machine that grows unbounded (a customer
|
|
351
|
+
// audit finding). The daemon prunes files older than the retention window
|
|
352
|
+
// once a day. Deliberately conservative: it only ever touches a directory
|
|
353
|
+
// that FCD itself configured (path contains "FullCourtDefense") — an
|
|
354
|
+
// org-managed transcript share set via GPO is never cleaned by us.
|
|
355
|
+
const DEFAULT_TRANSCRIPT_RETENTION_DAYS = 30;
|
|
356
|
+
/** Retention window in days — env override for fleets that want tighter/looser. */
|
|
357
|
+
function transcriptRetentionDays() {
|
|
358
|
+
const raw = Number(process.env.FCD_TRANSCRIPT_RETENTION_DAYS);
|
|
359
|
+
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : DEFAULT_TRANSCRIPT_RETENTION_DAYS;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Delete transcript files older than the retention window. Never throws;
|
|
363
|
+
* locked/in-use files (an open PowerShell session's live transcript) are
|
|
364
|
+
* skipped and picked up on a later run. Empty date folders are removed.
|
|
365
|
+
*/
|
|
366
|
+
function pruneTranscripts(maxAgeDays = transcriptRetentionDays(), dirOverride) {
|
|
367
|
+
if (process.platform !== 'win32' && !dirOverride)
|
|
368
|
+
return { pruned: 0, skipped: 'non-Windows' };
|
|
369
|
+
const dir = dirOverride ?? getWindowsAuditStatus().transcriptionPath;
|
|
370
|
+
if (!dir)
|
|
371
|
+
return { pruned: 0, skipped: 'transcription not configured' };
|
|
372
|
+
if (!/fullcourtdefense/i.test(dir))
|
|
373
|
+
return { pruned: 0, skipped: `transcript dir not FCD-managed (${dir})` };
|
|
374
|
+
if (!fs.existsSync(dir))
|
|
375
|
+
return { pruned: 0, skipped: 'transcript dir does not exist' };
|
|
376
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
377
|
+
let pruned = 0;
|
|
378
|
+
const pruneDir = (target, depth) => {
|
|
379
|
+
let entries;
|
|
380
|
+
try {
|
|
381
|
+
entries = fs.readdirSync(target, { withFileTypes: true });
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
const full = path.join(target, entry.name);
|
|
388
|
+
try {
|
|
389
|
+
if (entry.isDirectory()) {
|
|
390
|
+
if (depth < 2)
|
|
391
|
+
pruneDir(full, depth + 1);
|
|
392
|
+
try {
|
|
393
|
+
fs.rmdirSync(full);
|
|
394
|
+
}
|
|
395
|
+
catch { /* not empty — keep */ }
|
|
396
|
+
}
|
|
397
|
+
else if (fs.statSync(full).mtimeMs < cutoff) {
|
|
398
|
+
fs.unlinkSync(full);
|
|
399
|
+
pruned += 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch { /* locked (live session transcript) or race — next run */ }
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
pruneDir(dir, 0);
|
|
406
|
+
return { pruned };
|
|
407
|
+
}
|
|
222
408
|
/**
|
|
223
409
|
* `fullcourtdefense windows-audit` — show (default) or enable (--enable)
|
|
224
410
|
* Windows PowerShell audit coverage on this machine.
|
|
@@ -235,6 +421,13 @@ async function windowsAuditCommand(args) {
|
|
|
235
421
|
console.log(result.ok ? `\x1b[32m✓ ${result.message}\x1b[0m` : `\x1b[33m⚠ ${result.message}\x1b[0m`);
|
|
236
422
|
return;
|
|
237
423
|
}
|
|
424
|
+
if (args.disable === 'true') {
|
|
425
|
+
console.log('Disabling PowerShell audit logging (removes both HKLM policy keys)…');
|
|
426
|
+
console.log('\x1b[2mWindows will show an admin approval prompt (UAC).\x1b[0m');
|
|
427
|
+
const result = disableWindowsAudit();
|
|
428
|
+
console.log(result.ok ? `\x1b[32m✓ ${result.message}\x1b[0m` : `\x1b[33m⚠ ${result.message}\x1b[0m`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
238
431
|
const status = getWindowsAuditStatus();
|
|
239
432
|
const mark = (on) => (on ? '\x1b[32m✓ enabled\x1b[0m' : '\x1b[31m✗ disabled\x1b[0m');
|
|
240
433
|
console.log('\n\x1b[1mWindows PowerShell audit coverage\x1b[0m');
|
package/dist/index.js
CHANGED
|
@@ -150,7 +150,8 @@ function printHelp() {
|
|
|
150
150
|
or --mcp-command needed. Use --hooks false to skip runtime hooks.
|
|
151
151
|
install Alias for install-all.
|
|
152
152
|
windows-audit Show Windows PowerShell audit coverage (ScriptBlock Logging +
|
|
153
|
-
Transcription). Use --enable to turn it on
|
|
153
|
+
Transcription). Use --enable to turn it on, --disable to remove
|
|
154
|
+
both policy keys (one admin prompt either way).
|
|
154
155
|
install-shell-guard Block dangerous commands typed in a terminal (real-time,
|
|
155
156
|
offline rule cache): PowerShell on Windows, bash + zsh on macOS/Linux.
|
|
156
157
|
uninstall-shell-guard removes it; shell-guard-status shows coverage.
|
|
@@ -698,7 +699,7 @@ async function main() {
|
|
|
698
699
|
break;
|
|
699
700
|
}
|
|
700
701
|
case 'windows-audit': {
|
|
701
|
-
const args = { enable: flags.enable };
|
|
702
|
+
const args = { enable: flags.enable, disable: flags.disable };
|
|
702
703
|
await (0, windowsAudit_1.windowsAuditCommand)(args);
|
|
703
704
|
break;
|
|
704
705
|
}
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.5",
|
|
4
4
|
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"test:browser-credentials-rule": "npm run build && node scripts/test-browser-credentials-rule.js",
|
|
25
25
|
"test:taint-ledger": "npm run build && node scripts/test-taint-ledger.js",
|
|
26
26
|
"test:shell-audit": "npm run build && node scripts/test-shell-audit.js",
|
|
27
|
+
"test:audit-restore": "node scripts/test-audit-restore.js",
|
|
27
28
|
"test:shell-guard": "npm run build && node scripts/test-shell-guard.js",
|
|
28
29
|
"test:cmd-guard": "npm run build && node scripts/test-cmd-guard.js",
|
|
29
30
|
"test:posix-guard": "npm run build && node scripts/test-posix-guard.js",
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
"test:realworld-posture": "npm run build && node scripts/test-realworld-posture-fixtures.js",
|
|
33
34
|
"test:discover-stdio-mcp": "npm run build && node scripts/test-discover-stdio-mcp-tools.js",
|
|
34
35
|
"test:per-server-agent-name": "npm run build && node scripts/test-per-server-agent-name.js",
|
|
36
|
+
"test:user-objective-labels": "npm run build && node scripts/test-user-objective-labels.js",
|
|
35
37
|
"test:remote-mcp-gateway": "npm run build && node scripts/test-remote-mcp-gateway.js",
|
|
36
38
|
"test:e2e-onboarding-personas": "npm run build && node scripts/test-e2e-onboarding-personas.js",
|
|
37
39
|
"test:onboarding-transaction": "npm run build && node scripts/test-onboarding-transaction.js",
|