fullcourtdefense-cli 1.9.0 → 1.11.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/shellGuard.d.ts +46 -0
- package/dist/commands/shellGuard.js +554 -0
- package/dist/commands/windowsAudit.d.ts +2 -0
- package/dist/commands/windowsAudit.js +2 -0
- package/dist/index.js +54 -0
- package/dist/telemetry.js +6 -0
- 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,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,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.reason+' (rule '+hit.id+')');spoolEvent(hit,line,'allow');delegate(args);}`,
|
|
106
|
+
`console.error('[FullCourtDefense] BLOCKED: '+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
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface InstallAllArgs extends ProtectAllArgs {
|
|
|
11
11
|
autoProtect?: string;
|
|
12
12
|
intervalMinutes?: string;
|
|
13
13
|
windowsAudit?: string;
|
|
14
|
+
shellGuard?: string;
|
|
15
|
+
cmdGuard?: string;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* One-click install: discover/wrap every existing MCP server, install Cursor
|
|
@@ -10,6 +10,8 @@ const mcpGateway_1 = require("./mcpGateway");
|
|
|
10
10
|
const autoProtect_1 = require("./autoProtect");
|
|
11
11
|
const restartNotice_1 = require("./restartNotice");
|
|
12
12
|
const windowsAudit_1 = require("./windowsAudit");
|
|
13
|
+
const shellGuard_1 = require("./shellGuard");
|
|
14
|
+
const cmdGuard_1 = require("./cmdGuard");
|
|
13
15
|
/**
|
|
14
16
|
* One-click install: discover/wrap every existing MCP server, install Cursor
|
|
15
17
|
* hooks for built-in Cursor actions, optional self-heal and fleet upload.
|
|
@@ -93,6 +95,28 @@ async function installAllCommand(args, config) {
|
|
|
93
95
|
console.log(`\x1b[33m⚠ Warning: PowerShell audit logging step failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
|
|
94
96
|
}
|
|
95
97
|
}
|
|
98
|
+
// Interactive PowerShell guard — blocks dangerous commands TYPED in a
|
|
99
|
+
// terminal (inside or outside the IDE) using the same rule families the IDE
|
|
100
|
+
// hooks enforce, from the local cache (no admin needed, no network hot path).
|
|
101
|
+
if (process.platform === 'win32' && args.shellGuard !== 'false') {
|
|
102
|
+
console.log('\n\x1b[1mInstalling interactive PowerShell shell guard…\x1b[0m');
|
|
103
|
+
try {
|
|
104
|
+
await (0, shellGuard_1.installShellGuardCommand)();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.log(`\x1b[33m⚠ Warning: shell guard install failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Interactive cmd.exe guard — same rules, doskey + AutoRun hook (no admin).
|
|
111
|
+
if (process.platform === 'win32' && args.cmdGuard !== 'false') {
|
|
112
|
+
console.log('\n\x1b[1mInstalling interactive cmd.exe shell guard…\x1b[0m');
|
|
113
|
+
try {
|
|
114
|
+
await (0, cmdGuard_1.installCmdGuardCommand)();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.log(`\x1b[33m⚠ Warning: cmd guard install failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
96
120
|
if (args.autoProtect === 'true') {
|
|
97
121
|
console.log('\n\x1b[1mScheduling self-healing MCP protection…\x1b[0m');
|
|
98
122
|
await (0, autoProtect_1.autoProtectCommand)({
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ShellGuardRule {
|
|
2
|
+
id: string;
|
|
3
|
+
category: string;
|
|
4
|
+
/** .NET-compatible regex, matched with IgnoreCase against the typed line. */
|
|
5
|
+
pattern: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
source: 'builtin' | 'custom';
|
|
8
|
+
}
|
|
9
|
+
interface ShellGuardRulesFile {
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
mode: 'block' | 'monitor';
|
|
12
|
+
rules: ShellGuardRule[];
|
|
13
|
+
}
|
|
14
|
+
/** Write (or rewrite) the ruleset the profile guard loads at shell startup. */
|
|
15
|
+
export declare function writeShellGuardRules(): ShellGuardRulesFile;
|
|
16
|
+
/**
|
|
17
|
+
* Opportunistic rules/mode refresh — called from the telemetry flush cycle so a
|
|
18
|
+
* console mode flip or new custom rule reaches interactive shells within ~30s.
|
|
19
|
+
* No-op unless the guard is installed. Never throws.
|
|
20
|
+
*/
|
|
21
|
+
export declare function refreshShellGuardRules(): void;
|
|
22
|
+
export interface ShellGuardStatus {
|
|
23
|
+
supported: boolean;
|
|
24
|
+
installed: boolean;
|
|
25
|
+
profiles: Array<{
|
|
26
|
+
engine: string;
|
|
27
|
+
profilePath: string;
|
|
28
|
+
installed: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
rulesPresent: boolean;
|
|
31
|
+
ruleCount?: number;
|
|
32
|
+
mode?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Probe whether the profile guard is installed. Read-only, never throws. */
|
|
35
|
+
export declare function getShellGuardStatus(): ShellGuardStatus;
|
|
36
|
+
/** Lightweight boolean for the telemetry heartbeat / discovery report. */
|
|
37
|
+
export declare function isShellGuardInstalled(): boolean;
|
|
38
|
+
export interface InstallShellGuardArgs {
|
|
39
|
+
/** 'false' to skip writing profile entries (rules + script only). */
|
|
40
|
+
profiles?: string;
|
|
41
|
+
}
|
|
42
|
+
/** `fullcourtdefense install-shell-guard` — enable the interactive PowerShell guard. */
|
|
43
|
+
export declare function installShellGuardCommand(args?: InstallShellGuardArgs): Promise<void>;
|
|
44
|
+
/** `fullcourtdefense uninstall-shell-guard` — remove the guard from all profiles. */
|
|
45
|
+
export declare function uninstallShellGuardCommand(): Promise<void>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,554 @@
|
|
|
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.writeShellGuardRules = writeShellGuardRules;
|
|
37
|
+
exports.refreshShellGuardRules = refreshShellGuardRules;
|
|
38
|
+
exports.getShellGuardStatus = getShellGuardStatus;
|
|
39
|
+
exports.isShellGuardInstalled = isShellGuardInstalled;
|
|
40
|
+
exports.installShellGuardCommand = installShellGuardCommand;
|
|
41
|
+
exports.uninstallShellGuardCommand = uninstallShellGuardCommand;
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
/**
|
|
47
|
+
* Interactive PowerShell guard — real-time blocking of dangerous commands
|
|
48
|
+
* TYPED by a human in a terminal (inside or outside the IDE).
|
|
49
|
+
*
|
|
50
|
+
* How it works:
|
|
51
|
+
* - The CLI writes a ruleset JSON (~/.fullcourtdefense-shell-guard.json):
|
|
52
|
+
* the same dangerous-command families the deterministic guard enforces on
|
|
53
|
+
* agent tool calls (destructive filesystem, reverse shells, infra destroy,
|
|
54
|
+
* credential exfiltration, …) plus the org's custom block patterns from the
|
|
55
|
+
* cached local-safety snapshot, plus the Shield mode (block vs monitor).
|
|
56
|
+
* - A profile script (~/.fullcourtdefense-shell-guard.ps1) is dot-sourced from
|
|
57
|
+
* the user's PowerShell profiles (both Windows PowerShell 5.1 and pwsh 7).
|
|
58
|
+
* It compiles the rules ONCE at shell startup and hooks the PSReadLine
|
|
59
|
+
* Enter key: every submitted line is checked in-process (sub-millisecond —
|
|
60
|
+
* no node/network on the hot path).
|
|
61
|
+
* - Match in block mode → the line is cleared and never executes; monitor
|
|
62
|
+
* mode → yellow warning, the command still runs. Either way the event is
|
|
63
|
+
* appended to the telemetry spool and a detached flush reports it to the
|
|
64
|
+
* fleet dashboard.
|
|
65
|
+
* - Rules/mode refresh opportunistically on every telemetry flush (offline
|
|
66
|
+
* caches only — never blocks a shell).
|
|
67
|
+
*
|
|
68
|
+
* Scope honesty: this covers INTERACTIVE PowerShell (PSReadLine hosts, incl.
|
|
69
|
+
* IDE-integrated terminals). Non-interactive scripts are covered by ScriptBlock
|
|
70
|
+
* Logging (windowsAudit.ts). Native cmd.exe is covered separately (cmdGuard.ts).
|
|
71
|
+
*/
|
|
72
|
+
const GUARD_RULES_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.json');
|
|
73
|
+
const GUARD_PS1_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.ps1');
|
|
74
|
+
const SPOOL_PATH = path.join(os.homedir(), '.fullcourtdefense-spool.jsonl');
|
|
75
|
+
const RUNTIME_CACHE_PATH = path.join(os.homedir(), '.fullcourtdefense-runtime.json');
|
|
76
|
+
const SNAPSHOT_CACHE_DIR = path.join(os.homedir(), '.fullcourtdefense');
|
|
77
|
+
const MARKER_START = '# >>> fullcourtdefense shell guard >>>';
|
|
78
|
+
const MARKER_END = '# <<< fullcourtdefense shell guard <<<';
|
|
79
|
+
/**
|
|
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.
|
|
84
|
+
*/
|
|
85
|
+
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',
|
|
96
|
+
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',
|
|
100
|
+
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',
|
|
106
|
+
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',
|
|
108
|
+
pattern: String.raw `\bset-mppreference\b(?=.*-disablerealtimemonitoring)(?=.*(?:\$true|\s1\b))` },
|
|
109
|
+
// Reverse shells / droppers
|
|
110
|
+
{ id: 'bash_dev_tcp', category: 'reverse_shell', reason: 'bash reverse shell', source: 'builtin',
|
|
111
|
+
pattern: String.raw `\bbash\s+-i\b.*(?:/dev/tcp/)` },
|
|
112
|
+
{ id: 'netcat_exec', category: 'reverse_shell', reason: 'netcat reverse shell', source: 'builtin',
|
|
113
|
+
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',
|
|
115
|
+
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',
|
|
117
|
+
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',
|
|
119
|
+
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',
|
|
121
|
+
pattern: String.raw `\bpython(?:3)?\s+-c\b(?=.*socket)(?=.*subprocess)` },
|
|
122
|
+
// Cloud metadata / credential exfiltration
|
|
123
|
+
{ id: 'aws_gcp_metadata_ip', category: 'metadata_ssrf', reason: 'request to cloud metadata endpoint', source: 'builtin',
|
|
124
|
+
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',
|
|
126
|
+
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',
|
|
137
|
+
pattern: String.raw `\bgcloud\s+projects\s+delete\b` },
|
|
138
|
+
{ id: 'gcloud_sql_delete', category: 'infra_destroy', reason: 'GCP SQL instance deletion', source: 'builtin',
|
|
139
|
+
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',
|
|
143
|
+
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',
|
|
147
|
+
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))` },
|
|
150
|
+
// Destructive SQL typed straight into a terminal client
|
|
151
|
+
{ id: 'drop_database', category: 'destructive_sql', reason: 'DROP DATABASE', source: 'builtin',
|
|
152
|
+
pattern: String.raw `\bdrop\s+database\b` },
|
|
153
|
+
{ id: 'drop_schema', category: 'destructive_sql', reason: 'DROP SCHEMA', source: 'builtin',
|
|
154
|
+
pattern: String.raw `\bdrop\s+schema\b` },
|
|
155
|
+
];
|
|
156
|
+
function escapeDotNetRegex(literal) {
|
|
157
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
158
|
+
}
|
|
159
|
+
/** Cached Shield mode from the runtime bundle cache (offline read, never network). */
|
|
160
|
+
function cachedMode() {
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(fs.readFileSync(RUNTIME_CACHE_PATH, 'utf8'));
|
|
163
|
+
const entries = Object.values(parsed).filter(e => e && typeof e === 'object');
|
|
164
|
+
entries.sort((a, b) => (b.fetchedAt || 0) - (a.fetchedAt || 0));
|
|
165
|
+
const mode = entries[0]?.mode;
|
|
166
|
+
return mode === 'monitor' || mode === 'shadow' ? 'monitor' : 'block';
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return 'block';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Built-in rule ids toggled OFF in the dashboard (local-safety snapshot cache).
|
|
174
|
+
* Rule ids match the deterministic guard's item ids, so one console toggle
|
|
175
|
+
* disables the rule in IDE hooks AND interactive terminals.
|
|
176
|
+
*/
|
|
177
|
+
function cachedDisabledItemIds() {
|
|
178
|
+
const disabled = new Set();
|
|
179
|
+
try {
|
|
180
|
+
for (const file of fs.readdirSync(SNAPSHOT_CACHE_DIR)) {
|
|
181
|
+
if (!file.startsWith('local-safety-') || !file.endsWith('.json'))
|
|
182
|
+
continue;
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(SNAPSHOT_CACHE_DIR, file), 'utf8'));
|
|
185
|
+
const ids = parsed?.snapshot?.disabledBuiltInItemIds;
|
|
186
|
+
if (Array.isArray(ids)) {
|
|
187
|
+
for (const id of ids) {
|
|
188
|
+
if (typeof id === 'string' && id)
|
|
189
|
+
disabled.add(id);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch { /* skip malformed snapshot */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch { /* no snapshot cache yet */ }
|
|
197
|
+
return disabled;
|
|
198
|
+
}
|
|
199
|
+
/** Org custom block patterns from the cached local-safety snapshots (offline read). */
|
|
200
|
+
function cachedCustomRules() {
|
|
201
|
+
const rules = [];
|
|
202
|
+
try {
|
|
203
|
+
for (const file of fs.readdirSync(SNAPSHOT_CACHE_DIR)) {
|
|
204
|
+
if (!file.startsWith('local-safety-') || !file.endsWith('.json'))
|
|
205
|
+
continue;
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(SNAPSHOT_CACHE_DIR, file), 'utf8'));
|
|
208
|
+
const blocks = parsed?.snapshot?.customBlocks;
|
|
209
|
+
if (!Array.isArray(blocks))
|
|
210
|
+
continue;
|
|
211
|
+
for (const block of blocks.slice(0, 100)) {
|
|
212
|
+
if (!block || typeof block.pattern !== 'string' || !block.pattern.trim())
|
|
213
|
+
continue;
|
|
214
|
+
rules.push({
|
|
215
|
+
id: String(block.id || block.pattern).slice(0, 120),
|
|
216
|
+
category: String(block.categoryId || 'custom'),
|
|
217
|
+
// Snapshot custom blocks are substring matches — escape into a literal regex.
|
|
218
|
+
pattern: escapeDotNetRegex(block.pattern.trim()),
|
|
219
|
+
reason: typeof block.explanation === 'string' && block.explanation ? block.explanation : `matched custom safety pattern "${block.pattern.trim()}"`,
|
|
220
|
+
source: 'custom',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch { /* skip malformed snapshot */ }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch { /* no snapshot cache yet */ }
|
|
228
|
+
// Dedupe by id+pattern.
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
return rules.filter(rule => {
|
|
231
|
+
const key = `${rule.id}|${rule.pattern}`;
|
|
232
|
+
if (seen.has(key))
|
|
233
|
+
return false;
|
|
234
|
+
seen.add(key);
|
|
235
|
+
return true;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Write (or rewrite) the ruleset the profile guard loads at shell startup. */
|
|
239
|
+
function writeShellGuardRules() {
|
|
240
|
+
const disabled = cachedDisabledItemIds();
|
|
241
|
+
const data = {
|
|
242
|
+
updatedAt: new Date().toISOString(),
|
|
243
|
+
mode: cachedMode(),
|
|
244
|
+
rules: [...BUILTIN_RULES.filter(rule => !disabled.has(rule.id)), ...cachedCustomRules()],
|
|
245
|
+
};
|
|
246
|
+
fs.writeFileSync(GUARD_RULES_PATH, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
247
|
+
return data;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Opportunistic rules/mode refresh — called from the telemetry flush cycle so a
|
|
251
|
+
* console mode flip or new custom rule reaches interactive shells within ~30s.
|
|
252
|
+
* No-op unless the guard is installed. Never throws.
|
|
253
|
+
*/
|
|
254
|
+
function refreshShellGuardRules() {
|
|
255
|
+
try {
|
|
256
|
+
if (!fs.existsSync(GUARD_PS1_PATH))
|
|
257
|
+
return;
|
|
258
|
+
writeShellGuardRules();
|
|
259
|
+
}
|
|
260
|
+
catch { /* best-effort */ }
|
|
261
|
+
}
|
|
262
|
+
function psQuote(value) {
|
|
263
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
264
|
+
}
|
|
265
|
+
/** The profile guard script. Placeholders are substituted at install time. */
|
|
266
|
+
function buildGuardPs1(nodePath, cliEntry) {
|
|
267
|
+
const lines = [
|
|
268
|
+
`# FullCourtDefense interactive shell guard (installed by fullcourtdefense-cli).`,
|
|
269
|
+
`# Blocks dangerous commands typed in PowerShell using the org's cached rules.`,
|
|
270
|
+
`# Disable for one session: $env:FCD_SHELL_GUARD = 'off'`,
|
|
271
|
+
`# Remove permanently: fullcourtdefense uninstall-shell-guard`,
|
|
272
|
+
``,
|
|
273
|
+
`$global:FcdRulesPath = ${psQuote(GUARD_RULES_PATH)}`,
|
|
274
|
+
`$global:FcdSpoolPath = ${psQuote(SPOOL_PATH)}`,
|
|
275
|
+
`$global:FcdNodePath = ${psQuote(nodePath)}`,
|
|
276
|
+
`$global:FcdCliEntry = ${psQuote(cliEntry)}`,
|
|
277
|
+
`$global:FcdGuardMode = 'block'`,
|
|
278
|
+
`$global:FcdGuardRules = @()`,
|
|
279
|
+
``,
|
|
280
|
+
`try {`,
|
|
281
|
+
` if (Test-Path $global:FcdRulesPath) {`,
|
|
282
|
+
` $fcdRaw = Get-Content -Raw -Path $global:FcdRulesPath | ConvertFrom-Json`,
|
|
283
|
+
` if ($fcdRaw.mode -eq 'monitor') { $global:FcdGuardMode = 'monitor' }`,
|
|
284
|
+
` $global:FcdGuardRules = @($fcdRaw.rules | ForEach-Object {`,
|
|
285
|
+
` try {`,
|
|
286
|
+
` [pscustomobject]@{`,
|
|
287
|
+
` Id = [string]$_.id`,
|
|
288
|
+
` Category = [string]$_.category`,
|
|
289
|
+
` Reason = [string]$_.reason`,
|
|
290
|
+
` Source = [string]$_.source`,
|
|
291
|
+
` Regex = [regex]::new([string]$_.pattern, 'IgnoreCase')`,
|
|
292
|
+
` }`,
|
|
293
|
+
` } catch { $null }`,
|
|
294
|
+
` } | Where-Object { $null -ne $_ })`,
|
|
295
|
+
` }`,
|
|
296
|
+
`} catch { $global:FcdGuardRules = @() }`,
|
|
297
|
+
``,
|
|
298
|
+
`function global:Test-FcdShellGuard {`,
|
|
299
|
+
` param([string]$CommandLine)`,
|
|
300
|
+
` if ([string]::IsNullOrWhiteSpace($CommandLine)) { return $null }`,
|
|
301
|
+
` $normalized = ($CommandLine -replace '\\s+', ' ').Trim()`,
|
|
302
|
+
` foreach ($rule in $global:FcdGuardRules) {`,
|
|
303
|
+
` try {`,
|
|
304
|
+
` if ($rule.Regex.IsMatch($normalized) -or $rule.Regex.IsMatch($CommandLine)) { return $rule }`,
|
|
305
|
+
` } catch { }`,
|
|
306
|
+
` }`,
|
|
307
|
+
` return $null`,
|
|
308
|
+
`}`,
|
|
309
|
+
``,
|
|
310
|
+
`function global:Write-FcdGuardEvent {`,
|
|
311
|
+
` param($Rule, [string]$CommandLine, [string]$Decision)`,
|
|
312
|
+
` try {`,
|
|
313
|
+
` $evidence = $CommandLine.Trim()`,
|
|
314
|
+
` if ($evidence.Length -gt 180) { $evidence = $evidence.Substring(0, 180) + '...' }`,
|
|
315
|
+
` $event = [pscustomobject]@{`,
|
|
316
|
+
` eventId = [guid]::NewGuid().ToString()`,
|
|
317
|
+
` type = 'verdict'`,
|
|
318
|
+
` decision = $Decision`,
|
|
319
|
+
` toolName = 'powershell_terminal'`,
|
|
320
|
+
` operation = 'shell_command'`,
|
|
321
|
+
` reason = ('Shell guard: ' + $Rule.Reason)`,
|
|
322
|
+
` ruleId = $Rule.Id`,
|
|
323
|
+
` category = $Rule.Category`,
|
|
324
|
+
` source = $Rule.Source`,
|
|
325
|
+
` evidence = $evidence`,
|
|
326
|
+
` occurredAt = (Get-Date).ToUniversalTime().ToString('o')`,
|
|
327
|
+
` }`,
|
|
328
|
+
` # BOM-free UTF8 append — PS5's Add-Content -Encoding utf8 writes a BOM that breaks JSONL parsing.`,
|
|
329
|
+
` [System.IO.File]::AppendAllText($global:FcdSpoolPath, (($event | ConvertTo-Json -Compress) + "\`n"), (New-Object System.Text.UTF8Encoding($false)))`,
|
|
330
|
+
` if ((Test-Path $global:FcdNodePath) -and (Test-Path $global:FcdCliEntry)) {`,
|
|
331
|
+
` Start-Process -FilePath $global:FcdNodePath -ArgumentList @($global:FcdCliEntry, 'flush-spool', '--heartbeat', 'true') -WindowStyle Hidden -ErrorAction SilentlyContinue | Out-Null`,
|
|
332
|
+
` }`,
|
|
333
|
+
` } catch { }`,
|
|
334
|
+
`}`,
|
|
335
|
+
``,
|
|
336
|
+
`# Hook the Enter key only in interactive PSReadLine hosts (consoles, IDE terminals).`,
|
|
337
|
+
`# try/catch: in non-interactive hosts PSReadLine can be present but uninitialized.`,
|
|
338
|
+
`try {`,
|
|
339
|
+
`if ($env:FCD_SHELL_GUARD -ne 'off' -and (Get-Command Set-PSReadLineKeyHandler -ErrorAction SilentlyContinue)) {`,
|
|
340
|
+
` Set-PSReadLineKeyHandler -Key Enter -BriefDescription 'FCDShellGuard' -Description 'FullCourtDefense dangerous-command guard' -ScriptBlock {`,
|
|
341
|
+
` param($key, $arg)`,
|
|
342
|
+
` $line = $null`,
|
|
343
|
+
` $cursor = $null`,
|
|
344
|
+
` try { [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) } catch { }`,
|
|
345
|
+
` $hit = $null`,
|
|
346
|
+
` if ($line -and $env:FCD_SHELL_GUARD -ne 'off') {`,
|
|
347
|
+
` try { $hit = Test-FcdShellGuard -CommandLine $line } catch { $hit = $null }`,
|
|
348
|
+
` }`,
|
|
349
|
+
` if ($null -ne $hit) {`,
|
|
350
|
+
` if ($global:FcdGuardMode -eq 'monitor') {`,
|
|
351
|
+
` Write-Host ''`,
|
|
352
|
+
` Write-Host ('[FullCourtDefense] monitor: would block - ' + $hit.Reason + ' (rule ' + $hit.Id + ')') -ForegroundColor Yellow`,
|
|
353
|
+
` Write-FcdGuardEvent -Rule $hit -CommandLine $line -Decision 'allow'`,
|
|
354
|
+
` [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()`,
|
|
355
|
+
` return`,
|
|
356
|
+
` }`,
|
|
357
|
+
` [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()`,
|
|
358
|
+
` Write-Host ''`,
|
|
359
|
+
` Write-Host ('[FullCourtDefense] BLOCKED: ' + $hit.Reason + ' (rule ' + $hit.Id + ')') -ForegroundColor Red`,
|
|
360
|
+
` Write-Host 'This command was not executed. Reported to your security dashboard.' -ForegroundColor DarkGray`,
|
|
361
|
+
` Write-FcdGuardEvent -Rule $hit -CommandLine $line -Decision 'block'`,
|
|
362
|
+
` [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()`,
|
|
363
|
+
` return`,
|
|
364
|
+
` }`,
|
|
365
|
+
` [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()`,
|
|
366
|
+
` }`,
|
|
367
|
+
`}`,
|
|
368
|
+
`} catch { }`,
|
|
369
|
+
``,
|
|
370
|
+
];
|
|
371
|
+
return lines.join('\r\n');
|
|
372
|
+
}
|
|
373
|
+
/** Resolve each PowerShell engine's CurrentUserAllHosts profile path. */
|
|
374
|
+
function resolveProfilePaths() {
|
|
375
|
+
const out = [];
|
|
376
|
+
for (const engine of ['powershell.exe', 'pwsh.exe']) {
|
|
377
|
+
try {
|
|
378
|
+
const result = (0, child_process_1.spawnSync)(engine, ['-NoProfile', '-NonInteractive', '-Command', 'Write-Output $PROFILE.CurrentUserAllHosts'], {
|
|
379
|
+
encoding: 'utf8', windowsHide: true, timeout: 15000,
|
|
380
|
+
});
|
|
381
|
+
const profilePath = (result.stdout || '').trim().split(/\r?\n/).pop()?.trim();
|
|
382
|
+
if (result.status === 0 && profilePath && /\.ps1$/i.test(profilePath)) {
|
|
383
|
+
out.push({ engine: engine.replace(/\.exe$/i, ''), profilePath });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch { /* engine not installed */ }
|
|
387
|
+
}
|
|
388
|
+
// Dedupe (both engines can share a profile only if paths match — normally they differ).
|
|
389
|
+
const seen = new Set();
|
|
390
|
+
return out.filter(entry => {
|
|
391
|
+
const key = entry.profilePath.toLowerCase();
|
|
392
|
+
if (seen.has(key))
|
|
393
|
+
return false;
|
|
394
|
+
seen.add(key);
|
|
395
|
+
return true;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function profileSnippet() {
|
|
399
|
+
return [
|
|
400
|
+
MARKER_START,
|
|
401
|
+
`# Loads the FullCourtDefense dangerous-command guard for interactive sessions.`,
|
|
402
|
+
`if (Test-Path ${psQuote(GUARD_PS1_PATH)}) { . ${psQuote(GUARD_PS1_PATH)} }`,
|
|
403
|
+
MARKER_END,
|
|
404
|
+
].join('\r\n');
|
|
405
|
+
}
|
|
406
|
+
function addSnippetToProfile(profilePath) {
|
|
407
|
+
fs.mkdirSync(path.dirname(profilePath), { recursive: true });
|
|
408
|
+
let content = '';
|
|
409
|
+
try {
|
|
410
|
+
content = fs.readFileSync(profilePath, 'utf8');
|
|
411
|
+
}
|
|
412
|
+
catch { /* new profile */ }
|
|
413
|
+
if (content.includes(MARKER_START))
|
|
414
|
+
return 'already';
|
|
415
|
+
const sep = content && !content.endsWith('\n') ? '\r\n\r\n' : content ? '\r\n' : '';
|
|
416
|
+
fs.writeFileSync(profilePath, content + sep + profileSnippet() + '\r\n', 'utf8');
|
|
417
|
+
return 'installed';
|
|
418
|
+
}
|
|
419
|
+
function removeSnippetFromProfile(profilePath) {
|
|
420
|
+
try {
|
|
421
|
+
const content = fs.readFileSync(profilePath, 'utf8');
|
|
422
|
+
const start = content.indexOf(MARKER_START);
|
|
423
|
+
const end = content.indexOf(MARKER_END);
|
|
424
|
+
if (start === -1 || end === -1)
|
|
425
|
+
return false;
|
|
426
|
+
const cleaned = (content.slice(0, start) + content.slice(end + MARKER_END.length)).replace(/(\r?\n){3,}/g, '\r\n\r\n');
|
|
427
|
+
fs.writeFileSync(profilePath, cleaned, 'utf8');
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Probe whether the profile guard is installed. Read-only, never throws. */
|
|
435
|
+
function getShellGuardStatus() {
|
|
436
|
+
if (process.platform !== 'win32') {
|
|
437
|
+
return { supported: false, installed: false, profiles: [], rulesPresent: false };
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const profiles = resolveProfilePaths().map(entry => {
|
|
441
|
+
let installed = false;
|
|
442
|
+
try {
|
|
443
|
+
installed = fs.readFileSync(entry.profilePath, 'utf8').includes(MARKER_START);
|
|
444
|
+
}
|
|
445
|
+
catch { /* no profile */ }
|
|
446
|
+
return { ...entry, installed };
|
|
447
|
+
});
|
|
448
|
+
let ruleCount;
|
|
449
|
+
let mode;
|
|
450
|
+
let rulesPresent = false;
|
|
451
|
+
try {
|
|
452
|
+
const parsed = JSON.parse(fs.readFileSync(GUARD_RULES_PATH, 'utf8'));
|
|
453
|
+
rulesPresent = Array.isArray(parsed?.rules) && parsed.rules.length > 0;
|
|
454
|
+
ruleCount = Array.isArray(parsed?.rules) ? parsed.rules.length : undefined;
|
|
455
|
+
mode = typeof parsed?.mode === 'string' ? parsed.mode : undefined;
|
|
456
|
+
}
|
|
457
|
+
catch { /* not written yet */ }
|
|
458
|
+
return {
|
|
459
|
+
supported: true,
|
|
460
|
+
installed: profiles.some(p => p.installed) && fs.existsSync(GUARD_PS1_PATH),
|
|
461
|
+
profiles,
|
|
462
|
+
rulesPresent,
|
|
463
|
+
ruleCount,
|
|
464
|
+
mode,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return { supported: true, installed: false, profiles: [], rulesPresent: false };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/** Lightweight boolean for the telemetry heartbeat / discovery report. */
|
|
472
|
+
function isShellGuardInstalled() {
|
|
473
|
+
try {
|
|
474
|
+
if (process.platform !== 'win32')
|
|
475
|
+
return false;
|
|
476
|
+
if (!fs.existsSync(GUARD_PS1_PATH))
|
|
477
|
+
return false;
|
|
478
|
+
// Cheap check: at least one known profile contains the marker. Avoid
|
|
479
|
+
// spawning PowerShell on the heartbeat path — check well-known locations.
|
|
480
|
+
const candidates = [
|
|
481
|
+
path.join(os.homedir(), 'Documents', 'WindowsPowerShell', 'profile.ps1'),
|
|
482
|
+
path.join(os.homedir(), 'Documents', 'PowerShell', 'profile.ps1'),
|
|
483
|
+
path.join(os.homedir(), 'OneDrive', 'Documents', 'WindowsPowerShell', 'profile.ps1'),
|
|
484
|
+
path.join(os.homedir(), 'OneDrive', 'Documents', 'PowerShell', 'profile.ps1'),
|
|
485
|
+
];
|
|
486
|
+
for (const candidate of candidates) {
|
|
487
|
+
try {
|
|
488
|
+
if (fs.readFileSync(candidate, 'utf8').includes(MARKER_START))
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
catch { /* keep looking */ }
|
|
492
|
+
}
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** `fullcourtdefense install-shell-guard` — enable the interactive PowerShell guard. */
|
|
500
|
+
async function installShellGuardCommand(args = {}) {
|
|
501
|
+
if (process.platform !== 'win32') {
|
|
502
|
+
console.log('The interactive shell guard currently supports Windows PowerShell. macOS/Linux (bash/zsh preexec) is planned.');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const rules = writeShellGuardRules();
|
|
506
|
+
const nodePath = process.execPath;
|
|
507
|
+
const cliEntry = process.argv[1] || '';
|
|
508
|
+
fs.writeFileSync(GUARD_PS1_PATH, buildGuardPs1(nodePath, cliEntry), 'utf8');
|
|
509
|
+
console.log(`Guard rules written: ${rules.rules.length} rule(s), mode "${rules.mode}".`);
|
|
510
|
+
if (args.profiles === 'false') {
|
|
511
|
+
console.log('Skipped profile wiring (--profiles false): guard script + rules written only.');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const profiles = resolveProfilePaths();
|
|
515
|
+
if (profiles.length === 0) {
|
|
516
|
+
console.log('\x1b[33m⚠ Could not resolve any PowerShell profile path — guard script written but not wired into a profile.\x1b[0m');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
for (const profile of profiles) {
|
|
520
|
+
try {
|
|
521
|
+
const outcome = addSnippetToProfile(profile.profilePath);
|
|
522
|
+
console.log(outcome === 'installed'
|
|
523
|
+
? `\x1b[32m✓ ${profile.engine}: guard added to profile\x1b[0m \x1b[2m(${profile.profilePath})\x1b[0m`
|
|
524
|
+
: `\x1b[32m✓ ${profile.engine}: guard already in profile\x1b[0m \x1b[2m(${profile.profilePath})\x1b[0m`);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
console.log(`\x1b[33m⚠ ${profile.engine}: could not update profile (${error instanceof Error ? error.message : String(error)})\x1b[0m`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
console.log('\x1b[2mTakes effect in NEW PowerShell windows. Dangerous typed commands are blocked (or warned in monitor mode) and reported to the fleet dashboard.\x1b[0m');
|
|
531
|
+
}
|
|
532
|
+
/** `fullcourtdefense uninstall-shell-guard` — remove the guard from all profiles. */
|
|
533
|
+
async function uninstallShellGuardCommand() {
|
|
534
|
+
if (process.platform !== 'win32') {
|
|
535
|
+
console.log('Nothing to remove on this OS.');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
let removed = 0;
|
|
539
|
+
for (const profile of resolveProfilePaths()) {
|
|
540
|
+
if (removeSnippetFromProfile(profile.profilePath)) {
|
|
541
|
+
removed++;
|
|
542
|
+
console.log(`Removed guard from ${profile.engine} profile (${profile.profilePath}).`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
fs.unlinkSync(GUARD_PS1_PATH);
|
|
547
|
+
}
|
|
548
|
+
catch { /* not present */ }
|
|
549
|
+
try {
|
|
550
|
+
fs.unlinkSync(GUARD_RULES_PATH);
|
|
551
|
+
}
|
|
552
|
+
catch { /* not present */ }
|
|
553
|
+
console.log(removed > 0 ? 'Shell guard uninstalled. Open shells keep the guard until closed.' : 'Shell guard was not installed in any profile.');
|
|
554
|
+
}
|
|
@@ -12,6 +12,8 @@ export interface ShellAuditReport {
|
|
|
12
12
|
scriptBlockLogging: boolean;
|
|
13
13
|
transcription: boolean;
|
|
14
14
|
transcriptionPath?: string;
|
|
15
|
+
/** Interactive PowerShell profile guard installed (real-time blocking of typed commands). */
|
|
16
|
+
profileGuard?: boolean;
|
|
15
17
|
checkedAt: string;
|
|
16
18
|
}
|
|
17
19
|
/** Probe current audit coverage. Read-only, no elevation needed, never throws. */
|
|
@@ -41,6 +41,7 @@ const child_process_1 = require("child_process");
|
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const os = __importStar(require("os"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
|
+
const shellGuard_1 = require("./shellGuard");
|
|
44
45
|
/**
|
|
45
46
|
* Windows PowerShell audit coverage — ScriptBlock Logging + Transcription.
|
|
46
47
|
*
|
|
@@ -119,6 +120,7 @@ function getShellAuditReport() {
|
|
|
119
120
|
scriptBlockLogging: status.scriptBlockLogging,
|
|
120
121
|
transcription: status.transcription,
|
|
121
122
|
transcriptionPath: status.transcriptionPath,
|
|
123
|
+
profileGuard: (0, shellGuard_1.isShellGuardInstalled)(),
|
|
122
124
|
checkedAt: status.checkedAt,
|
|
123
125
|
};
|
|
124
126
|
}
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,8 @@ const mcpGateway_1 = require("./commands/mcpGateway");
|
|
|
51
51
|
const installAll_1 = require("./commands/installAll");
|
|
52
52
|
const autoProtect_1 = require("./commands/autoProtect");
|
|
53
53
|
const windowsAudit_1 = require("./commands/windowsAudit");
|
|
54
|
+
const shellGuard_1 = require("./commands/shellGuard");
|
|
55
|
+
const cmdGuard_1 = require("./commands/cmdGuard");
|
|
54
56
|
const fs = __importStar(require("fs"));
|
|
55
57
|
const path = __importStar(require("path"));
|
|
56
58
|
function readCliVersion() {
|
|
@@ -126,6 +128,11 @@ function printHelp() {
|
|
|
126
128
|
install Alias for install-all.
|
|
127
129
|
windows-audit Show Windows PowerShell audit coverage (ScriptBlock Logging +
|
|
128
130
|
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.
|
|
134
|
+
install-cmd-guard Block dangerous commands typed in cmd.exe (Command Prompt).
|
|
135
|
+
uninstall-cmd-guard removes it; cmd-guard-status shows coverage.
|
|
129
136
|
scan Runs the scan. Use --local for inside-organization scans.
|
|
130
137
|
discover Finds MCP servers, local secrets, agent rules/skills, and machine
|
|
131
138
|
blast radius on this laptop. Use --surface all --upload for AI Fleet.
|
|
@@ -596,6 +603,8 @@ async function main() {
|
|
|
596
603
|
autoProtect: flags['auto-protect'],
|
|
597
604
|
intervalMinutes: flags['interval-minutes'],
|
|
598
605
|
windowsAudit: flags['windows-audit'],
|
|
606
|
+
shellGuard: flags['shell-guard'],
|
|
607
|
+
cmdGuard: flags['cmd-guard'],
|
|
599
608
|
};
|
|
600
609
|
await (0, installAll_1.installAllCommand)(args, config);
|
|
601
610
|
break;
|
|
@@ -605,6 +614,51 @@ async function main() {
|
|
|
605
614
|
await (0, windowsAudit_1.windowsAuditCommand)(args);
|
|
606
615
|
break;
|
|
607
616
|
}
|
|
617
|
+
case 'install-shell-guard': {
|
|
618
|
+
await (0, shellGuard_1.installShellGuardCommand)({ profiles: flags.profiles });
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case 'uninstall-shell-guard': {
|
|
622
|
+
await (0, shellGuard_1.uninstallShellGuardCommand)();
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
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).');
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
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`);
|
|
634
|
+
}
|
|
635
|
+
if (status.rulesPresent)
|
|
636
|
+
console.log(` Rules: ${status.ruleCount} (mode: ${status.mode || 'block'})`);
|
|
637
|
+
console.log('');
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case 'install-cmd-guard': {
|
|
641
|
+
await (0, cmdGuard_1.installCmdGuardCommand)();
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
case 'uninstall-cmd-guard': {
|
|
645
|
+
await (0, cmdGuard_1.uninstallCmdGuardCommand)();
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case 'cmd-guard-status': {
|
|
649
|
+
const status = (0, cmdGuard_1.getCmdGuardStatus)();
|
|
650
|
+
if (!status.supported) {
|
|
651
|
+
console.log('cmd guard supports Windows Command Prompt only.');
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
console.log(`\nInteractive cmd.exe shell guard: ${status.installed ? '\x1b[32minstalled\x1b[0m' : '\x1b[31mnot installed\x1b[0m'}`);
|
|
655
|
+
if (status.autorunValue)
|
|
656
|
+
console.log(` AutoRun: \x1b[2m${status.autorunValue}\x1b[0m`);
|
|
657
|
+
if (status.rulesPresent)
|
|
658
|
+
console.log(` Rules: ${status.ruleCount} (mode: ${status.mode || 'block'})`);
|
|
659
|
+
console.log('');
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
608
662
|
case 'install-mcp-gateway': {
|
|
609
663
|
await (0, mcpGateway_1.installMcpGatewayCommand)(buildInstallArgs(), config);
|
|
610
664
|
break;
|
package/dist/telemetry.js
CHANGED
|
@@ -43,6 +43,8 @@ const os = __importStar(require("os"));
|
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const machineIdentity_1 = require("./machineIdentity");
|
|
45
45
|
const windowsAudit_1 = require("./commands/windowsAudit");
|
|
46
|
+
const shellGuard_1 = require("./commands/shellGuard");
|
|
47
|
+
const cmdGuard_1 = require("./commands/cmdGuard");
|
|
46
48
|
/**
|
|
47
49
|
* Local-first telemetry: every enforcement decision is appended to an on-disk
|
|
48
50
|
* spool (instant, offline-safe) and flushed to the backend in batches. Critical
|
|
@@ -109,6 +111,10 @@ function rewriteSpool(remaining) {
|
|
|
109
111
|
}
|
|
110
112
|
/** Drain the spool to the backend in one batch (+ optional heartbeat). Returns accepted count. */
|
|
111
113
|
async function flushSpool(input) {
|
|
114
|
+
// Piggyback on the flush cycle to re-sync terminal guard rule caches (mode flips
|
|
115
|
+
// + new custom rules reach open shells within ~30s).
|
|
116
|
+
(0, shellGuard_1.refreshShellGuardRules)();
|
|
117
|
+
(0, cmdGuard_1.refreshCmdGuardRules)();
|
|
112
118
|
const all = readSpool();
|
|
113
119
|
const batch = all.slice(0, MAX_BATCH);
|
|
114
120
|
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.11.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": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"test:deterministic-guard": "npm run build && node scripts/test-deterministic-guard.js",
|
|
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
|
+
"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",
|
|
20
22
|
"test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
|
|
21
23
|
"prepublishOnly": "npm run build"
|
|
22
24
|
},
|