@iacmp/cli 1.1.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.
@@ -0,0 +1,375 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/commands/audit-ha.ts
31
+ var audit_ha_exports = {};
32
+ __export(audit_ha_exports, {
33
+ default: () => AuditHA
34
+ });
35
+ module.exports = __toCommonJS(audit_ha_exports);
36
+ var import_core = require("@oclif/core");
37
+ var import_chalk = __toESM(require("chalk"));
38
+
39
+ // src/audit.ts
40
+ var fs2 = __toESM(require("fs"));
41
+ var path = __toESM(require("path"));
42
+
43
+ // src/utils.ts
44
+ var fs = __toESM(require("fs"));
45
+ function readJsonFile(filePath) {
46
+ let content;
47
+ try {
48
+ content = fs.readFileSync(filePath, "utf-8");
49
+ } catch (e) {
50
+ throw new Error(`Falha ao ler '${filePath}': ${errMessage(e)}`);
51
+ }
52
+ try {
53
+ return JSON.parse(content);
54
+ } catch (e) {
55
+ throw new Error(`JSON inv\xE1lido em '${filePath}': ${errMessage(e)}`);
56
+ }
57
+ }
58
+ function errMessage(e) {
59
+ if (e instanceof Error) return e.message;
60
+ if (typeof e === "string") return e;
61
+ try {
62
+ return JSON.stringify(e);
63
+ } catch {
64
+ return String(e);
65
+ }
66
+ }
67
+
68
+ // src/audit.ts
69
+ function readConfig(cwd) {
70
+ const configPath = path.join(cwd, "iacmp.json");
71
+ if (!fs2.existsSync(configPath)) throw new Error("iacmp.json n\xE3o encontrado. Rode: iacmp init");
72
+ const config = readJsonFile(configPath);
73
+ return {
74
+ name: config.name ?? path.basename(cwd),
75
+ provider: config.provider ?? "aws"
76
+ };
77
+ }
78
+ function resolveTsNode(projectDir) {
79
+ const dirs = [];
80
+ let dir = projectDir;
81
+ for (let i = 0; i < 5; i++) {
82
+ dirs.push(dir);
83
+ const parent = path.dirname(dir);
84
+ if (parent === dir) break;
85
+ dir = parent;
86
+ }
87
+ for (const d of dirs) {
88
+ const tsNodePath = path.join(d, "node_modules", "ts-node");
89
+ if (fs2.existsSync(tsNodePath)) return tsNodePath;
90
+ }
91
+ return null;
92
+ }
93
+ function findStackFiles(dir) {
94
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
95
+ const files = [];
96
+ for (const entry of entries) {
97
+ const full = path.join(dir, entry.name);
98
+ if (entry.isDirectory()) {
99
+ files.push(...findStackFiles(full));
100
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
101
+ files.push(full);
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ function loadStacks(cwd) {
107
+ const stacksDir = path.join(cwd, "stacks");
108
+ if (!fs2.existsSync(stacksDir)) throw new Error("Diret\xF3rio stacks/ n\xE3o encontrado.");
109
+ const stackFiles = findStackFiles(stacksDir);
110
+ if (stackFiles.length === 0) throw new Error("Nenhuma stack encontrada em stacks/");
111
+ const tsNodePath = resolveTsNode(cwd);
112
+ if (tsNodePath) {
113
+ require(tsNodePath).register({
114
+ transpileOnly: true,
115
+ skipProject: true,
116
+ compilerOptions: {
117
+ target: "ES2022",
118
+ module: "commonjs",
119
+ moduleResolution: "node",
120
+ esModuleInterop: true,
121
+ strict: false,
122
+ skipLibCheck: true
123
+ }
124
+ });
125
+ }
126
+ const result = [];
127
+ for (const stackPath of stackFiles) {
128
+ const stackName = path.basename(stackPath).replace(/\.(ts|js)$/, "");
129
+ try {
130
+ const mod = require(stackPath);
131
+ const raw = mod.default ?? mod.stack ?? mod;
132
+ if (!raw || typeof raw !== "object" || !("constructs" in raw)) {
133
+ console.warn(`[audit] ${path.basename(stackPath)} n\xE3o exporta uma Stack v\xE1lida \u2014 ignorado.`);
134
+ continue;
135
+ }
136
+ result.push({ name: stackName, stack: raw });
137
+ } catch (err) {
138
+ console.warn(`[audit] falha ao carregar ${path.basename(stackPath)}: ${errMessage(err)}`);
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ function saveReport(cwd, commandName, content) {
144
+ const auditDir = path.join(cwd, "audit");
145
+ if (!fs2.existsSync(auditDir)) fs2.mkdirSync(auditDir, { recursive: true });
146
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
147
+ const fileName = `${commandName}-${date}.md`;
148
+ const filePath = path.join(auditDir, fileName);
149
+ fs2.writeFileSync(filePath, content, "utf-8");
150
+ return path.join("audit", fileName);
151
+ }
152
+ function today() {
153
+ return (/* @__PURE__ */ new Date()).toLocaleDateString("pt-BR");
154
+ }
155
+
156
+ // src/commands/audit-ha.ts
157
+ function shouldFail(failOn, critical, warnings) {
158
+ if (failOn === "critical") return critical > 0;
159
+ if (failOn === "warning") return critical > 0 || warnings > 0;
160
+ return false;
161
+ }
162
+ function analyzeStack(stackName, stack) {
163
+ const findings = [];
164
+ const constructs = stack.constructs;
165
+ for (const c of constructs) {
166
+ const p = c.props;
167
+ if (c.type === "Database.SQL") {
168
+ if (p.multiAz !== true) {
169
+ findings.push({
170
+ status: "no-ha",
171
+ stackName,
172
+ constructId: c.id,
173
+ constructType: c.type,
174
+ title: `Database.SQL '${c.id}' \u2014 Single-AZ`,
175
+ detail: "multiAz is not enabled. A failure in the AZ will make the database unavailable.",
176
+ recommendation: "Set `multiAz: true`."
177
+ });
178
+ } else {
179
+ findings.push({
180
+ status: "ha-ok",
181
+ stackName,
182
+ constructId: c.id,
183
+ constructType: c.type,
184
+ title: `Database.SQL '${c.id}' \u2014 Multi-AZ enabled`,
185
+ detail: "Database is configured with Multi-AZ.",
186
+ recommendation: ""
187
+ });
188
+ }
189
+ }
190
+ if (c.type === "Network.VPC") {
191
+ const maxAzs = p.maxAzs;
192
+ if (maxAzs === void 0 || maxAzs === null || maxAzs < 2) {
193
+ findings.push({
194
+ status: "no-ha",
195
+ stackName,
196
+ constructId: c.id,
197
+ constructType: c.type,
198
+ title: `Network.VPC '${c.id}' \u2014 single AZ`,
199
+ detail: `maxAzs is ${maxAzs ?? "not defined"} (< 2). The network is restricted to a single availability zone.`,
200
+ recommendation: "Set `maxAzs: 2` or more."
201
+ });
202
+ } else {
203
+ findings.push({
204
+ status: "ha-ok",
205
+ stackName,
206
+ constructId: c.id,
207
+ constructType: c.type,
208
+ title: `Network.VPC '${c.id}' \u2014 ${maxAzs} AZs`,
209
+ detail: `Network is configured with ${maxAzs} availability zones.`,
210
+ recommendation: ""
211
+ });
212
+ }
213
+ }
214
+ if (c.type === "Function.Lambda") {
215
+ findings.push({
216
+ status: "ha-ok",
217
+ stackName,
218
+ constructId: c.id,
219
+ constructType: c.type,
220
+ title: `Function.Lambda '${c.id}' \u2014 native HA`,
221
+ detail: "Lambda functions are distributed across multiple AZs by the provider by default.",
222
+ recommendation: ""
223
+ });
224
+ }
225
+ if (c.type === "Storage.Bucket") {
226
+ findings.push({
227
+ status: "ha-ok",
228
+ stackName,
229
+ constructId: c.id,
230
+ constructType: c.type,
231
+ title: `Storage.Bucket '${c.id}' \u2014 native HA`,
232
+ detail: "Object storage buckets are automatically replicated by the provider.",
233
+ recommendation: ""
234
+ });
235
+ }
236
+ }
237
+ const vpcs = constructs.filter((c) => c.type === "Network.VPC");
238
+ if (vpcs.length === 0) {
239
+ findings.push({
240
+ status: "info",
241
+ stackName,
242
+ constructId: "stack",
243
+ constructType: "Stack",
244
+ title: `Stack '${stackName}' \u2014 no VPC`,
245
+ detail: "No Network.VPC found in the stack. No network isolation is defined.",
246
+ recommendation: "Consider adding a VPC to isolate network resources."
247
+ });
248
+ }
249
+ const instances = constructs.filter((c) => c.type === "Compute.Instance");
250
+ if (instances.length === 1) {
251
+ findings.push({
252
+ status: "info",
253
+ stackName,
254
+ constructId: instances[0].id,
255
+ constructType: "Compute.Instance",
256
+ title: `Compute.Instance '${instances[0].id}' \u2014 no redundancy`,
257
+ detail: "Only 1 compute instance found. If it fails, there is no backup instance.",
258
+ recommendation: "Add more instances or configure auto-scaling."
259
+ });
260
+ } else if (instances.length > 1) {
261
+ for (const inst of instances) {
262
+ findings.push({
263
+ status: "ha-ok",
264
+ stackName,
265
+ constructId: inst.id,
266
+ constructType: "Compute.Instance",
267
+ title: `Compute.Instance '${inst.id}' \u2014 redundancy detected`,
268
+ detail: `${instances.length} compute instances in the stack.`,
269
+ recommendation: ""
270
+ });
271
+ }
272
+ }
273
+ return findings;
274
+ }
275
+ var AuditHA = class _AuditHA extends import_core.Command {
276
+ static description = "Audit stacks for high availability (HA) issues";
277
+ static examples = [
278
+ "$ iacmp audit-ha",
279
+ "$ iacmp audit-ha --fail-on=critical"
280
+ ];
281
+ static flags = {
282
+ "fail-on": import_core.Flags.string({
283
+ description: "Sai com exit 1 quando h\xE1 achados no n\xEDvel indicado",
284
+ options: ["critical", "warning", "none"],
285
+ default: "none"
286
+ })
287
+ };
288
+ async run() {
289
+ const { flags } = await this.parse(_AuditHA);
290
+ const failOn = flags["fail-on"];
291
+ const cwd = process.cwd();
292
+ let config;
293
+ try {
294
+ config = readConfig(cwd);
295
+ } catch (err) {
296
+ this.error(err.message);
297
+ }
298
+ let stacks;
299
+ try {
300
+ stacks = loadStacks(cwd);
301
+ } catch (err) {
302
+ this.error(err.message);
303
+ }
304
+ const allFindings = [];
305
+ for (const { name, stack } of stacks) {
306
+ allFindings.push(...analyzeStack(name, stack));
307
+ }
308
+ const noHA = allFindings.filter((f) => f.status === "no-ha");
309
+ const partial = allFindings.filter((f) => f.status === "ha-partial");
310
+ const haOk = allFindings.filter((f) => f.status === "ha-ok");
311
+ const infos = allFindings.filter((f) => f.status === "info");
312
+ this.log(import_chalk.default.bold("\nHigh Availability (HA) Audit"));
313
+ this.log("\u2500".repeat(40));
314
+ this.log(`No HA: ${noHA.length > 0 ? import_chalk.default.red(noHA.length) : import_chalk.default.green(0)}`);
315
+ this.log(`Partial: ${partial.length > 0 ? import_chalk.default.yellow(partial.length) : import_chalk.default.green(0)}`);
316
+ this.log(`HA OK: ${import_chalk.default.green(haOk.length)}`);
317
+ this.log("");
318
+ for (const f of noHA) {
319
+ this.log(`${import_chalk.default.red("\u2717 [NO HA]")} ${f.title}`);
320
+ }
321
+ for (const f of partial) {
322
+ this.log(`${import_chalk.default.yellow("\u26A0 [PARTIAL HA]")} ${f.title}`);
323
+ }
324
+ for (const f of infos) {
325
+ this.log(`${import_chalk.default.yellow("\u26A0 [WARNING]")} ${f.title}`);
326
+ }
327
+ for (const f of haOk) {
328
+ this.log(`${import_chalk.default.green("\u2713 [HA OK]")} ${f.title}`);
329
+ }
330
+ let md = `# High Availability (HA) Audit Report \u2014 ${config.name}
331
+ `;
332
+ md += `Date: ${today()}
333
+
334
+ `;
335
+ md += `## Summary
336
+ `;
337
+ md += `- No HA: ${noHA.length} resources
338
+ `;
339
+ md += `- Partial HA: ${partial.length} resources
340
+ `;
341
+ md += `- HA OK: ${haOk.length} resources
342
+
343
+ `;
344
+ md += `## Findings
345
+
346
+ `;
347
+ for (const f of [...noHA, ...partial, ...infos]) {
348
+ const label = f.status === "no-ha" ? "NO HA" : f.status === "ha-partial" ? "PARTIAL HA" : "WARNING";
349
+ md += `### [${label}] ${f.title}
350
+ `;
351
+ md += `Stack: ${f.stackName}
352
+ `;
353
+ md += `${f.detail}
354
+ `;
355
+ if (f.recommendation) md += `Recommendation: ${f.recommendation}
356
+ `;
357
+ md += "\n";
358
+ }
359
+ if (haOk.length > 0) {
360
+ md += `## Resources with HA
361
+ `;
362
+ for (const f of haOk) {
363
+ md += `- ${f.constructType} '${f.constructId}' \u2014 HA OK
364
+ `;
365
+ }
366
+ md += "\n";
367
+ }
368
+ const relPath = saveReport(cwd, "ha", md);
369
+ this.log(`
370
+ Report saved to ${relPath}`);
371
+ if (shouldFail(failOn, noHA.length, partial.length + infos.length)) {
372
+ this.exit(1);
373
+ }
374
+ }
375
+ };