fullcourtdefense-cli 1.36.0 → 1.36.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.
Files changed (63) hide show
  1. package/dist/actionPolicyEngine.d.ts +23 -0
  2. package/dist/actionPolicyEngine.js +58 -19
  3. package/dist/approvalPrompt.d.ts +15 -0
  4. package/dist/approvalPrompt.js +157 -20
  5. package/dist/askDialog.d.ts +2 -0
  6. package/dist/askDialog.js +15 -1
  7. package/dist/askHeader.d.ts +81 -0
  8. package/dist/askHeader.js +573 -0
  9. package/dist/blockExplanation.d.ts +9 -0
  10. package/dist/blockExplanation.js +14 -0
  11. package/dist/callerEnv.d.ts +1 -1
  12. package/dist/callerEnv.js +14 -0
  13. package/dist/cloudGrant.d.ts +46 -0
  14. package/dist/cloudGrant.js +420 -0
  15. package/dist/cloudGrantTools.d.ts +97 -0
  16. package/dist/cloudGrantTools.js +1112 -0
  17. package/dist/commandNarrative.d.ts +8 -0
  18. package/dist/commandNarrative.js +222 -49
  19. package/dist/commands/cmdGuard.js +1 -1
  20. package/dist/commands/daemon.js +14 -0
  21. package/dist/commands/deterministicGuard.d.ts +8 -2
  22. package/dist/commands/deterministicGuard.js +219 -55
  23. package/dist/commands/discover.js +37 -14
  24. package/dist/commands/discoverAgentFiles.d.ts +2 -0
  25. package/dist/commands/discoverAgentFiles.js +1 -0
  26. package/dist/commands/discoverPaths.d.ts +4 -0
  27. package/dist/commands/discoverPaths.js +19 -1
  28. package/dist/commands/discoverProjectRoots.d.ts +72 -0
  29. package/dist/commands/discoverProjectRoots.js +327 -0
  30. package/dist/commands/discoverProxy.d.ts +1 -1
  31. package/dist/commands/discoverProxy.js +1 -1
  32. package/dist/commands/discoverSecrets.d.ts +44 -0
  33. package/dist/commands/discoverSecrets.js +158 -14
  34. package/dist/commands/hook.js +10 -7
  35. package/dist/commands/mcpGateway.js +9 -7
  36. package/dist/commands/posixShellGuard.js +1 -1
  37. package/dist/commands/shellGuard.js +70 -25
  38. package/dist/commands/uninstallAll.d.ts +1 -7
  39. package/dist/commands/uninstallAll.js +6 -45
  40. package/dist/config.d.ts +1 -1
  41. package/dist/config.js +3 -3
  42. package/dist/credentialGrant.d.ts +48 -0
  43. package/dist/credentialGrant.js +29 -18
  44. package/dist/credentialStore.js +1 -1
  45. package/dist/devConfirm.d.ts +44 -3
  46. package/dist/devConfirm.js +166 -30
  47. package/dist/distress.d.ts +2 -0
  48. package/dist/distress.js +2 -0
  49. package/dist/grantTargets.d.ts +21 -0
  50. package/dist/grantTargets.js +235 -0
  51. package/dist/localSafetySnapshot.js +2 -0
  52. package/dist/msiProductCode.d.ts +10 -0
  53. package/dist/msiProductCode.js +124 -0
  54. package/dist/notify.js +1 -0
  55. package/dist/selfTest.d.ts +1 -1
  56. package/dist/selfTest.js +1 -1
  57. package/dist/selfUpdate.d.ts +51 -2
  58. package/dist/selfUpdate.js +217 -19
  59. package/dist/shellAst.d.ts +2 -0
  60. package/dist/shellAst.js +7 -1
  61. package/dist/shellAstLoader.js +12 -1
  62. package/dist/version.json +1 -1
  63. package/package.json +6 -1
@@ -31,6 +31,14 @@ export interface CommandStep {
31
31
  }
32
32
  /** Quote-aware split on `;`, `&&`, `||`, newlines. Pipes stay inside a step. */
33
33
  export declare function splitCommandSteps(command: string): string[];
34
+ /**
35
+ * A code fragment for a step line that never ends mid-expression: cut at `max`,
36
+ * and when the cut (or the fragment itself) leaves a quote or bracket open,
37
+ * end with `…` and close them — `q @{` -> `q @{…}`, `@($m |` -> `@($m |…)`.
38
+ */
39
+ export declare function shortCode(value: string, max?: number): string;
40
+ /** Every host a token read on this line is sent to, as the step list names it. Empty when none is readable. */
41
+ export declare function tokenDestinations(command: string): string[];
34
42
  /** One plain-words line per step, in order. */
35
43
  export declare function describeShellCommand(command: string): CommandStep[];
36
44
  /** The step lines as the explanation prints them (`- …`). */
@@ -16,6 +16,8 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.splitCommandSteps = splitCommandSteps;
19
+ exports.shortCode = shortCode;
20
+ exports.tokenDestinations = tokenDestinations;
19
21
  exports.describeShellCommand = describeShellCommand;
20
22
  exports.describeShellCommandLines = describeShellCommandLines;
21
23
  const MAX_STEPS = 14;
@@ -68,13 +70,51 @@ function splitCommandSteps(command) {
68
70
  }
69
71
  /** Tokens with quotes stripped; `--flag=value` kept whole. */
70
72
  function tokens(segment) {
71
- const out = segment.match(/"(?:[^"\\]|\\.)*"|'[^']*'|[^\s]+/g) || [];
73
+ // `$( … )` stays one token: `--project $(cat p.txt)` must not read as `$(cat`.
74
+ const out = segment.match(/(?:\$\([^)]*\)|"(?:[^"\\]|\\.)*"|'[^']*'|[^\s"'$]|\$(?!\()|["'$])+/g) || [];
72
75
  return out.map(token => token.replace(/^["']|["']$/g, ''));
73
76
  }
74
77
  function shortValue(value, max = 80) {
75
78
  const text = value.replace(/\s+/g, ' ').trim();
76
79
  return text.length > max ? `${text.slice(0, max - 1)}…` : text;
77
80
  }
81
+ const CLOSER = { '(': ')', '{': '}', '[': ']' };
82
+ /**
83
+ * A code fragment for a step line that never ends mid-expression: cut at `max`,
84
+ * and when the cut (or the fragment itself) leaves a quote or bracket open,
85
+ * end with `…` and close them — `q @{` -> `q @{…}`, `@($m |` -> `@($m |…)`.
86
+ */
87
+ function shortCode(value, max = 60) {
88
+ const text = value.replace(/\s+/g, ' ').trim();
89
+ const cut = text.length > max ? text.slice(0, max - 1).trimEnd() : text;
90
+ const open = [];
91
+ let quote = '';
92
+ for (const ch of cut) {
93
+ if (quote) {
94
+ if (ch === quote)
95
+ quote = '';
96
+ continue;
97
+ }
98
+ if (ch === '"' || ch === "'") {
99
+ quote = ch;
100
+ continue;
101
+ }
102
+ if (CLOSER[ch])
103
+ open.push(CLOSER[ch]);
104
+ else if (open.length && ch === open[open.length - 1])
105
+ open.pop();
106
+ }
107
+ if (cut === text && !quote && !open.length)
108
+ return text;
109
+ return `${cut}…${quote}${open.reverse().join('')}`;
110
+ }
111
+ /** Names of functions the script defines itself (`function Q($b) { … }`), lower-cased -> as written. */
112
+ function localFunctions(command) {
113
+ const names = new Map();
114
+ for (const m of command.matchAll(/(?:^|[\s;{])function\s+([A-Za-z_][\w-]*)/gi))
115
+ names.set(m[1].toLowerCase(), m[1]);
116
+ return names;
117
+ }
78
118
  function bucketOf(uri) {
79
119
  const m = /^(?:gs|s3|az):\/\/([^/\s]+)/i.exec(uri);
80
120
  return m ? m[1] : uri;
@@ -94,6 +134,18 @@ function flagValue(toks, name) {
94
134
  }
95
135
  return undefined;
96
136
  }
137
+ /** Global options that take a separate value, per CLI: their value is never the subcommand. */
138
+ const KUBE_VALUED_FLAGS = /^(?:--context|-n|--namespace|--kubeconfig|--cluster|--user|-s|--server|--as|--as-group|--token|--request-timeout|--certificate-authority)$/;
139
+ const HELM_VALUED_FLAGS = /^(?:--kube-context|-n|--namespace|--kubeconfig|--kube-apiserver|--kube-token|--kube-as-user|--registry-config|--repository-config|--repository-cache)$/;
140
+ const AWS_VALUED_FLAGS = /^(?:--profile|--region|--output|--endpoint-url|--query|--color|--ca-bundle|--cli-read-timeout|--cli-connect-timeout)$/;
141
+ const AZ_VALUED_FLAGS = /^(?:--subscription|-o|--output|--query)$/;
142
+ /** The words after the program with its global options skipped: `kubectl --context prod apply -f x` -> `apply -f x`. */
143
+ function afterGlobalFlags(toks, valued) {
144
+ let i = 1;
145
+ while (i < toks.length && toks[i].startsWith('-'))
146
+ i += !toks[i].includes('=') && valued.test(toks[i]) ? 2 : 1;
147
+ return toks.slice(i);
148
+ }
97
149
  function operandsAfter(toks, from) {
98
150
  const out = [];
99
151
  for (let i = from; i < toks.length; i++) {
@@ -184,6 +236,11 @@ function describeGcloud(toks, captured) {
184
236
  const token = toks[i];
185
237
  if (token.startsWith('-'))
186
238
  continue;
239
+ // The first word names the service (`gcloud run …`), never the verb.
240
+ if (!groups.length && GCLOUD_SERVICE_NAMES[token]) {
241
+ groups.push(token);
242
+ continue;
243
+ }
187
244
  if (GCLOUD_READ.test(token) || GCLOUD_WRITE.test(token) || token === 'delete' || /^(?:export|import|cp|rsync|mv|cat|rm|login|print-access-token|print-identity-token|run|exec|ssh|scp)$/.test(token)) {
188
245
  verb = token;
189
246
  i++;
@@ -257,7 +314,8 @@ function describeHttp(program, segment, toks) {
257
314
  const host = hostOf(segment);
258
315
  const method = (/(?:\s-X|\s--request|\s-Method)[\s=]+["']?([A-Za-z]+)/i.exec(segment)?.[1] || '').toUpperCase();
259
316
  const body = /\s(?:-d|--data(?:-raw|-binary|-urlencode)?|-F|--form|-T|--upload-file|--json|-Body|-InFile)\b/i.test(segment);
260
- const target = host ? ` ${host}` : toks[1] ? ` ${shortValue(toks[1], 60)}` : '';
317
+ const operand = flagValue(toks, '-Uri') || flagValue(toks, '--url') || toks.slice(1).find((t, k) => !t.startsWith('-') && !(toks[k] || '').startsWith('-'));
318
+ const target = host ? ` ${host}` : operand ? ` ${shortCode(operand, 60)}` : '';
261
319
  if (method && method !== 'GET' || body)
262
320
  return `Sends ${method && method !== 'GET' ? `a ${method} request` : 'data'} to${target}`;
263
321
  return `Fetches${target} over HTTP`;
@@ -346,7 +404,7 @@ function peelGroups(text) {
346
404
  }
347
405
  return value;
348
406
  }
349
- function describeOne(segmentIn, captured) {
407
+ function describeOne(segmentIn, captured, fns = new Map()) {
350
408
  const segment = segmentIn.trim();
351
409
  const [head, ...pipe] = splitPipe(segment);
352
410
  const tail = describePipeTail(pipe);
@@ -358,7 +416,7 @@ function describeOne(segmentIn, captured) {
358
416
  // `if (…) { body }` / `else { body }`: the body is what runs; say so conditionally.
359
417
  const block = /^(if|elseif|else|try|catch|finally|foreach|while|for)\b[^{]*\{(.*)\}\s*$/is.exec(head);
360
418
  if (block) {
361
- const bodySteps = splitCommandSteps(block[2]).map(part => describeOne(part).text);
419
+ const bodySteps = splitCommandSteps(block[2]).map(part => describeOne(part, undefined, fns).text);
362
420
  const lead = /^(?:if|elseif)$/i.test(block[1]) ? 'If the condition holds' : /^else$/i.test(block[1]) ? 'Otherwise' : /^(?:try)$/i.test(block[1]) ? 'Tries' : /^(?:catch)$/i.test(block[1]) ? 'On error' : /^finally$/i.test(block[1]) ? 'Finally' : 'For each item';
363
421
  return { segment, text: shortValue(`${lead}: ${bodySteps.join(' · ') || 'nothing'}${tail}`, MAX_LINE), known: true };
364
422
  }
@@ -370,21 +428,31 @@ function describeOne(segmentIn, captured) {
370
428
  if (/^\$ErrorActionPreference$|^\$ProgressPreference$|^\$VerbosePreference$|^\$WarningPreference$|^\$InformationPreference$/i.test(`$${name}`))
371
429
  return step('Sets a PowerShell error/output preference');
372
430
  // A quoted string (interpolating or not), a number, a boolean, a literal array / hashtable.
373
- const literal = /^(?:"(?:[^"\\`]|\\.|`.)*"|'[^']*'|\d+|\$true|\$false|@\([^)]*\)|@\{[^}]*\})$/s.test(rhs);
431
+ // `@( … )` holding a pipe or a statement runs it; only a plain list is a literal.
432
+ const literal = /^(?:"(?:[^"\\`]|\\.|`.)*"|'[^']*'|\d+|\$true|\$false|@\([^)|;]*\)|@\{[^}]*\})$/s.test(rhs);
374
433
  if (literal) {
375
434
  const bare = rhs.replace(/^["']|["']$/g, '');
376
435
  const storage = /(?:gs|s3|az):\/\/([^/\s"'$]+)/i.exec(bare);
377
436
  if (storage)
378
437
  return step(`Sets variable ${name} to a location in storage bucket ${storage[1]}`);
438
+ const url = /^https?:\/\/([^/\s"'`$]+)/i.exec(bare);
439
+ if (url)
440
+ return step(`Sets variable ${name} to a URL on ${url[1].toLowerCase()}`);
379
441
  const interpolates = /\$\w+|\$\{|\$\(/.test(bare);
380
442
  if (interpolates && /(?:\$(?:env:)?(?:TEMP|TMP|LOCALAPPDATA|HOME|PWD|PSScriptRoot|USERPROFILE)\b|%TEMP%|%TMP%|%LOCALAPPDATA%|%USERPROFILE%)/i.test(bare)) {
381
443
  return step(`Sets variable ${name} to a path`);
382
444
  }
383
445
  return step(`Sets variable ${name}${interpolates ? ' from other values' : ` to ${shortValue(bare, 60)}`}`);
384
446
  }
447
+ // `$x = $y.Trim()` / `$x = $y.Count`: a value computed from another variable, not a command.
448
+ if (/^\$[\w:]+(?:\.[\w]+(?:\([^()]*\))?|\[[^\]]*\])+$/.test(rhs))
449
+ return step(`Sets variable ${name} from ${shortCode(rhs, 50)}`);
385
450
  const substituted = /^\$\((.*)\)$/s.exec(rhs) || /^`(.*)`$/s.exec(rhs);
386
- const inner = substituted ? substituted[1].trim() : rhs.startsWith('(') ? peelGroups(rhs) : rhs.replace(/\)\s*(?:\.\w+\(\))*$/, '');
387
- const innerStep = describeOne(inner, name);
451
+ // `@( … )` is an array of what the group outputs: the command is inside, like `( … )`.
452
+ // A stray closing `)` (+ `.Trim()`) left by the caller's split is dropped; a balanced `.Trim()` is kept.
453
+ const strayClose = (rhs.match(/\)/g) || []).length > (rhs.match(/\(/g) || []).length;
454
+ const inner = substituted ? substituted[1].trim() : /^@?\(/.test(rhs) ? peelGroups(rhs.replace(/^@/, '')) : strayClose ? rhs.replace(/\)\s*(?:\.\w+\(\))*$/, '') : rhs;
455
+ const innerStep = describeOne(inner, name, fns);
388
456
  const printer = TOKEN_PRINTERS.find(p => p.re.test(inner));
389
457
  if (printer) {
390
458
  return { segment, text: shortValue(`${innerStep.text}${tail}`, MAX_LINE), known: innerStep.known, captures: { variable: name, ...(printer.issuer ? { issuer: printer.issuer } : {}) } };
@@ -404,6 +472,16 @@ function describeOne(segmentIn, captured) {
404
472
  const programToken = toks[p] || '';
405
473
  const program = programToken.replace(/^.*[\\/]/, '').replace(/\.(?:exe|cmd|bat)$/i, '').toLowerCase();
406
474
  const rest = toks.slice(p);
475
+ // `function Q($b) { … }` runs nothing until called; say what a call does.
476
+ const defined = /^function\s+([A-Za-z_][\w-]*)[^{]*\{(.*)\}\s*$/is.exec(head);
477
+ if (defined) {
478
+ const body = splitCommandSteps(defined[2]).map(part => describeOne(part, undefined, fns).text).filter(text => text !== 'Script structure (no action by itself)');
479
+ return { segment, text: shortValue(`Defines function ${defined[1]}${body.length ? ` (when called: ${body.join(' · ')})` : ''}`, MAX_LINE), known: true };
480
+ }
481
+ // A call to a function the script defines itself: by its name, never the raw fragment.
482
+ const local = fns.get(program);
483
+ if (local)
484
+ return step(`Calls function ${local}`);
407
485
  // Control-flow / block structure
408
486
  if (/^(?:if|elseif|else|try|catch|finally|foreach|for|while|switch|function|param|do|done|fi|then|\}|\{|\)|\()$/i.test(program) || /^[{}()]+$/.test(head))
409
487
  return step('Script structure (no action by itself)');
@@ -445,32 +523,77 @@ function describeOne(segmentIn, captured) {
445
523
  return step(`Runs \`gsutil ${rest[1] || ''}\``, false);
446
524
  }
447
525
  case 'aws': {
448
- if (rest[1] === 's3' && /^(?:cp|sync|mv)$/.test(rest[2] || '')) {
449
- const t = describeTransfer('aws', rest);
526
+ const w = afterGlobalFlags(rest, AWS_VALUED_FLAGS);
527
+ if (w[0] === 's3' && /^(?:cp|sync|mv)$/.test(w[1] || '')) {
528
+ const t = describeTransfer('aws', [program, ...w]);
450
529
  if (t)
451
530
  return step(t);
452
531
  }
453
- if (rest[1] === 's3' && rest[2] === 'ls')
532
+ if (w[0] === 's3' && w[1] === 'ls')
454
533
  return step('Lists S3 buckets or objects');
455
- if (/^(?:describe|list|get)-/.test(rest[2] || ''))
456
- return step(`Reads ${rest[1]} ${(rest[2] || '').replace(/^(?:describe|list|get)-/, '').replace(/-/g, ' ')}`);
457
- if (/^(?:delete|terminate)-/.test(rest[2] || ''))
458
- return step(`Deletes ${rest[1]} ${(rest[2] || '').replace(/^(?:delete|terminate)-/, '').replace(/-/g, ' ')}`);
459
- if (/^(?:create|update|put|register)-/.test(rest[2] || ''))
460
- return step(`Changes ${rest[1]} ${(rest[2] || '').replace(/-/g, ' ')}`);
461
- return step(`Runs \`aws ${[rest[1], rest[2]].filter(Boolean).join(' ')}\``, false);
462
- }
463
- case 'kubectl': {
464
- const verb = rest[1] || '';
534
+ if (w[0] === 's3' && /^(?:rm|rb)$/.test(w[1] || '')) {
535
+ const uri = w.slice(2).find(isStorageUri);
536
+ return step(`Deletes ${w[1] === 'rb' ? 'storage bucket' : 'objects in storage bucket'} ${uri ? bucketOf(uri) : 'unknown'}`);
537
+ }
538
+ if (/^(?:describe|list|get)-/.test(w[1] || ''))
539
+ return step(`Reads ${w[0]} ${(w[1] || '').replace(/^(?:describe|list|get)-/, '').replace(/-/g, ' ')}`);
540
+ if (/^(?:delete|terminate)-/.test(w[1] || ''))
541
+ return step(`Deletes ${w[0]} ${(w[1] || '').replace(/^(?:delete|terminate)-/, '').replace(/-/g, ' ')}`);
542
+ if (/^(?:create|update|put|register)-/.test(w[1] || ''))
543
+ return step(`Changes ${w[0]} ${(w[1] || '').replace(/-/g, ' ')}`);
544
+ return step(`Runs \`aws ${[w[0], w[1]].filter(Boolean).join(' ')}\``, false);
545
+ }
546
+ case 'kubectl':
547
+ case 'oc': {
548
+ const w = afterGlobalFlags(rest, KUBE_VALUED_FLAGS);
549
+ const verb = w[0] || '';
550
+ const ns = flagValue(rest, '-n') || flagValue(rest, '--namespace');
551
+ const inNs = ns ? ` in namespace ${ns}` : '';
465
552
  if (/^(?:get|describe|logs|top|explain|api-resources|version)$/.test(verb))
466
- return step(`Reads Kubernetes ${rest[2] || verb}${flagValue(rest, '-n') || flagValue(rest, '--namespace') ? ` in namespace ${flagValue(rest, '-n') || flagValue(rest, '--namespace')}` : ''}`);
553
+ return step(`Reads Kubernetes ${w[1] && !w[1].startsWith('-') ? w[1] : verb}${inNs}`);
467
554
  if (verb === 'delete')
468
- return step(`Deletes Kubernetes ${rest.slice(2).filter(t => !t.startsWith('-')).join(' ')}`);
469
- if (/^(?:apply|create|patch|scale|rollout|set|label|annotate|edit|replace)$/.test(verb))
470
- return step(`Changes Kubernetes resources (${verb})`);
555
+ return step(`Deletes Kubernetes ${operandsAfter(w, 1).join(' ') || 'resources'}${inNs}`);
556
+ if (/^(?:apply|create|patch|scale|rollout|set|label|annotate|edit|replace)$/.test(verb)) {
557
+ const file = flagValue(w, '-f') || flagValue(w, '--filename');
558
+ return step(`Changes Kubernetes resources (${verb}${file ? ` ${shortValue(file, 40)}` : ''})${inNs}`);
559
+ }
471
560
  if (verb === 'exec')
472
561
  return step('Runs a command inside a Kubernetes pod', false);
473
- return step(`Runs \`kubectl ${verb}\``, false);
562
+ return step(`Runs \`${program} ${verb}\``, false);
563
+ }
564
+ case 'helm': {
565
+ const w = afterGlobalFlags(rest, HELM_VALUED_FLAGS);
566
+ const verb = w[0] || '';
567
+ const release = operandsAfter(w, 1)[0];
568
+ const ns = flagValue(rest, '-n') || flagValue(rest, '--namespace');
569
+ const where = `${release ? ` ${shortValue(release, 40)}` : ''}${ns ? ` in namespace ${ns}` : ''}`;
570
+ if (/^(?:upgrade|install|rollback)$/.test(verb))
571
+ return step(`${verb === 'rollback' ? 'Rolls back' : 'Deploys'} Helm release${where}`);
572
+ if (/^(?:uninstall|delete|del|un)$/.test(verb))
573
+ return step(`Deletes Helm release${where}`);
574
+ if (/^(?:list|ls|status|get|history|show|template|lint|search)$/.test(verb))
575
+ return step(`Reads Helm ${verb}${where}`);
576
+ return step(`Runs \`helm ${verb}\``, false);
577
+ }
578
+ case 'az': {
579
+ const w = afterGlobalFlags(rest, AZ_VALUED_FLAGS);
580
+ const path = [];
581
+ for (const word of w) {
582
+ if (word.startsWith('-'))
583
+ break;
584
+ path.push(word);
585
+ }
586
+ const verb = path[path.length - 1] || '';
587
+ const resource = path.slice(0, -1).join(' ') || 'resource';
588
+ const name = flagValue(w, '--name') || flagValue(w, '-n') || flagValue(w, '--resource-group') || flagValue(w, '-g');
589
+ const named = `az ${resource}${name ? ` ${shortValue(name, 40)}` : ''}`;
590
+ if (/^(?:delete|purge|remove)$/.test(verb))
591
+ return step(`Deletes ${named}`);
592
+ if (/^(?:list|show|get|get-access-token)$/.test(verb))
593
+ return step(verb === 'get-access-token' ? 'Reads an Azure access token' : `Reads ${named}`);
594
+ if (/^(?:create|update|set|restart|start|stop|deploy|up)$/.test(verb))
595
+ return step(`${verb === 'create' ? 'Creates' : 'Changes'} ${named}`);
596
+ return step(`Runs \`az ${path.join(' ')}\``, false);
474
597
  }
475
598
  case 'git': {
476
599
  const sub = rest.find((t, idx) => idx > 0 && !t.startsWith('-') && !/^(?:-c|-C)$/.test(rest[idx - 1] || '')) || '';
@@ -599,8 +722,10 @@ function describeOne(segmentIn, captured) {
599
722
  return step(`Runs \`docker ${sub}${rest[2] && !rest[2].startsWith('-') ? ` ${rest[2]}` : ''}\``, sub !== 'exec' && sub !== 'run');
600
723
  return step(`Runs \`docker ${sub}\``, false);
601
724
  }
602
- case 'terraform': {
603
- const sub = rest[1] || '';
725
+ case 'terraform':
726
+ case 'tofu': {
727
+ // Global options (`-chdir=envs/prod`) come before the subcommand.
728
+ const sub = rest.slice(1).find(t => !t.startsWith('-')) || '';
604
729
  if (/^(?:plan|validate|fmt|show|output|state)$/.test(sub))
605
730
  return step(`Reads Terraform ${sub}`);
606
731
  if (sub === 'apply')
@@ -650,10 +775,12 @@ function describeOne(segmentIn, captured) {
650
775
  case 'invoke-expression':
651
776
  case 'iex': return step('RUNS a string as code', false);
652
777
  default:
653
- if (/^\$/.test(programToken))
654
- return step(pipe.length ? `Uses ${shortValue(programToken, 40)}${tail}` : `Reads variable ${shortValue(programToken, 40)}`, false);
778
+ if (/^\$/.test(programToken)) {
779
+ const shaping = pipe.length > 0 && pipe.every(stage => /^(?:where-object|where|\?|foreach-object|%|select-object|select|sort-object|sort|format-table|ft|format-list|fl|out-string|measure-object|group-object)\b/i.test(stage));
780
+ return step(shaping ? `Filters or formats ${shortCode(programToken, 40)}` : pipe.length ? `Uses ${shortCode(programToken, 40)}` : `Reads variable ${shortCode(programToken, 40)}`, false);
781
+ }
655
782
  {
656
- const shown = shortValue([program, rest[1]].filter(t => t && !t.startsWith('-')).join(' '), 50);
783
+ const shown = shortCode([program, rest[1]].filter(t => t && !t.startsWith('-')).join(' '), 50);
657
784
  return step(shown ? `Runs \`${shown}\`` : 'Runs a command this dialog cannot describe', false);
658
785
  }
659
786
  }
@@ -708,39 +835,84 @@ function resolveStorageVariables(steps) {
708
835
  return { ...step, text };
709
836
  });
710
837
  }
838
+ /** `$base = "https://host/…"`, then `Invoke-RestMethod -Uri $base`: say the host. Only literal URLs resolve. */
839
+ function resolveUrlVariables(steps) {
840
+ const hosts = new Map();
841
+ for (const step of steps) {
842
+ const assign = /^\$?([\w:]+)\s*=\s*["']?https?:\/\/([^/\s"'`$]+)/i.exec(step.segment);
843
+ if (assign)
844
+ hosts.set(assign[1].toLowerCase(), assign[2].toLowerCase());
845
+ }
846
+ if (!hosts.size)
847
+ return steps;
848
+ return steps.map(step => ({
849
+ ...step,
850
+ text: step.text.replace(/((?:\bto|^Fetches) )\$([\w:]+)\b/g, (whole, lead, name) => {
851
+ const host = hosts.get(name.toLowerCase());
852
+ return host ? `${lead}${host} (via $${name})` : whole;
853
+ }),
854
+ }));
855
+ }
711
856
  /**
712
857
  * A token stored in a variable, then used later on the line: name the host(s)
713
858
  * that receive it and whether each is the token's issuer. Hosts are literal
714
859
  * URLs in the using step, or a variable assigned a literal URL earlier.
715
860
  */
716
- function noteTokenDestinations(steps) {
861
+ /**
862
+ * Hosts a token stored in `variable` (step `index`) reaches later on the line:
863
+ * literal URLs, or variables assigned a literal URL, in any later statement that
864
+ * uses the token — directly or through a variable built from it
865
+ * (`$H = @{ Authorization = "Bearer $tok" }`, then `Invoke-RestMethod -Headers $H -Uri $base`,
866
+ * also inside a function body the script defines).
867
+ */
868
+ function tokenHosts(steps, index, variable) {
717
869
  const urlVars = new Map();
718
870
  for (const step of steps) {
719
871
  const assign = /^\$?([\w:]+)\s*=\s*["']?https?:\/\/([^/\s"'`$]+)/i.exec(step.segment);
720
872
  if (assign)
721
873
  urlVars.set(assign[1].toLowerCase(), assign[2].toLowerCase());
722
874
  }
875
+ const carriers = new Set([variable.toLowerCase()]);
876
+ const uses = (segment) => [...segment.matchAll(/\$\{?([\w:]+)\}?/g)].some(m => carriers.has(m[1].toLowerCase()));
877
+ const hosts = new Set();
878
+ for (const later of steps.slice(index + 1)) {
879
+ if (!uses(later.segment))
880
+ continue;
881
+ const alias = /^\$([\w:]+)\s*=/.exec(later.segment);
882
+ if (alias)
883
+ carriers.add(alias[1].toLowerCase());
884
+ for (const m of later.segment.matchAll(/https?:\/\/([^/\s"'`<>]+)/gi))
885
+ if (!/[$({]/.test(m[1]))
886
+ hosts.add(m[1].toLowerCase().replace(/:\d+$/, ''));
887
+ for (const m of later.segment.matchAll(/\$\{?([\w:]+)\}?/g)) {
888
+ const host = urlVars.get(m[1].toLowerCase());
889
+ if (host)
890
+ hosts.add(host);
891
+ }
892
+ }
893
+ return [...hosts].sort();
894
+ }
895
+ /** Every host a token read on this line is sent to, as the step list names it. Empty when none is readable. */
896
+ function tokenDestinations(command) {
897
+ const steps = splitCommandSteps(command);
898
+ const fns = localFunctions(command);
899
+ const described = steps.map(segment => { try {
900
+ return describeOne(segment, undefined, fns);
901
+ }
902
+ catch {
903
+ return { segment, text: '', known: false };
904
+ } });
905
+ return [...new Set(described.flatMap((step, index) => step.captures ? tokenHosts(described, index, step.captures.variable) : []))].sort();
906
+ }
907
+ function noteTokenDestinations(steps) {
723
908
  return steps.map((step, index) => {
724
909
  if (!step.captures)
725
910
  return step;
726
911
  const { variable, issuer } = step.captures;
727
- const ref = new RegExp(`\\$\\{?${variable.replace(/[^\w:]/g, '')}\\}?(?![\\w])`, 'i');
728
- const hosts = new Set();
729
- for (const later of steps.slice(index + 1)) {
730
- if (!ref.test(later.segment))
731
- continue;
732
- for (const m of later.segment.matchAll(/https?:\/\/([^/\s"'`<>]+)/gi))
733
- if (!/[$({]/.test(m[1]))
734
- hosts.add(m[1].toLowerCase().replace(/:\d+$/, ''));
735
- for (const m of later.segment.matchAll(/\$\{?([\w:]+)\}?/g)) {
736
- const host = urlVars.get(m[1].toLowerCase());
737
- if (host)
738
- hosts.add(host);
739
- }
740
- }
741
- if (!hosts.size)
912
+ const hosts = tokenHosts(steps, index, variable);
913
+ if (!hosts.length)
742
914
  return step;
743
- const described = [...hosts].sort().map(host => {
915
+ const described = hosts.map(host => {
744
916
  if (!issuer)
745
917
  return host;
746
918
  const own = issuer.domains.some(d => host === d || host.endsWith(`.${d}`));
@@ -752,15 +924,16 @@ function noteTokenDestinations(steps) {
752
924
  /** One plain-words line per step, in order. */
753
925
  function describeShellCommand(command) {
754
926
  const steps = splitCommandSteps(command);
927
+ const fns = localFunctions(command);
755
928
  const described = steps.map(segment => {
756
929
  try {
757
- return describeOne(segment);
930
+ return describeOne(segment, undefined, fns);
758
931
  }
759
932
  catch {
760
933
  return { segment, text: 'Runs a command the grammar does not recognise', known: false };
761
934
  }
762
935
  });
763
- const out = resolveStorageVariables(noteTokenDestinations(described).slice(0, MAX_STEPS));
936
+ const out = resolveUrlVariables(resolveStorageVariables(noteTokenDestinations(described).slice(0, MAX_STEPS)));
764
937
  if (steps.length > MAX_STEPS)
765
938
  out.push({ segment: '', text: `… and ${steps.length - MAX_STEPS} more step${steps.length - MAX_STEPS === 1 ? '' : 's'}`, known: false });
766
939
  return out;
@@ -160,7 +160,7 @@ function buildGuardJs(nodePath, cliEntry) {
160
160
  `// Execution view (aligned with the CLI's stripInertDataSegments): quoted spans containing whitespace are`,
161
161
  `// prose (inert) unless in a code position — interpreter -c/-Command flags, remote executors (ssh/wsl/docker`,
162
162
  `// exec), $( ) substitution, or real backtick command substitution (>=2 chars; a 1-char pair is a PS escape).`,
163
- `function execView(line){if(/\\b(?:iex|invoke-expression|invoke-command)\\b|\\beval\\b|\\bxargs\\b|\\bbase64\\b[^\\n]*(?:-d\\b|--decode\\b)|frombase64string|-encodedcommand\\b|\\s-enc\\s|\\|\\s*&?\\s*(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\\.exe)?|cmd(?:\\.exe)?|node|python[0-9.]*|perl|ruby)\\b|\\b(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\\.exe)?|cmd(?:\\.exe)?|node|python[0-9.]*|perl|ruby)\\s+(?:-c|\\/c|-e|-command|-scriptblock)\\b|\\b(?:ssh|wsl(?:\\.exe)?|chroot)\\b|\\b(?:docker|kubectl|podman|nerdctl)(?:\\.exe)?\\s+(?:exec|run)\\b/i.test(line))return line;const K=/\\$\\(|\`[^\`]{2,}\`/;const C=/(?:(?:^|[;&|(]|\\s)(?:-c|\\/c|-e|-command|-scriptblock|-filter)[= ]\\s*|(?:^|[;&|(]\\s*)(?:\\S*[\\\\\\/])?(?:sudo\\s+|doas\\s+)?(?:ssh|wsl(?:\\.exe)?|chroot|su|screen|tmux)(?:\\s[^;&|"']*)?\\s|(?:\\S*[\\\\\\/])?(?:docker|kubectl|podman|nerdctl)(?:\\.exe)?\\s+(?:exec|run)\\b[^;&|"']*\\s)$/i;let o=line;o=o.replace(/@'[\\s\\S]*?'@/g,' fcd_inert_data ');o=o.replace(/@"[\\s\\S]*?"@/g,m=>m.includes('$(')?m:' fcd_inert_data ');o=o.replace(/<<-?\\s*'(\\w+)'[\\s\\S]*?(?:\\n\\1\\b|$)/g,' fcd_inert_data ');o=o.replace(/<<-?\\s*"?(\\w+)"?[\\s\\S]*?(?:\\n\\1\\b|$)/g,m=>K.test(m)?m:' fcd_inert_data ');const s=o;o=s.replace(/"((?:[^"\\\\]|\\\\.)*)"|'((?:[^'\\\\]|\\\\.)*)'/g,(w,dq,sq,i)=>{const b=dq!==undefined?dq:(sq||'');if(!/\\s/.test(b))return w;if(C.test(s.slice(Math.max(0,i-80),i)))return w;if(dq!==undefined&&K.test(b))return w;return ' fcd_inert_data ';});return o;}`,
163
+ `function execView(line){if(/\\b(?:iex|invoke-expression|invoke-command)\\b|\\beval\\b|\\bxargs\\b|\\bbase64\\b[^\\n]*(?:-d\\b|--decode\\b)|frombase64string|-encodedcommand\\b|\\s-enc\\s|(?<!\\|)\\|(?!\\|)\\s*&?\\s*(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\\.exe)?|cmd(?:\\.exe)?|node|python[0-9.]*|perl|ruby)\\b|\\b(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\\.exe)?|cmd(?:\\.exe)?|node|python[0-9.]*|perl|ruby)\\s+(?:-c|\\/c|-e|-command|-scriptblock)\\b|\\b(?:ssh|wsl(?:\\.exe)?|chroot)\\b|\\b(?:docker|kubectl|podman|nerdctl)(?:\\.exe)?\\s+(?:exec|run)\\b/i.test(line))return line;const K=/\\$\\(|\`[^\`]{2,}\`/;const C=/(?:(?:^|[;&|(]|\\s)(?:-c|\\/c|-e|-command|-scriptblock|-filter)[= ]\\s*|(?:^|[;&|(]\\s*)(?:\\S*[\\\\\\/])?(?:sudo\\s+|doas\\s+)?(?:ssh|wsl(?:\\.exe)?|chroot|su|screen|tmux)(?:\\s[^;&|"']*)?\\s|(?:\\S*[\\\\\\/])?(?:docker|kubectl|podman|nerdctl)(?:\\.exe)?\\s+(?:exec|run)\\b[^;&|"']*\\s)$/i;let o=line;o=o.replace(/@'[\\s\\S]*?'@/g,' fcd_inert_data ');o=o.replace(/@"[\\s\\S]*?"@/g,m=>m.includes('$(')?m:' fcd_inert_data ');o=o.replace(/<<-?\\s*'(\\w+)'[\\s\\S]*?(?:\\n\\1\\b|$)/g,' fcd_inert_data ');o=o.replace(/<<-?\\s*"?(\\w+)"?[\\s\\S]*?(?:\\n\\1\\b|$)/g,m=>K.test(m)?m:' fcd_inert_data ');const s=o;o=s.replace(/"((?:[^"\\\\]|\\\\.)*)"|'((?:[^'\\\\]|\\\\.)*)'/g,(w,dq,sq,i)=>{const b=dq!==undefined?dq:(sq||'');if(!/\\s/.test(b))return w;if(C.test(s.slice(Math.max(0,i-80),i)))return w;if(dq!==undefined&&K.test(b))return w;return ' fcd_inert_data ';});return o;}`,
164
164
  `// Precedence on one line: block outranks ask, ask outranks warn.`,
165
165
  `function matchLine(line,rules){const n=line.replace(/\\s+/g,' ').trim();const x=execView(line);const xn=x.replace(/\\s+/g,' ').trim();let warnHit=null;const asks=[];for(const r of rules){try{const t=r.source==='custom'?line:x;const tn=r.source==='custom'?n:xn;if(r.re.test(tn)||r.re.test(t)){if(r.action==='block')return r;if(r.action==='ask'){asks.push(r);}else if(!warnHit)warnHit=r;}}catch{}}if(asks.length===1)return asks[0];if(asks.length>1){asks.sort((a,b)=>a.id<b.id?-1:a.id>b.id?1:0);return{...asks[0],id:'asks:'+asks.map(r=>encodeURIComponent(r.id)).join('+'),reason:asks.map(r=>r.reason).join('; ')}}return warnHit;}`,
166
166
  `// Flush is DETACHED (spawn+unref, matching the posix guard) — a spawnSync here`,
@@ -85,6 +85,7 @@ const machineActionVerify_1 = require("../machineActionVerify");
85
85
  const perfSnapshot_1 = require("../perfSnapshot");
86
86
  const desktopChatGuard_1 = require("./desktopChatGuard");
87
87
  const honeypot_1 = require("../honeypot");
88
+ const msiProductCode_1 = require("../msiProductCode");
88
89
  const windowsAudit_1 = require("./windowsAudit");
89
90
  const hook_1 = require("./hook");
90
91
  const verdictIpc_1 = require("../verdictIpc");
@@ -529,6 +530,19 @@ async function runDaemon(args, config) {
529
530
  const quiet = args.quiet === 'true';
530
531
  log(`Daemon started (pid ${process.pid}, platform ${process.platform}).`);
531
532
  log(`Shield: ${creds.shieldId || '(none — protect-only mode, telemetry disabled)'} API: ${creds.apiUrl}`);
533
+ if (process.platform === 'win32') {
534
+ // `reg query /s` over the uninstall hive can take seconds; keep it off the startup path.
535
+ setTimeout(() => {
536
+ try {
537
+ const productCode = (0, msiProductCode_1.findMsiProductCode)();
538
+ if ((0, msiProductCode_1.recordMsiProductCode)(productCode))
539
+ log(`Recorded installed MSI ProductCode ${productCode} for the Local Safety uninstall rule.`);
540
+ }
541
+ catch (error) {
542
+ log(`Could not record the installed MSI ProductCode: ${error?.message || String(error)}`);
543
+ }
544
+ }, 20_000).unref();
545
+ }
532
546
  // A machine enrolled with a DPAPI key whose decrypt fails (blocked
533
547
  // PowerShell) lands here: shieldId readable, key not. Say so LOUDLY — this
534
548
  // state looks identical to "machine off" on the dashboard otherwise.
@@ -72,6 +72,12 @@ export interface LocalSafetyScanOptions {
72
72
  * attack fixtures (security tooling, guard regression tests).
73
73
  */
74
74
  trustedScriptPaths?: string[];
75
+ /**
76
+ * ProductCodes (`{GUID}`) of the installed FullCourtDefense MSI, recorded on
77
+ * this machine by the daemon from the uninstall registry key. The code changes
78
+ * with every build, so `msiexec /x {GUID}` is ours only when it is listed here.
79
+ */
80
+ fcdProductCodes?: string[];
75
81
  }
76
82
  /**
77
83
  * Built-ins that ship OFF until an org switches them on in the console. Shared
@@ -104,8 +110,8 @@ export interface SharedCommandRule {
104
110
  * useless against them because the secret never comes from a watched file
105
111
  * ("gh auth token" emits the same token that lives in hosts.yml). Obvious,
106
112
  * rarely-legitimate-for-agents forms are on by default; cloud token printers
107
- * that legitimate deploy tooling uses constantly are strict (off by default,
108
- * see DEFAULT_DISABLED_BUILT_INS). Ids match local-safety-catalog.ts.
113
+ * that legitimate deploy tooling uses constantly are strict (warn by default,
114
+ * see DEFAULT_WARN_BUILT_INS). Ids match local-safety-catalog.ts.
109
115
  */
110
116
  export declare const CREDENTIAL_COMMAND_RULES: readonly SharedCommandRule[];
111
117
  export declare const REMOTE_CONTROL_CATALOG_VERSION = "2026-09-26.1";