fullcourtdefense-cli 1.21.25 → 1.21.26

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.
@@ -18,3 +18,12 @@ export declare function installCmdGuardCommand(_args?: InstallCmdGuardArgs): Pro
18
18
  export declare function uninstallCmdGuardCommand(): Promise<void>;
19
19
  /** Refresh shared rules JSON when cmd guard is installed. */
20
20
  export declare function refreshCmdGuardRules(): void;
21
+ /**
22
+ * Self-heal a stale cmd-guard AutoRun: if the registry still references our
23
+ * autorun script but the file was deleted (manual cleanup, interrupted
24
+ * uninstall), EVERY cmd.exe on the machine errors on startup and — worse —
25
+ * poisons `cmd /c` exit codes to 1, making healthy installs/commands look
26
+ * failed. Strips only OUR fragment; a customer's own AutoRun entries survive.
27
+ * Returns true when a repair was applied.
28
+ */
29
+ export declare function repairStaleCmdAutorun(): boolean;
@@ -38,6 +38,7 @@ exports.getCmdGuardStatus = getCmdGuardStatus;
38
38
  exports.installCmdGuardCommand = installCmdGuardCommand;
39
39
  exports.uninstallCmdGuardCommand = uninstallCmdGuardCommand;
40
40
  exports.refreshCmdGuardRules = refreshCmdGuardRules;
41
+ exports.repairStaleCmdAutorun = repairStaleCmdAutorun;
41
42
  const child_process_1 = require("child_process");
42
43
  const fs = __importStar(require("fs"));
43
44
  const os = __importStar(require("os"));
@@ -264,3 +265,35 @@ function refreshCmdGuardRules() {
264
265
  }
265
266
  catch { /* best-effort */ }
266
267
  }
268
+ /**
269
+ * Self-heal a stale cmd-guard AutoRun: if the registry still references our
270
+ * autorun script but the file was deleted (manual cleanup, interrupted
271
+ * uninstall), EVERY cmd.exe on the machine errors on startup and — worse —
272
+ * poisons `cmd /c` exit codes to 1, making healthy installs/commands look
273
+ * failed. Strips only OUR fragment; a customer's own AutoRun entries survive.
274
+ * Returns true when a repair was applied.
275
+ */
276
+ function repairStaleCmdAutorun() {
277
+ try {
278
+ if (process.platform !== 'win32')
279
+ return false;
280
+ const current = readCurrentAutorun();
281
+ if (!current.includes(AUTORUN_MARKER))
282
+ return false;
283
+ if (fs.existsSync(AUTORUN_BAT_PATH))
284
+ return false;
285
+ const next = stripAutorunValue(current);
286
+ if (next) {
287
+ setRegString(CMD_AUTORUN_KEY, AUTORUN_VALUE, next);
288
+ }
289
+ else {
290
+ (0, child_process_1.execFileSync)('reg', ['delete', CMD_AUTORUN_KEY, '/v', AUTORUN_VALUE, '/f'], {
291
+ windowsHide: true, timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'],
292
+ });
293
+ }
294
+ return true;
295
+ }
296
+ catch {
297
+ return false;
298
+ }
299
+ }
@@ -55,6 +55,7 @@ const discoveryMarker_1 = require("../discoveryMarker");
55
55
  const selfUpdate_1 = require("../selfUpdate");
56
56
  const machineActionVerify_1 = require("../machineActionVerify");
57
57
  const desktopChatGuard_1 = require("./desktopChatGuard");
58
+ const honeypot_1 = require("../honeypot");
58
59
  const COLOR = {
59
60
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
60
61
  red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
@@ -732,6 +733,30 @@ async function runDaemon(args, config) {
732
733
  log,
733
734
  });
734
735
  }
736
+ // Honeypot decoys: org-controlled via fleet settings. Planting is
737
+ // idempotent (a decoy deleted by an attacker is re-planted on the next
738
+ // poll); disabling removes only files we created.
739
+ if (bundle.honeypot?.enabled) {
740
+ try {
741
+ const result = (0, honeypot_1.plantHoneypots)();
742
+ if (result.planted.length > 0) {
743
+ log(`Honeypot: planted ${result.planted.length} decoy credential file(s).`);
744
+ }
745
+ }
746
+ catch (error) {
747
+ log(`Honeypot: planting failed: ${error.message}`);
748
+ }
749
+ }
750
+ else if (bundle.honeypot && !bundle.honeypot.enabled && (0, honeypot_1.getHoneypotPaths)().length > 0) {
751
+ try {
752
+ const removed = (0, honeypot_1.removeHoneypots)();
753
+ if (removed.length > 0)
754
+ log(`Honeypot: removed ${removed.length} decoy file(s) (disabled by org policy).`);
755
+ }
756
+ catch (error) {
757
+ log(`Honeypot: removal failed: ${error.message}`);
758
+ }
759
+ }
735
760
  }
736
761
  catch { /* offline — cached stance applies */ }
737
762
  };
@@ -1,5 +1,5 @@
1
1
  export type DeterministicDirection = 'request' | 'response';
2
- export type DeterministicCategory = 'sensitive_file' | 'metadata_ssrf' | 'destructive_command' | 'destructive_sql' | 'infra_destroy' | 'reverse_shell' | 'secret_exfiltration';
2
+ export type DeterministicCategory = 'sensitive_file' | 'metadata_ssrf' | 'destructive_command' | 'destructive_sql' | 'infra_destroy' | 'reverse_shell' | 'secret_exfiltration' | 'honeypot';
3
3
  export interface DeterministicFinding {
4
4
  blocked: true;
5
5
  ruleId: string;
@@ -24,6 +24,8 @@ export interface LocalSafetyScanOptions {
24
24
  policyHash?: string;
25
25
  cwd?: string;
26
26
  inspectScripts?: boolean;
27
+ /** Absolute paths of planted honeypot decoy files — ANY reference blocks. */
28
+ honeypotPaths?: string[];
27
29
  }
28
30
  export declare function scanDeterministicToolCall(toolName: string, toolArgs: Record<string, unknown>, options?: LocalSafetyScanOptions): DeterministicFinding | undefined;
29
31
  export declare function scanDeterministicTextResponse(text: string, options?: LocalSafetyScanOptions): DeterministicFinding | undefined;
@@ -37,6 +37,7 @@ exports.scanDeterministicToolCall = scanDeterministicToolCall;
37
37
  exports.scanDeterministicTextResponse = scanDeterministicTextResponse;
38
38
  exports.scanDeterministicPrompt = scanDeterministicPrompt;
39
39
  const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
40
41
  const path = __importStar(require("path"));
41
42
  const SECRET_PATTERNS = [
42
43
  { itemId: 'private_key_material', label: 'private key material', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i },
@@ -262,6 +263,43 @@ function builtIn(itemId, categoryId, category, ruleId, reason, findingEvidence,
262
263
  policyHash: options?.policyHash,
263
264
  };
264
265
  }
266
+ /**
267
+ * Honeypot decoy touch — the highest-precision signal the guard has. The decoy
268
+ * paths are machine-local absolute paths planted by the daemon; matching is on
269
+ * the normalized absolute path or its home-anchored `~/relative` form, so a
270
+ * repo file that merely shares a basename (e.g. a project-local `.env.bak`)
271
+ * never matches. Fires regardless of tool context: no legitimate workflow
272
+ * references these files at all.
273
+ */
274
+ function honeypotTouch(value, options) {
275
+ const paths = options?.honeypotPaths;
276
+ if (!paths || paths.length === 0)
277
+ return undefined;
278
+ const text = normalizeForPath(value);
279
+ const home = normalizeForPath(os.homedir());
280
+ for (const decoyPath of paths) {
281
+ const decoy = normalizeForPath(decoyPath);
282
+ if (!decoy)
283
+ continue;
284
+ const candidates = [decoy];
285
+ if (home && decoy.startsWith(home + '/'))
286
+ candidates.push('~' + decoy.slice(home.length));
287
+ if (candidates.some(candidate => text.includes(candidate))) {
288
+ return {
289
+ blocked: true,
290
+ ruleId: 'local-honeypot',
291
+ category: 'honeypot',
292
+ categoryId: 'honeypot',
293
+ itemId: 'honeypot_decoy_access',
294
+ source: 'builtin',
295
+ reason: 'Blocked access to a decoy credential file (honeypot). No legitimate tool uses this file — this indicates credential-hunting behavior.',
296
+ evidence: evidence(value),
297
+ policyHash: options?.policyHash,
298
+ };
299
+ }
300
+ }
301
+ return undefined;
302
+ }
265
303
  function customBlock(value, options) {
266
304
  const lower = value.toLowerCase();
267
305
  for (const block of options?.customBlocks || []) {
@@ -441,6 +479,9 @@ function isSensitivePathContext(toolName, candidate) {
441
479
  return /(?:path|file|filename|dir|directory|target|source|src|dest|location|glob|pattern|uri|url)/i.test(lastKey);
442
480
  }
443
481
  function scanTextValue(toolName, value, options) {
482
+ const honeypot = honeypotTouch(value, options);
483
+ if (honeypot)
484
+ return honeypot;
444
485
  const custom = customBlock(value, options);
445
486
  if (custom)
446
487
  return custom;
@@ -505,7 +546,13 @@ function scanDeterministicToolCall(toolName, toolArgs, options) {
505
546
  continue;
506
547
  if (finding.category === 'sensitive_file' && !isSensitivePathContext(toolName, candidate))
507
548
  continue;
508
- if (isCommandContext(toolName, candidate) || finding.category === 'sensitive_file' || finding.category === 'metadata_ssrf')
549
+ // Honeypot decoy paths block in ANY context where the string can act —
550
+ // path access, command execution, or outbound egress (a decoy path inside
551
+ // a request body is exfiltration staging). Inert file-content mentions
552
+ // still pass, consistent with the other path rules.
553
+ if (finding.category === 'honeypot' && !(isActionableContext(toolName, candidate) || outbound))
554
+ continue;
555
+ if (isCommandContext(toolName, candidate) || finding.category === 'sensitive_file' || finding.category === 'metadata_ssrf' || finding.category === 'honeypot')
509
556
  return finding;
510
557
  }
511
558
  for (const candidate of candidates) {
@@ -248,6 +248,13 @@ async function onboardCommand(args, config) {
248
248
  // 1. Connectivity.
249
249
  console.log(`${BOLD}[1/6] Checking compatibility and connectivity…${RESET}`);
250
250
  mark('preflight', 'running');
251
+ // A stale cmd-guard AutoRun (registry points at a deleted autorun script)
252
+ // poisons every cmd.exe exit code on the machine — child spawns and the MSI's
253
+ // .cmd shim would report failure for successful work. Heal it before anything
254
+ // else runs.
255
+ if ((0, cmdGuard_1.repairStaleCmdAutorun)()) {
256
+ console.log(` ${DIM}Repaired a stale cmd.exe AutoRun entry left by an earlier uninstall.${RESET}`);
257
+ }
251
258
  const reachable = await checkBackendReachable(apiUrl);
252
259
  const compatible = checkCompatibility();
253
260
  printCheck(compatible);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Honeypot decoy credentials.
3
+ *
4
+ * When the org enables honeypots (fleet settings → delivered via the runtime
5
+ * bundle), the daemon plants a small set of decoy credential files in
6
+ * locations a credential-hunting agent would search. No legitimate tool ever
7
+ * reads these files, so ANY tool call touching one is a near-zero-false-
8
+ * positive compromise signal: the deterministic guard blocks it on-device and
9
+ * the resulting event (categoryId "honeypot") lets the backend auto-contain
10
+ * the machine.
11
+ *
12
+ * Safety rules:
13
+ * - A decoy is only created where NO file exists. If a real file is already
14
+ * at a decoy path we skip it and never track it — we must never overwrite
15
+ * or later delete a user's real file.
16
+ * - Only files recorded in the state file (i.e. files WE created) are ever
17
+ * removed when the feature is disabled.
18
+ */
19
+ export interface HoneypotDecoyState {
20
+ path: string;
21
+ itemId: string;
22
+ }
23
+ export interface HoneypotState {
24
+ version: 1;
25
+ enabled: boolean;
26
+ plantedAt: string;
27
+ decoys: HoneypotDecoyState[];
28
+ }
29
+ export interface PlantResult {
30
+ planted: string[];
31
+ existing: string[];
32
+ skipped: string[];
33
+ }
34
+ /**
35
+ * Plant (or re-plant) decoys. Idempotent — safe to call on every bundle poll.
36
+ * Never overwrites a file we did not create.
37
+ */
38
+ export declare function plantHoneypots(): PlantResult;
39
+ /** Remove decoys WE planted (state-tracked only) and clear the state file. */
40
+ export declare function removeHoneypots(): string[];
41
+ /**
42
+ * Absolute paths of active decoys, for the deterministic guard. Fast (one
43
+ * small JSON read), never throws — enforcement surfaces call this per scan.
44
+ */
45
+ export declare function getHoneypotPaths(): string[];
@@ -0,0 +1,190 @@
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.plantHoneypots = plantHoneypots;
37
+ exports.removeHoneypots = removeHoneypots;
38
+ exports.getHoneypotPaths = getHoneypotPaths;
39
+ const crypto = __importStar(require("crypto"));
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const STATE_DIR = path.join(os.homedir(), '.fullcourtdefense');
44
+ const STATE_FILE = path.join(STATE_DIR, 'honeypots.json');
45
+ function randomToken(length, alphabet) {
46
+ const bytes = crypto.randomBytes(length);
47
+ let out = '';
48
+ for (let i = 0; i < length; i++)
49
+ out += alphabet[bytes[i] % alphabet.length];
50
+ return out;
51
+ }
52
+ const UPPER_NUM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
53
+ const BASE64ISH = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
54
+ // PEM armor markers assembled at runtime so this source file (and the built
55
+ // bundle) never contains contiguous key-material signatures — our own secret
56
+ // scanners and third-party ones must not flag the decoy GENERATOR.
57
+ function pemMarker(kind) {
58
+ return ['-'.repeat(5) + kind, 'OPENSSH', 'PRIVATE', 'KEY' + '-'.repeat(5)].join(' ');
59
+ }
60
+ /** Decoy definitions. Content is random per machine so a leaked copy is traceable to this plant. */
61
+ function decoyDefinitions() {
62
+ const home = os.homedir();
63
+ return [
64
+ {
65
+ absPath: path.join(home, '.aws', 'credentials.bak'),
66
+ itemId: 'honeypot_aws_credentials',
67
+ content: () => [
68
+ '[default]',
69
+ `aws_access_key_id = ${'AKIA'}${randomToken(16, UPPER_NUM)}`,
70
+ `aws_secret_access_key = ${randomToken(40, BASE64ISH)}`,
71
+ '',
72
+ '[prod]',
73
+ `aws_access_key_id = ${'AKIA'}${randomToken(16, UPPER_NUM)}`,
74
+ `aws_secret_access_key = ${randomToken(40, BASE64ISH)}`,
75
+ '',
76
+ ].join('\n'),
77
+ },
78
+ {
79
+ absPath: path.join(home, '.ssh', 'id_rsa.bak'),
80
+ itemId: 'honeypot_ssh_key',
81
+ content: () => {
82
+ const lines = [pemMarker('BEGIN')];
83
+ for (let i = 0; i < 24; i++)
84
+ lines.push(randomToken(70, BASE64ISH));
85
+ lines.push(`${randomToken(28, BASE64ISH)}==`);
86
+ lines.push(pemMarker('END'));
87
+ lines.push('');
88
+ return lines.join('\n');
89
+ },
90
+ },
91
+ {
92
+ absPath: path.join(home, '.env.bak'),
93
+ itemId: 'honeypot_env_backup',
94
+ content: () => [
95
+ '# production backup',
96
+ `DATABASE_URL=postgres://app:${randomToken(24, BASE64ISH)}@db-prod.internal:5432/main`,
97
+ `STRIPE_SECRET_KEY=${'sk_live_'}${randomToken(24, BASE64ISH)}`,
98
+ `JWT_SECRET=${randomToken(48, BASE64ISH)}`,
99
+ `ADMIN_API_TOKEN=${randomToken(32, BASE64ISH)}`,
100
+ '',
101
+ ].join('\n'),
102
+ },
103
+ ];
104
+ }
105
+ function readState() {
106
+ try {
107
+ const parsed = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
108
+ if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.decoys))
109
+ return undefined;
110
+ return parsed;
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ }
116
+ function writeState(state) {
117
+ fs.mkdirSync(STATE_DIR, { recursive: true });
118
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
119
+ }
120
+ /**
121
+ * Plant (or re-plant) decoys. Idempotent — safe to call on every bundle poll.
122
+ * Never overwrites a file we did not create.
123
+ */
124
+ function plantHoneypots() {
125
+ const previous = readState();
126
+ const tracked = new Set((previous?.decoys || []).map(decoy => decoy.path));
127
+ const result = { planted: [], existing: [], skipped: [] };
128
+ const decoys = [];
129
+ for (const def of decoyDefinitions()) {
130
+ const exists = fs.existsSync(def.absPath);
131
+ if (exists && !tracked.has(def.absPath)) {
132
+ // A real user file lives here — leave it alone and never track it.
133
+ result.skipped.push(def.absPath);
134
+ continue;
135
+ }
136
+ if (exists) {
137
+ decoys.push({ path: def.absPath, itemId: def.itemId });
138
+ result.existing.push(def.absPath);
139
+ continue;
140
+ }
141
+ try {
142
+ fs.mkdirSync(path.dirname(def.absPath), { recursive: true });
143
+ fs.writeFileSync(def.absPath, def.content(), { encoding: 'utf8', mode: 0o600 });
144
+ decoys.push({ path: def.absPath, itemId: def.itemId });
145
+ result.planted.push(def.absPath);
146
+ }
147
+ catch {
148
+ // Unwritable location (permissions) — skip; other decoys still count.
149
+ result.skipped.push(def.absPath);
150
+ }
151
+ }
152
+ writeState({
153
+ version: 1,
154
+ enabled: true,
155
+ plantedAt: previous?.plantedAt || new Date().toISOString(),
156
+ decoys,
157
+ });
158
+ return result;
159
+ }
160
+ /** Remove decoys WE planted (state-tracked only) and clear the state file. */
161
+ function removeHoneypots() {
162
+ const state = readState();
163
+ if (!state)
164
+ return [];
165
+ const removed = [];
166
+ for (const decoy of state.decoys) {
167
+ try {
168
+ if (fs.existsSync(decoy.path)) {
169
+ fs.unlinkSync(decoy.path);
170
+ removed.push(decoy.path);
171
+ }
172
+ }
173
+ catch { /* best-effort — a locked file stays but detection also stays */ }
174
+ }
175
+ try {
176
+ fs.unlinkSync(STATE_FILE);
177
+ }
178
+ catch { /* already gone */ }
179
+ return removed;
180
+ }
181
+ /**
182
+ * Absolute paths of active decoys, for the deterministic guard. Fast (one
183
+ * small JSON read), never throws — enforcement surfaces call this per scan.
184
+ */
185
+ function getHoneypotPaths() {
186
+ const state = readState();
187
+ if (!state || state.enabled !== true)
188
+ return [];
189
+ return state.decoys.map(decoy => decoy.path).filter(Boolean);
190
+ }
@@ -40,6 +40,7 @@ const crypto = __importStar(require("crypto"));
40
40
  const fs = __importStar(require("fs"));
41
41
  const os = __importStar(require("os"));
42
42
  const path = __importStar(require("path"));
43
+ const honeypot_1 = require("./honeypot");
43
44
  const CACHE_TTL_MS = 60_000;
44
45
  const CACHE_DIR = path.join(os.homedir(), '.fullcourtdefense');
45
46
  function cachePath(input) {
@@ -132,6 +133,10 @@ function snapshotToScanOptions(snapshot, extra = {}) {
132
133
  disabledBuiltInItemIds: snapshot?.disabledBuiltInItemIds,
133
134
  customBlocks: snapshot?.customBlocks,
134
135
  policyHash: snapshot?.policyHash,
136
+ // Machine-local decoy paths planted by the daemon — every enforcement
137
+ // surface (hooks, MCP gateway, desktop chat guard) gets honeypot
138
+ // detection through this single seam.
139
+ honeypotPaths: (0, honeypot_1.getHoneypotPaths)(),
135
140
  ...extra,
136
141
  };
137
142
  }
@@ -33,6 +33,10 @@ export interface RuntimeBundle {
33
33
  enabled: boolean;
34
34
  targetVersion?: string;
35
35
  };
36
+ /** Org honeypot policy: the daemon plants/removes decoy credential files. */
37
+ honeypot?: {
38
+ enabled: boolean;
39
+ };
36
40
  /** One constrained, auditable action queued for the resident daemon. */
37
41
  machineAction?: {
38
42
  id: string;
@@ -72,7 +72,7 @@ async function getRuntimeBundle(input) {
72
72
  const cached = cache[input.shieldId];
73
73
  const fresh = cached && Date.now() - cached.fetchedAt < ttl;
74
74
  if (cached && fresh && !input.force) {
75
- return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, disabledScanRoots: cached.disabledScanRoots, autoUpdate: cached.autoUpdate, machineAction: cached.machineAction, source: 'cache' };
75
+ return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, disabledScanRoots: cached.disabledScanRoots, autoUpdate: cached.autoUpdate, honeypot: cached.honeypot, machineAction: cached.machineAction, source: 'cache' };
76
76
  }
77
77
  try {
78
78
  const headers = { 'Content-Type': 'application/json' };
@@ -92,7 +92,7 @@ async function getRuntimeBundle(input) {
92
92
  if (resp.status === 304 && cached) {
93
93
  cache[input.shieldId] = { ...cached, fetchedAt: Date.now() };
94
94
  writeCacheFile(cache);
95
- return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, disabledScanRoots: cached.disabledScanRoots, autoUpdate: cached.autoUpdate, machineAction: cached.machineAction, source: 'cache' };
95
+ return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, disabledScanRoots: cached.disabledScanRoots, autoUpdate: cached.autoUpdate, honeypot: cached.honeypot, machineAction: cached.machineAction, source: 'cache' };
96
96
  }
97
97
  if (resp.ok) {
98
98
  const body = await resp.json().catch(() => ({}));
@@ -117,6 +117,9 @@ async function getRuntimeBundle(input) {
117
117
  targetVersion: typeof body.data.autoUpdate.targetVersion === 'string' ? body.data.autoUpdate.targetVersion : undefined,
118
118
  }
119
119
  : undefined,
120
+ honeypot: body.data.honeypot && typeof body.data.honeypot === 'object'
121
+ ? { enabled: body.data.honeypot.enabled === true }
122
+ : undefined,
120
123
  machineAction: body.data.machineAction,
121
124
  fetchedAt: Date.now(),
122
125
  };
@@ -128,7 +131,7 @@ async function getRuntimeBundle(input) {
128
131
  }
129
132
  catch { /* fall through to cache / default */ }
130
133
  if (cached) {
131
- return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, disabledScanRoots: cached.disabledScanRoots, autoUpdate: cached.autoUpdate, machineAction: cached.machineAction, source: 'cache' };
134
+ return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, disabledScanRoots: cached.disabledScanRoots, autoUpdate: cached.autoUpdate, honeypot: cached.honeypot, machineAction: cached.machineAction, source: 'cache' };
132
135
  }
133
136
  return { mode: 'block', version: '', source: 'default' };
134
137
  }
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.21.25"
2
+ "version": "1.21.26"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.21.25",
3
+ "version": "1.21.26",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "build": "tsc && node scripts/copy-attack-corpus.js",
17
17
  "test:deterministic-guard": "npm run build && node scripts/test-deterministic-guard.js",
18
18
  "test:guard-content-context": "npm run build && node scripts/test-guard-content-context.js",
19
+ "test:honeypot": "npm run build && node scripts/test-honeypot.js",
19
20
  "test:browser-credentials-rule": "npm run build && node scripts/test-browser-credentials-rule.js",
20
21
  "test:taint-ledger": "npm run build && node scripts/test-taint-ledger.js",
21
22
  "test:shell-audit": "npm run build && node scripts/test-shell-audit.js",