fullcourtdefense-cli 1.28.0 → 1.29.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 +9 -0
- package/dist/commands/cmdGuard.js +21 -3
- package/dist/commands/deterministicGuard.d.ts +35 -0
- package/dist/commands/deterministicGuard.js +49 -35
- package/dist/commands/shellGuard.d.ts +10 -0
- package/dist/commands/shellGuard.js +68 -7
- package/dist/version.json +1 -1
- package/package.json +3 -2
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchors of the SHARED command rules (deterministicGuard ↔ shellGuard) that
|
|
3
|
+
* are cheap enough to wrap in cmd: `gh`, `vault`, `security`, … — derived, not
|
|
4
|
+
* hand-listed, so a new shared rule reaches cmd's index automatically unless
|
|
5
|
+
* its command is on the high-frequency diet above. cmd stays PARTIAL by
|
|
6
|
+
* construction (first token only, no `git`/`kubectl`/`aws` wrappers); the
|
|
7
|
+
* console labels it that way.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INTERCEPTED_COMMANDS: string[];
|
|
1
10
|
export declare function buildGuardJs(nodePath: string, cliEntry: string): string;
|
|
2
11
|
export interface CmdGuardStatus {
|
|
3
12
|
supported: boolean;
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.INTERCEPTED_COMMANDS = void 0;
|
|
36
37
|
exports.buildGuardJs = buildGuardJs;
|
|
37
38
|
exports.isCmdGuardInstalled = isCmdGuardInstalled;
|
|
38
39
|
exports.getCmdGuardStatus = getCmdGuardStatus;
|
|
@@ -75,7 +76,7 @@ const AUTORUN_MARKER = 'fullcourtdefense-cmd-autorun.bat';
|
|
|
75
76
|
* so pipes/`&&` chains where the dangerous command is not first are NOT caught by
|
|
76
77
|
* cmd (they ARE under PowerShell/zsh/bash and `shell-guard-check`).
|
|
77
78
|
*/
|
|
78
|
-
const
|
|
79
|
+
const FIXED_INTERCEPTED_COMMANDS = [
|
|
79
80
|
// Destructive filesystem / disk / permission tampering
|
|
80
81
|
'del', 'erase', 'rd', 'rmdir', 'format', 'cipher', 'diskpart', 'fsutil',
|
|
81
82
|
'takeown', 'icacls', 'attrib', 'compact',
|
|
@@ -97,8 +98,25 @@ const INTERCEPTED_COMMANDS = [
|
|
|
97
98
|
* Coverage is NOT lost where it matters: agent-driven commands go through the
|
|
98
99
|
* IDE hooks (full ruleset, any position in the line), and humans typing in
|
|
99
100
|
* PowerShell are covered in-process by the PSReadLine guard. Interactive
|
|
100
|
-
* cmd.exe keeps only the rare, high-signal destructive/tamper commands above
|
|
101
|
+
* cmd.exe keeps only the rare, high-signal destructive/tamper commands above
|
|
102
|
+
* plus the low-frequency anchors of the shared command rules (below).
|
|
101
103
|
*/
|
|
104
|
+
const CMD_HIGH_FREQUENCY_COMMANDS = new Set([
|
|
105
|
+
'docker', 'git', 'kubectl', 'aws', 'gcloud', 'az', 'terraform', 'pulumi', 'wsl',
|
|
106
|
+
'curl', 'wget', 'taskkill', 'robocopy', 'xcopy', 'psql', 'mysql', 'mongo', 'mongosh', 'sqlcmd',
|
|
107
|
+
]);
|
|
108
|
+
/**
|
|
109
|
+
* Anchors of the SHARED command rules (deterministicGuard ↔ shellGuard) that
|
|
110
|
+
* are cheap enough to wrap in cmd: `gh`, `vault`, `security`, … — derived, not
|
|
111
|
+
* hand-listed, so a new shared rule reaches cmd's index automatically unless
|
|
112
|
+
* its command is on the high-frequency diet above. cmd stays PARTIAL by
|
|
113
|
+
* construction (first token only, no `git`/`kubectl`/`aws` wrappers); the
|
|
114
|
+
* console labels it that way.
|
|
115
|
+
*/
|
|
116
|
+
exports.INTERCEPTED_COMMANDS = Array.from(new Set([
|
|
117
|
+
...FIXED_INTERCEPTED_COMMANDS,
|
|
118
|
+
...(0, shellGuard_1.builtinRuleCommandNames)().filter(name => !CMD_HIGH_FREQUENCY_COMMANDS.has(name)),
|
|
119
|
+
]));
|
|
102
120
|
function regString(key, value) {
|
|
103
121
|
try {
|
|
104
122
|
const out = (0, child_process_1.execFileSync)('reg', ['query', key, '/v', value], {
|
|
@@ -180,7 +198,7 @@ function buildGuardJs(nodePath, cliEntry) {
|
|
|
180
198
|
function buildMacroFile(nodePath) {
|
|
181
199
|
const guardJs = batQuote(GUARD_JS_PATH);
|
|
182
200
|
const node = batQuote(nodePath);
|
|
183
|
-
return `${INTERCEPTED_COMMANDS.map(cmd => `${cmd}=${node} ${guardJs} ${cmd} $*`).join('\r\n')}\r\n`;
|
|
201
|
+
return `${exports.INTERCEPTED_COMMANDS.map(cmd => `${cmd}=${node} ${guardJs} ${cmd} $*`).join('\r\n')}\r\n`;
|
|
184
202
|
}
|
|
185
203
|
function buildAutorunBat(nodePath) {
|
|
186
204
|
const guardJs = batQuote(GUARD_JS_PATH);
|
|
@@ -69,6 +69,41 @@ export interface LocalSafetyScanOptions {
|
|
|
69
69
|
*/
|
|
70
70
|
trustedScriptPaths?: string[];
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Built-ins that ship OFF until an org switches them on in the console. Shared
|
|
74
|
+
* with the terminal guards so a machine with no snapshot yet (fresh install,
|
|
75
|
+
* wiped cache) enforces the SAME default set in the IDE and in a shell.
|
|
76
|
+
*/
|
|
77
|
+
export declare const DEFAULT_DISABLED_BUILT_INS: ReadonlySet<string>;
|
|
78
|
+
/**
|
|
79
|
+
* One command rule shared by EVERY surface that can see a typed command: the
|
|
80
|
+
* IDE/agent hooks (this module), the PowerShell / bash / zsh profile guards and
|
|
81
|
+
* the cmd.exe doskey guard (shellGuard.ts loads this table into the terminal
|
|
82
|
+
* rule file verbatim). `pattern` is a regex SOURCE that must compile — and
|
|
83
|
+
* mean the same thing — under both JS and .NET with IgnoreCase: `\b`, `\s`,
|
|
84
|
+
* `(?:…)`, lookaheads and `[^\n;|&]` only. No lookbehind, no named groups,
|
|
85
|
+
* no `\d`-class differences. `commands` are the leading binaries a rule can
|
|
86
|
+
* fire on — the only thing a first-token hook (doskey) can key on.
|
|
87
|
+
*/
|
|
88
|
+
export interface SharedCommandRule {
|
|
89
|
+
itemId: string;
|
|
90
|
+
/** Catalog category id (local-safety-catalog.ts). */
|
|
91
|
+
categoryId: 'credential_commands';
|
|
92
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
93
|
+
pattern: string;
|
|
94
|
+
reason: string;
|
|
95
|
+
/** Leading command names this rule anchors on (for name-keyed hooks such as cmd.exe doskey). */
|
|
96
|
+
commands: string[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Commands that PRINT stored credentials to stdout — the file-path rules are
|
|
100
|
+
* useless against them because the secret never comes from a watched file
|
|
101
|
+
* ("gh auth token" emits the same token that lives in hosts.yml). Obvious,
|
|
102
|
+
* rarely-legitimate-for-agents forms are on by default; cloud token printers
|
|
103
|
+
* that legitimate deploy tooling uses constantly are strict (off by default,
|
|
104
|
+
* see DEFAULT_DISABLED_BUILT_INS). Ids match local-safety-catalog.ts.
|
|
105
|
+
*/
|
|
106
|
+
export declare const CREDENTIAL_COMMAND_RULES: readonly SharedCommandRule[];
|
|
72
107
|
export declare function stripInertDataSegments(text: string): string;
|
|
73
108
|
export declare function scanDeterministicToolCall(toolName: string, toolArgs: Record<string, unknown>, options?: LocalSafetyScanOptions): DeterministicFinding | undefined;
|
|
74
109
|
export declare function scanDeterministicTextResponse(text: string, options?: LocalSafetyScanOptions): DeterministicFinding | undefined;
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CREDENTIAL_COMMAND_RULES = exports.DEFAULT_DISABLED_BUILT_INS = void 0;
|
|
36
37
|
exports.toLocalSafetyRuleAction = toLocalSafetyRuleAction;
|
|
37
38
|
exports.stripInertDataSegments = stripInertDataSegments;
|
|
38
39
|
exports.scanDeterministicToolCall = scanDeterministicToolCall;
|
|
@@ -189,7 +190,12 @@ const SQL_KEYS = new Set([
|
|
|
189
190
|
'toolArgs.query',
|
|
190
191
|
'toolArgs.sql',
|
|
191
192
|
]);
|
|
192
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Built-ins that ship OFF until an org switches them on in the console. Shared
|
|
195
|
+
* with the terminal guards so a machine with no snapshot yet (fresh install,
|
|
196
|
+
* wiped cache) enforces the SAME default set in the IDE and in a shell.
|
|
197
|
+
*/
|
|
198
|
+
exports.DEFAULT_DISABLED_BUILT_INS = new Set([
|
|
193
199
|
'git_clean_force',
|
|
194
200
|
'generic_kubectl_delete',
|
|
195
201
|
'truncate_table',
|
|
@@ -413,7 +419,7 @@ function collectStrings(value, keyPath = 'args', out = [], depth = 0) {
|
|
|
413
419
|
return out;
|
|
414
420
|
}
|
|
415
421
|
function disabled(options) {
|
|
416
|
-
return new Set(options?.disabledBuiltInItemIds || Array.from(DEFAULT_DISABLED_BUILT_INS));
|
|
422
|
+
return new Set(options?.disabledBuiltInItemIds || Array.from(exports.DEFAULT_DISABLED_BUILT_INS));
|
|
417
423
|
}
|
|
418
424
|
function isEnabled(itemId, options) {
|
|
419
425
|
return !disabled(options).has(itemId);
|
|
@@ -587,39 +593,47 @@ function containsSensitiveCredentialPath(value) {
|
|
|
587
593
|
* useless against them because the secret never comes from a watched file
|
|
588
594
|
* ("gh auth token" emits the same token that lives in hosts.yml). Obvious,
|
|
589
595
|
* rarely-legitimate-for-agents forms are on by default; cloud token printers
|
|
590
|
-
* that legitimate deploy tooling uses constantly are strict (off by default
|
|
596
|
+
* that legitimate deploy tooling uses constantly are strict (off by default,
|
|
597
|
+
* see DEFAULT_DISABLED_BUILT_INS). Ids match local-safety-catalog.ts.
|
|
591
598
|
*/
|
|
599
|
+
exports.CREDENTIAL_COMMAND_RULES = [
|
|
600
|
+
{ itemId: 'gh_auth_token_cmd', categoryId: 'credential_commands', severity: 'critical', commands: ['gh'],
|
|
601
|
+
reason: 'GitHub CLI token print (gh auth token)',
|
|
602
|
+
pattern: String.raw `\bgh\s+auth\s+token\b|\bgh\s+auth\s+status\b(?=[^\n;|&]*(?:--show-token|\s-t\b))` },
|
|
603
|
+
{ itemId: 'git_credential_fill', categoryId: 'credential_commands', severity: 'critical', commands: ['git'],
|
|
604
|
+
reason: 'git credential helper dump',
|
|
605
|
+
pattern: String.raw `\bgit\s+credential(?:-manager|-store|-cache)?\s+(?:fill|get)\b` },
|
|
606
|
+
{ itemId: 'macos_keychain_dump', categoryId: 'credential_commands', severity: 'critical', commands: ['security'],
|
|
607
|
+
reason: 'macOS keychain password dump',
|
|
608
|
+
pattern: String.raw `\bsecurity\s+(?:find-generic-password|find-internet-password)\b[^\n;|&]*\s-w\b|\bsecurity\s+dump-keychain\b` },
|
|
609
|
+
{ itemId: 'kubectl_config_raw', categoryId: 'credential_commands', severity: 'high', commands: ['kubectl'],
|
|
610
|
+
reason: 'raw kubeconfig dump (kubectl config view --raw)',
|
|
611
|
+
pattern: String.raw `\bkubectl\s+config\s+view\b[^\n;|&]*--raw\b` },
|
|
612
|
+
{ itemId: 'aws_export_credentials', categoryId: 'credential_commands', severity: 'critical', commands: ['aws'],
|
|
613
|
+
reason: 'AWS secret access key export',
|
|
614
|
+
pattern: String.raw `\baws\s+configure\s+export-credentials\b|\baws\s+configure\s+get\b[^\n;|&]*secret` },
|
|
615
|
+
{ itemId: 'gcloud_access_token_cmd', categoryId: 'credential_commands', severity: 'high', commands: ['gcloud'],
|
|
616
|
+
reason: 'gcloud access-token print',
|
|
617
|
+
pattern: String.raw `\bgcloud\s+auth\b[^\n;|&]*\bprint-access-token\b` },
|
|
618
|
+
{ itemId: 'gcloud_secret_access_cmd', categoryId: 'credential_commands', severity: 'high', commands: ['gcloud'],
|
|
619
|
+
reason: 'GCP Secret Manager read',
|
|
620
|
+
pattern: String.raw `\bgcloud\s+secrets\s+versions\s+access\b` },
|
|
621
|
+
{ itemId: 'az_access_token_cmd', categoryId: 'credential_commands', severity: 'high', commands: ['az'],
|
|
622
|
+
reason: 'Azure access-token print',
|
|
623
|
+
pattern: String.raw `\baz\s+account\s+get-access-token\b` },
|
|
624
|
+
{ itemId: 'aws_secret_fetch_cmd', categoryId: 'credential_commands', severity: 'high', commands: ['aws'],
|
|
625
|
+
reason: 'AWS secret fetch',
|
|
626
|
+
pattern: String.raw `\baws\s+secretsmanager\s+get-secret-value\b|\baws\s+ssm\s+get-parameter\b[^\n;|&]*--with-decryption\b` },
|
|
627
|
+
{ itemId: 'vault_read_cmd', categoryId: 'credential_commands', severity: 'high', commands: ['vault'],
|
|
628
|
+
reason: 'Vault secret read',
|
|
629
|
+
pattern: String.raw `\bvault\s+(?:kv\s+get|read)\b` },
|
|
630
|
+
];
|
|
631
|
+
const CREDENTIAL_COMMAND_REGEXES = exports.CREDENTIAL_COMMAND_RULES.map(rule => ({ rule, re: new RegExp(rule.pattern, 'i') }));
|
|
592
632
|
function credentialCommandReason(value) {
|
|
593
|
-
return matchCommand(value,
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (/\bgit\s+credential(?:-manager|-store|-cache)?\s+(?:fill|get)\b/.test(lower)) {
|
|
598
|
-
return { itemId: 'git_credential_fill', reason: 'git credential helper dump' };
|
|
599
|
-
}
|
|
600
|
-
if (/\bsecurity\s+(?:find-generic-password|find-internet-password)\b[^\n;|&]*\s-w\b/.test(lower) || /\bsecurity\s+dump-keychain\b/.test(lower)) {
|
|
601
|
-
return { itemId: 'macos_keychain_dump', reason: 'macOS keychain password dump' };
|
|
602
|
-
}
|
|
603
|
-
if (/\bkubectl\s+config\s+view\b[^\n;|&]*--raw\b/.test(lower)) {
|
|
604
|
-
return { itemId: 'kubectl_config_raw', reason: 'raw kubeconfig dump (kubectl config view --raw)' };
|
|
605
|
-
}
|
|
606
|
-
if (/\baws\s+configure\s+export-credentials\b/.test(lower) || /\baws\s+configure\s+get\b[^\n;|&]*secret/.test(lower)) {
|
|
607
|
-
return { itemId: 'aws_export_credentials', reason: 'AWS secret access key export' };
|
|
608
|
-
}
|
|
609
|
-
if (/\bgcloud\s+auth\b[^\n;|&]*\bprint-access-token\b/.test(lower)) {
|
|
610
|
-
return { itemId: 'gcloud_access_token_cmd', reason: 'gcloud access-token print' };
|
|
611
|
-
}
|
|
612
|
-
if (/\bgcloud\s+secrets\s+versions\s+access\b/.test(lower)) {
|
|
613
|
-
return { itemId: 'gcloud_secret_access_cmd', reason: 'GCP Secret Manager read' };
|
|
614
|
-
}
|
|
615
|
-
if (/\baz\s+account\s+get-access-token\b/.test(lower)) {
|
|
616
|
-
return { itemId: 'az_access_token_cmd', reason: 'Azure access-token print' };
|
|
617
|
-
}
|
|
618
|
-
if (/\baws\s+secretsmanager\s+get-secret-value\b/.test(lower) || /\baws\s+ssm\s+get-parameter\b[^\n;|&]*--with-decryption\b/.test(lower)) {
|
|
619
|
-
return { itemId: 'aws_secret_fetch_cmd', reason: 'AWS secret fetch' };
|
|
620
|
-
}
|
|
621
|
-
if (/\bvault\s+(?:kv\s+get|read)\b/.test(lower)) {
|
|
622
|
-
return { itemId: 'vault_read_cmd', reason: 'Vault secret read' };
|
|
633
|
+
return matchCommand(value, text => {
|
|
634
|
+
for (const { rule, re } of CREDENTIAL_COMMAND_REGEXES) {
|
|
635
|
+
if (re.test(text))
|
|
636
|
+
return { itemId: rule.itemId, reason: rule.reason };
|
|
623
637
|
}
|
|
624
638
|
return undefined;
|
|
625
639
|
});
|
|
@@ -1190,7 +1204,7 @@ function scanReferencedScript(command, cwd, options) {
|
|
|
1190
1204
|
while (finding
|
|
1191
1205
|
&& (finding.category === 'sensitive_file' || finding.category === 'metadata_ssrf')
|
|
1192
1206
|
&& !scriptActsOnFinding(content, finding.category)) {
|
|
1193
|
-
const baseDisabled = scanOptions?.disabledBuiltInItemIds || Array.from(DEFAULT_DISABLED_BUILT_INS);
|
|
1207
|
+
const baseDisabled = scanOptions?.disabledBuiltInItemIds || Array.from(exports.DEFAULT_DISABLED_BUILT_INS);
|
|
1194
1208
|
scanOptions = { ...(scanOptions || {}), disabledBuiltInItemIds: [...baseDisabled, finding.itemId] };
|
|
1195
1209
|
finding = scanTextValue('script_file', content, scanOptions);
|
|
1196
1210
|
}
|
|
@@ -1212,7 +1226,7 @@ function withRuleSuppressed(options, finding) {
|
|
|
1212
1226
|
customBlocks: (options?.customBlocks || []).filter(block => block.id !== finding.itemId),
|
|
1213
1227
|
};
|
|
1214
1228
|
}
|
|
1215
|
-
const baseDisabled = options?.disabledBuiltInItemIds || Array.from(DEFAULT_DISABLED_BUILT_INS);
|
|
1229
|
+
const baseDisabled = options?.disabledBuiltInItemIds || Array.from(exports.DEFAULT_DISABLED_BUILT_INS);
|
|
1216
1230
|
return { ...(options || {}), disabledBuiltInItemIds: [...baseDisabled, finding.itemId] };
|
|
1217
1231
|
}
|
|
1218
1232
|
function findingAction(finding) {
|
|
@@ -51,6 +51,16 @@ export declare const SHELL_ASK_TIMEOUT_SECONDS = 30;
|
|
|
51
51
|
export declare const SHELL_ASK_REMEMBER_HOURS = 12;
|
|
52
52
|
/** Normalize a console action into what a typed-command guard can enforce. */
|
|
53
53
|
export declare function toShellGuardAction(raw: unknown): ShellGuardAction;
|
|
54
|
+
/**
|
|
55
|
+
* Leading command names that anchor a built-in rule — what a first-token hook
|
|
56
|
+
* (cmd.exe doskey) can key on. Derived from the shared tables so a new rule
|
|
57
|
+
* cannot be added without its command name reaching the cmd guard's index.
|
|
58
|
+
*/
|
|
59
|
+
/** Every built-in terminal rule id, ignoring console toggles (parity tests, docs). */
|
|
60
|
+
export declare function builtinRuleIds(): string[];
|
|
61
|
+
/** Every built-in terminal rule, all switched ON with default action (parity tests). */
|
|
62
|
+
export declare function builtinRulesAllOn(): ShellGuardRule[];
|
|
63
|
+
export declare function builtinRuleCommandNames(): string[];
|
|
54
64
|
interface ShellGuardRulesFile {
|
|
55
65
|
updatedAt: string;
|
|
56
66
|
mode: 'block' | 'monitor';
|
|
@@ -39,6 +39,9 @@ exports.guardArtifactStale = guardArtifactStale;
|
|
|
39
39
|
exports.writeGuardArtifact = writeGuardArtifact;
|
|
40
40
|
exports.guardCliEntry = guardCliEntry;
|
|
41
41
|
exports.toShellGuardAction = toShellGuardAction;
|
|
42
|
+
exports.builtinRuleIds = builtinRuleIds;
|
|
43
|
+
exports.builtinRulesAllOn = builtinRulesAllOn;
|
|
44
|
+
exports.builtinRuleCommandNames = builtinRuleCommandNames;
|
|
42
45
|
exports.writeShellGuardRules = writeShellGuardRules;
|
|
43
46
|
exports.activeShellGuardRules = activeShellGuardRules;
|
|
44
47
|
exports.evaluateShellCommand = evaluateShellCommand;
|
|
@@ -52,6 +55,7 @@ const child_process_1 = require("child_process");
|
|
|
52
55
|
const fs = __importStar(require("fs"));
|
|
53
56
|
const os = __importStar(require("os"));
|
|
54
57
|
const path = __importStar(require("path"));
|
|
58
|
+
const deterministicGuard_1 = require("./deterministicGuard");
|
|
55
59
|
/**
|
|
56
60
|
* Interactive PowerShell guard — real-time blocking of dangerous commands
|
|
57
61
|
* TYPED by a human in a terminal (inside or outside the IDE).
|
|
@@ -195,7 +199,10 @@ const BUILTIN_RULES = [
|
|
|
195
199
|
command: ['rd', 'rmdir'], flags: [String.raw `/s\b`], target: [String.raw `["']?[a-z]:[\\/]?["']?\s*(?:$|[;&|])`] }),
|
|
196
200
|
compileSpec({ id: 'windows_system_dir_delete', category: 'destructive_command', severity: 'critical', reason: 'recursive delete of a Windows system directory',
|
|
197
201
|
command: ['del', 'erase', 'rd', 'rmdir'], flags: [String.raw `/s\b`], target: [T_WIN_SYSDIR] }),
|
|
198
|
-
|
|
202
|
+
// Same catalog item as the `del /s` form above (windows_drive_delete): ONE
|
|
203
|
+
// console toggle, two syntaxes. Duplicate ids are intended — they share a
|
|
204
|
+
// switch and an action.
|
|
205
|
+
compileSpec({ id: 'windows_drive_delete', category: 'destructive_command', severity: 'critical', reason: 'recursive Remove-Item on a drive root',
|
|
199
206
|
command: ['remove-item', 'ri'], flags: [String.raw `-recurse\w*`], target: [String.raw `["']?[a-z]:[\\/]?["']?(?:\s|$|[;&|])`] }),
|
|
200
207
|
compileSpec({ id: 'chmod_root_777', category: 'destructive_command', severity: 'high', reason: 'recursive world-writable permission change on root',
|
|
201
208
|
command: ['chmod'], flags: [String.raw `-[a-z]*r[a-z]*|--recursive`, String.raw `[0-7]?777`], target: [T_UNIX_ROOT] }),
|
|
@@ -243,8 +250,16 @@ const BUILTIN_RULES = [
|
|
|
243
250
|
{ id: 'php_reverse_shell', category: 'reverse_shell', severity: 'high', reason: 'php reverse shell', source: 'builtin',
|
|
244
251
|
pattern: String.raw `\bphp\s+-r\b(?=.*fsockopen)` },
|
|
245
252
|
// Cloud metadata / credential exfiltration
|
|
246
|
-
|
|
247
|
-
|
|
253
|
+
// One terminal rule per CONSOLE toggle (catalog ids) — a single combined rule
|
|
254
|
+
// meant switching off / asking on one endpoint changed nothing in a shell.
|
|
255
|
+
{ id: 'aws_gcp_metadata_ip', category: 'metadata_ssrf', severity: 'high', reason: 'request to the AWS/GCP metadata endpoint (169.254.169.254)', source: 'builtin',
|
|
256
|
+
pattern: String.raw `(?=.*\b(?:curl|wget|iwr|invoke-webrequest|invoke-restmethod|irm)\b)(?=.*169\.254\.169\.254)` },
|
|
257
|
+
{ id: 'gcp_metadata_host', category: 'metadata_ssrf', severity: 'high', reason: 'request to the GCP metadata host (metadata.google.internal)', source: 'builtin',
|
|
258
|
+
pattern: String.raw `(?=.*\b(?:curl|wget|iwr|invoke-webrequest|invoke-restmethod|irm)\b)(?=.*metadata\.google\.internal)` },
|
|
259
|
+
{ id: 'azure_metadata_host', category: 'metadata_ssrf', severity: 'high', reason: 'request to the Azure metadata host (metadata.azure.com)', source: 'builtin',
|
|
260
|
+
pattern: String.raw `(?=.*\b(?:curl|wget|iwr|invoke-webrequest|invoke-restmethod|irm)\b)(?=.*metadata\.azure\.com)` },
|
|
261
|
+
{ id: 'alibaba_metadata_ip', category: 'metadata_ssrf', severity: 'high', reason: 'request to the Alibaba metadata endpoint (100.100.100.200)', source: 'builtin',
|
|
262
|
+
pattern: String.raw `(?=.*\b(?:curl|wget|iwr|invoke-webrequest|invoke-restmethod|irm)\b)(?=.*100\.100\.100\.200)` },
|
|
248
263
|
{ id: 'credential_exfiltration', category: 'secret_exfiltration', severity: 'critical', reason: 'credential file sent to the network', source: 'builtin',
|
|
249
264
|
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))` },
|
|
250
265
|
// Infra destroy — spec-compiled where flags matter (order-independent).
|
|
@@ -268,6 +283,10 @@ const BUILTIN_RULES = [
|
|
|
268
283
|
command: ['kubectl'], extra: [String.raw `\bdelete\s+(?:pvc|persistentvolumeclaims?)\b`], flags: [String.raw `--all`] }),
|
|
269
284
|
{ id: 'kubectl_delete_namespace', category: 'infra_destroy', severity: 'high', reason: 'destructive kubectl namespace delete', source: 'builtin',
|
|
270
285
|
pattern: String.raw `\bkubectl\s+delete\s+(?:namespace|ns)\b` },
|
|
286
|
+
// Catalog item (strict, OFF by default — DEFAULT_DISABLED_BUILT_INS): any
|
|
287
|
+
// kubectl delete. Present so the console toggle means something in a shell.
|
|
288
|
+
{ id: 'generic_kubectl_delete', category: 'infra_destroy', severity: 'medium', reason: 'kubectl delete (any resource)', source: 'builtin',
|
|
289
|
+
pattern: String.raw `\bkubectl\s+delete\b` },
|
|
271
290
|
{ id: 'gcloud_project_delete', category: 'infra_destroy', severity: 'high', reason: 'GCP project deletion', source: 'builtin',
|
|
272
291
|
pattern: String.raw `\bgcloud\s+projects\s+delete\b` },
|
|
273
292
|
{ id: 'gcloud_sql_delete', category: 'infra_destroy', severity: 'high', reason: 'GCP SQL instance deletion', source: 'builtin',
|
|
@@ -287,10 +306,11 @@ const BUILTIN_RULES = [
|
|
|
287
306
|
// Version-control footguns — imported from shellfirm / Sigma (git group).
|
|
288
307
|
compileSpec({ id: 'git_force_push', category: 'vcs_danger', severity: 'high', reason: 'git force push (overwrites remote history)',
|
|
289
308
|
command: ['git'], extra: [String.raw `\bpush\b`], flags: [String.raw `--force|-f\b`], neg: [String.raw `\s--force-with-lease`] }),
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
309
|
+
// One catalog item covers both forms (console: "Git destructive clean/reset",
|
|
310
|
+
// OFF by default) — same id + regex as the IDE guard, so the console toggle
|
|
311
|
+
// means the same thing in a terminal as it does for an agent.
|
|
312
|
+
{ id: 'git_clean_force', category: 'vcs_danger', severity: 'medium', reason: 'destructive git clean/reset (discards local work)', source: 'builtin',
|
|
313
|
+
pattern: String.raw `\bgit\s+(?:reset\s+--hard|clean\s+-f(?:d|x|dx)?\b)` },
|
|
294
314
|
{ id: 'git_branch_delete_force', category: 'vcs_danger', severity: 'low', reason: 'git force-delete branch', source: 'builtin',
|
|
295
315
|
pattern: String.raw `\bgit\s+branch\b(?=[^;&|]*\s-D\b)` },
|
|
296
316
|
// Anti-forensics / tamper
|
|
@@ -321,7 +341,38 @@ const BUILTIN_RULES = [
|
|
|
321
341
|
pattern: String.raw `\btruncate\s+table\b` },
|
|
322
342
|
{ id: 'drop_table', category: 'destructive_sql', severity: 'medium', reason: 'DROP TABLE', source: 'builtin',
|
|
323
343
|
pattern: String.raw `\bdrop\s+table\b` },
|
|
344
|
+
// Credential-revealing commands — the SAME table the IDE/agent guard
|
|
345
|
+
// evaluates (deterministicGuard.CREDENTIAL_COMMAND_RULES). One definition,
|
|
346
|
+
// every surface: a rule the console shows as "asks the developer" must ask
|
|
347
|
+
// the human typing `gh auth token` in PowerShell exactly as it asks an agent.
|
|
348
|
+
...deterministicGuard_1.CREDENTIAL_COMMAND_RULES.map((rule) => ({
|
|
349
|
+
id: rule.itemId,
|
|
350
|
+
category: rule.categoryId,
|
|
351
|
+
severity: rule.severity,
|
|
352
|
+
reason: rule.reason,
|
|
353
|
+
source: 'builtin',
|
|
354
|
+
pattern: rule.pattern,
|
|
355
|
+
})),
|
|
324
356
|
];
|
|
357
|
+
/**
|
|
358
|
+
* Leading command names that anchor a built-in rule — what a first-token hook
|
|
359
|
+
* (cmd.exe doskey) can key on. Derived from the shared tables so a new rule
|
|
360
|
+
* cannot be added without its command name reaching the cmd guard's index.
|
|
361
|
+
*/
|
|
362
|
+
/** Every built-in terminal rule id, ignoring console toggles (parity tests, docs). */
|
|
363
|
+
function builtinRuleIds() {
|
|
364
|
+
return BUILTIN_RULES.map(rule => rule.id);
|
|
365
|
+
}
|
|
366
|
+
/** Every built-in terminal rule, all switched ON with default action (parity tests). */
|
|
367
|
+
function builtinRulesAllOn() {
|
|
368
|
+
return BUILTIN_RULES.map(rule => ({ ...rule, action: 'block' }));
|
|
369
|
+
}
|
|
370
|
+
function builtinRuleCommandNames() {
|
|
371
|
+
const names = new Set();
|
|
372
|
+
for (const rule of deterministicGuard_1.CREDENTIAL_COMMAND_RULES)
|
|
373
|
+
rule.commands.forEach(name => names.add(name));
|
|
374
|
+
return Array.from(names).sort();
|
|
375
|
+
}
|
|
325
376
|
function escapeDotNetRegex(literal) {
|
|
326
377
|
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
327
378
|
}
|
|
@@ -348,6 +399,7 @@ function cachedMode() {
|
|
|
348
399
|
*/
|
|
349
400
|
function cachedDisabledItemIds() {
|
|
350
401
|
const disabled = new Set();
|
|
402
|
+
let snapshots = 0;
|
|
351
403
|
try {
|
|
352
404
|
for (const file of fs.readdirSync(SNAPSHOT_CACHE_DIR)) {
|
|
353
405
|
if (!file.startsWith('local-safety-') || !file.endsWith('.json'))
|
|
@@ -356,6 +408,7 @@ function cachedDisabledItemIds() {
|
|
|
356
408
|
const parsed = JSON.parse(fs.readFileSync(path.join(SNAPSHOT_CACHE_DIR, file), 'utf8'));
|
|
357
409
|
const ids = parsed?.snapshot?.disabledBuiltInItemIds;
|
|
358
410
|
if (Array.isArray(ids)) {
|
|
411
|
+
snapshots++;
|
|
359
412
|
for (const id of ids) {
|
|
360
413
|
if (typeof id === 'string' && id)
|
|
361
414
|
disabled.add(id);
|
|
@@ -366,6 +419,14 @@ function cachedDisabledItemIds() {
|
|
|
366
419
|
}
|
|
367
420
|
}
|
|
368
421
|
catch { /* no snapshot cache yet */ }
|
|
422
|
+
// No console snapshot on this machine yet (fresh install, wiped cache): fall
|
|
423
|
+
// back to the catalog's own default-off set — the same set the IDE/agent
|
|
424
|
+
// guard applies offline — so a shell never enforces MORE than the IDE does.
|
|
425
|
+
// The moment a snapshot exists it is authoritative (it lists every off item).
|
|
426
|
+
if (snapshots === 0) {
|
|
427
|
+
for (const id of deterministicGuard_1.DEFAULT_DISABLED_BUILT_INS)
|
|
428
|
+
disabled.add(id);
|
|
429
|
+
}
|
|
369
430
|
return disabled;
|
|
370
431
|
}
|
|
371
432
|
/**
|
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.29.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": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"test:cmd-guard": "npm run build && node scripts/test-cmd-guard.js",
|
|
33
33
|
"test:cmd-guard-perf": "npm run build && node scripts/test-cmd-guard-perf.js",
|
|
34
34
|
"test:posix-guard": "npm run build && node scripts/test-posix-guard.js",
|
|
35
|
+
"test:shell-parity": "npm run build && node scripts/test-shell-parity.js",
|
|
35
36
|
"test:secret-posture": "npm run build && node scripts/test-secret-posture-fixtures.js",
|
|
36
37
|
"test:agent-file-posture": "npm run build && node scripts/test-agent-file-posture.js",
|
|
37
38
|
"test:realworld-posture": "npm run build && node scripts/test-realworld-posture-fixtures.js",
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
"test:no-script-host": "npm run build && node scripts/test-no-script-host.js",
|
|
87
88
|
"test:s4u-migration": "npm run build && node scripts/test-s4u-task-migration.js",
|
|
88
89
|
"build:msi": "powershell -NoProfile -ExecutionPolicy Bypass -File installer/windows/Build-Msi.ps1",
|
|
89
|
-
"prepublishOnly": "npm run build"
|
|
90
|
+
"prepublishOnly": "npm run build && node scripts/test-shell-parity.js && node scripts/test-terminal-mcp-ask.js"
|
|
90
91
|
},
|
|
91
92
|
"keywords": [
|
|
92
93
|
"llm",
|