cloudburn 0.4.1 → 0.5.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 +2 -1
- package/dist/cli.js +181 -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,167 @@ 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:
|
|
264
|
+
dev:
|
|
265
|
+
ec2-allowed-instance-types:
|
|
266
|
+
allow: [t3.micro, t3.small, t3.medium]
|
|
267
|
+
|
|
268
|
+
rules:
|
|
269
|
+
ec2-allowed-instance-types:
|
|
270
|
+
severity: error
|
|
271
|
+
`;
|
|
272
|
+
var registerInitCommand = (program) => {
|
|
273
|
+
program.command("init").description("Print a starter .cloudburn.yml configuration").action(() => {
|
|
274
|
+
process.stdout.write(`${starterConfig}
|
|
275
|
+
`);
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// src/commands/rules-list.ts
|
|
280
|
+
import { awsCorePreset } from "@cloudburn/sdk";
|
|
281
|
+
var registerRulesListCommand = (program) => {
|
|
282
|
+
const rulesCommand = program.command("rules").description("Inspect built-in and custom rules");
|
|
283
|
+
rulesCommand.command("list").description("List built-in CloudBurn rule IDs").action(() => {
|
|
284
|
+
process.stdout.write(`${awsCorePreset.ruleIds.join("\n")}
|
|
285
|
+
`);
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
166
289
|
// src/commands/scan.ts
|
|
290
|
+
import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
|
|
291
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
167
292
|
var supportedScanFormats = ["table", "json", "sarif"];
|
|
168
293
|
var parseScanFormat = (value) => {
|
|
169
294
|
if (supportedScanFormats.includes(value)) {
|
|
170
295
|
return value;
|
|
171
296
|
}
|
|
172
|
-
throw new
|
|
297
|
+
throw new InvalidArgumentError2(`Invalid format "${value}". Allowed formats: ${supportedScanFormats.join(", ")}.`);
|
|
173
298
|
};
|
|
174
299
|
var formatters = {
|
|
175
300
|
json: formatJson,
|
|
@@ -177,19 +302,18 @@ var formatters = {
|
|
|
177
302
|
table: formatTable
|
|
178
303
|
};
|
|
179
304
|
var registerScanCommand = (program) => {
|
|
180
|
-
program.command("scan").description("Run an autodetected static IaC scan
|
|
305
|
+
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
306
|
"after",
|
|
182
307
|
`
|
|
183
308
|
Examples:
|
|
184
309
|
cloudburn scan ./main.tf
|
|
185
310
|
cloudburn scan ./template.yaml
|
|
186
311
|
cloudburn scan ./iac
|
|
187
|
-
cloudburn scan --live
|
|
188
312
|
`
|
|
189
313
|
).action(async (path, options) => {
|
|
190
314
|
try {
|
|
191
|
-
const scanner = new
|
|
192
|
-
const result =
|
|
315
|
+
const scanner = new CloudBurnClient2();
|
|
316
|
+
const result = await scanner.scanStatic(path ?? process.cwd());
|
|
193
317
|
const format = formatters[options.format ?? "table"];
|
|
194
318
|
const output = format(result);
|
|
195
319
|
process.stdout.write(`${output}
|
|
@@ -210,7 +334,8 @@ Examples:
|
|
|
210
334
|
// src/cli.ts
|
|
211
335
|
var createProgram = () => {
|
|
212
336
|
const program = new Command();
|
|
213
|
-
program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.
|
|
337
|
+
program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.5.0");
|
|
338
|
+
registerDiscoverCommand(program);
|
|
214
339
|
registerScanCommand(program);
|
|
215
340
|
registerInitCommand(program);
|
|
216
341
|
registerRulesListCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@biomejs/biome": "^2.4.6",
|