@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
|
@@ -27,6 +27,17 @@ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lin
|
|
|
27
27
|
* silent — the same deliberately shallow "bail rather than guess" posture
|
|
28
28
|
* `validateStepOutputRefs` takes, since the runtime brand on a real
|
|
29
29
|
* `StepOutputRef` is invisible to a source-level AST match.
|
|
30
|
+
*
|
|
31
|
+
* Fires on every `terraformApply` call, live root or stock. #2106 exempted
|
|
32
|
+
* live roots for one release, because choudoufu refused `-out` and
|
|
33
|
+
* `apply <planfile>` and the activity ran a bare `apply -auto-approve` with no
|
|
34
|
+
* plan file to pair. choudoufu v0.13.0 admits both under a `live` block
|
|
35
|
+
* ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878)), and
|
|
36
|
+
* there the pairing matters more than on a stock root, not less: the apply
|
|
37
|
+
* re-plans the live system and compares its fresh plan against the file it was
|
|
38
|
+
* handed, so a plan file spelled out as a literal is an approval for a run
|
|
39
|
+
* nobody can trace back to a plan step. The exemption is gone with the
|
|
40
|
+
* condition that produced it, and this rule reads no project config at all.
|
|
30
41
|
*/
|
|
31
42
|
export const planBeforeApplyRule: LintRule = {
|
|
32
43
|
id: "TF101",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root scope or child scope, and the `Callers:` chain that says which call
|
|
3
|
+
* reached a finding (chant #2112).
|
|
4
|
+
*
|
|
5
|
+
* Since the parse descends into local modules, `ctx.entities` holds two kinds
|
|
6
|
+
* of block: the root module's own, keyed `<root>/<address>`, and a child
|
|
7
|
+
* module's, keyed `<root>/module.<name>/<address>` with the call chain on
|
|
8
|
+
* `props.callers`. Every check has to say which it applies to, and the three
|
|
9
|
+
* shapes that answer live here:
|
|
10
|
+
*
|
|
11
|
+
* - {@link isRootScoped} for a check about how a ROOT is assembled (TF001's
|
|
12
|
+
* backend, TF002's `required_providers`): a child module has no backend and
|
|
13
|
+
* no provider requirements of its own to judge, and reading one as if it
|
|
14
|
+
* were the root's is how a root with a perfectly good backend gets flagged
|
|
15
|
+
* because a module beneath it has none.
|
|
16
|
+
* - {@link isChildScoped} for the mirror, the two rules that are ONLY about
|
|
17
|
+
* child modules (TF014, TF015).
|
|
18
|
+
* - {@link callersNote} for the sentence appended to a finding inside a
|
|
19
|
+
* child, naming each call site with the file and line it sits on. tflint
|
|
20
|
+
* prints the same thing as a `Callers:` block under the diagnostic
|
|
21
|
+
* (https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/calling-modules.md);
|
|
22
|
+
* a `PostSynthDiagnostic` carries no ranges, so the chain goes in the
|
|
23
|
+
* message, where a reader of any output format sees it.
|
|
24
|
+
*
|
|
25
|
+
* A check with no opinion (most of them: an unset `type` on a variable is the
|
|
26
|
+
* same finding wherever the variable lives) needs nothing from this module and
|
|
27
|
+
* runs over both scopes, which is what tflint does with its own language rules
|
|
28
|
+
* under `--call-module-type=local`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { PostSynthCheck, PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
32
|
+
import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
|
|
33
|
+
import { callersOfKey, type TerraformEntity } from "../../hcl/parse";
|
|
34
|
+
|
|
35
|
+
/** The `module.<name>` chain on an entity, outermost first. Empty for a root block. */
|
|
36
|
+
export function callersOf(entity: Declarable): readonly string[] {
|
|
37
|
+
if (!isResourceDeclarable(entity)) return [];
|
|
38
|
+
const props = (entity as TerraformEntity).props as Partial<TerraformEntity["props"]>;
|
|
39
|
+
return Array.isArray(props.callers) ? props.callers : [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Was this block declared in the root module itself? */
|
|
43
|
+
export function isRootScoped(entity: Declarable): boolean {
|
|
44
|
+
return callersOf(entity).length === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Was this block declared in a child module the root calls? */
|
|
48
|
+
export function isChildScoped(entity: Declarable): boolean {
|
|
49
|
+
return callersOf(entity).length > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `Callers:` sentence for a finding on `key`, or `""` when the finding is
|
|
54
|
+
* in the root module.
|
|
55
|
+
*
|
|
56
|
+
* The call sites are the key's own prefixes: a finding on
|
|
57
|
+
* `app/module.cdn/module.bucket/aws_s3_bucket.assets` was reached through
|
|
58
|
+
* `app/module.cdn` and then `app/module.cdn/module.bucket`, both of them
|
|
59
|
+
* `module` block entities in `ctx.entities`, so the file and line each was
|
|
60
|
+
* parsed from are already recorded. A prefix with no entity (a hand-built
|
|
61
|
+
* context in a test) renders as the bare call name.
|
|
62
|
+
*/
|
|
63
|
+
export function callersNote(ctx: PostSynthContext, key: string): string {
|
|
64
|
+
const callers = callersOfKey(key);
|
|
65
|
+
if (callers.length === 0) return "";
|
|
66
|
+
|
|
67
|
+
const root = key.slice(0, key.indexOf("/"));
|
|
68
|
+
const hops = callers.map((caller, i) => {
|
|
69
|
+
const callSite = [root, ...callers.slice(0, i + 1)].join("/");
|
|
70
|
+
const entity = ctx.entities.get(callSite);
|
|
71
|
+
if (!entity || !isResourceDeclarable(entity)) return caller;
|
|
72
|
+
const props = (entity as TerraformEntity).props as Partial<TerraformEntity["props"]>;
|
|
73
|
+
const file = typeof props.file === "string" ? props.file : "";
|
|
74
|
+
if (file === "") return caller;
|
|
75
|
+
return typeof props.line === "number" ? `${caller} (${file}:${props.line})` : `${caller} (${file})`;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return ` Callers: ${[root, ...hops].join(" -> ")}.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wrap every check so a finding inside a child module carries its
|
|
83
|
+
* {@link callersNote}, whichever rule produced it.
|
|
84
|
+
*
|
|
85
|
+
* Done once, here, rather than in each rule's message: a reader of a finding
|
|
86
|
+
* on `module.cdn`'s bucket needs to know which call reached that code no
|
|
87
|
+
* matter which of the twenty-odd checks reported it, and a rule that forgot
|
|
88
|
+
* to append the chain would be a silent gap. The wrapper is what
|
|
89
|
+
* `terraformPlugin.postSynthChecks()` returns; a rule's own `check()` is
|
|
90
|
+
* unchanged, which is what its unit tests call.
|
|
91
|
+
*
|
|
92
|
+
* A message that already names the chain is left alone, so a rule with
|
|
93
|
+
* something more specific to say about its callers can say it.
|
|
94
|
+
*/
|
|
95
|
+
export function withCallersChain(checks: readonly PostSynthCheck[]): PostSynthCheck[] {
|
|
96
|
+
return checks.map((check) => ({
|
|
97
|
+
...check,
|
|
98
|
+
check(ctx: PostSynthContext) {
|
|
99
|
+
return check.check(ctx).map((diagnostic) => {
|
|
100
|
+
if (typeof diagnostic.entity !== "string") return diagnostic;
|
|
101
|
+
if (diagnostic.message.includes("Callers:")) return diagnostic;
|
|
102
|
+
const note = callersNote(ctx, diagnostic.entity);
|
|
103
|
+
return note === "" ? diagnostic : { ...diagnostic, message: diagnostic.message + note };
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
}
|
package/dist/rules/tf001.ts
CHANGED
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TF001: a root module keeps its state on local disk.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and
|
|
4
|
+
* Two ways a root gets there, and this reports both (#2218). A `terraform`
|
|
5
|
+
* block with no `backend "<type>"` and no `cloud {}` falls back to the local
|
|
6
|
+
* backend implicitly. A `backend "local"` block asks for the same backend by
|
|
7
|
+
* name. Either way `terraform.tfstate` lands in the working directory, and
|
|
8
|
+
* that file holds every attribute of every managed resource, secrets
|
|
9
|
+
* included, and it is not shared, not locked, and not versioned. The first
|
|
10
|
+
* apply from a second machine or a CI runner starts from an empty state and
|
|
11
|
+
* proposes to create the estate again.
|
|
12
|
+
*
|
|
13
|
+
* So the rule reads the backend block's TYPE LABEL rather than its presence:
|
|
14
|
+
* the id means "no remote backend", which is what its name, its message and
|
|
15
|
+
* its page have always argued. `cloud {}` counts as remote (it is Terraform
|
|
16
|
+
* Cloud / HCP state, held off the machine and locked). Every other backend
|
|
17
|
+
* type is taken as remote without a list to maintain, since `local` is the
|
|
18
|
+
* only backend Terraform ships that writes to the working directory.
|
|
19
|
+
*
|
|
20
|
+
* A root that keeps local state on purpose says so with a
|
|
21
|
+
* `# chant-ignore-block: TF001` on the line above its `terraform` block
|
|
22
|
+
* (chant #2111), which is what the three shipped examples do.
|
|
10
23
|
*
|
|
11
24
|
* One diagnostic per root, fired from the root's `terraform` block. A root
|
|
12
25
|
* with no `terraform` block at all is not flagged: it declares no version
|
|
13
26
|
* constraints either, and the missing block is a different finding.
|
|
27
|
+
*
|
|
28
|
+
* Scope: root modules only (#2112). A child module must NOT declare a
|
|
29
|
+
* backend, which is the mirror image and TF015's finding, so a descended
|
|
30
|
+
* module's `terraform` block is skipped here rather than read as if it were
|
|
31
|
+
* the root's.
|
|
32
|
+
*
|
|
33
|
+
* Does not fire on a live root (#2103): a `backend` block is exactly what
|
|
34
|
+
* choudoufu refuses there (TF024's territory), and the fallback to local
|
|
35
|
+
* state this check warns about does not apply, since a live root keeps no
|
|
36
|
+
* state file at all, local or remote.
|
|
14
37
|
*/
|
|
15
38
|
|
|
16
39
|
import type {
|
|
@@ -20,13 +43,53 @@ import type {
|
|
|
20
43
|
} from "@intentius/chant/lint/post-synth";
|
|
21
44
|
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
22
45
|
import { TERRAFORM_TYPE, type BlockBody } from "../../hcl/parse";
|
|
46
|
+
import { attr } from "../../hcl/value";
|
|
47
|
+
import { isRootScoped } from "./scope";
|
|
23
48
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
/**
|
|
50
|
+
* `backend`/`cloud` are blocks, so hcl2json encodes them as a value under the
|
|
51
|
+
* key, structurally the same "literal value under this key" shape `attr()`
|
|
52
|
+
* (`../../hcl/value.ts`, chant #2113) reads for an ordinary attribute, so
|
|
53
|
+
* this reads through it rather than re-deriving presence by hand.
|
|
54
|
+
*/
|
|
55
|
+
export function hasBlock(body: BlockBody, key: string): boolean {
|
|
56
|
+
const a = attr(body, key);
|
|
57
|
+
if (a.kind === "absent") return false;
|
|
58
|
+
if (Array.isArray(a.value)) return a.value.length > 0;
|
|
59
|
+
return typeof a.value === "object" && a.value !== null ? Object.keys(a.value).length > 0 : true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The one backend type that writes state to the working directory. */
|
|
63
|
+
export const LOCAL_BACKEND = "local";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The type labels of the `backend` blocks in a `terraform` block body.
|
|
67
|
+
*
|
|
68
|
+
* `backend` takes one label, so hcl2json nests it one level deeper than an
|
|
69
|
+
* unlabelled block: `backend "s3" { bucket = "b" }` is
|
|
70
|
+
* `{ backend: { s3: [{ bucket: "b" }] } }`, where `cloud { ... }` is just
|
|
71
|
+
* `{ cloud: [{ ... }] }`. The keys of that inner object are the labels, which
|
|
72
|
+
* is the only place the type survives the parse. A malformed body that puts
|
|
73
|
+
* an array or a scalar under `backend` yields no labels, and the caller then
|
|
74
|
+
* treats the root as having no remote backend, the same verdict a missing
|
|
75
|
+
* block gets.
|
|
76
|
+
*/
|
|
77
|
+
export function backendTypes(body: BlockBody): string[] {
|
|
78
|
+
const a = attr(body, "backend");
|
|
79
|
+
if (a.kind === "absent") return [];
|
|
80
|
+
const value = a.value;
|
|
81
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
|
|
82
|
+
return Object.keys(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether the body configures state somewhere other than the working
|
|
87
|
+
* directory: any `backend` block whose type is not `local`, or a `cloud`
|
|
88
|
+
* block.
|
|
89
|
+
*/
|
|
90
|
+
export function hasRemoteBackend(body: BlockBody): boolean {
|
|
91
|
+
if (hasBlock(body, "cloud")) return true;
|
|
92
|
+
return backendTypes(body).some((type) => type !== LOCAL_BACKEND);
|
|
30
93
|
}
|
|
31
94
|
|
|
32
95
|
export const tf001: PostSynthCheck = {
|
|
@@ -40,24 +103,41 @@ export const tf001: PostSynthCheck = {
|
|
|
40
103
|
|
|
41
104
|
for (const [name, entity] of ctx.entities) {
|
|
42
105
|
if (entity.entityType !== TERRAFORM_TYPE) continue;
|
|
43
|
-
if (!isResourceDeclarable(entity)) continue;
|
|
44
|
-
const props = entity.props as { root?: unknown; body?: unknown };
|
|
106
|
+
if (!isResourceDeclarable(entity) || !isRootScoped(entity)) continue;
|
|
107
|
+
const props = entity.props as { root?: unknown; body?: unknown; mode?: unknown };
|
|
108
|
+
if (props.mode === "live") continue;
|
|
45
109
|
const root = typeof props.root === "string" ? props.root : "";
|
|
46
110
|
if (flagged.has(root)) continue;
|
|
47
111
|
|
|
48
112
|
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
49
|
-
if (
|
|
113
|
+
if (hasRemoteBackend(body)) continue;
|
|
114
|
+
|
|
115
|
+
// What the root actually declares, so the message names it rather than
|
|
116
|
+
// saying "no remote backend" over a block the reader can see.
|
|
117
|
+
const found = backendTypes(body).includes(LOCAL_BACKEND)
|
|
118
|
+
? 'the backend it declares is `backend "local"`'
|
|
119
|
+
: "there is no backend block at all";
|
|
50
120
|
|
|
51
121
|
flagged.add(root);
|
|
52
122
|
diagnostics.push({
|
|
53
123
|
checkId: "TF001",
|
|
54
124
|
severity: "warning",
|
|
55
125
|
message:
|
|
56
|
-
`Root module "${root}" declares no remote backend. State
|
|
57
|
-
"terraform.tfstate, which is unshared, unlocked and holds every
|
|
58
|
-
'plaintext.
|
|
126
|
+
`Root module "${root}" declares no remote backend: ${found}. State is a ` +
|
|
127
|
+
"terraform.tfstate in the working directory, which is unshared, unlocked and holds every " +
|
|
128
|
+
'resource attribute in plaintext. Declare a `backend "<type>"` naming a remote type (`s3`, ' +
|
|
129
|
+
"`gcs`, `azurerm`, `http`) or a `cloud {}` block, or keep the local state on purpose with a " +
|
|
130
|
+
"`# chant-ignore-block: TF001` above the terraform block.",
|
|
59
131
|
entity: name,
|
|
60
132
|
lexicon: "terraform",
|
|
133
|
+
// The missing-resource shape (chant #2113): there is no remote
|
|
134
|
+
// `backend`/`cloud` block to point at, only the root that lacks one.
|
|
135
|
+
// `entity` above still names the `terraform` block this fired from (by
|
|
136
|
+
// convenience, not because that block is what's wrong), so `missing`
|
|
137
|
+
// is what a suppression should key on instead. `kind` stays `backend`
|
|
138
|
+
// in both cases: what is absent is a remote backend, whether or not a
|
|
139
|
+
// `backend "local"` block is sitting where one should be.
|
|
140
|
+
missing: { kind: "backend", scope: root },
|
|
61
141
|
});
|
|
62
142
|
}
|
|
63
143
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF002: a provider a root implies has no `required_providers` entry, or an
|
|
3
|
+
* incomplete one.
|
|
4
|
+
*
|
|
5
|
+
* A provider is implied two ways: an explicit `provider "<name>" {}` block,
|
|
6
|
+
* or a resource/data type prefix (`aws_instance` implies `aws`, up to the
|
|
7
|
+
* first underscore, the same heuristic tflint's `terraform_required_providers`
|
|
8
|
+
* reads via `GetProviderRefs()`). The `terraform` builtin (the provider
|
|
9
|
+
* behind `terraform_remote_state`) is never implied: it has no registry
|
|
10
|
+
* entry to require. Every implied provider needs a `required_providers`
|
|
11
|
+
* entry that is an object carrying both `source` and `version`, since the old
|
|
12
|
+
* shorthand form (`aws = "~> 4.0"`, a bare string) has neither and fires the
|
|
13
|
+
* same as a missing entry.
|
|
14
|
+
*
|
|
15
|
+
* Fires once per incomplete or missing provider, not once per resource: two
|
|
16
|
+
* resources implying the same unconstrained provider produce one diagnostic.
|
|
17
|
+
* Anchored on whichever entity first implied the provider, preferring an
|
|
18
|
+
* explicit `provider` block over a resource/data type, so the diagnostic
|
|
19
|
+
* points at the most specific place a fix belongs.
|
|
20
|
+
*
|
|
21
|
+
* A root with no `terraform` block at all is out of scope, same as TF001:
|
|
22
|
+
* the missing block is that rule's finding, not this one's.
|
|
23
|
+
*
|
|
24
|
+
* Scope: root modules only (#2112). A child module's own `required_providers`
|
|
25
|
+
* are the child's business, and its resources are constrained by the root's
|
|
26
|
+
* block at plan time, so folding a descended module's blocks into this pass
|
|
27
|
+
* would both credit the root with requirements it never declared and judge it
|
|
28
|
+
* on providers it never named.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type {
|
|
32
|
+
PostSynthCheck,
|
|
33
|
+
PostSynthContext,
|
|
34
|
+
PostSynthDiagnostic,
|
|
35
|
+
} from "@intentius/chant/lint/post-synth";
|
|
36
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
37
|
+
import { isRootScoped } from "./scope";
|
|
38
|
+
import {
|
|
39
|
+
DATA_TYPE,
|
|
40
|
+
PROVIDER_TYPE,
|
|
41
|
+
RESOURCE_TYPE,
|
|
42
|
+
TERRAFORM_TYPE,
|
|
43
|
+
type BlockBody,
|
|
44
|
+
} from "../../hcl/parse";
|
|
45
|
+
|
|
46
|
+
/** The `terraform` provider backs `terraform_remote_state`; it ships with the binary, not the registry. */
|
|
47
|
+
const BUILTIN_PROVIDERS = new Set(["terraform"]);
|
|
48
|
+
|
|
49
|
+
function providerNameFromType(type: string): string {
|
|
50
|
+
return type.split("_")[0];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One implied provider name to the entity key that first implied it in file order. */
|
|
54
|
+
function impliedProviders(entities: PostSynthContext["entities"], root: string): Map<string, string> {
|
|
55
|
+
const implied = new Map<string, string>();
|
|
56
|
+
const fromProviderBlock = new Set<string>();
|
|
57
|
+
|
|
58
|
+
for (const [key, entity] of entities) {
|
|
59
|
+
if (!isResourceDeclarable(entity) || !isRootScoped(entity)) continue;
|
|
60
|
+
const props = entity.props as { root?: unknown; address?: unknown };
|
|
61
|
+
if (props.root !== root) continue;
|
|
62
|
+
const address = typeof props.address === "string" ? props.address : "";
|
|
63
|
+
|
|
64
|
+
if (entity.entityType === PROVIDER_TYPE) {
|
|
65
|
+
const name = address.slice("provider.".length);
|
|
66
|
+
if (BUILTIN_PROVIDERS.has(name)) continue;
|
|
67
|
+
implied.set(name, key);
|
|
68
|
+
fromProviderBlock.add(name);
|
|
69
|
+
} else if (entity.entityType === RESOURCE_TYPE) {
|
|
70
|
+
const type = address.split(".")[0] ?? "";
|
|
71
|
+
const name = providerNameFromType(type);
|
|
72
|
+
if (BUILTIN_PROVIDERS.has(name) || fromProviderBlock.has(name)) continue;
|
|
73
|
+
if (!implied.has(name)) implied.set(name, key);
|
|
74
|
+
} else if (entity.entityType === DATA_TYPE) {
|
|
75
|
+
const type = address.split(".")[1] ?? "";
|
|
76
|
+
const name = providerNameFromType(type);
|
|
77
|
+
if (BUILTIN_PROVIDERS.has(name) || fromProviderBlock.has(name)) continue;
|
|
78
|
+
if (!implied.has(name)) implied.set(name, key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return implied;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `required_providers` is a nested block: hcl2json encodes it as a one-element array of `{name: entry}`. */
|
|
86
|
+
function requiredProvidersMap(body: BlockBody): Record<string, unknown> {
|
|
87
|
+
const merged: Record<string, unknown> = {};
|
|
88
|
+
const blocks = body.required_providers;
|
|
89
|
+
if (!Array.isArray(blocks)) return merged;
|
|
90
|
+
for (const block of blocks) {
|
|
91
|
+
if (typeof block === "object" && block !== null) Object.assign(merged, block);
|
|
92
|
+
}
|
|
93
|
+
return merged;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Why a `required_providers` entry is incomplete, or `undefined` if it's fine. */
|
|
97
|
+
function entryProblem(entry: unknown): string | undefined {
|
|
98
|
+
if (entry === undefined) return "no `required_providers` entry";
|
|
99
|
+
if (typeof entry === "string") return "the legacy shorthand form (a bare version string), which carries no `source`";
|
|
100
|
+
if (typeof entry !== "object" || entry === null) return "an entry that is not a valid object";
|
|
101
|
+
const obj = entry as Record<string, unknown>;
|
|
102
|
+
const missing: string[] = [];
|
|
103
|
+
if (typeof obj.source !== "string" || obj.source === "") missing.push("`source`");
|
|
104
|
+
if (typeof obj.version !== "string" || obj.version === "") missing.push("`version`");
|
|
105
|
+
return missing.length > 0 ? `an entry missing ${missing.join(" and ")}` : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const tf002: PostSynthCheck = {
|
|
109
|
+
id: "TF002",
|
|
110
|
+
description: "A provider the root implies has no required_providers entry, or an incomplete one",
|
|
111
|
+
|
|
112
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
113
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
114
|
+
const roots = new Set<string>();
|
|
115
|
+
const requiredByRoot = new Map<string, Record<string, unknown>>();
|
|
116
|
+
|
|
117
|
+
for (const entity of ctx.entities.values()) {
|
|
118
|
+
if (entity.entityType !== TERRAFORM_TYPE || !isResourceDeclarable(entity)) continue;
|
|
119
|
+
if (!isRootScoped(entity)) continue;
|
|
120
|
+
const props = entity.props as { root?: unknown; body?: unknown };
|
|
121
|
+
const root = typeof props.root === "string" ? props.root : "";
|
|
122
|
+
roots.add(root);
|
|
123
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
124
|
+
const merged = requiredByRoot.get(root) ?? {};
|
|
125
|
+
Object.assign(merged, requiredProvidersMap(body));
|
|
126
|
+
requiredByRoot.set(root, merged);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const root of [...roots].sort()) {
|
|
130
|
+
const required = requiredByRoot.get(root) ?? {};
|
|
131
|
+
const implied = impliedProviders(ctx.entities, root);
|
|
132
|
+
|
|
133
|
+
for (const name of [...implied.keys()].sort()) {
|
|
134
|
+
const problem = entryProblem(required[name]);
|
|
135
|
+
if (!problem) continue;
|
|
136
|
+
diagnostics.push({
|
|
137
|
+
checkId: "TF002",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
message:
|
|
140
|
+
`Provider "${name}" is used in root module "${root}" but has ${problem} in ` +
|
|
141
|
+
"`required_providers`. Add `source` and `version` for it in the terraform block's " +
|
|
142
|
+
"`required_providers`, so a future provider release can't silently change behavior.",
|
|
143
|
+
entity: implied.get(name),
|
|
144
|
+
lexicon: "terraform",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return diagnostics;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF003: a root's `terraform` block has no `required_version`.
|
|
3
|
+
*
|
|
4
|
+
* Sits on the same `terraform` block TF001 already walks. Without
|
|
5
|
+
* `required_version`, a root can be applied by whatever Terraform binary
|
|
6
|
+
* happens to be on the runner's `PATH`, and a binary upgrade that changes
|
|
7
|
+
* behavior (a provider protocol bump, a changed default) surfaces as a
|
|
8
|
+
* surprise plan diff instead of a version-check failure before anything runs.
|
|
9
|
+
*
|
|
10
|
+
* One diagnostic per root, fired once even if the root spreads its
|
|
11
|
+
* `terraform` block across several files (a legacy-config split into
|
|
12
|
+
* `terraform.tf` and `versions.tf`, say). A root with no `terraform` block
|
|
13
|
+
* at all is out of scope, same as TF001 and TF002: the missing block is
|
|
14
|
+
* their finding, not this one's.
|
|
15
|
+
*
|
|
16
|
+
* Scope: root modules only (#2112). A child module may legitimately leave
|
|
17
|
+
* `required_version` to the root that calls it, so a descended module's
|
|
18
|
+
* `terraform` block is skipped here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
PostSynthCheck,
|
|
23
|
+
PostSynthContext,
|
|
24
|
+
PostSynthDiagnostic,
|
|
25
|
+
} from "@intentius/chant/lint/post-synth";
|
|
26
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
27
|
+
import { isRootScoped } from "./scope";
|
|
28
|
+
import { TERRAFORM_TYPE, type BlockBody } from "../../hcl/parse";
|
|
29
|
+
|
|
30
|
+
function hasRequiredVersion(body: BlockBody): boolean {
|
|
31
|
+
return typeof body.required_version === "string" && body.required_version.trim() !== "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const tf003: PostSynthCheck = {
|
|
35
|
+
id: "TF003",
|
|
36
|
+
description: "Root module's terraform block has no required_version",
|
|
37
|
+
|
|
38
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
39
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
40
|
+
/** Root name to the entity key of its first `terraform` block, and whether any block for it declares `required_version`. */
|
|
41
|
+
const seen = new Map<string, { anchor: string; hasVersion: boolean }>();
|
|
42
|
+
|
|
43
|
+
for (const [key, entity] of ctx.entities) {
|
|
44
|
+
if (entity.entityType !== TERRAFORM_TYPE || !isResourceDeclarable(entity)) continue;
|
|
45
|
+
if (!isRootScoped(entity)) continue;
|
|
46
|
+
const props = entity.props as { root?: unknown; body?: unknown };
|
|
47
|
+
const root = typeof props.root === "string" ? props.root : "";
|
|
48
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
49
|
+
|
|
50
|
+
const existing = seen.get(root);
|
|
51
|
+
const hasVersion = (existing?.hasVersion ?? false) || hasRequiredVersion(body);
|
|
52
|
+
seen.set(root, { anchor: existing?.anchor ?? key, hasVersion });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [root, { anchor, hasVersion }] of [...seen].sort(([a], [b]) => a.localeCompare(b))) {
|
|
56
|
+
if (hasVersion) continue;
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
checkId: "TF003",
|
|
59
|
+
severity: "warning",
|
|
60
|
+
message:
|
|
61
|
+
`Root module "${root}"'s terraform block has no \`required_version\`. Without it, ` +
|
|
62
|
+
"an upgraded Terraform binary can change behavior with no version-check failure to " +
|
|
63
|
+
"flag it first. Add `required_version = \">= <lowest supported version>\"`.",
|
|
64
|
+
entity: anchor,
|
|
65
|
+
lexicon: "terraform",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return diagnostics;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF004: a registry-sourced `module` block has no `version`.
|
|
3
|
+
*
|
|
4
|
+
* "Registry-sourced" means `source` parses as a Terraform module registry
|
|
5
|
+
* address (`classifyModuleSource`, `./module-source.ts`): a bare
|
|
6
|
+
* `namespace/name/target-system`, or a hostname-prefixed
|
|
7
|
+
* `host/namespace/name/target-system`. Without `version`, `terraform init`
|
|
8
|
+
* always resolves to the newest release that satisfies no constraint at
|
|
9
|
+
* all, so an upstream module release can change what a root provisions with
|
|
10
|
+
* no local edit and no diff to review first.
|
|
11
|
+
*
|
|
12
|
+
* A local (`./`, `../`) or git/hg source is never flagged here: git/hg is
|
|
13
|
+
* TF005's territory, and a local source cannot carry a registry version at
|
|
14
|
+
* all. One diagnostic per module block.
|
|
15
|
+
*
|
|
16
|
+
* Scope: root and child modules alike (#2112). The condition is a property
|
|
17
|
+
* of the block itself, so a descended module's block is read exactly as a
|
|
18
|
+
* root's is.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
PostSynthCheck,
|
|
23
|
+
PostSynthContext,
|
|
24
|
+
PostSynthDiagnostic,
|
|
25
|
+
} from "@intentius/chant/lint/post-synth";
|
|
26
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
27
|
+
import { MODULE_TYPE, type BlockBody } from "../../hcl/parse";
|
|
28
|
+
import { classifyModuleSource } from "./module-source";
|
|
29
|
+
|
|
30
|
+
export const tf004: PostSynthCheck = {
|
|
31
|
+
id: "TF004",
|
|
32
|
+
description: "Registry-sourced module block has no version",
|
|
33
|
+
|
|
34
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
35
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
36
|
+
|
|
37
|
+
for (const [key, entity] of ctx.entities) {
|
|
38
|
+
if (entity.entityType !== MODULE_TYPE || !isResourceDeclarable(entity)) continue;
|
|
39
|
+
const props = entity.props as { root?: unknown; address?: unknown; body?: unknown };
|
|
40
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
41
|
+
const source = typeof body.source === "string" ? body.source : undefined;
|
|
42
|
+
if (source === undefined) continue;
|
|
43
|
+
if (classifyModuleSource(source).kind !== "registry") continue;
|
|
44
|
+
|
|
45
|
+
const version = body.version;
|
|
46
|
+
if (typeof version === "string" && version.trim() !== "") continue;
|
|
47
|
+
|
|
48
|
+
const address = typeof props.address === "string" ? props.address : key;
|
|
49
|
+
diagnostics.push({
|
|
50
|
+
checkId: "TF004",
|
|
51
|
+
severity: "warning",
|
|
52
|
+
message:
|
|
53
|
+
`Module "${address}" sources "${source}" from a registry with no \`version\`. ` +
|
|
54
|
+
"`terraform init` resolves to the newest release with no constraint to hold it back. " +
|
|
55
|
+
"Add a `version` constraint (e.g. `~> 5.0`).",
|
|
56
|
+
entity: key,
|
|
57
|
+
lexicon: "terraform",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return diagnostics;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF005: a git or hg `module` source with no `?ref=`, or a `?ref=` that
|
|
3
|
+
* isn't tight enough to call pinned.
|
|
4
|
+
*
|
|
5
|
+
* "Git or hg" is `classifyModuleSource`'s `"git"` kind (`./module-source.ts`):
|
|
6
|
+
* an explicit `git::`/`hg::` prefix, the bare `github.com`/`bitbucket.org`
|
|
7
|
+
* shorthands, an scp-style `git@host:path`, or anything ending in `.git`.
|
|
8
|
+
* A ref counts as pinned only if it is a semver-shaped tag or a full 40-hex
|
|
9
|
+
* commit SHA (`isPinnedRef`), stricter than checkov's `CKV_TF_1`/`CKV_TF_2`,
|
|
10
|
+
* which accept any `?ref=` containing `\d\.\d` (so `v1.2-dev` passes there;
|
|
11
|
+
* it does not here, and the rule's page section says so). A ref matching a
|
|
12
|
+
* well-known mutable branch name (`main`, `master`, `develop`, `trunk`) gets
|
|
13
|
+
* its own, more specific message, since that's the exact case tflint's
|
|
14
|
+
* `flexible` style (the style this rule ships; see the page section for what
|
|
15
|
+
* `semver` style would add) already reports on.
|
|
16
|
+
*
|
|
17
|
+
* A local (`./`, `../`) source is never flagged: it can't be pinned to a
|
|
18
|
+
* ref at all, and checkov's own module-pinning checks return `UNKNOWN`
|
|
19
|
+
* (not a failure) for the same reason. A registry source is TF004's
|
|
20
|
+
* territory, not this rule's; the two never fire on the same module block.
|
|
21
|
+
*
|
|
22
|
+
* Scope: root and child modules alike (#2112). The condition is a property
|
|
23
|
+
* of the block itself, so a descended module's block is read exactly as a
|
|
24
|
+
* root's is.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
PostSynthCheck,
|
|
29
|
+
PostSynthContext,
|
|
30
|
+
PostSynthDiagnostic,
|
|
31
|
+
} from "@intentius/chant/lint/post-synth";
|
|
32
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
33
|
+
import { MODULE_TYPE, type BlockBody } from "../../hcl/parse";
|
|
34
|
+
import { classifyModuleSource, isDefaultBranch, isPinnedRef } from "./module-source";
|
|
35
|
+
|
|
36
|
+
export const tf005: PostSynthCheck = {
|
|
37
|
+
id: "TF005",
|
|
38
|
+
description: "Git/hg module source is unpinned, or pinned to a mutable ref",
|
|
39
|
+
|
|
40
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
41
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
42
|
+
|
|
43
|
+
for (const [key, entity] of ctx.entities) {
|
|
44
|
+
if (entity.entityType !== MODULE_TYPE || !isResourceDeclarable(entity)) continue;
|
|
45
|
+
const props = entity.props as { address?: unknown; body?: unknown };
|
|
46
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
47
|
+
const source = typeof body.source === "string" ? body.source : undefined;
|
|
48
|
+
if (source === undefined) continue;
|
|
49
|
+
|
|
50
|
+
const classified = classifyModuleSource(source);
|
|
51
|
+
if (classified.kind !== "git") continue;
|
|
52
|
+
|
|
53
|
+
const address = typeof props.address === "string" ? props.address : key;
|
|
54
|
+
let message: string | undefined;
|
|
55
|
+
|
|
56
|
+
if (!classified.ref) {
|
|
57
|
+
message = `Module "${address}" sources "${source}" from git/hg with no \`?ref=\`. Add one pinned to a tag or a full commit SHA.`;
|
|
58
|
+
} else if (isDefaultBranch(classified.ref)) {
|
|
59
|
+
message = `Module "${address}" sources "${source}" pinned to "${classified.ref}", a mutable branch. Pin to a tag or a full commit SHA instead.`;
|
|
60
|
+
} else if (!isPinnedRef(classified.ref)) {
|
|
61
|
+
message = `Module "${address}" sources "${source}" pinned to "${classified.ref}", which is neither a tag nor a full commit SHA and can move. Pin to a tag or a full commit SHA instead.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!message) continue;
|
|
65
|
+
diagnostics.push({
|
|
66
|
+
checkId: "TF005",
|
|
67
|
+
severity: "warning",
|
|
68
|
+
message,
|
|
69
|
+
entity: key,
|
|
70
|
+
lexicon: "terraform",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return diagnostics;
|
|
75
|
+
},
|
|
76
|
+
};
|