fullcourtdefense-cli 1.36.0 → 1.36.2
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/actionPolicyEngine.d.ts +23 -0
- package/dist/actionPolicyEngine.js +58 -19
- package/dist/approvalPrompt.d.ts +15 -0
- package/dist/approvalPrompt.js +157 -20
- package/dist/askDialog.d.ts +2 -0
- package/dist/askDialog.js +15 -1
- package/dist/askHeader.d.ts +81 -0
- package/dist/askHeader.js +573 -0
- package/dist/blockExplanation.d.ts +9 -0
- package/dist/blockExplanation.js +14 -0
- package/dist/callerEnv.d.ts +1 -1
- package/dist/callerEnv.js +14 -0
- package/dist/cloudGrant.d.ts +46 -0
- package/dist/cloudGrant.js +420 -0
- package/dist/cloudGrantTools.d.ts +97 -0
- package/dist/cloudGrantTools.js +1112 -0
- package/dist/commandNarrative.d.ts +8 -0
- package/dist/commandNarrative.js +222 -49
- package/dist/commands/cmdGuard.js +1 -1
- package/dist/commands/daemon.js +14 -0
- package/dist/commands/deterministicGuard.d.ts +8 -2
- package/dist/commands/deterministicGuard.js +219 -55
- package/dist/commands/discover.js +37 -14
- package/dist/commands/discoverAgentFiles.d.ts +2 -0
- package/dist/commands/discoverAgentFiles.js +1 -0
- package/dist/commands/discoverPaths.d.ts +4 -0
- package/dist/commands/discoverPaths.js +19 -1
- package/dist/commands/discoverProjectRoots.d.ts +72 -0
- package/dist/commands/discoverProjectRoots.js +327 -0
- package/dist/commands/discoverProxy.d.ts +1 -1
- package/dist/commands/discoverProxy.js +1 -1
- package/dist/commands/discoverSecrets.d.ts +44 -0
- package/dist/commands/discoverSecrets.js +158 -14
- package/dist/commands/hook.js +10 -7
- package/dist/commands/mcpGateway.js +9 -7
- package/dist/commands/posixShellGuard.js +1 -1
- package/dist/commands/shellGuard.js +70 -25
- package/dist/commands/uninstallAll.d.ts +1 -7
- package/dist/commands/uninstallAll.js +6 -45
- package/dist/config.d.ts +1 -1
- package/dist/config.js +3 -3
- package/dist/credentialGrant.d.ts +48 -0
- package/dist/credentialGrant.js +29 -18
- package/dist/credentialStore.js +1 -1
- package/dist/devConfirm.d.ts +44 -3
- package/dist/devConfirm.js +166 -30
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/grantTargets.d.ts +21 -0
- package/dist/grantTargets.js +235 -0
- package/dist/localSafetySnapshot.js +2 -0
- package/dist/msiProductCode.d.ts +10 -0
- package/dist/msiProductCode.js +124 -0
- package/dist/notify.js +1 -0
- package/dist/selfTest.d.ts +1 -1
- package/dist/selfTest.js +1 -1
- package/dist/selfUpdate.d.ts +51 -2
- package/dist/selfUpdate.js +217 -19
- package/dist/shellAst.d.ts +2 -0
- package/dist/shellAst.js +7 -1
- package/dist/shellAstLoader.js +12 -1
- package/dist/version.json +1 -1
- package/package.json +6 -1
|
@@ -33,11 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseMsiProductCodeFromRegOutput = exports.findMsiProductCode = void 0;
|
|
36
37
|
exports.listFcdConfigBackups = listFcdConfigBackups;
|
|
37
38
|
exports.removeFcdConfigBackups = removeFcdConfigBackups;
|
|
38
39
|
exports.purgeLocalState = purgeLocalState;
|
|
39
|
-
exports.parseMsiProductCodeFromRegOutput = parseMsiProductCodeFromRegOutput;
|
|
40
|
-
exports.findMsiProductCode = findMsiProductCode;
|
|
41
40
|
exports.detachCommand = detachCommand;
|
|
42
41
|
exports.reportUninstallToFleet = reportUninstallToFleet;
|
|
43
42
|
exports.uninstallAllCommand = uninstallAllCommand;
|
|
@@ -62,6 +61,7 @@ const credentialStore_1 = require("../credentialStore");
|
|
|
62
61
|
const machineKeyFile_1 = require("../machineKeyFile");
|
|
63
62
|
const selfUpdate_1 = require("../selfUpdate");
|
|
64
63
|
const backupManifest_1 = require("../backupManifest");
|
|
64
|
+
const msiProductCode_1 = require("../msiProductCode");
|
|
65
65
|
const BOLD = '\x1b[1m';
|
|
66
66
|
const GREEN = '\x1b[32m';
|
|
67
67
|
const YELLOW = '\x1b[33m';
|
|
@@ -193,48 +193,9 @@ function purgeLocalState(config) {
|
|
|
193
193
|
}
|
|
194
194
|
return { failures };
|
|
195
195
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
* installs or when the MSI entry is missing.
|
|
200
|
-
*/
|
|
201
|
-
function parseMsiProductCodeFromRegOutput(output) {
|
|
202
|
-
// `reg query … /s /f FullCourtDefense /d` prints the matching key path line
|
|
203
|
-
// followed by its matching values. We want the {GUID} keyed entry whose
|
|
204
|
-
// DisplayName is FullCourtDefense.
|
|
205
|
-
const lines = output.split(/\r?\n/);
|
|
206
|
-
let currentGuid;
|
|
207
|
-
for (const line of lines) {
|
|
208
|
-
const keyMatch = line.match(/\\Uninstall\\(\{[0-9A-Fa-f-]{36}\})\s*$/);
|
|
209
|
-
if (keyMatch) {
|
|
210
|
-
currentGuid = keyMatch[1];
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (currentGuid && /DisplayName\s+REG_SZ\s+FullCourtDefense\s*$/i.test(line.trim())) {
|
|
214
|
-
return currentGuid;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
function findMsiProductCode() {
|
|
220
|
-
if (process.platform !== 'win32')
|
|
221
|
-
return undefined;
|
|
222
|
-
const hives = [
|
|
223
|
-
'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
224
|
-
'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
225
|
-
];
|
|
226
|
-
for (const hive of hives) {
|
|
227
|
-
const query = (0, child_process_1.spawnSync)('reg', ['query', hive, '/s', '/f', 'FullCourtDefense', '/d'], {
|
|
228
|
-
encoding: 'utf8', windowsHide: true, timeout: 30_000,
|
|
229
|
-
});
|
|
230
|
-
if (query.status !== 0 || !query.stdout)
|
|
231
|
-
continue;
|
|
232
|
-
const code = parseMsiProductCodeFromRegOutput(query.stdout);
|
|
233
|
-
if (code)
|
|
234
|
-
return code;
|
|
235
|
-
}
|
|
236
|
-
return undefined;
|
|
237
|
-
}
|
|
196
|
+
var msiProductCode_2 = require("../msiProductCode");
|
|
197
|
+
Object.defineProperty(exports, "findMsiProductCode", { enumerable: true, get: function () { return msiProductCode_2.findMsiProductCode; } });
|
|
198
|
+
Object.defineProperty(exports, "parseMsiProductCodeFromRegOutput", { enumerable: true, get: function () { return msiProductCode_2.parseMsiProductCodeFromRegOutput; } });
|
|
238
199
|
/**
|
|
239
200
|
* The shared local teardown: daemon autostart, self-heal schedule, MCP gateway
|
|
240
201
|
* wraps, IDE hooks, terminal guards, and the daily discovery schedule. Each
|
|
@@ -405,7 +366,7 @@ async function uninstallAllCommand(args, config) {
|
|
|
405
366
|
}
|
|
406
367
|
}
|
|
407
368
|
if (msiHandoff) {
|
|
408
|
-
const productCode = findMsiProductCode();
|
|
369
|
+
const productCode = (0, msiProductCode_1.findMsiProductCode)();
|
|
409
370
|
if (productCode && launchMsiUninstall(productCode)) {
|
|
410
371
|
console.log(`${GREEN}${BOLD}Local teardown done — Windows uninstaller launched.${RESET}`);
|
|
411
372
|
console.log(`${DIM}Approve the Windows elevation prompt (UAC) to finish. That step removes the "FullCourtDefense Updater" SYSTEM task, the HKLM PowerShell audit policies FCD enabled, and the application files under Program Files.${RESET}`);
|
package/dist/config.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface PowershellHealth {
|
|
|
36
36
|
export declare function getPowershellHealth(): PowershellHealth | undefined;
|
|
37
37
|
/**
|
|
38
38
|
* PowerShell language mode — the "is this a hardened WDAC/AppLocker fleet
|
|
39
|
-
* machine?" bit that explained the
|
|
39
|
+
* machine?" bit that explained the devbox-01 incident. Spawned only on the
|
|
40
40
|
* DPAPI failure path (and from the deep self-test), never on healthy runs.
|
|
41
41
|
*/
|
|
42
42
|
export declare function capturePowershellLanguageMode(): string | undefined;
|
package/dist/config.js
CHANGED
|
@@ -171,7 +171,7 @@ function getPowershellHealth() {
|
|
|
171
171
|
}
|
|
172
172
|
/**
|
|
173
173
|
* PowerShell language mode — the "is this a hardened WDAC/AppLocker fleet
|
|
174
|
-
* machine?" bit that explained the
|
|
174
|
+
* machine?" bit that explained the devbox-01 incident. Spawned only on the
|
|
175
175
|
* DPAPI failure path (and from the deep self-test), never on healthy runs.
|
|
176
176
|
*/
|
|
177
177
|
function capturePowershellLanguageMode() {
|
|
@@ -250,7 +250,7 @@ function unprotectShieldKeyForCurrentWindowsUser(value) {
|
|
|
250
250
|
const result = powershellDpapi(exports.DPAPI_DECRYPT_SNIPPET, value);
|
|
251
251
|
if (!result && value) {
|
|
252
252
|
// The machine HAS a DPAPI-protected key it cannot read — the exact
|
|
253
|
-
// credential-broken state that 401-bricked
|
|
253
|
+
// credential-broken state that 401-bricked devbox-01. Emit a coded
|
|
254
254
|
// distress signal so the fleet console sees it without asking the user.
|
|
255
255
|
const health = powershellHealth;
|
|
256
256
|
if (health && !health.spawnOk) {
|
|
@@ -446,7 +446,7 @@ function resolveCliCredentials(config, overrides = {}, options = {}) {
|
|
|
446
446
|
lastCredentialTrace = { shieldKeySource, dpapiAttempted, at: new Date().toISOString() };
|
|
447
447
|
// Self-healing migration: whenever the key still resolves through a legacy
|
|
448
448
|
// source (DPAPI blob or plaintext config), copy it into the Credential
|
|
449
|
-
// Manager once. On fleets like
|
|
449
|
+
// Manager once. On fleets like devbox-01 the daemon's context CAN decrypt
|
|
450
450
|
// while IDE-spawned hooks CANNOT — after the daemon's first resolution the
|
|
451
451
|
// native entry exists and every process reads it without PowerShell.
|
|
452
452
|
if (shieldKey && resolvedShieldId
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Always allow" for a credential command, bound to where the credential goes (#1535).
|
|
3
|
+
*
|
|
4
|
+
* A grant under `gcloud_access_token_cmd` used to remember only programs
|
|
5
|
+
* (`curl`, `gcloud`), so for 30 days any line that minted a Google token and
|
|
6
|
+
* ran `curl` rode it — whatever host received the token. A credential grant
|
|
7
|
+
* now also binds:
|
|
8
|
+
*
|
|
9
|
+
* - ISSUER rules (the command prints a token its own provider accepts:
|
|
10
|
+
* gcloud / az access tokens, `gh auth token`, AWS keys): every host the line
|
|
11
|
+
* sends to must sit under the issuer's domain (`*.googleapis.com`). The
|
|
12
|
+
* 2026-09-14 fix survives — `logging.` and `monitoring.googleapis.com`
|
|
13
|
+
* share one grant — and a token sent anywhere else asks again.
|
|
14
|
+
* - EXACT rules (the command reads a stored secret — Secret Manager, AWS
|
|
15
|
+
* secrets, Vault, git / keychain / kubeconfig dumps): the store is not the
|
|
16
|
+
* issuer of what it holds (a GitHub token read from Secret Manager goes to
|
|
17
|
+
* `api.github.com`), so the grant binds the exact hosts and the named
|
|
18
|
+
* secrets seen when the developer pressed the button.
|
|
19
|
+
*
|
|
20
|
+
* A destination or secret this module cannot read (a host in a variable it
|
|
21
|
+
* cannot resolve, a program it does not model such as `node` or `Set-Content`,
|
|
22
|
+
* a proxy / `--resolve` override) refuses the grant: no "Always allow" on the
|
|
23
|
+
* dialog, and no standing grant matches — the developer is asked.
|
|
24
|
+
*
|
|
25
|
+
* Grants recorded before this binding carry no `credential` field; for a
|
|
26
|
+
* credential rule they match nothing (asked again, never wider).
|
|
27
|
+
*
|
|
28
|
+
* Deterministic structure only (words, flags, literal URLs, same-line
|
|
29
|
+
* assignments). Pure: no I/O.
|
|
30
|
+
*/
|
|
31
|
+
import { type ShellDialect } from './shellQuoteMask';
|
|
1
32
|
export type CredentialBindingMode = 'issuer' | 'exact';
|
|
2
33
|
export interface CredentialGrantBinding {
|
|
3
34
|
mode: CredentialBindingMode;
|
|
@@ -24,6 +55,23 @@ export interface CredentialRuleFacts {
|
|
|
24
55
|
}
|
|
25
56
|
/** Binding mode for the credential rules in one ask. Mixed or unknown rules bind exact hosts. */
|
|
26
57
|
export declare function credentialRuleFacts(itemIds: string[]): CredentialRuleFacts;
|
|
58
|
+
export declare const unquote: (word: string) => string;
|
|
59
|
+
export interface LineVars {
|
|
60
|
+
/** Variable -> every literal value assigned to it on the line; null = assigned from something unreadable. */
|
|
61
|
+
values: Map<string, string[] | null>;
|
|
62
|
+
/** The line can set variables in ways not read here (`Set-Variable`, `-OutVariable`, `read`, `source`): nothing resolves. */
|
|
63
|
+
poisoned: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every assignment on the line, wherever it sits (statement start, a script
|
|
67
|
+
* block, an `else` branch, a condition). Read on the masked line, so quoted
|
|
68
|
+
* text never counts as an assignment.
|
|
69
|
+
*/
|
|
70
|
+
export declare function lineVars(command: string, dialect: ShellDialect): LineVars;
|
|
71
|
+
/** `--name value` / `--name=value` as a literal; undefined when absent, null when dynamic. */
|
|
72
|
+
export declare function flagValue(words: string[], names: string[], vars: LineVars): string | null | undefined;
|
|
73
|
+
/** A literal word or a variable holding exactly one literal; null when dynamic. */
|
|
74
|
+
export declare function literalWord(word: string | undefined, vars: LineVars): string | null;
|
|
27
75
|
/**
|
|
28
76
|
* The binding for one credential-command line. `uses` are the program uses the
|
|
29
77
|
* grant scanner found (`scanShellPrograms`), `localOnly` the programs that only
|
package/dist/credentialGrant.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CREDENTIAL_GRANT_TTL_MS = exports.CREDENTIAL_ISSUERS = void 0;
|
|
3
|
+
exports.unquote = exports.CREDENTIAL_GRANT_TTL_MS = exports.CREDENTIAL_ISSUERS = void 0;
|
|
4
4
|
exports.credentialRuleFacts = credentialRuleFacts;
|
|
5
|
+
exports.lineVars = lineVars;
|
|
6
|
+
exports.flagValue = flagValue;
|
|
7
|
+
exports.literalWord = literalWord;
|
|
5
8
|
exports.credentialBindingFor = credentialBindingFor;
|
|
6
9
|
exports.withinIssuer = withinIssuer;
|
|
7
10
|
exports.credentialBindingCovers = credentialBindingCovers;
|
|
@@ -60,6 +63,7 @@ function credentialRuleFacts(itemIds) {
|
|
|
60
63
|
// Destinations
|
|
61
64
|
// ---------------------------------------------------------------------------
|
|
62
65
|
const unquote = (word) => word.replace(/^(["'])([\s\S]*)\1$/, '$2');
|
|
66
|
+
exports.unquote = unquote;
|
|
63
67
|
function assign(vars, name, value) {
|
|
64
68
|
const key = name.toLowerCase();
|
|
65
69
|
const prior = vars.values.get(key);
|
|
@@ -71,13 +75,19 @@ function assign(vars, name, value) {
|
|
|
71
75
|
}
|
|
72
76
|
vars.values.set(key, [...(prior || []), value]);
|
|
73
77
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
78
|
+
/**
|
|
79
|
+
* A right-hand side this module can use as data: literal text, no command or sub-expression.
|
|
80
|
+
* In PowerShell a bare word on the right (`$p = Read-Host`) RUNS that command; only quoted
|
|
81
|
+
* text, a hashtable or a number is data there.
|
|
82
|
+
*/
|
|
83
|
+
function literalValue(rhs, ps = false) {
|
|
76
84
|
const text = rhs.trim();
|
|
77
85
|
if (!text || /\$\(|@\(|`/.test(text))
|
|
78
86
|
return null;
|
|
79
87
|
if (/^'[^']*'$/.test(text) || /^"[^"]*"$/.test(text) || /^@\{[^{}]*\}$/.test(text))
|
|
80
88
|
return text;
|
|
89
|
+
if (ps)
|
|
90
|
+
return /^-?\d+(?:\.\d+)?$/.test(text) ? text : null;
|
|
81
91
|
if (/^[\w.:/@%+?=#~-]+$/.test(text))
|
|
82
92
|
return text;
|
|
83
93
|
return null;
|
|
@@ -118,7 +128,7 @@ function lineVars(command, dialect) {
|
|
|
118
128
|
for (const m of code.matchAll(assignment)) {
|
|
119
129
|
const from = m.index + m[0].length;
|
|
120
130
|
const rhs = command.slice(from, expressionEnd(code, from, !ps));
|
|
121
|
-
assign(vars, m[1], m[2] ? null : literalValue(rhs));
|
|
131
|
+
assign(vars, m[1], m[2] ? null : literalValue(rhs, ps));
|
|
122
132
|
}
|
|
123
133
|
const loops = ps ? /\bforeach\s*\(\s*\$([A-Za-z_]\w*)\s+in\s+/gi : /\bfor\s+([A-Za-z_]\w*)\s+in\s+/g;
|
|
124
134
|
for (const m of code.matchAll(loops)) {
|
|
@@ -128,14 +138,14 @@ function lineVars(command, dialect) {
|
|
|
128
138
|
if (!items.length)
|
|
129
139
|
assign(vars, m[1], null);
|
|
130
140
|
for (const item of items)
|
|
131
|
-
assign(vars, m[1], literalValue(item));
|
|
141
|
+
assign(vars, m[1], literalValue(item, ps));
|
|
132
142
|
}
|
|
133
143
|
return vars;
|
|
134
144
|
}
|
|
135
145
|
const HOST_OK = /^(?:[a-z0-9-]+\.)*[a-z0-9-]+$|^\[[0-9a-f:.]+\]$/;
|
|
136
146
|
/** Host of one URL-ish value; `unknown` when the host itself is dynamic or malformed. */
|
|
137
147
|
function hostOfUrl(value) {
|
|
138
|
-
const text = unquote(value).trim();
|
|
148
|
+
const text = (0, exports.unquote)(value).trim();
|
|
139
149
|
const match = text.match(/^(?:([a-z][a-z0-9+.-]*):\/\/)?([^/?#\s]*)/i);
|
|
140
150
|
const authority = (match?.[2] || '').replace(/^[^@]*@/, '');
|
|
141
151
|
if (!authority || /[$`({%]/.test(authority))
|
|
@@ -156,7 +166,7 @@ function urlHostsIn(text) {
|
|
|
156
166
|
}
|
|
157
167
|
/** A destination word: a literal URL / host, or `$var` / `"$var/path"` resolved through the line's literal assignments. */
|
|
158
168
|
function resolveDestination(word, vars) {
|
|
159
|
-
const text = unquote(word).trim();
|
|
169
|
+
const text = (0, exports.unquote)(word).trim();
|
|
160
170
|
const ref = text.match(/^[$@]\{?(?:(?:script|global|local|private):)?([A-Za-z_]\w*)\}?/);
|
|
161
171
|
if (ref) {
|
|
162
172
|
if (vars.poisoned)
|
|
@@ -221,7 +231,7 @@ function senderDestinations(grammar, words, vars) {
|
|
|
221
231
|
let positionalSeen = false;
|
|
222
232
|
for (let k = 1; k < words.length; k++) {
|
|
223
233
|
const word = words[k];
|
|
224
|
-
const bare = unquote(word);
|
|
234
|
+
const bare = (0, exports.unquote)(word);
|
|
225
235
|
if (grammar.powershell) {
|
|
226
236
|
const param = bare.match(/^-([A-Za-z]+)(?::(.*))?$/);
|
|
227
237
|
if (param) {
|
|
@@ -317,7 +327,7 @@ function registryDestination(words, vars) {
|
|
|
317
327
|
const valued = new Set(['-u', '--username', '-p', '--password', '--authfile', '--cert-dir']);
|
|
318
328
|
const start = verb === 'registry' ? verbAt + 2 : verbAt + 1;
|
|
319
329
|
for (let k = start; k < words.length; k++) {
|
|
320
|
-
const bare = unquote(words[k]);
|
|
330
|
+
const bare = (0, exports.unquote)(words[k]);
|
|
321
331
|
if (bare.startsWith('-')) {
|
|
322
332
|
if (valued.has(bare.split('=')[0].toLowerCase()) && !bare.includes('='))
|
|
323
333
|
k++;
|
|
@@ -330,9 +340,10 @@ function registryDestination(words, vars) {
|
|
|
330
340
|
// ---------------------------------------------------------------------------
|
|
331
341
|
// Secret resources
|
|
332
342
|
// ---------------------------------------------------------------------------
|
|
343
|
+
/** `--name value` / `--name=value` as a literal; undefined when absent, null when dynamic. */
|
|
333
344
|
function flagValue(words, names, vars) {
|
|
334
345
|
for (let k = 0; k < words.length; k++) {
|
|
335
|
-
const bare = unquote(words[k]);
|
|
346
|
+
const bare = (0, exports.unquote)(words[k]);
|
|
336
347
|
for (const name of names) {
|
|
337
348
|
if (bare === name)
|
|
338
349
|
return literalWord(words[k + 1], vars);
|
|
@@ -346,13 +357,13 @@ function flagValue(words, names, vars) {
|
|
|
346
357
|
function literalWord(word, vars) {
|
|
347
358
|
if (word === undefined)
|
|
348
359
|
return null;
|
|
349
|
-
const text = unquote(word).trim();
|
|
360
|
+
const text = (0, exports.unquote)(word).trim();
|
|
350
361
|
const ref = text.match(/^\$\{?(?:(?:script|global|local|private):)?([A-Za-z_]\w*)\}?$/);
|
|
351
362
|
if (ref) {
|
|
352
363
|
if (vars.poisoned)
|
|
353
364
|
return null;
|
|
354
365
|
const values = vars.values.get(ref[1].toLowerCase());
|
|
355
|
-
return values && values.length && new Set(values).size === 1 ? unquote(values[0]) : null;
|
|
366
|
+
return values && values.length && new Set(values).size === 1 ? (0, exports.unquote)(values[0]) : null;
|
|
356
367
|
}
|
|
357
368
|
return text && !/[$`(){}]/.test(text) ? text : null;
|
|
358
369
|
}
|
|
@@ -360,15 +371,15 @@ function secretResources(ruleId, uses, dialect, vars) {
|
|
|
360
371
|
const out = [];
|
|
361
372
|
for (const use of uses) {
|
|
362
373
|
const words = (0, shellQuoteMask_1.shellWords)(use.text.trim(), dialect);
|
|
363
|
-
const at = words.findIndex(w => unquote(w).toLowerCase().replace(/\.(exe|cmd)$/, '').endsWith(use.name));
|
|
374
|
+
const at = words.findIndex(w => (0, exports.unquote)(w).toLowerCase().replace(/\.(exe|cmd)$/, '').endsWith(use.name));
|
|
364
375
|
const args = words.slice(at + 1);
|
|
365
|
-
const lower = args.map(w => unquote(w).toLowerCase());
|
|
376
|
+
const lower = args.map(w => (0, exports.unquote)(w).toLowerCase());
|
|
366
377
|
if (ruleId === 'gcloud_secret_access_cmd' && use.name === 'gcloud') {
|
|
367
378
|
const idx = lower.findIndex((w, k) => w === 'secrets' && lower[k + 1] === 'versions' && lower[k + 2] === 'access');
|
|
368
379
|
if (idx < 0)
|
|
369
380
|
continue;
|
|
370
|
-
const positional = args.slice(idx + 3).find(w => !unquote(w).startsWith('-'));
|
|
371
|
-
const full = positional && unquote(positional).match(/^projects\/([^/]+)\/secrets\/([^/]+)/);
|
|
381
|
+
const positional = args.slice(idx + 3).find(w => !(0, exports.unquote)(w).startsWith('-'));
|
|
382
|
+
const full = positional && (0, exports.unquote)(positional).match(/^projects\/([^/]+)\/secrets\/([^/]+)/);
|
|
372
383
|
if (full) {
|
|
373
384
|
out.push(`projects/${full[1]}/secrets/${full[2]}`);
|
|
374
385
|
continue;
|
|
@@ -401,7 +412,7 @@ function secretResources(ruleId, uses, dialect, vars) {
|
|
|
401
412
|
if (!/^(?:get|read)$/.test(lower[verbAt] || ''))
|
|
402
413
|
continue;
|
|
403
414
|
const mount = flagValue(args, ['-mount'], vars);
|
|
404
|
-
const paths = args.slice(verbAt + 1).filter(w => !unquote(w).startsWith('-'));
|
|
415
|
+
const paths = args.slice(verbAt + 1).filter(w => !(0, exports.unquote)(w).startsWith('-'));
|
|
405
416
|
if (mount === null || paths.length !== 1)
|
|
406
417
|
return null;
|
|
407
418
|
const path = literalWord(paths[0], vars);
|
|
@@ -429,7 +440,7 @@ function credentialBindingFor(command, uses, localOnly, ruleCommands, facts) {
|
|
|
429
440
|
if (ruleCommands.includes(use.name) || localOnly(use.name))
|
|
430
441
|
continue;
|
|
431
442
|
const words = (0, shellQuoteMask_1.shellWords)(use.text.trim(), dialect);
|
|
432
|
-
const at = words.findIndex(w => unquote(w).toLowerCase().replace(/^.*[\\/]/, '').replace(/\.(exe|cmd|bat)$/, '') === use.name);
|
|
443
|
+
const at = words.findIndex(w => (0, exports.unquote)(w).toLowerCase().replace(/^.*[\\/]/, '').replace(/\.(exe|cmd|bat)$/, '') === use.name);
|
|
433
444
|
const argv = at >= 0 ? words.slice(at) : words;
|
|
434
445
|
const grammar = SENDERS[use.name];
|
|
435
446
|
const read = grammar ? senderDestinations(grammar, argv, vars)
|
package/dist/credentialStore.js
CHANGED
|
@@ -10,7 +10,7 @@ const distress_1 = require("./distress");
|
|
|
10
10
|
* Native credential store — Windows Credential Manager via `@napi-rs/keyring`
|
|
11
11
|
* (a prebuilt N-API binding; in-process win32 API calls, no PowerShell).
|
|
12
12
|
*
|
|
13
|
-
* WHY: the
|
|
13
|
+
* WHY: the devbox-01 incident class. The shield key at rest was DPAPI-protected
|
|
14
14
|
* and decrypted by spawning powershell.exe — exactly the pattern EDRs flag
|
|
15
15
|
* ("IDE process spawns PowerShell that touches credentials"). On hardened
|
|
16
16
|
* fleets PowerShell is additionally locked to Constrained Language Mode or
|
package/dist/devConfirm.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AlwaysCovers, type AskSubject, type NativeDialogView } from './approvalPrompt';
|
|
1
2
|
import { type CredentialGrantBinding, type CredentialRuleFacts } from './credentialGrant';
|
|
2
3
|
export { isCiEnvironment, isHumanPresent } from './callerEnv';
|
|
3
4
|
export type ConfirmRemember = 'none' | 'session' | '1h';
|
|
@@ -44,9 +45,30 @@ export interface NativeDialogOptions {
|
|
|
44
45
|
alwaysScopeLabel?: string;
|
|
45
46
|
/** How long / how many times it would hold, e.g. "30 days" or "7 days or 20 uses". */
|
|
46
47
|
alwaysLimitsLabel?: string;
|
|
48
|
+
/** What / How / Where the grant covers (`grantCoverage`), shown as one compact row. */
|
|
49
|
+
alwaysCovers?: AlwaysCovers;
|
|
50
|
+
/** Two buttons: why "Always allow" is not offered (`grantNotOfferedReason`), one short line. */
|
|
51
|
+
alwaysNotOffered?: string;
|
|
52
|
+
/** Envelope facts for the "Why you're asked" header (grant scope, rule id). */
|
|
53
|
+
subject?: AskSubject;
|
|
47
54
|
/** Icon and copy. Buttons stay Deny / Allow once / Always allow for `ask`. */
|
|
48
55
|
tone?: 'ask' | 'approval' | 'block';
|
|
49
56
|
}
|
|
57
|
+
/** Card HTML for macOS WKWebView. Button labels stay the existing Deny / Allow once / Always allow. */
|
|
58
|
+
export declare function dialogCardHtml(view: NativeDialogView, options: NativeDialogOptions): string;
|
|
59
|
+
/**
|
|
60
|
+
* The layout facts for the Windows card, base64 JSON, written to the dialog's STDIN.
|
|
61
|
+
* `-EncodedCommand` carries the script as UTF-16 base64 and Windows caps a command line
|
|
62
|
+
* at 32,767 characters: with the view inside it, a long script (a 9-step token query)
|
|
63
|
+
* failed to spawn and the ask silently fell back to the IDE card.
|
|
64
|
+
*/
|
|
65
|
+
export declare function windowsDialogPayload(view: NativeDialogView): string;
|
|
66
|
+
/**
|
|
67
|
+
* Windows branded card. Compact prompt text stays the first payload so tests still
|
|
68
|
+
* read the dialog copy; layout facts arrive on stdin (`windowsDialogPayload`).
|
|
69
|
+
* Exit codes: 0 = Allow once, 3 = Always allow, 2 = timed out, anything else = Deny.
|
|
70
|
+
*/
|
|
71
|
+
export declare function windowsDialogScript(message: string, timeoutSeconds: number, options: NativeDialogOptions, title: string, view: NativeDialogView): string;
|
|
50
72
|
export declare function confirmNativeActionDetailed(reason: string, timeoutMs?: number, options?: NativeDialogOptions): Promise<{
|
|
51
73
|
outcome: NativeDialogOutcome;
|
|
52
74
|
latencyMs: number;
|
|
@@ -138,6 +160,18 @@ export interface GrantScope {
|
|
|
138
160
|
* binding existed) never covers a credential-rule ask — asked again.
|
|
139
161
|
*/
|
|
140
162
|
credential?: CredentialGrantBinding;
|
|
163
|
+
/**
|
|
164
|
+
* Shell only, when the line runs a cloud CLI: `gcloud|<how>|<where>` per use,
|
|
165
|
+
* sorted and unique (`cloudGrant.ts`). A grant without it (recorded before
|
|
166
|
+
* the binding existed) never covers a cloud line — asked again.
|
|
167
|
+
*/
|
|
168
|
+
cloud?: string[];
|
|
169
|
+
/**
|
|
170
|
+
* Set when the rule that asked is about a removal that can never be granted
|
|
171
|
+
* (home folder, a drive, the filesystem root): no Always allow is offered,
|
|
172
|
+
* none is stored, and no standing grant covers it. Not part of the grant key.
|
|
173
|
+
*/
|
|
174
|
+
refusal?: 'destructive_scope';
|
|
141
175
|
}
|
|
142
176
|
/**
|
|
143
177
|
* A developer's standing "Always allow" for one scope under one rule. Bound to the
|
|
@@ -344,10 +378,13 @@ export declare function shellProgramsForRule(command: string, ruleCommands?: rea
|
|
|
344
378
|
export declare function splitLocalSafetyRuleIds(matchedRule?: string): string[] | undefined;
|
|
345
379
|
/**
|
|
346
380
|
* Can "Always allow" exist for this scope at all? Not for a shell line whose
|
|
347
|
-
* programs could not be read (empty set)
|
|
348
|
-
* destination or secret could not be read
|
|
349
|
-
* outside its issuer
|
|
381
|
+
* programs could not be read (empty set), not for a credential line whose
|
|
382
|
+
* destination or secret could not be read or that sends the credential
|
|
383
|
+
* outside its issuer, and not for a cloud line that deletes, changes
|
|
384
|
+
* permissions or whose target cannot be read. The dialog offers two
|
|
385
|
+
* buttons and says why in one line; no grant matches.
|
|
350
386
|
*/
|
|
387
|
+
export declare function grantNotOfferedReason(scope: GrantScope): string | undefined;
|
|
351
388
|
export declare function grantOffered(scope: GrantScope): boolean;
|
|
352
389
|
/** The caps a grant for this scope is created under: credential grants live 7 days at most. */
|
|
353
390
|
export declare function capGrantLimitsForScope(limits: GrantLimits | null, scope: GrantScope): GrantLimits | null;
|
|
@@ -391,6 +428,8 @@ export declare function describeGrantScope(scope: GrantScope, rule?: {
|
|
|
391
428
|
policyId?: string;
|
|
392
429
|
policyName?: string;
|
|
393
430
|
}): string;
|
|
431
|
+
/** What / How / Where an "Always allow" covers, as the dialog's compact rows. */
|
|
432
|
+
export declare function grantCoverage(scope: GrantScope): AlwaysCovers;
|
|
394
433
|
/**
|
|
395
434
|
* Which line-level facts the RULE that asked actually depends on. A grant binds
|
|
396
435
|
* the facts the rule fired on and nothing else: `gcloud_access_token_cmd` fired
|
|
@@ -414,6 +453,8 @@ export interface GrantRuleFacts {
|
|
|
414
453
|
* credential goes (issuer domain or exact hosts) and the secret read.
|
|
415
454
|
*/
|
|
416
455
|
credential?: CredentialRuleFacts;
|
|
456
|
+
/** The rule is a home / drive / root delete (`NEVER_GRANTED_ITEMS`): the scope carries a refusal. */
|
|
457
|
+
neverGrant?: boolean;
|
|
417
458
|
}
|
|
418
459
|
/**
|
|
419
460
|
* Derive `GrantRuleFacts` from the source of the ask. Unknown = bound (the
|