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.
Files changed (3) hide show
  1. package/README.md +35 -1
  2. package/dist/cli.js +55 -17
  3. 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 `${renderAsciiTable(findingRows, scanColumns)}
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 (options.exitCode && countScanResultFindings(result) > 0) {
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 (options.exitCode && countScanResultFindings(result) > 0) {
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.10.1").option("--debug", "Write execution trace messages to stderr").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
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.10.1",
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.24.1"
23
+ "@cloudburn/sdk": "0.26.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@biomejs/biome": "^2.5.4",
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"