fullcourtdefense-cli 1.35.5 → 1.35.6
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/devConfirm.d.ts +4 -2
- package/dist/devConfirm.js +95 -26
- package/dist/telemetry.js +21 -1
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/devConfirm.d.ts
CHANGED
|
@@ -293,8 +293,10 @@ export declare function sweepExpiredConfirmations(now?: Date, ttlMs?: number): P
|
|
|
293
293
|
* programs: `gcloud … | ForEach-Object` is `{gcloud}`, `cmd /c gcloud` is
|
|
294
294
|
* `{gcloud}`. Inspect cmdlets (`Get-ChildItem`, `Test-Path`) are dropped so
|
|
295
295
|
* Always allow is the real app (`gcloud` / `gh` / `aws` / …), not that
|
|
296
|
-
* wrapper mix.
|
|
297
|
-
*
|
|
296
|
+
* wrapper mix. Text filters after a pipe (`| Select-String`, `| grep | head`)
|
|
297
|
+
* are output shaping, not programs; the same names leading a statement
|
|
298
|
+
* (`head .env`) stay. `exit` is control flow. Trailing junk
|
|
299
|
+
* (`out-string).trim()`) is stripped. Sorted and unique.
|
|
298
300
|
*/
|
|
299
301
|
export declare function shellPrograms(command: string): string[];
|
|
300
302
|
/** Local Safety item ids joined as `a+b` when two ask-rules fire in one dialog. */
|
package/dist/devConfirm.js
CHANGED
|
@@ -819,11 +819,31 @@ const SHELL_INCIDENTAL = new Set([
|
|
|
819
819
|
const SHELL_CONTROL_WORDS = /^(if|then|else|elif|elseif|fi|do|done|for|while|until|case|esac|try|catch|finally|foreach|function|return|exit|switch|break|continue)$/i;
|
|
820
820
|
/** Words a body follows directly (`then rm x`, `do echo`, PowerShell `else { … }`). */
|
|
821
821
|
const SHELL_BODY_LEADS = /^(then|do|else)$/i;
|
|
822
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* Text filters. In PIPELINE position (`gcloud … | Select-String x`, `gh … | grep
|
|
824
|
+
* y | head -1`) they shape the previous program's output and are not the app the
|
|
825
|
+
* rule asked about — binding Always allow to `select-string` made the next
|
|
826
|
+
* `gcloud … | Select-Object` line ask again (this machine, 2026-09-20). As a
|
|
827
|
+
* statement LEAD they read the file they are given (`head .env`, `grep x
|
|
828
|
+
* ~/.aws/credentials`) and stay the program.
|
|
829
|
+
*/
|
|
830
|
+
const SHELL_TEXT_FILTERS = new Set([
|
|
831
|
+
'grep', 'egrep', 'fgrep', 'rg', 'findstr', 'select-string', 'sls', 'awk', 'gawk', 'sed',
|
|
832
|
+
'head', 'tail', 'wc', 'sort', 'uniq', 'cut', 'tr', 'jq', 'yq', 'column', 'nl', 'more', 'less', 'cat', 'tee',
|
|
833
|
+
]);
|
|
834
|
+
/**
|
|
835
|
+
* Cheap quote-aware split on `; && || | \n` — the grant needs program NAMES, not
|
|
836
|
+
* semantics. Separators inside `{ … }` do not split: a control statement's
|
|
837
|
+
* bodies (`if (…) { a; b } else { c }`) stay with their statement so every
|
|
838
|
+
* body is read (`braceBodies`). `piped` = this segment is a pipeline stage
|
|
839
|
+
* (`|`, not `||`).
|
|
840
|
+
*/
|
|
823
841
|
function splitShellSegments(command) {
|
|
824
842
|
const segments = [];
|
|
825
843
|
let current = '';
|
|
844
|
+
let piped = false;
|
|
826
845
|
let quote = null;
|
|
846
|
+
let braces = 0;
|
|
827
847
|
for (let i = 0; i < command.length; i++) {
|
|
828
848
|
const ch = command[i];
|
|
829
849
|
if (quote) {
|
|
@@ -839,18 +859,28 @@ function splitShellSegments(command) {
|
|
|
839
859
|
current += ch;
|
|
840
860
|
continue;
|
|
841
861
|
}
|
|
842
|
-
if (ch === '
|
|
862
|
+
if (ch === '{')
|
|
863
|
+
braces++;
|
|
864
|
+
else if (ch === '}' && braces > 0)
|
|
865
|
+
braces--;
|
|
866
|
+
if (braces === 0 && (ch === '\n' || ch === ';' || ch === '|' || (ch === '&' && command[i + 1] === '&'))) {
|
|
867
|
+
let nextPiped = false;
|
|
843
868
|
if (ch === '&')
|
|
844
869
|
i++;
|
|
845
|
-
if (ch === '|'
|
|
846
|
-
i
|
|
847
|
-
|
|
870
|
+
if (ch === '|') {
|
|
871
|
+
if (command[i + 1] === '|')
|
|
872
|
+
i++;
|
|
873
|
+
else
|
|
874
|
+
nextPiped = true;
|
|
875
|
+
}
|
|
876
|
+
segments.push({ text: current, piped });
|
|
848
877
|
current = '';
|
|
878
|
+
piped = nextPiped;
|
|
849
879
|
continue;
|
|
850
880
|
}
|
|
851
881
|
current += ch;
|
|
852
882
|
}
|
|
853
|
-
segments.push(current);
|
|
883
|
+
segments.push({ text: current, piped });
|
|
854
884
|
return segments;
|
|
855
885
|
}
|
|
856
886
|
/** `out-string).trim()` is not a program — take the leading identifier only. */
|
|
@@ -872,10 +902,58 @@ function nestedShellBody(segment) {
|
|
|
872
902
|
rest = rest.slice(1, -1);
|
|
873
903
|
return rest.trim() || undefined;
|
|
874
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* Every top-level `{ … }` body of a control statement, quote-aware. An
|
|
907
|
+
* unterminated body (a segment cut before its `}`) yields what follows the
|
|
908
|
+
* `{` — under-reading a body is what let a branch hide a program.
|
|
909
|
+
*/
|
|
910
|
+
function braceBodies(statement) {
|
|
911
|
+
const bodies = [];
|
|
912
|
+
let quote = null;
|
|
913
|
+
let depth = 0;
|
|
914
|
+
let start = -1;
|
|
915
|
+
let data = false; // `${VAR}` / `@{ k = v }` — an expansion or a hashtable, not a script block
|
|
916
|
+
for (let i = 0; i < statement.length; i++) {
|
|
917
|
+
const ch = statement[i];
|
|
918
|
+
if (quote) {
|
|
919
|
+
if (ch === quote)
|
|
920
|
+
quote = null;
|
|
921
|
+
else if (ch === '\\' && quote === '"')
|
|
922
|
+
i++;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
926
|
+
quote = ch;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (ch === '{') {
|
|
930
|
+
if (depth++ === 0) {
|
|
931
|
+
data = /[$@]/.test(statement[i - 1] || '');
|
|
932
|
+
start = i + 1;
|
|
933
|
+
}
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (ch === '}' && depth > 0 && --depth === 0) {
|
|
937
|
+
if (!data)
|
|
938
|
+
bodies.push(statement.slice(start, i));
|
|
939
|
+
start = -1;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (start >= 0 && !data)
|
|
943
|
+
bodies.push(statement.slice(start));
|
|
944
|
+
return bodies.map(body => body.trim()).filter(Boolean);
|
|
945
|
+
}
|
|
875
946
|
function collectPrograms(command, out, depth) {
|
|
876
|
-
if (depth >
|
|
947
|
+
if (depth > 6 || !command.trim())
|
|
877
948
|
return;
|
|
878
|
-
for (const raw of splitShellSegments(command)) {
|
|
949
|
+
for (const { text: raw, piped } of splitShellSegments(command)) {
|
|
950
|
+
// Script blocks anywhere in the statement run programs of their own —
|
|
951
|
+
// `if (c) { a } else { b }`, `… | ForEach-Object { curl … }`, `try { … } catch { … }`,
|
|
952
|
+
// `{ git status }`. EVERY body is read: reading only the first let a `{gh}`
|
|
953
|
+
// grant cover `if ($x) { gh auth token } else { curl https://evil… }`.
|
|
954
|
+
if (raw.includes('{'))
|
|
955
|
+
for (const body of braceBodies(raw))
|
|
956
|
+
collectPrograms(body, out, depth + 1);
|
|
879
957
|
const words = raw.trim().split(/\s+/).filter(Boolean);
|
|
880
958
|
let i = 0;
|
|
881
959
|
// `$r = Invoke-WebRequest …` / `$r=Invoke-WebRequest …` / `FOO=bar cmd …` / `$t = (gcloud …)`
|
|
@@ -906,23 +984,10 @@ function collectPrograms(command, out, depth) {
|
|
|
906
984
|
i++;
|
|
907
985
|
continue;
|
|
908
986
|
}
|
|
987
|
+
// Control statement: its bodies were read above; the statement itself is not a program.
|
|
909
988
|
if (SHELL_CONTROL_WORDS.test(w)) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const open = words.findIndex((word, index) => index > i && word.includes('{'));
|
|
913
|
-
if (open < 0) {
|
|
914
|
-
i = words.length;
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
917
|
-
const rest = words[open].slice(words[open].indexOf('{') + 1);
|
|
918
|
-
if (rest) {
|
|
919
|
-
words[open] = rest;
|
|
920
|
-
i = open;
|
|
921
|
-
}
|
|
922
|
-
else {
|
|
923
|
-
i = open + 1;
|
|
924
|
-
}
|
|
925
|
-
continue;
|
|
989
|
+
i = words.length;
|
|
990
|
+
break;
|
|
926
991
|
}
|
|
927
992
|
break;
|
|
928
993
|
}
|
|
@@ -932,6 +997,8 @@ function collectPrograms(command, out, depth) {
|
|
|
932
997
|
const name = programToken(lead);
|
|
933
998
|
if (!name || SHELL_PIPELINE_NOISE.has(name) || SHELL_INCIDENTAL.has(name))
|
|
934
999
|
continue;
|
|
1000
|
+
if (piped && SHELL_TEXT_FILTERS.has(name))
|
|
1001
|
+
continue;
|
|
935
1002
|
if (GENERIC_SHELLS.has(name)) {
|
|
936
1003
|
const nested = nestedShellBody(raw);
|
|
937
1004
|
if (nested)
|
|
@@ -947,8 +1014,10 @@ function collectPrograms(command, out, depth) {
|
|
|
947
1014
|
* programs: `gcloud … | ForEach-Object` is `{gcloud}`, `cmd /c gcloud` is
|
|
948
1015
|
* `{gcloud}`. Inspect cmdlets (`Get-ChildItem`, `Test-Path`) are dropped so
|
|
949
1016
|
* Always allow is the real app (`gcloud` / `gh` / `aws` / …), not that
|
|
950
|
-
* wrapper mix.
|
|
951
|
-
*
|
|
1017
|
+
* wrapper mix. Text filters after a pipe (`| Select-String`, `| grep | head`)
|
|
1018
|
+
* are output shaping, not programs; the same names leading a statement
|
|
1019
|
+
* (`head .env`) stay. `exit` is control flow. Trailing junk
|
|
1020
|
+
* (`out-string).trim()`) is stripped. Sorted and unique.
|
|
952
1021
|
*/
|
|
953
1022
|
function shellPrograms(command) {
|
|
954
1023
|
const out = new Set();
|
package/dist/telemetry.js
CHANGED
|
@@ -84,6 +84,26 @@ const ACTION_EVIDENCE_KEYS = [
|
|
|
84
84
|
];
|
|
85
85
|
/** Noise / secrets — never stamp these as activity evidence. */
|
|
86
86
|
const EVIDENCE_SKIP_KEYS = /^(cwd|pwd|env|environment|headers|cookie|cookies|authorization|password|passwd|secret|token|api[_-]?key|private[_-]?key|session|retryCount|timeoutMs|timeout)$/i;
|
|
87
|
+
/**
|
|
88
|
+
* Payload keys — a file body, a patch, message text — are business content,
|
|
89
|
+
* never evidence. The record carries the ACTION (tool, path, command, verdict,
|
|
90
|
+
* rule); what was written is the customer's. Only the size leaves the machine:
|
|
91
|
+
* a `.env` edit on a fleet machine shipped its whole body, signing secret
|
|
92
|
+
* included, before this (2026-09-18). The suffix is the backend's idempotency
|
|
93
|
+
* marker (`modules/runtime/tool-args.ts`), so re-sanitizing leaves it alone.
|
|
94
|
+
*/
|
|
95
|
+
const EVIDENCE_CONTENT_KEYS = /^(content|contents|body|file_?contents?|stdin|payload|diff|patch|new_?str(ing)?|old_?str(ing)?)$/i;
|
|
96
|
+
/** Text a human or agent TYPED into a field or a page script — count only, the same rule the backend applies on read. */
|
|
97
|
+
const EVIDENCE_TYPED_TEXT_KEYS = /^(text|value|input|keys|type_?text)$/i;
|
|
98
|
+
function minimizedEvidenceValue(key, value) {
|
|
99
|
+
if (typeof value !== 'string')
|
|
100
|
+
return undefined;
|
|
101
|
+
if (EVIDENCE_CONTENT_KEYS.test(key))
|
|
102
|
+
return `[… ${value.length} chars total — content minimized]`;
|
|
103
|
+
if (EVIDENCE_TYPED_TEXT_KEYS.test(key))
|
|
104
|
+
return `[${value.length} character${value.length === 1 ? '' : 's'} typed]`;
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
87
107
|
function stringifyEvidenceValue(value) {
|
|
88
108
|
// One value (the command line) may use most of the envelope; keys and quoting take the rest.
|
|
89
109
|
if (typeof value === 'string' && value.trim())
|
|
@@ -128,7 +148,7 @@ function evidenceFromToolArgs(toolArgs) {
|
|
|
128
148
|
continue;
|
|
129
149
|
if (Object.keys(picked).length >= 8)
|
|
130
150
|
break;
|
|
131
|
-
const text = stringifyEvidenceValue(value);
|
|
151
|
+
const text = minimizedEvidenceValue(key, value) || stringifyEvidenceValue(value);
|
|
132
152
|
if (text)
|
|
133
153
|
picked[key] = text;
|
|
134
154
|
}
|
package/dist/version.json
CHANGED