@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,127 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { tf032 } from "./tf032";
|
|
4
|
+
import { loadFixture } from "./fixtures/load";
|
|
5
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
6
|
+
|
|
7
|
+
async function ctxOf(source: string): Promise<PostSynthContext> {
|
|
8
|
+
const entities = await blocksToEntities([{ name: "main.tf", source }], "TF032");
|
|
9
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const errors = (d: PostSynthDiagnostic[]) => d.filter((x) => x.severity === "error");
|
|
13
|
+
const infos = (d: PostSynthDiagnostic[]) => d.filter((x) => x.severity === "info");
|
|
14
|
+
|
|
15
|
+
/** One task definition around a container_definitions expression. */
|
|
16
|
+
const task = (defs: string) => `resource "aws_ecs_task_definition" "t" {\n family = "t"\n container_definitions = ${defs}\n}\n`;
|
|
17
|
+
|
|
18
|
+
describe("TF032: plaintext credential in an ECS container definition", () => {
|
|
19
|
+
test("fires on each literal credential in a jsonencode'd environment (edstem paylink-platform)", async () => {
|
|
20
|
+
const diags = tf032.check(await loadFixture("TF032", "positive"));
|
|
21
|
+
expect(errors(diags).map((d) => d.message.match(/passes "([^"]+)"/)?.[1])).toEqual(["DB_PASSWORD", "PAYMENT_API_KEY"]);
|
|
22
|
+
// `image = "${var.ecr_repo}:latest"` is an unknown leaf, not a reason to give up on the container.
|
|
23
|
+
expect(infos(diags)).toHaveLength(0);
|
|
24
|
+
for (const d of errors(diags)) {
|
|
25
|
+
expect(d.checkId).toBe("TF032");
|
|
26
|
+
expect(d.entity).toBe("TF032/aws_ecs_task_definition.api");
|
|
27
|
+
expect(d.message).toContain("redacted");
|
|
28
|
+
expect(d.message).not.toContain("Pl-Pr0d-Db-2025!");
|
|
29
|
+
expect(d.message).not.toContain("pk_live_");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("reads a heredoc with no interpolation as JSON (trivy-checks' failing example)", async () => {
|
|
34
|
+
const diags = tf032.check(await loadFixture("TF032", "positive-heredoc"));
|
|
35
|
+
expect(errors(diags)).toHaveLength(1);
|
|
36
|
+
expect(errors(diags)[0].message).toContain('"DATABASE_PASSWORD"');
|
|
37
|
+
expect(errors(diags)[0].message).toContain('container "my_service"');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("fires on a literal sidecar password and reports the local-fed environments as not determined (govuk forms-admin)", async () => {
|
|
41
|
+
const diags = tf032.check(await loadFixture("TF032", "positive-sidecar"));
|
|
42
|
+
expect(errors(diags)).toHaveLength(1);
|
|
43
|
+
expect(errors(diags)[0].message).toContain('container "postgres" passes "POSTGRES_PASSWORD"');
|
|
44
|
+
const nd = infos(diags);
|
|
45
|
+
expect(nd).toHaveLength(2);
|
|
46
|
+
for (const d of nd) {
|
|
47
|
+
expect(d.message.startsWith("Not determined:")).toBe(true);
|
|
48
|
+
expect(d.message).toContain("`environment`");
|
|
49
|
+
}
|
|
50
|
+
expect(nd.map((d) => d.message.match(/container "([^"]+)"/)?.[1])).toEqual(["forms-admin", "forms-admin-seeding"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("is silent when credentials come through secrets (vapor penny-bot)", async () => {
|
|
54
|
+
expect(tf032.check(await loadFixture("TF032", "negative"))).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("is silent on trivy-checks' passing example", async () => {
|
|
58
|
+
expect(tf032.check(await loadFixture("TF032", "negative-heredoc"))).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a reference value is not a literal, a container with no environment is not insecure (testdrivenio)", async () => {
|
|
62
|
+
const diags = tf032.check(await loadFixture("TF032", "negative-references"));
|
|
63
|
+
expect(errors(diags)).toEqual([]);
|
|
64
|
+
// The one task whose definitions are `data.template_file.app.rendered`.
|
|
65
|
+
expect(infos(diags)).toHaveLength(1);
|
|
66
|
+
expect(infos(diags)[0].entity).toBe("TF032/aws_ecs_task_definition.app");
|
|
67
|
+
expect(infos(diags)[0].message).toMatch(/^Not determined: .*data\.template_file\.app\.rendered/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("not determined, never a warning", () => {
|
|
71
|
+
test.each([
|
|
72
|
+
["file()", 'file("service.json")', /call to file\(\)/],
|
|
73
|
+
["templatefile()", 'templatefile("defs.tpl", { image = var.image })', /call to templatefile\(\)/],
|
|
74
|
+
["a local", "local.container_definitions", /reference `local\.container_definitions`/],
|
|
75
|
+
["jsonencode(concat(...))", "jsonencode(concat([{ name = \"a\" }], var.extra))", /jsonencode argument holds/],
|
|
76
|
+
["a heredoc with interpolation", '<<EOT\n[{"name":"a","image":"${var.image}"}]\nEOT', /interpolation/],
|
|
77
|
+
])("%s", async (_label, defs, reason) => {
|
|
78
|
+
const diags = tf032.check(await ctxOf(task(defs)));
|
|
79
|
+
expect(diags).toHaveLength(1);
|
|
80
|
+
expect(diags[0].severity).toBe("info");
|
|
81
|
+
expect(diags[0].message.startsWith("Not determined:")).toBe(true);
|
|
82
|
+
expect(diags[0].message).toMatch(reason);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("an environment entry that is itself an expression", async () => {
|
|
86
|
+
const diags = tf032.check(
|
|
87
|
+
await ctxOf(task('jsonencode([{ name = "a", environment = [{ name = "MODE", value = "api" }, local.extra_env] }])')),
|
|
88
|
+
);
|
|
89
|
+
expect(diags).toHaveLength(1);
|
|
90
|
+
expect(diags[0].severity).toBe("info");
|
|
91
|
+
expect(diags[0].message).toContain('container "a"');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("the same name through secrets does not fire; a secret-named literal in environment does", async () => {
|
|
96
|
+
const viaSecrets = task(
|
|
97
|
+
'jsonencode([{ name = "a", secrets = [{ name = "DB_PASSWORD", valueFrom = "arn:aws:ssm:eu-west-1:123456789012:parameter/db" }] }])',
|
|
98
|
+
);
|
|
99
|
+
expect(tf032.check(await ctxOf(viaSecrets))).toEqual([]);
|
|
100
|
+
const viaEnv = task('jsonencode([{ name = "a", environment = [{ name = "DB_PASSWORD", value = "s3cr3t-Pa55word" }] }])');
|
|
101
|
+
expect(errors(tf032.check(await ctxOf(viaEnv)))).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("short values, placeholders and lookalike names are not credentials", async () => {
|
|
105
|
+
const defs = task(
|
|
106
|
+
'jsonencode([{ name = "a", environment = [' +
|
|
107
|
+
'{ name = "PASSWORD_MIN_LENGTH", value = "8" },' +
|
|
108
|
+
'{ name = "DB_PASSWORD", value = "changeme-please" },' +
|
|
109
|
+
'{ name = "TOKENIZER", value = "wordpiece-uncased" },' +
|
|
110
|
+
'{ name = "DB_SECRET_ARN", value = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:db" }' +
|
|
111
|
+
"] }])",
|
|
112
|
+
);
|
|
113
|
+
expect(tf032.check(await ctxOf(defs))).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a credential-shaped value fires whatever it is called", async () => {
|
|
117
|
+
const defs = task('jsonencode([{ name = "a", environment = [{ name = "UPSTREAM", value = "AKIAIOSFODNN7EXAMPLF" }] }])');
|
|
118
|
+
const diags = errors(tf032.check(await ctxOf(defs)));
|
|
119
|
+
expect(diags).toHaveLength(1);
|
|
120
|
+
expect(diags[0].message).toContain("shape of a credential");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("only aws_ecs_task_definition is read", async () => {
|
|
124
|
+
const src = 'resource "aws_ecs_service" "s" {\n name = "s"\n container_definitions = local.x\n}\n';
|
|
125
|
+
expect(tf032.check(await ctxOf(src))).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF032: an ECS container definition passes a credential as a plaintext
|
|
3
|
+
* environment value.
|
|
4
|
+
*
|
|
5
|
+
* The Terraform counterpart of the aws lexicon's WAW046. A value under a
|
|
6
|
+
* container's `environment` is stored verbatim in the task definition: it is
|
|
7
|
+
* readable by anyone with `ecs:DescribeTaskDefinition`, it shows in the
|
|
8
|
+
* console, and it is in Terraform state and plan output. ECS has a field for
|
|
9
|
+
* exactly this, `secrets`, whose `valueFrom` is a Secrets Manager or SSM
|
|
10
|
+
* Parameter Store ARN that the agent resolves at task start. A credential
|
|
11
|
+
* belongs there.
|
|
12
|
+
*
|
|
13
|
+
* Why this is a native rule and not WAW046 through a bridge
|
|
14
|
+
* (docs/design/waw-hcl-fidelity-probe.md): `container_definitions` is a JSON
|
|
15
|
+
* string. In HCL it is almost always `jsonencode([...])`, which hcl2json hands
|
|
16
|
+
* back as the call's unevaluated source text, so the structure WAW046 reads
|
|
17
|
+
* does not exist in the parse until something reads that text.
|
|
18
|
+
* `../../hcl/jsonencode.ts` does, for literal structure only, and never
|
|
19
|
+
* evaluates anything.
|
|
20
|
+
*
|
|
21
|
+
* The three answers, per task definition:
|
|
22
|
+
*
|
|
23
|
+
* - Readable. `container_definitions` is `jsonencode([...])` whose list and
|
|
24
|
+
* containers are literal, or a plain JSON string (a heredoc with no
|
|
25
|
+
* interpolation is one). A reference or function call in a leaf
|
|
26
|
+
* (`image = var.image`, `value = var.db_password`) does not stop the read:
|
|
27
|
+
* that leaf is unknown and the literal fields beside it are still read. An
|
|
28
|
+
* environment entry whose value is a reference is skipped, since a
|
|
29
|
+
* reference is not a committed literal (TF007 and TF022 judge where the
|
|
30
|
+
* referenced value comes from).
|
|
31
|
+
* - Violation. A readable environment entry whose `value` is a literal string
|
|
32
|
+
* and is secret-shaped by `../secret-shape.ts`: a credential-looking name
|
|
33
|
+
* with a literal of at least 8 characters that is not a placeholder, or a
|
|
34
|
+
* value that is a credential on its own (a vendor key shape, a JWT, a PEM
|
|
35
|
+
* key, a high-entropy token) whatever it is called.
|
|
36
|
+
* - Not determined. `container_definitions` is a reference
|
|
37
|
+
* (`local.defs`, `data.template_file.x.rendered`), `file()`,
|
|
38
|
+
* `templatefile()`, a heredoc or string with interpolation, a jsonencode
|
|
39
|
+
* argument that is not a literal list (`jsonencode(concat(...))`,
|
|
40
|
+
* `jsonencode(local.containers)`), or a container whose `environment` is an
|
|
41
|
+
* expression (`environment = local.env`). One `info` diagnostic starting
|
|
42
|
+
* `Not determined:` per task definition or container, never a warning.
|
|
43
|
+
*
|
|
44
|
+
* Absence: a container with no `environment` is not insecure, and a task
|
|
45
|
+
* definition with no `container_definitions` is not reported (the attribute
|
|
46
|
+
* is required, so that is a plan error, not this rule's finding). A name in
|
|
47
|
+
* `secrets` is never reported: `secrets` is not read at all.
|
|
48
|
+
*
|
|
49
|
+
* Findings never carry the value, only its length.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import type {
|
|
53
|
+
PostSynthCheck,
|
|
54
|
+
PostSynthContext,
|
|
55
|
+
PostSynthDiagnostic,
|
|
56
|
+
} from "@intentius/chant/lint/post-synth";
|
|
57
|
+
import { RESOURCE_TYPE } from "../../hcl/parse";
|
|
58
|
+
import { attr } from "../../hcl/value";
|
|
59
|
+
import { isUnknown, readJsonencode } from "../../hcl/jsonencode";
|
|
60
|
+
import { secretShapedAssignment } from "../secret-shape";
|
|
61
|
+
import { blocksOfType, type TerraformBlock } from "./blocks";
|
|
62
|
+
|
|
63
|
+
const TASK_DEFINITION = "aws_ecs_task_definition";
|
|
64
|
+
|
|
65
|
+
/** The container definitions of one task definition, or why they cannot be read. */
|
|
66
|
+
export type ContainerDefinitionsRead =
|
|
67
|
+
| { kind: "readable"; containers: unknown }
|
|
68
|
+
| { kind: "not-determined"; reason: string }
|
|
69
|
+
| { kind: "absent" };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read `container_definitions` as structure without evaluating anything.
|
|
73
|
+
* `jsonencode(...)` goes through the shared reader with unknown leaves; a
|
|
74
|
+
* plain string with no interpolation is JSON (a heredoc arrives as one);
|
|
75
|
+
* everything else is named in the not-determined reason.
|
|
76
|
+
*/
|
|
77
|
+
export function readContainerDefinitions(body: Record<string, unknown>): ContainerDefinitionsRead {
|
|
78
|
+
const raw = body.container_definitions;
|
|
79
|
+
if (raw === undefined || raw === null) return { kind: "absent" };
|
|
80
|
+
|
|
81
|
+
const encoded = readJsonencode(raw, { unknownLeaves: true });
|
|
82
|
+
if (encoded.kind === "literal") return { kind: "readable", containers: encoded.value };
|
|
83
|
+
if (encoded.kind === "not-determined") {
|
|
84
|
+
return { kind: "not-determined", reason: `its jsonencode argument holds ${encoded.reason}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof raw !== "string") return { kind: "not-determined", reason: "it is not a string" };
|
|
88
|
+
// attr() alone is not enough to call a string literal: its interpolation
|
|
89
|
+
// pattern tolerates one level of nested braces, so a deeper expression can
|
|
90
|
+
// read as `literal`. Any `${` at all means the parse did not hand us text.
|
|
91
|
+
if (!raw.includes("${")) {
|
|
92
|
+
try {
|
|
93
|
+
return { kind: "readable", containers: JSON.parse(raw) };
|
|
94
|
+
} catch {
|
|
95
|
+
return { kind: "not-determined", reason: "it is a string that is not valid JSON" };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { kind: "not-determined", reason: describeExpression(raw) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Name the expression an unreadable `container_definitions` holds, for the message. */
|
|
102
|
+
function describeExpression(raw: string): string {
|
|
103
|
+
const a = attr({ v: raw }, "v");
|
|
104
|
+
if (a.kind === "reference" && a.refs?.[0] !== undefined) {
|
|
105
|
+
const expr = a.refs[0];
|
|
106
|
+
const call = /^([a-z_][a-z0-9_]*)\s*\(/i.exec(expr);
|
|
107
|
+
if (call) return `it is a call to ${call[1]}(), which this rule does not evaluate`;
|
|
108
|
+
return `it is the reference \`${expr}\`, whose value is not in this module's source`;
|
|
109
|
+
}
|
|
110
|
+
return "it is a string or heredoc with interpolation, which this rule does not evaluate";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A container's display name: its literal `name`, else its position. */
|
|
114
|
+
function containerName(container: Record<string, unknown>, index: number): string {
|
|
115
|
+
return typeof container.name === "string" ? container.name : `#${index}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function notDetermined(block: TerraformBlock, what: string, reason: string): PostSynthDiagnostic {
|
|
119
|
+
return {
|
|
120
|
+
checkId: "TF032",
|
|
121
|
+
severity: "info",
|
|
122
|
+
message:
|
|
123
|
+
`Not determined: "${block.address}" ${what}: ${reason}. TF032 reads only literal structure, ` +
|
|
124
|
+
"so a plaintext credential in it can be neither found nor ruled out.",
|
|
125
|
+
entity: block.key,
|
|
126
|
+
lexicon: "terraform",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function checkEcsPlaintextCredentials(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
131
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
132
|
+
|
|
133
|
+
for (const block of blocksOfType(ctx, RESOURCE_TYPE)) {
|
|
134
|
+
if (!block.address.startsWith(`${TASK_DEFINITION}.`)) continue;
|
|
135
|
+
|
|
136
|
+
const read = readContainerDefinitions(block.body);
|
|
137
|
+
if (read.kind === "absent") continue;
|
|
138
|
+
if (read.kind === "not-determined") {
|
|
139
|
+
diagnostics.push(notDetermined(block, "`container_definitions`", read.reason));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// A literal that is not a list is not a valid container_definitions; the
|
|
143
|
+
// provider rejects it at plan, and there is no environment to read.
|
|
144
|
+
if (!Array.isArray(read.containers)) continue;
|
|
145
|
+
|
|
146
|
+
read.containers.forEach((container, index) => {
|
|
147
|
+
if (isUnknown(container)) {
|
|
148
|
+
diagnostics.push(
|
|
149
|
+
notDetermined(block, `container #${index} of \`container_definitions\``, `it is ${container.reason}`),
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (typeof container !== "object" || container === null || Array.isArray(container)) return;
|
|
154
|
+
const c = container as Record<string, unknown>;
|
|
155
|
+
const name = containerName(c, index);
|
|
156
|
+
|
|
157
|
+
const env = c.environment;
|
|
158
|
+
if (env === undefined || env === null) return;
|
|
159
|
+
if (isUnknown(env)) {
|
|
160
|
+
diagnostics.push(notDetermined(block, `container "${name}" \`environment\``, `it is ${env.reason}`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(env)) return;
|
|
164
|
+
|
|
165
|
+
const unreadable: string[] = [];
|
|
166
|
+
for (const entry of env) {
|
|
167
|
+
if (isUnknown(entry)) {
|
|
168
|
+
unreadable.push(entry.reason);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
172
|
+
const e = entry as Record<string, unknown>;
|
|
173
|
+
if (isUnknown(e.name)) {
|
|
174
|
+
// A computed variable name could be anything, so the entry is
|
|
175
|
+
// unreadable only when it also carries a literal value to judge.
|
|
176
|
+
if (typeof e.value === "string") unreadable.push(e.name.reason);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (typeof e.name !== "string") continue;
|
|
180
|
+
// A reference value is the remediation's neighbour, not a literal:
|
|
181
|
+
// skipped. Numbers and booleans are not credentials.
|
|
182
|
+
if (typeof e.value !== "string") continue;
|
|
183
|
+
|
|
184
|
+
const shape = secretShapedAssignment(e.name, e.value);
|
|
185
|
+
if (!shape) continue;
|
|
186
|
+
const why =
|
|
187
|
+
shape.reason === "name"
|
|
188
|
+
? "its name reads like a credential"
|
|
189
|
+
: "the value has the shape of a credential";
|
|
190
|
+
diagnostics.push({
|
|
191
|
+
checkId: "TF032",
|
|
192
|
+
severity: "error",
|
|
193
|
+
message:
|
|
194
|
+
`"${block.address}" container "${name}" passes "${e.name}" as a plaintext environment value ` +
|
|
195
|
+
`(${shape.length} characters, redacted here; ${why}). It is stored in the task definition, ` +
|
|
196
|
+
"visible to anyone who can describe it, and in state and plan output. Move it to the " +
|
|
197
|
+
"container's `secrets` with a `valueFrom` naming a Secrets Manager secret or SSM parameter, " +
|
|
198
|
+
"and rotate the committed value.",
|
|
199
|
+
entity: block.key,
|
|
200
|
+
lexicon: "terraform",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (unreadable.length > 0) {
|
|
205
|
+
diagnostics.push(
|
|
206
|
+
notDetermined(
|
|
207
|
+
block,
|
|
208
|
+
`container "${name}" \`environment\``,
|
|
209
|
+
`${unreadable.length} entr${unreadable.length === 1 ? "y is" : "ies are"} not literal (${unreadable[0]})`,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return diagnostics;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const tf032: PostSynthCheck = {
|
|
220
|
+
id: "TF032",
|
|
221
|
+
description: "ECS container definition passes a credential as a plaintext environment value",
|
|
222
|
+
|
|
223
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
224
|
+
return checkEcsPlaintextCredentials(ctx);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
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 { tf033 } from "./tf033";
|
|
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("TF033 over vendored real-world fixtures", () => {
|
|
15
|
+
test("storage_encrypted = true is silent (tmknom/example-pragmatic-terraform)", async () => {
|
|
16
|
+
expect(tf033.check(await loadFixture("TF033", "negative"))).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("storage_encrypted = false is an error (bridgecrewio/terragoat)", async () => {
|
|
20
|
+
const diags = tf033.check(await loadFixture("TF033", "positive"));
|
|
21
|
+
expect(verdicts(diags)).toEqual(["error TF033/aws_db_instance.default"]);
|
|
22
|
+
expect(diags[0]).toMatchObject({ checkId: "TF033", lexicon: "terraform" });
|
|
23
|
+
expect(diags[0].message).toContain("sets `storage_encrypted = false`");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("an instance that does not set storage_encrypted is an error; snapshot_identifier = \"\" is no source (zoitech)", async () => {
|
|
27
|
+
const diags = tf033.check(await loadFixture("TF033", "positive-absent"));
|
|
28
|
+
expect(verdicts(diags)).toEqual(["error TF033/aws_db_instance.postgres"]);
|
|
29
|
+
expect(diags[0].message).toContain("the provider default is `false`");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("an Aurora cluster without storage_encrypted is not determined (bridgecrewio/terragoat)", async () => {
|
|
33
|
+
const diags = tf033.check(await loadFixture("TF033", "not-determined-aurora"));
|
|
34
|
+
expect(verdicts(diags)).toEqual(["info TF033/aws_rds_cluster.app1-rds-cluster"]);
|
|
35
|
+
expect(diags[0].message).toMatch(/^Not determined: /);
|
|
36
|
+
expect(diags[0].message).toContain("2026-02-16");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("TF033 cases the fixtures do not isolate", () => {
|
|
41
|
+
test("an instance created from a source is not determined unless storage_encrypted = true", async () => {
|
|
42
|
+
const diags = tf033.check(
|
|
43
|
+
await inline(`
|
|
44
|
+
resource "aws_db_instance" "replica" {
|
|
45
|
+
replicate_source_db = aws_db_instance.primary.identifier
|
|
46
|
+
}
|
|
47
|
+
resource "aws_db_instance" "restored" {
|
|
48
|
+
snapshot_identifier = "rds:prod-2026-01-01"
|
|
49
|
+
storage_encrypted = false
|
|
50
|
+
}
|
|
51
|
+
resource "aws_db_instance" "pitr" {
|
|
52
|
+
restore_to_point_in_time {
|
|
53
|
+
use_latest_restorable_time = true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
resource "aws_db_instance" "explicit" {
|
|
57
|
+
snapshot_identifier = "rds:prod-2026-01-01"
|
|
58
|
+
storage_encrypted = true
|
|
59
|
+
}`),
|
|
60
|
+
);
|
|
61
|
+
expect(verdicts(diags)).toEqual([
|
|
62
|
+
"info app/aws_db_instance.pitr",
|
|
63
|
+
"info app/aws_db_instance.replica",
|
|
64
|
+
"info app/aws_db_instance.restored",
|
|
65
|
+
]);
|
|
66
|
+
expect(diags.find((d) => d.entity === "app/aws_db_instance.replica")!.message).toContain("`replicate_source_db`");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a storage_encrypted expression is not determined; the string \"true\" is true", async () => {
|
|
70
|
+
const diags = tf033.check(
|
|
71
|
+
await inline(`
|
|
72
|
+
resource "aws_db_instance" "v" {
|
|
73
|
+
storage_encrypted = var.encrypted
|
|
74
|
+
}
|
|
75
|
+
resource "aws_db_instance" "s" {
|
|
76
|
+
storage_encrypted = "true"
|
|
77
|
+
}`),
|
|
78
|
+
);
|
|
79
|
+
expect(verdicts(diags)).toEqual(["info app/aws_db_instance.v"]);
|
|
80
|
+
expect(diags[0].message).toContain("`storage_encrypted` is an expression (var.encrypted)");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a Multi-AZ DB cluster defaults to unencrypted; Aurora false and an engine expression are not determined", async () => {
|
|
84
|
+
const diags = tf033.check(
|
|
85
|
+
await inline(`
|
|
86
|
+
resource "aws_rds_cluster" "multi_az" {
|
|
87
|
+
engine = "postgres"
|
|
88
|
+
db_cluster_instance_class = "db.r6gd.large"
|
|
89
|
+
}
|
|
90
|
+
resource "aws_rds_cluster" "multi_az_off" {
|
|
91
|
+
engine = "mysql"
|
|
92
|
+
storage_encrypted = false
|
|
93
|
+
}
|
|
94
|
+
resource "aws_rds_cluster" "aurora_off" {
|
|
95
|
+
engine = "aurora-postgresql"
|
|
96
|
+
storage_encrypted = false
|
|
97
|
+
}
|
|
98
|
+
resource "aws_rds_cluster" "engine_var" {
|
|
99
|
+
engine = var.engine
|
|
100
|
+
}
|
|
101
|
+
resource "aws_rds_cluster" "restored" {
|
|
102
|
+
engine = "postgres"
|
|
103
|
+
snapshot_identifier = "arn:aws:rds:eu-west-1:111122223333:cluster-snapshot:x"
|
|
104
|
+
}
|
|
105
|
+
resource "aws_rds_cluster" "serverless_v1" {
|
|
106
|
+
engine = "aurora-mysql"
|
|
107
|
+
engine_mode = "serverless"
|
|
108
|
+
}
|
|
109
|
+
resource "aws_rds_cluster" "on" {
|
|
110
|
+
engine = "postgres"
|
|
111
|
+
storage_encrypted = true
|
|
112
|
+
}`),
|
|
113
|
+
);
|
|
114
|
+
expect(verdicts(diags)).toEqual([
|
|
115
|
+
"error app/aws_rds_cluster.multi_az",
|
|
116
|
+
"error app/aws_rds_cluster.multi_az_off",
|
|
117
|
+
"info app/aws_rds_cluster.aurora_off",
|
|
118
|
+
"info app/aws_rds_cluster.engine_var",
|
|
119
|
+
"info app/aws_rds_cluster.restored",
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("aws_rds_cluster_instance is never read", async () => {
|
|
124
|
+
expect(tf033.check(await inline(`resource "aws_rds_cluster_instance" "i" { cluster_identifier = "c" }`))).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF033: an RDS DB instance or cluster does not encrypt its storage
|
|
3
|
+
* (chant #2288, epic #2284).
|
|
4
|
+
*
|
|
5
|
+
* The Terraform counterpart of the aws lexicon's WAW021, written natively.
|
|
6
|
+
* WAW021 reads a template chant synthesized, where `StorageEncrypted` absent
|
|
7
|
+
* means false. Reading somebody else's HCL, absence means different things
|
|
8
|
+
* for the two resources and for how the database is created, so each case is
|
|
9
|
+
* decided here against the provider schema and the AWS default.
|
|
10
|
+
*
|
|
11
|
+
* `aws_db_instance` (provider `internal/service/rds/instance.go`, v6.67.0):
|
|
12
|
+
* `storage_encrypted` is optional, not computed, and the docs say "The
|
|
13
|
+
* default is `false` if not specified". The plain create path and `s3_import`
|
|
14
|
+
* send `StorageEncrypted: d.Get("storage_encrypted")`, so absent means false
|
|
15
|
+
* and is reported. The replica (`replicate_source_db`), snapshot
|
|
16
|
+
* (`snapshot_identifier`) and point-in-time (`restore_to_point_in_time`)
|
|
17
|
+
* paths never send it: the new instance takes its encryption from the source
|
|
18
|
+
* (and a cross-region replica from `kms_key_id`). For those the rule cannot
|
|
19
|
+
* know, and says so, unless `storage_encrypted = true` is written down.
|
|
20
|
+
*
|
|
21
|
+
* `aws_rds_cluster` (`cluster.go`): `storage_encrypted` is optional and
|
|
22
|
+
* computed, sent only when set, and "Terraform will only perform drift
|
|
23
|
+
* detection if a configuration value is provided". Two engine families:
|
|
24
|
+
*
|
|
25
|
+
* - Aurora (`engine` `aurora-mysql`/`aurora-postgresql`, or absent, which
|
|
26
|
+
* provider v5 defaulted to `aurora`). Since 2026-02-16 AWS encrypts every
|
|
27
|
+
* new Aurora cluster at rest with an AWS-owned key whatever
|
|
28
|
+
* `StorageEncrypted` says, and reports `StorageEncrypted: false` with
|
|
29
|
+
* `StorageEncryptionType: sse-rds` for it. A cluster created before that
|
|
30
|
+
* date with storage encryption off is unencrypted, and the configuration
|
|
31
|
+
* does not say when its cluster was created. So an Aurora cluster with
|
|
32
|
+
* `storage_encrypted` absent or `false` is not determined, not a finding.
|
|
33
|
+
* `engine_mode = "serverless"` (Serverless v1) was always encrypted and
|
|
34
|
+
* is not reported at all.
|
|
35
|
+
* - Multi-AZ DB clusters (`engine` `mysql`/`postgres`): the default is
|
|
36
|
+
* `false` and nothing changed it, so absent or `false` is a finding,
|
|
37
|
+
* unless the cluster is restored or replicated from a source
|
|
38
|
+
* (`snapshot_identifier`, `restore_to_point_in_time`,
|
|
39
|
+
* `replication_source_identifier`, `global_cluster_identifier`), which
|
|
40
|
+
* again decides the encryption.
|
|
41
|
+
*
|
|
42
|
+
* Not checked: `aws_rds_cluster_instance` (its `storage_encrypted` is
|
|
43
|
+
* computed-only and never appears in a body), `aws_rds_global_cluster`, and
|
|
44
|
+
* whether the key is customer managed.
|
|
45
|
+
*
|
|
46
|
+
* Scope: root and child modules alike (#2112).
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
50
|
+
import type { BlockBody } from "../../hcl/parse";
|
|
51
|
+
import type { TerraformBlock } from "./blocks";
|
|
52
|
+
import { finding, isGiven, notDetermined, readBool, readString, resourcesOfType, type Read } from "./flat-attributes";
|
|
53
|
+
|
|
54
|
+
const ID = "TF033";
|
|
55
|
+
|
|
56
|
+
const NOT_DETERMINED_TAIL =
|
|
57
|
+
"TF033 does not evaluate expressions or read the source database, so it cannot tell whether this storage is encrypted.";
|
|
58
|
+
|
|
59
|
+
/** The first source argument a body gives, if any. */
|
|
60
|
+
function sourceOf(body: BlockBody, names: readonly string[]): string | undefined {
|
|
61
|
+
return names.find((n) => isGiven(body, n));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const INSTANCE_SOURCES = ["replicate_source_db", "snapshot_identifier", "restore_to_point_in_time"] as const;
|
|
65
|
+
const CLUSTER_SOURCES = [
|
|
66
|
+
"snapshot_identifier",
|
|
67
|
+
"restore_to_point_in_time",
|
|
68
|
+
"replication_source_identifier",
|
|
69
|
+
"global_cluster_identifier",
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
72
|
+
function unencrypted(block: TerraformBlock, encrypted: Read<boolean>, what: string): PostSynthDiagnostic {
|
|
73
|
+
const how =
|
|
74
|
+
encrypted.kind === "absent"
|
|
75
|
+
? "does not set `storage_encrypted`, and the provider default is `false`"
|
|
76
|
+
: "sets `storage_encrypted = false`";
|
|
77
|
+
return finding(
|
|
78
|
+
ID,
|
|
79
|
+
"error",
|
|
80
|
+
block,
|
|
81
|
+
`${how}, so the ${what} stores its data unencrypted. Set \`storage_encrypted = true\` (and \`kms_key_id\` for a ` +
|
|
82
|
+
"customer managed key). Encryption cannot be turned on in place: an existing database is migrated by restoring " +
|
|
83
|
+
"an encrypted copy of a snapshot.",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function checkInstance(block: TerraformBlock): PostSynthDiagnostic | undefined {
|
|
88
|
+
const encrypted = readBool(block.body, "storage_encrypted");
|
|
89
|
+
if (encrypted.kind === "known" && encrypted.value) return undefined;
|
|
90
|
+
if (encrypted.kind === "unknown") return notDetermined(ID, block, encrypted.reason, NOT_DETERMINED_TAIL);
|
|
91
|
+
const source = sourceOf(block.body, INSTANCE_SOURCES);
|
|
92
|
+
if (source) {
|
|
93
|
+
return notDetermined(
|
|
94
|
+
ID,
|
|
95
|
+
block,
|
|
96
|
+
`is created from a source (\`${source}\`), and the provider does not send \`storage_encrypted\` on that path: ` +
|
|
97
|
+
"the instance takes its encryption from the source",
|
|
98
|
+
NOT_DETERMINED_TAIL,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return unencrypted(block, encrypted, "DB instance");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function checkCluster(block: TerraformBlock): PostSynthDiagnostic | undefined {
|
|
105
|
+
const encrypted = readBool(block.body, "storage_encrypted");
|
|
106
|
+
if (encrypted.kind === "known" && encrypted.value) return undefined;
|
|
107
|
+
if (encrypted.kind === "unknown") return notDetermined(ID, block, encrypted.reason, NOT_DETERMINED_TAIL);
|
|
108
|
+
|
|
109
|
+
const engine = readString(block.body, "engine");
|
|
110
|
+
if (engine.kind === "unknown") {
|
|
111
|
+
return notDetermined(
|
|
112
|
+
ID,
|
|
113
|
+
block,
|
|
114
|
+
`${engine.reason}, and whether an unset \`storage_encrypted\` means unencrypted depends on the engine`,
|
|
115
|
+
NOT_DETERMINED_TAIL,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const aurora = engine.kind === "absent" || engine.value.startsWith("aurora");
|
|
119
|
+
if (aurora) {
|
|
120
|
+
const mode = readString(block.body, "engine_mode");
|
|
121
|
+
if (mode.kind === "known" && mode.value === "serverless") return undefined;
|
|
122
|
+
const set = encrypted.kind === "absent" ? "does not set `storage_encrypted`" : "sets `storage_encrypted = false`";
|
|
123
|
+
return notDetermined(
|
|
124
|
+
ID,
|
|
125
|
+
block,
|
|
126
|
+
`is an Aurora cluster that ${set}. AWS encrypts every Aurora cluster created on or after 2026-02-16 with an ` +
|
|
127
|
+
"AWS-owned key whatever this says; one created earlier with storage encryption off is unencrypted, and the " +
|
|
128
|
+
"configuration does not say when the cluster was created",
|
|
129
|
+
"Set `storage_encrypted = true` to make the answer explicit.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const source = sourceOf(block.body, CLUSTER_SOURCES);
|
|
133
|
+
if (source) {
|
|
134
|
+
return notDetermined(
|
|
135
|
+
ID,
|
|
136
|
+
block,
|
|
137
|
+
`is created from a source (\`${source}\`), which decides its encryption`,
|
|
138
|
+
NOT_DETERMINED_TAIL,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return unencrypted(block, encrypted, "Multi-AZ DB cluster");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const tf033: PostSynthCheck = {
|
|
145
|
+
id: "TF033",
|
|
146
|
+
description: "RDS DB instance or cluster storage is not encrypted",
|
|
147
|
+
|
|
148
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
149
|
+
const out: PostSynthDiagnostic[] = [];
|
|
150
|
+
for (const block of resourcesOfType(ctx, "aws_db_instance")) {
|
|
151
|
+
const d = checkInstance(block);
|
|
152
|
+
if (d) out.push(d);
|
|
153
|
+
}
|
|
154
|
+
for (const block of resourcesOfType(ctx, "aws_rds_cluster")) {
|
|
155
|
+
const d = checkCluster(block);
|
|
156
|
+
if (d) out.push(d);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
},
|
|
160
|
+
};
|