@intentius/chant-lexicon-terraform 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/composites/terraform-adopt-op.d.ts +131 -0
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
- package/dist/composites/terraform-apply-op.d.ts +70 -17
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +104 -37
- package/dist/composites/terraform-watch-op.d.ts.map +1 -1
- package/dist/config.d.ts +49 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +256 -16
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/hcl/descend.d.ts +98 -0
- package/dist/hcl/descend.d.ts.map +1 -0
- package/dist/hcl/marker.d.ts +130 -0
- package/dist/hcl/marker.d.ts.map +1 -0
- package/dist/hcl/parse.d.ts +156 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/references.d.ts +76 -0
- package/dist/hcl/references.d.ts.map +1 -0
- package/dist/hcl/roots.d.ts +22 -3
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts +54 -0
- package/dist/hcl/suppressions.d.ts.map +1 -0
- package/dist/hcl/value.d.ts +77 -0
- package/dist/hcl/value.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +34 -5
- package/dist/lint/audit-catalog.d.ts +3 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +95 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/blocks.d.ts +53 -0
- package/dist/lint/post-synth/blocks.d.ts.map +1 -0
- package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
- package/dist/lint/post-synth/fixtures/load.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 +38 -0
- package/dist/lint/post-synth/module-source.d.ts.map +1 -0
- package/dist/lint/post-synth/scope.d.ts +65 -0
- package/dist/lint/post-synth/scope.d.ts.map +1 -0
- package/dist/lint/post-synth/tf001.d.ts +58 -6
- package/dist/lint/post-synth/tf001.d.ts.map +1 -1
- package/dist/lint/post-synth/tf002.d.ts +32 -0
- package/dist/lint/post-synth/tf002.d.ts.map +1 -0
- package/dist/lint/post-synth/tf003.d.ts +22 -0
- package/dist/lint/post-synth/tf003.d.ts.map +1 -0
- package/dist/lint/post-synth/tf004.d.ts +22 -0
- package/dist/lint/post-synth/tf004.d.ts.map +1 -0
- package/dist/lint/post-synth/tf005.d.ts +28 -0
- package/dist/lint/post-synth/tf005.d.ts.map +1 -0
- package/dist/lint/post-synth/tf006.d.ts +21 -0
- package/dist/lint/post-synth/tf006.d.ts.map +1 -0
- package/dist/lint/post-synth/tf007.d.ts +21 -0
- package/dist/lint/post-synth/tf007.d.ts.map +1 -0
- package/dist/lint/post-synth/tf008.d.ts +22 -0
- package/dist/lint/post-synth/tf008.d.ts.map +1 -0
- package/dist/lint/post-synth/tf009.d.ts +24 -0
- package/dist/lint/post-synth/tf009.d.ts.map +1 -0
- package/dist/lint/post-synth/tf010.d.ts +19 -0
- package/dist/lint/post-synth/tf010.d.ts.map +1 -0
- package/dist/lint/post-synth/tf011.d.ts +18 -0
- package/dist/lint/post-synth/tf011.d.ts.map +1 -0
- package/dist/lint/post-synth/tf012.d.ts +17 -0
- package/dist/lint/post-synth/tf012.d.ts.map +1 -0
- package/dist/lint/post-synth/tf013.d.ts +21 -0
- package/dist/lint/post-synth/tf013.d.ts.map +1 -0
- package/dist/lint/post-synth/tf014.d.ts +25 -0
- package/dist/lint/post-synth/tf014.d.ts.map +1 -0
- package/dist/lint/post-synth/tf015.d.ts +30 -0
- package/dist/lint/post-synth/tf015.d.ts.map +1 -0
- package/dist/lint/post-synth/tf016.d.ts +30 -0
- package/dist/lint/post-synth/tf016.d.ts.map +1 -0
- package/dist/lint/post-synth/tf017.d.ts +19 -0
- package/dist/lint/post-synth/tf017.d.ts.map +1 -0
- package/dist/lint/post-synth/tf018.d.ts +20 -0
- package/dist/lint/post-synth/tf018.d.ts.map +1 -0
- package/dist/lint/post-synth/tf019.d.ts +25 -0
- package/dist/lint/post-synth/tf019.d.ts.map +1 -0
- package/dist/lint/post-synth/tf020.d.ts +38 -0
- package/dist/lint/post-synth/tf020.d.ts.map +1 -0
- package/dist/lint/post-synth/tf021.d.ts +41 -0
- package/dist/lint/post-synth/tf021.d.ts.map +1 -0
- package/dist/lint/post-synth/tf022.d.ts +24 -0
- package/dist/lint/post-synth/tf022.d.ts.map +1 -0
- package/dist/lint/post-synth/tf024.d.ts +24 -0
- package/dist/lint/post-synth/tf024.d.ts.map +1 -0
- package/dist/lint/post-synth/tf025.d.ts +29 -0
- package/dist/lint/post-synth/tf025.d.ts.map +1 -0
- package/dist/lint/post-synth/tf026.d.ts +28 -0
- package/dist/lint/post-synth/tf026.d.ts.map +1 -0
- package/dist/lint/post-synth/tf027.d.ts +30 -0
- package/dist/lint/post-synth/tf027.d.ts.map +1 -0
- package/dist/lint/post-synth/tf028.d.ts +33 -0
- package/dist/lint/post-synth/tf028.d.ts.map +1 -0
- package/dist/lint/rules/fixtures/load.d.ts +13 -0
- package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
- package/dist/lint/rules/plan-before-apply.d.ts +11 -0
- package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
- package/dist/lint/secret-shape.d.ts +68 -0
- package/dist/lint/secret-shape.d.ts.map +1 -0
- package/dist/live-ownership.d.ts +55 -0
- package/dist/live-ownership.d.ts.map +1 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +26 -0
- package/dist/okf/rules/TF002.md +11 -0
- package/dist/okf/rules/TF003.md +11 -0
- package/dist/okf/rules/TF004.md +11 -0
- package/dist/okf/rules/TF005.md +11 -0
- package/dist/okf/rules/TF006.md +11 -0
- package/dist/okf/rules/TF007.md +11 -0
- package/dist/okf/rules/TF008.md +11 -0
- package/dist/okf/rules/TF009.md +11 -0
- package/dist/okf/rules/TF010.md +11 -0
- package/dist/okf/rules/TF011.md +11 -0
- package/dist/okf/rules/TF012.md +11 -0
- package/dist/okf/rules/TF013.md +11 -0
- package/dist/okf/rules/TF014.md +11 -0
- package/dist/okf/rules/TF015.md +11 -0
- package/dist/okf/rules/TF016.md +11 -0
- package/dist/okf/rules/TF017.md +11 -0
- package/dist/okf/rules/TF018.md +11 -0
- package/dist/okf/rules/TF019.md +11 -0
- package/dist/okf/rules/TF020.md +11 -0
- package/dist/okf/rules/TF021.md +11 -0
- package/dist/okf/rules/TF022.md +11 -0
- package/dist/okf/rules/TF024.md +11 -0
- package/dist/okf/rules/TF025.md +11 -0
- package/dist/okf/rules/TF026.md +11 -0
- package/dist/okf/rules/TF027.md +11 -0
- package/dist/okf/rules/TF028.md +11 -0
- package/dist/op/activities/index.d.ts +13 -6
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/live-detect.d.ts +51 -0
- package/dist/op/activities/live-detect.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +466 -17
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +148 -0
- package/dist/op/activity-contracts.d.ts.map +1 -0
- package/dist/op/adoption.d.ts +118 -0
- package/dist/op/adoption.d.ts.map +1 -0
- package/dist/op/builders.d.ts +48 -6
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +59 -0
- package/dist/op/resolve-root-mode.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/blocks.ts +109 -0
- package/dist/rules/module-source.ts +121 -0
- package/dist/rules/plan-before-apply.ts +11 -0
- package/dist/rules/scope.ts +107 -0
- package/dist/rules/tf001.ts +98 -18
- package/dist/rules/tf002.ts +151 -0
- package/dist/rules/tf003.ts +71 -0
- package/dist/rules/tf004.ts +63 -0
- package/dist/rules/tf005.ts +76 -0
- package/dist/rules/tf006.ts +53 -0
- package/dist/rules/tf007.ts +78 -0
- package/dist/rules/tf008.ts +75 -0
- package/dist/rules/tf009.ts +59 -0
- package/dist/rules/tf010.ts +51 -0
- package/dist/rules/tf011.ts +49 -0
- package/dist/rules/tf012.ts +47 -0
- package/dist/rules/tf013.ts +63 -0
- package/dist/rules/tf014.ts +72 -0
- package/dist/rules/tf015.ts +72 -0
- package/dist/rules/tf016.ts +75 -0
- package/dist/rules/tf017.ts +50 -0
- package/dist/rules/tf018.ts +72 -0
- package/dist/rules/tf019.ts +72 -0
- package/dist/rules/tf020.ts +123 -0
- package/dist/rules/tf021.ts +126 -0
- package/dist/rules/tf022.ts +61 -0
- package/dist/rules/tf024.ts +70 -0
- package/dist/rules/tf025.ts +101 -0
- package/dist/rules/tf026.ts +84 -0
- package/dist/rules/tf027.ts +74 -0
- package/dist/rules/tf028.ts +96 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/__fixtures__/ACCEPTANCE.md +181 -0
- package/src/__fixtures__/live/main.tf +28 -0
- package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
- package/src/__fixtures__/live-adopt/main.tf +21 -0
- package/src/__fixtures__/live-estate/README.md +58 -0
- package/src/__fixtures__/live-estate/compute.tf +10 -0
- package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-estate/logs.tf +8 -0
- package/src/__fixtures__/live-estate/network.tf +24 -0
- package/src/__fixtures__/live-estate/rule.tf +11 -0
- package/src/__fixtures__/live-estate/unowned.tf +21 -0
- package/src/__fixtures__/live-estate/versions.tf +31 -0
- package/src/__fixtures__/live-ls.json +98 -0
- package/src/__fixtures__/live-plan.json +285 -0
- package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
- package/src/__fixtures__/live-sidecar/main.tf +24 -0
- package/src/__fixtures__/module-tree/README.md +16 -0
- package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
- package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
- package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
- package/src/__fixtures__/module-tree/missing/main.tf +9 -0
- package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
- package/src/__fixtures__/module-tree/root/main.tf +28 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
- package/src/__fixtures__/module-tree/shared/main.tf +5 -0
- package/src/__fixtures__/references/main.tf +56 -0
- package/src/__fixtures__/remote-backend/main.tf +34 -0
- package/src/__fixtures__/with-backend/main.tf +7 -0
- package/src/codegen/lint-rules-page.test.ts +45 -0
- package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
- package/src/composites/terraform-adopt-op.test.ts +175 -0
- package/src/composites/terraform-adopt-op.ts +202 -0
- package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
- package/src/composites/terraform-apply-op.test.ts +179 -19
- package/src/composites/terraform-apply-op.ts +123 -30
- package/src/composites/terraform-watch-op.test.ts +169 -29
- package/src/composites/terraform-watch-op.ts +183 -75
- package/src/config.ts +32 -3
- package/src/describe-resources.live.test.ts +598 -0
- package/src/describe-resources.test.ts +53 -0
- package/src/describe-resources.ts +811 -28
- package/src/hcl/descend.test.ts +162 -0
- package/src/hcl/descend.ts +273 -0
- package/src/hcl/marker.test.ts +147 -0
- package/src/hcl/marker.ts +235 -0
- package/src/hcl/parse.test.ts +69 -0
- package/src/hcl/parse.ts +269 -27
- package/src/hcl/references.test.ts +116 -0
- package/src/hcl/references.ts +172 -0
- package/src/hcl/roots.ts +59 -5
- package/src/hcl/suppressions.test.ts +147 -0
- package/src/hcl/suppressions.ts +204 -0
- package/src/hcl/value.test.ts +73 -0
- package/src/hcl/value.ts +106 -0
- package/src/index.ts +31 -1
- package/src/lint/audit-catalog.ts +288 -3
- package/src/lint/audit-lineage.ts +384 -1
- package/src/lint/audit.test.ts +210 -3
- package/src/lint/post-synth/blocks.ts +109 -0
- package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
- package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
- package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
- package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
- package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
- package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
- package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
- package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
- package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
- package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
- package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
- package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
- package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
- package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
- package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
- package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
- package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
- package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
- package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
- package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
- package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
- package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
- package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
- package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
- package/src/lint/post-synth/fixtures/load.ts +108 -0
- package/src/lint/post-synth/index.ts +52 -0
- package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
- package/src/lint/post-synth/module-rules.test.ts +153 -0
- package/src/lint/post-synth/module-source.test.ts +75 -0
- package/src/lint/post-synth/module-source.ts +121 -0
- package/src/lint/post-synth/post-synth.test.ts +73 -16
- package/src/lint/post-synth/scope.ts +107 -0
- package/src/lint/post-synth/single-block.test.ts +263 -0
- package/src/lint/post-synth/suppression.test.ts +110 -0
- package/src/lint/post-synth/tf001.ts +98 -18
- package/src/lint/post-synth/tf002.test.ts +93 -0
- package/src/lint/post-synth/tf002.ts +151 -0
- package/src/lint/post-synth/tf003.test.ts +75 -0
- package/src/lint/post-synth/tf003.ts +71 -0
- package/src/lint/post-synth/tf004.test.ts +47 -0
- package/src/lint/post-synth/tf004.ts +63 -0
- package/src/lint/post-synth/tf005.test.ts +57 -0
- package/src/lint/post-synth/tf005.ts +76 -0
- package/src/lint/post-synth/tf006.ts +53 -0
- package/src/lint/post-synth/tf007.ts +78 -0
- package/src/lint/post-synth/tf008.ts +75 -0
- package/src/lint/post-synth/tf009.ts +59 -0
- package/src/lint/post-synth/tf010.ts +51 -0
- package/src/lint/post-synth/tf011.ts +49 -0
- package/src/lint/post-synth/tf012.ts +47 -0
- package/src/lint/post-synth/tf013.ts +63 -0
- package/src/lint/post-synth/tf014.ts +72 -0
- package/src/lint/post-synth/tf015.ts +72 -0
- package/src/lint/post-synth/tf016.ts +75 -0
- package/src/lint/post-synth/tf017.ts +50 -0
- package/src/lint/post-synth/tf018.ts +72 -0
- package/src/lint/post-synth/tf019.ts +72 -0
- package/src/lint/post-synth/tf020.ts +123 -0
- package/src/lint/post-synth/tf021.ts +126 -0
- package/src/lint/post-synth/tf022.ts +61 -0
- package/src/lint/post-synth/tf024.test.ts +42 -0
- package/src/lint/post-synth/tf024.ts +70 -0
- package/src/lint/post-synth/tf025.test.ts +46 -0
- package/src/lint/post-synth/tf025.ts +101 -0
- package/src/lint/post-synth/tf026.test.ts +56 -0
- package/src/lint/post-synth/tf026.ts +84 -0
- package/src/lint/post-synth/tf027.test.ts +62 -0
- package/src/lint/post-synth/tf027.ts +74 -0
- package/src/lint/post-synth/tf028.test.ts +92 -0
- package/src/lint/post-synth/tf028.ts +96 -0
- package/src/lint/presets.test.ts +72 -0
- package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
- package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
- package/src/lint/rules/fixtures/load.ts +53 -0
- package/src/lint/rules/plan-before-apply.test.ts +112 -7
- package/src/lint/rules/plan-before-apply.ts +11 -0
- package/src/lint/secret-shape.test.ts +150 -0
- package/src/lint/secret-shape.ts +159 -0
- package/src/live-ownership.ts +60 -0
- package/src/lsp/completions.test.ts +3 -2
- package/src/lsp/option-keys.ts +12 -6
- package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
- package/src/op/activities/choudoufu.test.ts +725 -0
- package/src/op/activities/index.ts +40 -4
- package/src/op/activities/live-detect.test.ts +143 -0
- package/src/op/activities/live-detect.ts +162 -0
- package/src/op/activities/registry.test.ts +11 -3
- package/src/op/activities/terraform.test.ts +7 -3
- package/src/op/activities/terraform.ts +861 -69
- package/src/op/activity-contracts.test.ts +147 -0
- package/src/op/activity-contracts.ts +178 -0
- package/src/op/adoption.test.ts +170 -0
- package/src/op/adoption.ts +241 -0
- package/src/op/builders.test.ts +60 -5
- package/src/op/builders.ts +95 -5
- package/src/op/resolve-root-mode.test.ts +85 -0
- package/src/op/resolve-root-mode.ts +86 -0
- package/src/plugin.test.ts +106 -0
- package/src/plugin.ts +186 -29
- package/src/skills/chant-terraform.md +1 -1
- package/src/validate.ts +10 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF022: a credential-named attribute of a resource or data source holds a
|
|
3
|
+
* literal.
|
|
4
|
+
*
|
|
5
|
+
* tfsec's `general-secrets-sensitive-in-attribute`, consolidated in its v1
|
|
6
|
+
* line as `no-plaintext-exposure`, and KICS's `Passwords And Secrets` query.
|
|
7
|
+
* Broader and noisier than TF007, because a resource body has far more
|
|
8
|
+
* attributes than a variable has defaults, so the name heuristic does the
|
|
9
|
+
* gating: an attribute has to read like a credential AND hold a committed
|
|
10
|
+
* literal before it is reported.
|
|
11
|
+
*
|
|
12
|
+
* The value is in git history, in state, and in every plan output, and the
|
|
13
|
+
* blast radius is whatever the resource is: an RDS master password, a
|
|
14
|
+
* provider API token, a Kubernetes secret's data. The fix is always the same
|
|
15
|
+
* shape, a reference rather than a constant, which is what the message says
|
|
16
|
+
* rather than "remove this".
|
|
17
|
+
*
|
|
18
|
+
* Scope: root and child modules alike (#2112). The condition is a property
|
|
19
|
+
* of the block itself, so a descended module's block is read exactly as a
|
|
20
|
+
* root's is.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
PostSynthCheck,
|
|
25
|
+
PostSynthContext,
|
|
26
|
+
PostSynthDiagnostic,
|
|
27
|
+
} from "@intentius/chant/lint/post-synth";
|
|
28
|
+
import { DATA_TYPE, RESOURCE_TYPE } from "../../hcl/parse";
|
|
29
|
+
import { isSecretName, secretShapedAssignment } from "../secret-shape";
|
|
30
|
+
import { blocksOfTypes, walkAttributes } from "./blocks";
|
|
31
|
+
|
|
32
|
+
export const tf022: PostSynthCheck = {
|
|
33
|
+
id: "TF022",
|
|
34
|
+
description: "Credential-named resource attribute holds a plaintext literal",
|
|
35
|
+
|
|
36
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
37
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
38
|
+
|
|
39
|
+
for (const block of blocksOfTypes(ctx, [RESOURCE_TYPE, DATA_TYPE])) {
|
|
40
|
+
for (const attr of walkAttributes(block.body)) {
|
|
41
|
+
if (!isSecretName(attr.name)) continue;
|
|
42
|
+
const shape = secretShapedAssignment(attr.name, attr.value);
|
|
43
|
+
if (!shape) continue;
|
|
44
|
+
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
checkId: "TF022",
|
|
47
|
+
severity: "error",
|
|
48
|
+
message:
|
|
49
|
+
`"${block.address}" sets \`${attr.path}\` to a plaintext literal (${shape.length} ` +
|
|
50
|
+
"characters, redacted here). The value is in git history, in state, and in plan output. " +
|
|
51
|
+
"Replace it with a reference: a `sensitive` variable, or a data source that reads the " +
|
|
52
|
+
"value from a secret manager at apply time.",
|
|
53
|
+
entity: `${block.key}.${attr.path}`,
|
|
54
|
+
lexicon: "terraform",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return diagnostics;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
5
|
+
import { tf024 } from "./tf024";
|
|
6
|
+
import { parseTerraformRootDir } from "../../hcl/parse";
|
|
7
|
+
|
|
8
|
+
const fixtures = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "TF024");
|
|
9
|
+
|
|
10
|
+
async function ctxFor(fixture: string, root = "estate"): Promise<PostSynthContext> {
|
|
11
|
+
const entities = await parseTerraformRootDir(join(fixtures, fixture), root, undefined, { binary: "choudoufu" });
|
|
12
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("TF024: live root declares a backend or cloud block", () => {
|
|
16
|
+
test("flags a live root with a backend block", async () => {
|
|
17
|
+
const diags = tf024.check(await ctxFor("live-with-backend"));
|
|
18
|
+
expect(diags).toHaveLength(1);
|
|
19
|
+
expect(diags[0].checkId).toBe("TF024");
|
|
20
|
+
expect(diags[0].message).toContain("backend");
|
|
21
|
+
expect(diags[0].message).toContain("Both a backend and a live");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("flags a live root with a cloud block", async () => {
|
|
25
|
+
const diags = tf024.check(await ctxFor("live-with-cloud"));
|
|
26
|
+
expect(diags).toHaveLength(1);
|
|
27
|
+
expect(diags[0].message).toContain("cloud");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("passes a live root with neither block", async () => {
|
|
31
|
+
const diags = tf024.check(await ctxFor("live-clean"));
|
|
32
|
+
expect(diags).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("does not fire on a stock root with a backend block (that is TF001's territory)", async () => {
|
|
36
|
+
const entities = await parseTerraformRootDir(join(fixtures, "live-with-backend"), "estate", undefined, {
|
|
37
|
+
binary: "terraform", // no estate takes effect: this root stays state mode
|
|
38
|
+
});
|
|
39
|
+
const ctx = { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
40
|
+
expect(tf024.check(ctx)).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF024: a live root declares a `backend` or `cloud` block.
|
|
3
|
+
*
|
|
4
|
+
* choudoufu refuses this combination at `init`, before any command runs,
|
|
5
|
+
* naming both blocks that collide: "Both a backend and a live configuration
|
|
6
|
+
* are present" (a `backend` block) or "Both a cloud and a live configuration
|
|
7
|
+
* are present" (a `cloud` block). A live root keeps no state to store, since
|
|
8
|
+
* prior state is a projection rebuilt from the live system every run, so
|
|
9
|
+
* either block is a contradiction about where the truth lives, not a
|
|
10
|
+
* redundant setting.
|
|
11
|
+
*
|
|
12
|
+
* TF001 is the equivalent check for a stock root (no remote backend is
|
|
13
|
+
* configured); it explicitly does not fire on a live root, and this check is
|
|
14
|
+
* why: the two are mutually exclusive by construction; a root is flagged by
|
|
15
|
+
* at most one of them.
|
|
16
|
+
*
|
|
17
|
+
* Scope: root modules only (#2112). A child module's backend or cloud block
|
|
18
|
+
* is TF015's finding, whatever binary the root runs.
|
|
19
|
+
*
|
|
20
|
+
* One diagnostic per root, fired from the root's `terraform` block.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
PostSynthCheck,
|
|
25
|
+
PostSynthContext,
|
|
26
|
+
PostSynthDiagnostic,
|
|
27
|
+
} from "@intentius/chant/lint/post-synth";
|
|
28
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
29
|
+
import { TERRAFORM_TYPE, type BlockBody } from "../../hcl/parse";
|
|
30
|
+
import { hasBlock } from "./tf001";
|
|
31
|
+
import { isRootScoped } from "./scope";
|
|
32
|
+
|
|
33
|
+
export const tf024: PostSynthCheck = {
|
|
34
|
+
id: "TF024",
|
|
35
|
+
description: "Live root declares a backend or cloud block, which choudoufu refuses",
|
|
36
|
+
|
|
37
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
38
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
39
|
+
const flagged = new Set<string>();
|
|
40
|
+
|
|
41
|
+
for (const [name, entity] of ctx.entities) {
|
|
42
|
+
if (entity.entityType !== TERRAFORM_TYPE) continue;
|
|
43
|
+
if (!isResourceDeclarable(entity) || !isRootScoped(entity)) continue;
|
|
44
|
+
const props = entity.props as { root?: unknown; body?: unknown; mode?: unknown };
|
|
45
|
+
if (props.mode !== "live") continue;
|
|
46
|
+
const root = typeof props.root === "string" ? props.root : "";
|
|
47
|
+
if (flagged.has(root)) continue;
|
|
48
|
+
|
|
49
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
50
|
+
const hasBackend = hasBlock(body, "backend");
|
|
51
|
+
const hasCloud = hasBlock(body, "cloud");
|
|
52
|
+
if (!hasBackend && !hasCloud) continue;
|
|
53
|
+
|
|
54
|
+
flagged.add(root);
|
|
55
|
+
const blockKind = hasBackend ? "backend" : "cloud";
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
checkId: "TF024",
|
|
58
|
+
severity: "error",
|
|
59
|
+
message:
|
|
60
|
+
`Root module "${root}" runs choudoufu with a declared estate and also declares a ` +
|
|
61
|
+
`${blockKind} block. choudoufu refuses this at init: "Both a ${blockKind} and a live ` +
|
|
62
|
+
`configuration are present." Remove the ${blockKind} block; a live root keeps no state to store.`,
|
|
63
|
+
entity: name,
|
|
64
|
+
lexicon: "terraform",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return diagnostics;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
5
|
+
import { tf025 } from "./tf025";
|
|
6
|
+
import { parseTerraformRootDir, type TerraformRootModeOptions } from "../../hcl/parse";
|
|
7
|
+
|
|
8
|
+
const fixtures = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "TF025");
|
|
9
|
+
|
|
10
|
+
async function ctxFor(
|
|
11
|
+
fixture: string,
|
|
12
|
+
modeOptions: TerraformRootModeOptions = { binary: "choudoufu" },
|
|
13
|
+
root = "estate",
|
|
14
|
+
): Promise<PostSynthContext> {
|
|
15
|
+
const entities = await parseTerraformRootDir(join(fixtures, fixture), root, undefined, modeOptions);
|
|
16
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("TF025: live root references a non-default terraform.workspace", () => {
|
|
20
|
+
test("flags a `${terraform.workspace}` reference in a live root's HCL", async () => {
|
|
21
|
+
const diags = tf025.check(await ctxFor("live-with-workspace-ref"));
|
|
22
|
+
expect(diags).toHaveLength(1);
|
|
23
|
+
expect(diags[0].checkId).toBe("TF025");
|
|
24
|
+
expect(diags[0].message).toContain("terraform.workspace");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("passes a live root with no reference and no configured workspace", async () => {
|
|
28
|
+
expect(tf025.check(await ctxFor("live-clean"))).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("flags a live root whose terraform.roots entry configures a non-default workspace", async () => {
|
|
32
|
+
const diags = tf025.check(await ctxFor("live-clean", { binary: "choudoufu", workspace: "prod" }));
|
|
33
|
+
expect(diags).toHaveLength(1);
|
|
34
|
+
expect(diags[0].message).toContain('workspace "prod"');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a workspace of \"default\" is not flagged", async () => {
|
|
38
|
+
const diags = tf025.check(await ctxFor("live-clean", { binary: "choudoufu", workspace: "default" }));
|
|
39
|
+
expect(diags).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("does not fire on a stock root (no estate takes effect under a non-choudoufu binary)", async () => {
|
|
43
|
+
const diags = tf025.check(await ctxFor("live-with-workspace-ref", { binary: "terraform", workspace: "prod" }));
|
|
44
|
+
expect(diags).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF025: a live root reads a non-default `terraform.workspace`, or configures
|
|
3
|
+
* one through `terraform.roots`.
|
|
4
|
+
*
|
|
5
|
+
* choudoufu refuses any workspace but `default` on a live root, and refuses
|
|
6
|
+
* `workspace new`/`workspace select` outright: only the default workspace
|
|
7
|
+
* exists there ("How you run it" in choudoufu's compatibility reference).
|
|
8
|
+
* Two independent ways a configuration ends up asking for one:
|
|
9
|
+
*
|
|
10
|
+
* - `terraform.roots.<name>.workspace` in `chant.config.*` selects a
|
|
11
|
+
* workspace via `TF_WORKSPACE` before any HCL is even read, chant's own
|
|
12
|
+
* equivalent of `workspace select`, so it is flagged the same way.
|
|
13
|
+
* - A `"${terraform.workspace}"` interpolation anywhere in a block's body
|
|
14
|
+
* reads the (necessarily `"default"`) workspace name in configuration,
|
|
15
|
+
* which is not itself refused by choudoufu but is dead code on a live
|
|
16
|
+
* root and a sign the configuration was written for a workspace-per-
|
|
17
|
+
* environment layout that live mode does not support.
|
|
18
|
+
*
|
|
19
|
+
* The first fires once per root (it names a root-level config field, not a
|
|
20
|
+
* particular block); the second fires once per block that contains the
|
|
21
|
+
* reference, since each is its own thing to fix.
|
|
22
|
+
*
|
|
23
|
+
* Scope: the config half is root-only, since it reads a root-level config
|
|
24
|
+
* field; the reference half reads any block of a live root, a descended child
|
|
25
|
+
* module's included, and names the call chain that reached it (#2112).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
PostSynthCheck,
|
|
30
|
+
PostSynthContext,
|
|
31
|
+
PostSynthDiagnostic,
|
|
32
|
+
} from "@intentius/chant/lint/post-synth";
|
|
33
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
34
|
+
import { isRootScoped } from "./scope";
|
|
35
|
+
|
|
36
|
+
/** Recursively search a parsed HCL body for a `terraform.workspace` reference, un-evaluated interpolations included. */
|
|
37
|
+
function containsWorkspaceRef(value: unknown): boolean {
|
|
38
|
+
if (typeof value === "string") return /\bterraform\.workspace\b/.test(value);
|
|
39
|
+
if (Array.isArray(value)) return value.some(containsWorkspaceRef);
|
|
40
|
+
if (value !== null && typeof value === "object") return Object.values(value).some(containsWorkspaceRef);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const tf025: PostSynthCheck = {
|
|
45
|
+
id: "TF025",
|
|
46
|
+
description: "Live root references a non-default terraform.workspace, which choudoufu refuses",
|
|
47
|
+
|
|
48
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
49
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
50
|
+
const flaggedRootWorkspace = new Set<string>();
|
|
51
|
+
|
|
52
|
+
for (const [name, entity] of ctx.entities) {
|
|
53
|
+
if (!isResourceDeclarable(entity)) continue;
|
|
54
|
+
const props = entity.props as {
|
|
55
|
+
root?: unknown;
|
|
56
|
+
body?: unknown;
|
|
57
|
+
mode?: unknown;
|
|
58
|
+
workspace?: unknown;
|
|
59
|
+
address?: unknown;
|
|
60
|
+
};
|
|
61
|
+
if (props.mode !== "live") continue;
|
|
62
|
+
const root = typeof props.root === "string" ? props.root : "";
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
typeof props.workspace === "string" &&
|
|
66
|
+
props.workspace !== "" &&
|
|
67
|
+
props.workspace !== "default" &&
|
|
68
|
+
isRootScoped(entity) &&
|
|
69
|
+
!flaggedRootWorkspace.has(root)
|
|
70
|
+
) {
|
|
71
|
+
flaggedRootWorkspace.add(root);
|
|
72
|
+
diagnostics.push({
|
|
73
|
+
checkId: "TF025",
|
|
74
|
+
severity: "error",
|
|
75
|
+
message:
|
|
76
|
+
`Root module "${root}" runs choudoufu with a declared estate and configures workspace ` +
|
|
77
|
+
`"${props.workspace}" in terraform.roots.${root}. choudoufu refuses any workspace but ` +
|
|
78
|
+
'"default" on a live root. Remove `workspace` from this root\'s config (or set it to "default").',
|
|
79
|
+
entity: name,
|
|
80
|
+
lexicon: "terraform",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (containsWorkspaceRef(props.body)) {
|
|
85
|
+
const address = typeof props.address === "string" ? props.address : name;
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
checkId: "TF025",
|
|
88
|
+
severity: "error",
|
|
89
|
+
message:
|
|
90
|
+
`"${address}" in root module "${root}" references \`terraform.workspace\`. choudoufu refuses ` +
|
|
91
|
+
'any workspace but "default" on a live root, so this always resolves to "default" there. ' +
|
|
92
|
+
"Remove the reference.",
|
|
93
|
+
entity: name,
|
|
94
|
+
lexicon: "terraform",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return diagnostics;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
5
|
+
import { tf026 } from "./tf026";
|
|
6
|
+
import { parseTerraformRootDir } from "../../hcl/parse";
|
|
7
|
+
import type { TerraformDeleteMode } from "../../config";
|
|
8
|
+
|
|
9
|
+
const fixtures = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "TF026");
|
|
10
|
+
|
|
11
|
+
async function ctxFor(
|
|
12
|
+
fixture: string,
|
|
13
|
+
opts: { binary?: string; delete?: TerraformDeleteMode } = {},
|
|
14
|
+
root = "estate",
|
|
15
|
+
): Promise<PostSynthContext> {
|
|
16
|
+
const entities = await parseTerraformRootDir(join(fixtures, fixture), root, undefined, {
|
|
17
|
+
binary: opts.binary ?? "choudoufu",
|
|
18
|
+
delete: opts.delete,
|
|
19
|
+
});
|
|
20
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("TF026: delete: \"never\" onto the policy block", () => {
|
|
24
|
+
test('fires when the policy block is absent (undeclared_tagged defaults to "delete")', async () => {
|
|
25
|
+
const diags = tf026.check(await ctxFor("live-default", { delete: "never" }));
|
|
26
|
+
expect(diags).toHaveLength(1);
|
|
27
|
+
expect(diags[0].checkId).toBe("TF026");
|
|
28
|
+
expect(diags[0].message).toContain("unset");
|
|
29
|
+
expect(diags[0].message).toContain("undeclared_tagged");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('fires when undeclared_tagged is explicitly "delete"', async () => {
|
|
33
|
+
const diags = tf026.check(await ctxFor("live-explicit-delete", { delete: "never" }));
|
|
34
|
+
expect(diags).toHaveLength(1);
|
|
35
|
+
expect(diags[0].message).toContain('undeclared_tagged = "delete"');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('passes when undeclared_tagged is "keep"', async () => {
|
|
39
|
+
const diags = tf026.check(await ctxFor("live-keep", { delete: "never" }));
|
|
40
|
+
expect(diags).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("passes when delete is not \"never\" at all", async () => {
|
|
44
|
+
const diags = tf026.check(await ctxFor("live-default", { delete: "owned-only" }));
|
|
45
|
+
expect(diags).toHaveLength(0);
|
|
46
|
+
const diagsGated = tf026.check(await ctxFor("live-default", { delete: "gated" }));
|
|
47
|
+
expect(diagsGated).toHaveLength(0);
|
|
48
|
+
const diagsUnset = tf026.check(await ctxFor("live-default", {}));
|
|
49
|
+
expect(diagsUnset).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("does not fire on a stock root even with delete: \"never\" recorded (inert off choudoufu)", async () => {
|
|
53
|
+
const diags = tf026.check(await ctxFor("live-default", { binary: "terraform", delete: "never" }));
|
|
54
|
+
expect(diags).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF026: a live root declares `delete: "never"` but its `policy` block still
|
|
3
|
+
* deletes an orphaned marked resource.
|
|
4
|
+
*
|
|
5
|
+
* chant's cross-lexicon delete-mode vocabulary (`terraform.roots.<name>.delete`,
|
|
6
|
+
* `../../config.ts`'s `TerraformDeleteMode`) maps onto choudoufu's `policy`
|
|
7
|
+
* block: `undeclared_tagged` governs what happens to a resource this estate
|
|
8
|
+
* marked that the configuration no longer declares, and defaults to
|
|
9
|
+
* `"delete"` when the `policy` block omits it, or the root declares no
|
|
10
|
+
* `policy` block at all. `delete: "never"` promises that `TerraformApplyOp`
|
|
11
|
+
* never proposes deleting a resource it owns, so a root making that promise
|
|
12
|
+
* has to have actually turned the quadrant's default off — `"keep"`,
|
|
13
|
+
* `"untag"` or `"report"` all satisfy it; anything else (the unset default,
|
|
14
|
+
* or an explicit `"delete"`) does not.
|
|
15
|
+
*
|
|
16
|
+
* One diagnostic per root, fired from the root's `Terraform::Live` entity —
|
|
17
|
+
* the same entity TF024 and TF025 fire from.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
PostSynthCheck,
|
|
22
|
+
PostSynthContext,
|
|
23
|
+
PostSynthDiagnostic,
|
|
24
|
+
} from "@intentius/chant/lint/post-synth";
|
|
25
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
26
|
+
import { LIVE_TYPE, type BlockBody } from "../../hcl/parse";
|
|
27
|
+
|
|
28
|
+
/** Verbs that satisfy `delete: "never"` for the `undeclared_tagged` quadrant. */
|
|
29
|
+
const SATISFIES_NEVER = new Set(["keep", "untag", "report"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The `policy { }` block nested in a `live { }` block's body, hcl2json's own
|
|
33
|
+
* "one block, wrapped in an array" shape. Exported for TF027 (`./tf027.ts`),
|
|
34
|
+
* which reads the other quadrant of the same block.
|
|
35
|
+
*/
|
|
36
|
+
export function policyBlockOf(liveBody: BlockBody): BlockBody | undefined {
|
|
37
|
+
const policy = liveBody["policy"];
|
|
38
|
+
return Array.isArray(policy) && policy.length > 0 && typeof policy[0] === "object" && policy[0] !== null
|
|
39
|
+
? (policy[0] as BlockBody)
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const tf026: PostSynthCheck = {
|
|
44
|
+
id: "TF026",
|
|
45
|
+
description: "Live root declares delete: \"never\" but its policy leaves undeclared_tagged at \"delete\"",
|
|
46
|
+
|
|
47
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
48
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
49
|
+
const flagged = new Set<string>();
|
|
50
|
+
|
|
51
|
+
for (const [name, entity] of ctx.entities) {
|
|
52
|
+
if (entity.entityType !== LIVE_TYPE) continue;
|
|
53
|
+
if (!isResourceDeclarable(entity)) continue;
|
|
54
|
+
const props = entity.props as { root?: unknown; body?: unknown; mode?: unknown; delete?: unknown };
|
|
55
|
+
if (props.mode !== "live") continue;
|
|
56
|
+
if (props.delete !== "never") continue;
|
|
57
|
+
const root = typeof props.root === "string" ? props.root : "";
|
|
58
|
+
if (flagged.has(root)) continue;
|
|
59
|
+
|
|
60
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
61
|
+
const policy = policyBlockOf(body);
|
|
62
|
+
const undeclaredTagged = policy && typeof policy["undeclared_tagged"] === "string" ? (policy["undeclared_tagged"] as string) : undefined;
|
|
63
|
+
if (undeclaredTagged !== undefined && SATISFIES_NEVER.has(undeclaredTagged)) continue;
|
|
64
|
+
|
|
65
|
+
flagged.add(root);
|
|
66
|
+
const found =
|
|
67
|
+
undeclaredTagged === undefined
|
|
68
|
+
? "leaves undeclared_tagged unset, which defaults to \"delete\""
|
|
69
|
+
: `sets undeclared_tagged = "${undeclaredTagged}"`;
|
|
70
|
+
diagnostics.push({
|
|
71
|
+
checkId: "TF026",
|
|
72
|
+
severity: "error",
|
|
73
|
+
message:
|
|
74
|
+
`Root module "${root}" declares delete: "never" (terraform.roots.${root}.delete), but its ` +
|
|
75
|
+
`policy block ${found}. Add undeclared_tagged = "keep" (or "untag" or "report") to the live ` +
|
|
76
|
+
"block's policy block, or change this root's delete mode.",
|
|
77
|
+
entity: name,
|
|
78
|
+
lexicon: "terraform",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return diagnostics;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
5
|
+
import { tf027 } from "./tf027";
|
|
6
|
+
import { parseTerraformRootDir } from "../../hcl/parse";
|
|
7
|
+
|
|
8
|
+
const fixtures = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "TF027");
|
|
9
|
+
|
|
10
|
+
async function ctxFor(
|
|
11
|
+
fixture: string,
|
|
12
|
+
opts: { binary?: string } = {},
|
|
13
|
+
root = "estate",
|
|
14
|
+
): Promise<PostSynthContext> {
|
|
15
|
+
const entities = await parseTerraformRootDir(join(fixtures, fixture), root, undefined, {
|
|
16
|
+
binary: opts.binary ?? "choudoufu",
|
|
17
|
+
});
|
|
18
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('TF027: undeclared_untagged = "delete" on a live root', () => {
|
|
22
|
+
test("fires on a policy block that sets it, naming the root and the setting", async () => {
|
|
23
|
+
const diags = tf027.check(await ctxFor("live-untagged-delete"));
|
|
24
|
+
expect(diags).toHaveLength(1);
|
|
25
|
+
expect(diags[0].checkId).toBe("TF027");
|
|
26
|
+
expect(diags[0].severity).toBe("error");
|
|
27
|
+
expect(diags[0].lexicon).toBe("terraform");
|
|
28
|
+
expect(diags[0].message).toContain('Root module "estate"');
|
|
29
|
+
expect(diags[0].message).toContain('undeclared_untagged = "delete"');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("fires even when a scope block narrows the account reconciliation", async () => {
|
|
33
|
+
const diags = tf027.check(await ctxFor("live-untagged-delete-scoped"));
|
|
34
|
+
expect(diags).toHaveLength(1);
|
|
35
|
+
expect(diags[0].checkId).toBe("TF027");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('passes when the quadrant is set to something else ("report")', async () => {
|
|
39
|
+
expect(tf027.check(await ctxFor("live-untagged-report"))).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("passes when the root declares no policy block at all", async () => {
|
|
43
|
+
expect(tf027.check(await ctxFor("live-default"))).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("does not fire on a stock root: the policy block is inert off choudoufu", async () => {
|
|
47
|
+
expect(tf027.check(await ctxFor("live-untagged-delete", { binary: "terraform" }))).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("fires once per root, not once per live entity", async () => {
|
|
51
|
+
const a = await parseTerraformRootDir(join(fixtures, "live-untagged-delete"), "estate", undefined, {
|
|
52
|
+
binary: "choudoufu",
|
|
53
|
+
});
|
|
54
|
+
const b = await parseTerraformRootDir(join(fixtures, "live-untagged-delete"), "other", undefined, {
|
|
55
|
+
binary: "choudoufu",
|
|
56
|
+
});
|
|
57
|
+
const entities = new Map([...a, ...b]);
|
|
58
|
+
const diags = tf027.check({ outputs: new Map(), entities } as unknown as PostSynthContext);
|
|
59
|
+
expect(diags).toHaveLength(2);
|
|
60
|
+
expect(diags.map((d) => d.message.match(/Root module "([^"]+)"/)?.[1]).sort()).toEqual(["estate", "other"]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF027: a live root's `policy` block sets `undeclared_untagged = "delete"`.
|
|
3
|
+
*
|
|
4
|
+
* The `undeclared_untagged` quadrant covers a live resource this estate never
|
|
5
|
+
* marked and the configuration does not declare, a resource chant does not
|
|
6
|
+
* own. `"delete"` there is account-scoped reconciliation: choudoufu compares
|
|
7
|
+
* the configuration against everything a `scope` block reaches and removes
|
|
8
|
+
* what is not in it. chant never proposes deleting a resource it does not
|
|
9
|
+
* own, and neither does an Op it generates, so the setting is refused on any
|
|
10
|
+
* live root the project builds, scoped or not. Narrowing the estate's own
|
|
11
|
+
* ownership answer (`undeclared_tagged`, which TF026 checks against
|
|
12
|
+
* `delete: "never"`) is the setting to reach for instead.
|
|
13
|
+
*
|
|
14
|
+
* `TerraformApplyOp` (`../../composites/terraform-apply-op.ts`) makes the
|
|
15
|
+
* same refusal when it builds, but only for a project on a `chant.config.json`
|
|
16
|
+
* whose root it can resolve synchronously (#2216): a `chant.config.ts` is
|
|
17
|
+
* project-authored code that `resolveRootModeSync` will not evaluate, so the
|
|
18
|
+
* composite reads the root as stock and never asks the policy question. This
|
|
19
|
+
* check reads `props.mode` off the parsed HCL instead, which the build stamps
|
|
20
|
+
* on every entity whatever the config file is written in, so it is the
|
|
21
|
+
* refusal that actually fires. It also fires for a live root with no apply Op
|
|
22
|
+
* at all, since the setting is a statement about the estate rather than about
|
|
23
|
+
* one Op.
|
|
24
|
+
*
|
|
25
|
+
* One diagnostic per root, fired from the root's `Terraform::Live` entity,
|
|
26
|
+
* the same entity TF024, TF025 and TF026 fire from.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
PostSynthCheck,
|
|
31
|
+
PostSynthContext,
|
|
32
|
+
PostSynthDiagnostic,
|
|
33
|
+
} from "@intentius/chant/lint/post-synth";
|
|
34
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
35
|
+
import { LIVE_TYPE, type BlockBody } from "../../hcl/parse";
|
|
36
|
+
import { policyBlockOf } from "./tf026";
|
|
37
|
+
|
|
38
|
+
export const tf027: PostSynthCheck = {
|
|
39
|
+
id: "TF027",
|
|
40
|
+
description: "Live root's policy block sets undeclared_untagged = \"delete\", which deletes resources chant does not own",
|
|
41
|
+
|
|
42
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
43
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
44
|
+
const flagged = new Set<string>();
|
|
45
|
+
|
|
46
|
+
for (const [name, entity] of ctx.entities) {
|
|
47
|
+
if (entity.entityType !== LIVE_TYPE) continue;
|
|
48
|
+
if (!isResourceDeclarable(entity)) continue;
|
|
49
|
+
const props = entity.props as { root?: unknown; body?: unknown; mode?: unknown };
|
|
50
|
+
if (props.mode !== "live") continue;
|
|
51
|
+
const root = typeof props.root === "string" ? props.root : "";
|
|
52
|
+
if (flagged.has(root)) continue;
|
|
53
|
+
|
|
54
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
55
|
+
const policy = policyBlockOf(body);
|
|
56
|
+
if (!policy || policy["undeclared_untagged"] !== "delete") continue;
|
|
57
|
+
|
|
58
|
+
flagged.add(root);
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
checkId: "TF027",
|
|
61
|
+
severity: "error",
|
|
62
|
+
message:
|
|
63
|
+
`Root module "${root}"'s policy block sets undeclared_untagged = "delete" (account-scoped ` +
|
|
64
|
+
"reconciliation, scoped by a `scope` block). chant never proposes deleting a resource it does " +
|
|
65
|
+
"not own, and neither does an Op it generates. Remove that setting from the policy block, or " +
|
|
66
|
+
"narrow the estate's ownership answer (undeclared_tagged) instead of the account's.",
|
|
67
|
+
entity: name,
|
|
68
|
+
lexicon: "terraform",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return diagnostics;
|
|
73
|
+
},
|
|
74
|
+
};
|