cloudburn 0.4.1 → 0.5.1
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 +2 -1
- package/dist/cli.js +177 -56
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,7 +21,8 @@ directory path you pass to `cloudburn scan`.
|
|
|
21
21
|
cloudburn scan ./main.tf
|
|
22
22
|
cloudburn scan ./template.yaml
|
|
23
23
|
cloudburn scan ./iac
|
|
24
|
-
cloudburn
|
|
24
|
+
cloudburn discover
|
|
25
|
+
cloudburn discover --region all
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
`cloudburn scan --format json` emits the lean canonical grouped result:
|
package/dist/cli.js
CHANGED
|
@@ -4,55 +4,11 @@
|
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
|
-
// src/commands/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
process.stdout.write(`Estimate request scaffold ready for server: ${options.server}
|
|
15
|
-
`);
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// src/commands/init.ts
|
|
20
|
-
var starterConfig = `version: 1
|
|
21
|
-
profile: dev
|
|
22
|
-
|
|
23
|
-
profiles:
|
|
24
|
-
dev:
|
|
25
|
-
ec2-allowed-instance-types:
|
|
26
|
-
allow: [t3.micro, t3.small, t3.medium]
|
|
27
|
-
|
|
28
|
-
rules:
|
|
29
|
-
ec2-allowed-instance-types:
|
|
30
|
-
severity: error
|
|
31
|
-
|
|
32
|
-
live:
|
|
33
|
-
tags:
|
|
34
|
-
Project: myapp
|
|
35
|
-
regions: [us-east-1]
|
|
36
|
-
`;
|
|
37
|
-
var registerInitCommand = (program) => {
|
|
38
|
-
program.command("init").description("Print a starter .cloudburn.yml configuration").action(() => {
|
|
39
|
-
process.stdout.write(`${starterConfig}
|
|
40
|
-
`);
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// src/commands/rules-list.ts
|
|
45
|
-
import { awsCorePreset } from "@cloudburn/sdk";
|
|
46
|
-
var registerRulesListCommand = (program) => {
|
|
47
|
-
const rulesCommand = program.command("rules").description("Inspect built-in and custom rules");
|
|
48
|
-
rulesCommand.command("list").description("List built-in CloudBurn rule IDs").action(() => {
|
|
49
|
-
process.stdout.write(`${awsCorePreset.ruleIds.join("\n")}
|
|
50
|
-
`);
|
|
51
|
-
});
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// src/commands/scan.ts
|
|
55
|
-
import { CloudBurnScanner } from "@cloudburn/sdk";
|
|
7
|
+
// src/commands/discover.ts
|
|
8
|
+
import {
|
|
9
|
+
assertValidAwsRegion,
|
|
10
|
+
CloudBurnClient
|
|
11
|
+
} from "@cloudburn/sdk";
|
|
56
12
|
import { InvalidArgumentError } from "commander";
|
|
57
13
|
|
|
58
14
|
// src/exit-codes.ts
|
|
@@ -61,6 +17,11 @@ var EXIT_CODE_POLICY_VIOLATION = 1;
|
|
|
61
17
|
var EXIT_CODE_RUNTIME_ERROR = 2;
|
|
62
18
|
|
|
63
19
|
// src/formatters/error.ts
|
|
20
|
+
import { isAwsDiscoveryErrorCode } from "@cloudburn/sdk";
|
|
21
|
+
var sanitizeRuntimeErrorMessage = (message) => message.replace(/169\.254\.169\.254/g, "[redacted-host]").replace(/fd00:ec2::254/gi, "[redacted-host]").replace(/(https?:\/\/)([^/\s:@]+):([^/\s@]+)@/gi, "$1[redacted-auth]@").replace(
|
|
22
|
+
/([?&](?:access_token|authorization|token|x-amz-security-token|x-amz-signature|signature)=)[^&\s]+/gi,
|
|
23
|
+
"$1[redacted]"
|
|
24
|
+
).replace(/(Bearer\s+)[A-Za-z0-9._~+/-]+/gi, "$1[redacted]");
|
|
64
25
|
var formatError = (err) => {
|
|
65
26
|
const envelope = { error: categorize(err) };
|
|
66
27
|
return JSON.stringify(envelope, null, 2);
|
|
@@ -85,7 +46,17 @@ var categorize = (err) => {
|
|
|
85
46
|
const path = err.path ?? "unknown";
|
|
86
47
|
return { code: "PATH_NOT_FOUND", message: `Path not found: ${path}` };
|
|
87
48
|
}
|
|
88
|
-
|
|
49
|
+
if ("code" in err && typeof err.code === "string" && isAwsDiscoveryErrorCode(err.code)) {
|
|
50
|
+
return {
|
|
51
|
+
code: err.code,
|
|
52
|
+
message: sanitizeRuntimeErrorMessage(err.message).trim() || "AWS Resource Explorer discovery failed."
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const sanitizedMessage = sanitizeRuntimeErrorMessage(err.message).trim();
|
|
56
|
+
return {
|
|
57
|
+
code: "RUNTIME_ERROR",
|
|
58
|
+
message: sanitizedMessage || "An unexpected error occurred."
|
|
59
|
+
};
|
|
89
60
|
};
|
|
90
61
|
|
|
91
62
|
// src/formatters/json.ts
|
|
@@ -163,13 +134,163 @@ var formatTable = (result) => {
|
|
|
163
134
|
}).join("\n");
|
|
164
135
|
};
|
|
165
136
|
|
|
137
|
+
// src/commands/discover.ts
|
|
138
|
+
var supportedDiscoverFormats = ["table", "json", "sarif"];
|
|
139
|
+
var parseDiscoverFormat = (value) => {
|
|
140
|
+
if (supportedDiscoverFormats.includes(value)) {
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
throw new InvalidArgumentError(`Invalid format "${value}". Allowed formats: ${supportedDiscoverFormats.join(", ")}.`);
|
|
144
|
+
};
|
|
145
|
+
var parseDiscoverListFormat = (value) => {
|
|
146
|
+
if (value === "table" || value === "json") {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
throw new InvalidArgumentError("Invalid format. Allowed formats: table, json.");
|
|
150
|
+
};
|
|
151
|
+
var parseAwsRegion = (value) => {
|
|
152
|
+
try {
|
|
153
|
+
return assertValidAwsRegion(value);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
throw new InvalidArgumentError(err instanceof Error ? err.message : "Invalid AWS region.");
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var parseDiscoverRegion = (value) => {
|
|
159
|
+
if (value === "all") {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
return parseAwsRegion(value);
|
|
163
|
+
};
|
|
164
|
+
var resolveDiscoveryTarget = (region) => region === void 0 ? { mode: "current" } : region === "all" ? { mode: "all" } : { mode: "region", region };
|
|
165
|
+
var scanFormatters = {
|
|
166
|
+
json: formatJson,
|
|
167
|
+
sarif: formatSarif,
|
|
168
|
+
table: formatTable
|
|
169
|
+
};
|
|
170
|
+
var formatValue = (value) => JSON.stringify(value, null, 2);
|
|
171
|
+
var formatEnabledRegions = (regions) => regions.length === 0 ? "No Resource Explorer indexes are enabled." : regions.map(({ region, type }) => `${region} ${type}`).join("\n");
|
|
172
|
+
var formatSupportedResourceTypes = (resourceTypes) => resourceTypes.length === 0 ? "No supported resource types were returned." : resourceTypes.map(({ resourceType, service }) => `${resourceType} ${service ?? "unknown"}`).join("\n");
|
|
173
|
+
var formatInitializationResult = (result) => result.status === "EXISTING" ? `Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Resource Explorer setup created in ${result.aggregatorRegion}.`;
|
|
174
|
+
var resolveListFormat = (parentCommand, options) => parentCommand.opts().format === "json" ? "json" : options.format ?? "table";
|
|
175
|
+
var runCommand = async (action) => {
|
|
176
|
+
try {
|
|
177
|
+
process.exitCode = await action() ?? EXIT_CODE_OK;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
process.stderr.write(`${formatError(err)}
|
|
180
|
+
`);
|
|
181
|
+
process.exitCode = EXIT_CODE_RUNTIME_ERROR;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var registerDiscoverCommand = (program) => {
|
|
185
|
+
const discoverCommand = program.command("discover").description("Run a live AWS discovery").enablePositionalOptions().option(
|
|
186
|
+
"--region <region>",
|
|
187
|
+
'Discovery region to use. Pass "all" to require an aggregator index.',
|
|
188
|
+
parseDiscoverRegion
|
|
189
|
+
).option("--format <format>", "Output format: table|json|sarif", parseDiscoverFormat, "table").option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
|
|
190
|
+
"after",
|
|
191
|
+
`
|
|
192
|
+
Examples:
|
|
193
|
+
cloudburn discover
|
|
194
|
+
cloudburn discover --region eu-central-1
|
|
195
|
+
cloudburn discover --region all
|
|
196
|
+
cloudburn discover list-enabled-regions
|
|
197
|
+
cloudburn discover init
|
|
198
|
+
`
|
|
199
|
+
).action(async (options) => {
|
|
200
|
+
await runCommand(async () => {
|
|
201
|
+
const scanner = new CloudBurnClient();
|
|
202
|
+
const result = await scanner.discover({ target: resolveDiscoveryTarget(options.region) });
|
|
203
|
+
const output = scanFormatters[options.format ?? "table"](result);
|
|
204
|
+
process.stdout.write(`${output}
|
|
205
|
+
`);
|
|
206
|
+
if (options.exitCode && countScanResultFindings(result) > 0) {
|
|
207
|
+
return EXIT_CODE_POLICY_VIOLATION;
|
|
208
|
+
}
|
|
209
|
+
return EXIT_CODE_OK;
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").option("--format <format>", "Output format: table|json", parseDiscoverListFormat, "table").action(async (options) => {
|
|
213
|
+
await runCommand(async () => {
|
|
214
|
+
const scanner = new CloudBurnClient();
|
|
215
|
+
const regions = await scanner.listEnabledDiscoveryRegions();
|
|
216
|
+
const format = resolveListFormat(discoverCommand, options);
|
|
217
|
+
const output = format === "json" ? formatValue(regions) : formatEnabledRegions(regions);
|
|
218
|
+
process.stdout.write(`${output}
|
|
219
|
+
`);
|
|
220
|
+
return EXIT_CODE_OK;
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option("--region <region>", "Aggregator region to create or reuse during setup", parseAwsRegion).action(async (options) => {
|
|
224
|
+
await runCommand(async () => {
|
|
225
|
+
const scanner = new CloudBurnClient();
|
|
226
|
+
const parentRegion = discoverCommand.opts().region;
|
|
227
|
+
const region = options.region ?? (parentRegion === "all" ? void 0 : parentRegion);
|
|
228
|
+
const result = await scanner.initializeDiscovery({ region });
|
|
229
|
+
process.stdout.write(`${formatInitializationResult(result)}
|
|
230
|
+
`);
|
|
231
|
+
return EXIT_CODE_OK;
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
discoverCommand.command("supported-resource-types").description("List Resource Explorer supported AWS resource types").option("--format <format>", "Output format: table|json", parseDiscoverListFormat, "table").action(async (options) => {
|
|
235
|
+
await runCommand(async () => {
|
|
236
|
+
const scanner = new CloudBurnClient();
|
|
237
|
+
const resourceTypes = await scanner.listSupportedDiscoveryResourceTypes();
|
|
238
|
+
const format = resolveListFormat(discoverCommand, options);
|
|
239
|
+
const output = format === "json" ? formatValue(resourceTypes) : formatSupportedResourceTypes(resourceTypes);
|
|
240
|
+
process.stdout.write(`${output}
|
|
241
|
+
`);
|
|
242
|
+
return EXIT_CODE_OK;
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/commands/estimate.ts
|
|
248
|
+
var registerEstimateCommand = (program) => {
|
|
249
|
+
program.command("estimate").description("Request optional pricing estimates from a self-hosted dashboard").option("--server <url>", "Dashboard API base URL").action((options) => {
|
|
250
|
+
if (!options.server) {
|
|
251
|
+
process.stdout.write("No server configured. This command is optional and requires a dashboard URL.\n");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
process.stdout.write(`Estimate request scaffold ready for server: ${options.server}
|
|
255
|
+
`);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/commands/init.ts
|
|
260
|
+
var starterConfig = `version: 1
|
|
261
|
+
profile: dev
|
|
262
|
+
|
|
263
|
+
# Profiles are parsed but not applied yet, so configure the active rules block directly for now.
|
|
264
|
+
rules:
|
|
265
|
+
ec2-instance-type-preferred:
|
|
266
|
+
severity: error
|
|
267
|
+
`;
|
|
268
|
+
var registerInitCommand = (program) => {
|
|
269
|
+
program.command("init").description("Print a starter .cloudburn.yml configuration").action(() => {
|
|
270
|
+
process.stdout.write(`${starterConfig}
|
|
271
|
+
`);
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/commands/rules-list.ts
|
|
276
|
+
import { awsCorePreset } from "@cloudburn/sdk";
|
|
277
|
+
var registerRulesListCommand = (program) => {
|
|
278
|
+
const rulesCommand = program.command("rules").description("Inspect built-in and custom rules");
|
|
279
|
+
rulesCommand.command("list").description("List built-in CloudBurn rule IDs").action(() => {
|
|
280
|
+
process.stdout.write(`${awsCorePreset.ruleIds.join("\n")}
|
|
281
|
+
`);
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
166
285
|
// src/commands/scan.ts
|
|
286
|
+
import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
|
|
287
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
167
288
|
var supportedScanFormats = ["table", "json", "sarif"];
|
|
168
289
|
var parseScanFormat = (value) => {
|
|
169
290
|
if (supportedScanFormats.includes(value)) {
|
|
170
291
|
return value;
|
|
171
292
|
}
|
|
172
|
-
throw new
|
|
293
|
+
throw new InvalidArgumentError2(`Invalid format "${value}". Allowed formats: ${supportedScanFormats.join(", ")}.`);
|
|
173
294
|
};
|
|
174
295
|
var formatters = {
|
|
175
296
|
json: formatJson,
|
|
@@ -177,19 +298,18 @@ var formatters = {
|
|
|
177
298
|
table: formatTable
|
|
178
299
|
};
|
|
179
300
|
var registerScanCommand = (program) => {
|
|
180
|
-
program.command("scan").description("Run an autodetected static IaC scan
|
|
301
|
+
program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--format <format>", "Output format: table|json|sarif", parseScanFormat, "table").option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
|
|
181
302
|
"after",
|
|
182
303
|
`
|
|
183
304
|
Examples:
|
|
184
305
|
cloudburn scan ./main.tf
|
|
185
306
|
cloudburn scan ./template.yaml
|
|
186
307
|
cloudburn scan ./iac
|
|
187
|
-
cloudburn scan --live
|
|
188
308
|
`
|
|
189
309
|
).action(async (path, options) => {
|
|
190
310
|
try {
|
|
191
|
-
const scanner = new
|
|
192
|
-
const result =
|
|
311
|
+
const scanner = new CloudBurnClient2();
|
|
312
|
+
const result = await scanner.scanStatic(path ?? process.cwd());
|
|
193
313
|
const format = formatters[options.format ?? "table"];
|
|
194
314
|
const output = format(result);
|
|
195
315
|
process.stdout.write(`${output}
|
|
@@ -210,7 +330,8 @@ Examples:
|
|
|
210
330
|
// src/cli.ts
|
|
211
331
|
var createProgram = () => {
|
|
212
332
|
const program = new Command();
|
|
213
|
-
program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.
|
|
333
|
+
program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.5.1");
|
|
334
|
+
registerDiscoverCommand(program);
|
|
214
335
|
registerScanCommand(program);
|
|
215
336
|
registerInitCommand(program);
|
|
216
337
|
registerRulesListCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Cloudburn CLI for cloud cost optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"commander": "^13.1.0",
|
|
14
|
-
"@cloudburn/sdk": "0.
|
|
14
|
+
"@cloudburn/sdk": "0.8.1"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@biomejs/biome": "^2.4.6",
|