cloudburn 0.18.7 → 0.18.9

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 (2) hide show
  1. package/dist/cli.js +50 -159
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -7,10 +7,10 @@ import { fileURLToPath } from "url";
7
7
  import { CommanderError } from "commander";
8
8
 
9
9
  // src/completion/engine.ts
10
- var buildCompletionTree = (command) => {
10
+ var createCompletionTree = (command) => {
11
11
  const help = command.createHelp();
12
12
  return {
13
- commands: help.visibleCommands(command).map((childCommand) => buildCompletionTree(childCommand)),
13
+ commands: help.visibleCommands(command).map((childCommand) => createCompletionTree(childCommand)),
14
14
  name: command.name(),
15
15
  options: collectVisibleOptions([...help.visibleOptions(command), ...help.visibleGlobalOptions(command)])
16
16
  };
@@ -44,7 +44,6 @@ var parseOptionToken = (token) => {
44
44
  name: token.slice(0, separatorIndex)
45
45
  };
46
46
  };
47
- var createCompletionTree = (command) => buildCompletionTree(command);
48
47
  var resolveCompletionSuggestions = (root, words) => {
49
48
  const committedWords = words.length === 0 ? [] : words.slice(0, -1);
50
49
  const partialWord = words.at(-1) ?? "";
@@ -478,69 +477,12 @@ var EXIT_CODE_POLICY_VIOLATION = 1;
478
477
  var EXIT_CODE_RUNTIME_ERROR = 2;
479
478
 
480
479
  // src/formatters/error.ts
481
- import { isAwsDiscoveryErrorCode } from "@cloudburn/sdk";
482
- 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(
483
- /([?&](?:access_token|authorization|token|x-amz-security-token|x-amz-signature|signature)=)[^&\s]+/gi,
484
- "$1[redacted]"
485
- ).replace(/(Bearer\s+)[A-Za-z0-9._~+/-]+/gi, "$1[redacted]");
486
- var formatError = (err) => {
487
- const envelope = { error: categorize(err) };
488
- return JSON.stringify(envelope, null, 2);
489
- };
490
- var categorize = (err) => {
491
- if (!(err instanceof Error)) {
492
- return { code: "RUNTIME_ERROR", message: "An unexpected error occurred." };
493
- }
494
- const code = "code" in err && typeof err.code === "string" ? err.code : void 0;
495
- if (err.name === "CredentialsProviderError" || err.name === "ExpiredTokenException" || code === "CredentialsProviderError" || code === "ExpiredTokenException") {
496
- return {
497
- code: "CREDENTIALS_ERROR",
498
- message: "AWS credentials not found or expired. Run 'aws sts get-caller-identity' to verify your session."
499
- };
500
- }
501
- if (err.name.includes("AccessDenied") || code?.includes("AccessDenied") === true) {
502
- return {
503
- code: "ACCESS_DENIED",
504
- message: sanitizeRuntimeErrorMessage(err.message).trim() || "Insufficient AWS permissions. Check your IAM role or policy."
505
- };
506
- }
507
- if (err.code === "ENOENT") {
508
- const path = err.path ?? "unknown";
509
- return { code: "PATH_NOT_FOUND", message: `Path not found: ${path}` };
510
- }
511
- if (code && isAwsDiscoveryErrorCode(code)) {
512
- return {
513
- code,
514
- message: sanitizeRuntimeErrorMessage(err.message).trim() || "AWS Resource Explorer discovery failed."
515
- };
516
- }
517
- const sanitizedMessage = sanitizeRuntimeErrorMessage(err.message).trim();
518
- return {
519
- code: "RUNTIME_ERROR",
520
- message: sanitizedMessage || "An unexpected error occurred."
521
- };
522
- };
480
+ import { categorizeError } from "@cloudburn/sdk";
481
+ var formatError = (err) => JSON.stringify({ error: categorizeError(err) }, null, 2);
523
482
 
524
483
  // src/formatters/output.ts
484
+ import { flattenFindings } from "@cloudburn/sdk";
525
485
  import { InvalidArgumentError } from "commander";
526
-
527
- // src/formatters/shared.ts
528
- var flattenScanResult = (result) => result.providers.flatMap(
529
- (providerGroup) => providerGroup.rules.flatMap(
530
- (ruleGroup) => ruleGroup.findings.map((finding) => ({
531
- provider: providerGroup.provider,
532
- ruleId: ruleGroup.ruleId,
533
- service: ruleGroup.service,
534
- severity: ruleGroup.severity,
535
- source: ruleGroup.source,
536
- message: ruleGroup.message,
537
- finding
538
- }))
539
- )
540
- );
541
- var getScanDiagnostics = (result) => result.diagnostics ?? [];
542
-
543
- // src/formatters/output.ts
544
486
  var DEFAULT_TABLE_WIDTH = 200;
545
487
  var MIN_COLUMN_WIDTH = 4;
546
488
  var PREFERRED_MIN_COLUMN_WIDTH = 8;
@@ -579,8 +521,7 @@ var ruleListColumns = [
579
521
  { key: "name", header: "Name" },
580
522
  { key: "description", header: "Description" }
581
523
  ];
582
- var formatOptionDescription = "Options: table: human-readable terminal output.\njson: machine-readable output for automation and downstream systems.";
583
- var OUTPUT_FORMAT_OPTION_DESCRIPTION = formatOptionDescription;
524
+ var OUTPUT_FORMAT_OPTION_DESCRIPTION = "Options: table: human-readable terminal output.\njson: machine-readable output for automation and downstream systems.";
584
525
  var parseOutputFormat = (value) => {
585
526
  if (supportedOutputFormats.includes(value)) {
586
527
  return value;
@@ -703,7 +644,7 @@ ${renderAsciiTable(diagnosticRows, diagnosticColumns)}`
703
644
  );
704
645
  }
705
646
  };
706
- var projectFindingRows = (result) => flattenScanResult(result).map(({ finding, message, provider, ruleId, service, severity, source }) => ({
647
+ var projectFindingRows = (result) => flattenFindings(result).map(({ finding, message, provider, ruleId, service, severity, source }) => ({
707
648
  accountId: finding.accountId ?? "",
708
649
  actionType: finding.actionType ?? "",
709
650
  message,
@@ -719,7 +660,7 @@ var projectFindingRows = (result) => flattenScanResult(result).map(({ finding, m
719
660
  column: finding.location?.column ?? "",
720
661
  line: finding.location?.line ?? ""
721
662
  }));
722
- var projectDiagnosticRows = (result) => getScanDiagnostics(result).map((diagnostic) => ({
663
+ var projectDiagnosticRows = (result) => (result.diagnostics ?? []).map((diagnostic) => ({
723
664
  message: diagnostic.message,
724
665
  provider: diagnostic.provider,
725
666
  region: diagnostic.region ?? "",
@@ -1077,7 +1018,8 @@ import { homedir } from "os";
1077
1018
  import { join as join2 } from "path";
1078
1019
  import {
1079
1020
  assertSupportedAwsRegion,
1080
- CloudBurnClient
1021
+ CloudBurnClient,
1022
+ resolveScanPolicy
1081
1023
  } from "@cloudburn/sdk";
1082
1024
  import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
1083
1025
 
@@ -1096,15 +1038,6 @@ var resolveCliDebugLogger = (command) => {
1096
1038
  };
1097
1039
  };
1098
1040
 
1099
- // src/policy.ts
1100
- import { evaluateScanPolicy } from "@cloudburn/sdk";
1101
- var hasPolicyViolation = (result, options) => {
1102
- if (options.failOn !== void 0) {
1103
- return evaluateScanPolicy(result, options.failOn).violated;
1104
- }
1105
- return options.exitCode === true ? evaluateScanPolicy(result).violated : result.policy?.violated ?? false;
1106
- };
1107
-
1108
1041
  // src/progress.ts
1109
1042
  var resolveCliDiscoveryProgressLogger = (command) => {
1110
1043
  if (isDebugEnabled(command) || process.stderr.isTTY !== true) {
@@ -1127,7 +1060,7 @@ var resolveCliDiscoveryProgressLogger = (command) => {
1127
1060
  };
1128
1061
 
1129
1062
  // src/commands/config-options.ts
1130
- import { builtInRuleMetadata, SEVERITIES } from "@cloudburn/sdk";
1063
+ import { SEVERITIES, validateServices } from "@cloudburn/sdk";
1131
1064
  import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
1132
1065
  var parseCommaSeparatedList = (value, itemLabel) => {
1133
1066
  const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
@@ -1146,22 +1079,25 @@ var parseSeverity = (value) => {
1146
1079
  var parseRuleIdList = (value) => {
1147
1080
  return parseCommaSeparatedList(value, "rule ID");
1148
1081
  };
1149
- var parseServiceList = (value) => parseCommaSeparatedList(value, "service").map((service) => service.toLowerCase());
1082
+ var parseServiceList = (mode) => (value) => {
1083
+ const services = parseCommaSeparatedList(value, "service");
1084
+ try {
1085
+ return validateServices(services, mode);
1086
+ } catch (err) {
1087
+ throw new InvalidArgumentError2(err.message);
1088
+ }
1089
+ };
1150
1090
  var parseSourceList = (value) => parseCommaSeparatedList(value, "source").map((source) => source.toLowerCase());
1151
- var validateServiceList = (mode, services) => {
1152
- if (services === void 0) {
1091
+ var toModeConfigOverride = (mode, options) => {
1092
+ if (options.enabledRules === void 0 && options.disabledRules === void 0 && options.service === void 0) {
1153
1093
  return void 0;
1154
1094
  }
1155
- const validServices = new Set(
1156
- builtInRuleMetadata.filter((rule) => rule.supports.includes(mode)).map((rule) => rule.service)
1157
- );
1158
- const invalidService = services.find((service) => !validServices.has(service));
1159
- if (invalidService) {
1160
- throw new InvalidArgumentError2(
1161
- `Unknown service "${invalidService}" for ${mode}. Allowed services: ${Array.from(validServices).sort().join(", ")}.`
1162
- );
1163
- }
1164
- return services;
1095
+ const modeConfig = {
1096
+ disabledRules: options.disabledRules,
1097
+ enabledRules: options.enabledRules,
1098
+ services: options.service
1099
+ };
1100
+ return mode === "iac" ? { iac: modeConfig } : { discovery: modeConfig };
1165
1101
  };
1166
1102
 
1167
1103
  // src/commands/discover.ts
@@ -1178,7 +1114,6 @@ var parseCacheMode = (value) => {
1178
1114
  }
1179
1115
  throw new InvalidArgumentError3("Cache mode must be normal, refresh, or off.");
1180
1116
  };
1181
- var parseDiscoveryServiceList = (value) => validateServiceList("discovery", parseServiceList(value)) ?? [];
1182
1117
  var describeInitializationMessage = (result) => {
1183
1118
  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}.`;
1184
1119
  const indexSummaryParts = [];
@@ -1258,18 +1193,6 @@ var resolveNestedRegionOption = (command, subcommandName) => {
1258
1193
  }
1259
1194
  return rawArgs[optionIndex + 1];
1260
1195
  };
1261
- var toDiscoveryConfigOverride = (options) => {
1262
- if (options.enabledRules === void 0 && options.disabledRules === void 0 && options.service === void 0) {
1263
- return void 0;
1264
- }
1265
- return {
1266
- discovery: {
1267
- disabledRules: options.disabledRules,
1268
- enabledRules: options.enabledRules,
1269
- services: options.service
1270
- }
1271
- };
1272
- };
1273
1196
  var runCommand = async (action) => {
1274
1197
  try {
1275
1198
  process.exitCode = await action() ?? EXIT_CODE_OK;
@@ -1299,13 +1222,13 @@ var registerDiscoverCommand = (program) => {
1299
1222
  ).option(
1300
1223
  "--service <services>",
1301
1224
  "Comma-separated services to include in the discovery rule set.",
1302
- parseDiscoveryServiceList
1225
+ parseServiceList("discovery")
1303
1226
  ).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) => {
1304
1227
  await runCommand(async () => {
1305
1228
  const debugLogger = resolveCliDebugLogger(command);
1306
1229
  const onProgress = resolveCliDiscoveryProgressLogger(command);
1307
1230
  const scanner = new CloudBurnClient({ debugLogger });
1308
- const configOverride = toDiscoveryConfigOverride(options);
1231
+ const configOverride = toModeConfigOverride("discovery", options);
1309
1232
  const loadedConfig = await scanner.loadConfig(options.config);
1310
1233
  const discoveryOptions = {
1311
1234
  cache: {
@@ -1332,10 +1255,7 @@ var registerDiscoverCommand = (program) => {
1332
1255
  const output = renderResponse({ kind: "scan-result", result }, format);
1333
1256
  process.stdout.write(`${output}
1334
1257
  `);
1335
- if (hasPolicyViolation(result, {
1336
- exitCode: options.exitCode,
1337
- failOn: options.failOn
1338
- })) {
1258
+ if (resolveScanPolicy(result, options).violated) {
1339
1259
  return EXIT_CODE_POLICY_VIOLATION;
1340
1260
  }
1341
1261
  return EXIT_CODE_OK;
@@ -1489,42 +1409,25 @@ var registerEstimateCommand = (program) => {
1489
1409
  };
1490
1410
 
1491
1411
  // src/commands/rules-list.ts
1492
- import { builtInRuleMetadata as builtInRuleMetadata2 } from "@cloudburn/sdk";
1412
+ import { filterBuiltInRules } from "@cloudburn/sdk";
1493
1413
  import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
1494
1414
  var VALID_SOURCES = ["discovery", "iac"];
1495
- var validateSelectedValues = (values, validValues, label) => {
1496
- const invalidValue = values.find((value) => !validValues.has(value));
1497
- if (invalidValue !== void 0) {
1498
- throw new InvalidArgumentError4(
1499
- `Unknown ${label} "${invalidValue}". Allowed ${label}s: ${Array.from(validValues).sort().join(", ")}.`
1500
- );
1415
+ var parseRulesListSourceList = (value) => {
1416
+ const sources = parseSourceList(value);
1417
+ const invalidSource = sources.find((source) => !VALID_SOURCES.includes(source));
1418
+ if (invalidSource !== void 0) {
1419
+ throw new InvalidArgumentError4(`Unknown source "${invalidSource}". Allowed sources: ${VALID_SOURCES.join(", ")}.`);
1501
1420
  }
1502
- return values;
1503
- };
1504
- var parseRulesListServiceList = (value) => validateSelectedValues(parseServiceList(value), new Set(builtInRuleMetadata2.map((rule) => rule.service)), "service");
1505
- var parseRulesListSourceList = (value) => validateSelectedValues(parseSourceList(value), new Set(VALID_SOURCES), "source");
1506
- var filterRules = (options) => {
1507
- return builtInRuleMetadata2.filter((rule) => {
1508
- if (options.service !== void 0 && !options.service.includes(rule.service)) {
1509
- return false;
1510
- }
1511
- if (options.source !== void 0 && !options.source.some((source) => rule.supports.includes(source))) {
1512
- return false;
1513
- }
1514
- if (options.severity !== void 0 && rule.severity !== options.severity) {
1515
- return false;
1516
- }
1517
- return true;
1518
- });
1421
+ return sources;
1519
1422
  };
1520
1423
  var registerRulesListCommand = (program) => {
1521
1424
  const rulesCommand = registerParentCommand(program, "rules", "Inspect built-in CloudBurn rules");
1522
- 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) {
1425
+ rulesCommand.command("list").description("List built-in CloudBurn rules").option("--service <services>", "Comma-separated services to include.", parseServiceList()).option("--severity <severity>", "Severity to include (`high`, `medium`, `low`).", parseSeverity).option("--source <sources>", "Comma-separated sources to include (`iac`, `discovery`).", parseRulesListSourceList).action(function(options) {
1523
1426
  const output = renderResponse(
1524
1427
  {
1525
1428
  kind: "rule-list",
1526
1429
  emptyMessage: "No built-in rules are available.",
1527
- rules: filterRules(options)
1430
+ rules: filterBuiltInRules({ services: options.service, severity: options.severity, sources: options.source })
1528
1431
  },
1529
1432
  resolveOutputFormat(this, void 0, "table")
1530
1433
  );
@@ -1534,20 +1437,7 @@ var registerRulesListCommand = (program) => {
1534
1437
  };
1535
1438
 
1536
1439
  // src/commands/scan.ts
1537
- import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
1538
- var parseIaCServiceList = (value) => validateServiceList("iac", parseServiceList(value)) ?? [];
1539
- var toScanConfigOverride = (options) => {
1540
- if (options.enabledRules === void 0 && options.disabledRules === void 0 && options.service === void 0) {
1541
- return void 0;
1542
- }
1543
- return {
1544
- iac: {
1545
- disabledRules: options.disabledRules,
1546
- enabledRules: options.enabledRules,
1547
- services: options.service
1548
- }
1549
- };
1550
- };
1440
+ import { CloudBurnClient as CloudBurnClient2, resolveScanPolicy as resolveScanPolicy2 } from "@cloudburn/sdk";
1551
1441
  var registerScanCommand = (program) => {
1552
1442
  setCommandExamples(
1553
1443
  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(
@@ -1558,22 +1448,23 @@ var registerScanCommand = (program) => {
1558
1448
  "--disabled-rules <ruleIds>",
1559
1449
  "Comma-separated rule IDs to disable from the default AWS Core preset.",
1560
1450
  parseRuleIdList
1561
- ).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) => {
1451
+ ).option(
1452
+ "--service <services>",
1453
+ "Comma-separated services to include in the scan rule set.",
1454
+ parseServiceList("iac")
1455
+ ).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) => {
1562
1456
  try {
1563
1457
  const debugLogger = resolveCliDebugLogger(command);
1564
1458
  const scanner = new CloudBurnClient2({ debugLogger });
1565
- const configOverride = toScanConfigOverride(options);
1566
1459
  const loadedConfig = await scanner.loadConfig(options.config);
1567
- const scanPath = path ?? process.cwd();
1568
- 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 });
1460
+ const result = await scanner.scanStatic(path ?? process.cwd(), toModeConfigOverride("iac", options), {
1461
+ configPath: options.config
1462
+ });
1569
1463
  const format = resolveOutputFormat(command, void 0, loadedConfig.iac.format ?? "table");
1570
1464
  const output = renderResponse({ kind: "scan-result", result }, format);
1571
1465
  process.stdout.write(`${output}
1572
1466
  `);
1573
- if (hasPolicyViolation(result, {
1574
- exitCode: options.exitCode,
1575
- failOn: options.failOn
1576
- })) {
1467
+ if (resolveScanPolicy2(result, options).violated) {
1577
1468
  process.exitCode = EXIT_CODE_POLICY_VIOLATION;
1578
1469
  return;
1579
1470
  }
@@ -1604,7 +1495,7 @@ var isCliEntrypoint = (moduleUrl, argvEntry = process.argv[1]) => {
1604
1495
  };
1605
1496
  var createProgram = () => {
1606
1497
  const program = createCliCommand();
1607
- program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.18.7").option("--debug", "Write execution trace messages to stderr").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1498
+ program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.18.9").option("--debug", "Write execution trace messages to stderr").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1608
1499
  configureCliHelp(program);
1609
1500
  program.exitOverride();
1610
1501
  registerCompletionCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.18.7",
3
+ "version": "0.18.9",
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": "^15.0.0",
23
- "@cloudburn/sdk": "0.37.2"
23
+ "@cloudburn/sdk": "0.38.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@biomejs/biome": "^2.5.12",