cloudburn 0.10.1 → 0.11.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 +35 -1
- package/dist/cli.js +55 -17
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -43,6 +43,14 @@ If you want to inspect the starter template without writing a file:
|
|
|
43
43
|
cloudburn config --print-template
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
CloudBurn does not implicitly trust repository config in CI. Pass the exact file explicitly when you want CI to use
|
|
47
|
+
its rule selection, output defaults, or severity gate:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cloudburn scan ./iac --config .cloudburn.yml
|
|
51
|
+
cloudburn discover --config .cloudburn.yml
|
|
52
|
+
```
|
|
53
|
+
|
|
46
54
|
### Scan
|
|
47
55
|
|
|
48
56
|
Use `scan` to check Terraform and CloudFormation before you deploy.
|
|
@@ -51,9 +59,32 @@ Use `scan` to check Terraform and CloudFormation before you deploy.
|
|
|
51
59
|
cloudburn scan ./main.tf
|
|
52
60
|
cloudburn scan ./template.yaml
|
|
53
61
|
cloudburn scan ./iac --exit-code
|
|
62
|
+
cloudburn scan ./iac --fail-on high
|
|
54
63
|
cloudburn --format json scan ./iac
|
|
55
64
|
```
|
|
56
65
|
|
|
66
|
+
`--fail-on high|medium|low` exits with code 1 when an active finding meets or exceeds the selected severity. The same
|
|
67
|
+
threshold can be configured as `iac.fail-on`. Plain `--exit-code` continues to gate on any active finding.
|
|
68
|
+
In CI, configured thresholds apply only when the config is passed with `--config` as shown above.
|
|
69
|
+
|
|
70
|
+
Terraform and CloudFormation YAML support resource-local exceptions. Put one of these comments immediately above or
|
|
71
|
+
inside the resource; text after the directive is retained as an optional reason:
|
|
72
|
+
|
|
73
|
+
```hcl
|
|
74
|
+
# cloudburn-ignore CLDBRN-AWS-EBS-1 migration scheduled
|
|
75
|
+
resource "aws_ebs_volume" "legacy" {
|
|
76
|
+
type = "gp2"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# cloudburn-ignore-all approved temporary exception
|
|
80
|
+
resource "aws_ebs_volume" "temporary" {
|
|
81
|
+
type = "gp2"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Suppressed findings remain available in JSON output under `suppressed`, are counted in table output, and never fail CI
|
|
86
|
+
gates. CloudFormation JSON does not support comments and therefore cannot contain inline suppressions.
|
|
87
|
+
|
|
57
88
|
### Discover
|
|
58
89
|
|
|
59
90
|
Use `discover` to run the same rules against live AWS resources.
|
|
@@ -69,11 +100,14 @@ cloudburn discover --region eu-central-1
|
|
|
69
100
|
cloudburn discover --config .cloudburn.yml --enabled-rules CLDBRN-AWS-EBS-1
|
|
70
101
|
cloudburn discover --enabled-rules CLDBRN-AWS-TAGGING-1
|
|
71
102
|
cloudburn discover --service ec2,s3
|
|
103
|
+
cloudburn discover --fail-on high
|
|
72
104
|
cloudburn --debug discover --region eu-central-1
|
|
73
105
|
cloudburn rules list
|
|
74
|
-
cloudburn rules list --service ec2 --source discovery
|
|
106
|
+
cloudburn rules list --service ec2 --source discovery --severity high
|
|
75
107
|
```
|
|
76
108
|
|
|
109
|
+
The discovery config equivalent is `discovery.fail-on`.
|
|
110
|
+
|
|
77
111
|
The CLI targets one region per run. Multi-region discovery remains available through the SDK and still needs an AWS Resource Explorer aggregator plus an unfiltered default view in the aggregator region.
|
|
78
112
|
`CLDBRN-AWS-TAGGING-1` is opt-in and requires an accessible aggregator; a local-only setup cannot run account-wide tagging discovery.
|
|
79
113
|
Use `--debug` to print SDK and provider execution tracing to `stderr` without changing the normal `stdout` format.
|
package/dist/cli.js
CHANGED
|
@@ -531,13 +531,13 @@ var flattenScanResult = (result) => result.providers.flatMap(
|
|
|
531
531
|
provider: providerGroup.provider,
|
|
532
532
|
ruleId: ruleGroup.ruleId,
|
|
533
533
|
service: ruleGroup.service,
|
|
534
|
+
severity: ruleGroup.severity,
|
|
534
535
|
source: ruleGroup.source,
|
|
535
536
|
message: ruleGroup.message,
|
|
536
537
|
finding
|
|
537
538
|
}))
|
|
538
539
|
)
|
|
539
540
|
);
|
|
540
|
-
var countScanResultFindings = (result) => flattenScanResult(result).length;
|
|
541
541
|
var getScanDiagnostics = (result) => result.diagnostics ?? [];
|
|
542
542
|
|
|
543
543
|
// src/formatters/output.ts
|
|
@@ -548,6 +548,7 @@ var supportedOutputFormats = ["json", "table"];
|
|
|
548
548
|
var scanColumns = [
|
|
549
549
|
{ key: "provider", header: "Provider" },
|
|
550
550
|
{ key: "ruleId", header: "RuleId" },
|
|
551
|
+
{ key: "severity", header: "Severity" },
|
|
551
552
|
{ key: "source", header: "Source" },
|
|
552
553
|
{ key: "service", header: "Service" },
|
|
553
554
|
{ key: "resourceId", header: "ResourceId" },
|
|
@@ -571,6 +572,7 @@ var ruleListColumns = [
|
|
|
571
572
|
{ key: "ruleId", header: "RuleId" },
|
|
572
573
|
{ key: "provider", header: "Provider" },
|
|
573
574
|
{ key: "service", header: "Service" },
|
|
575
|
+
{ key: "severity", header: "Severity" },
|
|
574
576
|
{ key: "supports", header: "Supports" },
|
|
575
577
|
{ key: "name", header: "Name" },
|
|
576
578
|
{ key: "description", header: "Description" }
|
|
@@ -649,20 +651,26 @@ ${regionsTable}`;
|
|
|
649
651
|
case "scan-result": {
|
|
650
652
|
const findingRows = projectFindingRows(response.result);
|
|
651
653
|
const diagnosticRows = projectDiagnosticRows(response.result);
|
|
654
|
+
const suppressedCount = response.result.suppressed?.length ?? 0;
|
|
655
|
+
const withSuppressedCount = (content) => suppressedCount > 0 ? `${content}
|
|
656
|
+
|
|
657
|
+
Suppressed: ${suppressedCount}` : content;
|
|
652
658
|
if (findingRows.length === 0 && diagnosticRows.length === 0) {
|
|
653
|
-
return "No findings.";
|
|
659
|
+
return withSuppressedCount(suppressedCount > 0 ? "No active findings." : "No findings.");
|
|
654
660
|
}
|
|
655
661
|
if (findingRows.length === 0) {
|
|
656
|
-
return `Diagnostics
|
|
657
|
-
${renderAsciiTable(diagnosticRows, diagnosticColumns)}
|
|
662
|
+
return withSuppressedCount(`Diagnostics
|
|
663
|
+
${renderAsciiTable(diagnosticRows, diagnosticColumns)}`);
|
|
658
664
|
}
|
|
659
665
|
if (diagnosticRows.length === 0) {
|
|
660
|
-
return renderAsciiTable(findingRows, scanColumns);
|
|
666
|
+
return withSuppressedCount(renderAsciiTable(findingRows, scanColumns));
|
|
661
667
|
}
|
|
662
|
-
return
|
|
668
|
+
return withSuppressedCount(
|
|
669
|
+
`${renderAsciiTable(findingRows, scanColumns)}
|
|
663
670
|
|
|
664
671
|
Diagnostics
|
|
665
|
-
${renderAsciiTable(diagnosticRows, diagnosticColumns)}
|
|
672
|
+
${renderAsciiTable(diagnosticRows, diagnosticColumns)}`
|
|
673
|
+
);
|
|
666
674
|
}
|
|
667
675
|
case "status":
|
|
668
676
|
return renderAsciiTable(
|
|
@@ -679,7 +687,7 @@ ${renderAsciiTable(diagnosticRows, diagnosticColumns)}`;
|
|
|
679
687
|
);
|
|
680
688
|
}
|
|
681
689
|
};
|
|
682
|
-
var projectFindingRows = (result) => flattenScanResult(result).map(({ finding, message, provider, ruleId, service, source }) => ({
|
|
690
|
+
var projectFindingRows = (result) => flattenScanResult(result).map(({ finding, message, provider, ruleId, service, severity, source }) => ({
|
|
683
691
|
accountId: finding.accountId ?? "",
|
|
684
692
|
message,
|
|
685
693
|
path: finding.location?.path ?? "",
|
|
@@ -688,6 +696,7 @@ var projectFindingRows = (result) => flattenScanResult(result).map(({ finding, m
|
|
|
688
696
|
resourceId: finding.resourceId,
|
|
689
697
|
ruleId,
|
|
690
698
|
service,
|
|
699
|
+
severity,
|
|
691
700
|
source,
|
|
692
701
|
column: finding.location?.column ?? "",
|
|
693
702
|
line: finding.location?.line ?? ""
|
|
@@ -718,6 +727,7 @@ var renderRuleTable = (rules, emptyMessage) => {
|
|
|
718
727
|
provider: rule.provider,
|
|
719
728
|
ruleId: rule.id,
|
|
720
729
|
service: rule.service,
|
|
730
|
+
severity: rule.severity,
|
|
721
731
|
supports: rule.supports
|
|
722
732
|
})),
|
|
723
733
|
ruleListColumns
|
|
@@ -857,6 +867,7 @@ var starterConfig = `# Static IaC scan configuration.
|
|
|
857
867
|
# disabled-rules removes specific rule IDs from the selected set.
|
|
858
868
|
# services restricts scans to rules for the listed services.
|
|
859
869
|
# format sets the default output format when --format is not passed.
|
|
870
|
+
# fail-on gates CI on findings at or above high, medium, or low severity.
|
|
860
871
|
iac:
|
|
861
872
|
enabled-rules:
|
|
862
873
|
- CLDBRN-AWS-EBS-1
|
|
@@ -866,6 +877,7 @@ iac:
|
|
|
866
877
|
- ebs
|
|
867
878
|
- ec2
|
|
868
879
|
format: table
|
|
880
|
+
# fail-on: high
|
|
869
881
|
|
|
870
882
|
# Live AWS discovery configuration.
|
|
871
883
|
# Use the same rule controls here to tune discover runs separately from IaC scans.
|
|
@@ -878,6 +890,7 @@ discovery:
|
|
|
878
890
|
- ebs
|
|
879
891
|
- s3
|
|
880
892
|
format: json
|
|
893
|
+
# fail-on: high
|
|
881
894
|
`;
|
|
882
895
|
var resolveExplicitOutputFormat = (command) => {
|
|
883
896
|
const options = typeof command.optsWithGlobals === "function" ? command.optsWithGlobals() : command.opts();
|
|
@@ -1063,6 +1076,15 @@ var resolveCliDebugLogger = (command) => {
|
|
|
1063
1076
|
};
|
|
1064
1077
|
};
|
|
1065
1078
|
|
|
1079
|
+
// src/policy.ts
|
|
1080
|
+
import { evaluateScanPolicy } from "@cloudburn/sdk";
|
|
1081
|
+
var hasPolicyViolation = (result, options) => {
|
|
1082
|
+
if (options.failOn !== void 0) {
|
|
1083
|
+
return evaluateScanPolicy(result, options.failOn).violated;
|
|
1084
|
+
}
|
|
1085
|
+
return options.exitCode === true ? evaluateScanPolicy(result).violated : result.policy?.violated ?? false;
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1066
1088
|
// src/progress.ts
|
|
1067
1089
|
var resolveCliDiscoveryProgressLogger = (command) => {
|
|
1068
1090
|
if (isDebugEnabled(command) || process.stderr.isTTY !== true) {
|
|
@@ -1078,7 +1100,7 @@ var resolveCliDiscoveryProgressLogger = (command) => {
|
|
|
1078
1100
|
};
|
|
1079
1101
|
|
|
1080
1102
|
// src/commands/config-options.ts
|
|
1081
|
-
import { builtInRuleMetadata } from "@cloudburn/sdk";
|
|
1103
|
+
import { builtInRuleMetadata, SEVERITIES } from "@cloudburn/sdk";
|
|
1082
1104
|
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1083
1105
|
var parseCommaSeparatedList = (value, itemLabel) => {
|
|
1084
1106
|
const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
@@ -1087,6 +1109,13 @@ var parseCommaSeparatedList = (value, itemLabel) => {
|
|
|
1087
1109
|
}
|
|
1088
1110
|
return items;
|
|
1089
1111
|
};
|
|
1112
|
+
var parseSeverity = (value) => {
|
|
1113
|
+
const severity = value.toLowerCase();
|
|
1114
|
+
if (!SEVERITIES.includes(severity)) {
|
|
1115
|
+
throw new InvalidArgumentError2(`Unknown severity "${value}". Allowed severities: ${SEVERITIES.join(", ")}.`);
|
|
1116
|
+
}
|
|
1117
|
+
return severity;
|
|
1118
|
+
};
|
|
1090
1119
|
var parseRuleIdList = (value) => {
|
|
1091
1120
|
return parseCommaSeparatedList(value, "rule ID");
|
|
1092
1121
|
};
|
|
@@ -1216,7 +1245,7 @@ var registerDiscoverCommand = (program) => {
|
|
|
1216
1245
|
"--region <region>",
|
|
1217
1246
|
"AWS region to discover. Defaults to the current AWS region from AWS_REGION when omitted.",
|
|
1218
1247
|
parseAwsRegion
|
|
1219
|
-
).option("--config <path>", "Explicit CloudBurn config file to load").option(
|
|
1248
|
+
).option("--config <path>", "Explicit CloudBurn config file to load (required for config files in CI)").option(
|
|
1220
1249
|
"--enabled-rules <ruleIds>",
|
|
1221
1250
|
"Comma-separated rule IDs to enable. When set, CloudBurn checks only these rules. By default, AWS Core preset rules are enabled.",
|
|
1222
1251
|
parseRuleIdList
|
|
@@ -1228,7 +1257,7 @@ var registerDiscoverCommand = (program) => {
|
|
|
1228
1257
|
"--service <services>",
|
|
1229
1258
|
"Comma-separated services to include in the discovery rule set.",
|
|
1230
1259
|
parseDiscoveryServiceList
|
|
1231
|
-
).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
|
|
1260
|
+
).option("--exit-code", "Exit with code 1 when findings exist").option("--fail-on <severity>", "Exit with code 1 for findings at or above this severity.", parseSeverity).action(async (options, command) => {
|
|
1232
1261
|
await runCommand(async () => {
|
|
1233
1262
|
const debugLogger = resolveCliDebugLogger(command);
|
|
1234
1263
|
const onProgress = resolveCliDiscoveryProgressLogger(command);
|
|
@@ -1252,7 +1281,10 @@ var registerDiscoverCommand = (program) => {
|
|
|
1252
1281
|
const output = renderResponse({ kind: "scan-result", result }, format);
|
|
1253
1282
|
process.stdout.write(`${output}
|
|
1254
1283
|
`);
|
|
1255
|
-
if (
|
|
1284
|
+
if (hasPolicyViolation(result, {
|
|
1285
|
+
exitCode: options.exitCode,
|
|
1286
|
+
failOn: options.failOn
|
|
1287
|
+
})) {
|
|
1256
1288
|
return EXIT_CODE_POLICY_VIOLATION;
|
|
1257
1289
|
}
|
|
1258
1290
|
return EXIT_CODE_OK;
|
|
@@ -1427,12 +1459,15 @@ var filterRules = (options) => {
|
|
|
1427
1459
|
if (options.source !== void 0 && !options.source.some((source) => rule.supports.includes(source))) {
|
|
1428
1460
|
return false;
|
|
1429
1461
|
}
|
|
1462
|
+
if (options.severity !== void 0 && rule.severity !== options.severity) {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1430
1465
|
return true;
|
|
1431
1466
|
});
|
|
1432
1467
|
};
|
|
1433
1468
|
var registerRulesListCommand = (program) => {
|
|
1434
1469
|
const rulesCommand = registerParentCommand(program, "rules", "Inspect built-in CloudBurn rules");
|
|
1435
|
-
rulesCommand.command("list").description("List built-in CloudBurn rules").option("--service <services>", "Comma-separated services to include.", parseRulesListServiceList).option("--source <sources>", "Comma-separated sources to include (`iac`, `discovery`).", parseRulesListSourceList).action(function(options) {
|
|
1470
|
+
rulesCommand.command("list").description("List built-in CloudBurn rules").option("--service <services>", "Comma-separated services to include.", parseRulesListServiceList).option("--severity <severity>", "Severity to include (`high`, `medium`, `low`).", parseSeverity).option("--source <sources>", "Comma-separated sources to include (`iac`, `discovery`).", parseRulesListSourceList).action(function(options) {
|
|
1436
1471
|
const output = renderResponse(
|
|
1437
1472
|
{
|
|
1438
1473
|
kind: "rule-list",
|
|
@@ -1463,7 +1498,7 @@ var toScanConfigOverride = (options) => {
|
|
|
1463
1498
|
};
|
|
1464
1499
|
var registerScanCommand = (program) => {
|
|
1465
1500
|
setCommandExamples(
|
|
1466
|
-
program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--config <path>", "Explicit CloudBurn config file to load").option(
|
|
1501
|
+
program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--config <path>", "Explicit CloudBurn config file to load (required for config files in CI)").option(
|
|
1467
1502
|
"--enabled-rules <ruleIds>",
|
|
1468
1503
|
"Comma-separated rule IDs to enable. When set, CloudBurn checks only these rules. By default, AWS Core preset rules are enabled.",
|
|
1469
1504
|
parseRuleIdList
|
|
@@ -1471,7 +1506,7 @@ var registerScanCommand = (program) => {
|
|
|
1471
1506
|
"--disabled-rules <ruleIds>",
|
|
1472
1507
|
"Comma-separated rule IDs to disable from the default AWS Core preset.",
|
|
1473
1508
|
parseRuleIdList
|
|
1474
|
-
).option("--service <services>", "Comma-separated services to include in the scan rule set.", parseIaCServiceList).option("--exit-code", "Exit with code 1 when findings exist").action(async (path, options, command) => {
|
|
1509
|
+
).option("--service <services>", "Comma-separated services to include in the scan rule set.", parseIaCServiceList).option("--exit-code", "Exit with code 1 when findings exist").option("--fail-on <severity>", "Exit with code 1 for findings at or above this severity.", parseSeverity).action(async (path, options, command) => {
|
|
1475
1510
|
try {
|
|
1476
1511
|
const debugLogger = resolveCliDebugLogger(command);
|
|
1477
1512
|
const scanner = new CloudBurnClient2({ debugLogger });
|
|
@@ -1483,7 +1518,10 @@ var registerScanCommand = (program) => {
|
|
|
1483
1518
|
const output = renderResponse({ kind: "scan-result", result }, format);
|
|
1484
1519
|
process.stdout.write(`${output}
|
|
1485
1520
|
`);
|
|
1486
|
-
if (
|
|
1521
|
+
if (hasPolicyViolation(result, {
|
|
1522
|
+
exitCode: options.exitCode,
|
|
1523
|
+
failOn: options.failOn
|
|
1524
|
+
})) {
|
|
1487
1525
|
process.exitCode = EXIT_CODE_POLICY_VIOLATION;
|
|
1488
1526
|
return;
|
|
1489
1527
|
}
|
|
@@ -1514,7 +1552,7 @@ var isCliEntrypoint = (moduleUrl, argvEntry = process.argv[1]) => {
|
|
|
1514
1552
|
};
|
|
1515
1553
|
var createProgram = () => {
|
|
1516
1554
|
const program = createCliCommand();
|
|
1517
|
-
program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.
|
|
1555
|
+
program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.11.1").option("--debug", "Write execution trace messages to stderr").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
|
|
1518
1556
|
configureCliHelp(program);
|
|
1519
1557
|
program.exitOverride();
|
|
1520
1558
|
registerCompletionCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Cloudburn CLI for cloud cost optimization",
|
|
5
5
|
"homepage": "https://cloudburn.io/docs",
|
|
6
6
|
"bugs": {
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"commander": "^13.1.0",
|
|
23
|
-
"@cloudburn/sdk": "0.
|
|
23
|
+
"@cloudburn/sdk": "0.26.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@biomejs/biome": "^2.5.
|
|
26
|
+
"@biomejs/biome": "^2.5.3",
|
|
27
27
|
"@types/node": "^24",
|
|
28
28
|
"@vitest/coverage-v8": "^4.0.18",
|
|
29
29
|
"tsup": "^8.5.1",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"build": "tsup",
|
|
52
52
|
"typecheck": "tsc --noEmit",
|
|
53
53
|
"test": "vitest run",
|
|
54
|
-
"lint": "biome check .",
|
|
54
|
+
"lint": "biome check --error-on-warnings .",
|
|
55
55
|
"lint:fix": "biome check --write .",
|
|
56
56
|
"clean": "rm -rf dist",
|
|
57
57
|
"dev": "tsup --watch"
|