cloudburn 0.8.0 → 0.8.3
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 -2
- package/dist/cli.js +323 -45
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -153,6 +153,13 @@ var buildCommandPath = (command) => {
|
|
|
153
153
|
}
|
|
154
154
|
return names.join(" ");
|
|
155
155
|
};
|
|
156
|
+
var getRootCommand = (command) => {
|
|
157
|
+
let current = command;
|
|
158
|
+
while (current.parent !== null) {
|
|
159
|
+
current = current.parent;
|
|
160
|
+
}
|
|
161
|
+
return current;
|
|
162
|
+
};
|
|
156
163
|
var visibleCommands = (command) => command.commands.filter((subcommand) => !subcommand._hidden);
|
|
157
164
|
var getCommandExamples = (command) => {
|
|
158
165
|
const { _cloudburnExamples } = command;
|
|
@@ -221,8 +228,9 @@ var formatCloudBurnHelp = (command, helper) => {
|
|
|
221
228
|
const cloudBurnHelp = helper;
|
|
222
229
|
const helpWidth = helper.helpWidth ?? 80;
|
|
223
230
|
const scenario = getHelpScenario(command);
|
|
224
|
-
const
|
|
225
|
-
const
|
|
231
|
+
const rootCommand = getRootCommand(command);
|
|
232
|
+
const localOptions = helper.visibleOptions(command).filter((option) => command.options.includes(option));
|
|
233
|
+
const globalOptions = helper.visibleGlobalOptions(command).filter((option) => rootCommand.options.includes(option));
|
|
226
234
|
const commands = helper.visibleCommands(command);
|
|
227
235
|
const examples = getCommandExamples(command);
|
|
228
236
|
const usageGuidance = getCommandUsageGuidance(command);
|
|
@@ -386,7 +394,7 @@ var setCommandUsageGuidance = (command, guidance) => {
|
|
|
386
394
|
};
|
|
387
395
|
|
|
388
396
|
// src/commands/completion.ts
|
|
389
|
-
var
|
|
397
|
+
var getRootCommand2 = (command) => {
|
|
390
398
|
let currentCommand = command;
|
|
391
399
|
while (currentCommand.parent !== null) {
|
|
392
400
|
currentCommand = currentCommand.parent;
|
|
@@ -441,14 +449,14 @@ var registerCompletionCommand = (program) => {
|
|
|
441
449
|
for (const shell of ["bash", "fish", "zsh"]) {
|
|
442
450
|
setCommandUsageGuidance(
|
|
443
451
|
completionCommand.command(shell).description(`Generate the autocompletion script for the ${shell} shell.`).option("--no-descriptions", "disable completion descriptions").action(function() {
|
|
444
|
-
const output = generateCompletionScript(shell,
|
|
452
|
+
const output = generateCompletionScript(shell, getRootCommand2(this));
|
|
445
453
|
process.stdout.write(output);
|
|
446
454
|
}),
|
|
447
455
|
getCompletionHelpText(shell)
|
|
448
456
|
);
|
|
449
457
|
}
|
|
450
458
|
program.command("__complete", { hidden: true }).argument("[words...]").allowUnknownOption().action(function(words) {
|
|
451
|
-
const suggestions = resolveCompletionSuggestions(createCompletionTree(
|
|
459
|
+
const suggestions = resolveCompletionSuggestions(createCompletionTree(getRootCommand2(this)), words ?? []);
|
|
452
460
|
if (suggestions.length === 0) {
|
|
453
461
|
return;
|
|
454
462
|
}
|
|
@@ -480,25 +488,26 @@ var categorize = (err) => {
|
|
|
480
488
|
if (!(err instanceof Error)) {
|
|
481
489
|
return { code: "RUNTIME_ERROR", message: "An unexpected error occurred." };
|
|
482
490
|
}
|
|
483
|
-
|
|
491
|
+
const code = "code" in err && typeof err.code === "string" ? err.code : void 0;
|
|
492
|
+
if (err.name === "CredentialsProviderError" || err.name === "ExpiredTokenException" || code === "CredentialsProviderError" || code === "ExpiredTokenException") {
|
|
484
493
|
return {
|
|
485
494
|
code: "CREDENTIALS_ERROR",
|
|
486
495
|
message: "AWS credentials not found or expired. Run 'aws sts get-caller-identity' to verify your session."
|
|
487
496
|
};
|
|
488
497
|
}
|
|
489
|
-
if (err.name.includes("AccessDenied")) {
|
|
498
|
+
if (err.name.includes("AccessDenied") || code?.includes("AccessDenied") === true) {
|
|
490
499
|
return {
|
|
491
500
|
code: "ACCESS_DENIED",
|
|
492
|
-
message: "Insufficient AWS permissions. Check your IAM role or policy."
|
|
501
|
+
message: sanitizeRuntimeErrorMessage(err.message).trim() || "Insufficient AWS permissions. Check your IAM role or policy."
|
|
493
502
|
};
|
|
494
503
|
}
|
|
495
504
|
if (err.code === "ENOENT") {
|
|
496
505
|
const path = err.path ?? "unknown";
|
|
497
506
|
return { code: "PATH_NOT_FOUND", message: `Path not found: ${path}` };
|
|
498
507
|
}
|
|
499
|
-
if (
|
|
508
|
+
if (code && isAwsDiscoveryErrorCode(code)) {
|
|
500
509
|
return {
|
|
501
|
-
code
|
|
510
|
+
code,
|
|
502
511
|
message: sanitizeRuntimeErrorMessage(err.message).trim() || "AWS Resource Explorer discovery failed."
|
|
503
512
|
};
|
|
504
513
|
}
|
|
@@ -526,8 +535,12 @@ var flattenScanResult = (result) => result.providers.flatMap(
|
|
|
526
535
|
)
|
|
527
536
|
);
|
|
528
537
|
var countScanResultFindings = (result) => flattenScanResult(result).length;
|
|
538
|
+
var getScanDiagnostics = (result) => result.diagnostics ?? [];
|
|
529
539
|
|
|
530
540
|
// src/formatters/output.ts
|
|
541
|
+
var DEFAULT_TABLE_WIDTH = 200;
|
|
542
|
+
var MIN_COLUMN_WIDTH = 4;
|
|
543
|
+
var PREFERRED_MIN_COLUMN_WIDTH = 8;
|
|
531
544
|
var supportedOutputFormats = ["text", "json", "table"];
|
|
532
545
|
var scanColumns = [
|
|
533
546
|
{ key: "provider", header: "Provider" },
|
|
@@ -538,8 +551,8 @@ var scanColumns = [
|
|
|
538
551
|
{ key: "accountId", header: "AccountId" },
|
|
539
552
|
{ key: "region", header: "Region" },
|
|
540
553
|
{ key: "path", header: "Path" },
|
|
541
|
-
{ key: "
|
|
542
|
-
{ key: "
|
|
554
|
+
{ key: "line", header: "Line" },
|
|
555
|
+
{ key: "column", header: "Column" },
|
|
543
556
|
{ key: "message", header: "Message" }
|
|
544
557
|
];
|
|
545
558
|
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.";
|
|
@@ -571,6 +584,8 @@ var renderJson = (response) => {
|
|
|
571
584
|
switch (response.kind) {
|
|
572
585
|
case "document":
|
|
573
586
|
return JSON.stringify({ content: response.content, contentType: response.contentType }, null, 2);
|
|
587
|
+
case "discovery-status":
|
|
588
|
+
return JSON.stringify({ summary: response.summary, regions: response.rows }, null, 2);
|
|
574
589
|
case "record-list":
|
|
575
590
|
return JSON.stringify(response.rows, null, 2);
|
|
576
591
|
case "rule-list":
|
|
@@ -587,6 +602,9 @@ var renderText = (response) => {
|
|
|
587
602
|
switch (response.kind) {
|
|
588
603
|
case "document":
|
|
589
604
|
return response.content;
|
|
605
|
+
case "discovery-status":
|
|
606
|
+
return `${response.summaryText}
|
|
607
|
+
${renderTextRows(response.rows, response.columns, "No discovery status available.")}`;
|
|
590
608
|
case "record-list":
|
|
591
609
|
return renderTextRows(response.rows, response.columns, response.emptyMessage);
|
|
592
610
|
case "rule-list":
|
|
@@ -612,6 +630,19 @@ var renderTable = (response) => {
|
|
|
612
630
|
{ key: "Value", header: "Value" }
|
|
613
631
|
]
|
|
614
632
|
);
|
|
633
|
+
case "discovery-status": {
|
|
634
|
+
const summaryTable = renderAsciiTable(
|
|
635
|
+
Object.entries(response.summary).map(([field, value]) => ({ Field: field, Value: value })),
|
|
636
|
+
[
|
|
637
|
+
{ key: "Field", header: "Field" },
|
|
638
|
+
{ key: "Value", header: "Value" }
|
|
639
|
+
]
|
|
640
|
+
);
|
|
641
|
+
const regionsTable = response.rows.length === 0 ? "No discovery status available." : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
|
|
642
|
+
return `${summaryTable}
|
|
643
|
+
|
|
644
|
+
${regionsTable}`;
|
|
645
|
+
}
|
|
615
646
|
case "record-list":
|
|
616
647
|
return response.rows.length === 0 ? response.emptyMessage : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
|
|
617
648
|
case "rule-list":
|
|
@@ -635,19 +666,34 @@ var renderTable = (response) => {
|
|
|
635
666
|
);
|
|
636
667
|
}
|
|
637
668
|
};
|
|
638
|
-
var projectScanRows = (result) =>
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
669
|
+
var projectScanRows = (result) => [
|
|
670
|
+
...flattenScanResult(result).map(({ finding, message, provider, ruleId, service, source }) => ({
|
|
671
|
+
accountId: finding.accountId ?? "",
|
|
672
|
+
message,
|
|
673
|
+
path: finding.location?.path ?? "",
|
|
674
|
+
provider,
|
|
675
|
+
region: finding.region ?? "",
|
|
676
|
+
resourceId: finding.resourceId,
|
|
677
|
+
ruleId,
|
|
678
|
+
service,
|
|
679
|
+
source,
|
|
680
|
+
column: finding.location?.column ?? "",
|
|
681
|
+
line: finding.location?.line ?? ""
|
|
682
|
+
})),
|
|
683
|
+
...getScanDiagnostics(result).map((diagnostic) => ({
|
|
684
|
+
accountId: "",
|
|
685
|
+
column: "",
|
|
686
|
+
line: "",
|
|
687
|
+
message: diagnostic.message,
|
|
688
|
+
path: "",
|
|
689
|
+
provider: diagnostic.provider,
|
|
690
|
+
region: diagnostic.region ?? "",
|
|
691
|
+
resourceId: "",
|
|
692
|
+
ruleId: "",
|
|
693
|
+
service: diagnostic.service,
|
|
694
|
+
source: diagnostic.source
|
|
695
|
+
}))
|
|
696
|
+
];
|
|
651
697
|
var renderTextRows = (rows, columns, emptyMessage) => {
|
|
652
698
|
if (rows.length === 0) {
|
|
653
699
|
return emptyMessage;
|
|
@@ -694,17 +740,118 @@ var toTextCell = (value) => {
|
|
|
694
740
|
}
|
|
695
741
|
return JSON.stringify(value);
|
|
696
742
|
};
|
|
697
|
-
var toTableCell = (value) =>
|
|
698
|
-
|
|
699
|
-
|
|
743
|
+
var toTableCell = (value) => {
|
|
744
|
+
if (Array.isArray(value)) {
|
|
745
|
+
return value.map((item) => toTextCell(item)).join(", ").replace(/\r?\n/g, "\\n");
|
|
746
|
+
}
|
|
747
|
+
return toTextCell(value).replace(/\r?\n/g, "\\n");
|
|
748
|
+
};
|
|
749
|
+
var resolveTableColumns = (rows, columns) => {
|
|
750
|
+
const visibleColumns = columns.filter((column) => rows.some((row) => toTableCell(row[column.key]).length > 0));
|
|
751
|
+
return visibleColumns.length === 0 ? columns : visibleColumns;
|
|
752
|
+
};
|
|
753
|
+
var getTargetTableWidth = () => {
|
|
754
|
+
const terminalWidth = process.stdout.columns;
|
|
755
|
+
return typeof terminalWidth === "number" && Number.isFinite(terminalWidth) && terminalWidth > 0 ? terminalWidth : DEFAULT_TABLE_WIDTH;
|
|
756
|
+
};
|
|
757
|
+
var measureTableWidth = (widths) => widths.reduce((total, width) => total + width, 0) + widths.length * 3 + 1;
|
|
758
|
+
var fitColumnWidths = (columns, rows) => {
|
|
759
|
+
const maxWidths = columns.map(
|
|
700
760
|
(column) => Math.max(column.header.length, ...rows.map((row) => toTableCell(row[column.key]).length))
|
|
701
761
|
);
|
|
762
|
+
const minWidths = columns.map(
|
|
763
|
+
(column, index) => Math.min(
|
|
764
|
+
maxWidths[index] ?? MIN_COLUMN_WIDTH,
|
|
765
|
+
Math.max(MIN_COLUMN_WIDTH, Math.min(column.header.length, PREFERRED_MIN_COLUMN_WIDTH))
|
|
766
|
+
)
|
|
767
|
+
);
|
|
768
|
+
const widths = [...maxWidths];
|
|
769
|
+
const targetWidth = getTargetTableWidth();
|
|
770
|
+
while (measureTableWidth(widths) > targetWidth) {
|
|
771
|
+
let widestColumnIndex = -1;
|
|
772
|
+
for (let index = 0; index < widths.length; index += 1) {
|
|
773
|
+
if ((widths[index] ?? 0) <= (minWidths[index] ?? MIN_COLUMN_WIDTH)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (widestColumnIndex === -1 || (widths[index] ?? 0) > (widths[widestColumnIndex] ?? 0)) {
|
|
777
|
+
widestColumnIndex = index;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (widestColumnIndex === -1) {
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
widths[widestColumnIndex] = Math.max(MIN_COLUMN_WIDTH, (widths[widestColumnIndex] ?? MIN_COLUMN_WIDTH) - 1);
|
|
784
|
+
}
|
|
785
|
+
return widths;
|
|
786
|
+
};
|
|
787
|
+
var wrapToken = (token, width) => {
|
|
788
|
+
if (token.length <= width) {
|
|
789
|
+
return [token];
|
|
790
|
+
}
|
|
791
|
+
const segments = [];
|
|
792
|
+
for (let start = 0; start < token.length; start += width) {
|
|
793
|
+
segments.push(token.slice(start, start + width));
|
|
794
|
+
}
|
|
795
|
+
return segments;
|
|
796
|
+
};
|
|
797
|
+
var wrapCell = (value, width) => {
|
|
798
|
+
if (value.length <= width) {
|
|
799
|
+
return [value];
|
|
800
|
+
}
|
|
801
|
+
const words = value.split(/\s+/).filter((word) => word.length > 0);
|
|
802
|
+
if (words.length === 0) {
|
|
803
|
+
return [""];
|
|
804
|
+
}
|
|
805
|
+
const lines = [];
|
|
806
|
+
let currentLine = "";
|
|
807
|
+
for (const word of words) {
|
|
808
|
+
if (word.length > width) {
|
|
809
|
+
if (currentLine.length > 0) {
|
|
810
|
+
lines.push(currentLine);
|
|
811
|
+
currentLine = "";
|
|
812
|
+
}
|
|
813
|
+
lines.push(...wrapToken(word, width));
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (currentLine.length === 0) {
|
|
817
|
+
currentLine = word;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (currentLine.length + 1 + word.length <= width) {
|
|
821
|
+
currentLine = `${currentLine} ${word}`;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
lines.push(currentLine);
|
|
825
|
+
currentLine = word;
|
|
826
|
+
}
|
|
827
|
+
if (currentLine.length > 0) {
|
|
828
|
+
lines.push(currentLine);
|
|
829
|
+
}
|
|
830
|
+
return lines;
|
|
831
|
+
};
|
|
832
|
+
var renderTableLines = (cells, widths) => {
|
|
833
|
+
const wrappedCells = cells.map((cell, index) => wrapCell(cell, widths[index] ?? MIN_COLUMN_WIDTH));
|
|
834
|
+
const height = Math.max(...wrappedCells.map((lines) => lines.length));
|
|
835
|
+
return Array.from({ length: height }, (_, lineIndex) => {
|
|
836
|
+
const line = wrappedCells.map((lines, index) => (lines[lineIndex] ?? "").padEnd(widths[index] ?? MIN_COLUMN_WIDTH)).join(" | ");
|
|
837
|
+
return `| ${line} |`;
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
var renderAsciiTable = (rows, columns) => {
|
|
841
|
+
const visibleColumns = resolveTableColumns(rows, columns);
|
|
842
|
+
const widths = fitColumnWidths(visibleColumns, rows);
|
|
702
843
|
const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
703
|
-
const header =
|
|
704
|
-
|
|
705
|
-
|
|
844
|
+
const header = renderTableLines(
|
|
845
|
+
visibleColumns.map((column) => column.header),
|
|
846
|
+
widths
|
|
847
|
+
);
|
|
848
|
+
const body = rows.flatMap(
|
|
849
|
+
(row) => renderTableLines(
|
|
850
|
+
visibleColumns.map((column) => toTableCell(row[column.key])),
|
|
851
|
+
widths
|
|
852
|
+
)
|
|
706
853
|
);
|
|
707
|
-
return [border, header, border, ...body, border].join("\n");
|
|
854
|
+
return [border, ...header, border, ...body, border].join("\n");
|
|
708
855
|
};
|
|
709
856
|
|
|
710
857
|
// src/commands/config-options.ts
|
|
@@ -718,6 +865,69 @@ var parseRuleIdList = (value) => {
|
|
|
718
865
|
};
|
|
719
866
|
|
|
720
867
|
// src/commands/discover.ts
|
|
868
|
+
var describeDiscoverySummary = (status) => {
|
|
869
|
+
const aggregatorSummary = status.aggregatorRegion ? ` Aggregator region: ${status.aggregatorRegion}.` : "";
|
|
870
|
+
const warningSummary = status.warning ? ` ${status.warning}` : "";
|
|
871
|
+
return `Coverage: ${status.coverage}. Indexed ${status.indexedRegionCount} of ${status.totalRegionCount} enabled regions.${aggregatorSummary}${warningSummary}`.trim();
|
|
872
|
+
};
|
|
873
|
+
var describeInitializationMessage = (result) => {
|
|
874
|
+
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}.`;
|
|
875
|
+
const indexSummaryParts = [];
|
|
876
|
+
if (result.createdIndexCount > 0) {
|
|
877
|
+
indexSummaryParts.push(
|
|
878
|
+
`Created ${result.createdIndexCount} ${result.createdIndexCount === 1 ? "index" : "indexes"}.`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (result.reusedIndexCount > 0) {
|
|
882
|
+
indexSummaryParts.push(
|
|
883
|
+
`Reused ${result.reusedIndexCount} existing ${result.reusedIndexCount === 1 ? "index" : "indexes"}.`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
const indexSummary = indexSummaryParts.length === 0 ? "" : ` ${indexSummaryParts.join(" ")}`;
|
|
887
|
+
const warnings = Array.from(new Set([result.warning, result.observedStatus.warning].filter(Boolean)));
|
|
888
|
+
const warning = warnings.length === 0 ? "" : ` ${warnings.join(" ")}`;
|
|
889
|
+
const convergenceNotice = result.verificationStatus === "timed_out" ? " Setup is still converging in AWS, so the observed coverage may still change." : "";
|
|
890
|
+
return `${baseMessage}${indexSummary}${warning}${convergenceNotice}`.trim();
|
|
891
|
+
};
|
|
892
|
+
var buildInitializationStatusData = (result, message, format) => {
|
|
893
|
+
const restrictedRegionCount = Math.max(
|
|
894
|
+
0,
|
|
895
|
+
result.observedStatus.totalRegionCount - result.observedStatus.accessibleRegionCount
|
|
896
|
+
);
|
|
897
|
+
if (format === "json") {
|
|
898
|
+
return {
|
|
899
|
+
aggregatorAction: result.aggregatorAction,
|
|
900
|
+
aggregatorRegion: result.aggregatorRegion,
|
|
901
|
+
coverage: result.coverage,
|
|
902
|
+
createdIndexCount: result.createdIndexCount,
|
|
903
|
+
indexType: result.indexType,
|
|
904
|
+
message,
|
|
905
|
+
observedStatus: result.observedStatus,
|
|
906
|
+
regions: result.regions,
|
|
907
|
+
reusedIndexCount: result.reusedIndexCount,
|
|
908
|
+
status: result.status,
|
|
909
|
+
taskId: result.taskId ?? "",
|
|
910
|
+
verificationStatus: result.verificationStatus,
|
|
911
|
+
...result.warning ? { warning: result.warning } : {}
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
aggregatorAction: result.aggregatorAction,
|
|
916
|
+
aggregatorRegion: result.aggregatorRegion,
|
|
917
|
+
coverage: result.coverage,
|
|
918
|
+
createdIndexes: String(result.createdIndexCount),
|
|
919
|
+
details: "Run `cloudburn discover status` for per-region details.",
|
|
920
|
+
indexedRegions: result.regions.length === 0 ? "none" : result.regions.join(", "),
|
|
921
|
+
indexedSummary: `${result.observedStatus.indexedRegionCount} of ${result.observedStatus.totalRegionCount}`,
|
|
922
|
+
indexType: result.indexType,
|
|
923
|
+
message,
|
|
924
|
+
reusedIndexes: String(result.reusedIndexCount),
|
|
925
|
+
...restrictedRegionCount > 0 ? { restrictedRegions: String(restrictedRegionCount) } : {},
|
|
926
|
+
status: result.status,
|
|
927
|
+
taskId: result.taskId ?? "",
|
|
928
|
+
verificationStatus: result.verificationStatus
|
|
929
|
+
};
|
|
930
|
+
};
|
|
721
931
|
var parseAwsRegion = (value) => {
|
|
722
932
|
try {
|
|
723
933
|
return assertValidAwsRegion(value);
|
|
@@ -756,9 +966,17 @@ var registerDiscoverCommand = (program) => {
|
|
|
756
966
|
const discoverCommand = setCommandExamples(
|
|
757
967
|
program.command("discover").description("Run a live AWS discovery").enablePositionalOptions().option(
|
|
758
968
|
"--region <region>",
|
|
759
|
-
'Discovery region to use. Pass "all" to
|
|
969
|
+
'Discovery region to use. Defaults to the current AWS region from AWS_REGION; use this flag to override it. Pass "all" to check resources in all regions that are indexed in AWS Resource Explorer.',
|
|
760
970
|
parseDiscoverRegion
|
|
761
|
-
).option("--config <path>", "Explicit CloudBurn config file to load").option(
|
|
971
|
+
).option("--config <path>", "Explicit CloudBurn config file to load").option(
|
|
972
|
+
"--enabled-rules <ruleIds>",
|
|
973
|
+
"Comma-separated rule IDs to enable. When set, CloudBurn checks only these rules. By default, all rules are enabled.",
|
|
974
|
+
parseRuleIdList
|
|
975
|
+
).option(
|
|
976
|
+
"--disabled-rules <ruleIds>",
|
|
977
|
+
"Comma-separated rule IDs to disable. By default, all rules are enabled; use this to exclude specific rules.",
|
|
978
|
+
parseRuleIdList
|
|
979
|
+
).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
|
|
762
980
|
await runCommand(async () => {
|
|
763
981
|
const scanner = new CloudBurnClient();
|
|
764
982
|
const loadedConfig = await scanner.loadConfig(options.config);
|
|
@@ -787,10 +1005,64 @@ var registerDiscoverCommand = (program) => {
|
|
|
787
1005
|
"cloudburn discover",
|
|
788
1006
|
"cloudburn discover --region eu-central-1",
|
|
789
1007
|
"cloudburn discover --region all",
|
|
1008
|
+
"cloudburn discover status",
|
|
790
1009
|
"cloudburn discover list-enabled-regions",
|
|
791
1010
|
"cloudburn discover init"
|
|
792
1011
|
]
|
|
793
1012
|
);
|
|
1013
|
+
discoverCommand.command("status").description("Show Resource Explorer status across all enabled AWS regions").action(async (_options, command) => {
|
|
1014
|
+
await runCommand(async () => {
|
|
1015
|
+
const scanner = new CloudBurnClient();
|
|
1016
|
+
const parentRegion = discoverCommand.opts().region;
|
|
1017
|
+
const region = parentRegion === "all" ? void 0 : parentRegion;
|
|
1018
|
+
const status = await scanner.getDiscoveryStatus({ region });
|
|
1019
|
+
const format = resolveOutputFormat(command);
|
|
1020
|
+
const rows = format === "json" ? status.regions.map((regionStatus) => ({
|
|
1021
|
+
...regionStatus,
|
|
1022
|
+
notes: regionStatus.notes ?? ""
|
|
1023
|
+
})) : status.regions.map((regionStatus) => ({
|
|
1024
|
+
region: regionStatus.region,
|
|
1025
|
+
indexType: regionStatus.indexType === void 0 ? "" : regionStatus.isAggregator ? `${regionStatus.indexType} (active)` : regionStatus.indexType,
|
|
1026
|
+
notes: regionStatus.notes ?? "",
|
|
1027
|
+
status: regionStatus.status,
|
|
1028
|
+
viewStatus: regionStatus.viewStatus ?? ""
|
|
1029
|
+
}));
|
|
1030
|
+
const summary = format === "json" ? {
|
|
1031
|
+
accessibleRegionCount: status.accessibleRegionCount,
|
|
1032
|
+
coverage: status.coverage,
|
|
1033
|
+
indexedRegionCount: status.indexedRegionCount,
|
|
1034
|
+
totalRegionCount: status.totalRegionCount,
|
|
1035
|
+
...status.aggregatorRegion ? { aggregatorRegion: status.aggregatorRegion } : {},
|
|
1036
|
+
...status.warning ? { warning: status.warning } : {}
|
|
1037
|
+
} : {
|
|
1038
|
+
accessibleRegionCount: status.accessibleRegionCount,
|
|
1039
|
+
aggregatorRegion: status.aggregatorRegion ?? "",
|
|
1040
|
+
coverage: status.coverage,
|
|
1041
|
+
indexedRegionCount: status.indexedRegionCount,
|
|
1042
|
+
totalRegionCount: status.totalRegionCount,
|
|
1043
|
+
...status.warning ? { warning: status.warning } : {}
|
|
1044
|
+
};
|
|
1045
|
+
const output = renderResponse(
|
|
1046
|
+
{
|
|
1047
|
+
kind: "discovery-status",
|
|
1048
|
+
columns: [
|
|
1049
|
+
{ key: "region", header: "Region" },
|
|
1050
|
+
{ key: "indexType", header: "IndexType" },
|
|
1051
|
+
{ key: "status", header: "Status" },
|
|
1052
|
+
{ key: "viewStatus", header: "ViewStatus" },
|
|
1053
|
+
{ key: "notes", header: "Notes" }
|
|
1054
|
+
],
|
|
1055
|
+
rows,
|
|
1056
|
+
summary,
|
|
1057
|
+
summaryText: describeDiscoverySummary(status)
|
|
1058
|
+
},
|
|
1059
|
+
format
|
|
1060
|
+
);
|
|
1061
|
+
process.stdout.write(`${output}
|
|
1062
|
+
`);
|
|
1063
|
+
return EXIT_CODE_OK;
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
794
1066
|
discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").action(async (_options, command) => {
|
|
795
1067
|
await runCommand(async () => {
|
|
796
1068
|
const scanner = new CloudBurnClient();
|
|
@@ -813,24 +1085,22 @@ var registerDiscoverCommand = (program) => {
|
|
|
813
1085
|
return EXIT_CODE_OK;
|
|
814
1086
|
});
|
|
815
1087
|
});
|
|
816
|
-
discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option(
|
|
1088
|
+
discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option(
|
|
1089
|
+
"--region <region>",
|
|
1090
|
+
"Requested aggregator region to create or reuse during setup. This is the main Resource Explorer region that aggregates indexes from other regions when cross-region setup succeeds. Defaults to the current AWS region from AWS_REGION; use this flag to override it.",
|
|
1091
|
+
parseAwsRegion
|
|
1092
|
+
).action(async (options, command) => {
|
|
817
1093
|
await runCommand(async () => {
|
|
818
1094
|
const scanner = new CloudBurnClient();
|
|
819
1095
|
const parentRegion = discoverCommand.opts().region;
|
|
820
1096
|
const region = options.region ?? (parentRegion === "all" ? void 0 : parentRegion);
|
|
821
1097
|
const result = await scanner.initializeDiscovery({ region });
|
|
822
|
-
const message = result
|
|
1098
|
+
const message = describeInitializationMessage(result);
|
|
823
1099
|
const format = resolveOutputFormat(command);
|
|
824
1100
|
const output = renderResponse(
|
|
825
1101
|
{
|
|
826
1102
|
kind: "status",
|
|
827
|
-
data:
|
|
828
|
-
aggregatorRegion: result.aggregatorRegion,
|
|
829
|
-
message,
|
|
830
|
-
regions: result.regions,
|
|
831
|
-
status: result.status,
|
|
832
|
-
taskId: result.taskId ?? ""
|
|
833
|
-
},
|
|
1103
|
+
data: buildInitializationStatusData(result, message, format),
|
|
834
1104
|
text: message
|
|
835
1105
|
},
|
|
836
1106
|
format
|
|
@@ -1041,7 +1311,15 @@ var toScanConfigOverride = (options) => {
|
|
|
1041
1311
|
};
|
|
1042
1312
|
var registerScanCommand = (program) => {
|
|
1043
1313
|
setCommandExamples(
|
|
1044
|
-
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(
|
|
1314
|
+
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(
|
|
1315
|
+
"--enabled-rules <ruleIds>",
|
|
1316
|
+
"Comma-separated rule IDs to enable. When set, CloudBurn checks only these rules. By default, all rules are enabled.",
|
|
1317
|
+
parseRuleIdList
|
|
1318
|
+
).option(
|
|
1319
|
+
"--disabled-rules <ruleIds>",
|
|
1320
|
+
"Comma-separated rule IDs to disable. By default, all rules are enabled; use this to exclude specific rules.",
|
|
1321
|
+
parseRuleIdList
|
|
1322
|
+
).option("--exit-code", "Exit with code 1 when findings exist").action(async (path, options, command) => {
|
|
1045
1323
|
try {
|
|
1046
1324
|
const scanner = new CloudBurnClient2();
|
|
1047
1325
|
const loadedConfig = await scanner.loadConfig(options.config);
|
|
@@ -1070,7 +1348,7 @@ var registerScanCommand = (program) => {
|
|
|
1070
1348
|
// src/cli.ts
|
|
1071
1349
|
var createProgram = () => {
|
|
1072
1350
|
const program = createCliCommand();
|
|
1073
|
-
program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.8.
|
|
1351
|
+
program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.8.3").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
|
|
1074
1352
|
configureCliHelp(program);
|
|
1075
1353
|
registerCompletionCommand(program);
|
|
1076
1354
|
registerDiscoverCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudburn",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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.13.2"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@biomejs/biome": "^2.4.6",
|