cloudburn 0.5.0 → 0.6.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.
- package/dist/cli.js +287 -123
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,11 +5,8 @@ import { pathToFileURL } from "url";
|
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/discover.ts
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
CloudBurnClient
|
|
11
|
-
} from "@cloudburn/sdk";
|
|
12
|
-
import { InvalidArgumentError } from "commander";
|
|
8
|
+
import { assertValidAwsRegion, CloudBurnClient } from "@cloudburn/sdk";
|
|
9
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
13
10
|
|
|
14
11
|
// src/exit-codes.ts
|
|
15
12
|
var EXIT_CODE_OK = 0;
|
|
@@ -59,8 +56,8 @@ var categorize = (err) => {
|
|
|
59
56
|
};
|
|
60
57
|
};
|
|
61
58
|
|
|
62
|
-
// src/formatters/
|
|
63
|
-
|
|
59
|
+
// src/formatters/output.ts
|
|
60
|
+
import { InvalidArgumentError } from "commander";
|
|
64
61
|
|
|
65
62
|
// src/formatters/shared.ts
|
|
66
63
|
var flattenScanResult = (result) => result.providers.flatMap(
|
|
@@ -77,82 +74,192 @@ var flattenScanResult = (result) => result.providers.flatMap(
|
|
|
77
74
|
);
|
|
78
75
|
var countScanResultFindings = (result) => flattenScanResult(result).length;
|
|
79
76
|
|
|
80
|
-
// src/formatters/
|
|
81
|
-
var
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
// src/formatters/output.ts
|
|
78
|
+
var supportedOutputFormats = ["text", "json", "table"];
|
|
79
|
+
var scanColumns = [
|
|
80
|
+
{ key: "provider", header: "Provider" },
|
|
81
|
+
{ key: "ruleId", header: "RuleId" },
|
|
82
|
+
{ key: "source", header: "Source" },
|
|
83
|
+
{ key: "service", header: "Service" },
|
|
84
|
+
{ key: "resourceId", header: "ResourceId" },
|
|
85
|
+
{ key: "accountId", header: "AccountId" },
|
|
86
|
+
{ key: "region", header: "Region" },
|
|
87
|
+
{ key: "path", header: "Path" },
|
|
88
|
+
{ key: "startLine", header: "StartLine" },
|
|
89
|
+
{ key: "startColumn", header: "StartColumn" },
|
|
90
|
+
{ key: "message", header: "Message" }
|
|
91
|
+
];
|
|
92
|
+
var formatOptionDescription = "Output format. table: human-readable terminal output. text: tab-delimited output for grep, sed, and awk. json: machine-readable output for automation and downstream systems.";
|
|
93
|
+
var OUTPUT_FORMAT_OPTION_DESCRIPTION = formatOptionDescription;
|
|
94
|
+
var parseOutputFormat = (value) => {
|
|
95
|
+
if (supportedOutputFormats.includes(value)) {
|
|
96
|
+
return value;
|
|
84
97
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
throw new InvalidArgumentError(`Invalid format "${value}". Allowed formats: ${supportedOutputFormats.join(", ")}.`);
|
|
99
|
+
};
|
|
100
|
+
var resolveOutputFormat = (command, localFormat, defaultFormat = "table") => {
|
|
101
|
+
if (localFormat) {
|
|
102
|
+
return localFormat;
|
|
103
|
+
}
|
|
104
|
+
const options = typeof command.optsWithGlobals === "function" ? command.optsWithGlobals() : command.opts();
|
|
105
|
+
return options.format ?? defaultFormat;
|
|
106
|
+
};
|
|
107
|
+
var renderResponse = (response, format) => {
|
|
108
|
+
switch (format) {
|
|
109
|
+
case "json":
|
|
110
|
+
return renderJson(response);
|
|
111
|
+
case "text":
|
|
112
|
+
return renderText(response);
|
|
113
|
+
case "table":
|
|
114
|
+
return renderTable(response);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var renderJson = (response) => {
|
|
118
|
+
switch (response.kind) {
|
|
119
|
+
case "document":
|
|
120
|
+
return JSON.stringify({ content: response.content, contentType: response.contentType }, null, 2);
|
|
121
|
+
case "record-list":
|
|
122
|
+
return JSON.stringify(response.rows, null, 2);
|
|
123
|
+
case "rule-list":
|
|
124
|
+
return JSON.stringify(response.rules, null, 2);
|
|
125
|
+
case "scan-result":
|
|
126
|
+
return JSON.stringify(response.result, null, 2);
|
|
127
|
+
case "status":
|
|
128
|
+
return JSON.stringify(response.data, null, 2);
|
|
129
|
+
case "string-list":
|
|
130
|
+
return JSON.stringify(response.values, null, 2);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var renderText = (response) => {
|
|
134
|
+
switch (response.kind) {
|
|
135
|
+
case "document":
|
|
136
|
+
return response.content;
|
|
137
|
+
case "record-list":
|
|
138
|
+
return renderTextRows(response.rows, response.columns, response.emptyMessage);
|
|
139
|
+
case "rule-list":
|
|
140
|
+
return renderRuleList(response.rules, response.emptyMessage);
|
|
141
|
+
case "scan-result":
|
|
142
|
+
return renderTextRows(projectScanRows(response.result), scanColumns, "No findings.");
|
|
143
|
+
case "status":
|
|
144
|
+
return response.text;
|
|
145
|
+
case "string-list":
|
|
146
|
+
return response.values.length === 0 ? response.emptyMessage : response.values.join("\n");
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var renderTable = (response) => {
|
|
150
|
+
switch (response.kind) {
|
|
151
|
+
case "document":
|
|
152
|
+
return renderAsciiTable(
|
|
153
|
+
[
|
|
154
|
+
{ Field: "ContentType", Value: response.contentType },
|
|
155
|
+
{ Field: "Content", Value: response.content }
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
{ key: "Field", header: "Field" },
|
|
159
|
+
{ key: "Value", header: "Value" }
|
|
160
|
+
]
|
|
161
|
+
);
|
|
162
|
+
case "record-list":
|
|
163
|
+
return response.rows.length === 0 ? response.emptyMessage : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
|
|
164
|
+
case "rule-list":
|
|
165
|
+
return renderRuleList(response.rules, response.emptyMessage);
|
|
166
|
+
case "scan-result": {
|
|
167
|
+
const rows = projectScanRows(response.result);
|
|
168
|
+
return rows.length === 0 ? "No findings." : renderAsciiTable(rows, scanColumns);
|
|
98
169
|
}
|
|
99
|
-
|
|
170
|
+
case "status":
|
|
171
|
+
return renderAsciiTable(
|
|
172
|
+
Object.entries(response.data).map(([field, value]) => ({ Field: field, Value: value })),
|
|
173
|
+
[
|
|
174
|
+
{ key: "Field", header: "Field" },
|
|
175
|
+
{ key: "Value", header: "Value" }
|
|
176
|
+
]
|
|
177
|
+
);
|
|
178
|
+
case "string-list":
|
|
179
|
+
return response.values.length === 0 ? response.emptyMessage : renderAsciiTable(
|
|
180
|
+
response.values.map((value) => ({ [response.columnHeader]: value })),
|
|
181
|
+
[{ key: response.columnHeader, header: response.columnHeader }]
|
|
182
|
+
);
|
|
183
|
+
}
|
|
100
184
|
};
|
|
101
|
-
var
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}))
|
|
118
|
-
}
|
|
119
|
-
]
|
|
120
|
-
},
|
|
121
|
-
null,
|
|
122
|
-
2
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// src/formatters/table.ts
|
|
126
|
-
var formatTable = (result) => {
|
|
127
|
-
const flattenedFindings = flattenScanResult(result);
|
|
128
|
-
if (flattenedFindings.length === 0) {
|
|
129
|
-
return "No findings.";
|
|
185
|
+
var projectScanRows = (result) => flattenScanResult(result).map(({ finding, message, provider, ruleId, service, source }) => ({
|
|
186
|
+
accountId: finding.accountId ?? "",
|
|
187
|
+
message,
|
|
188
|
+
path: finding.location?.path ?? "",
|
|
189
|
+
provider,
|
|
190
|
+
region: finding.region ?? "",
|
|
191
|
+
resourceId: finding.resourceId,
|
|
192
|
+
ruleId,
|
|
193
|
+
service,
|
|
194
|
+
source,
|
|
195
|
+
startColumn: finding.location?.startColumn ?? "",
|
|
196
|
+
startLine: finding.location?.startLine ?? ""
|
|
197
|
+
}));
|
|
198
|
+
var renderTextRows = (rows, columns, emptyMessage) => {
|
|
199
|
+
if (rows.length === 0) {
|
|
200
|
+
return emptyMessage;
|
|
130
201
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return `${provider} ${ruleId} ${source} ${service} ${finding.resourceId}${location} ${message}`;
|
|
134
|
-
}).join("\n");
|
|
202
|
+
const resolvedColumns = columns ?? inferColumns(rows);
|
|
203
|
+
return rows.map((row) => resolvedColumns.map((column) => toTextCell(row[column.key])).join(" ")).join("\n");
|
|
135
204
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
205
|
+
var inferColumns = (rows) => {
|
|
206
|
+
const keys = Array.from(new Set(rows.flatMap((row) => Object.keys(row)))).sort(
|
|
207
|
+
(left, right) => left.localeCompare(right)
|
|
208
|
+
);
|
|
209
|
+
return keys.map((key) => ({ key, header: key }));
|
|
210
|
+
};
|
|
211
|
+
var renderRuleList = (rules, emptyMessage) => {
|
|
212
|
+
if (rules.length === 0) {
|
|
213
|
+
return emptyMessage;
|
|
142
214
|
}
|
|
143
|
-
|
|
215
|
+
let currentProvider = "";
|
|
216
|
+
let currentService = "";
|
|
217
|
+
const lines = [];
|
|
218
|
+
for (const rule of rules) {
|
|
219
|
+
if (rule.provider !== currentProvider) {
|
|
220
|
+
currentProvider = rule.provider;
|
|
221
|
+
currentService = "";
|
|
222
|
+
lines.push(rule.provider);
|
|
223
|
+
}
|
|
224
|
+
if (rule.service !== currentService) {
|
|
225
|
+
currentService = rule.service;
|
|
226
|
+
lines.push(` ${rule.service}`);
|
|
227
|
+
}
|
|
228
|
+
lines.push(` ${rule.id}: ${rule.description}`);
|
|
229
|
+
}
|
|
230
|
+
return lines.join("\n");
|
|
144
231
|
};
|
|
145
|
-
var
|
|
146
|
-
if (value ===
|
|
232
|
+
var toTextCell = (value) => {
|
|
233
|
+
if (value === null || value === void 0) {
|
|
234
|
+
return "";
|
|
235
|
+
}
|
|
236
|
+
if (typeof value === "string") {
|
|
147
237
|
return value;
|
|
148
238
|
}
|
|
149
|
-
|
|
239
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
240
|
+
return String(value);
|
|
241
|
+
}
|
|
242
|
+
return JSON.stringify(value);
|
|
243
|
+
};
|
|
244
|
+
var toTableCell = (value) => toTextCell(value).replace(/\r?\n/g, "\\n");
|
|
245
|
+
var renderAsciiTable = (rows, columns) => {
|
|
246
|
+
const widths = columns.map(
|
|
247
|
+
(column) => Math.max(column.header.length, ...rows.map((row) => toTableCell(row[column.key]).length))
|
|
248
|
+
);
|
|
249
|
+
const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
250
|
+
const header = `| ${columns.map((column, index) => column.header.padEnd(widths[index] ?? 0)).join(" | ")} |`;
|
|
251
|
+
const body = rows.map(
|
|
252
|
+
(row) => `| ${columns.map((column, index) => toTableCell(row[column.key]).padEnd(widths[index] ?? 0)).join(" | ")} |`
|
|
253
|
+
);
|
|
254
|
+
return [border, header, border, ...body, border].join("\n");
|
|
150
255
|
};
|
|
256
|
+
|
|
257
|
+
// src/commands/discover.ts
|
|
151
258
|
var parseAwsRegion = (value) => {
|
|
152
259
|
try {
|
|
153
260
|
return assertValidAwsRegion(value);
|
|
154
261
|
} catch (err) {
|
|
155
|
-
throw new
|
|
262
|
+
throw new InvalidArgumentError2(err instanceof Error ? err.message : "Invalid AWS region.");
|
|
156
263
|
}
|
|
157
264
|
};
|
|
158
265
|
var parseDiscoverRegion = (value) => {
|
|
@@ -162,16 +269,6 @@ var parseDiscoverRegion = (value) => {
|
|
|
162
269
|
return parseAwsRegion(value);
|
|
163
270
|
};
|
|
164
271
|
var resolveDiscoveryTarget = (region) => region === void 0 ? { mode: "current" } : region === "all" ? { mode: "all" } : { mode: "region", region };
|
|
165
|
-
var scanFormatters = {
|
|
166
|
-
json: formatJson,
|
|
167
|
-
sarif: formatSarif,
|
|
168
|
-
table: formatTable
|
|
169
|
-
};
|
|
170
|
-
var formatValue = (value) => JSON.stringify(value, null, 2);
|
|
171
|
-
var formatEnabledRegions = (regions) => regions.length === 0 ? "No Resource Explorer indexes are enabled." : regions.map(({ region, type }) => `${region} ${type}`).join("\n");
|
|
172
|
-
var formatSupportedResourceTypes = (resourceTypes) => resourceTypes.length === 0 ? "No supported resource types were returned." : resourceTypes.map(({ resourceType, service }) => `${resourceType} ${service ?? "unknown"}`).join("\n");
|
|
173
|
-
var formatInitializationResult = (result) => result.status === "EXISTING" ? `Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Resource Explorer setup created in ${result.aggregatorRegion}.`;
|
|
174
|
-
var resolveListFormat = (parentCommand, options) => parentCommand.opts().format === "json" ? "json" : options.format ?? "table";
|
|
175
272
|
var runCommand = async (action) => {
|
|
176
273
|
try {
|
|
177
274
|
process.exitCode = await action() ?? EXIT_CODE_OK;
|
|
@@ -186,7 +283,7 @@ var registerDiscoverCommand = (program) => {
|
|
|
186
283
|
"--region <region>",
|
|
187
284
|
'Discovery region to use. Pass "all" to require an aggregator index.',
|
|
188
285
|
parseDiscoverRegion
|
|
189
|
-
).option("--format <format>",
|
|
286
|
+
).option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
|
|
190
287
|
"after",
|
|
191
288
|
`
|
|
192
289
|
Examples:
|
|
@@ -196,11 +293,12 @@ Examples:
|
|
|
196
293
|
cloudburn discover list-enabled-regions
|
|
197
294
|
cloudburn discover init
|
|
198
295
|
`
|
|
199
|
-
).action(async (options) => {
|
|
296
|
+
).action(async (options, command) => {
|
|
200
297
|
await runCommand(async () => {
|
|
201
298
|
const scanner = new CloudBurnClient();
|
|
202
299
|
const result = await scanner.discover({ target: resolveDiscoveryTarget(options.region) });
|
|
203
|
-
const
|
|
300
|
+
const format = resolveOutputFormat(command, options.format);
|
|
301
|
+
const output = renderResponse({ kind: "scan-result", result }, format);
|
|
204
302
|
process.stdout.write(`${output}
|
|
205
303
|
`);
|
|
206
304
|
if (options.exitCode && countScanResultFindings(result) > 0) {
|
|
@@ -209,34 +307,75 @@ Examples:
|
|
|
209
307
|
return EXIT_CODE_OK;
|
|
210
308
|
});
|
|
211
309
|
});
|
|
212
|
-
discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").option("--format <format>",
|
|
310
|
+
discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action(async (options, command) => {
|
|
213
311
|
await runCommand(async () => {
|
|
214
312
|
const scanner = new CloudBurnClient();
|
|
215
313
|
const regions = await scanner.listEnabledDiscoveryRegions();
|
|
216
|
-
const format =
|
|
217
|
-
const output =
|
|
314
|
+
const format = resolveOutputFormat(command, options.format);
|
|
315
|
+
const output = renderResponse(
|
|
316
|
+
{
|
|
317
|
+
kind: "record-list",
|
|
318
|
+
columns: [
|
|
319
|
+
{ key: "region", header: "Region" },
|
|
320
|
+
{ key: "type", header: "Type" }
|
|
321
|
+
],
|
|
322
|
+
emptyMessage: "No Resource Explorer indexes are enabled.",
|
|
323
|
+
rows: regions
|
|
324
|
+
},
|
|
325
|
+
format
|
|
326
|
+
);
|
|
218
327
|
process.stdout.write(`${output}
|
|
219
328
|
`);
|
|
220
329
|
return EXIT_CODE_OK;
|
|
221
330
|
});
|
|
222
331
|
});
|
|
223
|
-
discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option("--region <region>", "Aggregator region to create or reuse during setup", parseAwsRegion).action(async (options) => {
|
|
332
|
+
discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--region <region>", "Aggregator region to create or reuse during setup", parseAwsRegion).action(async (options, command) => {
|
|
224
333
|
await runCommand(async () => {
|
|
225
334
|
const scanner = new CloudBurnClient();
|
|
226
335
|
const parentRegion = discoverCommand.opts().region;
|
|
227
336
|
const region = options.region ?? (parentRegion === "all" ? void 0 : parentRegion);
|
|
228
337
|
const result = await scanner.initializeDiscovery({ region });
|
|
229
|
-
|
|
338
|
+
const message = result.status === "EXISTING" ? `Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Resource Explorer setup created in ${result.aggregatorRegion}.`;
|
|
339
|
+
const format = resolveOutputFormat(command, options.format);
|
|
340
|
+
const output = renderResponse(
|
|
341
|
+
{
|
|
342
|
+
kind: "status",
|
|
343
|
+
data: {
|
|
344
|
+
aggregatorRegion: result.aggregatorRegion,
|
|
345
|
+
message,
|
|
346
|
+
regions: result.regions,
|
|
347
|
+
status: result.status,
|
|
348
|
+
taskId: result.taskId ?? ""
|
|
349
|
+
},
|
|
350
|
+
text: message
|
|
351
|
+
},
|
|
352
|
+
format
|
|
353
|
+
);
|
|
354
|
+
process.stdout.write(`${output}
|
|
230
355
|
`);
|
|
231
356
|
return EXIT_CODE_OK;
|
|
232
357
|
});
|
|
233
358
|
});
|
|
234
|
-
discoverCommand.command("supported-resource-types").description("List Resource Explorer supported AWS resource types").option("--format <format>",
|
|
359
|
+
discoverCommand.command("supported-resource-types").description("List Resource Explorer supported AWS resource types").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action(async (options, command) => {
|
|
235
360
|
await runCommand(async () => {
|
|
236
361
|
const scanner = new CloudBurnClient();
|
|
237
362
|
const resourceTypes = await scanner.listSupportedDiscoveryResourceTypes();
|
|
238
|
-
const format =
|
|
239
|
-
const output =
|
|
363
|
+
const format = resolveOutputFormat(command, options.format);
|
|
364
|
+
const output = renderResponse(
|
|
365
|
+
{
|
|
366
|
+
kind: "record-list",
|
|
367
|
+
columns: [
|
|
368
|
+
{ key: "resourceType", header: "ResourceType" },
|
|
369
|
+
{ key: "service", header: "Service" }
|
|
370
|
+
],
|
|
371
|
+
emptyMessage: "No supported resource types were returned.",
|
|
372
|
+
rows: resourceTypes.map((resourceType) => ({
|
|
373
|
+
resourceType: resourceType.resourceType,
|
|
374
|
+
service: resourceType.service ?? "unknown"
|
|
375
|
+
}))
|
|
376
|
+
},
|
|
377
|
+
format
|
|
378
|
+
);
|
|
240
379
|
process.stdout.write(`${output}
|
|
241
380
|
`);
|
|
242
381
|
return EXIT_CODE_OK;
|
|
@@ -246,12 +385,38 @@ Examples:
|
|
|
246
385
|
|
|
247
386
|
// src/commands/estimate.ts
|
|
248
387
|
var registerEstimateCommand = (program) => {
|
|
249
|
-
program.command("estimate").description("Request optional pricing estimates from a self-hosted dashboard").option("--server <url>", "Dashboard API base URL").action((options) => {
|
|
388
|
+
program.command("estimate").description("Request optional pricing estimates from a self-hosted dashboard").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--server <url>", "Dashboard API base URL").action((options, command) => {
|
|
389
|
+
const format = resolveOutputFormat(command, options.format);
|
|
250
390
|
if (!options.server) {
|
|
251
|
-
|
|
391
|
+
const output2 = renderResponse(
|
|
392
|
+
{
|
|
393
|
+
kind: "status",
|
|
394
|
+
data: {
|
|
395
|
+
message: "No server configured. This command is optional and requires a dashboard URL.",
|
|
396
|
+
server: "",
|
|
397
|
+
status: "NOT_CONFIGURED"
|
|
398
|
+
},
|
|
399
|
+
text: "No server configured. This command is optional and requires a dashboard URL."
|
|
400
|
+
},
|
|
401
|
+
format
|
|
402
|
+
);
|
|
403
|
+
process.stdout.write(`${output2}
|
|
404
|
+
`);
|
|
252
405
|
return;
|
|
253
406
|
}
|
|
254
|
-
|
|
407
|
+
const output = renderResponse(
|
|
408
|
+
{
|
|
409
|
+
kind: "status",
|
|
410
|
+
data: {
|
|
411
|
+
message: `Estimate request scaffold ready for server: ${options.server}`,
|
|
412
|
+
server: options.server,
|
|
413
|
+
status: "READY"
|
|
414
|
+
},
|
|
415
|
+
text: `Estimate request scaffold ready for server: ${options.server}`
|
|
416
|
+
},
|
|
417
|
+
format
|
|
418
|
+
);
|
|
419
|
+
process.stdout.write(`${output}
|
|
255
420
|
`);
|
|
256
421
|
});
|
|
257
422
|
};
|
|
@@ -260,49 +425,48 @@ var registerEstimateCommand = (program) => {
|
|
|
260
425
|
var starterConfig = `version: 1
|
|
261
426
|
profile: dev
|
|
262
427
|
|
|
263
|
-
|
|
264
|
-
dev:
|
|
265
|
-
ec2-allowed-instance-types:
|
|
266
|
-
allow: [t3.micro, t3.small, t3.medium]
|
|
267
|
-
|
|
428
|
+
# Profiles are parsed but not applied yet, so configure the active rules block directly for now.
|
|
268
429
|
rules:
|
|
269
|
-
ec2-
|
|
430
|
+
ec2-instance-type-preferred:
|
|
270
431
|
severity: error
|
|
271
432
|
`;
|
|
272
433
|
var registerInitCommand = (program) => {
|
|
273
|
-
program.command("init").description("Print a starter .cloudburn.yml configuration").action(() => {
|
|
274
|
-
|
|
434
|
+
program.command("init").description("Print a starter .cloudburn.yml configuration").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action((options, command) => {
|
|
435
|
+
const output = renderResponse(
|
|
436
|
+
{
|
|
437
|
+
kind: "document",
|
|
438
|
+
content: starterConfig,
|
|
439
|
+
contentType: "application/yaml"
|
|
440
|
+
},
|
|
441
|
+
resolveOutputFormat(command, options.format, "text")
|
|
442
|
+
);
|
|
443
|
+
process.stdout.write(`${output}
|
|
275
444
|
`);
|
|
276
445
|
});
|
|
277
446
|
};
|
|
278
447
|
|
|
279
448
|
// src/commands/rules-list.ts
|
|
280
|
-
import {
|
|
449
|
+
import { builtInRuleMetadata } from "@cloudburn/sdk";
|
|
281
450
|
var registerRulesListCommand = (program) => {
|
|
282
|
-
const rulesCommand = program.command("rules").description("Inspect built-in
|
|
283
|
-
rulesCommand.command("list").description("List built-in CloudBurn
|
|
284
|
-
|
|
451
|
+
const rulesCommand = program.command("rules").description("Inspect built-in CloudBurn rules");
|
|
452
|
+
rulesCommand.command("list").description("List built-in CloudBurn rules").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action((options, command) => {
|
|
453
|
+
const output = renderResponse(
|
|
454
|
+
{
|
|
455
|
+
kind: "rule-list",
|
|
456
|
+
emptyMessage: "No built-in rules are available.",
|
|
457
|
+
rules: builtInRuleMetadata
|
|
458
|
+
},
|
|
459
|
+
resolveOutputFormat(command, options.format, "text")
|
|
460
|
+
);
|
|
461
|
+
process.stdout.write(`${output}
|
|
285
462
|
`);
|
|
286
463
|
});
|
|
287
464
|
};
|
|
288
465
|
|
|
289
466
|
// src/commands/scan.ts
|
|
290
467
|
import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
|
|
291
|
-
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
292
|
-
var supportedScanFormats = ["table", "json", "sarif"];
|
|
293
|
-
var parseScanFormat = (value) => {
|
|
294
|
-
if (supportedScanFormats.includes(value)) {
|
|
295
|
-
return value;
|
|
296
|
-
}
|
|
297
|
-
throw new InvalidArgumentError2(`Invalid format "${value}". Allowed formats: ${supportedScanFormats.join(", ")}.`);
|
|
298
|
-
};
|
|
299
|
-
var formatters = {
|
|
300
|
-
json: formatJson,
|
|
301
|
-
sarif: formatSarif,
|
|
302
|
-
table: formatTable
|
|
303
|
-
};
|
|
304
468
|
var registerScanCommand = (program) => {
|
|
305
|
-
program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--format <format>",
|
|
469
|
+
program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
|
|
306
470
|
"after",
|
|
307
471
|
`
|
|
308
472
|
Examples:
|
|
@@ -310,12 +474,12 @@ Examples:
|
|
|
310
474
|
cloudburn scan ./template.yaml
|
|
311
475
|
cloudburn scan ./iac
|
|
312
476
|
`
|
|
313
|
-
).action(async (path, options) => {
|
|
477
|
+
).action(async (path, options, command) => {
|
|
314
478
|
try {
|
|
315
479
|
const scanner = new CloudBurnClient2();
|
|
316
480
|
const result = await scanner.scanStatic(path ?? process.cwd());
|
|
317
|
-
const format =
|
|
318
|
-
const output =
|
|
481
|
+
const format = resolveOutputFormat(command, options.format);
|
|
482
|
+
const output = renderResponse({ kind: "scan-result", result }, format);
|
|
319
483
|
process.stdout.write(`${output}
|
|
320
484
|
`);
|
|
321
485
|
if (options.exitCode && countScanResultFindings(result) > 0) {
|
|
@@ -334,7 +498,7 @@ Examples:
|
|
|
334
498
|
// src/cli.ts
|
|
335
499
|
var createProgram = () => {
|
|
336
500
|
const program = new Command();
|
|
337
|
-
program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.
|
|
501
|
+
program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.6.0").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
|
|
338
502
|
registerDiscoverCommand(program);
|
|
339
503
|
registerScanCommand(program);
|
|
340
504
|
registerInitCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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.8.
|
|
14
|
+
"@cloudburn/sdk": "0.8.2"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@biomejs/biome": "^2.4.6",
|