fullcourtdefense-cli 1.35.10 → 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.
Files changed (74) hide show
  1. package/dist/actionPolicyEngine.d.ts +24 -0
  2. package/dist/actionPolicyEngine.js +79 -19
  3. package/dist/approvalPrompt.d.ts +15 -0
  4. package/dist/approvalPrompt.js +157 -20
  5. package/dist/askDialog.d.ts +2 -0
  6. package/dist/askDialog.js +15 -1
  7. package/dist/askHeader.d.ts +81 -0
  8. package/dist/askHeader.js +573 -0
  9. package/dist/blockExplanation.d.ts +9 -0
  10. package/dist/blockExplanation.js +14 -0
  11. package/dist/callerEnv.d.ts +1 -1
  12. package/dist/callerEnv.js +14 -0
  13. package/dist/cloudGrant.d.ts +46 -0
  14. package/dist/cloudGrant.js +420 -0
  15. package/dist/cloudGrantTools.d.ts +97 -0
  16. package/dist/cloudGrantTools.js +1112 -0
  17. package/dist/commandNarrative.d.ts +8 -0
  18. package/dist/commandNarrative.js +222 -49
  19. package/dist/commands/cmdGuard.js +3 -1
  20. package/dist/commands/daemon.js +56 -2
  21. package/dist/commands/deterministicGuard.d.ts +70 -4
  22. package/dist/commands/deterministicGuard.js +494 -63
  23. package/dist/commands/discover.js +50 -16
  24. package/dist/commands/discoverAgentFiles.d.ts +2 -0
  25. package/dist/commands/discoverAgentFiles.js +1 -0
  26. package/dist/commands/discoverPaths.d.ts +4 -0
  27. package/dist/commands/discoverPaths.js +19 -1
  28. package/dist/commands/discoverProjectRoots.d.ts +72 -0
  29. package/dist/commands/discoverProjectRoots.js +327 -0
  30. package/dist/commands/discoverProxy.d.ts +1 -1
  31. package/dist/commands/discoverProxy.js +1 -1
  32. package/dist/commands/discoverSecrets.d.ts +44 -0
  33. package/dist/commands/discoverSecrets.js +158 -14
  34. package/dist/commands/hook.js +25 -10
  35. package/dist/commands/knownMcpServers.d.ts +8 -2
  36. package/dist/commands/knownMcpServers.js +33 -2
  37. package/dist/commands/mcpGateway.js +9 -7
  38. package/dist/commands/posixShellGuard.js +1 -1
  39. package/dist/commands/shellGuard.js +86 -26
  40. package/dist/commands/uninstallAll.d.ts +1 -7
  41. package/dist/commands/uninstallAll.js +6 -45
  42. package/dist/commands/watchdog.js +17 -3
  43. package/dist/config.d.ts +1 -1
  44. package/dist/config.js +3 -3
  45. package/dist/credentialGrant.d.ts +48 -0
  46. package/dist/credentialGrant.js +29 -18
  47. package/dist/credentialStore.js +1 -1
  48. package/dist/devConfirm.d.ts +51 -3
  49. package/dist/devConfirm.js +226 -33
  50. package/dist/distress.d.ts +2 -0
  51. package/dist/distress.js +2 -0
  52. package/dist/grantTargets.d.ts +21 -0
  53. package/dist/grantTargets.js +235 -0
  54. package/dist/localSafetySnapshot.js +2 -0
  55. package/dist/msiProductCode.d.ts +10 -0
  56. package/dist/msiProductCode.js +124 -0
  57. package/dist/notify.js +1 -0
  58. package/dist/selfTest.d.ts +1 -1
  59. package/dist/selfTest.js +1 -1
  60. package/dist/selfUpdate.d.ts +108 -0
  61. package/dist/selfUpdate.js +353 -18
  62. package/dist/shellAst.d.ts +2 -0
  63. package/dist/shellAst.js +7 -1
  64. package/dist/shellAstLoader.js +14 -1
  65. package/dist/telemetry.d.ts +3 -0
  66. package/dist/telemetry.js +1 -0
  67. package/dist/version.json +1 -1
  68. package/installer/macos/guardian/ai.fullcourtdefense.guardian.plist +19 -0
  69. package/installer/macos/guardian/fcd-guardian.sh +590 -0
  70. package/installer/macos/guardian/install-guardian.sh +32 -0
  71. package/installer/macos/guardian/uninstall-guardian.sh +12 -0
  72. package/installer/windows/README.md +88 -0
  73. package/installer/windows/rescue/FullCourtDefense-Rescue.ps1 +229 -0
  74. package/package.json +11 -1
@@ -0,0 +1,46 @@
1
+ import { type AwsDefaults, type AzDefaults, type KubeDefaults, type TfDefaults } from './cloudGrantTools';
2
+ export type CloudGrantRefusal = 'delete' | 'permissions' | 'protected' | 'environment_unread' | 'target_unknown';
3
+ /** Resolves the project a gcloud use falls back to (env / active configuration). `null` = unknown. */
4
+ export type DefaultProject = (configuration: string | undefined) => string | null;
5
+ /**
6
+ * The cloud entries for one shell line: sorted, unique, one per cloud CLI use.
7
+ * `uses` are the program uses the grant scanner found (`scanShellPrograms`).
8
+ * Empty when the line runs no cloud CLI. `tools` are the aws / kube defaults
9
+ * (tests pass fixtures); omitted = read from the caller's environment.
10
+ * `destinationBound`: the grant already binds where data goes (`unreadTarget`).
11
+ */
12
+ export declare function cloudGrantEntries(command: string, uses: ReadonlyArray<{
13
+ name: string;
14
+ text: string;
15
+ }>, defaultProject: DefaultProject, tools?: {
16
+ aws?: AwsDefaults;
17
+ kube?: KubeDefaults;
18
+ az?: AzDefaults;
19
+ tf?: TfDefaults;
20
+ destinationBound?: boolean;
21
+ cwd?: string;
22
+ }): string[];
23
+ /** Why a line with these entries cannot carry an Always allow; undefined when it can. */
24
+ export declare function cloudGrantRefusal(entries: readonly string[] | undefined): CloudGrantRefusal | undefined;
25
+ /** Every live entry must be in the grant. A grant recorded before entries existed covers no cloud line. */
26
+ export declare function cloudGrantCovers(granted: readonly string[] | undefined, live: readonly string[] | undefined): boolean;
27
+ /** One short line for the dialog when Always allow is not offered. */
28
+ export declare function describeCloudRefusal(refusal: CloudGrantRefusal, entries?: readonly string[]): string;
29
+ /** How / Where in plain words, for the dialog and the grant list. */
30
+ export declare function describeCloudEntries(entries: readonly string[]): {
31
+ how: string;
32
+ where: string;
33
+ };
34
+ /**
35
+ * The environment names in cloud entries — gcloud projects, aws profiles,
36
+ * kube contexts — for the dialog's by-name environment tag. The tag itself is
37
+ * the dialog's one helper; this only says which names it applies to.
38
+ */
39
+ export declare function cloudEntryEnvironmentNames(entries: readonly string[]): string[];
40
+ /**
41
+ * The project gcloud uses when the command names none: CLOUDSDK_CORE_PROJECT,
42
+ * else `[core] project` of the active configuration (`CLOUDSDK_ACTIVE_CONFIG_NAME`,
43
+ * else the `active_config` file, else `default`) under CLOUDSDK_CONFIG or the
44
+ * per-user gcloud directory. Any read error or odd value = null (unknown).
45
+ */
46
+ export declare function activeGcloudProject(configuration?: string, env?: NodeJS.ProcessEnv): string | null;
@@ -0,0 +1,420 @@
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.cloudGrantEntries = cloudGrantEntries;
37
+ exports.cloudGrantRefusal = cloudGrantRefusal;
38
+ exports.cloudGrantCovers = cloudGrantCovers;
39
+ exports.describeCloudRefusal = describeCloudRefusal;
40
+ exports.describeCloudEntries = describeCloudEntries;
41
+ exports.cloudEntryEnvironmentNames = cloudEntryEnvironmentNames;
42
+ exports.activeGcloudProject = activeGcloudProject;
43
+ /**
44
+ * "Always allow" for a cloud CLI covers WHAT + HOW + WHERE (#1747).
45
+ *
46
+ * A shell grant used to remember only the programs on the line, so Always
47
+ * allow on `gcloud run deploy --project acme-staging` also covered
48
+ * `gcloud run services delete … --project acme-prod` under the same rule.
49
+ * Each gcloud use on the line now adds one entry `gcloud|<how>|<where>`:
50
+ *
51
+ * how read | change | permissions | delete — from the verb, with the
52
+ * engine's gcloud grammar (`gcloudVerbWord`, `GCLOUD_*_VERB_RE`).
53
+ * A verb that grammar does not know is bound to its exact command
54
+ * path (`cmd:builds submit`), never to a class.
55
+ * where `project=<id>` — `--project`, a `projects/<id>/…` resource, the
56
+ * `gcloud projects <verb> <id>` operand, else CLOUDSDK_CORE_PROJECT,
57
+ * else the active gcloud configuration; `?` when it cannot be read.
58
+ * Account-level groups (`auth`, `config`, …) act on no project and
59
+ * add no entry; a `config` change on the line makes every default `?`.
60
+ *
61
+ * aws and kubectl / oc / helm use the same format with their own how / where
62
+ * (`cloudGrantTools.ts`: profile + region, context + namespace). Every other
63
+ * use that acts outside the machine on a target not read yet (az, terraform,
64
+ * `git push`, psql, ssh, vault, a `curl` POST, … — `grantTargets.ts`) adds
65
+ * `<label>|?|?`: no Always allow for it.
66
+ *
67
+ * Deletes, permission changes and an unreadable target get no Always allow
68
+ * (`cloudGrantRefusal`). Grants match by inclusion: every live entry must be
69
+ * in the grant. Deterministic words and flags only; the I/O is reading the
70
+ * tools' configuration files (`activeGcloudProject`, `awsDefaultsFrom`,
71
+ * `kubeDefaultsFrom`), never running a CLI.
72
+ */
73
+ const fs = __importStar(require("fs"));
74
+ const os = __importStar(require("os"));
75
+ const path = __importStar(require("path"));
76
+ const callerEnv_1 = require("./callerEnv");
77
+ const cloudGrantTools_1 = require("./cloudGrantTools");
78
+ const credentialGrant_1 = require("./credentialGrant");
79
+ const grantTargets_1 = require("./grantTargets");
80
+ const shellQuoteMask_1 = require("./shellQuoteMask");
81
+ let grammar;
82
+ /** The engine is only needed when a line runs gcloud; the ask-dialog process never loads it. */
83
+ function gcloudGrammar() {
84
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
85
+ return grammar || (grammar = require('./actionPolicyEngine'));
86
+ }
87
+ const KUBE_TOOLS = new Set(['kubectl', 'oc', 'helm']);
88
+ /** The flag a person adds so the target can be read, per tool. */
89
+ const TARGET_HINT = {
90
+ gcloud: 'Always allow needs the project on the command: add --project.',
91
+ aws: 'Always allow needs the AWS profile and region on the command: add --profile and --region.',
92
+ kubectl: 'Always allow needs the cluster on the command: add --context and --namespace.',
93
+ oc: 'Always allow needs the cluster on the command: add --context and --namespace.',
94
+ helm: 'Always allow needs the cluster on the command: add --kube-context and --namespace.',
95
+ git: 'Always allow needs the remote and branch on the command: git push <remote> <branch>.',
96
+ az: 'Always allow needs a subscription this machine knows (its ID, or a name listed by az account list) and the resource group: add --subscription and -g.',
97
+ terraform: 'Always allow needs the terraform directory and workspace: use -chdir and -var-file (not -var) from a known folder.',
98
+ tofu: 'Always allow needs the tofu directory and workspace: use -chdir and -var-file (not -var) from a known folder.',
99
+ terragrunt: 'Always allow needs the terragrunt directory and workspace: one module, --working-dir, from a known folder.',
100
+ ssh: 'Always allow needs the host on the command, without -F or a ProxyCommand / HostName override.',
101
+ scp: 'Always allow needs the host on the command, without -F or a ProxyCommand / HostName override.',
102
+ sftp: 'Always allow needs the host on the command, without -F or a ProxyCommand / HostName override.',
103
+ rsync: 'Always allow needs the host on the command and plain ssh as the transport.',
104
+ psql: 'Always allow needs the database host and name on the command: add -h and -d.',
105
+ mysql: 'Always allow needs the database host and name on the command: add -h and the database.',
106
+ mariadb: 'Always allow needs the database host and name on the command: add -h and the database.',
107
+ };
108
+ /** Groups that act on the account or the local SDK, not on a project. */
109
+ const ACCOUNT_GROUPS = new Set(['auth', 'config', 'components', 'info', 'version', 'help', 'topic', 'init', 'survey', 'feedback', 'cheat-sheet']);
110
+ /** Release-track words that sit before the group. */
111
+ const TRACK_WORDS = new Set(['alpha', 'beta', 'preview']);
112
+ /** Command words that remove data although the engine grammar has no verb for them (`storage rm`). */
113
+ const DELETE_WORD_RE = /^(?:rm|purge)$/;
114
+ /** Global flags that take a separate value (`--project acme`); their value is not a command word. */
115
+ const VALUED_GLOBAL_FLAGS = new Set(['--project', '--configuration', '--account', '--billing-project', '--impersonate-service-account', '--format', '--verbosity', '--flags-file', '--region', '--zone', '--filter', '--limit', '--sort-by', '--page-size']);
116
+ const COMMAND_WORD_RE = /^[a-z][a-z0-9-]*$/;
117
+ /** A GCP project id or number, including domain-scoped `example.com:proj`. */
118
+ const PROJECT_ID_RE = /^(?:[a-z][a-z0-9.-]*:)?[a-z0-9][a-z0-9-]{3,62}$/;
119
+ const CONFIG_NAME_RE = /^[a-z][a-z0-9-]*$/;
120
+ /** A word as the grammar reads it: lowercase; a quoted value with spaces or code is no command word. */
121
+ function grammarWord(word) {
122
+ const bare = (0, credentialGrant_1.unquote)(word);
123
+ return /^["']/.test(word) && /[\s|;&$`()]/.test(bare) ? '"…"' : bare.toLowerCase();
124
+ }
125
+ /**
126
+ * Group / command words with their index: up to and including the verb when
127
+ * the grammar knows it, else up to the first flag once two words are read.
128
+ */
129
+ function commandPath(words, verb) {
130
+ const out = [];
131
+ for (let k = 0; k < words.length && out.length < 6; k++) {
132
+ const word = words[k];
133
+ if (word.startsWith('-')) {
134
+ if (!verb && out.length >= 2)
135
+ break;
136
+ if (VALUED_GLOBAL_FLAGS.has(word))
137
+ k++;
138
+ continue;
139
+ }
140
+ if (!COMMAND_WORD_RE.test(word))
141
+ break;
142
+ if (!out.length && TRACK_WORDS.has(word))
143
+ continue;
144
+ out.push({ word, at: k });
145
+ if (word === verb)
146
+ break;
147
+ }
148
+ return out;
149
+ }
150
+ function classify(verb, cmdPath) {
151
+ const { GCLOUD_READ_VERB_RE, GCLOUD_WRITE_VERB_RE, GCLOUD_DELETE_VERB_RE } = gcloudGrammar();
152
+ if (verb && GCLOUD_DELETE_VERB_RE.test(verb))
153
+ return 'delete';
154
+ if (verb && GCLOUD_READ_VERB_RE.test(verb))
155
+ return 'read';
156
+ if (verb && GCLOUD_WRITE_VERB_RE.test(verb))
157
+ return /iam-policy/.test(verb) || cmdPath.includes('iam') ? 'permissions' : 'change';
158
+ if (cmdPath.some(word => DELETE_WORD_RE.test(word)))
159
+ return 'delete';
160
+ return `cmd:${cmdPath.join(' ') || 'gcloud'}`;
161
+ }
162
+ /** The project this use names itself: flag, resource path or `projects <verb> <id>`. `?` when dynamic or ambiguous. */
163
+ function namedProject(argv, cmdPath, verbAt, vars) {
164
+ const named = new Set();
165
+ for (let k = 0; k < argv.length; k++) {
166
+ const bare = (0, credentialGrant_1.unquote)(argv[k]);
167
+ // A flags file can set any flag, the project included.
168
+ if (bare === '--flags-file' || bare.startsWith('--flags-file='))
169
+ return '?';
170
+ if (bare !== '--project' && !bare.startsWith('--project='))
171
+ continue;
172
+ const value = (0, credentialGrant_1.flagValue)(argv.slice(k, k + 2), ['--project'], vars);
173
+ if (!value)
174
+ return '?';
175
+ named.add(value.toLowerCase());
176
+ }
177
+ for (const word of argv) {
178
+ const bare = (0, credentialGrant_1.unquote)(word);
179
+ const value = bare.startsWith('-') ? bare.slice(bare.indexOf('=') + 1) : bare;
180
+ const ref = value.match(/(?:^|\/)projects\/([^/\s]+)/i);
181
+ if (!ref)
182
+ continue;
183
+ if (/[$`(){}%]/.test(ref[1]))
184
+ return '?';
185
+ named.add(ref[1].toLowerCase());
186
+ }
187
+ if (cmdPath[0] === 'projects' && cmdPath.length === 2) {
188
+ // `gcloud projects <verb> <id>`: the operand IS the project. Unknown verb = no operand read.
189
+ const operand = verbAt >= 0 ? argv.slice(verbAt + 1).find(w => !(0, credentialGrant_1.unquote)(w).startsWith('-')) : undefined;
190
+ if (operand === undefined && verbAt >= 0)
191
+ return cmdPath[1] === 'list' ? 'account' : '?';
192
+ if (operand !== undefined) {
193
+ const bare = (0, credentialGrant_1.unquote)(operand);
194
+ if (/[$`(){}%]/.test(bare))
195
+ return '?';
196
+ named.add(bare.toLowerCase());
197
+ }
198
+ }
199
+ if (named.size > 1)
200
+ return '?';
201
+ const only = [...named][0];
202
+ if (only === undefined)
203
+ return undefined;
204
+ return PROJECT_ID_RE.test(only) ? `project=${only}` : '?';
205
+ }
206
+ function readUse(text, dialect, vars) {
207
+ const argv = (0, cloudGrantTools_1.toolArgv)(text, dialect, ['gcloud']);
208
+ const words = argv.map(grammarWord);
209
+ const verb = gcloudGrammar().gcloudVerbWord(['gcloud', ...words].join(' '));
210
+ const pathWords = commandPath(words, verb);
211
+ const cmdPath = pathWords.map(p => p.word);
212
+ const how = classify(verb, cmdPath);
213
+ const mutatesConfig = (cmdPath[0] === 'config' || cmdPath[0] === 'init') && how !== 'read';
214
+ // `gcloud --version` / `gcloud help …` names no group: local SDK, no project.
215
+ if (!cmdPath.length || ACCOUNT_GROUPS.has(cmdPath[0]))
216
+ return { how, where: 'account', mutatesConfig };
217
+ const verbAt = verb && pathWords.length && pathWords[pathWords.length - 1].word === verb ? pathWords[pathWords.length - 1].at : -1;
218
+ const where = namedProject(argv, cmdPath, verbAt, vars);
219
+ const configuration = (0, credentialGrant_1.flagValue)(argv, ['--configuration'], vars);
220
+ return { how, where, configuration, mutatesConfig };
221
+ }
222
+ /**
223
+ * The cloud entries for one shell line: sorted, unique, one per cloud CLI use.
224
+ * `uses` are the program uses the grant scanner found (`scanShellPrograms`).
225
+ * Empty when the line runs no cloud CLI. `tools` are the aws / kube defaults
226
+ * (tests pass fixtures); omitted = read from the caller's environment.
227
+ * `destinationBound`: the grant already binds where data goes (`unreadTarget`).
228
+ */
229
+ function cloudGrantEntries(command, uses, defaultProject, tools) {
230
+ const entries = new Set();
231
+ const dialect = (0, shellQuoteMask_1.shellDialectOf)(command);
232
+ const env = (0, callerEnv_1.callerEnv)();
233
+ for (const use of uses) {
234
+ const unread = (0, grantTargets_1.unreadTarget)(use.name, (0, cloudGrantTools_1.toolArgv)(use.text, dialect, [use.name]), env, Boolean(tools?.destinationBound));
235
+ if (unread)
236
+ entries.add(`${unread.label}|?|?`);
237
+ }
238
+ const aws = uses.filter(use => use.name === 'aws');
239
+ const kube = uses.filter(use => KUBE_TOOLS.has(use.name));
240
+ const gcloud = uses.filter(use => use.name === 'gcloud');
241
+ const git = uses.filter(use => use.name === 'git');
242
+ const az = uses.filter(use => use.name === 'az');
243
+ const tf = uses.filter(use => (0, cloudGrantTools_1.isTfTool)(use.name));
244
+ const remote = uses.filter(use => (0, cloudGrantTools_1.isRemoteTool)(use.name));
245
+ const sql = uses.filter(use => (0, cloudGrantTools_1.isSqlTool)(use.name));
246
+ if (!aws.length && !kube.length && !gcloud.length && !git.length && !az.length && !tf.length && !remote.length && !sql.length)
247
+ return [...entries].sort();
248
+ const vars = (0, credentialGrant_1.lineVars)(command, dialect);
249
+ const toolUses = [
250
+ ...(0, cloudGrantTools_1.sqlUses)(sql.map(use => ({ tool: use.name, text: use.text })), command, dialect, vars, env),
251
+ ...(0, cloudGrantTools_1.remoteUses)(remote.map(use => ({ tool: use.name, text: use.text })), dialect, vars),
252
+ ...(tf.length ? (0, cloudGrantTools_1.tfUses)(tf.map(use => ({ tool: use.name, text: use.text })), command, dialect, vars, tools?.tf || (0, cloudGrantTools_1.tfDefaultsFrom)(env), tools?.cwd) : []),
253
+ ...(az.length ? (0, cloudGrantTools_1.azUses)(az.map(use => use.text), command, dialect, vars, tools?.az || (0, cloudGrantTools_1.azDefaultsFrom)(env)) : []),
254
+ ...git.map(use => (0, cloudGrantTools_1.gitPushUse)((0, cloudGrantTools_1.toolArgv)(use.text, dialect, ['git']), vars, tools?.cwd)).filter((use) => Boolean(use)),
255
+ ...(aws.length ? (0, cloudGrantTools_1.awsUses)(aws.map(use => use.text), command, dialect, vars, tools?.aws || (0, cloudGrantTools_1.awsDefaultsFrom)((0, callerEnv_1.callerEnv)())) : []),
256
+ ...(kube.length ? (0, cloudGrantTools_1.kubeUses)(kube.map(use => ({ tool: use.name, text: use.text })), command, dialect, vars, tools?.kube || (0, cloudGrantTools_1.kubeDefaultsFrom)((0, callerEnv_1.callerEnv)())) : []),
257
+ ];
258
+ // Local and sign-in commands act on no environment: no entry, like gcloud's account-level groups.
259
+ for (const use of toolUses)
260
+ if (use.where !== undefined)
261
+ entries.add(`${use.tool}|${use.how}|${use.where}`);
262
+ if (!gcloud.length)
263
+ return [...entries].sort();
264
+ const read = gcloud.map(use => readUse(use.text, dialect, vars));
265
+ // The line itself switches project or configuration: nothing read from the defaults holds.
266
+ const defaultsUnreliable = read.some(use => use.mutatesConfig) || /\bcloudsdk_/i.test(command);
267
+ for (const use of read) {
268
+ // Account-level and local-SDK commands touch no project: the program binding (and, for a
269
+ // credential rule, the credential binding) already says everything a grant needs.
270
+ if (use.where === 'account')
271
+ continue;
272
+ let where = use.where;
273
+ if (where === undefined) {
274
+ if (use.configuration === null || defaultsUnreliable)
275
+ where = '?';
276
+ else {
277
+ const project = defaultProject(use.configuration);
278
+ where = project && PROJECT_ID_RE.test(project.toLowerCase()) ? `project=${project.toLowerCase()}` : '?';
279
+ }
280
+ }
281
+ entries.add(`gcloud|${use.how}|${where}`);
282
+ }
283
+ return [...entries].sort();
284
+ }
285
+ /** Why a line with these entries cannot carry an Always allow; undefined when it can. */
286
+ function cloudGrantRefusal(entries) {
287
+ if (!entries || !entries.length)
288
+ return undefined;
289
+ const parts = entries.map(entry => entry.split('|'));
290
+ if (parts.some(p => p[1] === 'delete'))
291
+ return 'delete';
292
+ if (parts.some(p => p[1] === 'permissions'))
293
+ return 'permissions';
294
+ if (parts.some(p => p[1] === 'protected'))
295
+ return 'protected';
296
+ if (parts.some(p => p[1] === '?'))
297
+ return 'environment_unread';
298
+ if (parts.some(p => targetUnknown(p[2])))
299
+ return 'target_unknown';
300
+ return undefined;
301
+ }
302
+ /** `?`, or any `key=?` part of a compound where (`profile=dev,region=?`). */
303
+ function targetUnknown(where) {
304
+ return !where || where.split(',').some(part => part === '?' || part.endsWith('=?'));
305
+ }
306
+ /** The tools of the entries that match, sorted, as one phrase: `aws, kubectl`. */
307
+ function toolsWhere(entries, pick) {
308
+ return [...new Set(entries.map(entry => entry.split('|')).filter(pick).map(p => p[0]))].sort().join(', ');
309
+ }
310
+ /** Every live entry must be in the grant. A grant recorded before entries existed covers no cloud line. */
311
+ function cloudGrantCovers(granted, live) {
312
+ if (!live || !live.length)
313
+ return true;
314
+ return Array.isArray(granted) && live.every(entry => granted.includes(entry));
315
+ }
316
+ /** One short line for the dialog when Always allow is not offered. */
317
+ function describeCloudRefusal(refusal, entries = []) {
318
+ // Without entries the wording is gcloud's, as before other tools had entries.
319
+ if (refusal === 'delete')
320
+ return `Always allow is not offered for ${toolsWhere(entries, p => p[1] === 'delete') || 'gcloud'} deletes.`;
321
+ if (refusal === 'permissions')
322
+ return `Always allow is not offered for ${toolsWhere(entries, p => p[1] === 'permissions') || 'gcloud'} permission changes.`;
323
+ if (refusal === 'protected')
324
+ return 'Always allow is not offered for a force push, a tag push (--tags, --follow-tags, --all) or a push to main, master, release* or prod*.';
325
+ if (refusal === 'environment_unread') {
326
+ const labels = toolsWhere(entries, p => p[1] === '?');
327
+ const nouns = new Set(labels.split(', ').map(grantTargets_1.unreadNoun));
328
+ return `Always allow is not offered for ${labels} until the ${nouns.size === 1 ? [...nouns][0] : 'target'} can be read.`;
329
+ }
330
+ const tool = toolsWhere(entries, p => targetUnknown(p[2])).split(', ')[0];
331
+ return TARGET_HINT[tool] || TARGET_HINT.gcloud;
332
+ }
333
+ /** `profile=dev,region=eu-west-1` -> `profile dev, region eu-west-1`. */
334
+ function whereWords(tool, where) {
335
+ if (where === '?' || !where)
336
+ return tool === 'gcloud' ? 'unknown project' : 'unknown environment';
337
+ const parts = where.split(',');
338
+ const fromDefault = parts.includes('profile-from=default');
339
+ const words = parts.filter(part => part !== 'profile-from=default').map(part => {
340
+ const at = part.indexOf('=');
341
+ const key = part.slice(0, at);
342
+ const value = part.slice(at + 1);
343
+ if (value === '?')
344
+ return `unknown ${key}`;
345
+ if (value === '-')
346
+ return `no ${key === 'group' ? 'resource group' : key}`;
347
+ if (key === 'namespace' && value === '*')
348
+ return 'all namespaces';
349
+ return `${key} ${key === 'bucket' ? value.split('+').join(' + ') : value}`;
350
+ }).join(', ');
351
+ return fromDefault ? `${words} (profile from the default config — add --profile to bind it)` : words;
352
+ }
353
+ /** How / Where in plain words, for the dialog and the grant list. */
354
+ function describeCloudEntries(entries) {
355
+ const hows = new Set();
356
+ const wheres = new Set();
357
+ for (const entry of entries) {
358
+ const [tool, how = '', where = ''] = entry.split('|');
359
+ hows.add(how === '?' ? 'unknown' : how.startsWith('cmd:') ? how.slice(4) : how);
360
+ wheres.add(whereWords(tool, where));
361
+ }
362
+ const list = [...wheres];
363
+ return { how: [...hows].join(', '), where: list.join(list.some(w => w.includes(', ')) ? '; ' : ', ') };
364
+ }
365
+ /**
366
+ * The environment names in cloud entries — gcloud projects, aws profiles,
367
+ * kube contexts — for the dialog's by-name environment tag. The tag itself is
368
+ * the dialog's one helper; this only says which names it applies to.
369
+ */
370
+ function cloudEntryEnvironmentNames(entries) {
371
+ const names = new Set();
372
+ for (const entry of entries) {
373
+ for (const part of (entry.split('|')[2] || '').split(',')) {
374
+ const [key, value] = [part.slice(0, part.indexOf('=')), part.slice(part.indexOf('=') + 1)];
375
+ if ((key === 'project' || key === 'profile' || key === 'context') && value && value !== '?')
376
+ names.add(value);
377
+ }
378
+ }
379
+ return [...names];
380
+ }
381
+ /**
382
+ * The project gcloud uses when the command names none: CLOUDSDK_CORE_PROJECT,
383
+ * else `[core] project` of the active configuration (`CLOUDSDK_ACTIVE_CONFIG_NAME`,
384
+ * else the `active_config` file, else `default`) under CLOUDSDK_CONFIG or the
385
+ * per-user gcloud directory. Any read error or odd value = null (unknown).
386
+ */
387
+ function activeGcloudProject(configuration, env = (0, callerEnv_1.callerEnv)()) {
388
+ try {
389
+ const fromEnv = (env.CLOUDSDK_CORE_PROJECT || '').trim();
390
+ if (fromEnv)
391
+ return fromEnv;
392
+ const home = os.homedir();
393
+ const dir = (env.CLOUDSDK_CONFIG || '').trim()
394
+ || (process.platform === 'win32' ? path.join((env.APPDATA || '').trim() || path.join(home, 'AppData', 'Roaming'), 'gcloud') : path.join(home, '.config', 'gcloud'));
395
+ let name = (configuration || (env.CLOUDSDK_ACTIVE_CONFIG_NAME || '')).trim();
396
+ if (!name) {
397
+ const activeFile = path.join(dir, 'active_config');
398
+ name = fs.existsSync(activeFile) ? fs.readFileSync(activeFile, 'utf8').trim() : 'default';
399
+ }
400
+ if (!CONFIG_NAME_RE.test(name))
401
+ return null;
402
+ const ini = fs.readFileSync(path.join(dir, 'configurations', `config_${name}`), 'utf8');
403
+ let section = '';
404
+ for (const raw of ini.split(/\r?\n/)) {
405
+ const row = raw.trim();
406
+ const header = row.match(/^\[([^\]]+)\]$/);
407
+ if (header) {
408
+ section = header[1].trim().toLowerCase();
409
+ continue;
410
+ }
411
+ const kv = row.match(/^project\s*=\s*(.*)$/i);
412
+ if (section === 'core' && kv)
413
+ return kv[1].trim() || null;
414
+ }
415
+ return null;
416
+ }
417
+ catch {
418
+ return null;
419
+ }
420
+ }
@@ -0,0 +1,97 @@
1
+ import { type LineVars } from './credentialGrant';
2
+ import type { ShellDialect } from './shellQuoteMask';
3
+ export interface ToolUse {
4
+ tool: string;
5
+ how: string;
6
+ /** Undefined when the use acts on no environment (local / account-level): no entry. */
7
+ where?: string;
8
+ /** The use rewrites the tool's own defaults (`aws configure set`, `kubectl config use-context`). */
9
+ mutatesDefaults: boolean;
10
+ }
11
+ /**
12
+ * The words after the program on one use's text; empty when the program is not found.
13
+ * Redirections are the shell's, not arguments to the program: `git push origin x 2>&1`
14
+ * pushes the same branch as `git push origin x`.
15
+ */
16
+ export declare function toolArgv(text: string, dialect: ShellDialect, names: readonly string[]): string[];
17
+ /** The profile / region aws uses when the command names none. `null` = unknown. */
18
+ export interface AwsDefaults {
19
+ profile(): string | null;
20
+ region(profile: string): string | null;
21
+ }
22
+ /**
23
+ * `AWS_PROFILE` (else `AWS_DEFAULT_PROFILE`), else `default` when that profile
24
+ * exists in `~/.aws/config` or `~/.aws/credentials`. Static keys in the
25
+ * environment (`AWS_ACCESS_KEY_ID`) beat any profile: unknown. Region:
26
+ * `AWS_REGION`, else `AWS_DEFAULT_REGION`, else the profile's `region` in the
27
+ * config file (`AWS_CONFIG_FILE` or `~/.aws/config`).
28
+ */
29
+ export declare function awsDefaultsFrom(env: NodeJS.ProcessEnv): AwsDefaults;
30
+ /** aws uses on the line. `awsLineUnreliable`: the line itself names an AWS_* setting. */
31
+ export declare function awsUses(texts: readonly string[], command: string, dialect: ShellDialect, vars: LineVars, defaults: AwsDefaults): ToolUse[];
32
+ /** The subscription / resource group az falls back to. `null` = unknown. */
33
+ export interface AzDefaults {
34
+ /** The default subscription id (`isDefault` in azureProfile.json). */
35
+ subscription(): string | null;
36
+ /** A subscription name or id -> its id, from azureProfile.json. */
37
+ subscriptionId(nameOrId: string): string | null;
38
+ /** `AZURE_DEFAULTS_GROUP`, else `[defaults] group` in the az config file. */
39
+ group(): string | null;
40
+ }
41
+ /** azureProfile.json / config under AZURE_CONFIG_DIR or `~/.azure`. */
42
+ export declare function azDefaultsFrom(env: NodeJS.ProcessEnv): AzDefaults;
43
+ /** az uses on the line; a line that sets az defaults (`az account set`, `az config set`, `AZURE_*=`) makes every default unknown. */
44
+ export declare function azUses(texts: readonly string[], command: string, dialect: ShellDialect, vars: LineVars, defaults: AzDefaults): ToolUse[];
45
+ /** The workspace a terraform use falls back to. `null` = unknown. */
46
+ export interface TfDefaults {
47
+ /** `TF_WORKSPACE`, else `<dir>/.terraform/environment`, else `default` — when `cwd` is known. */
48
+ workspace(dir: string, cwd: string | undefined): string | null;
49
+ }
50
+ export declare function tfDefaultsFrom(env: NodeJS.ProcessEnv): TfDefaults;
51
+ /** terraform / tofu / terragrunt uses; `workspace select` or a `TF_*` assignment on the line makes the workspace unknown. */
52
+ export declare function tfUses(uses: ReadonlyArray<{
53
+ tool: string;
54
+ text: string;
55
+ }>, command: string, dialect: ShellDialect, vars: LineVars, defaults: TfDefaults, cwd: string | undefined): ToolUse[];
56
+ export declare function isTfTool(name: string): boolean;
57
+ export declare function isRemoteTool(name: string): boolean;
58
+ export declare function remoteUses(uses: ReadonlyArray<{
59
+ tool: string;
60
+ text: string;
61
+ }>, dialect: ShellDialect, vars: LineVars): ToolUse[];
62
+ /** The worst class of the statements in one SQL string. */
63
+ export declare function sqlHow(sql: string): string;
64
+ export declare function isSqlTool(name: string): boolean;
65
+ export declare function sqlUses(uses: ReadonlyArray<{
66
+ tool: string;
67
+ text: string;
68
+ }>, command: string, dialect: ShellDialect, vars: LineVars, env: NodeJS.ProcessEnv): ToolUse[];
69
+ /** The branch `HEAD` names in the repository at `dir` (walking up to `.git`), or null. */
70
+ export declare function currentGitBranch(dir: string): string | null;
71
+ /**
72
+ * `git push`: how = change, or `protected` (a force push, `--all` / `--tags` /
73
+ * `--follow-tags`, a push to main / master / release* / prod*), or delete
74
+ * (`--delete`, `:branch`, `--mirror`, `--prune`); `--dry-run` = read. Where =
75
+ * `remote=<name>,branch=<destination>` — the remote and refspecs on the command;
76
+ * `HEAD` / no refspec is the current branch, read from `.git/HEAD` only when the
77
+ * working directory is known (`cwd`), else unknown.
78
+ */
79
+ export declare function gitPushUse(argv: readonly string[], vars: LineVars, cwd: string | undefined): ToolUse | undefined;
80
+ /** The context and its namespace a kube use falls back to. `null` = unknown. */
81
+ export interface KubeDefaults {
82
+ context(kubeconfig: string | undefined): string | null;
83
+ namespace(context: string, kubeconfig: string | undefined): string | null;
84
+ /** helm's own environment (`HELM_KUBECONTEXT`, `HELM_NAMESPACE`); `unknown` when it points helm elsewhere. */
85
+ helm: {
86
+ context?: string;
87
+ namespace?: string;
88
+ unknown: boolean;
89
+ };
90
+ }
91
+ /** `--kubeconfig`, else KUBECONFIG (a path list), else `~/.kube/config`. */
92
+ export declare function kubeDefaultsFrom(env: NodeJS.ProcessEnv): KubeDefaults;
93
+ /** kubectl / oc / helm uses on the line, in order. */
94
+ export declare function kubeUses(uses: ReadonlyArray<{
95
+ tool: string;
96
+ text: string;
97
+ }>, command: string, dialect: ShellDialect, vars: LineVars, defaults: KubeDefaults): ToolUse[];