fullcourtdefense-cli 1.10.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.
- package/dist/commands/cmdGuard.d.ts +20 -0
- package/dist/commands/cmdGuard.js +245 -0
- package/dist/commands/installAll.d.ts +2 -0
- package/dist/commands/installAll.js +24 -0
- package/dist/commands/posixShellGuard.d.ts +26 -0
- package/dist/commands/posixShellGuard.js +380 -0
- package/dist/commands/shellGuard.d.ts +4 -1
- package/dist/commands/shellGuard.js +141 -53
- package/dist/index.js +57 -11
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.js +7 -2
- package/dist/version.json +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CmdGuardStatus {
|
|
2
|
+
supported: boolean;
|
|
3
|
+
installed: boolean;
|
|
4
|
+
autorunValue?: string;
|
|
5
|
+
rulesPresent: boolean;
|
|
6
|
+
ruleCount?: number;
|
|
7
|
+
mode?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function isCmdGuardInstalled(): boolean;
|
|
10
|
+
export declare function getCmdGuardStatus(): CmdGuardStatus;
|
|
11
|
+
export interface InstallCmdGuardArgs {
|
|
12
|
+
/** Reserved for future use. */
|
|
13
|
+
profiles?: string;
|
|
14
|
+
}
|
|
15
|
+
/** `fullcourtdefense install-cmd-guard` — enable interactive cmd.exe blocking. */
|
|
16
|
+
export declare function installCmdGuardCommand(_args?: InstallCmdGuardArgs): Promise<void>;
|
|
17
|
+
/** `fullcourtdefense uninstall-cmd-guard` */
|
|
18
|
+
export declare function uninstallCmdGuardCommand(): Promise<void>;
|
|
19
|
+
/** Refresh shared rules JSON when cmd guard is installed. */
|
|
20
|
+
export declare function refreshCmdGuardRules(): void;
|
|
@@ -0,0 +1,245 @@
|
|
|
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.isCmdGuardInstalled = isCmdGuardInstalled;
|
|
37
|
+
exports.getCmdGuardStatus = getCmdGuardStatus;
|
|
38
|
+
exports.installCmdGuardCommand = installCmdGuardCommand;
|
|
39
|
+
exports.uninstallCmdGuardCommand = uninstallCmdGuardCommand;
|
|
40
|
+
exports.refreshCmdGuardRules = refreshCmdGuardRules;
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const shellGuard_1 = require("./shellGuard");
|
|
46
|
+
/**
|
|
47
|
+
* Interactive cmd.exe guard — blocks dangerous commands TYPED in Command Prompt.
|
|
48
|
+
*
|
|
49
|
+
* cmd.exe has no PSReadLine/preexec hook. We use the supported AutoRun registry
|
|
50
|
+
* value + doskey macros: every new cmd session registers macros for high-risk
|
|
51
|
+
* command names (del, erase, rd, curl, …) that delegate to a Node checker
|
|
52
|
+
* sharing the same rules JSON as the PowerShell guard.
|
|
53
|
+
*
|
|
54
|
+
* Allowed commands run via `cmd /d /c` so AutoRun is not re-triggered (no loop).
|
|
55
|
+
* Interactive cmd only — `cmd /c del ...` from scripts bypasses doskey by design.
|
|
56
|
+
*/
|
|
57
|
+
const GUARD_RULES_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.json');
|
|
58
|
+
const GUARD_JS_PATH = path.join(os.homedir(), '.fullcourtdefense-cmd-guard.js');
|
|
59
|
+
const AUTORUN_BAT_PATH = path.join(os.homedir(), '.fullcourtdefense-cmd-autorun.bat');
|
|
60
|
+
const CMD_AUTORUN_KEY = 'HKCU\\Software\\Microsoft\\Command Processor';
|
|
61
|
+
const AUTORUN_VALUE = 'AutoRun';
|
|
62
|
+
const AUTORUN_MARKER = 'fullcourtdefense-cmd-autorun.bat';
|
|
63
|
+
/** cmd builtins / exes we intercept via doskey (first token of the typed line). */
|
|
64
|
+
const INTERCEPTED_COMMANDS = [
|
|
65
|
+
'del', 'erase', 'rd', 'rmdir', 'format',
|
|
66
|
+
'curl', 'wget', 'powershell', 'pwsh',
|
|
67
|
+
'terraform', 'kubectl', 'aws', 'gcloud', 'az',
|
|
68
|
+
'vssadmin', 'certutil', 'bitsadmin', 'reg', 'net', 'sc',
|
|
69
|
+
];
|
|
70
|
+
function regString(key, value) {
|
|
71
|
+
try {
|
|
72
|
+
const out = (0, child_process_1.execFileSync)('reg', ['query', key, '/v', value], {
|
|
73
|
+
encoding: 'utf8', windowsHide: true, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
74
|
+
});
|
|
75
|
+
const match = out.match(new RegExp(`${value}\\s+REG_(?:EXPAND_)?SZ\\s+(.+)`));
|
|
76
|
+
return match ? match[1].trim() : undefined;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function setRegString(key, valueName, data) {
|
|
83
|
+
(0, child_process_1.execFileSync)('reg', ['add', key, '/v', valueName, '/t', 'REG_SZ', '/d', data, '/f'], {
|
|
84
|
+
windowsHide: true, timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function batQuote(value) {
|
|
88
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
89
|
+
}
|
|
90
|
+
function buildGuardJs(nodePath, cliEntry) {
|
|
91
|
+
return [
|
|
92
|
+
`'use strict';`,
|
|
93
|
+
`const fs=require('fs');const os=require('os');const path=require('path');const{spawnSync}=require('child_process');const crypto=require('crypto');`,
|
|
94
|
+
`const RULES_PATH=${JSON.stringify(GUARD_RULES_PATH)};`,
|
|
95
|
+
`const SPOOL_PATH=${JSON.stringify(path.join(os.homedir(), '.fullcourtdefense-spool.jsonl'))};`,
|
|
96
|
+
`const NODE_PATH=${JSON.stringify(nodePath)};`,
|
|
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,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
|
+
`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,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
|
+
`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
|
+
`const args=process.argv.slice(2);if(!args.length)process.exit(0);if(process.env.FCD_CMD_GUARD==='off')delegate(args);`,
|
|
103
|
+
`const line=args.join(' ');const{mode,rules}=loadRules();const hit=matchLine(line,rules);`,
|
|
104
|
+
`if(!hit)delegate(args);`,
|
|
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
|
+
`console.error('This command was not executed. Reported to your security dashboard.');`,
|
|
108
|
+
`spoolEvent(hit,line,'block');process.exit(1);`,
|
|
109
|
+
``,
|
|
110
|
+
].join('\n');
|
|
111
|
+
}
|
|
112
|
+
function buildAutorunBat(nodePath) {
|
|
113
|
+
const guardJs = batQuote(GUARD_JS_PATH);
|
|
114
|
+
const node = batQuote(nodePath);
|
|
115
|
+
const doskeyLines = INTERCEPTED_COMMANDS.map(cmd => `doskey ${cmd}=${node} ${guardJs} ${cmd} $*`);
|
|
116
|
+
return [
|
|
117
|
+
'@echo off',
|
|
118
|
+
'REM FullCourtDefense cmd.exe guard (installed by fullcourtdefense-cli).',
|
|
119
|
+
'REM Disable for one session: set FCD_CMD_GUARD=off',
|
|
120
|
+
'REM Remove permanently: fullcourtdefense uninstall-cmd-guard',
|
|
121
|
+
'if defined FCD_CMD_AUTORUN_DONE goto :eof',
|
|
122
|
+
'set FCD_CMD_AUTORUN_DONE=1',
|
|
123
|
+
...doskeyLines,
|
|
124
|
+
'',
|
|
125
|
+
].join('\r\n');
|
|
126
|
+
}
|
|
127
|
+
function readCurrentAutorun() {
|
|
128
|
+
return regString(CMD_AUTORUN_KEY, AUTORUN_VALUE) || '';
|
|
129
|
+
}
|
|
130
|
+
function mergeAutorunValue(current) {
|
|
131
|
+
const ours = batQuote(AUTORUN_BAT_PATH);
|
|
132
|
+
const cleaned = stripAutorunValue(current);
|
|
133
|
+
if (!cleaned.trim())
|
|
134
|
+
return ours;
|
|
135
|
+
if (cleaned.includes(AUTORUN_MARKER))
|
|
136
|
+
return ours;
|
|
137
|
+
return `${ours} & ${cleaned}`;
|
|
138
|
+
}
|
|
139
|
+
function stripAutorunValue(current) {
|
|
140
|
+
const ours = batQuote(AUTORUN_BAT_PATH);
|
|
141
|
+
let next = current
|
|
142
|
+
.replace(new RegExp(`${ours.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*&\\s*`, 'gi'), '')
|
|
143
|
+
.replace(new RegExp(`\\s*&\\s*${ours.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'gi'), '')
|
|
144
|
+
.replace(ours, '')
|
|
145
|
+
.replace(/^\s*&\s*|\s*&\s*$/g, '')
|
|
146
|
+
.trim();
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
function isCmdGuardInstalled() {
|
|
150
|
+
try {
|
|
151
|
+
if (process.platform !== 'win32')
|
|
152
|
+
return false;
|
|
153
|
+
if (!fs.existsSync(GUARD_JS_PATH) || !fs.existsSync(AUTORUN_BAT_PATH))
|
|
154
|
+
return false;
|
|
155
|
+
return readCurrentAutorun().includes(AUTORUN_MARKER);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function getCmdGuardStatus() {
|
|
162
|
+
if (process.platform !== 'win32') {
|
|
163
|
+
return { supported: false, installed: false, rulesPresent: false };
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
let ruleCount;
|
|
167
|
+
let mode;
|
|
168
|
+
let rulesPresent = false;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(fs.readFileSync(GUARD_RULES_PATH, 'utf8'));
|
|
171
|
+
rulesPresent = Array.isArray(parsed?.rules) && parsed.rules.length > 0;
|
|
172
|
+
ruleCount = Array.isArray(parsed?.rules) ? parsed.rules.length : undefined;
|
|
173
|
+
mode = typeof parsed?.mode === 'string' ? parsed.mode : undefined;
|
|
174
|
+
}
|
|
175
|
+
catch { /* not written yet */ }
|
|
176
|
+
const autorunValue = readCurrentAutorun();
|
|
177
|
+
return {
|
|
178
|
+
supported: true,
|
|
179
|
+
installed: isCmdGuardInstalled(),
|
|
180
|
+
autorunValue: autorunValue || undefined,
|
|
181
|
+
rulesPresent,
|
|
182
|
+
ruleCount,
|
|
183
|
+
mode,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return { supported: true, installed: false, rulesPresent: false };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** `fullcourtdefense install-cmd-guard` — enable interactive cmd.exe blocking. */
|
|
191
|
+
async function installCmdGuardCommand(_args = {}) {
|
|
192
|
+
if (process.platform !== 'win32') {
|
|
193
|
+
console.log('The cmd.exe guard currently supports Windows only.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const rules = (0, shellGuard_1.writeShellGuardRules)();
|
|
197
|
+
const nodePath = process.execPath;
|
|
198
|
+
const cliEntry = process.argv[1] || '';
|
|
199
|
+
fs.writeFileSync(GUARD_JS_PATH, buildGuardJs(nodePath, cliEntry), 'utf8');
|
|
200
|
+
fs.writeFileSync(AUTORUN_BAT_PATH, buildAutorunBat(nodePath), 'utf8');
|
|
201
|
+
console.log(`Guard rules written: ${rules.rules.length} rule(s), mode "${rules.mode}".`);
|
|
202
|
+
const current = readCurrentAutorun();
|
|
203
|
+
const merged = mergeAutorunValue(current);
|
|
204
|
+
setRegString(CMD_AUTORUN_KEY, AUTORUN_VALUE, merged);
|
|
205
|
+
console.log(`\x1b[32m✓ cmd.exe guard installed\x1b[0m \x1b[2m(AutoRun + doskey macros)\x1b[0m`);
|
|
206
|
+
console.log('\x1b[2mOpen a NEW Command Prompt window (prompt must be C:\\...> not PS C:\\...>). Dangerous commands like del /s /q C:\\ are blocked and reported.\x1b[0m');
|
|
207
|
+
}
|
|
208
|
+
/** `fullcourtdefense uninstall-cmd-guard` */
|
|
209
|
+
async function uninstallCmdGuardCommand() {
|
|
210
|
+
if (process.platform !== 'win32') {
|
|
211
|
+
console.log('Nothing to remove on this OS.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const current = readCurrentAutorun();
|
|
215
|
+
const next = stripAutorunValue(current);
|
|
216
|
+
if (next) {
|
|
217
|
+
setRegString(CMD_AUTORUN_KEY, AUTORUN_VALUE, next);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
try {
|
|
221
|
+
(0, child_process_1.execFileSync)('reg', ['delete', CMD_AUTORUN_KEY, '/v', AUTORUN_VALUE, '/f'], {
|
|
222
|
+
windowsHide: true, timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch { /* value may already be absent */ }
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
fs.unlinkSync(GUARD_JS_PATH);
|
|
229
|
+
}
|
|
230
|
+
catch { /* not present */ }
|
|
231
|
+
try {
|
|
232
|
+
fs.unlinkSync(AUTORUN_BAT_PATH);
|
|
233
|
+
}
|
|
234
|
+
catch { /* not present */ }
|
|
235
|
+
console.log('cmd.exe guard uninstalled. Open cmd windows keep doskey until closed.');
|
|
236
|
+
}
|
|
237
|
+
/** Refresh shared rules JSON when cmd guard is installed. */
|
|
238
|
+
function refreshCmdGuardRules() {
|
|
239
|
+
try {
|
|
240
|
+
if (!isCmdGuardInstalled())
|
|
241
|
+
return;
|
|
242
|
+
(0, shellGuard_1.writeShellGuardRules)();
|
|
243
|
+
}
|
|
244
|
+
catch { /* best-effort */ }
|
|
245
|
+
}
|
|
@@ -12,6 +12,8 @@ export interface InstallAllArgs extends ProtectAllArgs {
|
|
|
12
12
|
intervalMinutes?: string;
|
|
13
13
|
windowsAudit?: string;
|
|
14
14
|
shellGuard?: string;
|
|
15
|
+
cmdGuard?: string;
|
|
16
|
+
posixGuard?: string;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* One-click install: discover/wrap every existing MCP server, install Cursor
|
|
@@ -11,6 +11,8 @@ const autoProtect_1 = require("./autoProtect");
|
|
|
11
11
|
const restartNotice_1 = require("./restartNotice");
|
|
12
12
|
const windowsAudit_1 = require("./windowsAudit");
|
|
13
13
|
const shellGuard_1 = require("./shellGuard");
|
|
14
|
+
const cmdGuard_1 = require("./cmdGuard");
|
|
15
|
+
const posixShellGuard_1 = require("./posixShellGuard");
|
|
14
16
|
/**
|
|
15
17
|
* One-click install: discover/wrap every existing MCP server, install Cursor
|
|
16
18
|
* hooks for built-in Cursor actions, optional self-heal and fleet upload.
|
|
@@ -106,6 +108,28 @@ async function installAllCommand(args, config) {
|
|
|
106
108
|
console.log(`\x1b[33m⚠ Warning: shell guard install failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
111
|
+
// Interactive cmd.exe guard — same rules, doskey + AutoRun hook (no admin).
|
|
112
|
+
if (process.platform === 'win32' && args.cmdGuard !== 'false') {
|
|
113
|
+
console.log('\n\x1b[1mInstalling interactive cmd.exe shell guard…\x1b[0m');
|
|
114
|
+
try {
|
|
115
|
+
await (0, cmdGuard_1.installCmdGuardCommand)();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.log(`\x1b[33m⚠ Warning: cmd guard install failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
|
|
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
|
+
}
|
|
109
133
|
if (args.autoProtect === 'true') {
|
|
110
134
|
console.log('\n\x1b[1mScheduling self-healing MCP protection…\x1b[0m');
|
|
111
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
|
-
|
|
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)
|
|
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,80 +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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
|
88
|
-
|
|
89
|
-
{ id: '
|
|
90
|
-
|
|
91
|
-
{ id: 'windows_drive_delete', category: 'destructive_command', reason: 'recursive
|
|
92
|
-
|
|
93
|
-
{ id: '
|
|
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',
|
|
94
128
|
pattern: String.raw `(?<![-\w])(?:format(?:\.com)?\s+[a-z]:(?:\s|$)|format-volume\b(?=.*-driveletter))` },
|
|
95
|
-
{ id: '
|
|
96
|
-
pattern: String.raw `:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:` },
|
|
97
|
-
{ 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',
|
|
98
130
|
pattern: String.raw `\bmkfs(?:\.[a-z0-9]+)?\s+/dev/` },
|
|
99
|
-
{ id: '
|
|
100
|
-
pattern: String.raw `\
|
|
101
|
-
{ id: '
|
|
102
|
-
pattern: String.raw
|
|
103
|
-
{ 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',
|
|
104
136
|
pattern: String.raw `\bvssadmin\b(?=.*\bdelete\b)(?=.*\bshadows\b)` },
|
|
105
|
-
{ id: 'defender_disable', category: '
|
|
137
|
+
{ id: 'defender_disable', category: 'security_tampering', severity: 'high', reason: 'disabling Windows Defender real-time protection', source: 'builtin',
|
|
106
138
|
pattern: String.raw `\bset-mppreference\b(?=.*-disablerealtimemonitoring)(?=.*(?:\$true|\s1\b))` },
|
|
107
139
|
// Reverse shells / droppers
|
|
108
|
-
{ 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',
|
|
109
141
|
pattern: String.raw `\bbash\s+-i\b.*(?:/dev/tcp/)` },
|
|
110
|
-
{ 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',
|
|
111
143
|
pattern: String.raw `\b(?:nc|ncat|netcat)\b.*\s-e\s+(?:/bin/)?(?:sh|bash|cmd(?:\.exe)?|powershell(?:\.exe)?)\b` },
|
|
112
|
-
{ 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',
|
|
113
145
|
pattern: String.raw `\bsocat\b.*\bexec:(?:/bin/)?(?:sh|bash)\b` },
|
|
114
|
-
{ 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',
|
|
115
147
|
pattern: String.raw `\b(?:curl|wget)\b[^|;&]{0,300}\|\s*(?:sudo\s+)?(?:sh|bash)\b` },
|
|
116
|
-
{ id: '
|
|
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',
|
|
117
151
|
pattern: String.raw `(?=.*\b(?:iex\b|invoke-expression\b))(?=.*(?:downloadstring|net\.webclient|invoke-webrequest\b|\biwr\b|invoke-restmethod\b|\birm\b))` },
|
|
118
|
-
{ 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',
|
|
119
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)` },
|
|
120
158
|
// Cloud metadata / credential exfiltration
|
|
121
|
-
{ 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',
|
|
122
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))` },
|
|
123
|
-
{ 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',
|
|
124
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))` },
|
|
125
|
-
// Infra destroy
|
|
126
|
-
{ id: 'terraform_destroy_auto', category: 'infra_destroy', reason: 'terraform destroy with auto-approve',
|
|
127
|
-
|
|
128
|
-
{ id: 'pulumi_destroy_yes', category: 'infra_destroy', reason: 'pulumi destroy with auto-approve',
|
|
129
|
-
|
|
130
|
-
{ id: '
|
|
131
|
-
|
|
132
|
-
{ id: '
|
|
133
|
-
|
|
134
|
-
{ id: '
|
|
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',
|
|
135
185
|
pattern: String.raw `\bgcloud\s+projects\s+delete\b` },
|
|
136
|
-
{ 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',
|
|
137
187
|
pattern: String.raw `\bgcloud\s+sql\s+instances\s+delete\b` },
|
|
138
|
-
{ id: '
|
|
139
|
-
pattern: String.raw `\
|
|
140
|
-
{ 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',
|
|
141
191
|
pattern: String.raw `\baws\s+cloudformation\s+delete-stack\b` },
|
|
142
|
-
{ id: '
|
|
143
|
-
pattern: String.raw `\baws\s+iam\s+attach-user-policy\b(?=.*AdministratorAccess)` },
|
|
144
|
-
{ 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',
|
|
145
193
|
pattern: String.raw `\baws\s+iam\s+create-access-key\b` },
|
|
146
|
-
{ id: '
|
|
147
|
-
pattern: String.raw `\
|
|
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` },
|
|
148
228
|
// Destructive SQL typed straight into a terminal client
|
|
149
|
-
{ 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',
|
|
150
230
|
pattern: String.raw `\bdrop\s+database\b` },
|
|
151
|
-
{ 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',
|
|
152
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` },
|
|
153
237
|
];
|
|
154
238
|
function escapeDotNetRegex(literal) {
|
|
155
239
|
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -209,9 +293,11 @@ function cachedCustomRules() {
|
|
|
209
293
|
for (const block of blocks.slice(0, 100)) {
|
|
210
294
|
if (!block || typeof block.pattern !== 'string' || !block.pattern.trim())
|
|
211
295
|
continue;
|
|
296
|
+
const sev = String(block.severity || '').toLowerCase();
|
|
212
297
|
rules.push({
|
|
213
298
|
id: String(block.id || block.pattern).slice(0, 120),
|
|
214
299
|
category: String(block.categoryId || 'custom'),
|
|
300
|
+
severity: (sev === 'critical' || sev === 'high' || sev === 'medium' || sev === 'low') ? sev : 'high',
|
|
215
301
|
// Snapshot custom blocks are substring matches — escape into a literal regex.
|
|
216
302
|
pattern: escapeDotNetRegex(block.pattern.trim()),
|
|
217
303
|
reason: typeof block.explanation === 'string' && block.explanation ? block.explanation : `matched custom safety pattern "${block.pattern.trim()}"`,
|
|
@@ -284,6 +370,7 @@ function buildGuardPs1(nodePath, cliEntry) {
|
|
|
284
370
|
` [pscustomobject]@{`,
|
|
285
371
|
` Id = [string]$_.id`,
|
|
286
372
|
` Category = [string]$_.category`,
|
|
373
|
+
` Severity = [string]$_.severity`,
|
|
287
374
|
` Reason = [string]$_.reason`,
|
|
288
375
|
` Source = [string]$_.source`,
|
|
289
376
|
` Regex = [regex]::new([string]$_.pattern, 'IgnoreCase')`,
|
|
@@ -319,6 +406,7 @@ function buildGuardPs1(nodePath, cliEntry) {
|
|
|
319
406
|
` reason = ('Shell guard: ' + $Rule.Reason)`,
|
|
320
407
|
` ruleId = $Rule.Id`,
|
|
321
408
|
` category = $Rule.Category`,
|
|
409
|
+
` severity = $Rule.Severity`,
|
|
322
410
|
` source = $Rule.Source`,
|
|
323
411
|
` evidence = $evidence`,
|
|
324
412
|
` occurredAt = (Get-Date).ToUniversalTime().ToString('o')`,
|
|
@@ -347,14 +435,14 @@ function buildGuardPs1(nodePath, cliEntry) {
|
|
|
347
435
|
` if ($null -ne $hit) {`,
|
|
348
436
|
` if ($global:FcdGuardMode -eq 'monitor') {`,
|
|
349
437
|
` Write-Host ''`,
|
|
350
|
-
` Write-Host ('[FullCourtDefense] monitor: would block
|
|
438
|
+
` Write-Host ('[FullCourtDefense] monitor: would block [' + $hit.Severity + '] ' + $hit.Reason + ' (rule ' + $hit.Id + ')') -ForegroundColor Yellow`,
|
|
351
439
|
` Write-FcdGuardEvent -Rule $hit -CommandLine $line -Decision 'allow'`,
|
|
352
440
|
` [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()`,
|
|
353
441
|
` return`,
|
|
354
442
|
` }`,
|
|
355
443
|
` [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()`,
|
|
356
444
|
` Write-Host ''`,
|
|
357
|
-
` 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`,
|
|
358
446
|
` Write-Host 'This command was not executed. Reported to your security dashboard.' -ForegroundColor DarkGray`,
|
|
359
447
|
` Write-FcdGuardEvent -Rule $hit -CommandLine $line -Decision 'block'`,
|
|
360
448
|
` [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()`,
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,8 @@ const installAll_1 = require("./commands/installAll");
|
|
|
52
52
|
const autoProtect_1 = require("./commands/autoProtect");
|
|
53
53
|
const windowsAudit_1 = require("./commands/windowsAudit");
|
|
54
54
|
const shellGuard_1 = require("./commands/shellGuard");
|
|
55
|
+
const cmdGuard_1 = require("./commands/cmdGuard");
|
|
56
|
+
const posixShellGuard_1 = require("./commands/posixShellGuard");
|
|
55
57
|
const fs = __importStar(require("fs"));
|
|
56
58
|
const path = __importStar(require("path"));
|
|
57
59
|
function readCliVersion() {
|
|
@@ -127,9 +129,11 @@ function printHelp() {
|
|
|
127
129
|
install Alias for install-all.
|
|
128
130
|
windows-audit Show Windows PowerShell audit coverage (ScriptBlock Logging +
|
|
129
131
|
Transcription). Use --enable to turn it on (one admin prompt).
|
|
130
|
-
install-shell-guard Block dangerous commands typed in
|
|
131
|
-
|
|
132
|
-
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.
|
|
135
|
+
install-cmd-guard Block dangerous commands typed in cmd.exe (Command Prompt).
|
|
136
|
+
uninstall-cmd-guard removes it; cmd-guard-status shows coverage.
|
|
133
137
|
scan Runs the scan. Use --local for inside-organization scans.
|
|
134
138
|
discover Finds MCP servers, local secrets, agent rules/skills, and machine
|
|
135
139
|
blast radius on this laptop. Use --surface all --upload for AI Fleet.
|
|
@@ -600,6 +604,9 @@ async function main() {
|
|
|
600
604
|
autoProtect: flags['auto-protect'],
|
|
601
605
|
intervalMinutes: flags['interval-minutes'],
|
|
602
606
|
windowsAudit: flags['windows-audit'],
|
|
607
|
+
shellGuard: flags['shell-guard'],
|
|
608
|
+
cmdGuard: flags['cmd-guard'],
|
|
609
|
+
posixGuard: flags['posix-guard'],
|
|
603
610
|
};
|
|
604
611
|
await (0, installAll_1.installAllCommand)(args, config);
|
|
605
612
|
break;
|
|
@@ -610,23 +617,62 @@ async function main() {
|
|
|
610
617
|
break;
|
|
611
618
|
}
|
|
612
619
|
case 'install-shell-guard': {
|
|
613
|
-
|
|
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
|
+
}
|
|
614
626
|
break;
|
|
615
627
|
}
|
|
616
628
|
case 'uninstall-shell-guard': {
|
|
617
|
-
|
|
629
|
+
if (process.platform === 'win32') {
|
|
630
|
+
await (0, shellGuard_1.uninstallShellGuardCommand)();
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
await (0, posixShellGuard_1.uninstallPosixShellGuardCommand)();
|
|
634
|
+
}
|
|
618
635
|
break;
|
|
619
636
|
}
|
|
620
637
|
case 'shell-guard-status': {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
console.log(
|
|
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('');
|
|
624
647
|
break;
|
|
625
648
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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`);
|
|
653
|
+
}
|
|
654
|
+
if (status.rulesPresent)
|
|
655
|
+
console.log(` Rules: ${status.ruleCount} (mode: ${status.mode || 'block'})`);
|
|
656
|
+
console.log('');
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case 'install-cmd-guard': {
|
|
660
|
+
await (0, cmdGuard_1.installCmdGuardCommand)();
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
case 'uninstall-cmd-guard': {
|
|
664
|
+
await (0, cmdGuard_1.uninstallCmdGuardCommand)();
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case 'cmd-guard-status': {
|
|
668
|
+
const status = (0, cmdGuard_1.getCmdGuardStatus)();
|
|
669
|
+
if (!status.supported) {
|
|
670
|
+
console.log('cmd guard supports Windows Command Prompt only.');
|
|
671
|
+
break;
|
|
629
672
|
}
|
|
673
|
+
console.log(`\nInteractive cmd.exe shell guard: ${status.installed ? '\x1b[32minstalled\x1b[0m' : '\x1b[31mnot installed\x1b[0m'}`);
|
|
674
|
+
if (status.autorunValue)
|
|
675
|
+
console.log(` AutoRun: \x1b[2m${status.autorunValue}\x1b[0m`);
|
|
630
676
|
if (status.rulesPresent)
|
|
631
677
|
console.log(` Rules: ${status.ruleCount} (mode: ${status.mode || 'block'})`);
|
|
632
678
|
console.log('');
|
package/dist/telemetry.d.ts
CHANGED
package/dist/telemetry.js
CHANGED
|
@@ -44,6 +44,8 @@ const path = __importStar(require("path"));
|
|
|
44
44
|
const machineIdentity_1 = require("./machineIdentity");
|
|
45
45
|
const windowsAudit_1 = require("./commands/windowsAudit");
|
|
46
46
|
const shellGuard_1 = require("./commands/shellGuard");
|
|
47
|
+
const cmdGuard_1 = require("./commands/cmdGuard");
|
|
48
|
+
const posixShellGuard_1 = require("./commands/posixShellGuard");
|
|
47
49
|
/**
|
|
48
50
|
* Local-first telemetry: every enforcement decision is appended to an on-disk
|
|
49
51
|
* spool (instant, offline-safe) and flushed to the backend in batches. Critical
|
|
@@ -71,6 +73,7 @@ function spoolEvent(event) {
|
|
|
71
73
|
category: event.category,
|
|
72
74
|
categoryId: event.categoryId,
|
|
73
75
|
itemId: event.itemId,
|
|
76
|
+
severity: event.severity,
|
|
74
77
|
source: event.source,
|
|
75
78
|
evidence: event.evidence,
|
|
76
79
|
explanation: event.explanation,
|
|
@@ -110,9 +113,11 @@ function rewriteSpool(remaining) {
|
|
|
110
113
|
}
|
|
111
114
|
/** Drain the spool to the backend in one batch (+ optional heartbeat). Returns accepted count. */
|
|
112
115
|
async function flushSpool(input) {
|
|
113
|
-
// Piggyback on the flush cycle to re-sync
|
|
114
|
-
//
|
|
116
|
+
// Piggyback on the flush cycle to re-sync terminal guard rule caches (mode flips
|
|
117
|
+
// + new custom rules reach open shells within ~30s).
|
|
115
118
|
(0, shellGuard_1.refreshShellGuardRules)();
|
|
119
|
+
(0, cmdGuard_1.refreshCmdGuardRules)();
|
|
120
|
+
(0, posixShellGuard_1.refreshPosixShellGuardRules)();
|
|
116
121
|
const all = readSpool();
|
|
117
122
|
const batch = all.slice(0, MAX_BATCH);
|
|
118
123
|
const overflow = all.slice(MAX_BATCH);
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.
|
|
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": {
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"test:taint-ledger": "npm run build && node scripts/test-taint-ledger.js",
|
|
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
|
+
"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",
|
|
21
23
|
"test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
|
|
22
24
|
"prepublishOnly": "npm run build"
|
|
23
25
|
},
|