cloud-doctor 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/bin/cloud-doctor.js +13 -0
- package/dist/chunk-4PZ65VRA.js +586 -0
- package/dist/chunk-4PZ65VRA.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +612 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# cloud-doctor
|
|
2
|
+
|
|
3
|
+
**Alpha** — early scaffold. One command grades your whole cloud account.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx cloud-doctor@alpha
|
|
7
|
+
npx cloud-doctor@alpha aws --profile prod
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Status
|
|
11
|
+
|
|
12
|
+
This is an **alpha name-reservation release**. The CLI shell works (provider picker, AWS profile discovery, identity validation). Security rules and scoring land in upcoming releases.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cloud-doctor # interactive: pick provider → AWS profile → scan
|
|
18
|
+
cloud-doctor aws # grade AWS account
|
|
19
|
+
cloud-doctor aws profiles # list ~/.aws/config profiles
|
|
20
|
+
cloud-doctor aws whoami # show resolved AWS identity
|
|
21
|
+
cloud-doctor aws --yes --json # CI-friendly JSON output
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import module from "node:module";
|
|
4
|
+
|
|
5
|
+
if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
|
|
6
|
+
try {
|
|
7
|
+
module.enableCompileCache();
|
|
8
|
+
} catch {
|
|
9
|
+
// Ignore compile-cache errors.
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await import("../dist/cli/index.js");
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
// ../core/dist/index.js
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { parseJSONC } from "confbox";
|
|
7
|
+
var PERFECT_SCORE = 100;
|
|
8
|
+
var SCORE_GOOD_THRESHOLD = 75;
|
|
9
|
+
var SCORE_OK_THRESHOLD = 50;
|
|
10
|
+
var TOP_FIXES_DISPLAY_COUNT = 5;
|
|
11
|
+
var ERROR_RULE_PENALTY = 1.5;
|
|
12
|
+
var WARNING_RULE_PENALTY = 0.75;
|
|
13
|
+
var DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
14
|
+
"Security",
|
|
15
|
+
"Misconfiguration",
|
|
16
|
+
"Observability",
|
|
17
|
+
"Reliability",
|
|
18
|
+
"Governance"
|
|
19
|
+
];
|
|
20
|
+
var getScoreLabel = (score) => {
|
|
21
|
+
if (score >= SCORE_GOOD_THRESHOLD) return "Great";
|
|
22
|
+
if (score >= SCORE_OK_THRESHOLD) return "Needs work";
|
|
23
|
+
return "Critical";
|
|
24
|
+
};
|
|
25
|
+
var calculateScore = (diagnostics) => {
|
|
26
|
+
if (diagnostics.length === 0) {
|
|
27
|
+
return { score: PERFECT_SCORE, label: getScoreLabel(PERFECT_SCORE) };
|
|
28
|
+
}
|
|
29
|
+
const errorRules = /* @__PURE__ */ new Set();
|
|
30
|
+
const warningRules = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const diagnostic of diagnostics) {
|
|
32
|
+
const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
33
|
+
if (diagnostic.severity === "error") {
|
|
34
|
+
errorRules.add(ruleKey);
|
|
35
|
+
} else {
|
|
36
|
+
warningRules.add(ruleKey);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const penalty = errorRules.size * ERROR_RULE_PENALTY + warningRules.size * WARNING_RULE_PENALTY;
|
|
40
|
+
const score = Math.max(0, Math.round(PERFECT_SCORE - penalty));
|
|
41
|
+
return { score, label: getScoreLabel(score) };
|
|
42
|
+
};
|
|
43
|
+
var highlighter = {
|
|
44
|
+
error: pc.red,
|
|
45
|
+
warn: pc.yellow,
|
|
46
|
+
info: pc.cyan,
|
|
47
|
+
success: pc.green,
|
|
48
|
+
dim: pc.dim,
|
|
49
|
+
gray: pc.gray,
|
|
50
|
+
bold: pc.bold
|
|
51
|
+
};
|
|
52
|
+
var setColorEnabled = (enabled) => {
|
|
53
|
+
const colors = pc.createColors(enabled);
|
|
54
|
+
highlighter.error = colors.red;
|
|
55
|
+
highlighter.warn = colors.yellow;
|
|
56
|
+
highlighter.info = colors.cyan;
|
|
57
|
+
highlighter.success = colors.green;
|
|
58
|
+
highlighter.dim = colors.dim;
|
|
59
|
+
highlighter.gray = colors.gray;
|
|
60
|
+
highlighter.bold = colors.bold;
|
|
61
|
+
};
|
|
62
|
+
var CONFIG_FILENAMES = [
|
|
63
|
+
"doctor.config.ts",
|
|
64
|
+
"doctor.config.mts",
|
|
65
|
+
"doctor.config.js",
|
|
66
|
+
"doctor.config.mjs",
|
|
67
|
+
"doctor.config.json",
|
|
68
|
+
"doctor.config.jsonc"
|
|
69
|
+
];
|
|
70
|
+
var findConfigFile = (startDir) => {
|
|
71
|
+
let current = path.resolve(startDir);
|
|
72
|
+
const root = path.parse(current).root;
|
|
73
|
+
while (true) {
|
|
74
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
75
|
+
const candidate = path.join(current, filename);
|
|
76
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
77
|
+
}
|
|
78
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
79
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
82
|
+
if (packageJson.cloudDoctor) {
|
|
83
|
+
return packageJsonPath;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (current === root) break;
|
|
89
|
+
current = path.dirname(current);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
var loadConfigModule = async (configPath) => {
|
|
94
|
+
const ext = path.extname(configPath);
|
|
95
|
+
if (ext === ".json" || ext === ".jsonc") {
|
|
96
|
+
return parseJSONC(fs.readFileSync(configPath, "utf8"));
|
|
97
|
+
}
|
|
98
|
+
if (configPath.endsWith("package.json")) {
|
|
99
|
+
const packageJson = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
100
|
+
return packageJson.cloudDoctor ?? {};
|
|
101
|
+
}
|
|
102
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
103
|
+
const loaded = await jiti.import(configPath);
|
|
104
|
+
const config = loaded.default ?? loaded;
|
|
105
|
+
return config;
|
|
106
|
+
};
|
|
107
|
+
var loadConfig = async (cwd = process.cwd()) => {
|
|
108
|
+
const configPath = findConfigFile(cwd);
|
|
109
|
+
if (!configPath) return {};
|
|
110
|
+
return loadConfigModule(configPath);
|
|
111
|
+
};
|
|
112
|
+
var defineConfig = (config) => config;
|
|
113
|
+
var PluginRegistry = class {
|
|
114
|
+
plugins = /* @__PURE__ */ new Map();
|
|
115
|
+
register(plugin) {
|
|
116
|
+
this.plugins.set(plugin.id, plugin);
|
|
117
|
+
}
|
|
118
|
+
get(id) {
|
|
119
|
+
return this.plugins.get(id);
|
|
120
|
+
}
|
|
121
|
+
require(id) {
|
|
122
|
+
const plugin = this.plugins.get(id);
|
|
123
|
+
if (!plugin) {
|
|
124
|
+
throw new Error(`Unknown provider: ${id}`);
|
|
125
|
+
}
|
|
126
|
+
return plugin;
|
|
127
|
+
}
|
|
128
|
+
list() {
|
|
129
|
+
return [...this.plugins.values()];
|
|
130
|
+
}
|
|
131
|
+
listAvailable() {
|
|
132
|
+
return this.list().filter((plugin) => plugin.status !== "coming-soon");
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var createPluginRegistry = () => new PluginRegistry();
|
|
136
|
+
var buildCategoryCounts = (diagnostics) => {
|
|
137
|
+
const counts = {};
|
|
138
|
+
for (const diagnostic of diagnostics) {
|
|
139
|
+
const bucket = counts[diagnostic.category] ?? { errors: 0, warnings: 0 };
|
|
140
|
+
if (diagnostic.severity === "error") {
|
|
141
|
+
bucket.errors += 1;
|
|
142
|
+
} else {
|
|
143
|
+
bucket.warnings += 1;
|
|
144
|
+
}
|
|
145
|
+
counts[diagnostic.category] = bucket;
|
|
146
|
+
}
|
|
147
|
+
return counts;
|
|
148
|
+
};
|
|
149
|
+
var buildScanSummary = (diagnostics) => {
|
|
150
|
+
const { score, label } = calculateScore(diagnostics);
|
|
151
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
152
|
+
const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
|
|
153
|
+
return {
|
|
154
|
+
score,
|
|
155
|
+
label,
|
|
156
|
+
errorCount,
|
|
157
|
+
warningCount,
|
|
158
|
+
categoryCounts: buildCategoryCounts(diagnostics)
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
var computeProjectedScore = (diagnostics, topRuleCount = TOP_FIXES_DISPLAY_COUNT) => {
|
|
162
|
+
const errorRuleKeys = /* @__PURE__ */ new Set();
|
|
163
|
+
const orderedErrorRules = [];
|
|
164
|
+
for (const diagnostic of diagnostics) {
|
|
165
|
+
if (diagnostic.severity !== "error") continue;
|
|
166
|
+
const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
167
|
+
if (!errorRuleKeys.has(ruleKey)) {
|
|
168
|
+
errorRuleKeys.add(ruleKey);
|
|
169
|
+
orderedErrorRules.push(ruleKey);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (orderedErrorRules.length === 0) return void 0;
|
|
173
|
+
const rulesToRemove = new Set(orderedErrorRules.slice(0, topRuleCount));
|
|
174
|
+
const remaining = diagnostics.filter(
|
|
175
|
+
(d) => !rulesToRemove.has(`${d.plugin}/${d.rule}`)
|
|
176
|
+
);
|
|
177
|
+
return calculateScore(remaining).score;
|
|
178
|
+
};
|
|
179
|
+
var buildJsonReport = (input) => {
|
|
180
|
+
const summary = buildScanSummary(input.diagnostics);
|
|
181
|
+
const projectedScore = computeProjectedScore(input.diagnostics);
|
|
182
|
+
if (projectedScore !== void 0) {
|
|
183
|
+
summary.projectedScore = projectedScore;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
provider: input.provider,
|
|
188
|
+
identity: input.identity,
|
|
189
|
+
summary,
|
|
190
|
+
diagnostics: input.diagnostics,
|
|
191
|
+
meta: input.meta
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
var buildJsonReportError = (input) => ({
|
|
195
|
+
schemaVersion: 1,
|
|
196
|
+
error: input.error instanceof Error ? input.error.message : String(input.error),
|
|
197
|
+
elapsedMilliseconds: input.elapsedMilliseconds
|
|
198
|
+
});
|
|
199
|
+
var groupDiagnosticsByRule = (diagnostics) => {
|
|
200
|
+
const groups = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const diagnostic of diagnostics) {
|
|
202
|
+
const key = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
203
|
+
const existing = groups.get(key) ?? [];
|
|
204
|
+
existing.push(diagnostic);
|
|
205
|
+
groups.set(key, existing);
|
|
206
|
+
}
|
|
207
|
+
return groups;
|
|
208
|
+
};
|
|
209
|
+
var getTopErrorRules = (diagnostics, limit = TOP_FIXES_DISPLAY_COUNT) => {
|
|
210
|
+
const errorDiagnostics = diagnostics.filter((d) => d.severity === "error");
|
|
211
|
+
const groups = groupDiagnosticsByRule(errorDiagnostics);
|
|
212
|
+
return [...groups.entries()].slice(0, limit).map(([ruleKey, ruleDiagnostics]) => ({ ruleKey, diagnostics: ruleDiagnostics }));
|
|
213
|
+
};
|
|
214
|
+
var sortCategoriesForDisplay = (categories) => {
|
|
215
|
+
const rank = new Map(DIAGNOSTIC_CATEGORY_BUCKETS.map((category, index) => [category, index]));
|
|
216
|
+
return [...categories].sort((a, b) => {
|
|
217
|
+
const rankA = rank.get(a);
|
|
218
|
+
const rankB = rank.get(b);
|
|
219
|
+
if (rankA !== void 0 && rankB !== void 0) return rankA - rankB;
|
|
220
|
+
if (rankA !== void 0) return -1;
|
|
221
|
+
if (rankB !== void 0) return 1;
|
|
222
|
+
return a.localeCompare(b);
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// ../plugin-aws/dist/index.js
|
|
227
|
+
import * as os from "os";
|
|
228
|
+
import * as path2 from "path";
|
|
229
|
+
import { loadSharedConfigFiles } from "@smithy/shared-ini-file-loader";
|
|
230
|
+
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
|
|
231
|
+
import { fromEnv, fromIni, fromInstanceMetadata, fromTokenFile } from "@aws-sdk/credential-providers";
|
|
232
|
+
var profileNameFromSection = (sectionName) => {
|
|
233
|
+
if (sectionName === "default") return "default";
|
|
234
|
+
if (sectionName.startsWith("profile ")) return sectionName.slice("profile ".length);
|
|
235
|
+
if (!sectionName.includes(" ")) return sectionName;
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
var classifyProfile = (profileName, configSection, hasStaticCredentials) => {
|
|
239
|
+
if (configSection["sso_start_url"] || configSection["sso_session"]) return "sso";
|
|
240
|
+
if (configSection["role_arn"]) return "assume-role";
|
|
241
|
+
if (configSection["web_identity_token_file"]) return "web-identity";
|
|
242
|
+
if (hasStaticCredentials) return "static";
|
|
243
|
+
if (profileName === "default" && hasStaticCredentials) return "static";
|
|
244
|
+
return "unknown";
|
|
245
|
+
};
|
|
246
|
+
var getAwsConfigPaths = () => {
|
|
247
|
+
const home = os.homedir();
|
|
248
|
+
return {
|
|
249
|
+
configFile: process.env.AWS_CONFIG_FILE ?? path2.join(home, ".aws", "config"),
|
|
250
|
+
credentialsFile: process.env.AWS_SHARED_CREDENTIALS_FILE ?? path2.join(home, ".aws", "credentials")
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
var listAwsProfiles = async () => {
|
|
254
|
+
const { configFile, credentialsFile } = getAwsConfigPaths();
|
|
255
|
+
const { configFile: configSections, credentialsFile: credentialSections } = await loadSharedConfigFiles({
|
|
256
|
+
configFilepath: configFile,
|
|
257
|
+
filepath: credentialsFile
|
|
258
|
+
});
|
|
259
|
+
const profileNames = /* @__PURE__ */ new Set();
|
|
260
|
+
for (const sectionName of Object.keys(configSections)) {
|
|
261
|
+
const profileName = profileNameFromSection(sectionName);
|
|
262
|
+
if (profileName) profileNames.add(profileName);
|
|
263
|
+
}
|
|
264
|
+
for (const sectionName of Object.keys(credentialSections)) {
|
|
265
|
+
const profileName = profileNameFromSection(sectionName);
|
|
266
|
+
if (profileName) profileNames.add(profileName);
|
|
267
|
+
}
|
|
268
|
+
if (profileNames.size === 0 && process.env.AWS_ACCESS_KEY_ID) {
|
|
269
|
+
return [
|
|
270
|
+
{
|
|
271
|
+
name: "(environment)",
|
|
272
|
+
kind: "unknown"
|
|
273
|
+
}
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
const profiles = [];
|
|
277
|
+
for (const profileName of [...profileNames].sort((a, b) => {
|
|
278
|
+
if (a === "default") return -1;
|
|
279
|
+
if (b === "default") return 1;
|
|
280
|
+
return a.localeCompare(b);
|
|
281
|
+
})) {
|
|
282
|
+
const configSection = configSections[`profile ${profileName}`] ?? configSections[profileName] ?? {};
|
|
283
|
+
const credentialSection = credentialSections[profileName] ?? {};
|
|
284
|
+
const hasStaticCredentials = Boolean(
|
|
285
|
+
credentialSection.aws_access_key_id && credentialSection.aws_secret_access_key
|
|
286
|
+
);
|
|
287
|
+
profiles.push({
|
|
288
|
+
name: profileName,
|
|
289
|
+
region: configSection.region,
|
|
290
|
+
kind: classifyProfile(profileName, configSection, hasStaticCredentials),
|
|
291
|
+
ssoSessionName: configSection.sso_session,
|
|
292
|
+
roleArn: configSection.role_arn,
|
|
293
|
+
sourceProfile: configSection.source_profile
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return profiles;
|
|
297
|
+
};
|
|
298
|
+
var resolveDefaultProfileName = (profiles) => {
|
|
299
|
+
if (process.env.AWS_PROFILE) return process.env.AWS_PROFILE;
|
|
300
|
+
if (profiles.some((profile) => profile.name === "default")) return "default";
|
|
301
|
+
if (profiles.length === 1 && profiles[0]?.name !== "(environment)") {
|
|
302
|
+
return profiles[0]?.name;
|
|
303
|
+
}
|
|
304
|
+
return void 0;
|
|
305
|
+
};
|
|
306
|
+
var hasEnvironmentCredentials = () => Boolean(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY);
|
|
307
|
+
var hasWebIdentityCredentials = () => Boolean(process.env.AWS_WEB_IDENTITY_TOKEN_FILE && process.env.AWS_ROLE_ARN);
|
|
308
|
+
var kindFromProfile = (profile) => {
|
|
309
|
+
switch (profile.kind) {
|
|
310
|
+
case "sso":
|
|
311
|
+
return "sso";
|
|
312
|
+
case "assume-role":
|
|
313
|
+
return "assume-role";
|
|
314
|
+
case "web-identity":
|
|
315
|
+
return "web-identity";
|
|
316
|
+
case "static":
|
|
317
|
+
return "profile";
|
|
318
|
+
default:
|
|
319
|
+
return "profile";
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var createCredentialProvider = (input) => {
|
|
323
|
+
if (input.manualAccessKeyId && input.manualSecretAccessKey) {
|
|
324
|
+
return async () => ({
|
|
325
|
+
accessKeyId: input.manualAccessKeyId,
|
|
326
|
+
secretAccessKey: input.manualSecretAccessKey,
|
|
327
|
+
sessionToken: input.manualSessionToken
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (input.useEnvironment || !input.profileName && process.env.AWS_ACCESS_KEY_ID) {
|
|
331
|
+
return fromEnv();
|
|
332
|
+
}
|
|
333
|
+
if (!input.profileName && process.env.AWS_WEB_IDENTITY_TOKEN_FILE) {
|
|
334
|
+
return fromTokenFile({
|
|
335
|
+
roleArn: process.env.AWS_ROLE_ARN,
|
|
336
|
+
webIdentityTokenFile: process.env.AWS_WEB_IDENTITY_TOKEN_FILE
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (input.profileName) {
|
|
340
|
+
return fromIni({ profile: input.profileName });
|
|
341
|
+
}
|
|
342
|
+
return async () => {
|
|
343
|
+
try {
|
|
344
|
+
return await fromEnv()();
|
|
345
|
+
} catch {
|
|
346
|
+
try {
|
|
347
|
+
return await fromIni()();
|
|
348
|
+
} catch {
|
|
349
|
+
return fromInstanceMetadata()();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
var validateAwsCredentials = async (input) => {
|
|
355
|
+
const credentials = createCredentialProvider({
|
|
356
|
+
profileName: input.profileName,
|
|
357
|
+
region: input.region,
|
|
358
|
+
useEnvironment: input.useEnvironment,
|
|
359
|
+
manualAccessKeyId: input.manualAccessKeyId,
|
|
360
|
+
manualSecretAccessKey: input.manualSecretAccessKey,
|
|
361
|
+
manualSessionToken: input.manualSessionToken
|
|
362
|
+
});
|
|
363
|
+
const client = new STSClient({
|
|
364
|
+
credentials,
|
|
365
|
+
region: input.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1"
|
|
366
|
+
});
|
|
367
|
+
try {
|
|
368
|
+
const response = await client.send(new GetCallerIdentityCommand({}));
|
|
369
|
+
return {
|
|
370
|
+
provider: "aws",
|
|
371
|
+
id: response.Account ?? "unknown",
|
|
372
|
+
label: input.label,
|
|
373
|
+
kind: input.kind,
|
|
374
|
+
profileName: input.profileName,
|
|
375
|
+
region: input.region,
|
|
376
|
+
accountId: response.Account,
|
|
377
|
+
arn: response.Arn
|
|
378
|
+
};
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
381
|
+
if (message.includes("Token has expired") || message.includes("sso")) {
|
|
382
|
+
throw new SsoLoginRequiredError(input.profileName ?? "default");
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
} finally {
|
|
386
|
+
client.destroy();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var SsoLoginRequiredError = class extends Error {
|
|
390
|
+
profileName;
|
|
391
|
+
constructor(profileName) {
|
|
392
|
+
super(`Profile "${profileName}" requires SSO login. Run: aws sso login --profile ${profileName}`);
|
|
393
|
+
this.name = "SsoLoginRequiredError";
|
|
394
|
+
this.profileName = profileName;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var profileToIdentityKind = kindFromProfile;
|
|
398
|
+
var runAwsRules = async (identity, _config) => {
|
|
399
|
+
void identity;
|
|
400
|
+
return [];
|
|
401
|
+
};
|
|
402
|
+
var formatProfileLabel = (profileName, region) => {
|
|
403
|
+
const regionSuffix = region ? ` \xB7 ${region}` : "";
|
|
404
|
+
return `${profileName}${regionSuffix}`;
|
|
405
|
+
};
|
|
406
|
+
var buildProfileSummaries = async () => {
|
|
407
|
+
const profiles = await listAwsProfiles();
|
|
408
|
+
const summaries = [];
|
|
409
|
+
for (const profile of profiles) {
|
|
410
|
+
if (profile.name === "(environment)") continue;
|
|
411
|
+
summaries.push({
|
|
412
|
+
id: profile.name,
|
|
413
|
+
label: formatProfileLabel(profile.name, profile.region),
|
|
414
|
+
kind: profileToIdentityKind(profile),
|
|
415
|
+
profileName: profile.name,
|
|
416
|
+
defaultRegion: profile.region,
|
|
417
|
+
status: "unknown",
|
|
418
|
+
hint: profile.kind === "sso" ? "SSO profile \u2014 will validate on selection" : profile.kind === "assume-role" ? `Assumes ${profile.roleArn ?? "role"}` : void 0
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (hasEnvironmentCredentials()) {
|
|
422
|
+
summaries.unshift({
|
|
423
|
+
id: "__environment__",
|
|
424
|
+
label: "(environment credentials)",
|
|
425
|
+
kind: "environment",
|
|
426
|
+
status: "unknown",
|
|
427
|
+
hint: "AWS_ACCESS_KEY_ID is set"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (hasWebIdentityCredentials()) {
|
|
431
|
+
summaries.unshift({
|
|
432
|
+
id: "__web_identity__",
|
|
433
|
+
label: "(web identity / OIDC)",
|
|
434
|
+
kind: "web-identity",
|
|
435
|
+
status: "unknown",
|
|
436
|
+
hint: process.env.AWS_ROLE_ARN
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return summaries;
|
|
440
|
+
};
|
|
441
|
+
var resolveProfileInput = (input, configProfile) => {
|
|
442
|
+
if (input.useEnvironment) return { useEnvironment: true };
|
|
443
|
+
if (input.profile) return { profileName: input.profile };
|
|
444
|
+
if (configProfile) return { profileName: configProfile };
|
|
445
|
+
if (process.env.AWS_PROFILE) return { profileName: process.env.AWS_PROFILE };
|
|
446
|
+
const envSelected = input.manualAccessKeyId ? void 0 : hasEnvironmentCredentials();
|
|
447
|
+
if (envSelected && !input.profile) return { useEnvironment: true };
|
|
448
|
+
return {};
|
|
449
|
+
};
|
|
450
|
+
var awsPlugin = {
|
|
451
|
+
id: "aws",
|
|
452
|
+
displayName: "AWS",
|
|
453
|
+
description: "Amazon Web Services account",
|
|
454
|
+
status: "stable",
|
|
455
|
+
async discoverIdentities(_ctx) {
|
|
456
|
+
return buildProfileSummaries();
|
|
457
|
+
},
|
|
458
|
+
async resolveIdentity(ctx, input) {
|
|
459
|
+
const profiles = await listAwsProfiles();
|
|
460
|
+
const resolution = resolveProfileInput(input, void 0);
|
|
461
|
+
if (resolution.useEnvironment) {
|
|
462
|
+
return {
|
|
463
|
+
provider: "aws",
|
|
464
|
+
id: "__environment__",
|
|
465
|
+
label: "(environment credentials)",
|
|
466
|
+
kind: "environment",
|
|
467
|
+
region: input.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (input.manualAccessKeyId && input.manualSecretAccessKey) {
|
|
471
|
+
return {
|
|
472
|
+
provider: "aws",
|
|
473
|
+
id: "__manual__",
|
|
474
|
+
label: "(manual credentials)",
|
|
475
|
+
kind: "manual",
|
|
476
|
+
region: input.region
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const profileName = resolution.profileName ?? resolveDefaultProfileName(profiles) ?? profiles[0]?.name;
|
|
480
|
+
if (!profileName) {
|
|
481
|
+
if (ctx.yes || ctx.json) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"No AWS profiles found. Configure ~/.aws/config or pass --profile, set AWS_PROFILE, or use environment credentials."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
throw new Error("No AWS profiles found");
|
|
487
|
+
}
|
|
488
|
+
const profile = profiles.find((entry) => entry.name === profileName);
|
|
489
|
+
return {
|
|
490
|
+
provider: "aws",
|
|
491
|
+
id: profileName,
|
|
492
|
+
label: formatProfileLabel(profileName, profile?.region ?? input.region),
|
|
493
|
+
kind: profile ? profileToIdentityKind(profile) : "profile",
|
|
494
|
+
profileName,
|
|
495
|
+
region: input.region ?? profile?.region
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
async validateIdentity(identity) {
|
|
499
|
+
if (identity.kind === "manual") {
|
|
500
|
+
throw new Error("Manual credentials must be validated via validateAwsCredentials before scan.");
|
|
501
|
+
}
|
|
502
|
+
return validateAwsCredentials({
|
|
503
|
+
profileName: identity.profileName,
|
|
504
|
+
region: identity.region,
|
|
505
|
+
label: identity.label,
|
|
506
|
+
kind: identity.kind,
|
|
507
|
+
useEnvironment: identity.kind === "environment"
|
|
508
|
+
});
|
|
509
|
+
},
|
|
510
|
+
async scan(identity, config) {
|
|
511
|
+
const start = performance.now();
|
|
512
|
+
const diagnostics = await runAwsRules(identity, config);
|
|
513
|
+
const summary = buildScanSummary(diagnostics);
|
|
514
|
+
return {
|
|
515
|
+
provider: "aws",
|
|
516
|
+
identity,
|
|
517
|
+
diagnostics,
|
|
518
|
+
summary,
|
|
519
|
+
meta: {
|
|
520
|
+
durationMs: Math.round(performance.now() - start),
|
|
521
|
+
regionsScanned: config.regions
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// ../plugins/dist/index.js
|
|
528
|
+
var comingSoon = (id, displayName, description) => ({
|
|
529
|
+
id,
|
|
530
|
+
displayName,
|
|
531
|
+
description,
|
|
532
|
+
status: "coming-soon",
|
|
533
|
+
async discoverIdentities() {
|
|
534
|
+
return [];
|
|
535
|
+
},
|
|
536
|
+
async resolveIdentity() {
|
|
537
|
+
throw new Error(`${displayName} support is coming soon. Use "aws" for now.`);
|
|
538
|
+
},
|
|
539
|
+
async validateIdentity(identity) {
|
|
540
|
+
return identity;
|
|
541
|
+
},
|
|
542
|
+
async scan() {
|
|
543
|
+
throw new Error(`${displayName} support is coming soon.`);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
var gcloudPlugin = comingSoon("gcloud", "GCloud", "Google Cloud project");
|
|
547
|
+
var k8sPlugin = comingSoon("k8s", "Kubernetes", "Cluster workloads");
|
|
548
|
+
var dbPlugin = comingSoon("db", "Database", "Postgres, MySQL, Redis, and more");
|
|
549
|
+
var cloudflarePlugin = comingSoon("cloudflare", "Cloudflare", "DNS, WAF, Workers");
|
|
550
|
+
var cachedRegistry = null;
|
|
551
|
+
var createDefaultRegistry = () => {
|
|
552
|
+
const registry = createPluginRegistry();
|
|
553
|
+
registry.register(awsPlugin);
|
|
554
|
+
registry.register(gcloudPlugin);
|
|
555
|
+
registry.register(k8sPlugin);
|
|
556
|
+
registry.register(dbPlugin);
|
|
557
|
+
registry.register(cloudflarePlugin);
|
|
558
|
+
return registry;
|
|
559
|
+
};
|
|
560
|
+
var getDefaultRegistry = () => {
|
|
561
|
+
if (!cachedRegistry) {
|
|
562
|
+
cachedRegistry = createDefaultRegistry();
|
|
563
|
+
}
|
|
564
|
+
return cachedRegistry;
|
|
565
|
+
};
|
|
566
|
+
var PROVIDER_IDS = ["aws", "gcloud", "k8s", "db", "cloudflare"];
|
|
567
|
+
var isProviderId = (value) => PROVIDER_IDS.includes(value);
|
|
568
|
+
|
|
569
|
+
export {
|
|
570
|
+
TOP_FIXES_DISPLAY_COUNT,
|
|
571
|
+
highlighter,
|
|
572
|
+
setColorEnabled,
|
|
573
|
+
loadConfig,
|
|
574
|
+
defineConfig,
|
|
575
|
+
computeProjectedScore,
|
|
576
|
+
buildJsonReport,
|
|
577
|
+
buildJsonReportError,
|
|
578
|
+
getTopErrorRules,
|
|
579
|
+
sortCategoriesForDisplay,
|
|
580
|
+
listAwsProfiles,
|
|
581
|
+
validateAwsCredentials,
|
|
582
|
+
SsoLoginRequiredError,
|
|
583
|
+
getDefaultRegistry,
|
|
584
|
+
isProviderId
|
|
585
|
+
};
|
|
586
|
+
//# sourceMappingURL=chunk-4PZ65VRA.js.map
|