@intentius/chant-lexicon-terraform 0.57.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composites/terraform-adopt-op.d.ts +126 -0
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
- package/dist/composites/terraform-apply-op.d.ts +63 -16
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +80 -36
- 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 +142 -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 +32 -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 +68 -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 +18 -0
- 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 +21 -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 +21 -0
- package/dist/lint/post-synth/tf026.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 +24 -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/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 +445 -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 +46 -6
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +45 -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 +32 -8
- 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 +63 -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 +80 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/package.json +7 -2
- 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/codegen/lint-rules-page.test.ts +28 -0
- package/src/composites/terraform-adopt-op.acceptance.test.ts +166 -0
- package/src/composites/terraform-adopt-op.test.ts +170 -0
- package/src/composites/terraform-adopt-op.ts +197 -0
- package/src/composites/terraform-apply-op.acceptance.test.ts +183 -5
- package/src/composites/terraform-apply-op.test.ts +167 -15
- package/src/composites/terraform-apply-op.ts +105 -28
- package/src/composites/terraform-watch-op.test.ts +85 -26
- package/src/composites/terraform-watch-op.ts +130 -73
- 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 +249 -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 +264 -3
- package/src/lint/audit-lineage.ts +319 -1
- package/src/lint/audit.test.ts +40 -1
- package/src/lint/post-synth/blocks.ts +109 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +28 -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/load.ts +108 -0
- package/src/lint/post-synth/index.ts +48 -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 +14 -15
- 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 +32 -8
- 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 +63 -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 +80 -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 +10 -5
- package/src/op/activities/choudoufu.acceptance.test.ts +143 -0
- package/src/op/activities/choudoufu.test.ts +693 -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 +823 -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 +93 -5
- package/src/op/resolve-root-mode.test.ts +85 -0
- package/src/op/resolve-root-mode.ts +72 -0
- package/src/plugin.test.ts +68 -0
- package/src/plugin.ts +75 -9
- package/src/skills/chant-terraform.md +1 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
2
5
|
import * as ts from "typescript";
|
|
3
6
|
import type { LintContext } from "@intentius/chant/lint/rule";
|
|
4
7
|
import { planBeforeApplyRule } from "./plan-before-apply";
|
|
8
|
+
import { loadRuleFixture } from "./fixtures/load";
|
|
5
9
|
|
|
6
10
|
function createContext(code: string, fileName = "infra.op.ts"): LintContext {
|
|
7
11
|
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
|
|
@@ -10,18 +14,18 @@ function createContext(code: string, fileName = "infra.op.ts"): LintContext {
|
|
|
10
14
|
|
|
11
15
|
describe("TF101: plan-before-apply", () => {
|
|
12
16
|
test("flags a literal plan path", () => {
|
|
13
|
-
const diags = planBeforeApplyRule.check(
|
|
14
|
-
createContext(`
|
|
15
|
-
const plan = terraformPlan({ root: "app" });
|
|
16
|
-
const apply = terraformApply({ planFile: "/tmp/plan.out" });
|
|
17
|
-
`),
|
|
18
|
-
);
|
|
17
|
+
const diags = planBeforeApplyRule.check(loadRuleFixture("TF101", "positive"));
|
|
19
18
|
expect(diags).toHaveLength(1);
|
|
20
19
|
expect(diags[0].ruleId).toBe("TF101");
|
|
21
20
|
expect(diags[0].severity).toBe("error");
|
|
22
21
|
expect(diags[0].message).toContain("literal path");
|
|
23
22
|
});
|
|
24
23
|
|
|
24
|
+
test("passes a step-output reference to the preceding plan (fixtures/TF101/negative.op.ts)", () => {
|
|
25
|
+
const diags = planBeforeApplyRule.check(loadRuleFixture("TF101", "negative"));
|
|
26
|
+
expect(diags).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
25
29
|
test("flags a template literal plan path", () => {
|
|
26
30
|
const diags = planBeforeApplyRule.check(
|
|
27
31
|
createContext(`
|
|
@@ -109,3 +113,104 @@ describe("TF101: plan-before-apply", () => {
|
|
|
109
113
|
expect(diags).toHaveLength(0);
|
|
110
114
|
});
|
|
111
115
|
});
|
|
116
|
+
|
|
117
|
+
describe("TF101: a live root is checked like any other (#2106 follow-up)", () => {
|
|
118
|
+
const dirs: string[] = [];
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/** A throwaway project with one `terraform.roots` entry, live or stock. */
|
|
125
|
+
function project(opts: { binary?: string; live?: boolean }): string {
|
|
126
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf101-mode-"));
|
|
127
|
+
dirs.push(dir);
|
|
128
|
+
mkdirSync(join(dir, "root"), { recursive: true });
|
|
129
|
+
writeFileSync(
|
|
130
|
+
join(dir, "root", "main.tf"),
|
|
131
|
+
opts.live
|
|
132
|
+
? ["terraform {", " live {", ' estate = "fixture-estate"', " }", "}", ""].join("\n")
|
|
133
|
+
: 'resource "null_resource" "x" {}\n',
|
|
134
|
+
);
|
|
135
|
+
writeFileSync(
|
|
136
|
+
join(dir, "chant.config.json"),
|
|
137
|
+
JSON.stringify({ terraform: { binary: opts.binary ?? "terraform", roots: { app: { dir: "./root" } } } }),
|
|
138
|
+
);
|
|
139
|
+
return dir;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A `LintContext` whose file lives inside `project`, so a config walk would find it if the rule did one. */
|
|
143
|
+
function contextIn(project: string, code: string): LintContext {
|
|
144
|
+
const filePath = join(project, "ops", "app.op.ts");
|
|
145
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
146
|
+
return { sourceFile, entities: [], filePath };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
test("fires on a terraformApply call over a live root, where #2106 exempted it", () => {
|
|
150
|
+
const dir = project({ binary: "choudoufu", live: true });
|
|
151
|
+
const diags = planBeforeApplyRule.check(
|
|
152
|
+
contextIn(
|
|
153
|
+
dir,
|
|
154
|
+
`
|
|
155
|
+
const apply = terraformApply("app", { planFile: "/tmp/plan.out" });
|
|
156
|
+
`,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
expect(diags).toHaveLength(1);
|
|
160
|
+
expect(diags[0].message).toContain("literal path");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("fires on a stock root in the same shape", () => {
|
|
164
|
+
const dir = project({ binary: "terraform", live: false });
|
|
165
|
+
const diags = planBeforeApplyRule.check(
|
|
166
|
+
contextIn(
|
|
167
|
+
dir,
|
|
168
|
+
`
|
|
169
|
+
const apply = terraformApply("app", { planFile: "/tmp/plan.out" });
|
|
170
|
+
`,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
expect(diags).toHaveLength(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("a live root's paired call still passes", () => {
|
|
177
|
+
const dir = project({ binary: "choudoufu", live: true });
|
|
178
|
+
const diags = planBeforeApplyRule.check(
|
|
179
|
+
contextIn(
|
|
180
|
+
dir,
|
|
181
|
+
`
|
|
182
|
+
const plan = terraformPlan("app", { id: "plan" });
|
|
183
|
+
const apply = terraformApply("app", { planFile: plan.out.planFile });
|
|
184
|
+
`,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
expect(diags).toHaveLength(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("the object-literal-only call form is checked the same way", () => {
|
|
191
|
+
const dir = project({ binary: "choudoufu", live: true });
|
|
192
|
+
const diags = planBeforeApplyRule.check(
|
|
193
|
+
contextIn(
|
|
194
|
+
dir,
|
|
195
|
+
`
|
|
196
|
+
const apply = terraformApply({ root: "app", planFile: "/tmp/plan.out" });
|
|
197
|
+
`,
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
expect(diags).toHaveLength(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("no project config at all changes nothing: the rule reads none", () => {
|
|
204
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf101-mode-"));
|
|
205
|
+
dirs.push(dir);
|
|
206
|
+
const diags = planBeforeApplyRule.check(
|
|
207
|
+
contextIn(
|
|
208
|
+
dir,
|
|
209
|
+
`
|
|
210
|
+
const apply = terraformApply("app", { planFile: "/tmp/plan.out" });
|
|
211
|
+
`,
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
expect(diags).toHaveLength(1);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -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,150 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isCredentialShapedValue,
|
|
4
|
+
isLiteralString,
|
|
5
|
+
isPlaceholderValue,
|
|
6
|
+
isSecretName,
|
|
7
|
+
secretShapedAssignment,
|
|
8
|
+
} from "./secret-shape";
|
|
9
|
+
|
|
10
|
+
describe("the name heuristic", () => {
|
|
11
|
+
test.each([
|
|
12
|
+
"password",
|
|
13
|
+
"db_password",
|
|
14
|
+
"PASSWORD",
|
|
15
|
+
"passwd",
|
|
16
|
+
"admin_passwd",
|
|
17
|
+
"secret",
|
|
18
|
+
"client_secret",
|
|
19
|
+
"secret_key",
|
|
20
|
+
"token",
|
|
21
|
+
"deploy_token",
|
|
22
|
+
"api_key",
|
|
23
|
+
"openai_api_key",
|
|
24
|
+
"private_key",
|
|
25
|
+
"access_key",
|
|
26
|
+
])("%s reads like a credential", (name) => {
|
|
27
|
+
expect(isSecretName(name)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test.each([
|
|
31
|
+
"region",
|
|
32
|
+
"instance_count",
|
|
33
|
+
"keystore",
|
|
34
|
+
"tokenizer",
|
|
35
|
+
"passwordless_sudo_enabled",
|
|
36
|
+
"bucket",
|
|
37
|
+
])("%s does not", (name) => {
|
|
38
|
+
expect(isSecretName(name)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test.each([
|
|
42
|
+
"secret_arn",
|
|
43
|
+
"db_password_file",
|
|
44
|
+
"api_key_id",
|
|
45
|
+
"private_key_path",
|
|
46
|
+
"deploy_token_name",
|
|
47
|
+
])("%s is a locator, not the credential itself", (name) => {
|
|
48
|
+
expect(isSecretName(name)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a locator suffix only excludes when it is the suffix", () => {
|
|
52
|
+
expect(isSecretName("arn_password")).toBe(true);
|
|
53
|
+
expect(isSecretName("file_secret")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("the value heuristic", () => {
|
|
58
|
+
test("an AWS access key id is credential-shaped, through core's own detector", () => {
|
|
59
|
+
expect(isCredentialShapedValue("AKIA2E0XYZ4PQRSTUVWX")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a GitHub token is, for the same reason", () => {
|
|
63
|
+
expect(isCredentialShapedValue("ghp_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a JWT is", () => {
|
|
67
|
+
expect(
|
|
68
|
+
isCredentialShapedValue("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc2NH0.9mAcYbBqp4Ck2XjLwr7QeR1sT3uVwXyZ"),
|
|
69
|
+
).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a PEM header is, even without the closing block", () => {
|
|
73
|
+
expect(isCredentialShapedValue("-----BEGIN RSA PRIVATE KEY-----\nMIIEow...")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a long high-entropy token is", () => {
|
|
77
|
+
expect(isCredentialShapedValue("8Jd0hVn2XqB7sLtR4mYcE1zPgWfKa5UiQ3oNrTbA")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test.each(["eu-west-1", "t3.micro", "postgres", "app-prod"])("%s is not", (value) => {
|
|
81
|
+
expect(isCredentialShapedValue(value)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a model id or image ref is not, through core's identifier-slug filter", () => {
|
|
85
|
+
expect(isCredentialShapedValue("registry.example.com/team/app-server")).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a value shorter than eight characters is never judged", () => {
|
|
89
|
+
expect(isCredentialShapedValue("aB3$xY")).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("placeholders", () => {
|
|
94
|
+
test.each(["changeme", "CHANGE_ME", "change-me", "YOUR_API_KEY", "example-secret", "0000000000000000"])(
|
|
95
|
+
"%s is a placeholder",
|
|
96
|
+
(value) => {
|
|
97
|
+
expect(isPlaceholderValue(value)).toBe(true);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
test("a placeholder is never a finding, whatever the attribute is called", () => {
|
|
102
|
+
expect(secretShapedAssignment("db_password", "CHANGE_ME")).toBeUndefined();
|
|
103
|
+
expect(secretShapedAssignment("api_key", "your-api-key")).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("literals versus references", () => {
|
|
108
|
+
test("an hcl2json expression is not a literal", () => {
|
|
109
|
+
// Both `x = var.y` and `x = "${var.y}"` parse to this string.
|
|
110
|
+
expect(isLiteralString("${var.db_password}")).toBe(false);
|
|
111
|
+
expect(isLiteralString("${data.aws_secretsmanager_secret_version.db.secret_string}")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a template with an interpolation in it is not a literal either", () => {
|
|
115
|
+
expect(isLiteralString("prefix-${var.env}-suffix")).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a plain string is", () => {
|
|
119
|
+
expect(isLiteralString("hunter2-prod-db")).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a non-string is not", () => {
|
|
123
|
+
expect(isLiteralString(3)).toBe(false);
|
|
124
|
+
expect(isLiteralString(true)).toBe(false);
|
|
125
|
+
expect(isLiteralString(["a"])).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("secretShapedAssignment", () => {
|
|
130
|
+
test("fires on the name when the value is an unremarkable literal", () => {
|
|
131
|
+
expect(secretShapedAssignment("db_password", "hunter2-prod-db")).toEqual({ reason: "name", length: 15 });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("fires on the value when the name says nothing", () => {
|
|
135
|
+
const shape = secretShapedAssignment("bootstrap", "AKIA2E0XYZ4PQRSTUVWX");
|
|
136
|
+
expect(shape?.reason).toBe("value");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("prefers the value reason when both hold", () => {
|
|
140
|
+
expect(secretShapedAssignment("api_key", "ghp_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8")?.reason).toBe("value");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("declines a reference, which is the fix rather than the finding", () => {
|
|
144
|
+
expect(secretShapedAssignment("db_password", "${var.db_password}")).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("declines a short value", () => {
|
|
148
|
+
expect(secretShapedAssignment("password", "abc123")).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two heuristics the secrets rules of the TF family share: does this
|
|
3
|
+
* attribute's NAME read like a credential, and does its VALUE look like one.
|
|
4
|
+
*
|
|
5
|
+
* tfsec shipped `general-secrets-sensitive-in-variable`,
|
|
6
|
+
* `-in-local` and `-in-attribute`, and trivy dropped all three when it
|
|
7
|
+
* absorbed tfsec, so no maintained tool checks them today (#2107's survey).
|
|
8
|
+
* TF007, TF008, TF009 and TF022 are chant's versions, and they only stay
|
|
9
|
+
* consistent with each other if "secret-shaped" means one thing. That is what
|
|
10
|
+
* this module is.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here re-implements a detector `packages/core/src/audit/secrets.ts`
|
|
13
|
+
* already has. The value heuristic runs a candidate through `scanForSecrets`
|
|
14
|
+
* first, so every vendor prefix the SEC family knows (AWS key ids, GitHub,
|
|
15
|
+
* Slack, Google, Stripe), the PEM block shape and the connection-string shape
|
|
16
|
+
* are recognised here for free, and it reuses that module's placeholder
|
|
17
|
+
* vocabulary, its identifier-slug filter and its entropy function. What it
|
|
18
|
+
* adds is what a Terraform attribute needs and a raw text scan does not: the
|
|
19
|
+
* JWT and bare PEM-header shapes, and a lower length floor than SEC010's 24
|
|
20
|
+
* characters, because an attribute value is already a single token rather
|
|
21
|
+
* than a line of prose to fish tokens out of.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
looksLikeIdentifierSlug,
|
|
26
|
+
looksLikePlaceholder,
|
|
27
|
+
scanForSecrets,
|
|
28
|
+
shannonEntropy,
|
|
29
|
+
} from "@intentius/chant/audit/secrets";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Credential words, matched against the whole attribute name on `_`
|
|
33
|
+
* boundaries, so `db_password` and `access_key` match but `keystore` and
|
|
34
|
+
* `tokenizer` do not.
|
|
35
|
+
*/
|
|
36
|
+
const SECRET_NAME_RE = /(^|_)(password|passwd|secret|token|api_key|private_key|access_key)(_|$)/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Suffixes that turn a credential word into a REFERENCE to a credential:
|
|
40
|
+
* `secret_arn`, `db_password_file`, `api_key_id`, `private_key_path`,
|
|
41
|
+
* `token_name`. The value under one of these is a locator, and a locator is
|
|
42
|
+
* exactly what the fix for the other rules asks for, so flagging it would
|
|
43
|
+
* flag the remediation.
|
|
44
|
+
*/
|
|
45
|
+
const LOCATOR_SUFFIX_RE = /_(file|path|arn|id|name)$/;
|
|
46
|
+
|
|
47
|
+
/** Minimum length for a value to be worth judging at all. */
|
|
48
|
+
export const MIN_SECRET_LENGTH = 8;
|
|
49
|
+
|
|
50
|
+
/** Minimum length for the generic high-entropy shape (below it, entropy says nothing). */
|
|
51
|
+
const MIN_ENTROPY_LENGTH = 16;
|
|
52
|
+
|
|
53
|
+
/** Bits per character above which a token of that length reads as random. */
|
|
54
|
+
const ENTROPY_THRESHOLD = 3.5;
|
|
55
|
+
|
|
56
|
+
/** `eyJ...header.eyJ...payload.signature`. An unsigned JWT still carries claims. */
|
|
57
|
+
const JWT_RE = /^ey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(\.[A-Za-z0-9_-]*)?$/;
|
|
58
|
+
|
|
59
|
+
/** A PEM header on its own, without the closing block `scanForSecrets` requires. */
|
|
60
|
+
const PEM_HEADER_RE = /-----BEGIN ([A-Z]+ )?PRIVATE KEY-----/;
|
|
61
|
+
|
|
62
|
+
/** Normalize an attribute or variable name for the name heuristic. */
|
|
63
|
+
function normalizeName(name: string): string {
|
|
64
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Does this attribute or variable name read like a credential? Case-insensitive
|
|
69
|
+
* and word-bounded, with the locator suffixes excluded.
|
|
70
|
+
*/
|
|
71
|
+
export function isSecretName(name: string): boolean {
|
|
72
|
+
const normalized = normalizeName(name);
|
|
73
|
+
if (LOCATOR_SUFFIX_RE.test(normalized)) return false;
|
|
74
|
+
return SECRET_NAME_RE.test(normalized);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Is this a plain string literal, as opposed to an expression? `hcl2json`
|
|
79
|
+
* renders every expression as a `"${...}"` template, so a value carrying one
|
|
80
|
+
* is a reference (`var.db_password`, `data.aws_secretsmanager_secret_version.x.secret_string`)
|
|
81
|
+
* or an interpolation, never a committed constant. Those are the fix, not the
|
|
82
|
+
* finding.
|
|
83
|
+
*/
|
|
84
|
+
export function isLiteralString(value: unknown): value is string {
|
|
85
|
+
return typeof value === "string" && !value.includes("${");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A placeholder rather than a credential. Core's list is checked twice: once
|
|
90
|
+
* as written, once with separators stripped, so the `CHANGE_ME` and
|
|
91
|
+
* `YOUR_API_KEY` spellings land on the same `changeme` / `your-api-key`
|
|
92
|
+
* entries as the unseparated ones.
|
|
93
|
+
*/
|
|
94
|
+
export function isPlaceholderValue(value: string): boolean {
|
|
95
|
+
return looksLikePlaceholder(value) || looksLikePlaceholder(value.replace(/[-_\s]/g, ""));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** At least 3 of {lower, upper, digit, symbol}, which cuts plain words and pure hex. */
|
|
99
|
+
function hasClassDiversity(value: string): boolean {
|
|
100
|
+
let classes = 0;
|
|
101
|
+
if (/[a-z]/.test(value)) classes++;
|
|
102
|
+
if (/[A-Z]/.test(value)) classes++;
|
|
103
|
+
if (/[0-9]/.test(value)) classes++;
|
|
104
|
+
if (/[^A-Za-z0-9]/.test(value)) classes++;
|
|
105
|
+
return classes >= 3;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The generic shape: a long, diverse, high-entropy token that isn't a slug. */
|
|
109
|
+
function isHighEntropyToken(value: string): boolean {
|
|
110
|
+
if (value.length < MIN_ENTROPY_LENGTH) return false;
|
|
111
|
+
if (!/^[A-Za-z0-9+/=_-]+$/.test(value)) return false;
|
|
112
|
+
if (!hasClassDiversity(value)) return false;
|
|
113
|
+
if (looksLikeIdentifierSlug(value)) return false;
|
|
114
|
+
return shannonEntropy(value) >= ENTROPY_THRESHOLD;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Does this value look like a live credential on its own, whatever it is
|
|
119
|
+
* called? A known vendor shape (through core's detectors), a JWT, a PEM key,
|
|
120
|
+
* or a high-entropy token.
|
|
121
|
+
*/
|
|
122
|
+
export function isCredentialShapedValue(value: unknown): boolean {
|
|
123
|
+
if (!isLiteralString(value)) return false;
|
|
124
|
+
if (value.length < MIN_SECRET_LENGTH) return false;
|
|
125
|
+
if (isPlaceholderValue(value)) return false;
|
|
126
|
+
if (JWT_RE.test(value)) return true;
|
|
127
|
+
if (PEM_HEADER_RE.test(value)) return true;
|
|
128
|
+
// Core's own scan, over the single value: every SEC00x detector plus its
|
|
129
|
+
// high-entropy catch-all, with no file to walk.
|
|
130
|
+
if (scanForSecrets([{ path: "value", content: value }]).length > 0) return true;
|
|
131
|
+
return isHighEntropyToken(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Which heuristic fired, so a diagnostic can say why it is complaining. */
|
|
135
|
+
export type SecretShapeReason = "name" | "value";
|
|
136
|
+
|
|
137
|
+
export interface SecretShape {
|
|
138
|
+
reason: SecretShapeReason;
|
|
139
|
+
/** The value's length, for a message that never quotes the value itself. */
|
|
140
|
+
length: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The combined test the rules use: is `value`, assigned to an attribute called
|
|
145
|
+
* `name`, a committed secret? Either heuristic is enough, but both sides of the
|
|
146
|
+
* gate are the same, since the value has to be a literal, long enough to be a
|
|
147
|
+
* credential, and not a placeholder, so `sensitive = true`-style
|
|
148
|
+
* remediation advice is never given for `password = "changeme"`.
|
|
149
|
+
*
|
|
150
|
+
* Findings never carry the value. Callers get the reason and the length.
|
|
151
|
+
*/
|
|
152
|
+
export function secretShapedAssignment(name: string, value: unknown): SecretShape | undefined {
|
|
153
|
+
if (!isLiteralString(value)) return undefined;
|
|
154
|
+
if (value.length < MIN_SECRET_LENGTH) return undefined;
|
|
155
|
+
if (isPlaceholderValue(value)) return undefined;
|
|
156
|
+
if (isCredentialShapedValue(value)) return { reason: "value", length: value.length };
|
|
157
|
+
if (isSecretName(name)) return { reason: "name", length: value.length };
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live root's ownership channel (#2104): choudoufu's two marker tags.
|
|
3
|
+
*
|
|
4
|
+
* Sibling of `./state-ownership.ts`, and here for the same mechanical reason:
|
|
5
|
+
* `plugin.ts` declares `ownershipChannel` eagerly while `describeResources`
|
|
6
|
+
* is behind a dynamic import, so the key names cannot live in the reader
|
|
7
|
+
* without dragging the reader, and `node:child_process` with it, into every
|
|
8
|
+
* plugin load.
|
|
9
|
+
*
|
|
10
|
+
* ## Why the keys are declared and the verdicts are set by hand
|
|
11
|
+
*
|
|
12
|
+
* The issue that added this asked which of two ways to go: widen core's
|
|
13
|
+
* `OwnershipChannel` so a lexicon can supply its own classifier, or declare
|
|
14
|
+
* the keys for discoverability and set the verdicts in the reader, the way
|
|
15
|
+
* `lexicons/fly/src/ownership.ts` does for its two tiers. The second, and the
|
|
16
|
+
* reason is that the first buys nothing.
|
|
17
|
+
*
|
|
18
|
+
* Core's `classifyOwnership` tests one thing: does `tags[keys.managedBy]`
|
|
19
|
+
* equal the literal `"chant"`. Nothing in core ever calls it on a lexicon's
|
|
20
|
+
* behalf. `packages/core/src/ownership.ts` exports it, every lexicon calls it
|
|
21
|
+
* itself where the test happens to fit, and the ones it does not fit, fly's
|
|
22
|
+
* app boundary and aws's stack tags, already set verdicts directly. Adding a
|
|
23
|
+
* `classify` member to `OwnershipChannel` would give core a field only this
|
|
24
|
+
* lexicon passes and only this lexicon reads back, which is a longer way of
|
|
25
|
+
* writing the function `./describe-resources.ts` already has.
|
|
26
|
+
*
|
|
27
|
+
* What the declaration is still for is the claim it makes. Declaring
|
|
28
|
+
* `describeResources` on the channel is what turns the observation
|
|
29
|
+
* conformance suite's ownership assertion from a shape check into a
|
|
30
|
+
* behavioural one, and that claim holds on a live root exactly as it does on
|
|
31
|
+
* a stock one, from a different reading. The suite runs for both modes in
|
|
32
|
+
* `./describe-resources.test.ts`.
|
|
33
|
+
*
|
|
34
|
+
* ## The keys, and the slot that does not fit
|
|
35
|
+
*
|
|
36
|
+
* `ChannelKeys` is a managed-by / stack / env triple, because everywhere else
|
|
37
|
+
* chant stamps three tags. choudoufu's marker is two tags and no managed-by
|
|
38
|
+
* key at all: per `live/MARKERS.md`'s "Ownership semantics", a resource
|
|
39
|
+
* carrying `tofu-estate` belongs to that estate and there is no secondary
|
|
40
|
+
* check, so `tofu-estate` is both the managed-by claim and the stack
|
|
41
|
+
* identity. `tofu-address` takes the third slot because it is the marker's
|
|
42
|
+
* other half, not because it is an environment: a choudoufu marker has no
|
|
43
|
+
* environment in it, which is why `ResourceMetadata.marker` on a live read
|
|
44
|
+
* carries the estate as its `stack` and leaves `env` absent rather than
|
|
45
|
+
* filling it with an address.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import type { ChannelKeys } from "@intentius/chant/ownership";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The two tag keys `live/MARKERS.md` spec version 1 defines, named here so a
|
|
52
|
+
* reader of the plugin can see what a live root's verdict is read from.
|
|
53
|
+
* `./hcl/marker.ts` holds the constants the reader itself uses, along with
|
|
54
|
+
* the escaping and the ownership semantics that go with them.
|
|
55
|
+
*/
|
|
56
|
+
export const TERRAFORM_LIVE_MARKER_KEYS: ChannelKeys = {
|
|
57
|
+
managedBy: "tofu-estate",
|
|
58
|
+
stack: "tofu-estate",
|
|
59
|
+
env: "tofu-address",
|
|
60
|
+
};
|
|
@@ -91,7 +91,8 @@ describe("LSP completions", () => {
|
|
|
91
91
|
const labels = items.map((i) => i.label);
|
|
92
92
|
expect(labels).toContain("gate");
|
|
93
93
|
expect(labels).toContain("compensate");
|
|
94
|
-
expect(labels).toContain("
|
|
94
|
+
expect(labels).toContain("gateTimeout");
|
|
95
|
+
expect(labels).not.toContain("taskQueue");
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
it("completes a builder's own opts keys inside its second argument", () => {
|
|
@@ -103,7 +104,7 @@ describe("LSP completions", () => {
|
|
|
103
104
|
it("completes root-entry keys inside chant.config.ts's terraform.roots.<name>", () => {
|
|
104
105
|
const content = "export default {\n terraform: {\n roots: {\n app: {\n ";
|
|
105
106
|
const items = completions(ctx({ content, position: { line: 4, character: 8 } }));
|
|
106
|
-
expect(items.map((i) => i.label).sort()).toEqual(["backendConfig", "dir", "varFiles", "workspace"]);
|
|
107
|
+
expect(items.map((i) => i.label).sort()).toEqual(["backendConfig", "delete", "dir", "varFiles", "workspace"]);
|
|
107
108
|
});
|
|
108
109
|
|
|
109
110
|
it("completes the terraform namespace's own top-level keys", () => {
|
package/src/lsp/option-keys.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface OptionKey {
|
|
|
15
15
|
|
|
16
16
|
/** `TerraformConfig`'s own top-level keys (`terraform: { <here> }`). */
|
|
17
17
|
export const CONFIG_NAMESPACE_KEYS: OptionKey[] = [
|
|
18
|
-
{ key: "binary", detail: '"terraform" | "tofu" — which CLI drives the roots. Default: "terraform".' },
|
|
18
|
+
{ key: "binary", detail: '"terraform" | "tofu" | "choudoufu" — which CLI drives the roots. Default: "terraform".' },
|
|
19
19
|
{ key: "roots", detail: "Named root modules. The name is the entity-key prefix." },
|
|
20
20
|
];
|
|
21
21
|
|
|
@@ -25,21 +25,21 @@ export const ROOT_ENTRY_KEYS: OptionKey[] = [
|
|
|
25
25
|
{ key: "workspace", detail: "Terraform workspace to select for this root. Omitted means `default`." },
|
|
26
26
|
{ key: "varFiles", detail: "`-var-file` arguments, in order, relative to `dir`." },
|
|
27
27
|
{ key: "backendConfig", detail: "`-backend-config` key/value pairs handed to `init`." },
|
|
28
|
+
{ key: "delete", detail: '"never" | "owned-only" | "gated" — this root\'s delete mode, mapped onto choudoufu\'s policy block on a live root.' },
|
|
28
29
|
];
|
|
29
30
|
|
|
30
31
|
/** `TerraformApplyOpConfig`'s keys (`TerraformApplyOp({ <here> })`). */
|
|
31
32
|
export const APPLY_OP_KEYS: OptionKey[] = [
|
|
32
|
-
{ key: "name", detail: "Op name (kebab-case).
|
|
33
|
+
{ key: "name", detail: "Op name (kebab-case). `signalName` defaults to `approve-<name>`." },
|
|
33
34
|
{ key: "root", detail: "Key into the project's `terraform.roots`." },
|
|
34
35
|
{ key: "planFile", detail: "Plan file written by Plan and consumed by Apply. Default: `chant.tfplan`." },
|
|
35
36
|
{ key: "gate", detail: '"on-destroy" | "always" | "never" — when to emit the approval gate. Default: "on-destroy".' },
|
|
36
37
|
{ key: "signalName", detail: "Gate signal name. Default: `approve-<name>`." },
|
|
37
|
-
{ key: "gateTimeout", detail: "
|
|
38
|
+
{ key: "gateTimeout", detail: "How long a recorded pending gate stays valid, as a duration string. Default: core's own (48h)." },
|
|
38
39
|
{ key: "gateDescription", detail: "Override the gate description shown to the approver." },
|
|
39
40
|
{ key: "upgrade", detail: "`-upgrade` on the Init step: re-resolve provider and module versions." },
|
|
40
41
|
{ key: "cwd", detail: "Directory each step starts the `chant.config.*` search from." },
|
|
41
42
|
{ key: "compensate", detail: "Saga-style rollback on a failed apply: `true` with no command throws; supply `{ command }`." },
|
|
42
|
-
{ key: "taskQueue", detail: "Override the task queue. Defaults to `name`." },
|
|
43
43
|
];
|
|
44
44
|
|
|
45
45
|
/** `compensate`'s own shape when it is an object: `compensate: { <here> }`. */
|
|
@@ -71,7 +71,12 @@ export const PLAN_OPTS_KEYS: OptionKey[] = [
|
|
|
71
71
|
|
|
72
72
|
/** `terraformApply(root, { <here> })`'s opts — `TerraformApplyArgs` minus `root`, plus step opts. */
|
|
73
73
|
export const APPLY_OPTS_KEYS: OptionKey[] = [
|
|
74
|
-
{
|
|
74
|
+
{
|
|
75
|
+
key: "planFile",
|
|
76
|
+
detail:
|
|
77
|
+
"The saved plan file to apply. Required on either kind of root; on a live root it is the approval " +
|
|
78
|
+
"artifact the apply re-plans against and refuses on a mismatch (choudoufu #878).",
|
|
79
|
+
},
|
|
75
80
|
{ key: "cwd", detail: "Directory to start the `chant.config.*` search from." },
|
|
76
81
|
...STEP_OPTS,
|
|
77
82
|
];
|