fullcourtdefense-cli 1.26.0 → 1.26.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.
@@ -1,5 +1,6 @@
1
1
  import { BotGuardConfig } from '../config';
2
2
  import { ProtectAllArgs } from './mcpGateway';
3
+ import { windowsTaskRunsInteractive, windowsTaskRunsVisibly } from '../windowsTaskState';
3
4
  export interface DaemonArgs extends ProtectAllArgs {
4
5
  install?: string;
5
6
  uninstall?: string;
@@ -83,15 +84,7 @@ export declare function registerHiddenTask(taskName: string, xml: string | strin
83
84
  /** True when an existing scheduled task's action still runs wscript/cscript
84
85
  * (the pre-1.22.1 VBS launcher) — the migration trigger. */
85
86
  export declare function windowsTaskReferencesScriptHost(taskName: string): boolean;
86
- /** True when an existing task still runs with an InteractiveToken principal —
87
- * the 1.22.1 window-flash bug (console window on every trigger). Migration
88
- * trigger for the windowless S4U principal. */
89
- export declare function windowsTaskRunsInteractive(taskName: string): boolean;
90
- /** True when triggering the task would open a VISIBLE console window:
91
- * InteractiveToken principal AND a bare console action (not wrapped in
92
- * `conhost --headless`). This — not the principal alone — is the flash
93
- * condition; a conhost-wrapped InteractiveToken task is fully windowless. */
94
- export declare function windowsTaskRunsVisibly(taskName: string): boolean;
87
+ export { windowsTaskRunsInteractive, windowsTaskRunsVisibly };
95
88
  /** Snapshot of the resident daemon for out-of-process callers (watchdog/status). */
96
89
  export interface DaemonRuntimeState {
97
90
  alive: boolean;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.windowsTaskRunsVisibly = exports.windowsTaskRunsInteractive = void 0;
36
37
  exports.daemonDiscoverSweepArgs = daemonDiscoverSweepArgs;
37
38
  exports.summarizeDiscoverStderr = summarizeDiscoverStderr;
38
39
  exports.discoverSweepCredentialEnv = discoverSweepCredentialEnv;
@@ -44,8 +45,6 @@ exports.buildTaskXml = buildTaskXml;
44
45
  exports.buildTaskXmlVariants = buildTaskXmlVariants;
45
46
  exports.registerHiddenTask = registerHiddenTask;
46
47
  exports.windowsTaskReferencesScriptHost = windowsTaskReferencesScriptHost;
47
- exports.windowsTaskRunsInteractive = windowsTaskRunsInteractive;
48
- exports.windowsTaskRunsVisibly = windowsTaskRunsVisibly;
49
48
  exports.daemonRuntimeState = daemonRuntimeState;
50
49
  exports.spawnDetachedDaemon = spawnDetachedDaemon;
51
50
  exports.pidIsAlive = pidIsAlive;
@@ -82,6 +81,9 @@ const honeypot_1 = require("../honeypot");
82
81
  const windowsAudit_1 = require("./windowsAudit");
83
82
  const hook_1 = require("./hook");
84
83
  const verdictIpc_1 = require("../verdictIpc");
84
+ const windowsTaskState_1 = require("../windowsTaskState");
85
+ Object.defineProperty(exports, "windowsTaskRunsInteractive", { enumerable: true, get: function () { return windowsTaskState_1.windowsTaskRunsInteractive; } });
86
+ Object.defineProperty(exports, "windowsTaskRunsVisibly", { enumerable: true, get: function () { return windowsTaskState_1.windowsTaskRunsVisibly; } });
85
87
  const COLOR = {
86
88
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
87
89
  red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
@@ -632,10 +634,18 @@ async function runDaemon(args, config) {
632
634
  body: JSON.stringify({ shieldId: creds.shieldId, machineId: identity.machineId, ...payload }),
633
635
  signal: AbortSignal.timeout(15_000),
634
636
  });
637
+ if (!resp.ok) {
638
+ // Log the STATUS and the server's own words. Swallowing them turned a
639
+ // precise backend rejection into "network or backend rejection" in the
640
+ // console, which cost a Cloud Run log dig to diagnose — on the very
641
+ // feature whose job is to explain a machine remotely.
642
+ const body = await resp.text().catch(() => '');
643
+ log(`Diagnostics upload rejected: HTTP ${resp.status}${body ? ` — ${body.slice(0, 300).replace(/\s+/g, ' ')}` : ''}`);
644
+ }
635
645
  return resp.ok;
636
646
  }
637
- catch {
638
- log('Diagnostics upload failed (network) — the bundle stays available locally.');
647
+ catch (error) {
648
+ log(`Diagnostics upload failed (network: ${error.message}) — the bundle stays available locally.`);
639
649
  return false;
640
650
  }
641
651
  };
@@ -1553,36 +1563,6 @@ function windowsTaskReferencesScriptHost(taskName) {
1553
1563
  return false;
1554
1564
  }
1555
1565
  }
1556
- /** True when an existing task still runs with an InteractiveToken principal —
1557
- * the 1.22.1 window-flash bug (console window on every trigger). Migration
1558
- * trigger for the windowless S4U principal. */
1559
- function windowsTaskRunsInteractive(taskName) {
1560
- return windowsTaskXmlState(taskName).interactive;
1561
- }
1562
- /** True when triggering the task would open a VISIBLE console window:
1563
- * InteractiveToken principal AND a bare console action (not wrapped in
1564
- * `conhost --headless`). This — not the principal alone — is the flash
1565
- * condition; a conhost-wrapped InteractiveToken task is fully windowless. */
1566
- function windowsTaskRunsVisibly(taskName) {
1567
- const state = windowsTaskXmlState(taskName);
1568
- return state.interactive && !state.headless;
1569
- }
1570
- function windowsTaskXmlState(taskName) {
1571
- try {
1572
- const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName, '/XML'], {
1573
- encoding: 'utf8', windowsHide: true, timeout: 10_000,
1574
- });
1575
- if (query.status !== 0 || typeof query.stdout !== 'string')
1576
- return { interactive: false, headless: false };
1577
- return {
1578
- interactive: /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(query.stdout),
1579
- headless: /conhost(\.exe)?<\/Command>[\s\S]*?--headless/i.test(query.stdout),
1580
- };
1581
- }
1582
- catch {
1583
- return { interactive: false, headless: false };
1584
- }
1585
- }
1586
1566
  /**
1587
1567
  * Upgrade an EXISTING (working) task to the windowless S4U principal. Tries
1588
1568
  * ONLY the S4U XML — on failure (unelevated daemon) the current task is left
@@ -1715,7 +1695,7 @@ function startDaemonNowWindows(viaTask = false) {
1715
1695
  }
1716
1696
  }
1717
1697
  catch { /* not running */ }
1718
- if (viaTask && !windowsTaskRunsVisibly(TASK_NAME)) {
1698
+ if (viaTask && !(0, windowsTaskState_1.windowsTaskRunsVisibly)(TASK_NAME)) {
1719
1699
  // Start through the scheduled task so the daemon runs with the task's
1720
1700
  // LIMITED (non-elevated) token. Launching directly from an elevated MSI
1721
1701
  // custom action would leave an elevated daemon that a normal-user CLI can
@@ -1825,12 +1805,12 @@ function ensureWindowsAutostartHealthy(logFn) {
1825
1805
  // registration always works and is just as windowless. The updater kick
1826
1806
  // stays as the final backstop for machines where even that failed.
1827
1807
  let migrationDenied = false;
1828
- if (daemonTask.status === 0 && !legacyDaemon && windowsTaskRunsVisibly(TASK_NAME)) {
1808
+ if (daemonTask.status === 0 && !legacyDaemon && (0, windowsTaskState_1.windowsTaskRunsVisibly)(TASK_NAME)) {
1829
1809
  const daemonXml = buildDaemonTaskXml();
1830
1810
  migrationDenied = !(upgradeTaskWindowless(TASK_NAME, daemonXml[0], logFn)
1831
1811
  || rewrapTaskHeadless(TASK_NAME, daemonXml[1], logFn)) || migrationDenied;
1832
1812
  }
1833
- if (watchdogQuery.status === 0 && !legacyWatchdog && windowsTaskRunsVisibly(WATCHDOG_TASK_NAME)) {
1813
+ if (watchdogQuery.status === 0 && !legacyWatchdog && (0, windowsTaskState_1.windowsTaskRunsVisibly)(WATCHDOG_TASK_NAME)) {
1834
1814
  const watchdogXml = buildWatchdogTaskXml();
1835
1815
  migrationDenied = !(upgradeTaskWindowless(WATCHDOG_TASK_NAME, watchdogXml[0], logFn)
1836
1816
  || rewrapTaskHeadless(WATCHDOG_TASK_NAME, watchdogXml[1], logFn)) || migrationDenied;
package/dist/selfTest.js CHANGED
@@ -9,6 +9,7 @@ const machineKeyFile_1 = require("./machineKeyFile");
9
9
  const runtimeConfig_1 = require("./runtimeConfig");
10
10
  const integrity_1 = require("./integrity");
11
11
  const selfUpdate_1 = require("./selfUpdate");
12
+ const windowsTaskState_1 = require("./windowsTaskState");
12
13
  /** Windows scheduled-task probe: exists + (when readable) its last result code. */
13
14
  function queryScheduledTask(taskName) {
14
15
  const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName, '/V', '/FO', 'LIST'], {
@@ -145,38 +146,49 @@ async function runDeepSelfTest(input) {
145
146
  add({ id: 'watchdog_task', label: 'Watchdog task registered', ok: false, detail: error instanceof Error ? error.message : String(error) });
146
147
  }
147
148
  }
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.
149
+ // 9. Windowless tasks. What users complain about is a console window flashing
150
+ // on every trigger (the "5-minute flicker" class); <Hidden>true</Hidden> does
151
+ // NOT suppress it. Two independent defences prevent it: the S4U logon type,
152
+ // and wrapping the action in `conhost --headless`. Either one is sufficient,
153
+ // so this check reports the OBSERVABLE symptom never the principal alone,
154
+ // which is red on every healthy MSI install (Windows denies S4U registration
155
+ // even to SYSTEM, which is exactly why the conhost wrapper exists).
153
156
  if (process.platform === 'win32') {
154
- const interactive = listInteractiveFcdTasks();
157
+ const flashing = listVisiblyFlashingFcdTasks();
158
+ const conhostShielded = listConhostShieldedFcdTasks();
155
159
  add({
156
160
  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`,
161
+ label: 'Scheduled tasks are windowless',
162
+ ok: flashing.length === 0,
163
+ detail: flashing.length === 0
164
+ ? conhostShielded.length === 0
165
+ ? 'no FCD task can open a console window (S4U principals)'
166
+ : `no FCD task can open a console window (${conhostShielded.length} still on an InteractiveToken principal, made windowless by conhost --headless)`
167
+ : `visible console window on every trigger of: ${flashing.join(', ')} — bare console action on an InteractiveToken principal (neither S4U nor conhost --headless)`,
162
168
  });
163
169
  }
164
170
  return { ok: checks.every(check => check.ok), checks, ranAt: new Date().toISOString() };
165
171
  }
166
- /** FCD per-user tasks whose XML still declares an InteractiveToken principal. */
172
+ /** FCD per-user tasks that could flash a console window on every trigger. */
167
173
  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;
174
+ /**
175
+ * Tasks that would actually open a VISIBLE window. Delegates to the shared
176
+ * `windowsTaskRunsVisibly` so this check can never drift from the daemon's own
177
+ * definition of the flash condition again: an InteractiveToken principal is
178
+ * harmless once the action runs through `conhost --headless`, and that is the
179
+ * normal state of an MSI install, because current Windows builds refuse S4U
180
+ * registration even to SYSTEM. Reporting on the principal alone failed every
181
+ * healthy machine and trained operators to ignore the whole report.
182
+ */
183
+ function listVisiblyFlashingFcdTasks() {
184
+ return WINDOWED_TASK_CANDIDATES.filter(taskName => (0, windowsTaskState_1.windowsTaskRunsVisibly)(taskName));
185
+ }
186
+ /** Registered tasks still on InteractiveToken but made windowless by conhost. */
187
+ function listConhostShieldedFcdTasks() {
188
+ return WINDOWED_TASK_CANDIDATES.filter(taskName => {
189
+ const state = (0, windowsTaskState_1.windowsTaskXmlState)(taskName);
190
+ return state.exists && state.interactive && state.headless;
191
+ });
180
192
  }
181
193
  /** Compact one-line summary for the machine-action result card. */
182
194
  function summarizeSelfTest(report) {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.26.0"
2
+ "version": "1.26.1"
3
3
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Windows scheduled-task principal/action state — the single source of truth
3
+ * for "would this task flash a console window?".
4
+ *
5
+ * Why this is its own module: the daemon (which registers and self-heals the
6
+ * tasks) and the deep self-test (which reports on them) both need the answer,
7
+ * and `daemon.ts` already imports `selfTest.ts` for the health_check action, so
8
+ * a direct import back would be a cycle. Both now depend on this leaf instead.
9
+ *
10
+ * It also settles a correctness split. `selfTest.ts` used to carry its own copy
11
+ * that checked the PRINCIPAL alone, so it reported a failure on every machine
12
+ * whose tasks are conhost-wrapped — which is all MSI installs, because current
13
+ * Windows builds deny S4U registration even to SYSTEM. The flash condition is
14
+ * the pair of facts, never the principal by itself.
15
+ *
16
+ * Pure apart from the `schtasks /Query` read; never throws.
17
+ */
18
+ export interface WindowsTaskState {
19
+ /** Task exists and declares an InteractiveToken principal. */
20
+ interactive: boolean;
21
+ /** Task action runs through `conhost.exe --headless` (windowless regardless of principal). */
22
+ headless: boolean;
23
+ /** Task is registered at all — absent tasks can't flash. */
24
+ exists: boolean;
25
+ }
26
+ export declare function windowsTaskXmlState(taskName: string): WindowsTaskState;
27
+ /**
28
+ * True when an existing task still runs with an InteractiveToken principal —
29
+ * the 1.22.1 window-flash bug's principal half. Migration trigger for the
30
+ * windowless S4U principal. NOT the flash condition on its own: see
31
+ * `windowsTaskRunsVisibly`.
32
+ */
33
+ export declare function windowsTaskRunsInteractive(taskName: string): boolean;
34
+ /**
35
+ * True when triggering the task would open a VISIBLE console window:
36
+ * InteractiveToken principal AND a bare console action (not wrapped in
37
+ * `conhost --headless`). This — not the principal alone — is the flash
38
+ * condition; a conhost-wrapped InteractiveToken task is fully windowless.
39
+ */
40
+ export declare function windowsTaskRunsVisibly(taskName: string): boolean;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.windowsTaskXmlState = windowsTaskXmlState;
4
+ exports.windowsTaskRunsInteractive = windowsTaskRunsInteractive;
5
+ exports.windowsTaskRunsVisibly = windowsTaskRunsVisibly;
6
+ const child_process_1 = require("child_process");
7
+ function windowsTaskXmlState(taskName) {
8
+ try {
9
+ const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName, '/XML'], {
10
+ encoding: 'utf8', windowsHide: true, timeout: 10_000,
11
+ });
12
+ if (query.status !== 0 || typeof query.stdout !== 'string') {
13
+ return { interactive: false, headless: false, exists: false };
14
+ }
15
+ return {
16
+ exists: true,
17
+ interactive: /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(query.stdout),
18
+ headless: /conhost(\.exe)?<\/Command>[\s\S]*?--headless/i.test(query.stdout),
19
+ };
20
+ }
21
+ catch {
22
+ return { interactive: false, headless: false, exists: false };
23
+ }
24
+ }
25
+ /**
26
+ * True when an existing task still runs with an InteractiveToken principal —
27
+ * the 1.22.1 window-flash bug's principal half. Migration trigger for the
28
+ * windowless S4U principal. NOT the flash condition on its own: see
29
+ * `windowsTaskRunsVisibly`.
30
+ */
31
+ function windowsTaskRunsInteractive(taskName) {
32
+ return windowsTaskXmlState(taskName).interactive;
33
+ }
34
+ /**
35
+ * True when triggering the task would open a VISIBLE console window:
36
+ * InteractiveToken principal AND a bare console action (not wrapped in
37
+ * `conhost --headless`). This — not the principal alone — is the flash
38
+ * condition; a conhost-wrapped InteractiveToken task is fully windowless.
39
+ */
40
+ function windowsTaskRunsVisibly(taskName) {
41
+ const state = windowsTaskXmlState(taskName);
42
+ return state.interactive && !state.headless;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.26.0",
3
+ "version": "1.26.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": {