@intentius/chant-lexicon-terraform 0.101.0 → 0.104.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/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
- package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
- package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
- package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
- package/dist/change-set.d.ts +67 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/changed-roots.d.ts +30 -0
- package/dist/changed-roots.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +1 -1
- package/dist/commands.d.ts +26 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/components/capability-plugin.d.ts +54 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
- package/dist/config.d.ts +3 -3
- package/dist/hcl/descend.d.ts +1 -1
- package/dist/hcl/descend.d.ts.map +1 -1
- package/dist/hcl/edges.d.ts +2 -2
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/jsonencode.d.ts +106 -0
- package/dist/hcl/jsonencode.d.ts.map +1 -0
- package/dist/hcl/marker.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +29 -1
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/roots.d.ts +1 -1
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +22 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +1 -1
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/flat-attributes.d.ts +69 -0
- package/dist/lint/post-synth/flat-attributes.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +27 -1
- package/dist/lint/post-synth/module-source.d.ts.map +1 -1
- package/dist/lint/post-synth/terragrunt.d.ts +37 -0
- package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
- package/dist/lint/post-synth/tf002.d.ts +3 -1
- package/dist/lint/post-synth/tf002.d.ts.map +1 -1
- package/dist/lint/post-synth/tf030.d.ts +51 -0
- package/dist/lint/post-synth/tf030.d.ts.map +1 -0
- package/dist/lint/post-synth/tf031.d.ts +55 -0
- package/dist/lint/post-synth/tf031.d.ts.map +1 -0
- package/dist/lint/post-synth/tf032.d.ts +71 -0
- package/dist/lint/post-synth/tf032.d.ts.map +1 -0
- package/dist/lint/post-synth/tf033.d.ts +50 -0
- package/dist/lint/post-synth/tf033.d.ts.map +1 -0
- package/dist/lint/post-synth/tf034.d.ts +25 -0
- package/dist/lint/post-synth/tf034.d.ts.map +1 -0
- package/dist/lint/post-synth/tf035.d.ts +33 -0
- package/dist/lint/post-synth/tf035.d.ts.map +1 -0
- package/dist/lint/post-synth/tf036.d.ts +33 -0
- package/dist/lint/post-synth/tf036.d.ts.map +1 -0
- package/dist/lint/post-synth/tf037.d.ts +31 -0
- package/dist/lint/post-synth/tf037.d.ts.map +1 -0
- package/dist/lint/post-synth/tf038.d.ts +22 -0
- package/dist/lint/post-synth/tf038.d.ts.map +1 -0
- package/dist/lint/post-synth/tf039.d.ts +23 -0
- package/dist/lint/post-synth/tf039.d.ts.map +1 -0
- package/dist/lint/post-synth/tf040.d.ts +30 -0
- package/dist/lint/post-synth/tf040.d.ts.map +1 -0
- package/dist/lint/post-synth/tf041.d.ts +20 -0
- package/dist/lint/post-synth/tf041.d.ts.map +1 -0
- package/dist/lint/post-synth/tf042.d.ts +18 -0
- package/dist/lint/post-synth/tf042.d.ts.map +1 -0
- package/dist/lint/post-synth/tf043.d.ts +17 -0
- package/dist/lint/post-synth/tf043.d.ts.map +1 -0
- package/dist/lint/post-synth/tf044.d.ts +17 -0
- package/dist/lint/post-synth/tf044.d.ts.map +1 -0
- package/dist/lint/post-synth/tf045.d.ts +22 -0
- package/dist/lint/post-synth/tf045.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +16 -0
- package/dist/okf/rules/TF030.md +11 -0
- package/dist/okf/rules/TF031.md +11 -0
- package/dist/okf/rules/TF032.md +11 -0
- package/dist/okf/rules/TF033.md +11 -0
- package/dist/okf/rules/TF034.md +11 -0
- package/dist/okf/rules/TF035.md +11 -0
- package/dist/okf/rules/TF036.md +11 -0
- package/dist/okf/rules/TF037.md +11 -0
- package/dist/okf/rules/TF038.md +11 -0
- package/dist/okf/rules/TF039.md +11 -0
- package/dist/okf/rules/TF040.md +11 -0
- package/dist/okf/rules/TF041.md +11 -0
- package/dist/okf/rules/TF042.md +11 -0
- package/dist/okf/rules/TF043.md +11 -0
- package/dist/okf/rules/TF044.md +11 -0
- package/dist/okf/rules/TF045.md +11 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.d.ts +46 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +24 -50
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +66 -18
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/pin/edit-ts.d.ts +21 -0
- package/dist/pin/edit-ts.d.ts.map +1 -0
- package/dist/pin/edit.d.ts +52 -0
- package/dist/pin/edit.d.ts.map +1 -0
- package/dist/pin/forge.d.ts +39 -0
- package/dist/pin/forge.d.ts.map +1 -0
- package/dist/pin/index.d.ts +22 -0
- package/dist/pin/index.d.ts.map +1 -0
- package/dist/pin/rollout.d.ts +116 -0
- package/dist/pin/rollout.d.ts.map +1 -0
- package/dist/pin/scan.d.ts +54 -0
- package/dist/pin/scan.d.ts.map +1 -0
- package/dist/pin/source.d.ts +93 -0
- package/dist/pin/source.d.ts.map +1 -0
- package/dist/pin/waves.d.ts +66 -0
- package/dist/pin/waves.d.ts.map +1 -0
- package/dist/plan-digest.d.ts +59 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/flat-attributes.ts +146 -0
- package/dist/rules/module-source.ts +56 -1
- package/dist/rules/terragrunt.ts +76 -0
- package/dist/rules/tf002.ts +1 -1
- package/dist/rules/tf030.ts +290 -0
- package/dist/rules/tf031.ts +370 -0
- package/dist/rules/tf032.ts +226 -0
- package/dist/rules/tf033.ts +160 -0
- package/dist/rules/tf034.ts +63 -0
- package/dist/rules/tf035.ts +74 -0
- package/dist/rules/tf036.ts +119 -0
- package/dist/rules/tf037.ts +101 -0
- package/dist/rules/tf038.ts +71 -0
- package/dist/rules/tf039.ts +64 -0
- package/dist/rules/tf040.ts +76 -0
- package/dist/rules/tf041.ts +56 -0
- package/dist/rules/tf042.ts +42 -0
- package/dist/rules/tf043.ts +49 -0
- package/dist/rules/tf044.ts +51 -0
- package/dist/rules/tf045.ts +47 -0
- package/dist/terragrunt/affected.d.ts +119 -0
- package/dist/terragrunt/affected.d.ts.map +1 -0
- package/dist/terragrunt/index.d.ts +11 -0
- package/dist/terragrunt/index.d.ts.map +1 -0
- package/dist/terragrunt/mocks.d.ts +147 -0
- package/dist/terragrunt/mocks.d.ts.map +1 -0
- package/dist/terragrunt/run.d.ts +130 -0
- package/dist/terragrunt/run.d.ts.map +1 -0
- package/dist/terragrunt/units.d.ts +103 -0
- package/dist/terragrunt/units.d.ts.map +1 -0
- package/dist/terragrunt/wave.d.ts +89 -0
- package/dist/terragrunt/wave.d.ts.map +1 -0
- package/package.json +13 -3
- package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
- package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
- package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
- package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
- package/src/__fixtures__/change-set/terraform.plan.json +589 -0
- package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/tofu.plan.json +588 -0
- package/src/__fixtures__/change-set/v1.tf.txt +21 -0
- package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
- package/src/__fixtures__/change-set/v2.tf.txt +21 -0
- package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
- package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
- package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
- package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
- package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
- package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
- package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
- package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
- package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
- package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/record.ts +56 -0
- package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
- package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
- package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
- package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
- package/src/change-set-combined-run.test.ts +156 -0
- package/src/change-set.test.ts +151 -0
- package/src/change-set.ts +273 -0
- package/src/changed-roots.test.ts +58 -0
- package/src/changed-roots.ts +102 -0
- package/src/commands.ts +112 -0
- package/src/components/capability-plugin.test.ts +91 -0
- package/src/components/capability-plugin.ts +131 -0
- package/src/components/pr-loop.tofu.test.ts +244 -0
- package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
- package/src/composites/terraform-pin-rollout-op.ts +60 -0
- package/src/hcl/descend.ts +1 -1
- package/src/hcl/edges.ts +2 -2
- package/src/hcl/jsonencode.test.ts +226 -0
- package/src/hcl/jsonencode.ts +454 -0
- package/src/hcl/parse.test.ts +25 -0
- package/src/hcl/parse.ts +91 -6
- package/src/hcl/roots.ts +1 -1
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +18 -0
- package/src/lint/audit-catalog.ts +230 -0
- package/src/lint/audit-lineage.ts +155 -1
- package/src/lint/audit.test.ts +3 -1
- package/src/lint/post-synth/estate-2284.test.ts +68 -0
- package/src/lint/post-synth/fixtures/TF030/negative-rules.tf +45 -0
- package/src/lint/post-synth/fixtures/TF030/negative.tf +55 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined-port.tf +28 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined-standalone.tf +32 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined-vpc-rule.tf +21 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined.tf +39 -0
- package/src/lint/post-synth/fixtures/TF030/positive-ipv6.tf +44 -0
- package/src/lint/post-synth/fixtures/TF030/positive-mixed.tf +36 -0
- package/src/lint/post-synth/fixtures/TF030/positive-standalone.tf +25 -0
- package/src/lint/post-synth/fixtures/TF030/positive.tf +43 -0
- package/src/lint/post-synth/fixtures/TF031/negative.tf +89 -0
- package/src/lint/post-synth/fixtures/TF031/not-determined.tf +139 -0
- package/src/lint/post-synth/fixtures/TF031/positive-data-source.tf +57 -0
- package/src/lint/post-synth/fixtures/TF031/positive-heredoc.tf +149 -0
- package/src/lint/post-synth/fixtures/TF031/positive-jsonencode.tf +115 -0
- package/src/lint/post-synth/fixtures/TF032/negative-heredoc.tf +20 -0
- package/src/lint/post-synth/fixtures/TF032/negative-references.tf +214 -0
- package/src/lint/post-synth/fixtures/TF032/negative.tf +99 -0
- package/src/lint/post-synth/fixtures/TF032/positive-heredoc.tf +22 -0
- package/src/lint/post-synth/fixtures/TF032/positive-sidecar.tf +197 -0
- package/src/lint/post-synth/fixtures/TF032/positive.tf +54 -0
- package/src/lint/post-synth/fixtures/TF033/negative.tf +39 -0
- package/src/lint/post-synth/fixtures/TF033/not-determined-aurora.tf +25 -0
- package/src/lint/post-synth/fixtures/TF033/positive-absent.tf +93 -0
- package/src/lint/post-synth/fixtures/TF033/positive.tf +50 -0
- package/src/lint/post-synth/fixtures/TF034/negative.tf +24 -0
- package/src/lint/post-synth/fixtures/TF034/positive-absent.tf +36 -0
- package/src/lint/post-synth/fixtures/TF034/positive.tf +14 -0
- package/src/lint/post-synth/fixtures/TF035/negative-absent.tf +43 -0
- package/src/lint/post-synth/fixtures/TF035/negative.tf +54 -0
- package/src/lint/post-synth/fixtures/TF035/positive.tf +37 -0
- package/src/lint/post-synth/fixtures/TF036/negative.tf +61 -0
- package/src/lint/post-synth/fixtures/TF036/not-determined.tf +28 -0
- package/src/lint/post-synth/fixtures/TF036/positive.tf +38 -0
- package/src/lint/post-synth/fixtures/TF037/negative-exclusion.tf +63 -0
- package/src/lint/post-synth/fixtures/TF037/negative.tf +27 -0
- package/src/lint/post-synth/fixtures/TF037/not-determined.tf +27 -0
- package/src/lint/post-synth/fixtures/TF037/positive-absent.tf +15 -0
- package/src/lint/post-synth/fixtures/TF037/positive.tf +26 -0
- package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
- package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
- package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
- package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/estate-2284/clean/main.tf +132 -0
- package/src/lint/post-synth/fixtures/estate-2284/violating/main.tf +112 -0
- package/src/lint/post-synth/flat-attributes.ts +146 -0
- package/src/lint/post-synth/index.ts +32 -0
- package/src/lint/post-synth/module-source.test.ts +42 -1
- package/src/lint/post-synth/module-source.ts +56 -1
- package/src/lint/post-synth/terragrunt.ts +76 -0
- package/src/lint/post-synth/tf002.ts +1 -1
- package/src/lint/post-synth/tf030.test.ts +219 -0
- package/src/lint/post-synth/tf030.ts +290 -0
- package/src/lint/post-synth/tf031.test.ts +229 -0
- package/src/lint/post-synth/tf031.ts +370 -0
- package/src/lint/post-synth/tf032.test.ts +127 -0
- package/src/lint/post-synth/tf032.ts +226 -0
- package/src/lint/post-synth/tf033.test.ts +126 -0
- package/src/lint/post-synth/tf033.ts +160 -0
- package/src/lint/post-synth/tf034.test.ts +71 -0
- package/src/lint/post-synth/tf034.ts +63 -0
- package/src/lint/post-synth/tf035.test.ts +68 -0
- package/src/lint/post-synth/tf035.ts +74 -0
- package/src/lint/post-synth/tf036.test.ts +87 -0
- package/src/lint/post-synth/tf036.ts +119 -0
- package/src/lint/post-synth/tf037.test.ts +88 -0
- package/src/lint/post-synth/tf037.ts +101 -0
- package/src/lint/post-synth/tf038.test.ts +51 -0
- package/src/lint/post-synth/tf038.ts +71 -0
- package/src/lint/post-synth/tf039.test.ts +40 -0
- package/src/lint/post-synth/tf039.ts +64 -0
- package/src/lint/post-synth/tf040.test.ts +55 -0
- package/src/lint/post-synth/tf040.ts +76 -0
- package/src/lint/post-synth/tf041.test.ts +50 -0
- package/src/lint/post-synth/tf041.ts +56 -0
- package/src/lint/post-synth/tf042.test.ts +19 -0
- package/src/lint/post-synth/tf042.ts +42 -0
- package/src/lint/post-synth/tf043.test.ts +65 -0
- package/src/lint/post-synth/tf043.ts +49 -0
- package/src/lint/post-synth/tf044.test.ts +27 -0
- package/src/lint/post-synth/tf044.ts +51 -0
- package/src/lint/post-synth/tf045.test.ts +30 -0
- package/src/lint/post-synth/tf045.ts +47 -0
- package/src/op/activities/index.ts +4 -0
- package/src/op/activities/pin-rollout.ts +77 -0
- package/src/op/activities/terraform.test.ts +13 -0
- package/src/op/activities/terraform.ts +46 -86
- package/src/op/activity-contracts.test.ts +4 -1
- package/src/op/activity-contracts.ts +42 -0
- package/src/op/builders.ts +13 -0
- package/src/pin/bundle.test.ts +53 -0
- package/src/pin/edit-ts.test.ts +39 -0
- package/src/pin/edit-ts.ts +77 -0
- package/src/pin/edit.test.ts +199 -0
- package/src/pin/edit.ts +157 -0
- package/src/pin/forge.ts +85 -0
- package/src/pin/index.ts +22 -0
- package/src/pin/rollout.test.ts +232 -0
- package/src/pin/rollout.ts +451 -0
- package/src/pin/scan.ts +309 -0
- package/src/pin/source.ts +206 -0
- package/src/pin/waves.test.ts +101 -0
- package/src/pin/waves.ts +125 -0
- package/src/plan-digest.ts +99 -0
- package/src/plan-summary.test.ts +154 -0
- package/src/plugin.ts +25 -2
- package/src/terragrunt/affected.acceptance.test.ts +83 -0
- package/src/terragrunt/affected.test.ts +177 -0
- package/src/terragrunt/affected.ts +348 -0
- package/src/terragrunt/index.ts +11 -0
- package/src/terragrunt/mocks.acceptance.test.ts +105 -0
- package/src/terragrunt/mocks.test.ts +309 -0
- package/src/terragrunt/mocks.ts +324 -0
- package/src/terragrunt/run.ts +369 -0
- package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
- package/src/terragrunt/terragrunt.test.ts +332 -0
- package/src/terragrunt/units.ts +235 -0
- package/src/terragrunt/wave.ts +181 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
4
|
+
import { loadFixture } from "./fixtures/load";
|
|
5
|
+
import { tf034 } from "./tf034";
|
|
6
|
+
|
|
7
|
+
async function inline(source: string): Promise<PostSynthContext> {
|
|
8
|
+
const entities = await blocksToEntities([{ name: "main.tf", source }], "app");
|
|
9
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const verdicts = (d: PostSynthDiagnostic[]) => d.map((x) => `${x.severity} ${x.entity}`).sort();
|
|
13
|
+
|
|
14
|
+
describe("TF034 over vendored real-world fixtures", () => {
|
|
15
|
+
test("a key from a managed KMS alias is silent (covidgreen/covid-green-infra)", async () => {
|
|
16
|
+
expect(tf034.check(await loadFixture("TF034", "negative"))).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('kms_master_key_id = "" is a warning (KICS test corpus)', async () => {
|
|
20
|
+
const diags = tf034.check(await loadFixture("TF034", "positive"));
|
|
21
|
+
expect(verdicts(diags)).toEqual(["warning TF034/aws_sns_topic.user_updates"]);
|
|
22
|
+
expect(diags[0]).toMatchObject({ checkId: "TF034", lexicon: "terraform" });
|
|
23
|
+
expect(diags[0].message).toContain('sets `kms_master_key_id = ""`');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a topic with no key is a warning: absence is insecure for SNS (radian-software/riju)", async () => {
|
|
27
|
+
const diags = tf034.check(await loadFixture("TF034", "positive-absent"));
|
|
28
|
+
expect(verdicts(diags)).toEqual(["warning TF034/aws_sns_topic.riju"]);
|
|
29
|
+
expect(diags[0].message).toContain("does not set `kms_master_key_id`");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("TF034 cases the fixtures do not isolate", () => {
|
|
34
|
+
test("the AWS managed alias, a template with literal text and a data source attribute are keys", async () => {
|
|
35
|
+
const diags = tf034.check(
|
|
36
|
+
await inline(`
|
|
37
|
+
resource "aws_sns_topic" "managed" {
|
|
38
|
+
kms_master_key_id = "alias/aws/sns"
|
|
39
|
+
}
|
|
40
|
+
resource "aws_sns_topic" "templated" {
|
|
41
|
+
kms_master_key_id = "alias/\${var.env}-sns"
|
|
42
|
+
}
|
|
43
|
+
resource "aws_sns_topic" "data" {
|
|
44
|
+
kms_master_key_id = data.aws_kms_key.sns.arn
|
|
45
|
+
}`),
|
|
46
|
+
);
|
|
47
|
+
expect(diags).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a variable, a conditional or a bare interpolation is not determined", async () => {
|
|
51
|
+
const diags = tf034.check(
|
|
52
|
+
await inline(`
|
|
53
|
+
resource "aws_sns_topic" "v" {
|
|
54
|
+
kms_master_key_id = var.kms_key_id
|
|
55
|
+
}
|
|
56
|
+
resource "aws_sns_topic" "c" {
|
|
57
|
+
kms_master_key_id = var.encrypt ? aws_kms_key.k.arn : null
|
|
58
|
+
}
|
|
59
|
+
resource "aws_sns_topic" "t" {
|
|
60
|
+
kms_master_key_id = "\${var.a}\${var.b}"
|
|
61
|
+
}`),
|
|
62
|
+
);
|
|
63
|
+
expect(verdicts(diags)).toEqual(["info app/aws_sns_topic.c", "info app/aws_sns_topic.t", "info app/aws_sns_topic.v"]);
|
|
64
|
+
for (const d of diags) expect(d.message).toMatch(/^Not determined: /);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("kms_master_key_id = null is the same as absent", async () => {
|
|
68
|
+
const diags = tf034.check(await inline(`resource "aws_sns_topic" "n" {\n kms_master_key_id = null\n}`));
|
|
69
|
+
expect(verdicts(diags)).toEqual(["warning app/aws_sns_topic.n"]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF034: an SNS topic has no server-side encryption (chant #2288, epic #2284).
|
|
3
|
+
*
|
|
4
|
+
* The Terraform counterpart of the aws lexicon's WAW025, written natively.
|
|
5
|
+
*
|
|
6
|
+
* Absence is insecure for this resource, and the rule reports it.
|
|
7
|
+
* `aws_sns_topic.kms_master_key_id` (provider `internal/service/sns/topic.go`,
|
|
8
|
+
* v6.67.0) is optional with no default, and SNS has no service-owned
|
|
9
|
+
* encryption that applies when no key is named: server-side encryption
|
|
10
|
+
* (https://docs.aws.amazon.com/sns/latest/dg/sns-server-side-encryption.html)
|
|
11
|
+
* is on only when the topic names a KMS key, either the AWS managed
|
|
12
|
+
* `alias/aws/sns` or a customer managed key. Unlike SQS (TF035), no AWS
|
|
13
|
+
* default turns it on for new topics. A literal `kms_master_key_id = ""`
|
|
14
|
+
* names no key and is the same finding.
|
|
15
|
+
*
|
|
16
|
+
* Present and secure: a non-empty literal, an attribute of a managed resource
|
|
17
|
+
* or data source (`aws_kms_key.sns.arn`, `aws_kms_alias.sns.arn`), or a
|
|
18
|
+
* template with literal text. Not determined: a variable, local, module
|
|
19
|
+
* output, conditional or function call, any of which can be `null` or `""`.
|
|
20
|
+
*
|
|
21
|
+
* Scope: root and child modules alike (#2112).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
25
|
+
import { finding, notDetermined, readKeySet, resourcesOfType } from "./flat-attributes";
|
|
26
|
+
|
|
27
|
+
const ID = "TF034";
|
|
28
|
+
|
|
29
|
+
export const tf034: PostSynthCheck = {
|
|
30
|
+
id: "TF034",
|
|
31
|
+
description: "SNS topic has no server-side encryption",
|
|
32
|
+
|
|
33
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
34
|
+
const out: PostSynthDiagnostic[] = [];
|
|
35
|
+
for (const block of resourcesOfType(ctx, "aws_sns_topic")) {
|
|
36
|
+
const key = readKeySet(block.body, "kms_master_key_id");
|
|
37
|
+
if (key.kind === "known" && key.value !== "") continue;
|
|
38
|
+
if (key.kind === "unknown") {
|
|
39
|
+
out.push(
|
|
40
|
+
notDetermined(
|
|
41
|
+
ID,
|
|
42
|
+
block,
|
|
43
|
+
key.reason,
|
|
44
|
+
"TF034 does not evaluate expressions, so it cannot tell whether this names a KMS key or is null.",
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const how = key.kind === "absent" ? "does not set `kms_master_key_id`" : 'sets `kms_master_key_id = ""`';
|
|
50
|
+
out.push(
|
|
51
|
+
finding(
|
|
52
|
+
ID,
|
|
53
|
+
"warning",
|
|
54
|
+
block,
|
|
55
|
+
`${how}, so SNS stores its messages without server-side encryption; no AWS default encrypts a topic that ` +
|
|
56
|
+
'names no key. Set `kms_master_key_id` to a customer managed key, or to `"alias/aws/sns"` for the AWS ' +
|
|
57
|
+
"managed one.",
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
4
|
+
import { loadFixture } from "./fixtures/load";
|
|
5
|
+
import { tf035 } from "./tf035";
|
|
6
|
+
|
|
7
|
+
async function inline(source: string): Promise<PostSynthContext> {
|
|
8
|
+
const entities = await blocksToEntities([{ name: "main.tf", source }], "app");
|
|
9
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const verdicts = (d: PostSynthDiagnostic[]) => d.map((x) => `${x.severity} ${x.entity}`).sort();
|
|
13
|
+
|
|
14
|
+
describe("TF035 over vendored real-world fixtures (cds-snc/notification-terraform)", () => {
|
|
15
|
+
test("sqs_managed_sse_enabled = true is silent", async () => {
|
|
16
|
+
expect(tf035.check(await loadFixture("TF035", "negative"))).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("sqs_managed_sse_enabled = false with no key is a warning", async () => {
|
|
20
|
+
const diags = tf035.check(await loadFixture("TF035", "positive"));
|
|
21
|
+
expect(verdicts(diags)).toEqual(["warning TF035/aws_sqs_queue.notify_internal_tasks_queue"]);
|
|
22
|
+
expect(diags[0]).toMatchObject({ checkId: "TF035", lexicon: "terraform" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("a queue that sets neither attribute is silent: AWS encrypts it with SSE-SQS by default", async () => {
|
|
26
|
+
expect(tf035.check(await loadFixture("TF035", "negative-absent"))).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("TF035 cases the fixtures do not isolate", () => {
|
|
31
|
+
test("a KMS key beside sqs_managed_sse_enabled = false, or a variable key alone, is encrypted", async () => {
|
|
32
|
+
const diags = tf035.check(
|
|
33
|
+
await inline(`
|
|
34
|
+
resource "aws_sqs_queue" "kms" {
|
|
35
|
+
kms_master_key_id = "alias/aws/sqs"
|
|
36
|
+
}
|
|
37
|
+
resource "aws_sqs_queue" "managed_key" {
|
|
38
|
+
kms_master_key_id = aws_kms_key.sqs.arn
|
|
39
|
+
}
|
|
40
|
+
resource "aws_sqs_queue" "var_key_default_sse" {
|
|
41
|
+
kms_master_key_id = var.kms_key
|
|
42
|
+
}`),
|
|
43
|
+
);
|
|
44
|
+
expect(diags).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("an expression for sqs_managed_sse_enabled, or a variable key beside false, is not determined", async () => {
|
|
48
|
+
const diags = tf035.check(
|
|
49
|
+
await inline(`
|
|
50
|
+
resource "aws_sqs_queue" "sse_var" {
|
|
51
|
+
sqs_managed_sse_enabled = var.sse
|
|
52
|
+
}
|
|
53
|
+
resource "aws_sqs_queue" "off_var_key" {
|
|
54
|
+
sqs_managed_sse_enabled = false
|
|
55
|
+
kms_master_key_id = var.kms_key
|
|
56
|
+
}
|
|
57
|
+
resource "aws_sqs_queue" "off_empty_key" {
|
|
58
|
+
sqs_managed_sse_enabled = false
|
|
59
|
+
kms_master_key_id = ""
|
|
60
|
+
}`),
|
|
61
|
+
);
|
|
62
|
+
expect(verdicts(diags)).toEqual([
|
|
63
|
+
"info app/aws_sqs_queue.off_var_key",
|
|
64
|
+
"info app/aws_sqs_queue.sse_var",
|
|
65
|
+
"warning app/aws_sqs_queue.off_empty_key",
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF035: an SQS queue turns its server-side encryption off (chant #2288,
|
|
3
|
+
* epic #2284).
|
|
4
|
+
*
|
|
5
|
+
* The Terraform counterpart of the aws lexicon's WAW026, and the case that
|
|
6
|
+
* shows why the five flat rules could not be bridged. WAW026 reports a queue
|
|
7
|
+
* with neither `SqsManagedSseEnabled: true` nor `KmsMasterKeyId`. Bridged onto
|
|
8
|
+
* HCL, that reports every correctly encrypted queue in an estate
|
|
9
|
+
* (docs/design/waw-hcl-fidelity-probe.md).
|
|
10
|
+
*
|
|
11
|
+
* Absence is secure for this resource, and the rule does not report it.
|
|
12
|
+
* `sqs_managed_sse_enabled` is optional and computed (provider
|
|
13
|
+
* `internal/service/sqs/queue.go`, v6.67.0), and the provider docs say
|
|
14
|
+
* Terraform "will only perform drift detection of its value when present in
|
|
15
|
+
* a configuration". Since October 2022 SQS encrypts every new queue with
|
|
16
|
+
* SSE-SQS unless the request turns it off
|
|
17
|
+
* (https://aws.amazon.com/blogs/compute/announcing-server-side-encryption-with-amazon-simple-queue-service-managed-encryption-keys-sse-sqs-by-default/),
|
|
18
|
+
* and the provider creates queues over HTTPS, so a queue that sets neither
|
|
19
|
+
* attribute is encrypted. A queue created before that date and never
|
|
20
|
+
* changed can still be unencrypted; the configuration cannot show that, and
|
|
21
|
+
* TF035 does not guess it.
|
|
22
|
+
*
|
|
23
|
+
* The finding: `sqs_managed_sse_enabled = false` with no KMS key. Present and
|
|
24
|
+
* secure: `sqs_managed_sse_enabled = true`, or a `kms_master_key_id` that is
|
|
25
|
+
* a non-empty literal, a managed key's attribute, or a template with literal
|
|
26
|
+
* text. Not determined: `sqs_managed_sse_enabled` from an expression with no
|
|
27
|
+
* key, or a key from a variable beside `sqs_managed_sse_enabled = false`.
|
|
28
|
+
*
|
|
29
|
+
* Scope: root and child modules alike (#2112).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
33
|
+
import { finding, notDetermined, readBool, readKeySet, resourcesOfType } from "./flat-attributes";
|
|
34
|
+
|
|
35
|
+
const ID = "TF035";
|
|
36
|
+
|
|
37
|
+
const TAIL = "TF035 does not evaluate expressions, so it cannot tell whether this queue encrypts its messages.";
|
|
38
|
+
|
|
39
|
+
export const tf035: PostSynthCheck = {
|
|
40
|
+
id: "TF035",
|
|
41
|
+
description: "SQS queue turns server-side encryption off",
|
|
42
|
+
|
|
43
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
44
|
+
const out: PostSynthDiagnostic[] = [];
|
|
45
|
+
for (const block of resourcesOfType(ctx, "aws_sqs_queue")) {
|
|
46
|
+
const key = readKeySet(block.body, "kms_master_key_id");
|
|
47
|
+
const sse = readBool(block.body, "sqs_managed_sse_enabled");
|
|
48
|
+
if (key.kind === "known" && key.value !== "") continue;
|
|
49
|
+
if (sse.kind === "known" && sse.value) continue;
|
|
50
|
+
// Neither attribute turns encryption off: AWS's SSE-SQS default applies.
|
|
51
|
+
if (sse.kind === "absent") continue;
|
|
52
|
+
|
|
53
|
+
if (sse.kind === "unknown") {
|
|
54
|
+
out.push(notDetermined(ID, block, `${sse.reason} and names no KMS key`, TAIL));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (key.kind === "unknown") {
|
|
58
|
+
out.push(notDetermined(ID, block, `sets \`sqs_managed_sse_enabled = false\` and ${key.reason}`, TAIL));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
out.push(
|
|
62
|
+
finding(
|
|
63
|
+
ID,
|
|
64
|
+
"warning",
|
|
65
|
+
block,
|
|
66
|
+
"sets `sqs_managed_sse_enabled = false` and names no KMS key, which turns off the SSE-SQS encryption AWS " +
|
|
67
|
+
"applies to new queues by default, so messages are stored unencrypted. Remove the line (the default is " +
|
|
68
|
+
'encrypted), set it to `true`, or set `kms_master_key_id` (`"alias/aws/sqs"` or a customer managed key).',
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
4
|
+
import { loadFixture } from "./fixtures/load";
|
|
5
|
+
import { tf036 } from "./tf036";
|
|
6
|
+
|
|
7
|
+
async function inline(source: string): Promise<PostSynthContext> {
|
|
8
|
+
const entities = await blocksToEntities([{ name: "main.tf", source }], "app");
|
|
9
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const verdicts = (d: PostSynthDiagnostic[]) => d.map((x) => `${x.severity} ${x.entity}`).sort();
|
|
13
|
+
|
|
14
|
+
describe("TF036 over vendored real-world fixtures", () => {
|
|
15
|
+
test("encrypted = true is silent (ned1313/terraform-tuesdays)", async () => {
|
|
16
|
+
expect(tf036.check(await loadFixture("TF036", "negative"))).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("encrypted = false is a warning (stelligent/config-lint)", async () => {
|
|
20
|
+
const diags = tf036.check(await loadFixture("TF036", "positive"));
|
|
21
|
+
expect(verdicts(diags)).toEqual(["warning TF036/aws_ebs_volume.vol2"]);
|
|
22
|
+
expect(diags[0]).toMatchObject({ checkId: "TF036", lexicon: "terraform" });
|
|
23
|
+
expect(diags[0].message).toContain("replace it on every apply");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a volume that does not set encrypted is not determined (bridgecrewio/terragoat)", async () => {
|
|
27
|
+
const diags = tf036.check(await loadFixture("TF036", "not-determined"));
|
|
28
|
+
expect(verdicts(diags)).toEqual(["info TF036/aws_ebs_volume.web_host_storage"]);
|
|
29
|
+
expect(diags[0].message).toMatch(/^Not determined: .*EBS encryption by default/);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("TF036 cases the fixtures do not isolate", () => {
|
|
34
|
+
test("depends_on an enabled aws_ebs_encryption_by_default settles absence", async () => {
|
|
35
|
+
const diags = tf036.check(
|
|
36
|
+
await inline(`
|
|
37
|
+
resource "aws_ebs_encryption_by_default" "on" {}
|
|
38
|
+
resource "aws_ebs_volume" "ordered" {
|
|
39
|
+
availability_zone = "eu-west-1a"
|
|
40
|
+
size = 10
|
|
41
|
+
depends_on = [aws_ebs_encryption_by_default.on]
|
|
42
|
+
}
|
|
43
|
+
resource "aws_ebs_volume" "unordered" {
|
|
44
|
+
availability_zone = "eu-west-1a"
|
|
45
|
+
size = 10
|
|
46
|
+
}`),
|
|
47
|
+
);
|
|
48
|
+
expect(verdicts(diags)).toEqual(["info app/aws_ebs_volume.unordered"]);
|
|
49
|
+
expect(diags[0].message).toContain("`aws_ebs_encryption_by_default.on` turns the default on in this module");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a disabled default, or one on another provider, does not settle it", async () => {
|
|
53
|
+
const diags = tf036.check(
|
|
54
|
+
await inline(`
|
|
55
|
+
resource "aws_ebs_encryption_by_default" "off" {
|
|
56
|
+
enabled = false
|
|
57
|
+
}
|
|
58
|
+
resource "aws_ebs_encryption_by_default" "west" {
|
|
59
|
+
provider = aws.west
|
|
60
|
+
}
|
|
61
|
+
resource "aws_ebs_volume" "a" {
|
|
62
|
+
size = 10
|
|
63
|
+
depends_on = [aws_ebs_encryption_by_default.off]
|
|
64
|
+
}
|
|
65
|
+
resource "aws_ebs_volume" "b" {
|
|
66
|
+
size = 10
|
|
67
|
+
depends_on = [aws_ebs_encryption_by_default.west]
|
|
68
|
+
}`),
|
|
69
|
+
);
|
|
70
|
+
expect(verdicts(diags)).toEqual(["info app/aws_ebs_volume.a", "info app/aws_ebs_volume.b"]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("snapshot_id is named in the reason; an encrypted expression is not determined", async () => {
|
|
74
|
+
const diags = tf036.check(
|
|
75
|
+
await inline(`
|
|
76
|
+
resource "aws_ebs_volume" "snap" {
|
|
77
|
+
snapshot_id = aws_ebs_snapshot.s.id
|
|
78
|
+
}
|
|
79
|
+
resource "aws_ebs_volume" "v" {
|
|
80
|
+
size = 10
|
|
81
|
+
encrypted = var.encrypted
|
|
82
|
+
}`),
|
|
83
|
+
);
|
|
84
|
+
expect(verdicts(diags)).toEqual(["info app/aws_ebs_volume.snap", "info app/aws_ebs_volume.v"]);
|
|
85
|
+
expect(diags.find((d) => d.entity === "app/aws_ebs_volume.snap")!.message).toContain("`snapshot_id`");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF036: an EBS volume does not ask for encryption (chant #2288, epic #2284).
|
|
3
|
+
*
|
|
4
|
+
* The Terraform counterpart of the aws lexicon's WAW028, written natively.
|
|
5
|
+
*
|
|
6
|
+
* Absence is not determined for this resource. `aws_ebs_volume.encrypted` is
|
|
7
|
+
* optional, computed and force-new (provider `internal/service/ec2/
|
|
8
|
+
* ebs_volume.go`, v6.67.0). Whether a volume created without it is encrypted
|
|
9
|
+
* depends on the Region's EBS encryption by default
|
|
10
|
+
* (https://docs.aws.amazon.com/ebs/latest/userguide/encryption-by-default.html),
|
|
11
|
+
* which is account state the parse cannot see, and a volume built from
|
|
12
|
+
* `snapshot_id` also follows its snapshot. So an unset `encrypted` emits one
|
|
13
|
+
* `info` diagnostic, not a finding.
|
|
14
|
+
*
|
|
15
|
+
* One configuration does settle absence: the volume `depends_on` an
|
|
16
|
+
* `aws_ebs_encryption_by_default` in the same module whose `enabled` is
|
|
17
|
+
* absent (the provider default is `true`) or literal `true`, on the same
|
|
18
|
+
* `provider` and `region`. The `depends_on` matters: without it Terraform may
|
|
19
|
+
* create the volume before it turns the default on, and the default "has no
|
|
20
|
+
* effect on existing EBS volumes".
|
|
21
|
+
*
|
|
22
|
+
* `encrypted = false` is a finding even though encryption by default would
|
|
23
|
+
* override it. The literal is wrong either way: with the default off the
|
|
24
|
+
* volume is unencrypted, and with it on AWS encrypts the volume anyway ("you
|
|
25
|
+
* cannot disable it for individual volumes"), the volume no longer matches
|
|
26
|
+
* the configuration, and because `encrypted` forces replacement Terraform
|
|
27
|
+
* plans to replace it on every apply.
|
|
28
|
+
*
|
|
29
|
+
* Scope: root and child modules alike (#2112).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
33
|
+
import { scopeOfKey } from "../../hcl/parse";
|
|
34
|
+
import type { TerraformBlock } from "./blocks";
|
|
35
|
+
import { finding, isGiven, notDetermined, readBool, resourcesOfType } from "./flat-attributes";
|
|
36
|
+
|
|
37
|
+
const ID = "TF036";
|
|
38
|
+
const DEFAULT_TYPE = "aws_ebs_encryption_by_default";
|
|
39
|
+
|
|
40
|
+
/** The `aws_ebs_encryption_by_default.<name>` addresses a volume's `depends_on` names. */
|
|
41
|
+
function dependsOnDefaults(block: TerraformBlock): string[] {
|
|
42
|
+
const deps = block.body.depends_on;
|
|
43
|
+
if (!Array.isArray(deps)) return [];
|
|
44
|
+
const out: string[] = [];
|
|
45
|
+
for (const d of deps) {
|
|
46
|
+
if (typeof d !== "string") continue;
|
|
47
|
+
const m = /^\$\{\s*(aws_ebs_encryption_by_default\.[A-Za-z_][\w-]*)\s*\}$/.exec(d) ?? /^(aws_ebs_encryption_by_default\.[A-Za-z_][\w-]*)$/.exec(d);
|
|
48
|
+
if (m) out.push(m[1]);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Same provider configuration and Region, compared as written. */
|
|
54
|
+
function sameTarget(a: TerraformBlock, b: TerraformBlock): boolean {
|
|
55
|
+
return a.body.provider === b.body.provider && a.body.region === b.body.region;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const tf036: PostSynthCheck = {
|
|
59
|
+
id: "TF036",
|
|
60
|
+
description: "EBS volume does not ask for encryption",
|
|
61
|
+
|
|
62
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
63
|
+
const out: PostSynthDiagnostic[] = [];
|
|
64
|
+
const defaults = resourcesOfType(ctx, DEFAULT_TYPE);
|
|
65
|
+
|
|
66
|
+
for (const block of resourcesOfType(ctx, "aws_ebs_volume")) {
|
|
67
|
+
const encrypted = readBool(block.body, "encrypted");
|
|
68
|
+
if (encrypted.kind === "known" && encrypted.value) continue;
|
|
69
|
+
if (encrypted.kind === "known") {
|
|
70
|
+
out.push(
|
|
71
|
+
finding(
|
|
72
|
+
ID,
|
|
73
|
+
"warning",
|
|
74
|
+
block,
|
|
75
|
+
"sets `encrypted = false`. With the Region's EBS encryption by default off, the volume is unencrypted; " +
|
|
76
|
+
"with it on, AWS encrypts the volume anyway and, because `encrypted` forces replacement, Terraform " +
|
|
77
|
+
"plans to replace it on every apply. Set `encrypted = true` (and `kms_key_id` for a customer managed " +
|
|
78
|
+
"key), or remove the line.",
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (encrypted.kind === "unknown") {
|
|
84
|
+
out.push(
|
|
85
|
+
notDetermined(ID, block, encrypted.reason, "TF036 does not evaluate expressions, so it cannot tell whether this volume is encrypted."),
|
|
86
|
+
);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const scope = scopeOfKey(block.key);
|
|
91
|
+
const inScope = defaults.filter((d) => scopeOfKey(d.key) === scope && sameTarget(d, block));
|
|
92
|
+
const enabled = inScope.filter((d) => {
|
|
93
|
+
const e = readBool(d.body, "enabled");
|
|
94
|
+
return e.kind === "absent" || (e.kind === "known" && e.value);
|
|
95
|
+
});
|
|
96
|
+
const named = new Set(dependsOnDefaults(block));
|
|
97
|
+
if (enabled.some((d) => named.has(d.address))) continue;
|
|
98
|
+
|
|
99
|
+
const parts = ["does not set `encrypted`"];
|
|
100
|
+
if (isGiven(block.body, "snapshot_id")) parts.push("and is built from `snapshot_id`, whose encryption it inherits");
|
|
101
|
+
const reason =
|
|
102
|
+
`${parts.join(" ")}, so whether it is encrypted depends on the Region's EBS encryption by default, which is ` +
|
|
103
|
+
"account state" +
|
|
104
|
+
(enabled.length > 0
|
|
105
|
+
? `. \`${enabled[0].address}\` turns the default on in this module, but the volume does not \`depends_on\` it, so it may be created first`
|
|
106
|
+
: "");
|
|
107
|
+
out.push(
|
|
108
|
+
notDetermined(
|
|
109
|
+
ID,
|
|
110
|
+
block,
|
|
111
|
+
reason,
|
|
112
|
+
"Set `encrypted = true` to make the answer explicit, or add `depends_on` naming an enabled " +
|
|
113
|
+
"`aws_ebs_encryption_by_default`.",
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
4
|
+
import { loadFixture } from "./fixtures/load";
|
|
5
|
+
import { tf037 } from "./tf037";
|
|
6
|
+
|
|
7
|
+
async function inline(source: string): Promise<PostSynthContext> {
|
|
8
|
+
const entities = await blocksToEntities([{ name: "main.tf", source }], "app");
|
|
9
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const verdicts = (d: PostSynthDiagnostic[]) => d.map((x) => `${x.severity} ${x.entity}`).sort();
|
|
13
|
+
|
|
14
|
+
describe("TF037 over vendored real-world fixtures", () => {
|
|
15
|
+
test("IMMUTABLE is silent (aws-samples/sample-scribe-ai)", async () => {
|
|
16
|
+
expect(tf037.check(await loadFixture("TF037", "negative"))).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("IMMUTABLE_WITH_EXCLUSION with named moving tags is silent (lexicalunit/spellbot)", async () => {
|
|
20
|
+
expect(tf037.check(await loadFixture("TF037", "negative-exclusion"))).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("MUTABLE is an error (bridgecrewio/terragoat)", async () => {
|
|
24
|
+
const diags = tf037.check(await loadFixture("TF037", "positive"));
|
|
25
|
+
expect(verdicts(diags)).toEqual(["error TF037/aws_ecr_repository.repository"]);
|
|
26
|
+
expect(diags[0]).toMatchObject({ checkId: "TF037", lexicon: "terraform" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a repository that does not set image_tag_mutability is an error: the default is MUTABLE (outerbounds)", async () => {
|
|
30
|
+
const diags = tf037.check(await loadFixture("TF037", "positive-absent"));
|
|
31
|
+
expect(verdicts(diags)).toEqual(["error TF037/aws_ecr_repository.metaflow_batch_image"]);
|
|
32
|
+
expect(diags[0].message).toContain("the provider default is `MUTABLE`");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a variable is not determined, whatever its default (turnerlabs/terraform-ecs-fargate)", async () => {
|
|
36
|
+
const diags = tf037.check(await loadFixture("TF037", "not-determined"));
|
|
37
|
+
expect(verdicts(diags)).toEqual(["info TF037/aws_ecr_repository.app"]);
|
|
38
|
+
expect(diags[0].message).toContain("var.image_tag_mutability");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("TF037 cases the fixtures do not isolate", () => {
|
|
43
|
+
test("MUTABLE_WITH_EXCLUSION and a match-everything exclusion filter are errors", async () => {
|
|
44
|
+
const diags = tf037.check(
|
|
45
|
+
await inline(`
|
|
46
|
+
resource "aws_ecr_repository" "mostly_mutable" {
|
|
47
|
+
image_tag_mutability = "MUTABLE_WITH_EXCLUSION"
|
|
48
|
+
image_tag_mutability_exclusion_filter {
|
|
49
|
+
filter = "release-*"
|
|
50
|
+
filter_type = "WILDCARD"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
resource "aws_ecr_repository" "star" {
|
|
54
|
+
image_tag_mutability = "IMMUTABLE_WITH_EXCLUSION"
|
|
55
|
+
image_tag_mutability_exclusion_filter {
|
|
56
|
+
filter = "*"
|
|
57
|
+
filter_type = "WILDCARD"
|
|
58
|
+
}
|
|
59
|
+
}`),
|
|
60
|
+
);
|
|
61
|
+
expect(verdicts(diags)).toEqual(["error app/aws_ecr_repository.mostly_mutable", "error app/aws_ecr_repository.star"]);
|
|
62
|
+
expect(diags.find((d) => d.entity === "app/aws_ecr_repository.star")!.message).toContain("matches every tag");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("an exclusion filter from an expression or a dynamic block is not determined", async () => {
|
|
66
|
+
const diags = tf037.check(
|
|
67
|
+
await inline(`
|
|
68
|
+
resource "aws_ecr_repository" "f" {
|
|
69
|
+
image_tag_mutability = "IMMUTABLE_WITH_EXCLUSION"
|
|
70
|
+
image_tag_mutability_exclusion_filter {
|
|
71
|
+
filter = var.moving_tag
|
|
72
|
+
filter_type = "WILDCARD"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
resource "aws_ecr_repository" "d" {
|
|
76
|
+
image_tag_mutability = "IMMUTABLE_WITH_EXCLUSION"
|
|
77
|
+
dynamic "image_tag_mutability_exclusion_filter" {
|
|
78
|
+
for_each = var.filters
|
|
79
|
+
content {
|
|
80
|
+
filter = image_tag_mutability_exclusion_filter.value
|
|
81
|
+
filter_type = "WILDCARD"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}`),
|
|
85
|
+
);
|
|
86
|
+
expect(verdicts(diags)).toEqual(["info app/aws_ecr_repository.d", "info app/aws_ecr_repository.f"]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF037: an ECR repository lets an image tag be repointed (chant #2288, epic
|
|
3
|
+
* #2284).
|
|
4
|
+
*
|
|
5
|
+
* The Terraform counterpart of the aws lexicon's WAW054, written natively.
|
|
6
|
+
*
|
|
7
|
+
* Absence is insecure for this resource, and the rule reports it.
|
|
8
|
+
* `aws_ecr_repository.image_tag_mutability` (provider
|
|
9
|
+
* `internal/service/ecr/repository.go`, v6.67.0) is optional with
|
|
10
|
+
* `Default: MUTABLE`, so a repository that does not set it lets anyone with
|
|
11
|
+
* push access move a tag (`prod`, `v1.4.2`) to different image content after
|
|
12
|
+
* it was deployed, scanned or signed.
|
|
13
|
+
*
|
|
14
|
+
* Provider v6 accepts four values (AWS added the two `_WITH_EXCLUSION` modes
|
|
15
|
+
* in 2025), and the rule reads them as follows:
|
|
16
|
+
*
|
|
17
|
+
* - `IMMUTABLE`: secure.
|
|
18
|
+
* - `IMMUTABLE_WITH_EXCLUSION`: secure. Tags matching an
|
|
19
|
+
* `image_tag_mutability_exclusion_filter` stay mutable, a written-down
|
|
20
|
+
* exception such as `latest`. A filter that is nothing but `*` excludes
|
|
21
|
+
* every tag, so it is a finding; a filter from an expression is not
|
|
22
|
+
* determined.
|
|
23
|
+
* - `MUTABLE`, absent, and `MUTABLE_WITH_EXCLUSION` (every tag mutable except
|
|
24
|
+
* those the filters name): a finding.
|
|
25
|
+
* - an expression: not determined.
|
|
26
|
+
*
|
|
27
|
+
* Scope: root and child modules alike (#2112).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
31
|
+
import type { TerraformBlock } from "./blocks";
|
|
32
|
+
import { finding, nestedBodies, notDetermined, readString, resourcesOfType } from "./flat-attributes";
|
|
33
|
+
|
|
34
|
+
const ID = "TF037";
|
|
35
|
+
|
|
36
|
+
const FIX =
|
|
37
|
+
'Set `image_tag_mutability = "IMMUTABLE"` and push a new tag for each build, or `"IMMUTABLE_WITH_EXCLUSION"` with ' +
|
|
38
|
+
"an `image_tag_mutability_exclusion_filter` for the few moving tags (`latest`) that must stay mutable.";
|
|
39
|
+
|
|
40
|
+
const TAIL = "TF037 does not evaluate expressions, so it cannot tell whether this repository's tags are immutable.";
|
|
41
|
+
|
|
42
|
+
/** A diagnostic for an `IMMUTABLE_WITH_EXCLUSION` repository, or nothing. */
|
|
43
|
+
function checkExclusions(block: TerraformBlock): PostSynthDiagnostic | undefined {
|
|
44
|
+
for (const [i, f] of nestedBodies(block.body, "image_tag_mutability_exclusion_filter").entries()) {
|
|
45
|
+
const filter = readString(f, "filter");
|
|
46
|
+
if (filter.kind === "unknown") {
|
|
47
|
+
return notDetermined(ID, block, `exclusion filter ${i + 1}: ${filter.reason}`, TAIL);
|
|
48
|
+
}
|
|
49
|
+
if (filter.kind === "known" && /^\*+$/.test(filter.value)) {
|
|
50
|
+
return finding(
|
|
51
|
+
ID,
|
|
52
|
+
"error",
|
|
53
|
+
block,
|
|
54
|
+
`is \`IMMUTABLE_WITH_EXCLUSION\`, but exclusion filter ${i + 1} is "${filter.value}", which matches every tag, ` +
|
|
55
|
+
`so every tag stays mutable. ${FIX}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (nestedBodies(block.body, "dynamic").some((d) => d.image_tag_mutability_exclusion_filter !== undefined)) {
|
|
60
|
+
return notDetermined(ID, block, 'has a `dynamic "image_tag_mutability_exclusion_filter"` block', TAIL);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const tf037: PostSynthCheck = {
|
|
66
|
+
id: "TF037",
|
|
67
|
+
description: "ECR repository allows mutable image tags",
|
|
68
|
+
|
|
69
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
70
|
+
const out: PostSynthDiagnostic[] = [];
|
|
71
|
+
for (const block of resourcesOfType(ctx, "aws_ecr_repository")) {
|
|
72
|
+
const mode = readString(block.body, "image_tag_mutability");
|
|
73
|
+
if (mode.kind === "unknown") {
|
|
74
|
+
out.push(notDetermined(ID, block, mode.reason, TAIL));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (mode.kind === "known" && mode.value === "IMMUTABLE") continue;
|
|
78
|
+
if (mode.kind === "known" && mode.value === "IMMUTABLE_WITH_EXCLUSION") {
|
|
79
|
+
const d = checkExclusions(block);
|
|
80
|
+
if (d) out.push(d);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const how =
|
|
84
|
+
mode.kind === "absent"
|
|
85
|
+
? "does not set `image_tag_mutability`, and the provider default is `MUTABLE`"
|
|
86
|
+
: mode.value === "MUTABLE_WITH_EXCLUSION"
|
|
87
|
+
? "is `MUTABLE_WITH_EXCLUSION`, so every tag the exclusion filters do not name stays mutable"
|
|
88
|
+
: `sets \`image_tag_mutability = "${mode.value}"\``;
|
|
89
|
+
out.push(
|
|
90
|
+
finding(
|
|
91
|
+
ID,
|
|
92
|
+
"error",
|
|
93
|
+
block,
|
|
94
|
+
`${how}: a pushed tag can later be repointed at different image content, so what was deployed, scanned or ` +
|
|
95
|
+
`signed under that tag is no longer what it names. ${FIX}`,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
},
|
|
101
|
+
};
|