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.
- package/dist/actionPolicyEngine.d.ts +23 -0
- package/dist/actionPolicyEngine.js +58 -19
- package/dist/approvalPrompt.d.ts +15 -0
- package/dist/approvalPrompt.js +157 -20
- package/dist/askDialog.d.ts +2 -0
- package/dist/askDialog.js +15 -1
- package/dist/askHeader.d.ts +81 -0
- package/dist/askHeader.js +573 -0
- package/dist/blockExplanation.d.ts +9 -0
- package/dist/blockExplanation.js +14 -0
- package/dist/callerEnv.d.ts +1 -1
- package/dist/callerEnv.js +14 -0
- package/dist/cloudGrant.d.ts +46 -0
- package/dist/cloudGrant.js +420 -0
- package/dist/cloudGrantTools.d.ts +97 -0
- package/dist/cloudGrantTools.js +1112 -0
- package/dist/commandNarrative.d.ts +8 -0
- package/dist/commandNarrative.js +222 -49
- package/dist/commands/cmdGuard.js +1 -1
- package/dist/commands/daemon.js +14 -0
- package/dist/commands/deterministicGuard.d.ts +8 -2
- package/dist/commands/deterministicGuard.js +219 -55
- package/dist/commands/discover.js +37 -14
- package/dist/commands/discoverAgentFiles.d.ts +2 -0
- package/dist/commands/discoverAgentFiles.js +1 -0
- package/dist/commands/discoverPaths.d.ts +4 -0
- package/dist/commands/discoverPaths.js +19 -1
- package/dist/commands/discoverProjectRoots.d.ts +72 -0
- package/dist/commands/discoverProjectRoots.js +327 -0
- package/dist/commands/discoverProxy.d.ts +1 -1
- package/dist/commands/discoverProxy.js +1 -1
- package/dist/commands/discoverSecrets.d.ts +44 -0
- package/dist/commands/discoverSecrets.js +158 -14
- package/dist/commands/hook.js +10 -7
- package/dist/commands/mcpGateway.js +9 -7
- package/dist/commands/posixShellGuard.js +1 -1
- package/dist/commands/shellGuard.js +70 -25
- package/dist/commands/uninstallAll.d.ts +1 -7
- package/dist/commands/uninstallAll.js +6 -45
- package/dist/config.d.ts +1 -1
- package/dist/config.js +3 -3
- package/dist/credentialGrant.d.ts +48 -0
- package/dist/credentialGrant.js +29 -18
- package/dist/credentialStore.js +1 -1
- package/dist/devConfirm.d.ts +44 -3
- package/dist/devConfirm.js +166 -30
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/grantTargets.d.ts +21 -0
- package/dist/grantTargets.js +235 -0
- package/dist/localSafetySnapshot.js +2 -0
- package/dist/msiProductCode.d.ts +10 -0
- package/dist/msiProductCode.js +124 -0
- package/dist/notify.js +1 -0
- package/dist/selfTest.d.ts +1 -1
- package/dist/selfTest.js +1 -1
- package/dist/selfUpdate.d.ts +51 -2
- package/dist/selfUpdate.js +217 -19
- package/dist/shellAst.d.ts +2 -0
- package/dist/shellAst.js +7 -1
- package/dist/shellAstLoader.js +12 -1
- package/dist/version.json +1 -1
- 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 (`- …`). */
|
package/dist/commandNarrative.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
-
|
|
449
|
-
|
|
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 (
|
|
532
|
+
if (w[0] === 's3' && w[1] === 'ls')
|
|
454
533
|
return step('Lists S3 buckets or objects');
|
|
455
|
-
if (/^(?:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (/^(?:
|
|
460
|
-
return step(`
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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 ${
|
|
553
|
+
return step(`Reads Kubernetes ${w[1] && !w[1].startsWith('-') ? w[1] : verb}${inNs}`);
|
|
467
554
|
if (verb === 'delete')
|
|
468
|
-
return step(`Deletes Kubernetes ${
|
|
469
|
-
if (/^(?:apply|create|patch|scale|rollout|set|label|annotate|edit|replace)$/.test(verb))
|
|
470
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
728
|
-
|
|
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 =
|
|
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
|
|
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`,
|
package/dist/commands/daemon.js
CHANGED
|
@@ -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 (
|
|
108
|
-
* see
|
|
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";
|