cloudburn 0.8.5 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +122 -87
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -64,8 +64,10 @@ cloudburn discover
64
64
  cloudburn discover --region eu-central-1
65
65
  cloudburn discover --region all
66
66
  cloudburn discover --config .cloudburn.yml --enabled-rules CLDBRN-AWS-EBS-1
67
+ cloudburn discover --service ec2,s3
67
68
  cloudburn discover list-enabled-regions --format text
68
69
  cloudburn rules list
70
+ cloudburn rules list --service ec2 --source discovery
69
71
  ```
70
72
 
71
73
  `cloudburn discover --region all` needs an AWS Resource Explorer aggregator and an unfiltered default view in the aggregator region.
package/dist/cli.js CHANGED
@@ -543,7 +543,7 @@ var getScanDiagnostics = (result) => result.diagnostics ?? [];
543
543
  var DEFAULT_TABLE_WIDTH = 200;
544
544
  var MIN_COLUMN_WIDTH = 4;
545
545
  var PREFERRED_MIN_COLUMN_WIDTH = 8;
546
- var supportedOutputFormats = ["text", "json", "table"];
546
+ var supportedOutputFormats = ["json", "table"];
547
547
  var scanColumns = [
548
548
  { key: "provider", header: "Provider" },
549
549
  { key: "ruleId", header: "RuleId" },
@@ -557,7 +557,15 @@ var scanColumns = [
557
557
  { key: "column", header: "Column" },
558
558
  { key: "message", header: "Message" }
559
559
  ];
560
- var formatOptionDescription = "Options: table: human-readable terminal output.\ntext: tab-delimited output for grep, sed, and awk.\njson: machine-readable output for automation and downstream systems.";
560
+ var ruleListColumns = [
561
+ { key: "ruleId", header: "RuleId" },
562
+ { key: "provider", header: "Provider" },
563
+ { key: "service", header: "Service" },
564
+ { key: "supports", header: "Supports" },
565
+ { key: "name", header: "Name" },
566
+ { key: "description", header: "Description" }
567
+ ];
568
+ var formatOptionDescription = "Options: table: human-readable terminal output.\njson: machine-readable output for automation and downstream systems.";
561
569
  var OUTPUT_FORMAT_OPTION_DESCRIPTION = formatOptionDescription;
562
570
  var parseOutputFormat = (value) => {
563
571
  if (supportedOutputFormats.includes(value)) {
@@ -576,8 +584,6 @@ var renderResponse = (response, format) => {
576
584
  switch (format) {
577
585
  case "json":
578
586
  return renderJson(response);
579
- case "text":
580
- return renderText(response);
581
587
  case "table":
582
588
  return renderTable(response);
583
589
  }
@@ -600,25 +606,6 @@ var renderJson = (response) => {
600
606
  return JSON.stringify(response.values, null, 2);
601
607
  }
602
608
  };
603
- var renderText = (response) => {
604
- switch (response.kind) {
605
- case "document":
606
- return response.content;
607
- case "discovery-status":
608
- return `${response.summaryText}
609
- ${renderTextRows(response.rows, response.columns, "No discovery status available.")}`;
610
- case "record-list":
611
- return renderTextRows(response.rows, response.columns, response.emptyMessage);
612
- case "rule-list":
613
- return renderRuleList(response.rules, response.emptyMessage);
614
- case "scan-result":
615
- return renderTextRows(projectScanRows(response.result), scanColumns, "No findings.");
616
- case "status":
617
- return response.text;
618
- case "string-list":
619
- return response.values.length === 0 ? response.emptyMessage : response.values.join("\n");
620
- }
621
- };
622
609
  var renderTable = (response) => {
623
610
  switch (response.kind) {
624
611
  case "document":
@@ -648,7 +635,7 @@ ${regionsTable}`;
648
635
  case "record-list":
649
636
  return response.rows.length === 0 ? response.emptyMessage : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
650
637
  case "rule-list":
651
- return renderRuleList(response.rules, response.emptyMessage);
638
+ return renderRuleTable(response.rules, response.emptyMessage);
652
639
  case "scan-result": {
653
640
  const rows = projectScanRows(response.result);
654
641
  return rows.length === 0 ? "No findings." : renderAsciiTable(rows, scanColumns);
@@ -696,39 +683,27 @@ var projectScanRows = (result) => [
696
683
  source: diagnostic.source
697
684
  }))
698
685
  ];
699
- var renderTextRows = (rows, columns, emptyMessage) => {
700
- if (rows.length === 0) {
701
- return emptyMessage;
702
- }
703
- const resolvedColumns = columns ?? inferColumns(rows);
704
- return rows.map((row) => resolvedColumns.map((column) => toTextCell(row[column.key])).join(" ")).join("\n");
705
- };
706
686
  var inferColumns = (rows) => {
707
687
  const keys = Array.from(new Set(rows.flatMap((row) => Object.keys(row)))).sort(
708
688
  (left, right) => left.localeCompare(right)
709
689
  );
710
690
  return keys.map((key) => ({ key, header: key }));
711
691
  };
712
- var renderRuleList = (rules, emptyMessage) => {
692
+ var renderRuleTable = (rules, emptyMessage) => {
713
693
  if (rules.length === 0) {
714
694
  return emptyMessage;
715
695
  }
716
- let currentProvider = "";
717
- let currentService = "";
718
- const lines = [];
719
- for (const rule of rules) {
720
- if (rule.provider !== currentProvider) {
721
- currentProvider = rule.provider;
722
- currentService = "";
723
- lines.push(rule.provider);
724
- }
725
- if (rule.service !== currentService) {
726
- currentService = rule.service;
727
- lines.push(` ${rule.service}`);
728
- }
729
- lines.push(` ${rule.id}: ${rule.description}`);
730
- }
731
- return lines.join("\n");
696
+ return renderAsciiTable(
697
+ rules.map((rule) => ({
698
+ description: rule.description,
699
+ name: rule.name,
700
+ provider: rule.provider,
701
+ ruleId: rule.id,
702
+ service: rule.service,
703
+ supports: rule.supports
704
+ })),
705
+ ruleListColumns
706
+ );
732
707
  };
733
708
  var toTextCell = (value) => {
734
709
  if (value === null || value === void 0) {
@@ -857,21 +832,38 @@ var renderAsciiTable = (rows, columns) => {
857
832
  };
858
833
 
859
834
  // src/commands/config-options.ts
835
+ import { builtInRuleMetadata } from "@cloudburn/sdk";
860
836
  import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
837
+ var parseCommaSeparatedList = (value, itemLabel) => {
838
+ const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
839
+ if (items.length === 0) {
840
+ throw new InvalidArgumentError2(`Provide at least one ${itemLabel}.`);
841
+ }
842
+ return items;
843
+ };
861
844
  var parseRuleIdList = (value) => {
862
- const ruleIds = value.split(",").map((ruleId) => ruleId.trim()).filter((ruleId) => ruleId.length > 0);
863
- if (ruleIds.length === 0) {
864
- throw new InvalidArgumentError2("Provide at least one rule ID.");
845
+ return parseCommaSeparatedList(value, "rule ID");
846
+ };
847
+ var parseServiceList = (value) => parseCommaSeparatedList(value, "service").map((service) => service.toLowerCase());
848
+ var parseSourceList = (value) => parseCommaSeparatedList(value, "source").map((source) => source.toLowerCase());
849
+ var validateServiceList = (mode, services) => {
850
+ if (services === void 0) {
851
+ return void 0;
865
852
  }
866
- return ruleIds;
853
+ const validServices = new Set(
854
+ builtInRuleMetadata.filter((rule) => rule.supports.includes(mode)).map((rule) => rule.service)
855
+ );
856
+ const invalidService = services.find((service) => !validServices.has(service));
857
+ if (invalidService) {
858
+ throw new InvalidArgumentError2(
859
+ `Unknown service "${invalidService}" for ${mode}. Allowed services: ${Array.from(validServices).sort().join(", ")}.`
860
+ );
861
+ }
862
+ return services;
867
863
  };
868
864
 
869
865
  // src/commands/discover.ts
870
- var describeDiscoverySummary = (status) => {
871
- const aggregatorSummary = status.aggregatorRegion ? ` Aggregator region: ${status.aggregatorRegion}.` : "";
872
- const warningSummary = status.warning ? ` ${status.warning}` : "";
873
- return `Coverage: ${status.coverage}. Indexed ${status.indexedRegionCount} of ${status.totalRegionCount} enabled regions.${aggregatorSummary}${warningSummary}`.trim();
874
- };
866
+ var parseDiscoveryServiceList = (value) => validateServiceList("discovery", parseServiceList(value)) ?? [];
875
867
  var describeInitializationMessage = (result) => {
876
868
  const baseMessage = result.indexType === "aggregator" ? result.aggregatorAction === "promoted" ? `Promoted the existing local Resource Explorer index in ${result.aggregatorRegion} to the aggregator.` : result.aggregatorAction === "created" ? `Configured ${result.aggregatorRegion} as the Resource Explorer aggregator.` : result.coverage === "full" ? `Resource Explorer aggregator already exists in ${result.aggregatorRegion}.` : `Resource Explorer aggregator already exists in ${result.aggregatorRegion}, but only ${result.observedStatus.indexedRegionCount} of ${result.observedStatus.totalRegionCount} regions are indexed.` : result.status === "EXISTING" ? `Local Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Local Resource Explorer setup created in ${result.aggregatorRegion}.`;
877
869
  const indexSummaryParts = [];
@@ -945,13 +937,14 @@ var parseDiscoverRegion = (value) => {
945
937
  };
946
938
  var resolveDiscoveryTarget = (region) => region === void 0 ? { mode: "current" } : region === "all" ? { mode: "all" } : { mode: "region", region };
947
939
  var toDiscoveryConfigOverride = (options) => {
948
- if (options.enabledRules === void 0 && options.disabledRules === void 0) {
940
+ if (options.enabledRules === void 0 && options.disabledRules === void 0 && options.service === void 0) {
949
941
  return void 0;
950
942
  }
951
943
  return {
952
944
  discovery: {
953
945
  disabledRules: options.disabledRules,
954
- enabledRules: options.enabledRules
946
+ enabledRules: options.enabledRules,
947
+ services: options.service
955
948
  }
956
949
  };
957
950
  };
@@ -978,14 +971,18 @@ var registerDiscoverCommand = (program) => {
978
971
  "--disabled-rules <ruleIds>",
979
972
  "Comma-separated rule IDs to disable. By default, all rules are enabled; use this to exclude specific rules.",
980
973
  parseRuleIdList
974
+ ).option(
975
+ "--service <services>",
976
+ "Comma-separated services to include in the discovery rule set.",
977
+ parseDiscoveryServiceList
981
978
  ).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
982
979
  await runCommand(async () => {
983
980
  const scanner = new CloudBurnClient();
981
+ const configOverride = toDiscoveryConfigOverride(options);
984
982
  const loadedConfig = await scanner.loadConfig(options.config);
985
983
  const discoveryOptions = {
986
984
  target: resolveDiscoveryTarget(options.region)
987
985
  };
988
- const configOverride = toDiscoveryConfigOverride(options);
989
986
  if (configOverride !== void 0) {
990
987
  discoveryOptions.config = configOverride;
991
988
  }
@@ -1055,8 +1052,7 @@ var registerDiscoverCommand = (program) => {
1055
1052
  { key: "notes", header: "Notes" }
1056
1053
  ],
1057
1054
  rows,
1058
- summary,
1059
- summaryText: describeDiscoverySummary(status)
1055
+ summary
1060
1056
  },
1061
1057
  format
1062
1058
  );
@@ -1102,8 +1098,7 @@ var registerDiscoverCommand = (program) => {
1102
1098
  const output = renderResponse(
1103
1099
  {
1104
1100
  kind: "status",
1105
- data: buildInitializationStatusData(result, message, format),
1106
- text: message
1101
+ data: buildInitializationStatusData(result, message, format)
1107
1102
  },
1108
1103
  format
1109
1104
  );
@@ -1151,8 +1146,7 @@ var registerEstimateCommand = (program) => {
1151
1146
  message: "No server configured. This command is optional and requires a dashboard URL.",
1152
1147
  server: "",
1153
1148
  status: "NOT_CONFIGURED"
1154
- },
1155
- text: "No server configured. This command is optional and requires a dashboard URL."
1149
+ }
1156
1150
  },
1157
1151
  format
1158
1152
  );
@@ -1167,8 +1161,7 @@ var registerEstimateCommand = (program) => {
1167
1161
  message: `Estimate request scaffold ready for server: ${options.server}`,
1168
1162
  server: options.server,
1169
1163
  status: "READY"
1170
- },
1171
- text: `Estimate request scaffold ready for server: ${options.server}`
1164
+ }
1172
1165
  },
1173
1166
  format
1174
1167
  );
@@ -1184,12 +1177,16 @@ var CONFIG_FILENAMES = [".cloudburn.yml", ".cloudburn.yaml"];
1184
1177
  var starterConfig = `# Static IaC scan configuration.
1185
1178
  # enabled-rules restricts scans to only the listed rule IDs.
1186
1179
  # disabled-rules removes specific rule IDs from the active set.
1180
+ # services restricts scans to rules for the listed services.
1187
1181
  # format sets the default output format when --format is not passed.
1188
1182
  iac:
1189
1183
  enabled-rules:
1190
1184
  - CLDBRN-AWS-EBS-1
1191
1185
  disabled-rules:
1192
1186
  - CLDBRN-AWS-EC2-2
1187
+ services:
1188
+ - ebs
1189
+ - ec2
1193
1190
  format: table
1194
1191
 
1195
1192
  # Live AWS discovery configuration.
@@ -1199,16 +1196,29 @@ discovery:
1199
1196
  - CLDBRN-AWS-EBS-1
1200
1197
  disabled-rules:
1201
1198
  - CLDBRN-AWS-S3-1
1199
+ services:
1200
+ - ebs
1201
+ - s3
1202
1202
  format: json
1203
1203
  `;
1204
- var renderStarterConfig = (command) => renderResponse(
1205
- {
1206
- kind: "document",
1207
- content: starterConfig,
1208
- contentType: "application/yaml"
1209
- },
1210
- resolveOutputFormat(command, void 0, "text")
1211
- );
1204
+ var resolveExplicitOutputFormat = (command) => {
1205
+ const options = typeof command.optsWithGlobals === "function" ? command.optsWithGlobals() : command.opts();
1206
+ return options.format;
1207
+ };
1208
+ var renderStarterConfig = (command) => {
1209
+ const explicitFormat = resolveExplicitOutputFormat(command);
1210
+ if (explicitFormat === void 0) {
1211
+ return starterConfig;
1212
+ }
1213
+ return renderResponse(
1214
+ {
1215
+ kind: "document",
1216
+ content: starterConfig,
1217
+ contentType: "application/yaml"
1218
+ },
1219
+ explicitFormat
1220
+ );
1221
+ };
1212
1222
  var fileExists = async (path) => {
1213
1223
  try {
1214
1224
  await access(path);
@@ -1264,8 +1274,7 @@ var registerInitCommand = (program) => {
1264
1274
  data: {
1265
1275
  message: "Created CloudBurn config.",
1266
1276
  path: configPath
1267
- },
1268
- text: `Created ${configPath}.`
1277
+ }
1269
1278
  },
1270
1279
  resolveOutputFormat(this)
1271
1280
  );
@@ -1281,17 +1290,41 @@ var registerInitCommand = (program) => {
1281
1290
  };
1282
1291
 
1283
1292
  // src/commands/rules-list.ts
1284
- import { builtInRuleMetadata } from "@cloudburn/sdk";
1293
+ import { builtInRuleMetadata as builtInRuleMetadata2 } from "@cloudburn/sdk";
1294
+ import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
1295
+ var VALID_SOURCES = ["discovery", "iac"];
1296
+ var validateSelectedValues = (values, validValues, label) => {
1297
+ const invalidValue = values.find((value) => !validValues.has(value));
1298
+ if (invalidValue !== void 0) {
1299
+ throw new InvalidArgumentError4(
1300
+ `Unknown ${label} "${invalidValue}". Allowed ${label}s: ${Array.from(validValues).sort().join(", ")}.`
1301
+ );
1302
+ }
1303
+ return values;
1304
+ };
1305
+ var parseRulesListServiceList = (value) => validateSelectedValues(parseServiceList(value), new Set(builtInRuleMetadata2.map((rule) => rule.service)), "service");
1306
+ var parseRulesListSourceList = (value) => validateSelectedValues(parseSourceList(value), new Set(VALID_SOURCES), "source");
1307
+ var filterRules = (options) => {
1308
+ return builtInRuleMetadata2.filter((rule) => {
1309
+ if (options.service !== void 0 && !options.service.includes(rule.service)) {
1310
+ return false;
1311
+ }
1312
+ if (options.source !== void 0 && !options.source.some((source) => rule.supports.includes(source))) {
1313
+ return false;
1314
+ }
1315
+ return true;
1316
+ });
1317
+ };
1285
1318
  var registerRulesListCommand = (program) => {
1286
1319
  const rulesCommand = registerParentCommand(program, "rules", "Inspect built-in CloudBurn rules");
1287
- rulesCommand.command("list").description("List built-in CloudBurn rules").action(function() {
1320
+ 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) {
1288
1321
  const output = renderResponse(
1289
1322
  {
1290
1323
  kind: "rule-list",
1291
1324
  emptyMessage: "No built-in rules are available.",
1292
- rules: builtInRuleMetadata
1325
+ rules: filterRules(options)
1293
1326
  },
1294
- resolveOutputFormat(this, void 0, "text")
1327
+ resolveOutputFormat(this, void 0, "table")
1295
1328
  );
1296
1329
  process.stdout.write(`${output}
1297
1330
  `);
@@ -1300,14 +1333,16 @@ var registerRulesListCommand = (program) => {
1300
1333
 
1301
1334
  // src/commands/scan.ts
1302
1335
  import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
1336
+ var parseIaCServiceList = (value) => validateServiceList("iac", parseServiceList(value)) ?? [];
1303
1337
  var toScanConfigOverride = (options) => {
1304
- if (options.enabledRules === void 0 && options.disabledRules === void 0) {
1338
+ if (options.enabledRules === void 0 && options.disabledRules === void 0 && options.service === void 0) {
1305
1339
  return void 0;
1306
1340
  }
1307
1341
  return {
1308
1342
  iac: {
1309
1343
  disabledRules: options.disabledRules,
1310
- enabledRules: options.enabledRules
1344
+ enabledRules: options.enabledRules,
1345
+ services: options.service
1311
1346
  }
1312
1347
  };
1313
1348
  };
@@ -1321,11 +1356,11 @@ var registerScanCommand = (program) => {
1321
1356
  "--disabled-rules <ruleIds>",
1322
1357
  "Comma-separated rule IDs to disable. By default, all rules are enabled; use this to exclude specific rules.",
1323
1358
  parseRuleIdList
1324
- ).option("--exit-code", "Exit with code 1 when findings exist").action(async (path, options, command) => {
1359
+ ).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) => {
1325
1360
  try {
1326
1361
  const scanner = new CloudBurnClient2();
1327
- const loadedConfig = await scanner.loadConfig(options.config);
1328
1362
  const configOverride = toScanConfigOverride(options);
1363
+ const loadedConfig = await scanner.loadConfig(options.config);
1329
1364
  const scanPath = path ?? process.cwd();
1330
1365
  const result = configOverride === void 0 && options.config === void 0 ? await scanner.scanStatic(scanPath) : options.config === void 0 ? await scanner.scanStatic(scanPath, configOverride) : await scanner.scanStatic(scanPath, configOverride, { configPath: options.config });
1331
1366
  const format = resolveOutputFormat(command, void 0, loadedConfig.iac.format ?? "table");
@@ -1363,7 +1398,7 @@ var isCliEntrypoint = (moduleUrl, argvEntry = process.argv[1]) => {
1363
1398
  };
1364
1399
  var createProgram = () => {
1365
1400
  const program = createCliCommand();
1366
- program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.8.5").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1401
+ program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.9.0").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1367
1402
  configureCliHelp(program);
1368
1403
  registerCompletionCommand(program);
1369
1404
  registerDiscoverCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "description": "Cloudburn CLI for cloud cost optimization",
5
5
  "homepage": "https://cloudburn.io/docs",
6
6
  "bugs": {
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "commander": "^13.1.0",
23
- "@cloudburn/sdk": "0.13.3"
23
+ "@cloudburn/sdk": "0.15.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@biomejs/biome": "^2.4.6",