fullcourtdefense-cli 1.11.0 → 1.13.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.
@@ -95,15 +95,15 @@ function buildGuardJs(nodePath, cliEntry) {
95
95
  `const SPOOL_PATH=${JSON.stringify(path.join(os.homedir(), '.fullcourtdefense-spool.jsonl'))};`,
96
96
  `const NODE_PATH=${JSON.stringify(nodePath)};`,
97
97
  `const CLI_ENTRY=${JSON.stringify(cliEntry)};`,
98
- `function loadRules(){try{const raw=JSON.parse(fs.readFileSync(RULES_PATH,'utf8'));const rules=(raw.rules||[]).map(r=>{try{return{id:r.id,category:r.category,reason:r.reason,source:r.source,re:new RegExp(r.pattern,'i')};}catch{return null;}}).filter(Boolean);return{mode:raw.mode==='monitor'?'monitor':'block',rules};}catch{return{mode:'block',rules:[]};}}`,
98
+ `function loadRules(){try{const raw=JSON.parse(fs.readFileSync(RULES_PATH,'utf8'));const rules=(raw.rules||[]).map(r=>{try{return{id:r.id,category:r.category,severity:r.severity,reason:r.reason,source:r.source,re:new RegExp(r.pattern,'i')};}catch{return null;}}).filter(Boolean);return{mode:raw.mode==='monitor'?'monitor':'block',rules};}catch{return{mode:'block',rules:[]};}}`,
99
99
  `function matchLine(line,rules){const n=line.replace(/\\s+/g,' ').trim();for(const r of rules){try{if(r.re.test(n)||r.re.test(line))return r;}catch{}}return null;}`,
100
- `function spoolEvent(rule,line,decision){try{const ev=line.trim();const evidence=ev.length>180?ev.slice(0,180)+'...':ev;const event={eventId:crypto.randomUUID(),type:'verdict',decision,toolName:'cmd_terminal',operation:'shell_command',reason:'Shell guard: '+rule.reason,ruleId:rule.id,category:rule.category,source:rule.source,evidence,occurredAt:new Date().toISOString()};fs.appendFileSync(SPOOL_PATH,JSON.stringify(event)+'\\n',{encoding:'utf8',mode:0o600});if(fs.existsSync(NODE_PATH)&&fs.existsSync(CLI_ENTRY)){spawnSync(NODE_PATH,[CLI_ENTRY,'flush-spool','--heartbeat','true'],{stdio:'ignore',windowsHide:true});}}catch{}}`,
100
+ `function spoolEvent(rule,line,decision){try{const ev=line.trim();const evidence=ev.length>180?ev.slice(0,180)+'...':ev;const event={eventId:crypto.randomUUID(),type:'verdict',decision,toolName:'cmd_terminal',operation:'shell_command',reason:'Shell guard: '+rule.reason,ruleId:rule.id,category:rule.category,severity:rule.severity,source:rule.source,evidence,occurredAt:new Date().toISOString()};fs.appendFileSync(SPOOL_PATH,JSON.stringify(event)+'\\n',{encoding:'utf8',mode:0o600});if(fs.existsSync(NODE_PATH)&&fs.existsSync(CLI_ENTRY)){spawnSync(NODE_PATH,[CLI_ENTRY,'flush-spool','--heartbeat','true'],{stdio:'ignore',windowsHide:true});}}catch{}}`,
101
101
  `function delegate(args){const r=spawnSync(process.env.ComSpec||'cmd.exe',['/d','/c',...args],{stdio:'inherit',windowsHide:true});process.exit(typeof r.status==='number'?r.status:1);}`,
102
102
  `const args=process.argv.slice(2);if(!args.length)process.exit(0);if(process.env.FCD_CMD_GUARD==='off')delegate(args);`,
103
103
  `const line=args.join(' ');const{mode,rules}=loadRules();const hit=matchLine(line,rules);`,
104
104
  `if(!hit)delegate(args);`,
105
- `if(mode==='monitor'){console.error('[FullCourtDefense] monitor: would block - '+hit.reason+' (rule '+hit.id+')');spoolEvent(hit,line,'allow');delegate(args);}`,
106
- `console.error('[FullCourtDefense] BLOCKED: '+hit.reason+' (rule '+hit.id+')');`,
105
+ `if(mode==='monitor'){console.error('[FullCourtDefense] monitor: would block ['+(hit.severity||'')+'] '+hit.reason+' (rule '+hit.id+')');spoolEvent(hit,line,'allow');delegate(args);}`,
106
+ `console.error('[FullCourtDefense] BLOCKED ['+(hit.severity||'')+']: '+hit.reason+' (rule '+hit.id+')');`,
107
107
  `console.error('This command was not executed. Reported to your security dashboard.');`,
108
108
  `spoolEvent(hit,line,'block');process.exit(1);`,
109
109
  ``,
@@ -13,6 +13,7 @@ export interface InstallAllArgs extends ProtectAllArgs {
13
13
  windowsAudit?: string;
14
14
  shellGuard?: string;
15
15
  cmdGuard?: string;
16
+ posixGuard?: string;
16
17
  }
17
18
  /**
18
19
  * One-click install: discover/wrap every existing MCP server, install Cursor
@@ -12,6 +12,7 @@ const restartNotice_1 = require("./restartNotice");
12
12
  const windowsAudit_1 = require("./windowsAudit");
13
13
  const shellGuard_1 = require("./shellGuard");
14
14
  const cmdGuard_1 = require("./cmdGuard");
15
+ const posixShellGuard_1 = require("./posixShellGuard");
15
16
  /**
16
17
  * One-click install: discover/wrap every existing MCP server, install Cursor
17
18
  * hooks for built-in Cursor actions, optional self-heal and fleet upload.
@@ -117,6 +118,18 @@ async function installAllCommand(args, config) {
117
118
  console.log(`\x1b[33m⚠ Warning: cmd guard install failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
118
119
  }
119
120
  }
121
+ // Interactive macOS/Linux guard — same rules, zsh accept-line widget + bash
122
+ // function wrappers, from the local cache (no admin, no network hot path).
123
+ // Gated to darwin/linux so Windows install-all behavior is unchanged.
124
+ if ((process.platform === 'darwin' || process.platform === 'linux') && args.posixGuard !== 'false') {
125
+ console.log('\n\x1b[1mInstalling interactive shell guard (bash + zsh)…\x1b[0m');
126
+ try {
127
+ await (0, posixShellGuard_1.installPosixShellGuardCommand)();
128
+ }
129
+ catch (error) {
130
+ console.log(`\x1b[33m⚠ Warning: shell guard install failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
131
+ }
132
+ }
120
133
  if (args.autoProtect === 'true') {
121
134
  console.log('\n\x1b[1mScheduling self-healing MCP protection…\x1b[0m');
122
135
  await (0, autoProtect_1.autoProtectCommand)({
@@ -0,0 +1,26 @@
1
+ export interface PosixShellGuardStatus {
2
+ supported: boolean;
3
+ installed: boolean;
4
+ rcFiles: Array<{
5
+ shell: string;
6
+ rcPath: string;
7
+ installed: boolean;
8
+ }>;
9
+ rulesPresent: boolean;
10
+ ruleCount?: number;
11
+ mode?: string;
12
+ }
13
+ /** Read-only probe. Never throws. */
14
+ export declare function getPosixShellGuardStatus(): PosixShellGuardStatus;
15
+ /** Lightweight boolean for the telemetry heartbeat / discovery report. */
16
+ export declare function isPosixShellGuardInstalled(): boolean;
17
+ export interface InstallPosixShellGuardArgs {
18
+ /** 'false' to write the guard scripts + rules without wiring rc files. */
19
+ profiles?: string;
20
+ }
21
+ /** `fullcourtdefense install-shell-guard` on macOS/Linux — enable the bash/zsh guard. */
22
+ export declare function installPosixShellGuardCommand(args?: InstallPosixShellGuardArgs): Promise<void>;
23
+ /** `fullcourtdefense uninstall-shell-guard` on macOS/Linux — remove the guard. */
24
+ export declare function uninstallPosixShellGuardCommand(): Promise<void>;
25
+ /** Refresh the shared rules JSON when the POSIX guard is installed. Never throws. */
26
+ export declare function refreshPosixShellGuardRules(): void;
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getPosixShellGuardStatus = getPosixShellGuardStatus;
37
+ exports.isPosixShellGuardInstalled = isPosixShellGuardInstalled;
38
+ exports.installPosixShellGuardCommand = installPosixShellGuardCommand;
39
+ exports.uninstallPosixShellGuardCommand = uninstallPosixShellGuardCommand;
40
+ exports.refreshPosixShellGuardRules = refreshPosixShellGuardRules;
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const shellGuard_1 = require("./shellGuard");
45
+ /**
46
+ * Interactive macOS/Linux shell guard — real-time blocking of dangerous commands
47
+ * TYPED by a human into bash or zsh (inside or outside the IDE).
48
+ *
49
+ * Shares the exact ruleset the PowerShell/cmd guards use (writeShellGuardRules):
50
+ * the deterministic dangerous-command families + the org's cached custom blocks +
51
+ * the Shield mode (block vs monitor). One dashboard toggle disables a rule
52
+ * everywhere.
53
+ *
54
+ * Two hook mechanisms, one shared Node checker (~/.fullcourtdefense-posix-guard.js).
55
+ * Each shell gets its OWN sourced file (never cross-parsed — zsh-only expansions
56
+ * would otherwise break bash at source time):
57
+ * - zsh (macOS default), ~/.fullcourtdefense-shell-guard.zsh: a ZLE
58
+ * `accept-line` widget inspects the WHOLE typed line before it runs — the
59
+ * strongest form (catches pipes, e.g. `curl … | sh`). Block mode → the line
60
+ * is not accepted (never executes); monitor → warning + the line runs.
61
+ * Analogous to the PowerShell PSReadLine hook.
62
+ * - bash, ~/.fullcourtdefense-shell-guard.bash: function wrappers for a
63
+ * watchlist of high-risk command names (rm, dd, chmod, sudo, curl, kubectl…) —
64
+ * the same model as the cmd.exe doskey guard. Per-command (does not see the
65
+ * full pipeline), so pipe-to-shell is only fully covered under zsh; the
66
+ * flagship cases (rm -rf /, sudo rm, dd, infra destroy…) are covered under both.
67
+ *
68
+ * Safety: every layer fails OPEN. If the checker is missing or errors, the command
69
+ * runs — a guard bug must never brick a developer's shell. Only an explicit block
70
+ * (checker exit code 97) stops a command. Disable for a session with
71
+ * `export FCD_SHELL_GUARD=off`.
72
+ *
73
+ * Rules/mode refresh opportunistically on every telemetry flush (offline caches
74
+ * only — never blocks a shell). Blocks are spooled and reported to the fleet
75
+ * dashboard as `shell_terminal` events.
76
+ */
77
+ const GUARD_RULES_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.json');
78
+ const GUARD_JS_PATH = path.join(os.homedir(), '.fullcourtdefense-posix-guard.js');
79
+ const GUARD_ZSH_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.zsh');
80
+ const GUARD_BASH_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.bash');
81
+ const SPOOL_PATH = path.join(os.homedir(), '.fullcourtdefense-spool.jsonl');
82
+ const MARKER_START = '# >>> fullcourtdefense shell guard >>>';
83
+ const MARKER_END = '# <<< fullcourtdefense shell guard <<<';
84
+ 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
+ function isPosixPlatform() {
99
+ return process.platform === 'darwin' || process.platform === 'linux';
100
+ }
101
+ function shSingleQuote(value) {
102
+ return `'${value.replace(/'/g, `'\\''`)}'`;
103
+ }
104
+ /** The shared Node checker: exits 97 on a confirmed block, 0 otherwise (fail-open). */
105
+ function buildGuardJs(nodePath, cliEntry) {
106
+ return [
107
+ `'use strict';`,
108
+ `const fs=require('fs');const crypto=require('crypto');const{spawn}=require('child_process');`,
109
+ `const RULES_PATH=${JSON.stringify(GUARD_RULES_PATH)};`,
110
+ `const SPOOL_PATH=${JSON.stringify(SPOOL_PATH)};`,
111
+ `const NODE_PATH=${JSON.stringify(nodePath)};`,
112
+ `const CLI_ENTRY=${JSON.stringify(cliEntry)};`,
113
+ `const BLOCK_CODE=${BLOCK_CODE};`,
114
+ `function loadRules(){try{const raw=JSON.parse(fs.readFileSync(RULES_PATH,'utf8'));const rules=(raw.rules||[]).map(r=>{try{return{id:r.id,category:r.category,severity:r.severity,reason:r.reason,source:r.source,re:new RegExp(r.pattern,'i')};}catch{return null;}}).filter(Boolean);return{mode:raw.mode==='monitor'?'monitor':'block',rules};}catch{return{mode:'block',rules:[]};}}`,
115
+ `function matchLine(line,rules){const n=line.replace(/\\s+/g,' ').trim();for(const r of rules){try{if(r.re.test(n)||r.re.test(line))return r;}catch{}}return null;}`,
116
+ `function triggerFlush(){try{if(fs.existsSync(NODE_PATH)&&fs.existsSync(CLI_ENTRY)){const c=spawn(NODE_PATH,[CLI_ENTRY,'flush-spool','--heartbeat','true'],{detached:true,stdio:'ignore'});c.unref();}}catch{}}`,
117
+ `function spoolEvent(rule,line,decision){try{const ev=line.trim();const evidence=ev.length>180?ev.slice(0,180)+'...':ev;const event={eventId:crypto.randomUUID(),type:'verdict',decision,toolName:'shell_terminal',operation:'shell_command',reason:'Shell guard: '+rule.reason,ruleId:rule.id,category:rule.category,severity:rule.severity,source:rule.source,evidence,occurredAt:new Date().toISOString()};fs.appendFileSync(SPOOL_PATH,JSON.stringify(event)+'\\n',{encoding:'utf8',mode:0o600});triggerFlush();}catch{}}`,
118
+ `let argv=process.argv.slice(2);if(argv[0]==='--')argv=argv.slice(1);const line=argv.join(' ');`,
119
+ `if(!line.trim())process.exit(0);`,
120
+ `if(process.env.FCD_SHELL_GUARD==='off')process.exit(0);`,
121
+ `const{mode,rules}=loadRules();const hit=matchLine(line,rules);`,
122
+ `if(!hit)process.exit(0);`,
123
+ `if(mode==='monitor'){console.error('[FullCourtDefense] monitor: would block ['+(hit.severity||'')+'] '+hit.reason+' (rule '+hit.id+')');spoolEvent(hit,line,'allow');process.exit(0);}`,
124
+ `console.error('[FullCourtDefense] BLOCKED ['+(hit.severity||'')+']: '+hit.reason+' (rule '+hit.id+')');`,
125
+ `console.error('This command was not executed. Reported to your security dashboard.');`,
126
+ `spoolEvent(hit,line,'block');process.exit(BLOCK_CODE);`,
127
+ ``,
128
+ ].join('\n');
129
+ }
130
+ /** zsh guard — sourced ONLY from .zshrc (contains zsh-specific ZLE syntax). */
131
+ function buildGuardZsh(nodePath) {
132
+ const watch = ` ${WATCHLIST.join(' ')} `;
133
+ return [
134
+ `# FullCourtDefense interactive shell guard for zsh (installed by fullcourtdefense-cli).`,
135
+ `# Disable for one session: export FCD_SHELL_GUARD=off`,
136
+ `# Remove permanently: fullcourtdefense uninstall-shell-guard`,
137
+ ``,
138
+ `case $- in *i*) ;; *) return 0 2>/dev/null || true ;; esac`,
139
+ ``,
140
+ `FCD_GUARD_NODE=${shSingleQuote(nodePath)}`,
141
+ `FCD_GUARD_JS=${shSingleQuote(GUARD_JS_PATH)}`,
142
+ `__fcd_watch=${shSingleQuote(watch)}`,
143
+ ``,
144
+ `# Inspect the WHOLE typed line before accepting it (catches pipes too).`,
145
+ `__fcd_zle_accept_line() {`,
146
+ ` emulate -L zsh`,
147
+ ` local __fcd_line=$BUFFER`,
148
+ ` if [[ -n "$__fcd_line" && "\${FCD_SHELL_GUARD:-}" != "off" ]]; then`,
149
+ ` local __fcd_first="\${\${(z)__fcd_line}[1]:t}"`,
150
+ ` if [[ "$__fcd_first" == "sudo" ]]; then`,
151
+ ` __fcd_first="\${\${(z)__fcd_line}[2]:t}"`,
152
+ ` fi`,
153
+ ` if [[ -x "$FCD_GUARD_NODE" && -f "$FCD_GUARD_JS" && "$__fcd_watch" == *" $__fcd_first "* ]]; then`,
154
+ ` local __fcd_out`,
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"`,
162
+ ` fi`,
163
+ ` fi`,
164
+ ` zle .accept-line`,
165
+ `}`,
166
+ `if [[ "\${FCD_SHELL_GUARD:-}" != "off" ]] && zle -l >/dev/null 2>&1; then`,
167
+ ` zle -N accept-line __fcd_zle_accept_line`,
168
+ `fi`,
169
+ ``,
170
+ ].join('\n');
171
+ }
172
+ /** bash guard — sourced ONLY from .bashrc/.bash_profile (pure bash/POSIX syntax). */
173
+ function buildGuardBash(nodePath) {
174
+ const watch = ` ${WATCHLIST.join(' ')} `;
175
+ return [
176
+ `# FullCourtDefense interactive shell guard for bash (installed by fullcourtdefense-cli).`,
177
+ `# Disable for one session: export FCD_SHELL_GUARD=off`,
178
+ `# Remove permanently: fullcourtdefense uninstall-shell-guard`,
179
+ ``,
180
+ `case $- in *i*) ;; *) return 0 2>/dev/null || true ;; esac`,
181
+ ``,
182
+ `FCD_GUARD_NODE=${shSingleQuote(nodePath)}`,
183
+ `FCD_GUARD_JS=${shSingleQuote(GUARD_JS_PATH)}`,
184
+ `__fcd_watch=${shSingleQuote(watch)}`,
185
+ ``,
186
+ `# Wrap a high-risk command. Blocks only on an explicit checker verdict (97);`,
187
+ `# a missing/erroring checker runs the command (fail open).`,
188
+ `__fcd_wrap() {`,
189
+ ` local __fcd_cmd="$1"; shift`,
190
+ ` if [ "\${FCD_SHELL_GUARD:-}" = "off" ] || [ ! -x "$FCD_GUARD_NODE" ] || [ ! -f "$FCD_GUARD_JS" ]; then`,
191
+ ` command "$__fcd_cmd" "$@"; return $?`,
192
+ ` fi`,
193
+ ` "$FCD_GUARD_NODE" "$FCD_GUARD_JS" -- "$__fcd_cmd $*"`,
194
+ ` if [ "$?" -eq 97 ]; then return 1; fi`,
195
+ ` command "$__fcd_cmd" "$@"`,
196
+ `}`,
197
+ `for __fcd_c in $__fcd_watch; do`,
198
+ ` eval "\${__fcd_c}() { __fcd_wrap \${__fcd_c} \\"\\$@\\"; }"`,
199
+ `done`,
200
+ `unset __fcd_c`,
201
+ ``,
202
+ ].join('\n');
203
+ }
204
+ /** rc files we wire the guard into. .bash_profile only when it already exists. */
205
+ function resolveRcTargets() {
206
+ const home = os.homedir();
207
+ const out = [
208
+ { shell: 'zsh', rcPath: path.join(home, '.zshrc'), guardPath: GUARD_ZSH_PATH },
209
+ { shell: 'bash', rcPath: path.join(home, '.bashrc'), guardPath: GUARD_BASH_PATH },
210
+ ];
211
+ const bashProfile = path.join(home, '.bash_profile');
212
+ if (fs.existsSync(bashProfile))
213
+ out.push({ shell: 'bash', rcPath: bashProfile, guardPath: GUARD_BASH_PATH });
214
+ return out;
215
+ }
216
+ function rcSnippet(guardPath) {
217
+ return [
218
+ MARKER_START,
219
+ `# Loads the FullCourtDefense dangerous-command guard for interactive sessions.`,
220
+ `[ -f ${shSingleQuote(guardPath)} ] && . ${shSingleQuote(guardPath)}`,
221
+ MARKER_END,
222
+ ].join('\n');
223
+ }
224
+ function addSnippetToRc(target) {
225
+ fs.mkdirSync(path.dirname(target.rcPath), { recursive: true });
226
+ let content = '';
227
+ try {
228
+ content = fs.readFileSync(target.rcPath, 'utf8');
229
+ }
230
+ catch { /* new rc */ }
231
+ if (content.includes(MARKER_START))
232
+ return 'already';
233
+ const sep = content && !content.endsWith('\n') ? '\n\n' : content ? '\n' : '';
234
+ fs.writeFileSync(target.rcPath, content + sep + rcSnippet(target.guardPath) + '\n', 'utf8');
235
+ return 'installed';
236
+ }
237
+ function removeSnippetFromRc(rcPath) {
238
+ try {
239
+ const content = fs.readFileSync(rcPath, 'utf8');
240
+ const start = content.indexOf(MARKER_START);
241
+ const end = content.indexOf(MARKER_END);
242
+ if (start === -1 || end === -1)
243
+ return false;
244
+ const cleaned = (content.slice(0, start) + content.slice(end + MARKER_END.length)).replace(/\n{3,}/g, '\n\n');
245
+ fs.writeFileSync(rcPath, cleaned, 'utf8');
246
+ return true;
247
+ }
248
+ catch {
249
+ return false;
250
+ }
251
+ }
252
+ /** Read-only probe. Never throws. */
253
+ function getPosixShellGuardStatus() {
254
+ if (!isPosixPlatform()) {
255
+ return { supported: false, installed: false, rcFiles: [], rulesPresent: false };
256
+ }
257
+ try {
258
+ const rcFiles = resolveRcTargets().map(entry => {
259
+ let installed = false;
260
+ try {
261
+ installed = fs.readFileSync(entry.rcPath, 'utf8').includes(MARKER_START);
262
+ }
263
+ catch { /* no rc */ }
264
+ return { shell: entry.shell, rcPath: entry.rcPath, installed };
265
+ });
266
+ let ruleCount;
267
+ let mode;
268
+ let rulesPresent = false;
269
+ try {
270
+ const parsed = JSON.parse(fs.readFileSync(GUARD_RULES_PATH, 'utf8'));
271
+ rulesPresent = Array.isArray(parsed?.rules) && parsed.rules.length > 0;
272
+ ruleCount = Array.isArray(parsed?.rules) ? parsed.rules.length : undefined;
273
+ mode = typeof parsed?.mode === 'string' ? parsed.mode : undefined;
274
+ }
275
+ catch { /* not written yet */ }
276
+ return {
277
+ supported: true,
278
+ installed: rcFiles.some(r => r.installed) && (fs.existsSync(GUARD_ZSH_PATH) || fs.existsSync(GUARD_BASH_PATH)),
279
+ rcFiles,
280
+ rulesPresent,
281
+ ruleCount,
282
+ mode,
283
+ };
284
+ }
285
+ catch {
286
+ return { supported: true, installed: false, rcFiles: [], rulesPresent: false };
287
+ }
288
+ }
289
+ /** Lightweight boolean for the telemetry heartbeat / discovery report. */
290
+ function isPosixShellGuardInstalled() {
291
+ try {
292
+ if (!isPosixPlatform())
293
+ return false;
294
+ if (!fs.existsSync(GUARD_ZSH_PATH) && !fs.existsSync(GUARD_BASH_PATH))
295
+ return false;
296
+ for (const { rcPath } of resolveRcTargets()) {
297
+ try {
298
+ if (fs.readFileSync(rcPath, 'utf8').includes(MARKER_START))
299
+ return true;
300
+ }
301
+ catch { /* keep looking */ }
302
+ }
303
+ return false;
304
+ }
305
+ catch {
306
+ return false;
307
+ }
308
+ }
309
+ /** `fullcourtdefense install-shell-guard` on macOS/Linux — enable the bash/zsh guard. */
310
+ async function installPosixShellGuardCommand(args = {}) {
311
+ if (!isPosixPlatform()) {
312
+ console.log('The POSIX shell guard supports macOS and Linux (bash/zsh).');
313
+ return;
314
+ }
315
+ const rules = (0, shellGuard_1.writeShellGuardRules)();
316
+ const nodePath = process.execPath;
317
+ const cliEntry = process.argv[1] || '';
318
+ fs.writeFileSync(GUARD_JS_PATH, buildGuardJs(nodePath, cliEntry), { encoding: 'utf8', mode: 0o600 });
319
+ fs.writeFileSync(GUARD_ZSH_PATH, buildGuardZsh(nodePath), { encoding: 'utf8', mode: 0o600 });
320
+ fs.writeFileSync(GUARD_BASH_PATH, buildGuardBash(nodePath), { encoding: 'utf8', mode: 0o600 });
321
+ console.log(`Guard rules written: ${rules.rules.length} rule(s), mode "${rules.mode}".`);
322
+ if (args.profiles === 'false') {
323
+ console.log('Skipped rc wiring (--profiles false): guard scripts + rules written only.');
324
+ return;
325
+ }
326
+ let wired = 0;
327
+ for (const target of resolveRcTargets()) {
328
+ try {
329
+ const outcome = addSnippetToRc(target);
330
+ if (outcome === 'installed')
331
+ wired++;
332
+ console.log(outcome === 'installed'
333
+ ? `\x1b[32m✓ ${target.shell}: guard added\x1b[0m \x1b[2m(${target.rcPath})\x1b[0m`
334
+ : `\x1b[32m✓ ${target.shell}: guard already present\x1b[0m \x1b[2m(${target.rcPath})\x1b[0m`);
335
+ }
336
+ catch (error) {
337
+ console.log(`\x1b[33m⚠ ${target.shell}: could not update ${target.rcPath} (${error instanceof Error ? error.message : String(error)})\x1b[0m`);
338
+ }
339
+ }
340
+ console.log('\x1b[2mTakes effect in NEW terminals (or run: source ~/.zshrc). Dangerous typed commands are blocked (or warned in monitor mode) and reported to the fleet dashboard.\x1b[0m');
341
+ if (wired === 0) {
342
+ console.log('\x1b[33m⚠ Guard scripts written but no rc file was newly wired — check the entries above.\x1b[0m');
343
+ }
344
+ }
345
+ /** `fullcourtdefense uninstall-shell-guard` on macOS/Linux — remove the guard. */
346
+ async function uninstallPosixShellGuardCommand() {
347
+ if (!isPosixPlatform()) {
348
+ console.log('Nothing to remove on this OS.');
349
+ return;
350
+ }
351
+ let removed = 0;
352
+ for (const target of resolveRcTargets()) {
353
+ if (removeSnippetFromRc(target.rcPath)) {
354
+ removed++;
355
+ console.log(`Removed guard from ${target.shell} rc (${target.rcPath}).`);
356
+ }
357
+ }
358
+ try {
359
+ fs.unlinkSync(GUARD_ZSH_PATH);
360
+ }
361
+ catch { /* not present */ }
362
+ try {
363
+ fs.unlinkSync(GUARD_BASH_PATH);
364
+ }
365
+ catch { /* not present */ }
366
+ try {
367
+ fs.unlinkSync(GUARD_JS_PATH);
368
+ }
369
+ catch { /* not present */ }
370
+ console.log(removed > 0 ? 'Shell guard uninstalled. Open shells keep the guard until closed.' : 'Shell guard was not installed in any rc file.');
371
+ }
372
+ /** Refresh the shared rules JSON when the POSIX guard is installed. Never throws. */
373
+ function refreshPosixShellGuardRules() {
374
+ try {
375
+ if (!isPosixShellGuardInstalled())
376
+ return;
377
+ (0, shellGuard_1.writeShellGuardRules)();
378
+ }
379
+ catch { /* best-effort */ }
380
+ }
@@ -1,7 +1,10 @@
1
+ /** Blast-severity levels (shellfirm-style). Drives per-severity console policy. */
2
+ export type ShellGuardSeverity = 'critical' | 'high' | 'medium' | 'low';
1
3
  export interface ShellGuardRule {
2
4
  id: string;
3
5
  category: string;
4
- /** .NET-compatible regex, matched with IgnoreCase against the typed line. */
6
+ severity: ShellGuardSeverity;
7
+ /** Regex compiled with IgnoreCase in BOTH .NET (PowerShell) and JS (cmd/posix). */
5
8
  pattern: string;
6
9
  reason: string;
7
10
  source: 'builtin' | 'custom';
@@ -67,7 +67,9 @@ const path = __importStar(require("path"));
67
67
  *
68
68
  * Scope honesty: this covers INTERACTIVE PowerShell (PSReadLine hosts, incl.
69
69
  * IDE-integrated terminals). Non-interactive scripts are covered by ScriptBlock
70
- * Logging (windowsAudit.ts). Native cmd.exe is covered separately (cmdGuard.ts).
70
+ * Logging (windowsAudit.ts). Native cmd.exe is covered separately (cmdGuard.ts),
71
+ * and macOS/Linux bash+zsh by posixShellGuard.ts — all three share the ruleset
72
+ * emitted by writeShellGuardRules() below.
71
73
  */
72
74
  const GUARD_RULES_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.json');
73
75
  const GUARD_PS1_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.ps1');
@@ -76,82 +78,162 @@ const RUNTIME_CACHE_PATH = path.join(os.homedir(), '.fullcourtdefense-runtime.js
76
78
  const SNAPSHOT_CACHE_DIR = path.join(os.homedir(), '.fullcourtdefense');
77
79
  const MARKER_START = '# >>> fullcourtdefense shell guard >>>';
78
80
  const MARKER_END = '# <<< fullcourtdefense shell guard <<<';
81
+ /** Confine every lookahead to a single command segment (never span `;` `&` `|`). */
82
+ const SEG = String.raw `[^;&|]*`;
83
+ function compileSpec(spec) {
84
+ let pattern = String.raw `\b(?:${spec.command.join('|')})\b`;
85
+ for (const flag of spec.flags || [])
86
+ pattern += `(?=${SEG}\\s(?:${flag}))`;
87
+ for (const body of spec.extra || [])
88
+ pattern += `(?=${SEG}${body})`;
89
+ for (const n of spec.neg || [])
90
+ pattern += `(?!${SEG}(?:${n}))`;
91
+ if (spec.target && spec.target.length)
92
+ pattern += `(?=${SEG}\\s(?:${spec.target.join('|')}))`;
93
+ return { id: spec.id, category: spec.category, severity: spec.severity, reason: spec.reason, source: 'builtin', pattern };
94
+ }
95
+ // Reusable target fragments.
96
+ const T_UNIX_ROOT = String.raw `["']?/["']?(?:\s|$|[;&|])|\*(?:\s|$|[;&|])`;
97
+ const T_WIN_DRIVE = String.raw `["']?[a-z]:(?:[\\/](?:\*(?:\.\*)?)?)?["']?\s*(?:$|[;&|])`;
98
+ const T_WIN_SYSDIR = String.raw `["']?[a-z]:[\\/](?:windows|program files(?: \(x86\))?|programdata|users)\b`;
79
99
  /**
80
- * Built-in dangerous-command rules the shell-relevant subset of
81
- * deterministicGuard.ts, expressed as .NET-compatible regexes (PowerShell
82
- * compiles them with IgnoreCase). Item ids match the deterministic guard so
83
- * dashboard events line up across IDE hooks and terminal blocks.
100
+ * Built-in dangerous-command rules. Flag/target-bearing rules are compiled from
101
+ * specs (order-independent by construction); the rest are literal regexes. Item
102
+ * ids match the deterministic guard so dashboard events line up across IDE hooks
103
+ * and terminal blocks. Every rule carries a severity for console policy.
84
104
  */
85
105
  const BUILTIN_RULES = [
86
- // Destructive filesystem
87
- { id: 'rm_rf_root', category: 'destructive_command', reason: 'recursive force delete of filesystem root', source: 'builtin',
88
- pattern: String.raw `\b(?:sudo\s+)?rm\s+-[a-z]*r[a-z]*f[a-z]*\s+(?:["']?/["']?|\*)(?:\s|$|[;&|])` },
89
- { id: 'windows_drive_delete', category: 'destructive_command', reason: 'recursive Windows drive delete', source: 'builtin',
90
- pattern: String.raw `\b(?:del|erase)\s+/[a-z]*s[a-z]*\s+/[a-z]*q[a-z]*\s+[a-z]:\\?(?:\s|$|[\\/])` },
91
- { id: 'windows_drive_rmdir', category: 'destructive_command', reason: 'recursive Windows drive remove directory', source: 'builtin',
92
- pattern: String.raw `\b(?:rd|rmdir)\s+/[a-z]*s[a-z]*\s+/[a-z]*q[a-z]*\s+[a-z]:\\?(?:\s|$|[\\/])` },
93
- { id: 'windows_drive_delete', category: 'destructive_command', reason: 'recursive Remove-Item on a drive root', source: 'builtin',
94
- pattern: String.raw `\bremove-item\b(?=.*-recurse)(?=.*-force)(?=.*\s[a-z]:[\\/]?(?:\s|$))` },
95
- { id: 'format_drive', category: 'destructive_command', reason: 'drive format command', source: 'builtin',
106
+ // Destructive filesystem — spec-compiled (order-independent, optional flags not required).
107
+ compileSpec({ id: 'rm_rf_root', category: 'destructive_command', severity: 'critical', reason: 'recursive delete of filesystem root',
108
+ command: ['rm'], flags: [String.raw `-[a-z]*r[a-z]*|--recursive`], target: [T_UNIX_ROOT] }),
109
+ compileSpec({ id: 'rm_rf_home', category: 'destructive_command', severity: 'critical', reason: 'recursive delete of the home directory',
110
+ command: ['rm'], flags: [String.raw `-[a-z]*r[a-z]*|--recursive`], target: [String.raw `["']?(?:~|\$HOME)(?:/\S*)?["']?(?:\s|$|[;&|])`] }),
111
+ compileSpec({ id: 'windows_drive_delete', category: 'destructive_command', severity: 'critical', reason: 'recursive Windows drive delete',
112
+ command: ['del', 'erase'], flags: [String.raw `/s\b`], target: [T_WIN_DRIVE] }),
113
+ compileSpec({ id: 'windows_drive_rmdir', category: 'destructive_command', severity: 'critical', reason: 'recursive Windows drive remove directory',
114
+ command: ['rd', 'rmdir'], flags: [String.raw `/s\b`], target: [String.raw `["']?[a-z]:[\\/]?["']?\s*(?:$|[;&|])`] }),
115
+ compileSpec({ id: 'windows_system_dir_delete', category: 'destructive_command', severity: 'critical', reason: 'recursive delete of a Windows system directory',
116
+ command: ['del', 'erase', 'rd', 'rmdir'], flags: [String.raw `/s\b`], target: [T_WIN_SYSDIR] }),
117
+ compileSpec({ id: 'remove_item_drive', category: 'destructive_command', severity: 'critical', reason: 'recursive Remove-Item on a drive root',
118
+ command: ['remove-item', 'ri'], flags: [String.raw `-recurse\w*`], target: [String.raw `["']?[a-z]:[\\/]?["']?(?:\s|$|[;&|])`] }),
119
+ compileSpec({ id: 'chmod_root_777', category: 'destructive_command', severity: 'high', reason: 'recursive world-writable permission change on root',
120
+ command: ['chmod'], flags: [String.raw `-[a-z]*r[a-z]*|--recursive`, String.raw `[0-7]?777`], target: [T_UNIX_ROOT] }),
121
+ compileSpec({ id: 'chown_root_recursive', category: 'destructive_command', severity: 'high', reason: 'recursive ownership change on the filesystem root',
122
+ command: ['chown'], flags: [String.raw `-[a-z]*r[a-z]*|--recursive`], target: [T_UNIX_ROOT] }),
123
+ compileSpec({ id: 'dd_disk_overwrite', category: 'destructive_command', severity: 'critical', reason: 'raw disk overwrite command',
124
+ command: ['dd'], target: [String.raw `of=/dev/(?:sd|xvd|hd|nvme|disk)\S*`] }),
125
+ { id: 'disk_device_overwrite', category: 'destructive_command', severity: 'critical', reason: 'redirecting output over a raw disk device', source: 'builtin',
126
+ pattern: String.raw `>\s*/dev/(?:sd|nvme|hd|disk|xvd|vd)[a-z0-9]*` },
127
+ { id: 'format_drive', category: 'destructive_command', severity: 'critical', reason: 'drive format command', source: 'builtin',
96
128
  pattern: String.raw `(?<![-\w])(?:format(?:\.com)?\s+[a-z]:(?:\s|$)|format-volume\b(?=.*-driveletter))` },
97
- { id: 'fork_bomb', category: 'destructive_command', reason: 'fork bomb', source: 'builtin',
98
- pattern: String.raw `:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:` },
99
- { id: 'mkfs_device', category: 'destructive_command', reason: 'filesystem format command', source: 'builtin',
129
+ { id: 'mkfs_device', category: 'destructive_command', severity: 'critical', reason: 'filesystem format command', source: 'builtin',
100
130
  pattern: String.raw `\bmkfs(?:\.[a-z0-9]+)?\s+/dev/` },
101
- { id: 'dd_disk_overwrite', category: 'destructive_command', reason: 'raw disk overwrite command', source: 'builtin',
102
- pattern: String.raw `\bdd\b(?=.*\bof=/dev/(?:sd|xvd|hd|nvme|disk))` },
103
- { id: 'chmod_root_777', category: 'destructive_command', reason: 'recursive permission change on root', source: 'builtin',
104
- pattern: String.raw `\bchmod\s+-r\s+777\s+/(?:\s|$)` },
105
- { id: 'vssadmin_delete_shadows', category: 'destructive_command', reason: 'shadow copy deletion (ransomware pattern)', source: 'builtin',
131
+ { id: 'macos_erase_disk', category: 'destructive_command', severity: 'critical', reason: 'erasing a macOS disk/volume', source: 'builtin',
132
+ pattern: String.raw `\bdiskutil\s+(?:erasedisk|erasevolume|reformat|zerodisk|secureerase)\b` },
133
+ { id: 'fork_bomb', category: 'destructive_command', severity: 'high', reason: 'fork bomb', source: 'builtin',
134
+ pattern: String.raw `:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:` },
135
+ { id: 'vssadmin_delete_shadows', category: 'destructive_command', severity: 'critical', reason: 'shadow copy deletion (ransomware pattern)', source: 'builtin',
106
136
  pattern: String.raw `\bvssadmin\b(?=.*\bdelete\b)(?=.*\bshadows\b)` },
107
- { id: 'defender_disable', category: 'destructive_command', reason: 'disabling Windows Defender real-time protection', source: 'builtin',
137
+ { id: 'defender_disable', category: 'security_tampering', severity: 'high', reason: 'disabling Windows Defender real-time protection', source: 'builtin',
108
138
  pattern: String.raw `\bset-mppreference\b(?=.*-disablerealtimemonitoring)(?=.*(?:\$true|\s1\b))` },
109
139
  // Reverse shells / droppers
110
- { id: 'bash_dev_tcp', category: 'reverse_shell', reason: 'bash reverse shell', source: 'builtin',
140
+ { id: 'bash_dev_tcp', category: 'reverse_shell', severity: 'critical', reason: 'bash reverse shell', source: 'builtin',
111
141
  pattern: String.raw `\bbash\s+-i\b.*(?:/dev/tcp/)` },
112
- { id: 'netcat_exec', category: 'reverse_shell', reason: 'netcat reverse shell', source: 'builtin',
142
+ { id: 'netcat_exec', category: 'reverse_shell', severity: 'critical', reason: 'netcat reverse shell', source: 'builtin',
113
143
  pattern: String.raw `\b(?:nc|ncat|netcat)\b.*\s-e\s+(?:/bin/)?(?:sh|bash|cmd(?:\.exe)?|powershell(?:\.exe)?)\b` },
114
- { id: 'socat_exec', category: 'reverse_shell', reason: 'socat reverse shell', source: 'builtin',
144
+ { id: 'socat_exec', category: 'reverse_shell', severity: 'critical', reason: 'socat reverse shell', source: 'builtin',
115
145
  pattern: String.raw `\bsocat\b.*\bexec:(?:/bin/)?(?:sh|bash)\b` },
116
- { id: 'curl_pipe_shell', category: 'reverse_shell', reason: 'remote script piped to shell', source: 'builtin',
146
+ { id: 'curl_pipe_shell', category: 'reverse_shell', severity: 'high', reason: 'remote script piped to shell', source: 'builtin',
117
147
  pattern: String.raw `\b(?:curl|wget)\b[^|;&]{0,300}\|\s*(?:sudo\s+)?(?:sh|bash)\b` },
118
- { id: 'powershell_download_exec', category: 'reverse_shell', reason: 'download-and-execute (IEX + web download)', source: 'builtin',
148
+ { id: 'remote_pipe_interpreter', category: 'reverse_shell', severity: 'high', reason: 'remote script piped to an interpreter', source: 'builtin',
149
+ pattern: String.raw `\b(?:curl|wget)\b[^|;&]{0,300}\|\s*(?:sudo\s+)?(?:python[0-9.]*|perl|ruby|node|php)\b` },
150
+ { id: 'powershell_download_exec', category: 'reverse_shell', severity: 'high', reason: 'download-and-execute (IEX + web download)', source: 'builtin',
119
151
  pattern: String.raw `(?=.*\b(?:iex\b|invoke-expression\b))(?=.*(?:downloadstring|net\.webclient|invoke-webrequest\b|\biwr\b|invoke-restmethod\b|\birm\b))` },
120
- { id: 'python_socket_subprocess', category: 'reverse_shell', reason: 'python reverse shell', source: 'builtin',
152
+ { id: 'python_socket_subprocess', category: 'reverse_shell', severity: 'high', reason: 'python reverse shell', source: 'builtin',
121
153
  pattern: String.raw `\bpython(?:3)?\s+-c\b(?=.*socket)(?=.*subprocess)` },
154
+ { id: 'perl_reverse_shell', category: 'reverse_shell', severity: 'high', reason: 'perl reverse shell', source: 'builtin',
155
+ pattern: String.raw `\bperl\s+-e\b(?=.*socket)(?=.*(?:exec|system|/bin/(?:sh|bash)))` },
156
+ { id: 'php_reverse_shell', category: 'reverse_shell', severity: 'high', reason: 'php reverse shell', source: 'builtin',
157
+ pattern: String.raw `\bphp\s+-r\b(?=.*fsockopen)` },
122
158
  // Cloud metadata / credential exfiltration
123
- { id: 'aws_gcp_metadata_ip', category: 'metadata_ssrf', reason: 'request to cloud metadata endpoint', source: 'builtin',
159
+ { id: 'aws_gcp_metadata_ip', category: 'metadata_ssrf', severity: 'high', reason: 'request to cloud metadata endpoint', source: 'builtin',
124
160
  pattern: String.raw `(?=.*\b(?:curl|wget|iwr|invoke-webrequest|invoke-restmethod|irm)\b)(?=.*(?:169\.254\.169\.254|metadata\.google\.internal|metadata\.azure\.com|100\.100\.100\.200))` },
125
- { id: 'credential_exfiltration', category: 'secret_exfiltration', reason: 'credential file sent to the network', source: 'builtin',
161
+ { id: 'credential_exfiltration', category: 'secret_exfiltration', severity: 'critical', reason: 'credential file sent to the network', source: 'builtin',
126
162
  pattern: String.raw `(?=.*\b(?:curl|wget|iwr|invoke-webrequest|invoke-restmethod|scp|nc|ncat)\b)(?=.*(?:\.ssh[\\/]id_(?:rsa|ed25519|ecdsa|dsa)|\.aws[\\/]credentials|application_default_credentials\.json|\.npmrc|\.netrc|\.kube[\\/]config))` },
127
- // Infra destroy
128
- { id: 'terraform_destroy_auto', category: 'infra_destroy', reason: 'terraform destroy with auto-approve', source: 'builtin',
129
- pattern: String.raw `\bterraform\s+destroy\b(?=.*--auto-approve)` },
130
- { id: 'pulumi_destroy_yes', category: 'infra_destroy', reason: 'pulumi destroy with auto-approve', source: 'builtin',
131
- pattern: String.raw `\bpulumi\s+destroy\b(?=.*(?:--yes|\s-y\b))` },
132
- { id: 'kubectl_delete_namespace', category: 'infra_destroy', reason: 'destructive kubectl namespace delete', source: 'builtin',
133
- pattern: String.raw `\bkubectl\s+delete\s+namespace\b` },
134
- { id: 'kubectl_delete_all_all', category: 'infra_destroy', reason: 'broad kubectl delete all', source: 'builtin',
135
- pattern: String.raw `\bkubectl\s+delete\s+all\b(?=.*--all)` },
136
- { id: 'gcloud_project_delete', category: 'infra_destroy', reason: 'GCP project deletion', source: 'builtin',
163
+ // Infra destroy — spec-compiled where flags matter (order-independent).
164
+ compileSpec({ id: 'terraform_destroy_auto', category: 'infra_destroy', severity: 'high', reason: 'terraform destroy with auto-approve',
165
+ command: ['terraform'], extra: [String.raw `\bdestroy\b`], flags: [String.raw `--auto-approve`] }),
166
+ compileSpec({ id: 'pulumi_destroy_yes', category: 'infra_destroy', severity: 'high', reason: 'pulumi destroy with auto-approve',
167
+ command: ['pulumi'], extra: [String.raw `\bdestroy\b`], flags: [String.raw `--yes|-y\b`] }),
168
+ compileSpec({ id: 'kubectl_delete_all_all', category: 'infra_destroy', severity: 'high', reason: 'broad kubectl delete all',
169
+ command: ['kubectl'], extra: [String.raw `\bdelete\b`, String.raw `\ball\b`], flags: [String.raw `--all`] }),
170
+ compileSpec({ id: 'aws_s3_recursive_rm', category: 'infra_destroy', severity: 'high', reason: 'recursive S3 deletion',
171
+ command: ['aws'], extra: [String.raw `\bs3\s+rm\b`], flags: [String.raw `--recursive`], target: [String.raw `s3://\S+`] }),
172
+ compileSpec({ id: 'aws_iam_admin_policy', category: 'infra_destroy', severity: 'high', reason: 'IAM administrator privilege grant',
173
+ command: ['aws'], extra: [String.raw `\biam\s+attach-user-policy\b`], flags: [String.raw `AdministratorAccess`] }),
174
+ compileSpec({ id: 'az_group_delete_yes', category: 'infra_destroy', severity: 'high', reason: 'Azure resource group deletion',
175
+ command: ['az'], extra: [String.raw `\bgroup\s+delete\b`], flags: [String.raw `--yes|-y\b`] }),
176
+ compileSpec({ id: 'az_vm_delete', category: 'infra_destroy', severity: 'high', reason: 'Azure VM deletion',
177
+ command: ['az'], extra: [String.raw `\bvm\s+delete\b`], flags: [String.raw `--yes|-y\b`] }),
178
+ compileSpec({ id: 'docker_prune_all', category: 'infra_destroy', severity: 'medium', reason: 'destructive docker prune (removes all images/volumes)',
179
+ command: ['docker'], extra: [String.raw `\b(?:system|volume|image)\s+prune\b`], flags: [String.raw `-a\b|--all|--volumes`] }),
180
+ compileSpec({ id: 'kubectl_delete_pvc_all', category: 'infra_destroy', severity: 'high', reason: 'kubectl delete all persistent volume claims',
181
+ command: ['kubectl'], extra: [String.raw `\bdelete\s+(?:pvc|persistentvolumeclaims?)\b`], flags: [String.raw `--all`] }),
182
+ { id: 'kubectl_delete_namespace', category: 'infra_destroy', severity: 'high', reason: 'destructive kubectl namespace delete', source: 'builtin',
183
+ pattern: String.raw `\bkubectl\s+delete\s+(?:namespace|ns)\b` },
184
+ { id: 'gcloud_project_delete', category: 'infra_destroy', severity: 'high', reason: 'GCP project deletion', source: 'builtin',
137
185
  pattern: String.raw `\bgcloud\s+projects\s+delete\b` },
138
- { id: 'gcloud_sql_delete', category: 'infra_destroy', reason: 'GCP SQL instance deletion', source: 'builtin',
186
+ { id: 'gcloud_sql_delete', category: 'infra_destroy', severity: 'high', reason: 'GCP SQL instance deletion', source: 'builtin',
139
187
  pattern: String.raw `\bgcloud\s+sql\s+instances\s+delete\b` },
140
- { id: 'aws_s3_recursive_rm', category: 'infra_destroy', reason: 'recursive S3 deletion', source: 'builtin',
141
- pattern: String.raw `\baws\s+s3\s+rm\s+s3://\S+\s+--recursive\b` },
142
- { id: 'aws_cloudformation_delete_stack', category: 'infra_destroy', reason: 'CloudFormation stack deletion', source: 'builtin',
188
+ { id: 'gcloud_gke_delete', category: 'infra_destroy', severity: 'high', reason: 'GKE cluster deletion', source: 'builtin',
189
+ pattern: String.raw `\bgcloud\s+container\s+clusters\s+delete\b` },
190
+ { id: 'aws_cloudformation_delete_stack', category: 'infra_destroy', severity: 'high', reason: 'CloudFormation stack deletion', source: 'builtin',
143
191
  pattern: String.raw `\baws\s+cloudformation\s+delete-stack\b` },
144
- { id: 'aws_iam_admin_policy', category: 'infra_destroy', reason: 'IAM administrator privilege grant', source: 'builtin',
145
- pattern: String.raw `\baws\s+iam\s+attach-user-policy\b(?=.*AdministratorAccess)` },
146
- { id: 'aws_iam_access_key', category: 'infra_destroy', reason: 'IAM access key creation', source: 'builtin',
192
+ { id: 'aws_iam_access_key', category: 'infra_destroy', severity: 'medium', reason: 'IAM access key creation', source: 'builtin',
147
193
  pattern: String.raw `\baws\s+iam\s+create-access-key\b` },
148
- { id: 'az_group_delete_yes', category: 'infra_destroy', reason: 'Azure resource group deletion', source: 'builtin',
149
- pattern: String.raw `\baz\s+group\s+delete\b(?=.*(?:--yes|\s-y\b))` },
194
+ { id: 'aws_ec2_terminate', category: 'infra_destroy', severity: 'high', reason: 'EC2 instance termination', source: 'builtin',
195
+ pattern: String.raw `\baws\s+ec2\s+terminate-instances\b` },
196
+ { id: 'aws_dynamodb_delete_table', category: 'infra_destroy', severity: 'high', reason: 'DynamoDB table deletion', source: 'builtin',
197
+ pattern: String.raw `\baws\s+dynamodb\s+delete-table\b` },
198
+ { id: 'aws_rds_delete', category: 'infra_destroy', severity: 'high', reason: 'RDS instance deletion', source: 'builtin',
199
+ pattern: String.raw `\baws\s+rds\s+delete-db-instance\b` },
200
+ // Version-control footguns — imported from shellfirm / Sigma (git group).
201
+ compileSpec({ id: 'git_force_push', category: 'vcs_danger', severity: 'high', reason: 'git force push (overwrites remote history)',
202
+ command: ['git'], extra: [String.raw `\bpush\b`], flags: [String.raw `--force|-f\b`], neg: [String.raw `\s--force-with-lease`] }),
203
+ compileSpec({ id: 'git_reset_hard', category: 'vcs_danger', severity: 'medium', reason: 'git reset --hard (discards local changes)',
204
+ command: ['git'], extra: [String.raw `\breset\b`], flags: [String.raw `--hard`] }),
205
+ compileSpec({ id: 'git_clean_force', category: 'vcs_danger', severity: 'medium', reason: 'git clean -fd (deletes untracked files)',
206
+ command: ['git'], extra: [String.raw `\bclean\b`], flags: [String.raw `-[a-z]*f`, String.raw `-[a-z]*d`] }),
207
+ { id: 'git_branch_delete_force', category: 'vcs_danger', severity: 'low', reason: 'git force-delete branch', source: 'builtin',
208
+ pattern: String.raw `\bgit\s+branch\b(?=[^;&|]*\s-D\b)` },
209
+ // Anti-forensics / tamper
210
+ { id: 'clear_shell_history', category: 'anti_forensics', severity: 'medium', reason: 'clearing shell history', source: 'builtin',
211
+ pattern: String.raw `\bhistory\s+-c\b|\brm\b[^|;&]{0,80}\.(?:bash|zsh)_history\b` },
212
+ { id: 'windows_clear_eventlog', category: 'anti_forensics', severity: 'high', reason: 'clearing Windows event logs', source: 'builtin',
213
+ pattern: String.raw `\b(?:wevtutil\s+cl|clear-eventlog)\b` },
214
+ // Security-control tampering
215
+ { id: 'disable_firewall_windows', category: 'security_tampering', severity: 'high', reason: 'disabling the Windows firewall', source: 'builtin',
216
+ pattern: String.raw `\bnetsh\s+advfirewall\s+set\b(?=.*\bstate\s+off)` },
217
+ { id: 'disable_firewall_unix', category: 'security_tampering', severity: 'high', reason: 'disabling the host firewall', source: 'builtin',
218
+ pattern: String.raw `\bufw\s+disable\b|\biptables\s+-F\b|\bsystemctl\s+(?:stop|disable)\s+firewalld\b` },
219
+ { id: 'macos_disable_gatekeeper', category: 'security_tampering', severity: 'high', reason: 'disabling macOS Gatekeeper', source: 'builtin',
220
+ pattern: String.raw `\bspctl\s+--master-disable\b` },
221
+ { id: 'macos_disable_sip', category: 'security_tampering', severity: 'high', reason: 'disabling macOS System Integrity Protection', source: 'builtin',
222
+ pattern: String.raw `\bcsrutil\s+disable\b` },
223
+ // Persistence / backdoor
224
+ { id: 'crontab_from_stdin', category: 'persistence', severity: 'medium', reason: 'installing a crontab from stdin', source: 'builtin',
225
+ pattern: String.raw `\|\s*crontab\b` },
226
+ { id: 'authorized_keys_backdoor', category: 'persistence', severity: 'high', reason: 'appending an SSH authorized key', source: 'builtin',
227
+ pattern: String.raw `>>?\s*(?:~|\$HOME|/root|/home/\S+)?/?\.ssh/authorized_keys\b` },
150
228
  // Destructive SQL typed straight into a terminal client
151
- { id: 'drop_database', category: 'destructive_sql', reason: 'DROP DATABASE', source: 'builtin',
229
+ { id: 'drop_database', category: 'destructive_sql', severity: 'high', reason: 'DROP DATABASE', source: 'builtin',
152
230
  pattern: String.raw `\bdrop\s+database\b` },
153
- { id: 'drop_schema', category: 'destructive_sql', reason: 'DROP SCHEMA', source: 'builtin',
231
+ { id: 'drop_schema', category: 'destructive_sql', severity: 'high', reason: 'DROP SCHEMA', source: 'builtin',
154
232
  pattern: String.raw `\bdrop\s+schema\b` },
233
+ { id: 'truncate_table', category: 'destructive_sql', severity: 'medium', reason: 'TRUNCATE TABLE', source: 'builtin',
234
+ pattern: String.raw `\btruncate\s+table\b` },
235
+ { id: 'drop_table', category: 'destructive_sql', severity: 'medium', reason: 'DROP TABLE', source: 'builtin',
236
+ pattern: String.raw `\bdrop\s+table\b` },
155
237
  ];
156
238
  function escapeDotNetRegex(literal) {
157
239
  return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -211,9 +293,11 @@ function cachedCustomRules() {
211
293
  for (const block of blocks.slice(0, 100)) {
212
294
  if (!block || typeof block.pattern !== 'string' || !block.pattern.trim())
213
295
  continue;
296
+ const sev = String(block.severity || '').toLowerCase();
214
297
  rules.push({
215
298
  id: String(block.id || block.pattern).slice(0, 120),
216
299
  category: String(block.categoryId || 'custom'),
300
+ severity: (sev === 'critical' || sev === 'high' || sev === 'medium' || sev === 'low') ? sev : 'high',
217
301
  // Snapshot custom blocks are substring matches — escape into a literal regex.
218
302
  pattern: escapeDotNetRegex(block.pattern.trim()),
219
303
  reason: typeof block.explanation === 'string' && block.explanation ? block.explanation : `matched custom safety pattern "${block.pattern.trim()}"`,
@@ -286,6 +370,7 @@ function buildGuardPs1(nodePath, cliEntry) {
286
370
  ` [pscustomobject]@{`,
287
371
  ` Id = [string]$_.id`,
288
372
  ` Category = [string]$_.category`,
373
+ ` Severity = [string]$_.severity`,
289
374
  ` Reason = [string]$_.reason`,
290
375
  ` Source = [string]$_.source`,
291
376
  ` Regex = [regex]::new([string]$_.pattern, 'IgnoreCase')`,
@@ -321,6 +406,7 @@ function buildGuardPs1(nodePath, cliEntry) {
321
406
  ` reason = ('Shell guard: ' + $Rule.Reason)`,
322
407
  ` ruleId = $Rule.Id`,
323
408
  ` category = $Rule.Category`,
409
+ ` severity = $Rule.Severity`,
324
410
  ` source = $Rule.Source`,
325
411
  ` evidence = $evidence`,
326
412
  ` occurredAt = (Get-Date).ToUniversalTime().ToString('o')`,
@@ -349,14 +435,14 @@ function buildGuardPs1(nodePath, cliEntry) {
349
435
  ` if ($null -ne $hit) {`,
350
436
  ` if ($global:FcdGuardMode -eq 'monitor') {`,
351
437
  ` Write-Host ''`,
352
- ` Write-Host ('[FullCourtDefense] monitor: would block - ' + $hit.Reason + ' (rule ' + $hit.Id + ')') -ForegroundColor Yellow`,
438
+ ` Write-Host ('[FullCourtDefense] monitor: would block [' + $hit.Severity + '] ' + $hit.Reason + ' (rule ' + $hit.Id + ')') -ForegroundColor Yellow`,
353
439
  ` Write-FcdGuardEvent -Rule $hit -CommandLine $line -Decision 'allow'`,
354
440
  ` [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()`,
355
441
  ` return`,
356
442
  ` }`,
357
443
  ` [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()`,
358
444
  ` Write-Host ''`,
359
- ` Write-Host ('[FullCourtDefense] BLOCKED: ' + $hit.Reason + ' (rule ' + $hit.Id + ')') -ForegroundColor Red`,
445
+ ` Write-Host ('[FullCourtDefense] BLOCKED [' + $hit.Severity + ']: ' + $hit.Reason + ' (rule ' + $hit.Id + ')') -ForegroundColor Red`,
360
446
  ` Write-Host 'This command was not executed. Reported to your security dashboard.' -ForegroundColor DarkGray`,
361
447
  ` Write-FcdGuardEvent -Rule $hit -CommandLine $line -Decision 'block'`,
362
448
  ` [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()`,
package/dist/index.js CHANGED
@@ -53,6 +53,7 @@ const autoProtect_1 = require("./commands/autoProtect");
53
53
  const windowsAudit_1 = require("./commands/windowsAudit");
54
54
  const shellGuard_1 = require("./commands/shellGuard");
55
55
  const cmdGuard_1 = require("./commands/cmdGuard");
56
+ const posixShellGuard_1 = require("./commands/posixShellGuard");
56
57
  const fs = __importStar(require("fs"));
57
58
  const path = __importStar(require("path"));
58
59
  function readCliVersion() {
@@ -128,9 +129,9 @@ function printHelp() {
128
129
  install Alias for install-all.
129
130
  windows-audit Show Windows PowerShell audit coverage (ScriptBlock Logging +
130
131
  Transcription). Use --enable to turn it on (one admin prompt).
131
- install-shell-guard Block dangerous commands typed in PowerShell terminals
132
- (real-time, offline rule cache). uninstall-shell-guard removes it;
133
- shell-guard-status shows coverage.
132
+ install-shell-guard Block dangerous commands typed in a terminal (real-time,
133
+ offline rule cache): PowerShell on Windows, bash + zsh on macOS/Linux.
134
+ uninstall-shell-guard removes it; shell-guard-status shows coverage.
134
135
  install-cmd-guard Block dangerous commands typed in cmd.exe (Command Prompt).
135
136
  uninstall-cmd-guard removes it; cmd-guard-status shows coverage.
136
137
  scan Runs the scan. Use --local for inside-organization scans.
@@ -605,6 +606,7 @@ async function main() {
605
606
  windowsAudit: flags['windows-audit'],
606
607
  shellGuard: flags['shell-guard'],
607
608
  cmdGuard: flags['cmd-guard'],
609
+ posixGuard: flags['posix-guard'],
608
610
  };
609
611
  await (0, installAll_1.installAllCommand)(args, config);
610
612
  break;
@@ -615,22 +617,39 @@ async function main() {
615
617
  break;
616
618
  }
617
619
  case 'install-shell-guard': {
618
- await (0, shellGuard_1.installShellGuardCommand)({ profiles: flags.profiles });
620
+ if (process.platform === 'win32') {
621
+ await (0, shellGuard_1.installShellGuardCommand)({ profiles: flags.profiles });
622
+ }
623
+ else {
624
+ await (0, posixShellGuard_1.installPosixShellGuardCommand)({ profiles: flags.profiles });
625
+ }
619
626
  break;
620
627
  }
621
628
  case 'uninstall-shell-guard': {
622
- await (0, shellGuard_1.uninstallShellGuardCommand)();
629
+ if (process.platform === 'win32') {
630
+ await (0, shellGuard_1.uninstallShellGuardCommand)();
631
+ }
632
+ else {
633
+ await (0, posixShellGuard_1.uninstallPosixShellGuardCommand)();
634
+ }
623
635
  break;
624
636
  }
625
637
  case 'shell-guard-status': {
626
- const status = (0, shellGuard_1.getShellGuardStatus)();
627
- if (!status.supported) {
628
- console.log('Shell guard supports Windows PowerShell (bash/zsh planned).');
638
+ if (process.platform === 'win32') {
639
+ const status = (0, shellGuard_1.getShellGuardStatus)();
640
+ console.log(`\nInteractive PowerShell shell guard: ${status.installed ? '\x1b[32minstalled\x1b[0m' : '\x1b[31mnot installed\x1b[0m'}`);
641
+ for (const profile of status.profiles) {
642
+ console.log(` ${profile.engine}: ${profile.installed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} \x1b[2m${profile.profilePath}\x1b[0m`);
643
+ }
644
+ if (status.rulesPresent)
645
+ console.log(` Rules: ${status.ruleCount} (mode: ${status.mode || 'block'})`);
646
+ console.log('');
629
647
  break;
630
648
  }
631
- console.log(`\nInteractive PowerShell shell guard: ${status.installed ? '\x1b[32minstalled\x1b[0m' : '\x1b[31mnot installed\x1b[0m'}`);
632
- for (const profile of status.profiles) {
633
- console.log(` ${profile.engine}: ${profile.installed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} \x1b[2m${profile.profilePath}\x1b[0m`);
649
+ const status = (0, posixShellGuard_1.getPosixShellGuardStatus)();
650
+ console.log(`\nInteractive shell guard (bash + zsh): ${status.installed ? '\x1b[32minstalled\x1b[0m' : '\x1b[31mnot installed\x1b[0m'}`);
651
+ for (const rc of status.rcFiles) {
652
+ console.log(` ${rc.shell}: ${rc.installed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} \x1b[2m${rc.rcPath}\x1b[0m`);
634
653
  }
635
654
  if (status.rulesPresent)
636
655
  console.log(` Rules: ${status.ruleCount} (mode: ${status.mode || 'block'})`);
@@ -9,6 +9,7 @@ export interface SpoolEvent {
9
9
  category?: string;
10
10
  categoryId?: string;
11
11
  itemId?: string;
12
+ severity?: 'critical' | 'high' | 'medium' | 'low';
12
13
  source?: 'builtin' | 'custom' | 'taint' | 'backend';
13
14
  evidence?: string;
14
15
  explanation?: string;
package/dist/telemetry.js CHANGED
@@ -45,6 +45,7 @@ const machineIdentity_1 = require("./machineIdentity");
45
45
  const windowsAudit_1 = require("./commands/windowsAudit");
46
46
  const shellGuard_1 = require("./commands/shellGuard");
47
47
  const cmdGuard_1 = require("./commands/cmdGuard");
48
+ const posixShellGuard_1 = require("./commands/posixShellGuard");
48
49
  /**
49
50
  * Local-first telemetry: every enforcement decision is appended to an on-disk
50
51
  * spool (instant, offline-safe) and flushed to the backend in batches. Critical
@@ -72,6 +73,7 @@ function spoolEvent(event) {
72
73
  category: event.category,
73
74
  categoryId: event.categoryId,
74
75
  itemId: event.itemId,
76
+ severity: event.severity,
75
77
  source: event.source,
76
78
  evidence: event.evidence,
77
79
  explanation: event.explanation,
@@ -115,6 +117,7 @@ async function flushSpool(input) {
115
117
  // + new custom rules reach open shells within ~30s).
116
118
  (0, shellGuard_1.refreshShellGuardRules)();
117
119
  (0, cmdGuard_1.refreshCmdGuardRules)();
120
+ (0, posixShellGuard_1.refreshPosixShellGuardRules)();
118
121
  const all = readSpool();
119
122
  const batch = all.slice(0, MAX_BATCH);
120
123
  const overflow = all.slice(MAX_BATCH);
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.11.0"
2
+ "version": "1.13.0"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "test:shell-audit": "npm run build && node scripts/test-shell-audit.js",
20
20
  "test:shell-guard": "npm run build && node scripts/test-shell-guard.js",
21
21
  "test:cmd-guard": "npm run build && node scripts/test-cmd-guard.js",
22
+ "test:posix-guard": "npm run build && node scripts/test-posix-guard.js",
22
23
  "test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
23
24
  "prepublishOnly": "npm run build"
24
25
  },