@intentius/chant-lexicon-terraform 0.58.0 → 0.60.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/README.md +9 -0
- package/dist/composites/terraform-adopt-op.d.ts +7 -2
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.d.ts +8 -2
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +30 -7
- package/dist/composites/terraform-watch-op.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +14 -0
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/integrity.json +7 -5
- package/dist/lint/audit-catalog.d.ts +2 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +39 -12
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/tf001.d.ts +40 -6
- package/dist/lint/post-synth/tf001.d.ts.map +1 -1
- package/dist/lint/post-synth/tf015.d.ts +11 -2
- package/dist/lint/post-synth/tf015.d.ts.map +1 -1
- package/dist/lint/post-synth/tf026.d.ts +7 -0
- package/dist/lint/post-synth/tf026.d.ts.map +1 -1
- package/dist/lint/post-synth/tf027.d.ts +30 -0
- package/dist/lint/post-synth/tf027.d.ts.map +1 -0
- package/dist/lint/post-synth/tf028.d.ts +33 -0
- package/dist/lint/post-synth/tf028.d.ts.map +1 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +2 -0
- package/dist/okf/rules/TF027.md +11 -0
- package/dist/okf/rules/TF028.md +11 -0
- package/dist/op/activities/terraform.d.ts +32 -11
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/builders.d.ts +7 -5
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +29 -15
- package/dist/op/resolve-root-mode.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/tf001.ts +70 -14
- package/dist/rules/tf015.ts +11 -2
- package/dist/rules/tf026.ts +6 -2
- package/dist/rules/tf027.ts +74 -0
- package/dist/rules/tf028.ts +96 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__fixtures__/ACCEPTANCE.md +181 -0
- package/src/__fixtures__/remote-backend/main.tf +34 -0
- package/src/__fixtures__/with-backend/main.tf +7 -0
- package/src/codegen/lint-rules-page.test.ts +18 -1
- package/src/composites/terraform-adopt-op.acceptance.test.ts +46 -19
- package/src/composites/terraform-adopt-op.test.ts +9 -4
- package/src/composites/terraform-adopt-op.ts +8 -3
- package/src/composites/terraform-apply-op.acceptance.test.ts +14 -9
- package/src/composites/terraform-apply-op.test.ts +13 -5
- package/src/composites/terraform-apply-op.ts +23 -7
- package/src/composites/terraform-watch-op.test.ts +85 -4
- package/src/composites/terraform-watch-op.ts +59 -8
- package/src/hcl/parse.ts +20 -0
- package/src/lint/audit-catalog.ts +26 -2
- package/src/lint/audit-lineage.ts +79 -14
- package/src/lint/audit.test.ts +171 -3
- package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +4 -2
- package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
- package/src/lint/post-synth/fixtures/TF014/negative/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF014/positive/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
- package/src/lint/post-synth/module-rules.test.ts +1 -1
- package/src/lint/post-synth/post-synth.test.ts +64 -6
- package/src/lint/post-synth/tf001.ts +70 -14
- package/src/lint/post-synth/tf015.ts +11 -2
- package/src/lint/post-synth/tf026.ts +6 -2
- package/src/lint/post-synth/tf027.test.ts +62 -0
- package/src/lint/post-synth/tf027.ts +74 -0
- package/src/lint/post-synth/tf028.test.ts +92 -0
- package/src/lint/post-synth/tf028.ts +96 -0
- package/src/lsp/option-keys.ts +3 -2
- package/src/op/activities/choudoufu.acceptance.test.ts +7 -23
- package/src/op/activities/choudoufu.test.ts +49 -17
- package/src/op/activities/terraform.ts +67 -29
- package/src/op/builders.ts +7 -5
- package/src/op/resolve-root-mode.ts +29 -15
- package/src/plugin.test.ts +38 -0
- package/src/plugin.ts +122 -31
- package/src/validate.ts +10 -1
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* only the `-no-color` plan text can reach an issue or PR body.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, describe, test, expect } from "vitest";
|
|
8
11
|
import {
|
|
9
12
|
isStepOutputRef,
|
|
10
13
|
collectStepOutputRefs,
|
|
@@ -71,7 +74,7 @@ describe("TerraformWatchOp phases (#2087)", () => {
|
|
|
71
74
|
});
|
|
72
75
|
|
|
73
76
|
describe("TerraformWatchOp finding modes (#2087)", () => {
|
|
74
|
-
const findingStep = (mode: "issue" | "pull-request"): ActivityStep => {
|
|
77
|
+
const findingStep = (mode: "issue" | "comment" | "pull-request"): ActivityStep => {
|
|
75
78
|
const op = props({ name: "app-watch", root: "app", findingMode: mode });
|
|
76
79
|
expect(phaseNames(op)).toEqual(["Init", "Plan", "Report"]);
|
|
77
80
|
return op.phases[2].steps[0] as ActivityStep;
|
|
@@ -92,6 +95,14 @@ describe("TerraformWatchOp finding modes (#2087)", () => {
|
|
|
92
95
|
expect(step.outcomeAttribute).toEqual({ name: "PR", from: "prUrl" });
|
|
93
96
|
});
|
|
94
97
|
|
|
98
|
+
test("comment mode posts on the triggering pull request and surfaces the comment URL (#2231)", () => {
|
|
99
|
+
const step = findingStep("comment");
|
|
100
|
+
expect(step.fn).toBe("reconcilePr");
|
|
101
|
+
expect(step.args?.mode).toBe("comment");
|
|
102
|
+
expect(step.args?.env).toBe("app");
|
|
103
|
+
expect(step.outcomeAttribute).toEqual({ name: "Comment", from: "commentUrl" });
|
|
104
|
+
});
|
|
105
|
+
|
|
95
106
|
test("the finding step derives no plan of its own", () => {
|
|
96
107
|
// `entries: []` is what stops reconcilePr shelling to `chant lifecycle
|
|
97
108
|
// plan --json`: the finding is already written by the time it runs.
|
|
@@ -114,7 +125,7 @@ describe("TerraformWatchOp finding modes (#2087)", () => {
|
|
|
114
125
|
});
|
|
115
126
|
|
|
116
127
|
describe("TerraformWatchOp posts the human plan and never the plan JSON (#2087)", () => {
|
|
117
|
-
for (const mode of ["issue", "pull-request"] as const) {
|
|
128
|
+
for (const mode of ["issue", "comment", "pull-request"] as const) {
|
|
118
129
|
test(`${mode}: the body is a reference to the Plan step's -no-color text`, () => {
|
|
119
130
|
const op = props({ name: "app-watch", root: "app", findingMode: mode });
|
|
120
131
|
const report = op.phases[2].steps[0] as ActivityStep;
|
|
@@ -192,7 +203,7 @@ describe("TerraformWatchOp on a live root (#2105)", () => {
|
|
|
192
203
|
expect(plan.outcomeAttribute).toEqual({ name: "Drift", from: "changed" });
|
|
193
204
|
});
|
|
194
205
|
|
|
195
|
-
for (const mode of ["issue", "pull-request"] as const) {
|
|
206
|
+
for (const mode of ["issue", "comment", "pull-request"] as const) {
|
|
196
207
|
test(`${mode}: the body is the plan text plus the adoption ledger, as one field`, () => {
|
|
197
208
|
const op = live({ findingMode: mode });
|
|
198
209
|
expect(phaseNames(op)).toEqual(["Init", "Plan", "Report"]);
|
|
@@ -241,3 +252,73 @@ describe("TerraformWatchOp schedule (#2087, #2120)", () => {
|
|
|
241
252
|
});
|
|
242
253
|
});
|
|
243
254
|
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The `live` flag against the root it names (#2216). Best-effort: the mode
|
|
258
|
+
* resolves from a `chant.config.json` only, so these projects are written in
|
|
259
|
+
* that format. TF028 (`../lint/post-synth/tf028.ts`) is what reports the same
|
|
260
|
+
* mismatch on a `chant.config.ts` project, where the mode does not resolve
|
|
261
|
+
* here at all.
|
|
262
|
+
*/
|
|
263
|
+
describe("TerraformWatchOp cross-checks live against the root's mode (#2216)", () => {
|
|
264
|
+
const dirs: string[] = [];
|
|
265
|
+
|
|
266
|
+
afterEach(() => {
|
|
267
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const LIVE_ROOT = [
|
|
271
|
+
"terraform {",
|
|
272
|
+
" live {",
|
|
273
|
+
' estate = "prod-estate"',
|
|
274
|
+
" }",
|
|
275
|
+
"}",
|
|
276
|
+
"",
|
|
277
|
+
].join("\n");
|
|
278
|
+
|
|
279
|
+
/** A project on a `chant.config.json` whose root `estate` is `mainTf`, run on `binary`. */
|
|
280
|
+
function project(binary: string, mainTf: string): string {
|
|
281
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-watch-mode-"));
|
|
282
|
+
dirs.push(dir);
|
|
283
|
+
mkdirSync(join(dir, "estate"), { recursive: true });
|
|
284
|
+
writeFileSync(join(dir, "estate", "main.tf"), mainTf);
|
|
285
|
+
writeFileSync(
|
|
286
|
+
join(dir, "chant.config.json"),
|
|
287
|
+
JSON.stringify({ terraform: { binary, roots: { estate: { dir: "./estate" } } } }),
|
|
288
|
+
);
|
|
289
|
+
return dir;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
test("refuses a live root watched in stock mode, naming the Op and the root", () => {
|
|
293
|
+
const cwd = project("choudoufu", LIVE_ROOT);
|
|
294
|
+
expect(() => TerraformWatchOp({ name: "estate-watch", root: "estate", cwd })).toThrow(
|
|
295
|
+
/TerraformWatchOp "estate-watch": root "estate" runs choudoufu with a declared estate/,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("accepts the same root with live: true", () => {
|
|
300
|
+
const cwd = project("choudoufu", LIVE_ROOT);
|
|
301
|
+
expect(() => TerraformWatchOp({ name: "estate-watch", root: "estate", live: true, cwd })).not.toThrow();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("refuses live: true on a root that declares no estate", () => {
|
|
305
|
+
const cwd = project("choudoufu", 'resource "null_resource" "first" {}\n');
|
|
306
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd })).toThrow(
|
|
307
|
+
/declares no estate/,
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("refuses live: true when the binary is not choudoufu, whatever the root declares", () => {
|
|
312
|
+
const cwd = project("terraform", LIVE_ROOT);
|
|
313
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd })).toThrow(
|
|
314
|
+
/declares no estate/,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("says nothing when the mode does not resolve: no config to read", () => {
|
|
319
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-watch-mode-"));
|
|
320
|
+
dirs.push(dir);
|
|
321
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", cwd: dir })).not.toThrow();
|
|
322
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd: dir })).not.toThrow();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -51,16 +51,20 @@
|
|
|
51
51
|
*
|
|
52
52
|
* ## Findings reuse `reconcilePr`
|
|
53
53
|
*
|
|
54
|
-
* `issue` and `pull-request` call core's `reconcilePr` activity,
|
|
55
|
-
* in chant that shells to `gh
|
|
56
|
-
*
|
|
54
|
+
* `issue`, `comment` and `pull-request` call core's `reconcilePr` activity,
|
|
55
|
+
* the one place in chant that shells to `gh`, rather than growing a second
|
|
56
|
+
* copy of those calls here.
|
|
57
57
|
* That activity built its own change-set summary and had no way to be handed
|
|
58
58
|
* one, so #2087 added a single field to it (`ReconcilePrArgs.body`), which is
|
|
59
59
|
* what carries the `-no-color` plan through. Note that the pull-request mode
|
|
60
60
|
* of `reconcilePr` regenerates chant TypeScript via `chant import`; a
|
|
61
61
|
* terraform-native regeneration of HCL from live state is #2089, so
|
|
62
62
|
* `findingMode: "issue"` is the mode with an end-to-end answer and
|
|
63
|
-
* `"pull-request"` opens a PR whose body is the plan.
|
|
63
|
+
* `"pull-request"` opens a PR whose body is the plan. `"comment"` (#2231) is
|
|
64
|
+
* the mode for the plan-on-pull-request shape: the plan is posted onto the
|
|
65
|
+
* pull request that triggered the run and re-edited there on the next push,
|
|
66
|
+
* which is where a reviewer is already looking. It needs that trigger; a run
|
|
67
|
+
* without one fails the Report step by name rather than posting elsewhere. On a live root #2089's
|
|
64
68
|
* question has a different answer rather than a pending one, and it is not
|
|
65
69
|
* regeneration: see `TerraformAdoptOp`.
|
|
66
70
|
*
|
|
@@ -80,6 +84,13 @@
|
|
|
80
84
|
* // one-shot, local executor: chant run app-watch
|
|
81
85
|
* export const { op } = TerraformWatchOp({ name: "app-watch", root: "app" });
|
|
82
86
|
*
|
|
87
|
+
* // the plan as a sticky comment on the pull request that triggered the run
|
|
88
|
+
* export const { op } = TerraformWatchOp({
|
|
89
|
+
* name: "app-plan",
|
|
90
|
+
* root: "app",
|
|
91
|
+
* findingMode: "comment",
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
83
94
|
* // nightly, opening an issue when the plan is non-empty
|
|
84
95
|
* export const { op } = TerraformWatchOp({
|
|
85
96
|
* name: "app-watch",
|
|
@@ -99,6 +110,7 @@
|
|
|
99
110
|
*/
|
|
100
111
|
|
|
101
112
|
import { Op, phase, OpResource, type ActivityStep } from "@intentius/chant/op";
|
|
113
|
+
import { resolveRootModeSync } from "../op/resolve-root-mode";
|
|
102
114
|
import { CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL, DEFAULT_PLAN_FILE } from "../op/activities/terraform";
|
|
103
115
|
import {
|
|
104
116
|
terraformInit as initStep,
|
|
@@ -110,8 +122,14 @@ import {
|
|
|
110
122
|
* What to do with a non-empty plan. A subset of core's `OpFindingMode`
|
|
111
123
|
* (`packages/core/src/lexicon.ts`): `merge-request` is GitLab's spelling of
|
|
112
124
|
* `pull-request` and is produced by the CI generator, not chosen here.
|
|
125
|
+
*
|
|
126
|
+
* `comment` (#2231) is the mode a plan-on-pull-request Op wants: the plan
|
|
127
|
+
* lands as one comment on the pull request that triggered the run and the
|
|
128
|
+
* next push edits that same comment. It needs a `pull_request` trigger on the
|
|
129
|
+
* `ScheduledOpSpec` the workflow is generated from, and the github generator
|
|
130
|
+
* refuses it by name on any other trigger.
|
|
113
131
|
*/
|
|
114
|
-
export type TerraformFindingMode = "report" | "issue" | "pull-request";
|
|
132
|
+
export type TerraformFindingMode = "report" | "issue" | "comment" | "pull-request";
|
|
115
133
|
|
|
116
134
|
export interface TerraformWatchOpConfig {
|
|
117
135
|
/** Op name (kebab-case). */
|
|
@@ -138,8 +156,14 @@ export interface TerraformWatchOpConfig {
|
|
|
138
156
|
* Declared here rather than detected, because a composite is built by
|
|
139
157
|
* `chant build` without reading a root module's `.tf` files. The activities
|
|
140
158
|
* detect the estate at run time (`op/activities/live-detect.ts`), but by then
|
|
141
|
-
* the phases are already serialized.
|
|
142
|
-
*
|
|
159
|
+
* the phases are already serialized.
|
|
160
|
+
*
|
|
161
|
+
* Two things check the answer against the root's real mode, since a wrong
|
|
162
|
+
* one is otherwise silent (#2216). This function refuses a mismatch outright
|
|
163
|
+
* when it can resolve the mode synchronously, which is a
|
|
164
|
+
* `chant.config.json` project (`op/resolve-root-mode.ts`); TF028
|
|
165
|
+
* (`lint/post-synth/tf028.ts`) reports the same mismatch off the parsed HCL
|
|
166
|
+
* for every project, `chant.config.ts` included.
|
|
143
167
|
*/
|
|
144
168
|
live?: boolean;
|
|
145
169
|
/**
|
|
@@ -193,6 +217,29 @@ export function TerraformWatchOp(config: TerraformWatchOpConfig): TerraformWatch
|
|
|
193
217
|
);
|
|
194
218
|
}
|
|
195
219
|
|
|
220
|
+
// The `live` flag against the root it names. Best-effort and synchronous:
|
|
221
|
+
// `resolveRootModeSync` reads a `chant.config.json` only and returns
|
|
222
|
+
// undefined for anything else, so a `chant.config.ts` project reaches
|
|
223
|
+
// neither branch and TF028 is what reports the mismatch there. Where the
|
|
224
|
+
// mode does resolve, refusing at build time names the Op, which a
|
|
225
|
+
// post-synth diagnostic does too but a run-time failure at the first step
|
|
226
|
+
// does not.
|
|
227
|
+
const resolvedMode = resolveRootModeSync(config.root, config.cwd)?.mode;
|
|
228
|
+
if (resolvedMode === "live" && !config.live) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`TerraformWatchOp "${config.name}": root "${config.root}" runs choudoufu with a declared estate, ` +
|
|
231
|
+
"but this Op watches it in stock mode. A stock plan reports drift alone and stays quiet about " +
|
|
232
|
+
"every unowned and adoptable resource in the estate. Set live: true.",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (resolvedMode === "state" && config.live) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`TerraformWatchOp "${config.name}": live: true, but root "${config.root}" declares no estate (or ` +
|
|
238
|
+
"terraform.binary is not \"choudoufu\"), so there is no live system to plan against. Drop " +
|
|
239
|
+
"live, or declare the root's estate.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
196
243
|
// `id` is what makes `plan.out` legal, and one field of `plan.out` is the
|
|
197
244
|
// only channel by which any part of the plan leaves this Op.
|
|
198
245
|
//
|
|
@@ -238,7 +285,11 @@ export function TerraformWatchOp(config: TerraformWatchOpConfig): TerraformWatch
|
|
|
238
285
|
...(config.branch ? { branch: config.branch } : {}),
|
|
239
286
|
},
|
|
240
287
|
outcomeAttribute:
|
|
241
|
-
findingMode === "pull-request"
|
|
288
|
+
findingMode === "pull-request"
|
|
289
|
+
? { name: "PR", from: "prUrl" }
|
|
290
|
+
: findingMode === "comment"
|
|
291
|
+
? { name: "Comment", from: "commentUrl" }
|
|
292
|
+
: { name: "Issue", from: "issueUrl" },
|
|
242
293
|
};
|
|
243
294
|
phases.push(phase("Report", [report]));
|
|
244
295
|
}
|
package/src/hcl/parse.ts
CHANGED
|
@@ -436,6 +436,26 @@ export async function parseTerraformRootContent(
|
|
|
436
436
|
return blocksToEntities(splitBundleContent(content), root, hcl2json, modeOptions);
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
/** The root name an audit of the audited directory itself parses under. */
|
|
440
|
+
export const AUDIT_ROOT_NAME = "audit-root";
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The root name one `chant audit` input parses under (#2217).
|
|
444
|
+
*
|
|
445
|
+
* A build names its roots from `terraform.roots`; an audit has no such
|
|
446
|
+
* config, so the name comes from the input's own path, which is the one thing
|
|
447
|
+
* that tells two discovered roots apart. `envs/prod` becomes `envs.prod`,
|
|
448
|
+
* because an entity key is `<root>/<address>` with the module chain between
|
|
449
|
+
* them and a `/` inside the root name would read as a module scope. The
|
|
450
|
+
* audited directory itself keeps the name {@link AUDIT_ROOT_NAME}, so a
|
|
451
|
+
* single-root audit reads the way it did before an input path was available.
|
|
452
|
+
*/
|
|
453
|
+
export function auditRootName(path?: string): string {
|
|
454
|
+
const trimmed = (path ?? "").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
455
|
+
if (trimmed === "" || trimmed === ".") return AUDIT_ROOT_NAME;
|
|
456
|
+
return trimmed.replace(/\//g, ".");
|
|
457
|
+
}
|
|
458
|
+
|
|
439
459
|
/**
|
|
440
460
|
* The module scope an entity key names: the part before the address.
|
|
441
461
|
*
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Every post-synth check needs an entry or it contributes nothing to
|
|
5
5
|
* `chant audit`, silently, and `packages/core/src/audit/catalog.test.ts` fails.
|
|
6
6
|
*
|
|
7
|
-
* TF001
|
|
8
|
-
* never emitted output, so `yamlBased` is false for all
|
|
7
|
+
* TF001 and TF024 through TF028 all read the chant model (`ctx.entities`),
|
|
8
|
+
* never emitted output, so `yamlBased` is false for all of them. Prior-art
|
|
9
9
|
* lineage lives in ./audit-lineage.ts.
|
|
10
10
|
*/
|
|
11
11
|
import type { Authority, RuleMeta } from "@intentius/chant/audit/catalog";
|
|
@@ -69,6 +69,30 @@ export const terraformAuditCatalog: Record<string, RuleMeta> = {
|
|
|
69
69
|
"deleted after all.",
|
|
70
70
|
yamlBased: false,
|
|
71
71
|
},
|
|
72
|
+
TF027: {
|
|
73
|
+
id: "TF027",
|
|
74
|
+
tier: "merge-worthy",
|
|
75
|
+
fixKind: "guidance",
|
|
76
|
+
category: "correctness",
|
|
77
|
+
title: 'Live root\'s policy block sets undeclared_untagged = "delete"',
|
|
78
|
+
remediation:
|
|
79
|
+
'Remove undeclared_untagged = "delete" from the live root\'s policy block. chant never proposes ' +
|
|
80
|
+
"deleting a resource it does not own; narrow the estate's own ownership answer " +
|
|
81
|
+
"(undeclared_tagged) instead of the account's.",
|
|
82
|
+
yamlBased: false,
|
|
83
|
+
},
|
|
84
|
+
TF028: {
|
|
85
|
+
id: "TF028",
|
|
86
|
+
tier: "merge-worthy",
|
|
87
|
+
fixKind: "guidance",
|
|
88
|
+
category: "correctness",
|
|
89
|
+
title: "Live root is watched by a TerraformWatchOp built without live: true",
|
|
90
|
+
remediation:
|
|
91
|
+
"Set live: true on the TerraformWatchOp that names this root, so the Plan phase runs " +
|
|
92
|
+
"choudoufuLivePlan and reports the unowned and adoptable counts as well as drift. Drop the root's " +
|
|
93
|
+
"estate instead if it is not a live root.",
|
|
94
|
+
yamlBased: false,
|
|
95
|
+
},
|
|
72
96
|
TF002: {
|
|
73
97
|
id: "TF002",
|
|
74
98
|
tier: "merge-worthy",
|
|
@@ -42,26 +42,37 @@
|
|
|
42
42
|
* TF021) added `gcp-terraform-best-practices`, the last row of the table below
|
|
43
43
|
* that no shipped rule had cited yet.
|
|
44
44
|
*
|
|
45
|
+
* Each row's Sweep note says whether `scripts/prior-art-sweep.ts` can index the
|
|
46
|
+
* source and, when it cannot, why (#2219). Five of these are indexed as of
|
|
47
|
+
* 2026-09-07: the three tflint rulesets, tfsec and the Sentinel policy set,
|
|
48
|
+
* 311 upstream rule ids in `scripts/prior-art/snapshot.json`. The rest are
|
|
49
|
+
* prose or code, so the tools they credit are re-read by hand when their
|
|
50
|
+
* documents change.
|
|
51
|
+
*
|
|
45
52
|
* | Key | Name | URL | Licence | Kind | Note |
|
|
46
53
|
* |---|---|---|---|---|---|
|
|
47
|
-
* | `tflint` | tflint | https://github.com/terraform-linters/tflint | MPL-2.0 (its `terraform` package is BUSL-1.1; binaries are bound by both) | scanner | The plugin host; ships no language rules of its own. |
|
|
48
|
-
* | `tflint-ruleset-terraform` | tflint-ruleset-terraform | https://github.com/terraform-linters/tflint-ruleset-terraform | MPL-2.0 | scanner | The official language-level ruleset, bundled into tflint. |
|
|
49
|
-
* | `tflint-ruleset-redeploy` | tflint-ruleset-redeploy | https://github.com/RedeployAB/tflint-ruleset-redeploy | 0BSD | scanner | Community ruleset; richest source of language-level rules outside the official set. |
|
|
50
|
-
* | `tflint-ruleset-avm` | tflint-ruleset-avm | https://github.com/Azure/tflint-ruleset-avm | MIT | scanner | Azure Verified Modules conformance; two of its 33 rules are provider-agnostic. |
|
|
51
|
-
* | `tfsec` | tfsec | https://github.com/aquasecurity/tfsec | MIT | scanner | Historical: not archived but effectively frozen (no release since 2025-05); its docs site 404s above v0.61.x, so cite pinned repository paths (`github.com/aquasecurity/tfsec/blob/master/docs/checks/...`), not the docs site. |
|
|
52
|
-
* | `trivy-checks` | trivy-checks | https://github.com/aquasecurity/trivy-checks | MIT (not the trivy scanner's own Apache-2.0; cite the checks repo's licence, not the engine's) | scanner | Where tfsec's rules live today (tfsec -> defsec -> trivy + trivy-checks). Cite rules by `long_id` (stable, human-readable); `AVD-*` ids are aliases now. |
|
|
53
|
-
* | `terraform-sentinel-policies` | HashiCorp reference Sentinel policies | https://github.com/hashicorp/terraform-sentinel-policies | MPL-2.0 | specification | HashiCorp's own example policy set; ships zero rules baked into HCP Terraform itself, so treat a cited policy as a documented pattern (`specification`) unless a future entry runs it as a real check (`scanner`); the `kind` union allows either. |
|
|
54
|
-
* | `choudoufu` | choudoufu | https://github.com/INTENTIUS/choudoufu | MPL-2.0 | scanner | Its `internal/live/lint` package; experimental, AWS-only. |
|
|
55
|
-
* | `hashicorp-style-guide` | HashiCorp Terraform style guide | https://developer.hashicorp.com/terraform/language/style | n/a | specification | |
|
|
56
|
-
* | `gcp-terraform-best-practices` | Google Cloud Terraform best-practices series | https://docs.cloud.google.com/docs/terraform/best-practices/general-style-structure | n/a | specification | A multi-page series; cite the specific page's fragment. |
|
|
57
|
-
* | `aws-terraform-prescriptive-guidance` | AWS prescriptive guidance for Terraform | https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/ | n/a | specification | |
|
|
54
|
+
* | `tflint` | tflint | https://github.com/terraform-linters/tflint | MPL-2.0 (its `terraform` package is BUSL-1.1; binaries are bound by both) | scanner | The plugin host; ships no language rules of its own. Sweep: not registered in `PRIOR_ART` (no rule cites it), so it is not swept. |
|
|
55
|
+
* | `tflint-ruleset-terraform` | tflint-ruleset-terraform | https://github.com/terraform-linters/tflint-ruleset-terraform | MPL-2.0 | scanner | The official language-level ruleset, bundled into tflint. Sweep: indexed from `docs/rules/README.md`, whose table links each rule's page (20 rules). |
|
|
56
|
+
* | `tflint-ruleset-redeploy` | tflint-ruleset-redeploy | https://github.com/RedeployAB/tflint-ruleset-redeploy | 0BSD | scanner | Community ruleset; richest source of language-level rules outside the official set. Sweep: indexed from `docs/rules/README.md`, same table shape (33 rules). |
|
|
57
|
+
* | `tflint-ruleset-avm` | tflint-ruleset-avm | https://github.com/Azure/tflint-ruleset-avm | MIT | scanner | Azure Verified Modules conformance; two of its 33 rules are provider-agnostic. Sweep: indexed from the repository-root `RULES.md`, since `docs/` holds pages for only two rules (33 rules). |
|
|
58
|
+
* | `tfsec` | tfsec | https://github.com/aquasecurity/tfsec | MIT | scanner | Historical: not archived but effectively frozen (no release since 2025-05); its docs site 404s above v0.61.x, so cite pinned repository paths (`github.com/aquasecurity/tfsec/blob/master/docs/checks/...`), not the docs site. Sweep: indexed from the root `rules.md` on `master`, a flat table of all 152 check ids, because `docs/checks` nests a directory per provider, service and check. |
|
|
59
|
+
* | `trivy-checks` | trivy-checks | https://github.com/aquasecurity/trivy-checks | MIT (not the trivy scanner's own Apache-2.0; cite the checks repo's licence, not the engine's) | scanner | Where tfsec's rules live today (tfsec -> defsec -> trivy + trivy-checks). Cite rules by `long_id` (stable, human-readable); `AVD-*` ids are aliases now. Sweep: not registered in `PRIOR_ART` (no rule cites it yet) and its `long_id`s live in Rego metadata under `checks/<kind>/<provider>/<service>/`, with no flat index. |
|
|
60
|
+
* | `terraform-sentinel-policies` | HashiCorp reference Sentinel policies | https://github.com/hashicorp/terraform-sentinel-policies | MPL-2.0 | specification | HashiCorp's own example policy set; ships zero rules baked into HCP Terraform itself, so treat a cited policy as a documented pattern (`specification`) unless a future entry runs it as a real check (`scanner`); the `kind` union allows either. Sweep: indexed by listing the five per-cloud directories, whose `.sentinel` filenames are the policy ids credits cite (73 policies); the repository publishes no index page. |
|
|
61
|
+
* | `choudoufu` | choudoufu | https://github.com/INTENTIUS/choudoufu | MPL-2.0 | scanner | Its `internal/live/lint` package; experimental, AWS-only. Sweep: unsweepable, its rule ids are Go constants in `internal/live/lint/issue.go` and it publishes no rule index. |
|
|
62
|
+
* | `hashicorp-style-guide` | HashiCorp Terraform style guide | https://developer.hashicorp.com/terraform/language/style | n/a | specification | Sweep: unsweepable, one prose page of recommendations with no rule ids; a credit quotes the sentence and links its fragment. |
|
|
63
|
+
* | `gcp-terraform-best-practices` | Google Cloud Terraform best-practices series | https://docs.cloud.google.com/docs/terraform/best-practices/general-style-structure | n/a | specification | A multi-page series; cite the specific page's fragment. Sweep: unsweepable, prose across several pages with no rule ids. |
|
|
64
|
+
* | `aws-terraform-prescriptive-guidance` | AWS prescriptive guidance for Terraform | https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/ | n/a | specification | Sweep: unsweepable, a prose guide whose sections carry headings rather than rule ids. |
|
|
58
65
|
*
|
|
59
66
|
* Deliberately excluded, per the survey: Terrascan and regula (both archived,
|
|
60
67
|
* do not build on dead projects); Snyk IaC (a product, not a rule source);
|
|
61
68
|
* semgrep (no SPDX id: its engine is LGPL-2.1 but its rules ship under the
|
|
62
69
|
* proprietary Semgrep Rules License v1.0, which forbids redistribution; cite
|
|
63
70
|
* it by name in prose if a rule's idea comes from it, never register it in
|
|
64
|
-
* `PRIOR_ART` with an SPDX licence).
|
|
71
|
+
* `PRIOR_ART` with an SPDX licence). TF008 credits semgrep by name under that
|
|
72
|
+
* rule, so `PRIOR_ART` does carry the entry, with the licence spelled out
|
|
73
|
+
* rather than as an SPDX id. Sweep: unsweepable, the registry publishes no
|
|
74
|
+
* index and a rule id is the path of its YAML file under
|
|
75
|
+
* `terraform/<provider>/<category>/`, which no single directory listing covers.
|
|
65
76
|
*
|
|
66
77
|
* TF024 (#2103, live root declares a backend/cloud block) credits choudoufu's
|
|
67
78
|
* own `RuleStateBackend`, `internal/live/lint/issue.go` of
|
|
@@ -83,6 +94,22 @@
|
|
|
83
94
|
* activities/kubectl.ts`'s `ApplyDeleteMode`, `packages/core/src/op/
|
|
84
95
|
* activities/apply.ts`'s `DeleteMode`), applied here to a tool that does not
|
|
85
96
|
* itself carry the concept. Ship with no lineage rather than invent a credit.
|
|
97
|
+
*
|
|
98
|
+
* TF027 (#2216, a live root's `policy` block setting `undeclared_untagged =
|
|
99
|
+
* "delete"`) does have one: choudoufu's `RulePolicyScope`
|
|
100
|
+
* (`internal/live/lint/policy.go`) refuses the same assignment, and issue #67
|
|
101
|
+
* there makes it a lint refusal rather than a default. The relation is
|
|
102
|
+
* `extends`, because choudoufu refuses it only when no `scope` block narrows
|
|
103
|
+
* the purge, while chant refuses it either way: a `scope` block bounds an
|
|
104
|
+
* account-wide sweep to a service, type or region, and every resource inside
|
|
105
|
+
* that boundary is still one this estate never marked. "Bounded" is not
|
|
106
|
+
* "owned", and owning it is the condition chant applies.
|
|
107
|
+
*
|
|
108
|
+
* TF028 (#2216, a live root watched by a `TerraformWatchOp` built without
|
|
109
|
+
* `live: true`) has no entry. It reads a chant Op's own configuration against
|
|
110
|
+
* a root's mode, and no surveyed tool has an Op model to read: choudoufu lints
|
|
111
|
+
* HCL, and every tflint ruleset, tfsec/trivy-checks and the Sentinel policies
|
|
112
|
+
* do the same. There is nothing upstream to credit.
|
|
86
113
|
*/
|
|
87
114
|
import type { Lineage } from "@intentius/chant/audit/catalog";
|
|
88
115
|
|
|
@@ -93,6 +120,14 @@ const TFLINT_RULES = "https://github.com/terraform-linters/tflint-ruleset-terraf
|
|
|
93
120
|
const REDEPLOY_RULES = "https://github.com/RedeployAB/tflint-ruleset-redeploy/blob/main/docs/rules";
|
|
94
121
|
|
|
95
122
|
export const terraformAuditLineage: Record<string, Lineage[]> = {
|
|
123
|
+
TF027: [
|
|
124
|
+
{
|
|
125
|
+
tool: "choudoufu",
|
|
126
|
+
rule: "RulePolicyScope",
|
|
127
|
+
url: "https://github.com/INTENTIUS/choudoufu/blob/main/internal/live/lint/policy.go",
|
|
128
|
+
relation: "extends",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
96
131
|
TF024: [
|
|
97
132
|
{
|
|
98
133
|
tool: "choudoufu",
|
|
@@ -196,6 +231,19 @@ export const terraformAuditLineage: Record<string, Lineage[]> = {
|
|
|
196
231
|
{ tool: "tfsec", rule: "general-secrets-sensitive-in-variable", url: `${TFSEC_V061}/sensitive-in-variable.md`, relation: "equivalent" },
|
|
197
232
|
{ tool: "tfsec", rule: "general-secrets-sensitive-in-local", url: `${TFSEC_V061}/sensitive-in-local.md`, relation: "equivalent" },
|
|
198
233
|
],
|
|
234
|
+
// #2107's rule table credits TF008 with Snyk `SNYK-CC-TF-74` ("Credentials
|
|
235
|
+
// are configured via provider attributes") alongside these three. That
|
|
236
|
+
// credit is deliberately absent here, and the absence is the epic's own
|
|
237
|
+
// decision rather than an oversight: Snyk IaC is a product, not a rule
|
|
238
|
+
// source, which is why it sits in this file's "deliberately excluded" list
|
|
239
|
+
// above and has no `PRIOR_ART` entry to cite. The rules have no open
|
|
240
|
+
// repository behind them, and the public index (security.snyk.io/rules/
|
|
241
|
+
// cloud) does not even enumerate the legacy `SNYK-CC-TF-*` ids, so a
|
|
242
|
+
// credit here could not be swept the way every other one in this file is
|
|
243
|
+
// (scripts/prior-art-sweep.ts). The condition loses nothing by it:
|
|
244
|
+
// `SNYK-CC-TF-74`, checkov's `CKV_AWS_41` and semgrep's
|
|
245
|
+
// `aws-provider-static-credentials` are the same check three times over,
|
|
246
|
+
// and the other two are credited below (#2220).
|
|
199
247
|
TF008: [
|
|
200
248
|
{ tool: "checkov", rule: "CKV_AWS_41", url: "https://www.checkov.io/5.Policy%20Index/terraform.html", relation: "overlaps" },
|
|
201
249
|
{ tool: "kics", rule: "d7b9d850-3e06-4a75-852f-c46c2e92240b", url: "https://docs.kics.io/latest/queries/terraform-queries/aws/d7b9d850-3e06-4a75-852f-c46c2e92240b/", relation: "overlaps" },
|
|
@@ -235,6 +283,18 @@ export const terraformAuditLineage: Record<string, Lineage[]> = {
|
|
|
235
283
|
TF013: [
|
|
236
284
|
{ tool: "tflint-ruleset-redeploy", rule: "terraform_ignore_changes_all", url: `${REDEPLOY_RULES}/terraform_ignore_changes_all.md`, relation: "equivalent" },
|
|
237
285
|
],
|
|
286
|
+
// The two vendor documents below (and the one on TF015, and the HashiCorp
|
|
287
|
+
// style guide on TF021) are `Lineage` entries, not `RuleMeta.authority`
|
|
288
|
+
// ones, though #2107's table calls them "authority". `authority` is not a
|
|
289
|
+
// free-standing "this document says so" field: an authority citation forces
|
|
290
|
+
// `category: "security"` (`packages/core/src/audit/catalog.ts`), and
|
|
291
|
+
// `catalog.test.ts` binds the two together in both directions, "an
|
|
292
|
+
// authority citation always means security" and "authority citations only
|
|
293
|
+
// attach to merge-worthy entries". A provider block in a child module is a
|
|
294
|
+
// structural rule, not a security finding, so citing these guides as
|
|
295
|
+
// authority would have relabelled three rules as security to get a citation
|
|
296
|
+
// rendered. Lineage carries the same URLs and never touches tier or
|
|
297
|
+
// category, so that is where they live (#2112, recorded here by #2220).
|
|
238
298
|
TF014: [
|
|
239
299
|
{
|
|
240
300
|
tool: "tflint-ruleset-avm",
|
|
@@ -262,7 +322,9 @@ export const terraformAuditLineage: Record<string, Lineage[]> = {
|
|
|
262
322
|
// provider block TF014 credits. What states the rule is Google Cloud's
|
|
263
323
|
// reusable-modules guide, so that document is the single credit here, and
|
|
264
324
|
// the page section says plainly that the credit is a document rather than
|
|
265
|
-
// a tool (#2112).
|
|
325
|
+
// a tool (#2112). It is lineage rather than `authority` for the reason on
|
|
326
|
+
// TF014 above: an authority citation would force `category: "security"` on
|
|
327
|
+
// a rule about where state configuration belongs.
|
|
266
328
|
TF015: [
|
|
267
329
|
{
|
|
268
330
|
tool: "gcp-terraform-best-practices",
|
|
@@ -303,7 +365,10 @@ export const terraformAuditLineage: Record<string, Lineage[]> = {
|
|
|
303
365
|
// the ones that build an identity out of `count.index`, which is a
|
|
304
366
|
// strict subset, hence `overlaps` on both credits rather than
|
|
305
367
|
// `equivalent`. choudoufu's own rule is the survey's deepest reading of
|
|
306
|
-
// the identity-bearing question (#2112).
|
|
368
|
+
// the identity-bearing question (#2112). The HashiCorp style guide entry
|
|
369
|
+
// is lineage rather than `authority` for the reason recorded on TF014: an
|
|
370
|
+
// authority citation forces `category: "security"`, and `count` versus
|
|
371
|
+
// `for_each` is not a security finding.
|
|
307
372
|
{
|
|
308
373
|
tool: "tflint-ruleset-redeploy",
|
|
309
374
|
rule: "terraform_prefer_for_each",
|