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
|
@@ -0,0 +1,1112 @@
|
|
|
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.toolArgv = toolArgv;
|
|
37
|
+
exports.awsDefaultsFrom = awsDefaultsFrom;
|
|
38
|
+
exports.awsUses = awsUses;
|
|
39
|
+
exports.azDefaultsFrom = azDefaultsFrom;
|
|
40
|
+
exports.azUses = azUses;
|
|
41
|
+
exports.tfDefaultsFrom = tfDefaultsFrom;
|
|
42
|
+
exports.tfUses = tfUses;
|
|
43
|
+
exports.isTfTool = isTfTool;
|
|
44
|
+
exports.isRemoteTool = isRemoteTool;
|
|
45
|
+
exports.remoteUses = remoteUses;
|
|
46
|
+
exports.sqlHow = sqlHow;
|
|
47
|
+
exports.isSqlTool = isSqlTool;
|
|
48
|
+
exports.sqlUses = sqlUses;
|
|
49
|
+
exports.currentGitBranch = currentGitBranch;
|
|
50
|
+
exports.gitPushUse = gitPushUse;
|
|
51
|
+
exports.kubeDefaultsFrom = kubeDefaultsFrom;
|
|
52
|
+
exports.kubeUses = kubeUses;
|
|
53
|
+
/**
|
|
54
|
+
* WHAT + HOW + WHERE for aws and the Kubernetes CLIs (kubectl, oc, helm) —
|
|
55
|
+
* the entries `cloudGrant.ts` adds to a shell grant, same format as gcloud:
|
|
56
|
+
* `<tool>|<how>|<where>`.
|
|
57
|
+
*
|
|
58
|
+
* aws how from service + operation: `rm` / `rb` / `delete-*` / `terminate-*` /
|
|
59
|
+
* `remove-*` / `deregister-*` / `purge-*` = delete; `iam`, `sso-admin`,
|
|
60
|
+
* `identitystore`, `*-policy` / `*-permission` / `*-acl` / `*-grant` writes,
|
|
61
|
+
* `create-access-key`, `authorize-*` / `revoke-*` = permissions;
|
|
62
|
+
* `describe-*` / `list-*` / `get-*` / `ls` / `head-*` / `wait` = read; else change.
|
|
63
|
+
* where `profile=<p>,region=<r>` (+ `,account=<12 digits>` from an ARN,
|
|
64
|
+
* + `,bucket=<a>+<b>` from `s3://…` / `--bucket`).
|
|
65
|
+
* kube how from the verb: delete / uninstall = delete; `create (cluster)role(binding)` /
|
|
66
|
+
* `create token` / `auth` / `certificate approve|deny` / `oc adm policy` /
|
|
67
|
+
* `oc policy` = permissions; get / describe / logs / top / … = read;
|
|
68
|
+
* exec / attach / cp / port-forward / debug / proxy = `exec` (a shell or a
|
|
69
|
+
* tunnel into a workload: its own class, a change grant never covers it);
|
|
70
|
+
* apply / scale / rollout / patch / set / … = change; an unknown verb (a
|
|
71
|
+
* plugin such as `kubectl ctx`) is bound to its exact word, `cmd:<verb>`.
|
|
72
|
+
* where `context=<c>,namespace=<n>` (+ `,kubeconfig=<path>` with `--kubeconfig`).
|
|
73
|
+
*
|
|
74
|
+
* A value that cannot be read is `?` and the entry gets no Always allow. Defaults
|
|
75
|
+
* come from the caller's environment and the tool's own files, never from an
|
|
76
|
+
* assignment on the same line (`AWS_PROFILE=prod aws …`, `$env:KUBECONFIG=…`):
|
|
77
|
+
* such a line makes every default `?`, like `gcloud config set` does for gcloud.
|
|
78
|
+
* Deterministic words and flags only; the I/O is reading `~/.aws/config`,
|
|
79
|
+
* `~/.aws/credentials` and the kubeconfig files, never running a CLI.
|
|
80
|
+
*/
|
|
81
|
+
const fs = __importStar(require("fs"));
|
|
82
|
+
const os = __importStar(require("os"));
|
|
83
|
+
const path = __importStar(require("path"));
|
|
84
|
+
const credentialGrant_1 = require("./credentialGrant");
|
|
85
|
+
const shellQuoteMask_1 = require("./shellQuoteMask");
|
|
86
|
+
/** An unquoted redirection word: `2>&1`, `>out.txt`, `2>$null`, `*>>log`, `<in` (target attached). */
|
|
87
|
+
const REDIRECT_ATTACHED_RE = /^(?:\d|\*|&)?(?:>>?|<)(?:&\d|&-)?\S+$/;
|
|
88
|
+
/** A bare redirection operator whose target is the next word: `>`, `2>`, `>>`, `*>`, `<`. */
|
|
89
|
+
const REDIRECT_BARE_RE = /^(?:\d|\*|&)?(?:>>?|<)$/;
|
|
90
|
+
/**
|
|
91
|
+
* The words after the program on one use's text; empty when the program is not found.
|
|
92
|
+
* Redirections are the shell's, not arguments to the program: `git push origin x 2>&1`
|
|
93
|
+
* pushes the same branch as `git push origin x`.
|
|
94
|
+
*/
|
|
95
|
+
function toolArgv(text, dialect, names) {
|
|
96
|
+
const words = (0, shellQuoteMask_1.shellWords)(text.trim(), dialect);
|
|
97
|
+
const at = words.findIndex(w => names.includes((0, credentialGrant_1.unquote)(w).toLowerCase().replace(/^.*[\\/]/, '').replace(/\.(exe|cmd|bat)$/, '')));
|
|
98
|
+
if (at < 0)
|
|
99
|
+
return [];
|
|
100
|
+
const argv = [];
|
|
101
|
+
for (let k = at + 1; k < words.length; k++) {
|
|
102
|
+
const word = words[k];
|
|
103
|
+
if (REDIRECT_BARE_RE.test(word)) {
|
|
104
|
+
k++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (REDIRECT_ATTACHED_RE.test(word))
|
|
108
|
+
continue;
|
|
109
|
+
argv.push(word);
|
|
110
|
+
}
|
|
111
|
+
return argv;
|
|
112
|
+
}
|
|
113
|
+
const WORD_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
114
|
+
const UNSAFE_VALUE_RE = /[\s|,$`(){}%<>;&"']/;
|
|
115
|
+
/** `[section]` → key/value, lowercase section and key. Any read error = null. */
|
|
116
|
+
function readIni(file) {
|
|
117
|
+
try {
|
|
118
|
+
const sections = new Map();
|
|
119
|
+
let current;
|
|
120
|
+
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
121
|
+
const row = raw.trim();
|
|
122
|
+
if (!row || row.startsWith('#') || row.startsWith(';'))
|
|
123
|
+
continue;
|
|
124
|
+
const header = row.match(/^\[([^\]]+)\]$/);
|
|
125
|
+
if (header) {
|
|
126
|
+
const name = header[1].trim().replace(/\s+/g, ' ');
|
|
127
|
+
current = sections.get(name) || new Map();
|
|
128
|
+
sections.set(name, current);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const kv = row.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/);
|
|
132
|
+
if (kv && current)
|
|
133
|
+
current.set(kv[1].toLowerCase(), kv[2].trim());
|
|
134
|
+
}
|
|
135
|
+
return sections;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ---- aws --------------------------------------------------------------------------------
|
|
142
|
+
/** Global options that take a separate value; their value is not a command word. */
|
|
143
|
+
const AWS_VALUED_GLOBALS = new Set(['--profile', '--region', '--output', '--endpoint-url', '--query', '--color', '--cli-read-timeout', '--cli-connect-timeout', '--ca-bundle', '--cli-binary-format']);
|
|
144
|
+
const AWS_PERMISSION_SERVICES = new Set(['iam', 'sso-admin', 'identitystore']);
|
|
145
|
+
const AWS_DELETE_OP_RE = /^(?:rm|rb|delete|terminate|remove|deregister|purge)(?:-|$)/;
|
|
146
|
+
const AWS_READ_OP_RE = /^(?:describe|list|get|ls|head|wait|lookup|search|scan|query|presign)(?:-|$)/;
|
|
147
|
+
const AWS_PERMISSION_OP_RE = /(?:^|-)(?:policy|policies|permission|permissions|acl|grant|grants)(?:-|$)|^create-access-key$|^(?:authorize|revoke)-/;
|
|
148
|
+
/** Configuration names the CLI reads from the environment. A line that sets one overrides every default. */
|
|
149
|
+
const AWS_ENV_ON_LINE_RE = /\baws_(?:profile|default_profile|region|default_region|config_file|shared_credentials_file|access_key_id|secret_access_key|session_token)\b/i;
|
|
150
|
+
const PROFILE_RE = /^[A-Za-z0-9_.+@-]{1,64}$/;
|
|
151
|
+
const REGION_RE = /^[a-z]{2}(?:-[a-z]+)+-\d{1,2}$/;
|
|
152
|
+
const BUCKET_RE = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
153
|
+
function awsHow(service, op, rest) {
|
|
154
|
+
if (service === 's3') {
|
|
155
|
+
if (op === 'rm' || op === 'rb')
|
|
156
|
+
return 'delete';
|
|
157
|
+
// `mv s3://… x` removes the source objects.
|
|
158
|
+
if (op === 'mv' && (0, credentialGrant_1.unquote)(rest.find(w => !(0, credentialGrant_1.unquote)(w).startsWith('-')) || '').toLowerCase().startsWith('s3://'))
|
|
159
|
+
return 'delete';
|
|
160
|
+
if (op === 'sync' && rest.some(w => (0, credentialGrant_1.unquote)(w) === '--delete'))
|
|
161
|
+
return 'delete';
|
|
162
|
+
if (op === 'ls' || op === 'presign')
|
|
163
|
+
return 'read';
|
|
164
|
+
return 'change';
|
|
165
|
+
}
|
|
166
|
+
if (AWS_DELETE_OP_RE.test(op))
|
|
167
|
+
return 'delete';
|
|
168
|
+
if (AWS_PERMISSION_SERVICES.has(service))
|
|
169
|
+
return 'permissions';
|
|
170
|
+
if (AWS_READ_OP_RE.test(op))
|
|
171
|
+
return 'read';
|
|
172
|
+
if (AWS_PERMISSION_OP_RE.test(op))
|
|
173
|
+
return 'permissions';
|
|
174
|
+
return 'change';
|
|
175
|
+
}
|
|
176
|
+
/** Buckets and ARN accounts named on the use. `?` = one of them is computed. */
|
|
177
|
+
function awsResources(argv, vars) {
|
|
178
|
+
const buckets = new Set();
|
|
179
|
+
const accounts = new Set();
|
|
180
|
+
for (let k = 0; k < argv.length; k++) {
|
|
181
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
182
|
+
if (bare === '--bucket' || bare.startsWith('--bucket=')) {
|
|
183
|
+
const value = (0, credentialGrant_1.flagValue)(argv.slice(k, k + 2), ['--bucket'], vars);
|
|
184
|
+
if (!value)
|
|
185
|
+
return '?';
|
|
186
|
+
buckets.add(value.toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
const s3 = bare.match(/^s3:\/\/([^/]*)/i);
|
|
189
|
+
if (s3) {
|
|
190
|
+
if (/[$`(){}%]/.test(s3[1]))
|
|
191
|
+
return '?';
|
|
192
|
+
buckets.add(s3[1].toLowerCase());
|
|
193
|
+
}
|
|
194
|
+
for (const arn of bare.matchAll(/arn:aws[a-z-]*:[a-z0-9-]*:[a-z0-9-]*:([^:\s]*):/gi)) {
|
|
195
|
+
if (!/^\d{12}$/.test(arn[1])) {
|
|
196
|
+
if (arn[1])
|
|
197
|
+
return '?';
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
accounts.add(arn[1]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (accounts.size > 1 || [...buckets].some(b => !BUCKET_RE.test(b)))
|
|
204
|
+
return '?';
|
|
205
|
+
return { account: [...accounts][0], buckets: [...buckets].sort() };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* `AWS_PROFILE` (else `AWS_DEFAULT_PROFILE`), else `default` when that profile
|
|
209
|
+
* exists in `~/.aws/config` or `~/.aws/credentials`. Static keys in the
|
|
210
|
+
* environment (`AWS_ACCESS_KEY_ID`) beat any profile: unknown. Region:
|
|
211
|
+
* `AWS_REGION`, else `AWS_DEFAULT_REGION`, else the profile's `region` in the
|
|
212
|
+
* config file (`AWS_CONFIG_FILE` or `~/.aws/config`).
|
|
213
|
+
*/
|
|
214
|
+
function awsDefaultsFrom(env) {
|
|
215
|
+
const configFile = (env.AWS_CONFIG_FILE || '').trim() || path.join(os.homedir(), '.aws', 'config');
|
|
216
|
+
const credentialsFile = (env.AWS_SHARED_CREDENTIALS_FILE || '').trim() || path.join(os.homedir(), '.aws', 'credentials');
|
|
217
|
+
return {
|
|
218
|
+
profile() {
|
|
219
|
+
if ((env.AWS_ACCESS_KEY_ID || '').trim())
|
|
220
|
+
return null;
|
|
221
|
+
const named = (env.AWS_PROFILE || '').trim() || (env.AWS_DEFAULT_PROFILE || '').trim();
|
|
222
|
+
if (named)
|
|
223
|
+
return named;
|
|
224
|
+
const config = readIni(configFile);
|
|
225
|
+
const credentials = readIni(credentialsFile);
|
|
226
|
+
return config?.has('default') || credentials?.has('default') ? 'default' : null;
|
|
227
|
+
},
|
|
228
|
+
region(profile) {
|
|
229
|
+
const fromEnv = (env.AWS_REGION || '').trim() || (env.AWS_DEFAULT_REGION || '').trim();
|
|
230
|
+
if (fromEnv)
|
|
231
|
+
return fromEnv;
|
|
232
|
+
const config = readIni(configFile);
|
|
233
|
+
const section = config?.get(profile === 'default' ? 'default' : `profile ${profile}`);
|
|
234
|
+
return section?.get('region') || null;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function awsUse(argv, vars, defaults, defaultsUnreliable) {
|
|
239
|
+
const words = [];
|
|
240
|
+
let opAt = argv.length;
|
|
241
|
+
for (let k = 0; k < argv.length && words.length < 2; k++) {
|
|
242
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
243
|
+
if (bare.startsWith('-')) {
|
|
244
|
+
if (AWS_VALUED_GLOBALS.has(bare))
|
|
245
|
+
k++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (!WORD_RE.test(bare.toLowerCase()))
|
|
249
|
+
break;
|
|
250
|
+
words.push(bare.toLowerCase());
|
|
251
|
+
opAt = k;
|
|
252
|
+
}
|
|
253
|
+
const [service = '', op = ''] = words;
|
|
254
|
+
// `aws --version`, `aws help`, `aws sso login`: the local CLI or the sign-in, no environment.
|
|
255
|
+
if (!service || service === 'help' || op === 'help' || ((service === 'sso' || service === 'sso-oidc') && /^(?:login|logout)$/.test(op))) {
|
|
256
|
+
return { tool: 'aws', how: 'read', mutatesDefaults: false };
|
|
257
|
+
}
|
|
258
|
+
const mutatesDefaults = service === 'configure' && !/^(?:list|list-profiles|get|export-credentials)$/.test(op);
|
|
259
|
+
const how = service === 'configure' ? (mutatesDefaults ? 'change' : 'read') : awsHow(service, op, argv.slice(opAt + 1));
|
|
260
|
+
// A parameter file can set any parameter, the bucket and the ARN included.
|
|
261
|
+
if (argv.some(w => /^--cli-input-(?:json|yaml)(?:=|$)/.test((0, credentialGrant_1.unquote)(w))))
|
|
262
|
+
return { tool: 'aws', how, where: '?', mutatesDefaults };
|
|
263
|
+
const flagged = (names) => (0, credentialGrant_1.flagValue)([...argv], names, vars);
|
|
264
|
+
const profileFlag = flagged(['--profile']);
|
|
265
|
+
const regionFlag = flagged(['--region']);
|
|
266
|
+
let profile = profileFlag === undefined ? (defaultsUnreliable ? null : defaults.profile()) : profileFlag;
|
|
267
|
+
if (profile && !PROFILE_RE.test(profile))
|
|
268
|
+
profile = null;
|
|
269
|
+
let region = regionFlag === undefined ? (defaultsUnreliable || !profile ? null : defaults.region(profile)) : regionFlag;
|
|
270
|
+
if (region)
|
|
271
|
+
region = region.toLowerCase();
|
|
272
|
+
if (region && !REGION_RE.test(region))
|
|
273
|
+
region = null;
|
|
274
|
+
const resources = awsResources(argv, vars);
|
|
275
|
+
if (resources === '?')
|
|
276
|
+
return { tool: 'aws', how, where: '?', mutatesDefaults };
|
|
277
|
+
const parts = [`profile=${profile || '?'}`, `region=${region || '?'}`];
|
|
278
|
+
// A default profile is what the hook's environment says; an `$env:AWS_PROFILE` set by an
|
|
279
|
+
// earlier command in the agent's terminal is invisible here. Only `--profile` binds it.
|
|
280
|
+
if (profile && profileFlag === undefined)
|
|
281
|
+
parts.push('profile-from=default');
|
|
282
|
+
if (resources.account)
|
|
283
|
+
parts.push(`account=${resources.account}`);
|
|
284
|
+
if (resources.buckets.length)
|
|
285
|
+
parts.push(`bucket=${resources.buckets.join('+')}`);
|
|
286
|
+
return { tool: 'aws', how, where: parts.join(','), mutatesDefaults };
|
|
287
|
+
}
|
|
288
|
+
/** aws uses on the line. `awsLineUnreliable`: the line itself names an AWS_* setting. */
|
|
289
|
+
function awsUses(texts, command, dialect, vars, defaults) {
|
|
290
|
+
const argvs = texts.map(text => toolArgv(text, dialect, ['aws']));
|
|
291
|
+
const lineSetsEnv = AWS_ENV_ON_LINE_RE.test(command);
|
|
292
|
+
const first = argvs.map(argv => awsUse(argv, vars, defaults, lineSetsEnv));
|
|
293
|
+
if (!first.some(use => use.mutatesDefaults))
|
|
294
|
+
return first;
|
|
295
|
+
return argvs.map(argv => awsUse(argv, vars, defaults, true));
|
|
296
|
+
}
|
|
297
|
+
// ---- az ---------------------------------------------------------------------------------
|
|
298
|
+
const AZ_VALUED_GLOBALS = new Set(['--subscription', '--output', '-o', '--query', '--resource-group', '-g', '--name', '-n', '--ids', '--scope', '--location', '-l']);
|
|
299
|
+
/** First words that act on the local CLI or the sign-in, not on a subscription. */
|
|
300
|
+
const AZ_LOCAL = new Set(['login', 'logout', 'version', 'help', 'extension', 'upgrade', 'feedback', 'find', 'interactive', 'init', 'bicep', 'cloud', 'self-test', 'survey']);
|
|
301
|
+
const AZ_DELETE_VERB_RE = /^(?:delete|purge|remove|wipe)(?:-|$)/;
|
|
302
|
+
const AZ_READ_VERB_RE = /^(?:show|list|get|exists|check|query|download|wait|export|browse|tail)(?:-|$)/;
|
|
303
|
+
const AZ_PERMISSION_GROUPS = new Set(['role', 'ad']);
|
|
304
|
+
const AZ_PERMISSION_VERB_RE = /(?:^|-)(?:policy|permission|permissions|grant|assign|access)(?:-|$)|^generate-sas$|^(?:renew|regenerate)(?:-|$)/;
|
|
305
|
+
const AZ_ENV_ON_LINE_RE = /\b(?:azure_config_dir|azure_defaults_group|azure_defaults_location)\b/i;
|
|
306
|
+
const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
307
|
+
const RG_RE = /^[A-Za-z0-9._()-]{1,90}$/;
|
|
308
|
+
/** azureProfile.json / config under AZURE_CONFIG_DIR or `~/.azure`. */
|
|
309
|
+
function azDefaultsFrom(env) {
|
|
310
|
+
const dir = (env.AZURE_CONFIG_DIR || '').trim() || path.join(os.homedir(), '.azure');
|
|
311
|
+
const subscriptions = () => {
|
|
312
|
+
try {
|
|
313
|
+
const raw = fs.readFileSync(path.join(dir, 'azureProfile.json'), 'utf8').replace(/^\uFEFF/, '');
|
|
314
|
+
const list = JSON.parse(raw)?.subscriptions;
|
|
315
|
+
return Array.isArray(list) ? list.filter(s => s && typeof s.id === 'string').map(s => ({ id: String(s.id).toLowerCase(), name: String(s.name || ''), isDefault: s.isDefault === true })) : [];
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
subscription() {
|
|
323
|
+
const defaults = subscriptions().filter(s => s.isDefault);
|
|
324
|
+
return defaults.length === 1 && GUID_RE.test(defaults[0].id) ? defaults[0].id : null;
|
|
325
|
+
},
|
|
326
|
+
subscriptionId(nameOrId) {
|
|
327
|
+
if (GUID_RE.test(nameOrId))
|
|
328
|
+
return nameOrId.toLowerCase();
|
|
329
|
+
const matches = subscriptions().filter(s => s.name.toLowerCase() === nameOrId.toLowerCase());
|
|
330
|
+
return matches.length === 1 ? matches[0].id : null;
|
|
331
|
+
},
|
|
332
|
+
group() {
|
|
333
|
+
const fromEnv = (env.AZURE_DEFAULTS_GROUP || '').trim();
|
|
334
|
+
if (fromEnv)
|
|
335
|
+
return fromEnv;
|
|
336
|
+
return readIni(path.join(dir, 'config'))?.get('defaults')?.get('group') || null;
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** `/subscriptions/<id>/resourceGroups/<rg>/…` references on the use. `?` when they disagree or are computed. */
|
|
341
|
+
function azResourceIds(argv) {
|
|
342
|
+
const subs = new Set();
|
|
343
|
+
const groups = new Set();
|
|
344
|
+
for (const word of argv) {
|
|
345
|
+
const bare = (0, credentialGrant_1.unquote)(word);
|
|
346
|
+
for (const m of bare.matchAll(/\/subscriptions\/([^/\s]+)(?:\/resourceGroups\/([^/\s]+))?/gi)) {
|
|
347
|
+
if (/[$`(){}%]/.test(m[0]))
|
|
348
|
+
return '?';
|
|
349
|
+
subs.add(m[1].toLowerCase());
|
|
350
|
+
if (m[2])
|
|
351
|
+
groups.add(m[2].toLowerCase());
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (subs.size > 1 || groups.size > 1)
|
|
355
|
+
return '?';
|
|
356
|
+
return { subscription: [...subs][0], group: [...groups][0] };
|
|
357
|
+
}
|
|
358
|
+
function azUse(argv, vars, defaults, defaultsUnreliable) {
|
|
359
|
+
const words = commandPathOf(argv, AZ_VALUED_GLOBALS, 5);
|
|
360
|
+
const [group = '', ...rest] = words;
|
|
361
|
+
const verb = words[words.length - 1] || '';
|
|
362
|
+
if (!group || AZ_LOCAL.has(group))
|
|
363
|
+
return { tool: 'az', how: 'read', mutatesDefaults: false };
|
|
364
|
+
if (group === 'config' || group === 'configure')
|
|
365
|
+
return { tool: 'az', how: 'change', mutatesDefaults: !/^(?:get|show|list)$/.test(rest[0] || '') };
|
|
366
|
+
// `az account …` is the signed-in account (like `gcloud auth`): no subscription entry; `account set` moves the default.
|
|
367
|
+
if (group === 'account')
|
|
368
|
+
return { tool: 'az', how: rest[0] === 'set' ? 'change' : 'read', mutatesDefaults: rest[0] === 'set' };
|
|
369
|
+
const how = AZ_DELETE_VERB_RE.test(verb) ? 'delete'
|
|
370
|
+
: AZ_PERMISSION_GROUPS.has(group) || words.some(w => AZ_PERMISSION_VERB_RE.test(w)) || words.includes('keys') ? 'permissions'
|
|
371
|
+
: AZ_READ_VERB_RE.test(verb) ? 'read' : 'change';
|
|
372
|
+
const ids = azResourceIds(argv);
|
|
373
|
+
if (ids === '?')
|
|
374
|
+
return { tool: 'az', how, where: '?', mutatesDefaults: false };
|
|
375
|
+
const subFlag = (0, credentialGrant_1.flagValue)([...argv], ['--subscription'], vars);
|
|
376
|
+
let subscription;
|
|
377
|
+
if (subFlag === null)
|
|
378
|
+
subscription = null;
|
|
379
|
+
else if (subFlag !== undefined)
|
|
380
|
+
subscription = defaults.subscriptionId(subFlag);
|
|
381
|
+
else
|
|
382
|
+
subscription = ids.subscription || (defaultsUnreliable ? null : defaults.subscription());
|
|
383
|
+
if (ids.subscription && subscription && ids.subscription !== subscription)
|
|
384
|
+
subscription = null;
|
|
385
|
+
// `az group <verb> --name rg` names the resource group itself.
|
|
386
|
+
const rgFlag = (0, credentialGrant_1.flagValue)([...argv], group === 'group' ? ['--name', '-n', '--resource-group', '-g'] : ['--resource-group', '-g'], vars);
|
|
387
|
+
let rg;
|
|
388
|
+
if (rgFlag === null)
|
|
389
|
+
rg = null;
|
|
390
|
+
else if (rgFlag !== undefined)
|
|
391
|
+
rg = rgFlag.toLowerCase();
|
|
392
|
+
else if (ids.group)
|
|
393
|
+
rg = ids.group;
|
|
394
|
+
else if (group === 'group' && (verb === 'list' || verb === 'create'))
|
|
395
|
+
rg = verb === 'list' ? '-' : null;
|
|
396
|
+
else
|
|
397
|
+
rg = defaultsUnreliable ? null : (defaults.group() || '-').toLowerCase();
|
|
398
|
+
if (rg && rg !== '-' && !RG_RE.test(rg))
|
|
399
|
+
rg = null;
|
|
400
|
+
return { tool: 'az', how, where: `subscription=${subscription || '?'},group=${rg || '?'}`, mutatesDefaults: false };
|
|
401
|
+
}
|
|
402
|
+
/** Command words of a use: non-flag words, skipping the values of `valued` flags. */
|
|
403
|
+
function commandPathOf(argv, valued, max) {
|
|
404
|
+
const out = [];
|
|
405
|
+
for (let k = 0; k < argv.length && out.length < max; k++) {
|
|
406
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
407
|
+
if (bare.startsWith('-')) {
|
|
408
|
+
if (valued.has(bare))
|
|
409
|
+
k++;
|
|
410
|
+
if (out.length)
|
|
411
|
+
break;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (!WORD_RE.test(bare.toLowerCase()))
|
|
415
|
+
break;
|
|
416
|
+
out.push(bare.toLowerCase());
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
/** az uses on the line; a line that sets az defaults (`az account set`, `az config set`, `AZURE_*=`) makes every default unknown. */
|
|
421
|
+
function azUses(texts, command, dialect, vars, defaults) {
|
|
422
|
+
const argvs = texts.map(text => toolArgv(text, dialect, ['az']));
|
|
423
|
+
const first = argvs.map(argv => azUse(argv, vars, defaults, AZ_ENV_ON_LINE_RE.test(command)));
|
|
424
|
+
if (!first.some(use => use.mutatesDefaults))
|
|
425
|
+
return first;
|
|
426
|
+
return argvs.map(argv => azUse(argv, vars, defaults, true));
|
|
427
|
+
}
|
|
428
|
+
// ---- terraform / tofu / terragrunt ------------------------------------------------------
|
|
429
|
+
const TF_TOOLS = new Set(['terraform', 'tofu', 'terragrunt']);
|
|
430
|
+
const TF_LOCAL = new Set(['version', 'help', 'fmt', 'validate', 'hclfmt', 'render-json', 'graph-dependencies', '-version', 'login', 'logout', 'metadata']);
|
|
431
|
+
const TF_READ = new Set(['plan', 'show', 'output', 'graph', 'providers', 'console', 'get']);
|
|
432
|
+
const TF_ENV_ON_LINE_RE = /\b(?:tf_workspace|tf_data_dir|tf_cli_args\w*|tf_var_\w+|terragrunt_working_dir)\b/i;
|
|
433
|
+
const TF_DIR_RE = /^[^|,$`(){}%<>;&"'*?]{1,300}$/;
|
|
434
|
+
const TF_WORKSPACE_RE = /^[A-Za-z0-9._-]{1,90}$/;
|
|
435
|
+
function tfDefaultsFrom(env) {
|
|
436
|
+
return {
|
|
437
|
+
workspace(dir, cwd) {
|
|
438
|
+
const fromEnv = (env.TF_WORKSPACE || '').trim();
|
|
439
|
+
if (fromEnv)
|
|
440
|
+
return fromEnv;
|
|
441
|
+
if (!cwd)
|
|
442
|
+
return null;
|
|
443
|
+
try {
|
|
444
|
+
const file = path.join(path.resolve(cwd, dir), (env.TF_DATA_DIR || '').trim() || '.terraform', 'environment');
|
|
445
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').trim() || 'default' : 'default';
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function tfUse(tool, argv, vars, defaults, cwd, defaultsUnreliable) {
|
|
454
|
+
let dir = '.';
|
|
455
|
+
let dirKnown = true;
|
|
456
|
+
const words = [];
|
|
457
|
+
const varFiles = [];
|
|
458
|
+
let inlineVars = false;
|
|
459
|
+
let destroyFlag = false;
|
|
460
|
+
for (let k = 0; k < argv.length; k++) {
|
|
461
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
462
|
+
const chdir = bare.match(/^-chdir=(.*)$/) || bare.match(/^--(?:terragrunt-)?working-dir=(.*)$/);
|
|
463
|
+
if (chdir) {
|
|
464
|
+
const value = (0, credentialGrant_1.literalWord)(chdir[1], vars);
|
|
465
|
+
dir = value || '.';
|
|
466
|
+
dirKnown = Boolean(value);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (bare === '--terragrunt-working-dir' || bare === '--working-dir') {
|
|
470
|
+
const value = (0, credentialGrant_1.literalWord)(argv[++k], vars);
|
|
471
|
+
dir = value || '.';
|
|
472
|
+
dirKnown = Boolean(value);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (/^-var-file(?:=|$)/.test(bare)) {
|
|
476
|
+
varFiles.push((0, credentialGrant_1.literalWord)(bare.includes('=') ? bare.slice(bare.indexOf('=') + 1) : argv[++k], vars));
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (/^-var(?:=|$)/.test(bare)) {
|
|
480
|
+
inlineVars = true;
|
|
481
|
+
if (!bare.includes('='))
|
|
482
|
+
k++;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (bare === '-destroy') {
|
|
486
|
+
destroyFlag = true;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (bare.startsWith('-'))
|
|
490
|
+
continue;
|
|
491
|
+
if (words.length < 2 && WORD_RE.test(bare.toLowerCase()))
|
|
492
|
+
words.push(bare.toLowerCase());
|
|
493
|
+
}
|
|
494
|
+
const [verb = '', sub = ''] = words;
|
|
495
|
+
if (!verb || TF_LOCAL.has(verb))
|
|
496
|
+
return { tool, how: 'read', mutatesDefaults: false };
|
|
497
|
+
if (verb === 'run-all' || verb === 'run')
|
|
498
|
+
return { tool, how: 'change', where: '?', mutatesDefaults: false };
|
|
499
|
+
let how = 'change';
|
|
500
|
+
let mutatesDefaults = false;
|
|
501
|
+
if (verb === 'destroy' || (verb === 'apply' && destroyFlag) || verb === 'force-unlock')
|
|
502
|
+
how = 'delete';
|
|
503
|
+
else if (verb === 'import')
|
|
504
|
+
how = 'permissions';
|
|
505
|
+
else if (verb === 'state')
|
|
506
|
+
how = /^(?:list|show|pull)$/.test(sub) ? 'read' : 'delete';
|
|
507
|
+
else if (verb === 'workspace') {
|
|
508
|
+
if (sub === 'delete')
|
|
509
|
+
how = 'delete';
|
|
510
|
+
else if (sub === 'select' || sub === 'new')
|
|
511
|
+
mutatesDefaults = true;
|
|
512
|
+
else
|
|
513
|
+
return { tool, how: 'read', mutatesDefaults: false };
|
|
514
|
+
}
|
|
515
|
+
else if (TF_READ.has(verb))
|
|
516
|
+
how = 'read';
|
|
517
|
+
// The directory is bound absolutely: `.` in two repositories is two environments. No working directory = unknown.
|
|
518
|
+
const absDir = cwd && dirKnown ? path.resolve(cwd, dir).replace(/\\/g, '/').replace(/\/+$/, '') : null;
|
|
519
|
+
const cleanDir = absDir && process.platform === 'win32' ? absDir.toLowerCase() : absDir;
|
|
520
|
+
if (!cleanDir || !TF_DIR_RE.test(cleanDir) || inlineVars || varFiles.some(f => f === null))
|
|
521
|
+
return { tool, how, where: '?', mutatesDefaults };
|
|
522
|
+
const workspace = defaultsUnreliable ? null : defaults.workspace(dir, cwd);
|
|
523
|
+
const parts = [`dir=${cleanDir}`, `workspace=${workspace && TF_WORKSPACE_RE.test(workspace) ? workspace : '?'}`];
|
|
524
|
+
const files = varFiles.map(f => f.replace(/\\/g, '/'));
|
|
525
|
+
if (files.length)
|
|
526
|
+
parts.push(`varfile=${files.every(f => TF_DIR_RE.test(f)) ? [...new Set(files)].sort().join('+') : '?'}`);
|
|
527
|
+
return { tool, how, where: parts.join(','), mutatesDefaults };
|
|
528
|
+
}
|
|
529
|
+
/** terraform / tofu / terragrunt uses; `workspace select` or a `TF_*` assignment on the line makes the workspace unknown. */
|
|
530
|
+
function tfUses(uses, command, dialect, vars, defaults, cwd) {
|
|
531
|
+
const argvs = uses.map(use => ({ tool: use.tool, argv: toolArgv(use.text, dialect, [use.tool]) }));
|
|
532
|
+
const first = argvs.map(({ tool, argv }) => tfUse(tool, argv, vars, defaults, cwd, TF_ENV_ON_LINE_RE.test(command)));
|
|
533
|
+
if (!first.some(use => use.mutatesDefaults))
|
|
534
|
+
return first;
|
|
535
|
+
return argvs.map(({ tool, argv }) => tfUse(tool, argv, vars, defaults, cwd, true));
|
|
536
|
+
}
|
|
537
|
+
function isTfTool(name) {
|
|
538
|
+
return TF_TOOLS.has(name);
|
|
539
|
+
}
|
|
540
|
+
// ---- ssh / scp / sftp / rsync -----------------------------------------------------------
|
|
541
|
+
const REMOTE_TOOLS = new Set(['ssh', 'scp', 'sftp', 'rsync']);
|
|
542
|
+
const SSH_VALUED = new Set(['-p', '-i', '-l', '-o', '-F', '-J', '-L', '-R', '-D', '-b', '-c', '-E', '-e', '-I', '-m', '-O', '-Q', '-S', '-W', '-w', '-B', '-P']);
|
|
543
|
+
const RSYNC_VALUED = new Set(['-e', '--rsh', '--exclude', '--include', '--exclude-from', '--include-from', '--files-from', '--filter', '-f', '--port', '--password-file', '--log-file', '--backup-dir', '--suffix', '--chmod', '--chown', '--timeout', '--contimeout', '--bwlimit', '--compare-dest', '--copy-dest', '--link-dest', '-T', '--temp-dir', '--partial-dir', '--max-size', '--min-size', '--rsync-path']);
|
|
544
|
+
const HOST_RE = /^[A-Za-z0-9_.-]{1,253}$/;
|
|
545
|
+
const USER_RE = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
546
|
+
/** `[user@]host[:path]` / `ssh://[user@]host[:port]/…` / `rsync://host/…`; null when it is not remote, `?` when computed. */
|
|
547
|
+
function remoteEndpoint(word, vars, needsColon) {
|
|
548
|
+
const value = (0, credentialGrant_1.literalWord)(word, vars);
|
|
549
|
+
if (value === null)
|
|
550
|
+
return /[:@]/.test((0, credentialGrant_1.unquote)(word)) ? '?' : null;
|
|
551
|
+
const url = value.match(/^(?:ssh|rsync|sftp|scp):\/\/(?:([^@/]+)@)?([^:/]+)/i);
|
|
552
|
+
const plain = url ? null : needsColon ? value.match(/^(?:([^@\s/:]+)@)?([A-Za-z0-9_.-]+):/) : value.match(/^(?:([^@\s/:]+)@)?([^@\s/:]+)$/);
|
|
553
|
+
if (!url && /^[A-Za-z]:[\\/]/.test(value))
|
|
554
|
+
return null;
|
|
555
|
+
const m = url || plain;
|
|
556
|
+
if (!m)
|
|
557
|
+
return null;
|
|
558
|
+
const host = m[2].toLowerCase();
|
|
559
|
+
if (!HOST_RE.test(host) || (m[1] && !USER_RE.test(m[1])))
|
|
560
|
+
return '?';
|
|
561
|
+
return { host, ...(m[1] ? { user: m[1] } : {}) };
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* ssh / sftp = the destination host (`exec`: a shell or a command runs there);
|
|
565
|
+
* scp / rsync = every `host:path` operand, `change` when a remote operand is the
|
|
566
|
+
* last (destination) one, else `read`; rsync `--delete*` / `--remove-source-files`
|
|
567
|
+
* = delete. `-o` / `-F` / `-J` / a non-ssh `rsync -e` can send the connection
|
|
568
|
+
* elsewhere: unknown. Where = `host=<h>[+<h2>][,user=<u>]`.
|
|
569
|
+
*/
|
|
570
|
+
function remoteUse(tool, argv, vars) {
|
|
571
|
+
const valued = tool === 'rsync' ? RSYNC_VALUED : SSH_VALUED;
|
|
572
|
+
const operands = [];
|
|
573
|
+
let unknown = false;
|
|
574
|
+
let deletes = false;
|
|
575
|
+
let remoteCommand = [];
|
|
576
|
+
// `-l user` / `-o User=` name the login like `user@host`; `-p` (ssh) / `-P` (scp, sftp) the port.
|
|
577
|
+
let loginUser;
|
|
578
|
+
let port;
|
|
579
|
+
const optionValue = (k, bare, flag) => (bare.length > flag.length ? (0, credentialGrant_1.literalWord)(bare.slice(flag.length), vars) : (0, credentialGrant_1.literalWord)(argv[k + 1], vars));
|
|
580
|
+
for (let k = 0; k < argv.length; k++) {
|
|
581
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
582
|
+
if (tool === 'ssh' && operands.length) {
|
|
583
|
+
remoteCommand = argv.slice(k);
|
|
584
|
+
break;
|
|
585
|
+
} // the rest is the remote command
|
|
586
|
+
if (bare === '--')
|
|
587
|
+
continue;
|
|
588
|
+
if (bare.startsWith('-')) {
|
|
589
|
+
const flag = bare.split('=')[0];
|
|
590
|
+
if (/^--(?:delete|remove-source-files)/.test(flag))
|
|
591
|
+
deletes = true;
|
|
592
|
+
if (tool !== 'rsync' && /^-F/.test(bare))
|
|
593
|
+
unknown = true;
|
|
594
|
+
if ((tool === 'ssh' || tool === 'sftp') && /^-l/.test(bare))
|
|
595
|
+
loginUser = optionValue(k, bare, '-l');
|
|
596
|
+
if ((tool === 'ssh' && /^-p/.test(bare)) || ((tool === 'scp' || tool === 'sftp') && /^-P/.test(bare)))
|
|
597
|
+
port = optionValue(k, bare, bare.slice(0, 2));
|
|
598
|
+
if (tool !== 'rsync' && /^-o/.test(bare)) {
|
|
599
|
+
const option = bare.length > 2 ? bare.slice(2) : (0, credentialGrant_1.unquote)(argv[k + 1] || '');
|
|
600
|
+
if (/^(?:proxycommand|hostname|localcommand|include|permitlocalcommand)\b/i.test(option.trim()))
|
|
601
|
+
unknown = true;
|
|
602
|
+
const user = option.trim().match(/^user\s*[= ]\s*(\S+)$/i);
|
|
603
|
+
if (user)
|
|
604
|
+
loginUser = USER_RE.test(user[1]) ? user[1] : null;
|
|
605
|
+
const optPort = option.trim().match(/^port\s*[= ]\s*(\S+)$/i);
|
|
606
|
+
if (optPort)
|
|
607
|
+
port = optPort[1];
|
|
608
|
+
}
|
|
609
|
+
if (tool === 'rsync' && (flag === '-e' || flag === '--rsh')) {
|
|
610
|
+
const rsh = bare.includes('=') ? bare.slice(bare.indexOf('=') + 1) : (0, credentialGrant_1.unquote)(argv[k + 1] || '');
|
|
611
|
+
if (!/^ssh(?:\s|$)/.test(rsh) || /\s-F|proxycommand|hostname=/i.test(rsh))
|
|
612
|
+
unknown = true;
|
|
613
|
+
}
|
|
614
|
+
if (valued.has(flag) && !bare.includes('=') && bare.length === flag.length)
|
|
615
|
+
k++;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
operands.push(argv[k]);
|
|
619
|
+
}
|
|
620
|
+
// `ssh -V`, `rsync --version`: no operand, nothing remote.
|
|
621
|
+
if (!operands.length)
|
|
622
|
+
return undefined;
|
|
623
|
+
const ends = operands.map(op => remoteEndpoint(op, vars, tool === 'scp' || tool === 'rsync'));
|
|
624
|
+
if (tool === 'ssh' || tool === 'sftp')
|
|
625
|
+
ends.splice(1);
|
|
626
|
+
const remote = ends.filter(e => e !== null);
|
|
627
|
+
if (!remote.length)
|
|
628
|
+
return tool === 'rsync' || tool === 'scp' ? undefined : { tool, how: 'exec', where: '?', mutatesDefaults: false };
|
|
629
|
+
const how = tool === 'ssh' || tool === 'sftp' ? (remoteCommandHow(remoteCommand) || 'exec') : deletes ? 'delete' : ends[ends.length - 1] !== null ? 'change' : 'read';
|
|
630
|
+
if (unknown || remote.some(e => e === '?') || loginUser === null || port === null || (port !== undefined && !/^\d{1,5}$/.test(port)))
|
|
631
|
+
return { tool, how, where: '?', mutatesDefaults: false };
|
|
632
|
+
const endpoints = remote;
|
|
633
|
+
// `user@host` and a different `-l` / `-o User=` on one line: which one ssh uses is not read here.
|
|
634
|
+
if (loginUser && endpoints.some(e => e.user && e.user !== loginUser))
|
|
635
|
+
return { tool, how, where: '?', mutatesDefaults: false };
|
|
636
|
+
const hosts = [...new Set(endpoints.map(e => e.host))].sort();
|
|
637
|
+
const users = [...new Set(endpoints.map(e => e.user || loginUser).filter(Boolean))].sort();
|
|
638
|
+
const portPart = port && port !== '22' ? `,port=${port}` : '';
|
|
639
|
+
return { tool, how, where: `host=${hosts.join('+')}${users.length ? `,user=${users.join('+')}` : ''}${portPart}`, mutatesDefaults: false };
|
|
640
|
+
}
|
|
641
|
+
/** Leading programs that change who may do what on the remote machine. */
|
|
642
|
+
const REMOTE_PERMISSION_PROGRAMS = new Set(['chmod', 'chown', 'chgrp', 'setfacl', 'usermod', 'useradd', 'userdel', 'groupmod', 'gpasswd', 'passwd', 'chpasswd', 'visudo', 'icacls', 'takeown']);
|
|
643
|
+
/**
|
|
644
|
+
* The remote command of `ssh host <command>`: a delete (the engine's operation) or a permission
|
|
645
|
+
* change (leading program) is its own entry, so an `exec` grant on the host never covers it and
|
|
646
|
+
* Always allow is refused for it. Anything else stays `exec`.
|
|
647
|
+
*/
|
|
648
|
+
function remoteCommandHow(words) {
|
|
649
|
+
const text = words.map(credentialGrant_1.unquote).join(' ').trim();
|
|
650
|
+
if (!text)
|
|
651
|
+
return undefined;
|
|
652
|
+
const { operation } = require('./actionPolicyEngine').inferBundledToolContext('shell', { command: text });
|
|
653
|
+
if (/^(?:DELETE|DROP|TRUNCATE)$/.test(operation))
|
|
654
|
+
return 'delete';
|
|
655
|
+
if (/^(?:GRANT|REVOKE)$/.test(operation))
|
|
656
|
+
return 'permissions';
|
|
657
|
+
for (const segment of text.split(/;|&&|\|\||\||\n/)) {
|
|
658
|
+
const program = segment.trim().split(/\s+/).find(w => w && !/^(?:sudo|doas|env|-\S+|[A-Za-z_][A-Za-z0-9_]*=\S*)$/.test(w));
|
|
659
|
+
if (program && REMOTE_PERMISSION_PROGRAMS.has(program.replace(/^.*\//, '').toLowerCase()))
|
|
660
|
+
return 'permissions';
|
|
661
|
+
}
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
function isRemoteTool(name) {
|
|
665
|
+
return REMOTE_TOOLS.has(name);
|
|
666
|
+
}
|
|
667
|
+
function remoteUses(uses, dialect, vars) {
|
|
668
|
+
return uses.map(use => remoteUse(use.tool, toolArgv(use.text, dialect, [use.tool]), vars)).filter((use) => Boolean(use));
|
|
669
|
+
}
|
|
670
|
+
// ---- psql / mysql / mariadb -------------------------------------------------------------
|
|
671
|
+
const SQL_TOOLS = new Set(['psql', 'mysql', 'mariadb']);
|
|
672
|
+
const PSQL_VALUED = new Set(['-h', '--host', '-d', '--dbname', '-U', '--username', '-p', '--port', '-c', '--command', '-f', '--file', '-v', '--set', '--variable', '-o', '--output', '-L', '--log-file', '-P', '--pset', '-F', '--field-separator', '-R', '--record-separator', '-T', '--table-attr']);
|
|
673
|
+
const MYSQL_VALUED = new Set(['-h', '--host', '-D', '--database', '-u', '--user', '-P', '--port', '-e', '--execute', '-S', '--socket', '--protocol', '--default-character-set', '--init-command']);
|
|
674
|
+
const DB_NAME_RE = /^[A-Za-z0-9_.$-]{1,128}$/;
|
|
675
|
+
const SQL_ENV_ON_LINE_RE = /\b(?:pghost|pgdatabase|pgservice|pgport|mysql_host|mysql_pwd)\b/i;
|
|
676
|
+
/** The worst class of the statements in one SQL string. */
|
|
677
|
+
function sqlHow(sql) {
|
|
678
|
+
let worst = 'read';
|
|
679
|
+
const rank = ['read', 'change', 'permissions', 'delete'];
|
|
680
|
+
for (const statement of sql.split(';')) {
|
|
681
|
+
const words = statement.trim().split(/\s+/).map(w => w.toUpperCase());
|
|
682
|
+
const [first = '', second = ''] = words;
|
|
683
|
+
if (!first)
|
|
684
|
+
continue;
|
|
685
|
+
let how = 'change';
|
|
686
|
+
if (/^(?:DROP|TRUNCATE|DELETE)$/.test(first))
|
|
687
|
+
how = 'delete';
|
|
688
|
+
else if (/^(?:GRANT|REVOKE)$/.test(first) || (/^(?:CREATE|ALTER)$/.test(first) && /^(?:ROLE|USER|POLICY)$/.test(second)))
|
|
689
|
+
how = 'permissions';
|
|
690
|
+
else if (/^(?:SELECT|SHOW|EXPLAIN|DESCRIBE|DESC|WITH|VALUES|TABLE|\\[A-Z]+)$/.test(first) && !/\b(?:INSERT|UPDATE|DELETE|DROP|TRUNCATE)\b/.test(words.join(' ')))
|
|
691
|
+
how = 'read';
|
|
692
|
+
if (rank.indexOf(how) > rank.indexOf(worst))
|
|
693
|
+
worst = how;
|
|
694
|
+
}
|
|
695
|
+
return worst;
|
|
696
|
+
}
|
|
697
|
+
/** Quoted SQL is data even with parentheses; only an expansion (`$x`, a backtick) makes it computed. */
|
|
698
|
+
function sqlText(word) {
|
|
699
|
+
if (word === undefined)
|
|
700
|
+
return null;
|
|
701
|
+
if (/^'[\s\S]*'$/.test(word))
|
|
702
|
+
return word.slice(1, -1);
|
|
703
|
+
if (/^"[\s\S]*"$/.test(word) && !/[$`]/.test(word))
|
|
704
|
+
return word.slice(1, -1);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
/** `postgres://user@host:port/db`, `host=x dbname=y` or a bare name. */
|
|
708
|
+
function pgConnection(value) {
|
|
709
|
+
const url = value.match(/^postgres(?:ql)?:\/\/(?:[^@/]*@)?([^:/?]+)?(?::\d+)?(?:\/([^?]+))?/i);
|
|
710
|
+
if (url)
|
|
711
|
+
return { host: url[1], db: url[2] };
|
|
712
|
+
if (/=/.test(value)) {
|
|
713
|
+
if (/\bservice\s*=/.test(value))
|
|
714
|
+
return { unknown: true };
|
|
715
|
+
return { host: value.match(/\bhost\s*=\s*(\S+)/)?.[1], db: value.match(/\bdbname\s*=\s*(\S+)/)?.[1] };
|
|
716
|
+
}
|
|
717
|
+
return { db: value };
|
|
718
|
+
}
|
|
719
|
+
function sqlUse(tool, argv, vars, env, defaultsUnreliable) {
|
|
720
|
+
const pg = tool === 'psql';
|
|
721
|
+
const valued = pg ? PSQL_VALUED : MYSQL_VALUED;
|
|
722
|
+
let host;
|
|
723
|
+
let db;
|
|
724
|
+
let sql;
|
|
725
|
+
let file;
|
|
726
|
+
let unknown = false;
|
|
727
|
+
const positional = [];
|
|
728
|
+
for (let k = 0; k < argv.length; k++) {
|
|
729
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
730
|
+
if (bare === '--version' || bare === '-V' || bare === '--help' || bare === '-?')
|
|
731
|
+
return undefined;
|
|
732
|
+
if (!pg && /^--(?:defaults-file|defaults-extra-file|login-path)(?:=|$)/.test(bare))
|
|
733
|
+
unknown = true;
|
|
734
|
+
const long = bare.match(/^(--[a-z-]+)=(.*)$/s);
|
|
735
|
+
const shortGlued = !long && /^-[A-Za-z]./.test(bare) && valued.has(bare.slice(0, 2)) ? [bare.slice(0, 2), bare.slice(2)] : null;
|
|
736
|
+
const flag = long ? long[1] : shortGlued ? shortGlued[0] : bare;
|
|
737
|
+
let value;
|
|
738
|
+
if (long)
|
|
739
|
+
value = (0, credentialGrant_1.literalWord)(long[2], vars);
|
|
740
|
+
else if (shortGlued)
|
|
741
|
+
value = (0, credentialGrant_1.literalWord)(shortGlued[1], vars);
|
|
742
|
+
else if (bare.startsWith('-') && valued.has(bare))
|
|
743
|
+
value = (0, credentialGrant_1.literalWord)(argv[++k], vars);
|
|
744
|
+
else if (bare.startsWith('-'))
|
|
745
|
+
continue;
|
|
746
|
+
else {
|
|
747
|
+
positional.push(argv[k]);
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (flag === '-h' || flag === '--host')
|
|
751
|
+
host = value;
|
|
752
|
+
else if (flag === '-d' || flag === '--dbname') {
|
|
753
|
+
if (value === null)
|
|
754
|
+
db = null;
|
|
755
|
+
else {
|
|
756
|
+
const conn = pgConnection(value);
|
|
757
|
+
if (conn.unknown)
|
|
758
|
+
unknown = true;
|
|
759
|
+
db = conn.db ?? db;
|
|
760
|
+
if (conn.host)
|
|
761
|
+
host = conn.host;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else if (flag === '-D' || flag === '--database')
|
|
765
|
+
db = value;
|
|
766
|
+
else if (flag === '-c' || flag === '--command' || flag === '-e' || flag === '--execute')
|
|
767
|
+
sql = value ?? sqlText(long ? long[2] : shortGlued ? shortGlued[1] : argv[k]);
|
|
768
|
+
else if (flag === '-f' || flag === '--file')
|
|
769
|
+
file = value;
|
|
770
|
+
}
|
|
771
|
+
if (db === undefined && positional.length) {
|
|
772
|
+
const value = (0, credentialGrant_1.literalWord)(positional[0], vars);
|
|
773
|
+
if (value === null)
|
|
774
|
+
db = null;
|
|
775
|
+
else if (pg) {
|
|
776
|
+
const conn = pgConnection(value);
|
|
777
|
+
if (conn.unknown)
|
|
778
|
+
unknown = true;
|
|
779
|
+
db = conn.db;
|
|
780
|
+
if (conn.host && host === undefined)
|
|
781
|
+
host = conn.host;
|
|
782
|
+
}
|
|
783
|
+
else
|
|
784
|
+
db = value;
|
|
785
|
+
}
|
|
786
|
+
const how = sql !== undefined ? (sql === null ? '?' : sqlHow(sql)) : file !== undefined ? (file ? `script:${file.replace(/\\/g, '/')}` : '?') : 'session';
|
|
787
|
+
if (how === '?')
|
|
788
|
+
return { tool, how: 'change', where: '?', mutatesDefaults: false };
|
|
789
|
+
if (host === undefined)
|
|
790
|
+
host = defaultsUnreliable ? null : ((pg ? env.PGHOST : env.MYSQL_HOST) || '').trim() || 'local';
|
|
791
|
+
if (db === undefined)
|
|
792
|
+
db = defaultsUnreliable || !pg ? null : (env.PGDATABASE || '').trim() || null;
|
|
793
|
+
if (pg && (env.PGSERVICE || '').trim())
|
|
794
|
+
unknown = true;
|
|
795
|
+
const hostOk = host && (host === 'local' || HOST_RE.test(host)) ? host.toLowerCase() : null;
|
|
796
|
+
const dbOk = db && DB_NAME_RE.test(db) ? db : null;
|
|
797
|
+
if (unknown)
|
|
798
|
+
return { tool, how, where: '?', mutatesDefaults: false };
|
|
799
|
+
return { tool, how, where: `host=${hostOk || '?'},database=${dbOk || '?'}`, mutatesDefaults: false };
|
|
800
|
+
}
|
|
801
|
+
function isSqlTool(name) {
|
|
802
|
+
return SQL_TOOLS.has(name);
|
|
803
|
+
}
|
|
804
|
+
function sqlUses(uses, command, dialect, vars, env) {
|
|
805
|
+
const unreliable = SQL_ENV_ON_LINE_RE.test(command);
|
|
806
|
+
return uses.map(use => sqlUse(use.tool, toolArgv(use.text, dialect, [use.tool]), vars, env, unreliable)).filter((use) => Boolean(use));
|
|
807
|
+
}
|
|
808
|
+
// ---- git push ---------------------------------------------------------------------------
|
|
809
|
+
const GIT_VALUED_GLOBALS = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path']);
|
|
810
|
+
const GIT_PUSH_VALUED = new Set(['--repo', '-o', '--push-option', '--receive-pack', '--exec']);
|
|
811
|
+
/** Branches a standing grant never covers: the ones a release or production deploy is cut from. */
|
|
812
|
+
const PROTECTED_BRANCH_RE = /^(?:main|master|release.*|prod.*)$/i;
|
|
813
|
+
const BRANCH_RE = /^[A-Za-z0-9._][A-Za-z0-9._/-]{0,199}$/;
|
|
814
|
+
/** The branch `HEAD` names in the repository at `dir` (walking up to `.git`), or null. */
|
|
815
|
+
function currentGitBranch(dir) {
|
|
816
|
+
try {
|
|
817
|
+
let at = path.resolve(dir);
|
|
818
|
+
for (let depth = 0; depth < 40; depth++) {
|
|
819
|
+
const dotGit = path.join(at, '.git');
|
|
820
|
+
if (fs.existsSync(dotGit)) {
|
|
821
|
+
let gitDir = dotGit;
|
|
822
|
+
if (fs.statSync(dotGit).isFile()) {
|
|
823
|
+
const pointer = fs.readFileSync(dotGit, 'utf8').match(/^gitdir:\s*(.+)$/m);
|
|
824
|
+
if (!pointer)
|
|
825
|
+
return null;
|
|
826
|
+
gitDir = path.resolve(at, pointer[1].trim());
|
|
827
|
+
}
|
|
828
|
+
const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim().match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
829
|
+
return head && BRANCH_RE.test(head[1]) ? head[1] : null;
|
|
830
|
+
}
|
|
831
|
+
const up = path.dirname(at);
|
|
832
|
+
if (up === at)
|
|
833
|
+
return null;
|
|
834
|
+
at = up;
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* `git push`: how = change, or `protected` (a force push, `--all` / `--tags` /
|
|
844
|
+
* `--follow-tags`, a push to main / master / release* / prod*), or delete
|
|
845
|
+
* (`--delete`, `:branch`, `--mirror`, `--prune`); `--dry-run` = read. Where =
|
|
846
|
+
* `remote=<name>,branch=<destination>` — the remote and refspecs on the command;
|
|
847
|
+
* `HEAD` / no refspec is the current branch, read from `.git/HEAD` only when the
|
|
848
|
+
* working directory is known (`cwd`), else unknown.
|
|
849
|
+
*/
|
|
850
|
+
function gitPushUse(argv, vars, cwd) {
|
|
851
|
+
let k = 0;
|
|
852
|
+
let dir = cwd;
|
|
853
|
+
for (; k < argv.length; k++) {
|
|
854
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
855
|
+
if (!bare.startsWith('-'))
|
|
856
|
+
break;
|
|
857
|
+
if (bare === '-C') {
|
|
858
|
+
const value = (0, credentialGrant_1.literalWord)(argv[k + 1], vars);
|
|
859
|
+
dir = value && dir ? path.resolve(dir, value) : undefined;
|
|
860
|
+
}
|
|
861
|
+
if (GIT_VALUED_GLOBALS.has(bare))
|
|
862
|
+
k++;
|
|
863
|
+
}
|
|
864
|
+
if ((0, credentialGrant_1.unquote)(argv[k] || '').toLowerCase() !== 'push')
|
|
865
|
+
return undefined;
|
|
866
|
+
let how = 'change';
|
|
867
|
+
let remote;
|
|
868
|
+
const refspecs = [];
|
|
869
|
+
const mark = (next) => { if (next === 'delete' || (next === 'protected' && how !== 'delete') || (next === 'read' && how === 'change'))
|
|
870
|
+
how = next; };
|
|
871
|
+
for (k++; k < argv.length; k++) {
|
|
872
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
873
|
+
if (bare === '--')
|
|
874
|
+
continue;
|
|
875
|
+
if (bare.startsWith('--')) {
|
|
876
|
+
const flag = bare.split('=')[0];
|
|
877
|
+
if (/^--force(?:-with-lease|-if-includes)?$/.test(flag) || flag === '--all' || flag === '--branches' || flag === '--tags' || flag === '--follow-tags')
|
|
878
|
+
mark('protected');
|
|
879
|
+
else if (flag === '--delete' || flag === '--mirror' || flag === '--prune')
|
|
880
|
+
mark('delete');
|
|
881
|
+
else if (flag === '--dry-run')
|
|
882
|
+
mark('read');
|
|
883
|
+
else if (flag === '--repo')
|
|
884
|
+
remote = bare.includes('=') ? (0, credentialGrant_1.literalWord)(bare.slice(7), vars) : (0, credentialGrant_1.literalWord)(argv[++k], vars);
|
|
885
|
+
else if (GIT_PUSH_VALUED.has(flag) && !bare.includes('='))
|
|
886
|
+
k++;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (bare.startsWith('-') && bare.length > 1) {
|
|
890
|
+
if (/f/.test(bare.slice(1)))
|
|
891
|
+
mark('protected');
|
|
892
|
+
if (/d/.test(bare.slice(1)))
|
|
893
|
+
mark('delete');
|
|
894
|
+
if (/n/.test(bare.slice(1)))
|
|
895
|
+
mark('read');
|
|
896
|
+
if (bare === '-o')
|
|
897
|
+
k++;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (remote === undefined) {
|
|
901
|
+
remote = (0, credentialGrant_1.literalWord)(argv[k], vars);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
refspecs.push((0, credentialGrant_1.literalWord)(argv[k], vars));
|
|
905
|
+
}
|
|
906
|
+
const branches = [];
|
|
907
|
+
const head = () => (dir ? currentGitBranch(dir) : null);
|
|
908
|
+
if (!refspecs.length)
|
|
909
|
+
branches.push(remote ? head() : null);
|
|
910
|
+
for (const spec of refspecs) {
|
|
911
|
+
if (spec === null) {
|
|
912
|
+
branches.push(null);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
let ref = spec;
|
|
916
|
+
if (ref.startsWith('+')) {
|
|
917
|
+
mark('protected');
|
|
918
|
+
ref = ref.slice(1);
|
|
919
|
+
}
|
|
920
|
+
const colon = ref.indexOf(':');
|
|
921
|
+
if (colon === 0) {
|
|
922
|
+
mark('delete');
|
|
923
|
+
ref = ref.slice(1);
|
|
924
|
+
}
|
|
925
|
+
else if (colon > 0)
|
|
926
|
+
ref = ref.slice(colon + 1);
|
|
927
|
+
if (ref.startsWith('refs/tags/')) {
|
|
928
|
+
mark('protected');
|
|
929
|
+
ref = ref.slice(10);
|
|
930
|
+
}
|
|
931
|
+
ref = ref.replace(/^refs\/heads\//, '');
|
|
932
|
+
branches.push(ref === 'HEAD' || ref === '@' ? head() : ref);
|
|
933
|
+
}
|
|
934
|
+
for (const branch of branches)
|
|
935
|
+
if (branch && PROTECTED_BRANCH_RE.test(branch))
|
|
936
|
+
mark('protected');
|
|
937
|
+
const remoteOk = remote && BRANCH_RE.test(remote) && !/[:/@]/.test(remote) ? remote : null;
|
|
938
|
+
const known = branches.every(b => b && BRANCH_RE.test(b));
|
|
939
|
+
const where = `remote=${remoteOk || '?'},branch=${known ? [...new Set(branches)].sort().join('+') : '?'}`;
|
|
940
|
+
return { tool: 'git', how, where, mutatesDefaults: false };
|
|
941
|
+
}
|
|
942
|
+
// ---- kubectl / oc / helm ---------------------------------------------------------------
|
|
943
|
+
const KUBE_VALUED_GLOBALS = new Set(['-n', '--namespace', '--context', '--kube-context', '--kubeconfig', '--cluster', '--user', '-s', '--server', '--token', '--as', '--as-group', '--as-uid', '--request-timeout', '-v', '--v', '--kube-apiserver', '--kube-token', '--kube-as-user', '--kube-as-group', '--kube-ca-file', '--registry-config', '--repository-config', '--repository-cache']);
|
|
944
|
+
/** Flags that point the use at another cluster or identity than the context says. */
|
|
945
|
+
const KUBE_TARGET_OVERRIDES = ['--cluster', '--user', '-s', '--server', '--token', '--as', '--as-group', '--as-uid', '--kube-apiserver', '--kube-token', '--kube-as-user', '--kube-as-group'];
|
|
946
|
+
const KUBE_READ_VERBS = new Set(['get', 'describe', 'logs', 'log', 'top', 'explain', 'api-resources', 'api-versions', 'version', 'cluster-info', 'events', 'diff', 'wait', 'status', 'history', 'projects', 'whoami']);
|
|
947
|
+
const KUBE_EXEC_VERBS = new Set(['exec', 'attach', 'cp', 'port-forward', 'debug', 'proxy', 'rsh', 'rsync']);
|
|
948
|
+
const KUBE_CHANGE_VERBS = new Set(['apply', 'create', 'scale', 'rollout', 'patch', 'set', 'label', 'annotate', 'edit', 'expose', 'run', 'autoscale', 'cordon', 'uncordon', 'drain', 'taint', 'replace', 'kustomize', 'install', 'upgrade', 'rollback', 'test', 'new-app', 'new-project', 'new-build', 'start-build', 'import-image', 'tag', 'adm', 'process']);
|
|
949
|
+
const KUBE_DELETE_VERBS = new Set(['delete', 'uninstall', 'del', 'un']);
|
|
950
|
+
/** helm commands that never talk to a cluster. */
|
|
951
|
+
const HELM_LOCAL = new Set(['template', 'lint', 'package', 'repo', 'search', 'show', 'inspect', 'version', 'env', 'dependency', 'dep', 'plugin', 'completion', 'create', 'pull', 'fetch', 'push', 'registry', 'verify', 'help']);
|
|
952
|
+
/** kubectl / oc commands that act on the local client or the sign-in, not a cluster. */
|
|
953
|
+
const KUBE_LOCAL = new Set(['help', 'completion', 'plugin', 'login', 'logout', 'kustomize', 'options', 'alpha']);
|
|
954
|
+
const KUBE_CONFIG_READ = new Set(['view', 'current-context', 'get-contexts', 'get-clusters', 'get-users']);
|
|
955
|
+
const KUBE_ENV_ON_LINE_RE = /\b(?:kubeconfig|helm_kubecontext|helm_namespace|helm_kubeapiserver|helm_kubetoken|helm_kubeasuser)\s*=|\$env:(?:kubeconfig|helm_\w+)\b|\bexport\s+(?:kubeconfig|helm_\w+)\b/i;
|
|
956
|
+
const KUBE_NAME_RE = /^[^\s|,$`(){}%<>;&"']{1,253}$/;
|
|
957
|
+
const NAMESPACE_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
958
|
+
function readKubeconfigs(files) {
|
|
959
|
+
try {
|
|
960
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
961
|
+
const YAML = require('yaml');
|
|
962
|
+
const merged = { namespaces: new Map() };
|
|
963
|
+
let readAny = false;
|
|
964
|
+
for (const file of files) {
|
|
965
|
+
if (!fs.existsSync(file))
|
|
966
|
+
continue;
|
|
967
|
+
const doc = YAML.parse(fs.readFileSync(file, 'utf8'));
|
|
968
|
+
readAny = true;
|
|
969
|
+
if (!doc || typeof doc !== 'object')
|
|
970
|
+
continue;
|
|
971
|
+
// First file to set a value wins (kubectl's merge rule).
|
|
972
|
+
if (merged.current === undefined && typeof doc['current-context'] === 'string' && doc['current-context'])
|
|
973
|
+
merged.current = doc['current-context'];
|
|
974
|
+
for (const entry of Array.isArray(doc.contexts) ? doc.contexts : []) {
|
|
975
|
+
const name = entry && typeof entry.name === 'string' ? entry.name : undefined;
|
|
976
|
+
if (!name || merged.namespaces.has(name))
|
|
977
|
+
continue;
|
|
978
|
+
const ns = entry.context && typeof entry.context.namespace === 'string' ? entry.context.namespace : undefined;
|
|
979
|
+
merged.namespaces.set(name, ns);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return readAny ? merged : null;
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/** `--kubeconfig`, else KUBECONFIG (a path list), else `~/.kube/config`. */
|
|
989
|
+
function kubeDefaultsFrom(env) {
|
|
990
|
+
const filesFor = (kubeconfig) => kubeconfig
|
|
991
|
+
? [kubeconfig]
|
|
992
|
+
: (env.KUBECONFIG || '').trim()
|
|
993
|
+
? env.KUBECONFIG.split(path.delimiter).map(f => f.trim()).filter(Boolean)
|
|
994
|
+
: [path.join(os.homedir(), '.kube', 'config')];
|
|
995
|
+
const helmValue = (key) => (env[key] || '').trim() || undefined;
|
|
996
|
+
return {
|
|
997
|
+
helm: {
|
|
998
|
+
context: helmValue('HELM_KUBECONTEXT'),
|
|
999
|
+
namespace: helmValue('HELM_NAMESPACE'),
|
|
1000
|
+
unknown: Boolean(helmValue('HELM_KUBEAPISERVER') || helmValue('HELM_KUBETOKEN') || helmValue('HELM_KUBEASUSER')),
|
|
1001
|
+
},
|
|
1002
|
+
context(kubeconfig) {
|
|
1003
|
+
return readKubeconfigs(filesFor(kubeconfig))?.current || null;
|
|
1004
|
+
},
|
|
1005
|
+
namespace(context, kubeconfig) {
|
|
1006
|
+
const config = readKubeconfigs(filesFor(kubeconfig));
|
|
1007
|
+
if (!config || !config.namespaces.has(context))
|
|
1008
|
+
return null;
|
|
1009
|
+
return config.namespaces.get(context) || 'default';
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function kubeHow(tool, verb, rest) {
|
|
1014
|
+
const next = rest.map(w => (0, credentialGrant_1.unquote)(w).toLowerCase()).find(w => !w.startsWith('-')) || '';
|
|
1015
|
+
if (tool === 'helm') {
|
|
1016
|
+
if (HELM_LOCAL.has(verb))
|
|
1017
|
+
return { how: 'read', local: true };
|
|
1018
|
+
if (KUBE_DELETE_VERBS.has(verb))
|
|
1019
|
+
return { how: 'delete' };
|
|
1020
|
+
if (verb === 'list' || verb === 'ls' || verb === 'status' || verb === 'get' || verb === 'history')
|
|
1021
|
+
return { how: 'read' };
|
|
1022
|
+
if (KUBE_CHANGE_VERBS.has(verb))
|
|
1023
|
+
return { how: 'change' };
|
|
1024
|
+
return { how: `cmd:${verb}`, mutatesDefaults: true };
|
|
1025
|
+
}
|
|
1026
|
+
if (verb === 'config')
|
|
1027
|
+
return KUBE_CONFIG_READ.has(next) ? { how: 'read', local: true } : { how: 'change', local: true, mutatesDefaults: true };
|
|
1028
|
+
if (tool === 'oc' && verb === 'project')
|
|
1029
|
+
return next ? { how: 'change', local: true, mutatesDefaults: true } : { how: 'read' };
|
|
1030
|
+
if (KUBE_LOCAL.has(verb))
|
|
1031
|
+
return { how: 'read', local: true };
|
|
1032
|
+
if (verb === 'delete')
|
|
1033
|
+
return { how: 'delete' };
|
|
1034
|
+
if (verb === 'replace' && rest.some(w => (0, credentialGrant_1.unquote)(w) === '--force'))
|
|
1035
|
+
return { how: 'delete' };
|
|
1036
|
+
if (verb === 'auth' || verb === 'policy')
|
|
1037
|
+
return { how: 'permissions' };
|
|
1038
|
+
if (verb === 'certificate' && /^(?:approve|deny)$/.test(next))
|
|
1039
|
+
return { how: 'permissions' };
|
|
1040
|
+
if (verb === 'create' && /^(?:role|clusterrole|rolebinding|clusterrolebinding|token)$/.test(next))
|
|
1041
|
+
return { how: 'permissions' };
|
|
1042
|
+
if (tool === 'oc' && verb === 'adm' && next === 'policy')
|
|
1043
|
+
return { how: 'permissions' };
|
|
1044
|
+
if (KUBE_READ_VERBS.has(verb))
|
|
1045
|
+
return { how: 'read' };
|
|
1046
|
+
if (KUBE_EXEC_VERBS.has(verb))
|
|
1047
|
+
return { how: 'exec' };
|
|
1048
|
+
if (KUBE_CHANGE_VERBS.has(verb))
|
|
1049
|
+
return { how: 'change' };
|
|
1050
|
+
// A plugin (`kubectl ctx`, `kubectl ns`) can switch the context itself.
|
|
1051
|
+
return { how: `cmd:${verb}`, mutatesDefaults: true };
|
|
1052
|
+
}
|
|
1053
|
+
function kubeUse(tool, argv, vars, defaults, defaultsUnreliable) {
|
|
1054
|
+
let verb = '';
|
|
1055
|
+
let verbAt = -1;
|
|
1056
|
+
for (let k = 0; k < argv.length; k++) {
|
|
1057
|
+
const bare = (0, credentialGrant_1.unquote)(argv[k]);
|
|
1058
|
+
if (bare.startsWith('-')) {
|
|
1059
|
+
if (KUBE_VALUED_GLOBALS.has(bare))
|
|
1060
|
+
k++;
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (WORD_RE.test(bare.toLowerCase())) {
|
|
1064
|
+
verb = bare.toLowerCase();
|
|
1065
|
+
verbAt = k;
|
|
1066
|
+
}
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
if (!verb)
|
|
1070
|
+
return { tool, how: 'read', mutatesDefaults: false };
|
|
1071
|
+
const { how, local, mutatesDefaults = false } = kubeHow(tool, verb, argv.slice(verbAt + 1));
|
|
1072
|
+
if (local)
|
|
1073
|
+
return { tool, how, mutatesDefaults };
|
|
1074
|
+
const words = [...argv];
|
|
1075
|
+
const helm = tool === 'helm';
|
|
1076
|
+
if (KUBE_TARGET_OVERRIDES.some(flag => (0, credentialGrant_1.flagValue)(words, [flag], vars) !== undefined) || (helm && defaults.helm.unknown))
|
|
1077
|
+
return { tool, how, where: '?', mutatesDefaults };
|
|
1078
|
+
const kubeconfigFlag = (0, credentialGrant_1.flagValue)(words, ['--kubeconfig'], vars);
|
|
1079
|
+
if (kubeconfigFlag === null)
|
|
1080
|
+
return { tool, how, where: '?', mutatesDefaults };
|
|
1081
|
+
const contextFlag = (0, credentialGrant_1.flagValue)(words, helm ? ['--kube-context'] : ['--context'], vars);
|
|
1082
|
+
const contextDefault = () => defaultsUnreliable ? null : (helm && defaults.helm.context) || defaults.context(kubeconfigFlag);
|
|
1083
|
+
let context = contextFlag === undefined ? contextDefault() : contextFlag;
|
|
1084
|
+
if (context && !KUBE_NAME_RE.test(context))
|
|
1085
|
+
context = null;
|
|
1086
|
+
let namespace;
|
|
1087
|
+
if (words.some(w => /^(?:-A|--all-namespaces(?:=true)?)$/.test((0, credentialGrant_1.unquote)(w))))
|
|
1088
|
+
namespace = '*';
|
|
1089
|
+
else {
|
|
1090
|
+
const glued = words.map(credentialGrant_1.unquote).find(w => /^-n[a-z0-9]/.test(w));
|
|
1091
|
+
const nsFlag = glued ? (0, credentialGrant_1.literalWord)(glued.slice(2), vars) : (0, credentialGrant_1.flagValue)(words, ['-n', '--namespace'], vars);
|
|
1092
|
+
const nsDefault = () => !context || defaultsUnreliable ? null : (helm && defaults.helm.namespace) || defaults.namespace(context, kubeconfigFlag);
|
|
1093
|
+
namespace = nsFlag === undefined ? nsDefault() : nsFlag;
|
|
1094
|
+
}
|
|
1095
|
+
if (namespace && namespace !== '*' && !NAMESPACE_RE.test(namespace))
|
|
1096
|
+
namespace = null;
|
|
1097
|
+
const parts = [`context=${context || '?'}`, `namespace=${namespace || '?'}`];
|
|
1098
|
+
if (kubeconfigFlag) {
|
|
1099
|
+
const file = kubeconfigFlag.replace(/\\/g, '/');
|
|
1100
|
+
parts.push(`kubeconfig=${UNSAFE_VALUE_RE.test(file) ? '?' : file}`);
|
|
1101
|
+
}
|
|
1102
|
+
return { tool, how, where: parts.join(','), mutatesDefaults };
|
|
1103
|
+
}
|
|
1104
|
+
/** kubectl / oc / helm uses on the line, in order. */
|
|
1105
|
+
function kubeUses(uses, command, dialect, vars, defaults) {
|
|
1106
|
+
const argvs = uses.map(use => ({ tool: use.tool, argv: toolArgv(use.text, dialect, [use.tool]) }));
|
|
1107
|
+
const lineSetsEnv = KUBE_ENV_ON_LINE_RE.test(command);
|
|
1108
|
+
const first = argvs.map(({ tool, argv }) => kubeUse(tool, argv, vars, defaults, lineSetsEnv));
|
|
1109
|
+
if (!first.some(use => use.mutatesDefaults))
|
|
1110
|
+
return first;
|
|
1111
|
+
return argvs.map(({ tool, argv }) => kubeUse(tool, argv, vars, defaults, true));
|
|
1112
|
+
}
|