fullcourtdefense-cli 1.13.0 → 1.14.0
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/commands/cmdGuard.js +26 -5
- package/dist/commands/posixShellGuard.js +69 -56
- package/dist/commands/shellGuard.d.ts +17 -0
- package/dist/commands/shellGuard.js +54 -0
- package/dist/index.js +66 -2
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -60,12 +60,33 @@ const AUTORUN_BAT_PATH = path.join(os.homedir(), '.fullcourtdefense-cmd-autorun.
|
|
|
60
60
|
const CMD_AUTORUN_KEY = 'HKCU\\Software\\Microsoft\\Command Processor';
|
|
61
61
|
const AUTORUN_VALUE = 'AutoRun';
|
|
62
62
|
const AUTORUN_MARKER = 'fullcourtdefense-cmd-autorun.bat';
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* cmd builtins / exes we intercept via doskey (first token of the typed line).
|
|
65
|
+
*
|
|
66
|
+
* cmd.exe has NO general pre-execution hook (unlike PowerShell PSReadLine or the
|
|
67
|
+
* zsh/bash whole-line hooks), so coverage here is by enumeration: every command
|
|
68
|
+
* name that anchors a dangerous rule must be listed or its rule can never fire in
|
|
69
|
+
* interactive cmd. This is the known residual gap — a command whose leading name
|
|
70
|
+
* is not below runs unchecked. Keep in sync with the builtin rule command anchors
|
|
71
|
+
* in shellGuard.ts. Only leading-token names matter (doskey is first-token only),
|
|
72
|
+
* so pipes/`&&` chains where the dangerous command is not first are NOT caught by
|
|
73
|
+
* cmd (they ARE under PowerShell/zsh/bash and `shell-guard-check`).
|
|
74
|
+
*/
|
|
64
75
|
const INTERCEPTED_COMMANDS = [
|
|
65
|
-
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
|
|
76
|
+
// Destructive filesystem / disk
|
|
77
|
+
'del', 'erase', 'rd', 'rmdir', 'format', 'cipher', 'diskpart', 'fsutil',
|
|
78
|
+
'robocopy', 'xcopy', 'takeown', 'icacls', 'attrib', 'compact',
|
|
79
|
+
// Backup / recovery / shadow-copy tampering (ransomware patterns)
|
|
80
|
+
'vssadmin', 'wbadmin', 'bcdedit', 'wevtutil',
|
|
81
|
+
// Interpreters / download-and-exec vectors
|
|
82
|
+
'powershell', 'pwsh', 'curl', 'wget', 'certutil', 'bitsadmin', 'mshta',
|
|
83
|
+
'rundll32', 'regsvr32', 'wscript', 'cscript', 'wmic', 'wsl',
|
|
84
|
+
// Service / firewall / registry / accounts
|
|
85
|
+
'reg', 'net', 'net1', 'sc', 'netsh', 'schtasks', 'taskkill',
|
|
86
|
+
// Infra / cloud CLIs
|
|
87
|
+
'terraform', 'pulumi', 'kubectl', 'aws', 'gcloud', 'az', 'docker',
|
|
88
|
+
// VCS + database clients
|
|
89
|
+
'git', 'psql', 'mysql', 'mongo', 'mongosh', 'sqlcmd',
|
|
69
90
|
];
|
|
70
91
|
function regString(key, value) {
|
|
71
92
|
try {
|
|
@@ -52,18 +52,23 @@ const shellGuard_1 = require("./shellGuard");
|
|
|
52
52
|
* everywhere.
|
|
53
53
|
*
|
|
54
54
|
* Two hook mechanisms, one shared Node checker (~/.fullcourtdefense-posix-guard.js).
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* Both are COMMAND-AGNOSTIC — they inspect the WHOLE typed line against every
|
|
56
|
+
* rule, so any command in any position is covered (not a fixed watchlist of
|
|
57
|
+
* names). Each shell gets its OWN sourced file (never cross-parsed — zsh-only
|
|
58
|
+
* expansions would otherwise break bash at source time):
|
|
57
59
|
* - zsh (macOS default), ~/.fullcourtdefense-shell-guard.zsh: a ZLE
|
|
58
|
-
* `accept-line` widget inspects the
|
|
59
|
-
*
|
|
60
|
-
* is not accepted (never executes); monitor → warning + the line runs.
|
|
60
|
+
* `accept-line` widget inspects the whole typed line before it runs (catches
|
|
61
|
+
* pipes, e.g. `curl … | sh`, and chained `foo && rm -rf /`). Block mode → the
|
|
62
|
+
* line is not accepted (never executes); monitor → warning + the line runs.
|
|
61
63
|
* Analogous to the PowerShell PSReadLine hook.
|
|
62
|
-
* - bash, ~/.fullcourtdefense-shell-guard.bash:
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* - bash, ~/.fullcourtdefense-shell-guard.bash: a `DEBUG`-trap preexec hook
|
|
65
|
+
* (bash-preexec style) reads the full command line from history right before
|
|
66
|
+
* it runs and, with `extdebug`, skips it on an explicit block (checker exit
|
|
67
|
+
* 97). Whole-line, command-agnostic — the bash analogue of zsh's accept-line.
|
|
68
|
+
*
|
|
69
|
+
* Cost of whole-line coverage: the Node checker runs once per typed command. Off
|
|
70
|
+
* with `export FCD_SHELL_GUARD=off`. Benign lines exit the checker in ~1ms after
|
|
71
|
+
* regex matching; the only real overhead is Node startup per command.
|
|
67
72
|
*
|
|
68
73
|
* Safety: every layer fails OPEN. If the checker is missing or errors, the command
|
|
69
74
|
* runs — a guard bug must never brick a developer's shell. Only an explicit block
|
|
@@ -82,19 +87,6 @@ const SPOOL_PATH = path.join(os.homedir(), '.fullcourtdefense-spool.jsonl');
|
|
|
82
87
|
const MARKER_START = '# >>> fullcourtdefense shell guard >>>';
|
|
83
88
|
const MARKER_END = '# <<< fullcourtdefense shell guard <<<';
|
|
84
89
|
const BLOCK_CODE = 97;
|
|
85
|
-
/**
|
|
86
|
-
* Leading command names that trigger the checker. Everything else runs with zero
|
|
87
|
-
* overhead. Each name maps to at least one builtin rule anchor. (No `node`/`git` —
|
|
88
|
-
* too common, no destructive builtin rule anchors on them.)
|
|
89
|
-
*/
|
|
90
|
-
const WATCHLIST = [
|
|
91
|
-
'rm', 'rmdir', 'dd', 'mkfs', 'chmod', 'chown', 'sudo',
|
|
92
|
-
'bash', 'sh', 'zsh', 'nc', 'ncat', 'netcat', 'socat',
|
|
93
|
-
'curl', 'wget', 'python', 'python2', 'python3', 'perl', 'php', 'ruby',
|
|
94
|
-
'terraform', 'pulumi', 'kubectl', 'gcloud', 'aws', 'az', 'docker',
|
|
95
|
-
'diskutil', 'csrutil', 'spctl', 'iptables', 'ufw', 'systemctl', 'service',
|
|
96
|
-
'launchctl', 'chattr', 'scp', 'psql', 'mysql', 'mongo', 'mongosh', 'crontab',
|
|
97
|
-
];
|
|
98
90
|
function isPosixPlatform() {
|
|
99
91
|
return process.platform === 'darwin' || process.platform === 'linux';
|
|
100
92
|
}
|
|
@@ -129,7 +121,6 @@ function buildGuardJs(nodePath, cliEntry) {
|
|
|
129
121
|
}
|
|
130
122
|
/** zsh guard — sourced ONLY from .zshrc (contains zsh-specific ZLE syntax). */
|
|
131
123
|
function buildGuardZsh(nodePath) {
|
|
132
|
-
const watch = ` ${WATCHLIST.join(' ')} `;
|
|
133
124
|
return [
|
|
134
125
|
`# FullCourtDefense interactive shell guard for zsh (installed by fullcourtdefense-cli).`,
|
|
135
126
|
`# Disable for one session: export FCD_SHELL_GUARD=off`,
|
|
@@ -139,27 +130,21 @@ function buildGuardZsh(nodePath) {
|
|
|
139
130
|
``,
|
|
140
131
|
`FCD_GUARD_NODE=${shSingleQuote(nodePath)}`,
|
|
141
132
|
`FCD_GUARD_JS=${shSingleQuote(GUARD_JS_PATH)}`,
|
|
142
|
-
`__fcd_watch=${shSingleQuote(watch)}`,
|
|
143
133
|
``,
|
|
144
|
-
`# Inspect the WHOLE typed line before accepting it
|
|
134
|
+
`# Inspect the WHOLE typed line against every rule before accepting it`,
|
|
135
|
+
`# (command-agnostic: catches any command, pipes, and chained segments).`,
|
|
145
136
|
`__fcd_zle_accept_line() {`,
|
|
146
137
|
` emulate -L zsh`,
|
|
147
138
|
` local __fcd_line=$BUFFER`,
|
|
148
|
-
` if [[ -n "$__fcd_line" && "\${FCD_SHELL_GUARD:-}" != "off" ]]; then`,
|
|
149
|
-
` local
|
|
150
|
-
`
|
|
151
|
-
`
|
|
152
|
-
`
|
|
153
|
-
`
|
|
154
|
-
`
|
|
155
|
-
` __fcd_out="$("$FCD_GUARD_NODE" "$FCD_GUARD_JS" -- "$__fcd_line" 2>&1)"`,
|
|
156
|
-
` local __fcd_rc=$?`,
|
|
157
|
-
` if [[ $__fcd_rc -eq 97 ]]; then`,
|
|
158
|
-
` zle -M -- "$__fcd_out"`,
|
|
159
|
-
` return`,
|
|
160
|
-
` fi`,
|
|
161
|
-
` [[ -n "$__fcd_out" ]] && print -u2 -r -- "$__fcd_out"`,
|
|
139
|
+
` if [[ -n "$__fcd_line" && "\${FCD_SHELL_GUARD:-}" != "off" && -x "$FCD_GUARD_NODE" && -f "$FCD_GUARD_JS" ]]; then`,
|
|
140
|
+
` local __fcd_out`,
|
|
141
|
+
` __fcd_out="$("$FCD_GUARD_NODE" "$FCD_GUARD_JS" -- "$__fcd_line" 2>&1)"`,
|
|
142
|
+
` local __fcd_rc=$?`,
|
|
143
|
+
` if [[ $__fcd_rc -eq 97 ]]; then`,
|
|
144
|
+
` zle -M -- "$__fcd_out"`,
|
|
145
|
+
` return`,
|
|
162
146
|
` fi`,
|
|
147
|
+
` [[ -n "$__fcd_out" ]] && print -u2 -r -- "$__fcd_out"`,
|
|
163
148
|
` fi`,
|
|
164
149
|
` zle .accept-line`,
|
|
165
150
|
`}`,
|
|
@@ -169,9 +154,19 @@ function buildGuardZsh(nodePath) {
|
|
|
169
154
|
``,
|
|
170
155
|
].join('\n');
|
|
171
156
|
}
|
|
172
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* bash guard — sourced ONLY from .bashrc/.bash_profile (pure bash syntax).
|
|
159
|
+
*
|
|
160
|
+
* Command-agnostic whole-line hook via a DEBUG trap (bash-preexec style). The
|
|
161
|
+
* trap fires before each command; it reads the FULL interactive line the user
|
|
162
|
+
* typed from `history` (so pipelines like `curl … | sh` and chained lines like
|
|
163
|
+
* `foo && rm -rf /` are evaluated as one string, exactly like zsh's accept-line
|
|
164
|
+
* BUFFER — not per-simple-command). It de-duplicates so the checker runs once
|
|
165
|
+
* per typed line. With `extdebug` enabled, returning 1 from the DEBUG trap
|
|
166
|
+
* SKIPS the command — so an explicit block (checker exit 97) prevents execution.
|
|
167
|
+
* A missing/erroring checker returns 0 → the command runs (fail open).
|
|
168
|
+
*/
|
|
173
169
|
function buildGuardBash(nodePath) {
|
|
174
|
-
const watch = ` ${WATCHLIST.join(' ')} `;
|
|
175
170
|
return [
|
|
176
171
|
`# FullCourtDefense interactive shell guard for bash (installed by fullcourtdefense-cli).`,
|
|
177
172
|
`# Disable for one session: export FCD_SHELL_GUARD=off`,
|
|
@@ -181,23 +176,41 @@ function buildGuardBash(nodePath) {
|
|
|
181
176
|
``,
|
|
182
177
|
`FCD_GUARD_NODE=${shSingleQuote(nodePath)}`,
|
|
183
178
|
`FCD_GUARD_JS=${shSingleQuote(GUARD_JS_PATH)}`,
|
|
184
|
-
`
|
|
179
|
+
`__fcd_last_line=""`,
|
|
185
180
|
``,
|
|
186
|
-
`#
|
|
187
|
-
|
|
188
|
-
`
|
|
189
|
-
`
|
|
190
|
-
`
|
|
191
|
-
`
|
|
181
|
+
`# Run the shared checker on one full line. Returns 1 to cancel (extdebug).`,
|
|
182
|
+
`__fcd_check_line() {`,
|
|
183
|
+
` local __fcd_line="$1"`,
|
|
184
|
+
` [ -z "$__fcd_line" ] && return 0`,
|
|
185
|
+
` [ "\${FCD_SHELL_GUARD:-}" = "off" ] && return 0`,
|
|
186
|
+
` if [ ! -x "$FCD_GUARD_NODE" ] || [ ! -f "$FCD_GUARD_JS" ]; then return 0; fi`,
|
|
187
|
+
` local __fcd_out __fcd_rc`,
|
|
188
|
+
` __fcd_out="$("$FCD_GUARD_NODE" "$FCD_GUARD_JS" -- "$__fcd_line" 2>&1)"`,
|
|
189
|
+
` __fcd_rc=$?`,
|
|
190
|
+
` [ -n "$__fcd_out" ] && printf '%s\\n' "$__fcd_out" >&2`,
|
|
191
|
+
` [ "$__fcd_rc" -eq 97 ] && return 1`,
|
|
192
|
+
` return 0`,
|
|
193
|
+
`}`,
|
|
194
|
+
``,
|
|
195
|
+
`# DEBUG trap: skip completions and the guard functions, then read the whole`,
|
|
196
|
+
`# typed line from history (number stripped) and check it once per line.`,
|
|
197
|
+
`__fcd_debug_trap() {`,
|
|
198
|
+
` [ -n "\${COMP_LINE:-}" ] && return 0`,
|
|
199
|
+
` case "$BASH_COMMAND" in __fcd_*|__fcd_debug_trap) return 0 ;; esac`,
|
|
200
|
+
` local __fcd_hist __fcd_num __fcd_line HISTTIMEFORMAT=`,
|
|
201
|
+
` __fcd_hist=$(builtin history 1 2>/dev/null)`,
|
|
202
|
+
` read -r __fcd_num __fcd_line <<< "$__fcd_hist"`,
|
|
203
|
+
` [ -z "$__fcd_line" ] && return 0`,
|
|
204
|
+
` if [ "$__fcd_line" != "$__fcd_last_line" ]; then`,
|
|
205
|
+
` __fcd_last_line="$__fcd_line"`,
|
|
206
|
+
` __fcd_check_line "$__fcd_line" || return 1`,
|
|
192
207
|
` fi`,
|
|
193
|
-
`
|
|
194
|
-
` if [ "$?" -eq 97 ]; then return 1; fi`,
|
|
195
|
-
` command "$__fcd_cmd" "$@"`,
|
|
208
|
+
` return 0`,
|
|
196
209
|
`}`,
|
|
197
|
-
`
|
|
198
|
-
`
|
|
199
|
-
`
|
|
200
|
-
`
|
|
210
|
+
`if [ "\${FCD_SHELL_GUARD:-}" != "off" ]; then`,
|
|
211
|
+
` shopt -s extdebug 2>/dev/null || true`,
|
|
212
|
+
` trap '__fcd_debug_trap' DEBUG`,
|
|
213
|
+
`fi`,
|
|
201
214
|
``,
|
|
202
215
|
].join('\n');
|
|
203
216
|
}
|
|
@@ -16,6 +16,23 @@ interface ShellGuardRulesFile {
|
|
|
16
16
|
}
|
|
17
17
|
/** Write (or rewrite) the ruleset the profile guard loads at shell startup. */
|
|
18
18
|
export declare function writeShellGuardRules(): ShellGuardRulesFile;
|
|
19
|
+
/** The active ruleset in memory (builtins minus dashboard-disabled + org custom). */
|
|
20
|
+
export declare function activeShellGuardRules(): ShellGuardRule[];
|
|
21
|
+
export interface ShellGuardMatch {
|
|
22
|
+
rule: ShellGuardRule;
|
|
23
|
+
/** The command segment that matched (chained commands are split on ; & |). */
|
|
24
|
+
segment: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Evaluate a typed command line WITHOUT executing it — the dry-run classifier
|
|
28
|
+
* behind `shell-guard-check`. Uses the exact same rules + JS regex engine as the
|
|
29
|
+
* live cmd/posix checkers (equivalent to .NET for these patterns). Also splits
|
|
30
|
+
* chained commands (`a && del /s c:\`) and tests each segment, so combinations
|
|
31
|
+
* are caught even when the dangerous part is not first.
|
|
32
|
+
*
|
|
33
|
+
* Returns ALL matches (one per matched segment/rule), first-match-per-segment.
|
|
34
|
+
*/
|
|
35
|
+
export declare function evaluateShellCommand(line: string, rules?: ShellGuardRule[]): ShellGuardMatch[];
|
|
19
36
|
/**
|
|
20
37
|
* Opportunistic rules/mode refresh — called from the telemetry flush cycle so a
|
|
21
38
|
* console mode flip or new custom rule reaches interactive shells within ~30s.
|
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.writeShellGuardRules = writeShellGuardRules;
|
|
37
|
+
exports.activeShellGuardRules = activeShellGuardRules;
|
|
38
|
+
exports.evaluateShellCommand = evaluateShellCommand;
|
|
37
39
|
exports.refreshShellGuardRules = refreshShellGuardRules;
|
|
38
40
|
exports.getShellGuardStatus = getShellGuardStatus;
|
|
39
41
|
exports.isShellGuardInstalled = isShellGuardInstalled;
|
|
@@ -134,6 +136,12 @@ const BUILTIN_RULES = [
|
|
|
134
136
|
pattern: String.raw `:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:` },
|
|
135
137
|
{ id: 'vssadmin_delete_shadows', category: 'destructive_command', severity: 'critical', reason: 'shadow copy deletion (ransomware pattern)', source: 'builtin',
|
|
136
138
|
pattern: String.raw `\bvssadmin\b(?=.*\bdelete\b)(?=.*\bshadows\b)` },
|
|
139
|
+
{ id: 'wbadmin_delete_backup', category: 'destructive_command', severity: 'critical', reason: 'Windows backup catalog deletion (ransomware pattern)', source: 'builtin',
|
|
140
|
+
pattern: String.raw `\bwbadmin\b(?=.*\bdelete\b)(?=.*\b(?:catalog|systemstatebackup|backup)\b)` },
|
|
141
|
+
{ id: 'bcdedit_recovery_disable', category: 'security_tampering', severity: 'critical', reason: 'disabling Windows recovery/boot repair (ransomware pattern)', source: 'builtin',
|
|
142
|
+
pattern: String.raw `\bbcdedit\b(?=.*(?:\brecoveryenabled\b\s+no\b|\bbootstatuspolicy\b\s+ignoreallfailures\b))` },
|
|
143
|
+
{ id: 'cipher_wipe_disk', category: 'destructive_command', severity: 'high', reason: 'wiping deleted data from a drive (cipher /w)', source: 'builtin',
|
|
144
|
+
pattern: String.raw `\bcipher(?:\.exe)?\b(?=.*\s/w(?::|\b))` },
|
|
137
145
|
{ id: 'defender_disable', category: 'security_tampering', severity: 'high', reason: 'disabling Windows Defender real-time protection', source: 'builtin',
|
|
138
146
|
pattern: String.raw `\bset-mppreference\b(?=.*-disablerealtimemonitoring)(?=.*(?:\$true|\s1\b))` },
|
|
139
147
|
// Reverse shells / droppers
|
|
@@ -330,6 +338,52 @@ function writeShellGuardRules() {
|
|
|
330
338
|
fs.writeFileSync(GUARD_RULES_PATH, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
331
339
|
return data;
|
|
332
340
|
}
|
|
341
|
+
/** The active ruleset in memory (builtins minus dashboard-disabled + org custom). */
|
|
342
|
+
function activeShellGuardRules() {
|
|
343
|
+
const disabled = cachedDisabledItemIds();
|
|
344
|
+
return [...BUILTIN_RULES.filter(rule => !disabled.has(rule.id)), ...cachedCustomRules()];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Evaluate a typed command line WITHOUT executing it — the dry-run classifier
|
|
348
|
+
* behind `shell-guard-check`. Uses the exact same rules + JS regex engine as the
|
|
349
|
+
* live cmd/posix checkers (equivalent to .NET for these patterns). Also splits
|
|
350
|
+
* chained commands (`a && del /s c:\`) and tests each segment, so combinations
|
|
351
|
+
* are caught even when the dangerous part is not first.
|
|
352
|
+
*
|
|
353
|
+
* Returns ALL matches (one per matched segment/rule), first-match-per-segment.
|
|
354
|
+
*/
|
|
355
|
+
function evaluateShellCommand(line, rules = activeShellGuardRules()) {
|
|
356
|
+
const compiled = rules.map(r => {
|
|
357
|
+
try {
|
|
358
|
+
return { rule: r, re: new RegExp(r.pattern, 'i') };
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}).filter((x) => !!x);
|
|
364
|
+
// Whole line first (some rules intentionally span a pipe, e.g. curl | sh),
|
|
365
|
+
// then each &&/||/;/newline segment (catches "cleanup && del /s c:\").
|
|
366
|
+
const candidates = [line, ...line.split(/&&|\|\||[;&\n]|\|/).map(s => s.trim()).filter(Boolean)];
|
|
367
|
+
const seen = new Set();
|
|
368
|
+
const matches = [];
|
|
369
|
+
for (const candidate of candidates) {
|
|
370
|
+
const norm = candidate.replace(/\s+/g, ' ').trim();
|
|
371
|
+
for (const { rule, re } of compiled) {
|
|
372
|
+
try {
|
|
373
|
+
if (re.test(norm) || re.test(candidate)) {
|
|
374
|
+
const key = `${rule.id}|${candidate}`;
|
|
375
|
+
if (seen.has(key))
|
|
376
|
+
continue;
|
|
377
|
+
seen.add(key);
|
|
378
|
+
matches.push({ rule, segment: candidate });
|
|
379
|
+
break; // first rule per segment is enough
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch { /* skip bad regex */ }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return matches;
|
|
386
|
+
}
|
|
333
387
|
/**
|
|
334
388
|
* Opportunistic rules/mode refresh — called from the telemetry flush cycle so a
|
|
335
389
|
* console mode flip or new custom rule reaches interactive shells within ~30s.
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,7 @@ function readCliVersion() {
|
|
|
69
69
|
const VERSION = readCliVersion();
|
|
70
70
|
function parseArgs(argv) {
|
|
71
71
|
const flags = {};
|
|
72
|
+
const positional = [];
|
|
72
73
|
let command = '';
|
|
73
74
|
for (let i = 0; i < argv.length; i++) {
|
|
74
75
|
const arg = argv[i];
|
|
@@ -76,6 +77,10 @@ function parseArgs(argv) {
|
|
|
76
77
|
command = arg;
|
|
77
78
|
continue;
|
|
78
79
|
}
|
|
80
|
+
if (command && !arg.startsWith('-')) {
|
|
81
|
+
positional.push(arg);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
79
84
|
if (arg.startsWith('--')) {
|
|
80
85
|
const key = arg.slice(2);
|
|
81
86
|
const eqIdx = key.indexOf('=');
|
|
@@ -105,7 +110,7 @@ function parseArgs(argv) {
|
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
|
-
return { command, flags };
|
|
113
|
+
return { command, flags, positional };
|
|
109
114
|
}
|
|
110
115
|
function printHelp() {
|
|
111
116
|
console.log(`
|
|
@@ -134,6 +139,9 @@ function printHelp() {
|
|
|
134
139
|
uninstall-shell-guard removes it; shell-guard-status shows coverage.
|
|
135
140
|
install-cmd-guard Block dangerous commands typed in cmd.exe (Command Prompt).
|
|
136
141
|
uninstall-cmd-guard removes it; cmd-guard-status shows coverage.
|
|
142
|
+
shell-guard-check Dry-run: test whether a command (or a file/stdin list of
|
|
143
|
+
commands) would be blocked — nothing is executed. Splits chained
|
|
144
|
+
commands (a && del /s c:\). --file <path>, --json; exits 1 if any block.
|
|
137
145
|
scan Runs the scan. Use --local for inside-organization scans.
|
|
138
146
|
discover Finds MCP servers, local secrets, agent rules/skills, and machine
|
|
139
147
|
blast radius on this laptop. Use --surface all --upload for AI Fleet.
|
|
@@ -350,7 +358,7 @@ function printHelp() {
|
|
|
350
358
|
`);
|
|
351
359
|
}
|
|
352
360
|
async function main() {
|
|
353
|
-
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
361
|
+
const { command, flags, positional } = parseArgs(process.argv.slice(2));
|
|
354
362
|
const buildInstallArgs = () => ({
|
|
355
363
|
clients: flags.clients,
|
|
356
364
|
cursorProject: flags['cursor-project'],
|
|
@@ -656,6 +664,62 @@ async function main() {
|
|
|
656
664
|
console.log('');
|
|
657
665
|
break;
|
|
658
666
|
}
|
|
667
|
+
case 'shell-guard-check': {
|
|
668
|
+
// Dry-run classifier — evaluate command(s) against the guard rules WITHOUT
|
|
669
|
+
// executing anything. Positional args are one command; --file reads one
|
|
670
|
+
// command per line; no args reads stdin. Exit 1 if ANY command is blocked
|
|
671
|
+
// (so it can gate CI). --json for machine output.
|
|
672
|
+
const asJson = flags.json === 'true' || flags.json === '';
|
|
673
|
+
const lines = [];
|
|
674
|
+
if (flags.file) {
|
|
675
|
+
try {
|
|
676
|
+
lines.push(...fs.readFileSync(flags.file, 'utf8').split(/\r?\n/));
|
|
677
|
+
}
|
|
678
|
+
catch (e) {
|
|
679
|
+
console.error(`Cannot read --file: ${e instanceof Error ? e.message : String(e)}`);
|
|
680
|
+
process.exit(2);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else if (positional.length > 0) {
|
|
684
|
+
lines.push(positional.join(' '));
|
|
685
|
+
}
|
|
686
|
+
else if (!process.stdin.isTTY) {
|
|
687
|
+
lines.push(...fs.readFileSync(0, 'utf8').split(/\r?\n/));
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
console.log('Usage: fullcourtdefense shell-guard-check "<command>" | --file cmds.txt | echo "<cmd>" | fullcourtdefense shell-guard-check');
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
const cmds = lines.map(l => l.trim()).filter(Boolean);
|
|
694
|
+
const rules = (0, shellGuard_1.activeShellGuardRules)();
|
|
695
|
+
let blocked = 0;
|
|
696
|
+
const results = cmds.map(cmd => {
|
|
697
|
+
const matches = (0, shellGuard_1.evaluateShellCommand)(cmd, rules);
|
|
698
|
+
if (matches.length)
|
|
699
|
+
blocked++;
|
|
700
|
+
return { command: cmd, blocked: matches.length > 0, matches: matches.map(m => ({ ruleId: m.rule.id, severity: m.rule.severity, category: m.rule.category, reason: m.rule.reason, segment: m.segment })) };
|
|
701
|
+
});
|
|
702
|
+
if (asJson) {
|
|
703
|
+
console.log(JSON.stringify({ total: cmds.length, blocked, ruleCount: rules.length, results }, null, 2));
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
console.log(`\nEvaluated ${cmds.length} command(s) against ${rules.length} rules (dry-run — nothing executed):\n`);
|
|
707
|
+
for (const r of results) {
|
|
708
|
+
if (r.blocked) {
|
|
709
|
+
const m = r.matches[0];
|
|
710
|
+
console.log(` \x1b[31mBLOCK\x1b[0m [\x1b[1m${m.severity}\x1b[0m] ${r.command}`);
|
|
711
|
+
console.log(` \x1b[2m${m.reason} (rule ${m.ruleId})${r.matches.length > 1 ? ` +${r.matches.length - 1} more` : ''}\x1b[0m`);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
console.log(` \x1b[32mALLOW\x1b[0m ${r.command}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
console.log(`\n${blocked} blocked, ${cmds.length - blocked} allowed.`);
|
|
718
|
+
}
|
|
719
|
+
if (blocked > 0)
|
|
720
|
+
process.exit(1);
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
659
723
|
case 'install-cmd-guard': {
|
|
660
724
|
await (0, cmdGuard_1.installCmdGuardCommand)();
|
|
661
725
|
break;
|
package/dist/version.json
CHANGED