fullcourtdefense-cli 1.18.0 → 1.18.2
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 +50 -2
- package/dist/discoveryMarker.d.ts +7 -0
- package/dist/discoveryMarker.js +17 -0
- package/dist/selfUpdate.d.ts +3 -0
- package/dist/selfUpdate.js +34 -3
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -76,6 +76,10 @@ const HEARTBEAT_INTERVAL_MS = envMs('FCD_DAEMON_HEARTBEAT_MS', 5 * 60_000);
|
|
|
76
76
|
const BUNDLE_POLL_MS = envMs('FCD_DAEMON_BUNDLE_POLL_MS', 60_000);
|
|
77
77
|
/** Delay before the one-time initial discovery sweep on a fresh machine. */
|
|
78
78
|
const INITIAL_DISCOVER_DELAY_MS = envMs('FCD_DAEMON_INITIAL_DISCOVER_MS', 2 * 60_000);
|
|
79
|
+
/** A discovery upload older than this is stale — the daemon catches up itself. */
|
|
80
|
+
const DISCOVER_STALE_MS = envMs('FCD_DAEMON_DISCOVER_STALE_MS', 20 * 60 * 60_000);
|
|
81
|
+
/** How often the daemon re-checks discovery freshness. */
|
|
82
|
+
const DISCOVER_CHECK_INTERVAL_MS = envMs('FCD_DAEMON_DISCOVER_CHECK_MS', 60 * 60_000);
|
|
79
83
|
/** Rotate the daemon log when it grows past this size. */
|
|
80
84
|
const LOG_MAX_BYTES = 1_000_000;
|
|
81
85
|
function daemonDir() {
|
|
@@ -535,9 +539,12 @@ async function runDaemon(args, config) {
|
|
|
535
539
|
if (!target)
|
|
536
540
|
throw new Error('No target version available — the control plane could not resolve the latest release.');
|
|
537
541
|
log(`Upgrade CLI: admin requested an upgrade to ${target}.`);
|
|
538
|
-
|
|
542
|
+
// force: bypass the auto-update retry cooldown — an explicit admin
|
|
543
|
+
// action must actually attempt and report the real outcome, not
|
|
544
|
+
// "in progress" while a broken path silently retries hourly.
|
|
545
|
+
const outcome = (0, selfUpdate_1.maybeSelfUpdate)({ currentVersion: cliVersion(), targetVersion: target, enabled: true, force: true, log });
|
|
539
546
|
if (!outcome) {
|
|
540
|
-
resultSummary = `CLI ${cliVersion() || 'unknown'} is already at ${target}
|
|
547
|
+
resultSummary = `CLI ${cliVersion() || 'unknown'} is already at ${target}.`;
|
|
541
548
|
}
|
|
542
549
|
else if (outcome.started) {
|
|
543
550
|
resultSummary = outcome.detail;
|
|
@@ -693,6 +700,45 @@ async function runDaemon(args, config) {
|
|
|
693
700
|
await uploadLogTail();
|
|
694
701
|
}, INITIAL_DISCOVER_DELAY_MS);
|
|
695
702
|
}
|
|
703
|
+
// Staleness catch-up: the daily scheduled job fires at a FIXED hour, so a
|
|
704
|
+
// laptop that was asleep/powered off at that hour misses the whole day —
|
|
705
|
+
// admins then see day-old discovery/posture data even though the machine is
|
|
706
|
+
// online. The daemon closes that gap: whenever the last successful upload is
|
|
707
|
+
// older than ~20h, run a full sweep now. Checked shortly after boot (wake-up
|
|
708
|
+
// catch-up) and hourly thereafter; the marker written by `discover` keeps
|
|
709
|
+
// this idempotent alongside the scheduled task.
|
|
710
|
+
let discoverCatchUpRunning = false;
|
|
711
|
+
const maybeCatchUpDiscovery = async (reason) => {
|
|
712
|
+
if (stopped || discoverCatchUpRunning || !creds.shieldId)
|
|
713
|
+
return;
|
|
714
|
+
if (!(0, discoveryMarker_1.hasDiscoveryUploadMarker)())
|
|
715
|
+
return; // fresh machine — initial sweep owns it
|
|
716
|
+
const last = (0, discoveryMarker_1.lastDiscoveryUploadAt)();
|
|
717
|
+
if (last && Date.now() - last.getTime() < DISCOVER_STALE_MS)
|
|
718
|
+
return;
|
|
719
|
+
discoverCatchUpRunning = true;
|
|
720
|
+
log(`Discovery catch-up (${reason}): last upload ${last ? Math.round((Date.now() - last.getTime()) / 3_600_000) + 'h ago' : 'unknown'} — starting full sweep (MCP + secrets + agent files + posture)…`);
|
|
721
|
+
await uploadLogTail();
|
|
722
|
+
const logPump = setInterval(() => { void uploadLogTail(); }, 15_000);
|
|
723
|
+
try {
|
|
724
|
+
const exitCode = await runDiscoverSweep();
|
|
725
|
+
if (exitCode !== 0)
|
|
726
|
+
throw new Error(`discover exited with code ${exitCode}`);
|
|
727
|
+
log('Discovery catch-up: upload complete — dashboard discovery + posture timestamps are now fresh.');
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
log(`Discovery catch-up failed (retried hourly): ${error.message}`);
|
|
731
|
+
}
|
|
732
|
+
finally {
|
|
733
|
+
clearInterval(logPump);
|
|
734
|
+
discoverCatchUpRunning = false;
|
|
735
|
+
await uploadLogTail();
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
// Give the machine a couple of minutes to settle after boot/wake before the
|
|
739
|
+
// first check — same grace as the initial sweep.
|
|
740
|
+
const discoverCatchUpBootTimer = setTimeout(() => { void maybeCatchUpDiscovery('after start'); }, INITIAL_DISCOVER_DELAY_MS);
|
|
741
|
+
const discoverCatchUpTimer = setInterval(() => { void maybeCatchUpDiscovery('hourly check'); }, DISCOVER_CHECK_INTERVAL_MS);
|
|
696
742
|
const rescanTimer = setInterval(() => {
|
|
697
743
|
const count = refreshWatchTargets();
|
|
698
744
|
log(`Rescan: watching ${count} config file(s).`);
|
|
@@ -707,6 +753,8 @@ async function runDaemon(args, config) {
|
|
|
707
753
|
clearInterval(rescanTimer);
|
|
708
754
|
clearInterval(bundleTimer);
|
|
709
755
|
clearInterval(heartbeatTimer);
|
|
756
|
+
clearTimeout(discoverCatchUpBootTimer);
|
|
757
|
+
clearInterval(discoverCatchUpTimer);
|
|
710
758
|
if (initialDiscoverTimer)
|
|
711
759
|
clearTimeout(initialDiscoverTimer);
|
|
712
760
|
if (debounceTimer)
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export declare function hasDiscoveryUploadMarker(): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* When the last successful full discovery upload completed, or undefined if
|
|
4
|
+
* this machine never uploaded one. Drives the daemon's staleness catch-up:
|
|
5
|
+
* laptops that were powered off at the daily task's fixed hour still get a
|
|
6
|
+
* fresh scan shortly after they wake up.
|
|
7
|
+
*/
|
|
8
|
+
export declare function lastDiscoveryUploadAt(): Date | undefined;
|
|
2
9
|
export declare function writeDiscoveryUploadMarker(trigger: string): void;
|
package/dist/discoveryMarker.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.hasDiscoveryUploadMarker = hasDiscoveryUploadMarker;
|
|
37
|
+
exports.lastDiscoveryUploadAt = lastDiscoveryUploadAt;
|
|
37
38
|
exports.writeDiscoveryUploadMarker = writeDiscoveryUploadMarker;
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const os = __importStar(require("os"));
|
|
@@ -55,6 +56,22 @@ function hasDiscoveryUploadMarker() {
|
|
|
55
56
|
return false;
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* When the last successful full discovery upload completed, or undefined if
|
|
61
|
+
* this machine never uploaded one. Drives the daemon's staleness catch-up:
|
|
62
|
+
* laptops that were powered off at the daily task's fixed hour still get a
|
|
63
|
+
* fresh scan shortly after they wake up.
|
|
64
|
+
*/
|
|
65
|
+
function lastDiscoveryUploadAt() {
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(fs.readFileSync(markerFile(), 'utf8'));
|
|
68
|
+
const date = raw.completedAt ? new Date(raw.completedAt) : undefined;
|
|
69
|
+
return date && !Number.isNaN(date.getTime()) ? date : undefined;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
58
75
|
function writeDiscoveryUploadMarker(trigger) {
|
|
59
76
|
try {
|
|
60
77
|
fs.mkdirSync(path.dirname(markerFile()), { recursive: true });
|
package/dist/selfUpdate.d.ts
CHANGED
|
@@ -36,6 +36,9 @@ export declare function maybeSelfUpdate(input: {
|
|
|
36
36
|
log?: (message: string) => void;
|
|
37
37
|
/** Skip while a remote machine action is executing (never upgrade mid-action). */
|
|
38
38
|
busy?: boolean;
|
|
39
|
+
/** Bypass the retry cooldown — used by explicit admin upgrade_cli actions so
|
|
40
|
+
* they always attempt and report the REAL outcome instead of "in progress". */
|
|
41
|
+
force?: boolean;
|
|
39
42
|
}): SelfUpdateResult | undefined;
|
|
40
43
|
/** Version the MSI updater script reads from the installed package.json. */
|
|
41
44
|
export declare function installedMsiVersion(installFolder: string): string | undefined;
|
package/dist/selfUpdate.js
CHANGED
|
@@ -116,14 +116,45 @@ function startNpmSelfUpdate(targetVersion, log) {
|
|
|
116
116
|
* but group policy can deny it — in that case the daily trigger remains the
|
|
117
117
|
* backstop and we report that honestly.
|
|
118
118
|
*/
|
|
119
|
+
/** Install folder of an MSI deployment (…\FullCourtDefense), from the running entry. */
|
|
120
|
+
function msiInstallRoot() {
|
|
121
|
+
const entry = path.resolve(process.argv[1] || '');
|
|
122
|
+
// dist\index.js → install root is the parent of dist.
|
|
123
|
+
const dist = path.dirname(entry);
|
|
124
|
+
const root = path.dirname(dist);
|
|
125
|
+
return fs.existsSync(path.join(root, 'Update-FullCourtDefense.ps1')) ? root : undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Self-heal a missing updater task. Machines installed from an older MSI (or
|
|
129
|
+
* where task creation was denied at install time) ship the updater SCRIPT but
|
|
130
|
+
* not the scheduled task — without this, every self-update tick dead-ends with
|
|
131
|
+
* "reinstall the MSI". Registration needs an elevated token, so this succeeds
|
|
132
|
+
* on elevated daemons and stays a silent no-op otherwise.
|
|
133
|
+
*/
|
|
134
|
+
function tryRegisterMsiUpdaterTask(log) {
|
|
135
|
+
const root = msiInstallRoot();
|
|
136
|
+
if (!root)
|
|
137
|
+
return false;
|
|
138
|
+
const script = path.join(root, 'Update-FullCourtDefense.ps1');
|
|
139
|
+
const taskCommand = `powershell.exe -NoProfile -ExecutionPolicy Bypass -File \\"${script}\\"`;
|
|
140
|
+
const create = (0, child_process_1.spawnSync)('schtasks', [
|
|
141
|
+
'/Create', '/TN', exports.MSI_UPDATER_TASK_NAME, '/TR', taskCommand,
|
|
142
|
+
'/SC', 'DAILY', '/ST', '03:07', '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F',
|
|
143
|
+
], { stdio: 'ignore', windowsHide: true, timeout: 20_000 });
|
|
144
|
+
if (create.status === 0) {
|
|
145
|
+
log('Self-update: registered the missing MSI updater task (self-heal).');
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
119
150
|
function startMsiSelfUpdate(targetVersion, log) {
|
|
120
151
|
const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', exports.MSI_UPDATER_TASK_NAME], { stdio: 'ignore', windowsHide: true, timeout: 10_000 });
|
|
121
|
-
if (query.status !== 0) {
|
|
152
|
+
if (query.status !== 0 && !tryRegisterMsiUpdaterTask(log)) {
|
|
122
153
|
log('Self-update: MSI updater task is not registered on this machine — reinstall the MSI to enable silent updates.');
|
|
123
154
|
return {
|
|
124
155
|
started: false,
|
|
125
156
|
kind: 'msi',
|
|
126
|
-
detail: `The "${exports.MSI_UPDATER_TASK_NAME}" scheduled task is missing. Reinstall the latest MSI (or redeploy via MDM) to restore silent updates.`,
|
|
157
|
+
detail: `The "${exports.MSI_UPDATER_TASK_NAME}" scheduled task is missing and could not be self-registered (needs an elevated daemon). Reinstall the latest MSI once (or redeploy via MDM) to restore silent updates.`,
|
|
127
158
|
};
|
|
128
159
|
}
|
|
129
160
|
const run = (0, child_process_1.spawnSync)('schtasks', ['/Run', '/TN', exports.MSI_UPDATER_TASK_NAME], { stdio: 'ignore', windowsHide: true, timeout: 20_000 });
|
|
@@ -153,7 +184,7 @@ function maybeSelfUpdate(input) {
|
|
|
153
184
|
return undefined;
|
|
154
185
|
if (compareCliVersions(input.targetVersion, input.currentVersion) <= 0)
|
|
155
186
|
return undefined;
|
|
156
|
-
if (Date.now() - updateInFlightSince < UPDATE_RETRY_COOLDOWN_MS)
|
|
187
|
+
if (!input.force && Date.now() - updateInFlightSince < UPDATE_RETRY_COOLDOWN_MS)
|
|
157
188
|
return undefined;
|
|
158
189
|
updateInFlightSince = Date.now();
|
|
159
190
|
log(`Self-update: CLI ${input.currentVersion || 'unknown'} -> ${input.targetVersion} (org auto-update).`);
|
package/dist/version.json
CHANGED