@aaa-sentinel/iac-cost 0.3.0 → 0.3.1
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 +7 -2
- package/dist/cli.js +96 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,9 @@ internet is caught in review instead of after the deploy. With `iac-cost fix`, t
|
|
|
7
7
|
your last scan on resources this Terraform manages come back to you as **pull requests**, not as
|
|
8
8
|
console changes (see [Fixes as pull requests](#fixes-as-pull-requests)).
|
|
9
9
|
|
|
10
|
+
> **Changed in 0.3.1:** the pull requests of `fix` pass `terraform fmt -check`: an added attribute is
|
|
11
|
+
> aligned with its group (also around heredocs and after removing a block), as `terraform fmt` does.
|
|
12
|
+
|
|
10
13
|
> **New in 0.3.0:** the `fix` subcommand. Nothing changes for the existing command: same output,
|
|
11
14
|
> same exit codes.
|
|
12
15
|
|
|
@@ -456,8 +459,10 @@ the reason, and the app shows it that way:
|
|
|
456
459
|
| `rule_not_automated` | No deterministic code fix for that rule |
|
|
457
460
|
|
|
458
461
|
The rest of the file is kept byte for byte, comments and line endings included (a removed block takes
|
|
459
|
-
the comment lines right above it, up to the first blank line)
|
|
460
|
-
|
|
462
|
+
the comment lines right above it, up to the first blank line). An added attribute is aligned like
|
|
463
|
+
`terraform fmt` does: if its group of consecutive one-line attributes was already aligned, the whole
|
|
464
|
+
group is realigned to the longest name, so the pull request passes `terraform fmt -check`; if the
|
|
465
|
+
group was not formatted, only the new line is written as `name = value`. The pull request says what changed and why;
|
|
461
466
|
review its `terraform plan` before merging. The finding closes after `terraform apply` and the next scan.
|
|
462
467
|
|
|
463
468
|
| Option | Meaning |
|
package/dist/cli.js
CHANGED
|
@@ -7100,8 +7100,68 @@ function removeListItems(text, attribute, list, remove) {
|
|
|
7100
7100
|
const body2 = keep.map((item) => `${itemIndent}${text.slice(item.start, item.end)},${eol}`).join("");
|
|
7101
7101
|
return { start: attribute.valueStart, end: attribute.valueEnd, text: `[${eol}${body2}${closeIndent}]` };
|
|
7102
7102
|
}
|
|
7103
|
+
function singleLine(text, item) {
|
|
7104
|
+
if (item.kind !== "attribute") return false;
|
|
7105
|
+
return !text.slice(item.start, item.valueEnd).includes("\n") || text.startsWith("<<", item.valueStart);
|
|
7106
|
+
}
|
|
7107
|
+
function adjacent(text, previous, next) {
|
|
7108
|
+
const gap = text.slice(previous.valueEnd, next.start);
|
|
7109
|
+
const eol = gap.indexOf("\n");
|
|
7110
|
+
if (eol === -1 || gap.indexOf("\n", eol + 1) !== -1) return false;
|
|
7111
|
+
const tail = gap.slice(0, eol).trim();
|
|
7112
|
+
const comment = tail === "" || tail.startsWith("#") || tail.startsWith("//") || tail.startsWith("/*") && tail.endsWith("*/");
|
|
7113
|
+
return comment && gap.slice(eol + 1).trim() === "";
|
|
7114
|
+
}
|
|
7115
|
+
function alignmentGroups(text, block) {
|
|
7116
|
+
const groups = [];
|
|
7117
|
+
let current = [];
|
|
7118
|
+
for (const item of blockBody(text, block)) {
|
|
7119
|
+
const last = current[current.length - 1];
|
|
7120
|
+
if (singleLine(text, item) && last && adjacent(text, last, item)) {
|
|
7121
|
+
current.push(item);
|
|
7122
|
+
continue;
|
|
7123
|
+
}
|
|
7124
|
+
if (current.length > 0) groups.push(current);
|
|
7125
|
+
current = singleLine(text, item) ? [item] : [];
|
|
7126
|
+
}
|
|
7127
|
+
if (current.length > 0) groups.push(current);
|
|
7128
|
+
return groups;
|
|
7129
|
+
}
|
|
7130
|
+
function equalsSpan(text, attribute) {
|
|
7131
|
+
return { start: attribute.start + attribute.name.length, end: attribute.valueStart };
|
|
7132
|
+
}
|
|
7133
|
+
function fmtAligned(text, group) {
|
|
7134
|
+
const width = Math.max(...group.map((attribute) => attribute.name.length));
|
|
7135
|
+
return group.every((attribute) => {
|
|
7136
|
+
const span = equalsSpan(text, attribute);
|
|
7137
|
+
return text.slice(span.start, span.end) === `${" ".repeat(width - attribute.name.length + 1)}= `;
|
|
7138
|
+
});
|
|
7139
|
+
}
|
|
7140
|
+
function alignAttributes(text, block, names, original) {
|
|
7141
|
+
const edits = [];
|
|
7142
|
+
const before = original ? alignmentGroups(original.text, original.block) : null;
|
|
7143
|
+
for (const group of alignmentGroups(text, block)) {
|
|
7144
|
+
const targets = group.filter((attribute) => names.includes(attribute.name));
|
|
7145
|
+
if (targets.length === 0) continue;
|
|
7146
|
+
let aligned;
|
|
7147
|
+
if (before && original) {
|
|
7148
|
+
const contributing = new Set(group.flatMap((attribute) => before.filter((old) => old.some((a) => a.name === attribute.name))));
|
|
7149
|
+
aligned = [...contributing].every((old) => fmtAligned(original.text, old));
|
|
7150
|
+
} else {
|
|
7151
|
+
const others = group.filter((attribute) => !names.includes(attribute.name));
|
|
7152
|
+
aligned = others.length === 0 || fmtAligned(text, others);
|
|
7153
|
+
}
|
|
7154
|
+
const width = Math.max(...group.map((attribute) => attribute.name.length));
|
|
7155
|
+
for (const attribute of aligned ? group : targets) {
|
|
7156
|
+
const wanted = `${" ".repeat((aligned ? width : attribute.name.length) - attribute.name.length + 1)}= `;
|
|
7157
|
+
const span = equalsSpan(text, attribute);
|
|
7158
|
+
if (text.slice(span.start, span.end) !== wanted) edits.push({ ...span, text: wanted });
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
return edits;
|
|
7162
|
+
}
|
|
7103
7163
|
function applyEdits(text, edits) {
|
|
7104
|
-
const sorted =
|
|
7164
|
+
const sorted = edits.map((edit2, i) => ({ edit: edit2, i })).sort((a, b) => b.edit.start - a.edit.start || b.edit.end - a.edit.end || b.i - a.i).map(({ edit: edit2 }) => edit2);
|
|
7105
7165
|
let out = text;
|
|
7106
7166
|
let limit = Number.POSITIVE_INFINITY;
|
|
7107
7167
|
for (const edit2 of sorted) {
|
|
@@ -8244,6 +8304,19 @@ var ruleTitle = (id) => RULE_TITLES[id] ?? id;
|
|
|
8244
8304
|
var comboTitle = (id) => COMBO_TITLES[id] ?? id;
|
|
8245
8305
|
|
|
8246
8306
|
// src/fix/catalog.ts
|
|
8307
|
+
function finalizePatch(patch) {
|
|
8308
|
+
const find = (source, target) => parseBody(source, 0, source.length).find(
|
|
8309
|
+
(item) => item.kind === "block" && item.type === "resource" && item.labels[0] === target.type && item.labels[1] === target.name
|
|
8310
|
+
);
|
|
8311
|
+
let text = applyEdits(patch.original, patch.edits);
|
|
8312
|
+
for (const target of patch.align) {
|
|
8313
|
+
const block = find(text, target);
|
|
8314
|
+
if (!block) continue;
|
|
8315
|
+
const before = find(patch.original, target);
|
|
8316
|
+
text = applyEdits(text, alignAttributes(text, block, target.attributes, before ? { text: patch.original, block: before } : void 0));
|
|
8317
|
+
}
|
|
8318
|
+
return text;
|
|
8319
|
+
}
|
|
8247
8320
|
var OPEN_CIDRS = /* @__PURE__ */ new Set(["0.0.0.0/0", "::/0"]);
|
|
8248
8321
|
var ADMIN_PORTS = [22, 3389];
|
|
8249
8322
|
var Suggest = class extends Error {
|
|
@@ -8295,10 +8368,16 @@ var PatchBuilder = class {
|
|
|
8295
8368
|
byFile = /* @__PURE__ */ new Map();
|
|
8296
8369
|
changes = [];
|
|
8297
8370
|
first = null;
|
|
8298
|
-
add(where, edit2, attribute, change, lifecycleName = attribute) {
|
|
8371
|
+
add(where, edit2, attribute, change, lifecycleName = attribute, alignName = null) {
|
|
8299
8372
|
if (lifecycleName !== null && ignoresChanges(where.text, where.block, lifecycleName)) throw new Suggest("ignore_changes", lifecycleName);
|
|
8300
|
-
const patch = this.byFile.get(where.absolutePath) ?? { file: where.file, absolutePath: where.absolutePath, original: where.text, edits: [] };
|
|
8373
|
+
const patch = this.byFile.get(where.absolutePath) ?? { file: where.file, absolutePath: where.absolutePath, original: where.text, edits: [], align: [] };
|
|
8301
8374
|
patch.edits.push(edit2);
|
|
8375
|
+
const [type, name] = where.block.labels;
|
|
8376
|
+
if (alignName !== null && type && name) {
|
|
8377
|
+
const target = patch.align.find((a) => a.type === type && a.name === name);
|
|
8378
|
+
if (target) target.attributes.push(alignName);
|
|
8379
|
+
else patch.align.push({ type, name, attributes: [alignName] });
|
|
8380
|
+
}
|
|
8302
8381
|
this.byFile.set(where.absolutePath, patch);
|
|
8303
8382
|
this.changes.push(change);
|
|
8304
8383
|
this.first ??= { file: where.file, attribute };
|
|
@@ -8327,7 +8406,10 @@ function setLiteral(builder, where, items, name, wanted, ok, insertWhenAbsent =
|
|
|
8327
8406
|
where,
|
|
8328
8407
|
edit(() => setAttribute(where.text, where.block, name, wanted), name),
|
|
8329
8408
|
name,
|
|
8330
|
-
|
|
8409
|
+
// Solo se alinea lo añadido: cambiar un valor no mueve el `=` de nadie.
|
|
8410
|
+
before === null ? `add \`${name} = ${wanted}\`` : `\`${name}\`: \`${before}\` \u2192 \`${wanted}\``,
|
|
8411
|
+
name,
|
|
8412
|
+
before === null ? name : null
|
|
8331
8413
|
);
|
|
8332
8414
|
return "changed";
|
|
8333
8415
|
}
|
|
@@ -8439,21 +8521,25 @@ function awsAdminOpen(ctx, builder) {
|
|
|
8439
8521
|
if (blocksOf(items, "dynamic").some((block) => block.labels[0] === "ingress")) throw new Suggest("dynamic_block", "ingress");
|
|
8440
8522
|
if (attributeOf(items, "ingress")) throw new Suggest("value_from_expression", "ingress");
|
|
8441
8523
|
let judged = 0;
|
|
8442
|
-
const apply = (target, block, blockItems, shape, label) => {
|
|
8524
|
+
const apply = (target, block, blockItems, shape, label, alignName = null) => {
|
|
8443
8525
|
const verdict = judgeRule(target.text, block, blockItems, shape, label);
|
|
8444
8526
|
if (!verdict.open) return false;
|
|
8445
8527
|
judged += 1;
|
|
8446
8528
|
const lifecycleName = shape === INLINE_RULE ? "ingress" : verdict.removesBlock ? null : verdict.attribute;
|
|
8447
|
-
verdict.edits.forEach(
|
|
8529
|
+
verdict.edits.forEach(
|
|
8530
|
+
(fn, i) => builder.add(target, edit(fn, verdict.attribute), verdict.attribute, verdict.changes[i] ?? verdict.changes[0] ?? "", lifecycleName, verdict.removesBlock ? alignName : null)
|
|
8531
|
+
);
|
|
8448
8532
|
return verdict.removesBlock;
|
|
8449
8533
|
};
|
|
8450
8534
|
const inline = blocksOf(items, "ingress");
|
|
8451
8535
|
let inlineRemoved = 0;
|
|
8452
8536
|
for (const ingress of inline) {
|
|
8453
|
-
|
|
8537
|
+
const before = items[items.indexOf(ingress) - 1];
|
|
8538
|
+
const alignName = before?.kind === "attribute" ? before.name : null;
|
|
8539
|
+
if (apply(where, ingress, body(where.text, ingress), INLINE_RULE, `the \`ingress\` block of \`${anchor2.address}\``, alignName)) inlineRemoved += 1;
|
|
8454
8540
|
}
|
|
8455
8541
|
if (inline.length > 0 && inlineRemoved === inline.length) {
|
|
8456
|
-
builder.add(where, edit(() => setAttribute(where.text, where.block, "ingress", "[]"), "ingress"), "ingress", "set `ingress = []` so Terraform removes the rule instead of leaving it unmanaged");
|
|
8542
|
+
builder.add(where, edit(() => setAttribute(where.text, where.block, "ingress", "[]"), "ingress"), "ingress", "set `ingress = []` so Terraform removes the rule instead of leaving it unmanaged", "ingress", "ingress");
|
|
8457
8543
|
}
|
|
8458
8544
|
const parents = ["aws_security_group", "aws_default_security_group"];
|
|
8459
8545
|
const children = [
|
|
@@ -8743,7 +8829,7 @@ async function runFix(options, deps) {
|
|
|
8743
8829
|
if (outcome.kind === "patch") {
|
|
8744
8830
|
try {
|
|
8745
8831
|
for (const patch2 of outcome.patches) {
|
|
8746
|
-
const patched =
|
|
8832
|
+
const patched = finalizePatch(patch2);
|
|
8747
8833
|
parseBody(patched, 0, patched.length);
|
|
8748
8834
|
}
|
|
8749
8835
|
} catch (error2) {
|
|
@@ -8834,7 +8920,7 @@ async function runFix(options, deps) {
|
|
|
8834
8920
|
bases.push(atBase);
|
|
8835
8921
|
}
|
|
8836
8922
|
const files = patch.patches.map((file, i) => {
|
|
8837
|
-
const patched =
|
|
8923
|
+
const patched = finalizePatch(file);
|
|
8838
8924
|
const atBase = bases[i] ?? file.original;
|
|
8839
8925
|
return { path: file.file, content: atBase === file.original ? patched : likeBase(patched, atBase) };
|
|
8840
8926
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaa-sentinel/iac-cost",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Estimate the monthly cost delta of a Terraform plan, check it against the AAA Sentinel security rules and open security fixes as pull requests from your CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|