@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.
Files changed (381) hide show
  1. package/dist/composites/terraform-adopt-op.d.ts +126 -0
  2. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  3. package/dist/composites/terraform-apply-op.d.ts +63 -16
  4. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  5. package/dist/composites/terraform-watch-op.d.ts +80 -36
  6. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  7. package/dist/config.d.ts +49 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/describe-resources.d.ts +256 -16
  10. package/dist/describe-resources.d.ts.map +1 -1
  11. package/dist/hcl/descend.d.ts +98 -0
  12. package/dist/hcl/descend.d.ts.map +1 -0
  13. package/dist/hcl/marker.d.ts +130 -0
  14. package/dist/hcl/marker.d.ts.map +1 -0
  15. package/dist/hcl/parse.d.ts +142 -9
  16. package/dist/hcl/parse.d.ts.map +1 -1
  17. package/dist/hcl/references.d.ts +76 -0
  18. package/dist/hcl/references.d.ts.map +1 -0
  19. package/dist/hcl/roots.d.ts +22 -3
  20. package/dist/hcl/roots.d.ts.map +1 -1
  21. package/dist/hcl/suppressions.d.ts +54 -0
  22. package/dist/hcl/suppressions.d.ts.map +1 -0
  23. package/dist/hcl/value.d.ts +77 -0
  24. package/dist/hcl/value.d.ts.map +1 -0
  25. package/dist/index.d.ts +5 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/integrity.json +32 -5
  28. package/dist/lint/audit-catalog.d.ts +3 -2
  29. package/dist/lint/audit-catalog.d.ts.map +1 -1
  30. package/dist/lint/audit-lineage.d.ts +68 -0
  31. package/dist/lint/audit-lineage.d.ts.map +1 -1
  32. package/dist/lint/post-synth/blocks.d.ts +53 -0
  33. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  34. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
  36. package/dist/lint/post-synth/index.d.ts.map +1 -1
  37. package/dist/lint/post-synth/module-source.d.ts +38 -0
  38. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  39. package/dist/lint/post-synth/scope.d.ts +65 -0
  40. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  41. package/dist/lint/post-synth/tf001.d.ts +18 -0
  42. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  43. package/dist/lint/post-synth/tf002.d.ts +32 -0
  44. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  45. package/dist/lint/post-synth/tf003.d.ts +22 -0
  46. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  47. package/dist/lint/post-synth/tf004.d.ts +22 -0
  48. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf005.d.ts +28 -0
  50. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  51. package/dist/lint/post-synth/tf006.d.ts +21 -0
  52. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  53. package/dist/lint/post-synth/tf007.d.ts +21 -0
  54. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  55. package/dist/lint/post-synth/tf008.d.ts +22 -0
  56. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  57. package/dist/lint/post-synth/tf009.d.ts +24 -0
  58. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  59. package/dist/lint/post-synth/tf010.d.ts +19 -0
  60. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  61. package/dist/lint/post-synth/tf011.d.ts +18 -0
  62. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  63. package/dist/lint/post-synth/tf012.d.ts +17 -0
  64. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  65. package/dist/lint/post-synth/tf013.d.ts +21 -0
  66. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  67. package/dist/lint/post-synth/tf014.d.ts +25 -0
  68. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  69. package/dist/lint/post-synth/tf015.d.ts +21 -0
  70. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  71. package/dist/lint/post-synth/tf016.d.ts +30 -0
  72. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  73. package/dist/lint/post-synth/tf017.d.ts +19 -0
  74. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  75. package/dist/lint/post-synth/tf018.d.ts +20 -0
  76. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  77. package/dist/lint/post-synth/tf019.d.ts +25 -0
  78. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  79. package/dist/lint/post-synth/tf020.d.ts +38 -0
  80. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  81. package/dist/lint/post-synth/tf021.d.ts +41 -0
  82. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  83. package/dist/lint/post-synth/tf022.d.ts +24 -0
  84. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  85. package/dist/lint/post-synth/tf024.d.ts +24 -0
  86. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  87. package/dist/lint/post-synth/tf025.d.ts +29 -0
  88. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  89. package/dist/lint/post-synth/tf026.d.ts +21 -0
  90. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  91. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  92. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  93. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  94. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  95. package/dist/lint/secret-shape.d.ts +68 -0
  96. package/dist/lint/secret-shape.d.ts.map +1 -0
  97. package/dist/live-ownership.d.ts +55 -0
  98. package/dist/live-ownership.d.ts.map +1 -0
  99. package/dist/lsp/option-keys.d.ts.map +1 -1
  100. package/dist/manifest.json +1 -1
  101. package/dist/okf/index.md +24 -0
  102. package/dist/okf/rules/TF002.md +11 -0
  103. package/dist/okf/rules/TF003.md +11 -0
  104. package/dist/okf/rules/TF004.md +11 -0
  105. package/dist/okf/rules/TF005.md +11 -0
  106. package/dist/okf/rules/TF006.md +11 -0
  107. package/dist/okf/rules/TF007.md +11 -0
  108. package/dist/okf/rules/TF008.md +11 -0
  109. package/dist/okf/rules/TF009.md +11 -0
  110. package/dist/okf/rules/TF010.md +11 -0
  111. package/dist/okf/rules/TF011.md +11 -0
  112. package/dist/okf/rules/TF012.md +11 -0
  113. package/dist/okf/rules/TF013.md +11 -0
  114. package/dist/okf/rules/TF014.md +11 -0
  115. package/dist/okf/rules/TF015.md +11 -0
  116. package/dist/okf/rules/TF016.md +11 -0
  117. package/dist/okf/rules/TF017.md +11 -0
  118. package/dist/okf/rules/TF018.md +11 -0
  119. package/dist/okf/rules/TF019.md +11 -0
  120. package/dist/okf/rules/TF020.md +11 -0
  121. package/dist/okf/rules/TF021.md +11 -0
  122. package/dist/okf/rules/TF022.md +11 -0
  123. package/dist/okf/rules/TF024.md +11 -0
  124. package/dist/okf/rules/TF025.md +11 -0
  125. package/dist/okf/rules/TF026.md +11 -0
  126. package/dist/op/activities/index.d.ts +13 -6
  127. package/dist/op/activities/index.d.ts.map +1 -1
  128. package/dist/op/activities/live-detect.d.ts +51 -0
  129. package/dist/op/activities/live-detect.d.ts.map +1 -0
  130. package/dist/op/activities/terraform.d.ts +445 -17
  131. package/dist/op/activities/terraform.d.ts.map +1 -1
  132. package/dist/op/activity-contracts.d.ts +148 -0
  133. package/dist/op/activity-contracts.d.ts.map +1 -0
  134. package/dist/op/adoption.d.ts +118 -0
  135. package/dist/op/adoption.d.ts.map +1 -0
  136. package/dist/op/builders.d.ts +46 -6
  137. package/dist/op/builders.d.ts.map +1 -1
  138. package/dist/op/resolve-root-mode.d.ts +45 -0
  139. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  140. package/dist/plugin.d.ts.map +1 -1
  141. package/dist/rules/blocks.ts +109 -0
  142. package/dist/rules/module-source.ts +121 -0
  143. package/dist/rules/plan-before-apply.ts +11 -0
  144. package/dist/rules/scope.ts +107 -0
  145. package/dist/rules/tf001.ts +32 -8
  146. package/dist/rules/tf002.ts +151 -0
  147. package/dist/rules/tf003.ts +71 -0
  148. package/dist/rules/tf004.ts +63 -0
  149. package/dist/rules/tf005.ts +76 -0
  150. package/dist/rules/tf006.ts +53 -0
  151. package/dist/rules/tf007.ts +78 -0
  152. package/dist/rules/tf008.ts +75 -0
  153. package/dist/rules/tf009.ts +59 -0
  154. package/dist/rules/tf010.ts +51 -0
  155. package/dist/rules/tf011.ts +49 -0
  156. package/dist/rules/tf012.ts +47 -0
  157. package/dist/rules/tf013.ts +63 -0
  158. package/dist/rules/tf014.ts +72 -0
  159. package/dist/rules/tf015.ts +63 -0
  160. package/dist/rules/tf016.ts +75 -0
  161. package/dist/rules/tf017.ts +50 -0
  162. package/dist/rules/tf018.ts +72 -0
  163. package/dist/rules/tf019.ts +72 -0
  164. package/dist/rules/tf020.ts +123 -0
  165. package/dist/rules/tf021.ts +126 -0
  166. package/dist/rules/tf022.ts +61 -0
  167. package/dist/rules/tf024.ts +70 -0
  168. package/dist/rules/tf025.ts +101 -0
  169. package/dist/rules/tf026.ts +80 -0
  170. package/dist/skills/chant-terraform.md +1 -1
  171. package/package.json +7 -2
  172. package/src/__fixtures__/live/main.tf +28 -0
  173. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  174. package/src/__fixtures__/live-adopt/main.tf +21 -0
  175. package/src/__fixtures__/live-estate/README.md +58 -0
  176. package/src/__fixtures__/live-estate/compute.tf +10 -0
  177. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  178. package/src/__fixtures__/live-estate/logs.tf +8 -0
  179. package/src/__fixtures__/live-estate/network.tf +24 -0
  180. package/src/__fixtures__/live-estate/rule.tf +11 -0
  181. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  182. package/src/__fixtures__/live-estate/versions.tf +31 -0
  183. package/src/__fixtures__/live-ls.json +98 -0
  184. package/src/__fixtures__/live-plan.json +285 -0
  185. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  186. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  187. package/src/__fixtures__/module-tree/README.md +16 -0
  188. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  189. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  190. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  191. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  192. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  193. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  194. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  195. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  196. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  197. package/src/__fixtures__/references/main.tf +56 -0
  198. package/src/codegen/lint-rules-page.test.ts +28 -0
  199. package/src/composites/terraform-adopt-op.acceptance.test.ts +166 -0
  200. package/src/composites/terraform-adopt-op.test.ts +170 -0
  201. package/src/composites/terraform-adopt-op.ts +197 -0
  202. package/src/composites/terraform-apply-op.acceptance.test.ts +183 -5
  203. package/src/composites/terraform-apply-op.test.ts +167 -15
  204. package/src/composites/terraform-apply-op.ts +105 -28
  205. package/src/composites/terraform-watch-op.test.ts +85 -26
  206. package/src/composites/terraform-watch-op.ts +130 -73
  207. package/src/config.ts +32 -3
  208. package/src/describe-resources.live.test.ts +598 -0
  209. package/src/describe-resources.test.ts +53 -0
  210. package/src/describe-resources.ts +811 -28
  211. package/src/hcl/descend.test.ts +162 -0
  212. package/src/hcl/descend.ts +273 -0
  213. package/src/hcl/marker.test.ts +147 -0
  214. package/src/hcl/marker.ts +235 -0
  215. package/src/hcl/parse.test.ts +69 -0
  216. package/src/hcl/parse.ts +249 -27
  217. package/src/hcl/references.test.ts +116 -0
  218. package/src/hcl/references.ts +172 -0
  219. package/src/hcl/roots.ts +59 -5
  220. package/src/hcl/suppressions.test.ts +147 -0
  221. package/src/hcl/suppressions.ts +204 -0
  222. package/src/hcl/value.test.ts +73 -0
  223. package/src/hcl/value.ts +106 -0
  224. package/src/index.ts +31 -1
  225. package/src/lint/audit-catalog.ts +264 -3
  226. package/src/lint/audit-lineage.ts +319 -1
  227. package/src/lint/audit.test.ts +40 -1
  228. package/src/lint/post-synth/blocks.ts +109 -0
  229. package/src/lint/post-synth/fixtures/TF001/negative.tf +28 -0
  230. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  231. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  232. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  233. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  234. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  235. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  236. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  237. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  238. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  239. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  240. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  241. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  242. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  243. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  244. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  245. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  246. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  247. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  248. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  249. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  250. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  251. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  252. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  253. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  254. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  255. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  256. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  257. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  258. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  259. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  260. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  261. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  262. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  263. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  264. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  265. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  266. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  267. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  268. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  269. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  270. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  271. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  272. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  273. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  274. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  275. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  276. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  277. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  278. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  279. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  280. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  281. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  282. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  283. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  284. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  285. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  286. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  287. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  288. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  289. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  290. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  291. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  292. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  293. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  294. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  295. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  296. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  297. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  298. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  299. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  300. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  301. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  302. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  303. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  304. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  305. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  306. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  307. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  309. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  310. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  311. package/src/lint/post-synth/fixtures/load.ts +108 -0
  312. package/src/lint/post-synth/index.ts +48 -0
  313. package/src/lint/post-synth/module-rules.test.ts +153 -0
  314. package/src/lint/post-synth/module-source.test.ts +75 -0
  315. package/src/lint/post-synth/module-source.ts +121 -0
  316. package/src/lint/post-synth/post-synth.test.ts +14 -15
  317. package/src/lint/post-synth/scope.ts +107 -0
  318. package/src/lint/post-synth/single-block.test.ts +263 -0
  319. package/src/lint/post-synth/suppression.test.ts +110 -0
  320. package/src/lint/post-synth/tf001.ts +32 -8
  321. package/src/lint/post-synth/tf002.test.ts +93 -0
  322. package/src/lint/post-synth/tf002.ts +151 -0
  323. package/src/lint/post-synth/tf003.test.ts +75 -0
  324. package/src/lint/post-synth/tf003.ts +71 -0
  325. package/src/lint/post-synth/tf004.test.ts +47 -0
  326. package/src/lint/post-synth/tf004.ts +63 -0
  327. package/src/lint/post-synth/tf005.test.ts +57 -0
  328. package/src/lint/post-synth/tf005.ts +76 -0
  329. package/src/lint/post-synth/tf006.ts +53 -0
  330. package/src/lint/post-synth/tf007.ts +78 -0
  331. package/src/lint/post-synth/tf008.ts +75 -0
  332. package/src/lint/post-synth/tf009.ts +59 -0
  333. package/src/lint/post-synth/tf010.ts +51 -0
  334. package/src/lint/post-synth/tf011.ts +49 -0
  335. package/src/lint/post-synth/tf012.ts +47 -0
  336. package/src/lint/post-synth/tf013.ts +63 -0
  337. package/src/lint/post-synth/tf014.ts +72 -0
  338. package/src/lint/post-synth/tf015.ts +63 -0
  339. package/src/lint/post-synth/tf016.ts +75 -0
  340. package/src/lint/post-synth/tf017.ts +50 -0
  341. package/src/lint/post-synth/tf018.ts +72 -0
  342. package/src/lint/post-synth/tf019.ts +72 -0
  343. package/src/lint/post-synth/tf020.ts +123 -0
  344. package/src/lint/post-synth/tf021.ts +126 -0
  345. package/src/lint/post-synth/tf022.ts +61 -0
  346. package/src/lint/post-synth/tf024.test.ts +42 -0
  347. package/src/lint/post-synth/tf024.ts +70 -0
  348. package/src/lint/post-synth/tf025.test.ts +46 -0
  349. package/src/lint/post-synth/tf025.ts +101 -0
  350. package/src/lint/post-synth/tf026.test.ts +56 -0
  351. package/src/lint/post-synth/tf026.ts +80 -0
  352. package/src/lint/presets.test.ts +72 -0
  353. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  354. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  355. package/src/lint/rules/fixtures/load.ts +53 -0
  356. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  357. package/src/lint/rules/plan-before-apply.ts +11 -0
  358. package/src/lint/secret-shape.test.ts +150 -0
  359. package/src/lint/secret-shape.ts +159 -0
  360. package/src/live-ownership.ts +60 -0
  361. package/src/lsp/completions.test.ts +3 -2
  362. package/src/lsp/option-keys.ts +10 -5
  363. package/src/op/activities/choudoufu.acceptance.test.ts +143 -0
  364. package/src/op/activities/choudoufu.test.ts +693 -0
  365. package/src/op/activities/index.ts +40 -4
  366. package/src/op/activities/live-detect.test.ts +143 -0
  367. package/src/op/activities/live-detect.ts +162 -0
  368. package/src/op/activities/registry.test.ts +11 -3
  369. package/src/op/activities/terraform.test.ts +7 -3
  370. package/src/op/activities/terraform.ts +823 -69
  371. package/src/op/activity-contracts.test.ts +147 -0
  372. package/src/op/activity-contracts.ts +178 -0
  373. package/src/op/adoption.test.ts +170 -0
  374. package/src/op/adoption.ts +241 -0
  375. package/src/op/builders.test.ts +60 -5
  376. package/src/op/builders.ts +93 -5
  377. package/src/op/resolve-root-mode.test.ts +85 -0
  378. package/src/op/resolve-root-mode.ts +72 -0
  379. package/src/plugin.test.ts +68 -0
  380. package/src/plugin.ts +75 -9
  381. package/src/skills/chant-terraform.md +1 -1
@@ -11,6 +11,16 @@
11
11
  * One diagnostic per root, fired from the root's `terraform` block. A root
12
12
  * with no `terraform` block at all is not flagged: it declares no version
13
13
  * constraints either, and the missing block is a different finding.
14
+ *
15
+ * Scope: root modules only (#2112). A child module must NOT declare a
16
+ * backend, which is the mirror image and TF015's finding, so a descended
17
+ * module's `terraform` block is skipped here rather than read as if it were
18
+ * the root's.
19
+ *
20
+ * Does not fire on a live root (#2103): a `backend` block is exactly what
21
+ * choudoufu refuses there (TF024's territory), and the fallback to local
22
+ * state this check warns about does not apply, since a live root keeps no
23
+ * state file at all, local or remote.
14
24
  */
15
25
 
16
26
  import type {
@@ -20,13 +30,20 @@ import type {
20
30
  } from "@intentius/chant/lint/post-synth";
21
31
  import { isResourceDeclarable } from "@intentius/chant/declarable";
22
32
  import { TERRAFORM_TYPE, type BlockBody } from "../../hcl/parse";
33
+ import { attr } from "../../hcl/value";
34
+ import { isRootScoped } from "./scope";
23
35
 
24
- /** `backend`/`cloud` are blocks, so hcl2json encodes them as a value under the key. */
25
- function hasBlock(body: BlockBody, key: string): boolean {
26
- const value = body[key];
27
- if (value === undefined || value === null) return false;
28
- if (Array.isArray(value)) return value.length > 0;
29
- return typeof value === "object" ? Object.keys(value as object).length > 0 : true;
36
+ /**
37
+ * `backend`/`cloud` are blocks, so hcl2json encodes them as a value under the
38
+ * key, structurally the same "literal value under this key" shape `attr()`
39
+ * (`../../hcl/value.ts`, chant #2113) reads for an ordinary attribute, so
40
+ * this reads through it rather than re-deriving presence by hand.
41
+ */
42
+ export function hasBlock(body: BlockBody, key: string): boolean {
43
+ const a = attr(body, key);
44
+ if (a.kind === "absent") return false;
45
+ if (Array.isArray(a.value)) return a.value.length > 0;
46
+ return typeof a.value === "object" && a.value !== null ? Object.keys(a.value).length > 0 : true;
30
47
  }
31
48
 
32
49
  export const tf001: PostSynthCheck = {
@@ -40,8 +57,9 @@ export const tf001: PostSynthCheck = {
40
57
 
41
58
  for (const [name, entity] of ctx.entities) {
42
59
  if (entity.entityType !== TERRAFORM_TYPE) continue;
43
- if (!isResourceDeclarable(entity)) continue;
44
- const props = entity.props as { root?: unknown; body?: unknown };
60
+ if (!isResourceDeclarable(entity) || !isRootScoped(entity)) continue;
61
+ const props = entity.props as { root?: unknown; body?: unknown; mode?: unknown };
62
+ if (props.mode === "live") continue;
45
63
  const root = typeof props.root === "string" ? props.root : "";
46
64
  if (flagged.has(root)) continue;
47
65
 
@@ -58,6 +76,12 @@ export const tf001: PostSynthCheck = {
58
76
  'plaintext. Add a `backend "<type>"` or a `cloud {}` block to the terraform block.',
59
77
  entity: name,
60
78
  lexicon: "terraform",
79
+ // The missing-resource shape (chant #2113): there is no `backend`/
80
+ // `cloud` block to point at, only the root that lacks one. `entity`
81
+ // above still names the `terraform` block this fired from (by
82
+ // convenience, not because that block is what's wrong), so `missing`
83
+ // is what a suppression should key on instead.
84
+ missing: { kind: "backend", scope: root },
61
85
  });
62
86
  }
63
87
 
@@ -0,0 +1,93 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+ import { tf002 } from "./tf002";
4
+ import { loadFixture } from "./fixtures/load";
5
+ import { TERRAFORM_TYPE, PROVIDER_TYPE, RESOURCE_TYPE, terraformEntity } from "../../hcl/parse";
6
+
7
+ function makeCtx(entities: Record<string, ReturnType<typeof terraformEntity>>): PostSynthContext {
8
+ return {
9
+ outputs: new Map(),
10
+ entities: new Map(Object.entries(entities)),
11
+ } as unknown as PostSynthContext;
12
+ }
13
+
14
+ describe("TF002: provider implied by the root has no required_providers entry", () => {
15
+ test("check metadata", () => {
16
+ expect(tf002.id).toBe("TF002");
17
+ expect(tf002.description).toContain("required_providers");
18
+ });
19
+
20
+ test("flags a provider implied by a resource type prefix with no entry at all", async () => {
21
+ const diags = tf002.check(await loadFixture("TF002", "positive"));
22
+ expect(diags).toHaveLength(1);
23
+ expect(diags[0].checkId).toBe("TF002");
24
+ expect(diags[0].message).toContain("google");
25
+ expect(diags[0].lexicon).toBe("terraform");
26
+ });
27
+
28
+ test("flags the legacy shorthand form as lacking source", async () => {
29
+ const diags = tf002.check(await loadFixture("TF002", "positive-legacy"));
30
+ expect(diags).toHaveLength(1);
31
+ expect(diags[0].message).toContain("aws");
32
+ expect(diags[0].message).toContain("source");
33
+ });
34
+
35
+ test("flags an entry missing version", async () => {
36
+ const diags = tf002.check(await loadFixture("TF002", "positive-no-version"));
37
+ expect(diags).toHaveLength(1);
38
+ expect(diags[0].message).toContain("version");
39
+ });
40
+
41
+ test("passes a fully constrained provider", async () => {
42
+ const diags = tf002.check(await loadFixture("TF002", "negative"));
43
+ expect(diags).toHaveLength(0);
44
+ });
45
+
46
+ test("fires once per provider, not once per resource", () => {
47
+ const diags = tf002.check(
48
+ makeCtx({
49
+ "app/terraform": terraformEntity(TERRAFORM_TYPE, "terraform", { required_version: ">= 1.5.0" }, "main.tf", "app"),
50
+ "app/aws_instance.a": terraformEntity(RESOURCE_TYPE, "aws_instance.a", {}, "main.tf", "app"),
51
+ "app/aws_instance.b": terraformEntity(RESOURCE_TYPE, "aws_instance.b", {}, "main.tf", "app"),
52
+ }),
53
+ );
54
+ expect(diags).toHaveLength(1);
55
+ });
56
+
57
+ test("skips the builtin terraform provider (terraform_remote_state)", () => {
58
+ const diags = tf002.check(
59
+ makeCtx({
60
+ "app/terraform": terraformEntity(TERRAFORM_TYPE, "terraform", { required_version: ">= 1.5.0" }, "main.tf", "app"),
61
+ "app/data.terraform_remote_state.other": terraformEntity(
62
+ "Terraform::Data",
63
+ "data.terraform_remote_state.other",
64
+ {},
65
+ "main.tf",
66
+ "app",
67
+ ),
68
+ }),
69
+ );
70
+ expect(diags).toHaveLength(0);
71
+ });
72
+
73
+ test("anchors on the explicit provider block over the resource that also implies it", () => {
74
+ const diags = tf002.check(
75
+ makeCtx({
76
+ "app/terraform": terraformEntity(TERRAFORM_TYPE, "terraform", { required_version: ">= 1.5.0" }, "main.tf", "app"),
77
+ "app/aws_instance.web": terraformEntity(RESOURCE_TYPE, "aws_instance.web", {}, "main.tf", "app"),
78
+ "app/provider.aws": terraformEntity(PROVIDER_TYPE, "provider.aws", {}, "main.tf", "app"),
79
+ }),
80
+ );
81
+ expect(diags).toHaveLength(1);
82
+ expect(diags[0].entity).toBe("app/provider.aws");
83
+ });
84
+
85
+ test("ignores a root with no terraform block at all", () => {
86
+ const diags = tf002.check(
87
+ makeCtx({
88
+ "app/aws_instance.web": terraformEntity(RESOURCE_TYPE, "aws_instance.web", {}, "main.tf", "app"),
89
+ }),
90
+ );
91
+ expect(diags).toHaveLength(0);
92
+ });
93
+ });
@@ -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,75 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+ import { tf003 } from "./tf003";
4
+ import { loadFixture } from "./fixtures/load";
5
+ import { TERRAFORM_TYPE, terraformEntity } from "../../hcl/parse";
6
+
7
+ function makeCtx(entities: Record<string, ReturnType<typeof terraformEntity>>): PostSynthContext {
8
+ return {
9
+ outputs: new Map(),
10
+ entities: new Map(Object.entries(entities)),
11
+ } as unknown as PostSynthContext;
12
+ }
13
+
14
+ function terraformBlock(root: string, body: Record<string, unknown>): ReturnType<typeof terraformEntity> {
15
+ return terraformEntity(TERRAFORM_TYPE, "terraform", body, "main.tf", root);
16
+ }
17
+
18
+ describe("TF003: root module's terraform block has no required_version", () => {
19
+ test("check metadata", () => {
20
+ expect(tf003.id).toBe("TF003");
21
+ expect(tf003.description).toContain("required_version");
22
+ });
23
+
24
+ test("flags a terraform block missing required_version", async () => {
25
+ const diags = tf003.check(await loadFixture("TF003", "positive"));
26
+ expect(diags).toHaveLength(1);
27
+ expect(diags[0].checkId).toBe("TF003");
28
+ expect(diags[0].entity).toBe("TF003/terraform");
29
+ expect(diags[0].lexicon).toBe("terraform");
30
+ });
31
+
32
+ test("passes a terraform block with required_version", async () => {
33
+ const diags = tf003.check(await loadFixture("TF003", "negative"));
34
+ expect(diags).toHaveLength(0);
35
+ });
36
+
37
+ test("fires once per root even split across two terraform blocks", () => {
38
+ const diags = tf003.check(
39
+ makeCtx({
40
+ "app/terraform": terraformBlock("app", {}),
41
+ "app/terraform~2": terraformBlock("app", { required_providers: [{}] }),
42
+ }),
43
+ );
44
+ expect(diags).toHaveLength(1);
45
+ });
46
+
47
+ test("passes when required_version is declared on a later block for the same root", () => {
48
+ const diags = tf003.check(
49
+ makeCtx({
50
+ "app/terraform": terraformBlock("app", {}),
51
+ "app/terraform~2": terraformBlock("app", { required_version: ">= 1.5.0" }),
52
+ }),
53
+ );
54
+ expect(diags).toHaveLength(0);
55
+ });
56
+
57
+ test("reports each root separately", () => {
58
+ const diags = tf003.check(
59
+ makeCtx({
60
+ "legacy/terraform": terraformBlock("legacy", {}),
61
+ "app/terraform": terraformBlock("app", { required_version: ">= 1.5.0" }),
62
+ }),
63
+ );
64
+ expect(diags.map((d) => d.entity)).toEqual(["legacy/terraform"]);
65
+ });
66
+
67
+ test("ignores a root with no terraform block at all", () => {
68
+ const diags = tf003.check(
69
+ makeCtx({
70
+ "app/null_resource.first": terraformEntity("Terraform::Resource", "null_resource.first", {}, "main.tf", "app"),
71
+ }),
72
+ );
73
+ expect(diags).toHaveLength(0);
74
+ });
75
+ });
@@ -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,47 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { tf004 } from "./tf004";
3
+ import { tf005 } from "./tf005";
4
+ import { loadFixture } from "./fixtures/load";
5
+
6
+ describe("TF004: registry-sourced module block has no version", () => {
7
+ test("check metadata", () => {
8
+ expect(tf004.id).toBe("TF004");
9
+ expect(tf004.description.toLowerCase()).toContain("registry");
10
+ });
11
+
12
+ test("flags a registry module with no version", async () => {
13
+ const diags = tf004.check(await loadFixture("TF004", "positive"));
14
+ expect(diags).toHaveLength(1);
15
+ expect(diags[0].checkId).toBe("TF004");
16
+ expect(diags[0].message).toContain("terraform-aws-modules/vpc/aws");
17
+ expect(diags[0].entity).toBe("TF004/module.vpc");
18
+ expect(diags[0].lexicon).toBe("terraform");
19
+ });
20
+
21
+ test("passes a registry module with a version", async () => {
22
+ const diags = tf004.check(await loadFixture("TF004", "negative"));
23
+ expect(diags).toHaveLength(0);
24
+ });
25
+
26
+ test("never fires on a local source", async () => {
27
+ const diags = tf004.check(await loadFixture("TF004", "negative-local"));
28
+ expect(diags).toHaveLength(0);
29
+ });
30
+
31
+ test("never fires on a git source", async () => {
32
+ const diags = tf004.check(await loadFixture("TF004", "negative-git"));
33
+ expect(diags).toHaveLength(0);
34
+ });
35
+
36
+ test("is exclusive of TF005 on a git-sourced module block", async () => {
37
+ const ctx = await loadFixture("TF004", "negative-git");
38
+ expect(tf004.check(ctx)).toHaveLength(0);
39
+ expect(tf005.check(ctx)).toHaveLength(0); // pinned to a tag, so neither fires
40
+ });
41
+
42
+ test("is exclusive of TF005 on a registry-sourced module block", async () => {
43
+ const ctx = await loadFixture("TF004", "positive");
44
+ expect(tf004.check(ctx)).toHaveLength(1);
45
+ expect(tf005.check(ctx)).toHaveLength(0);
46
+ });
47
+ });
@@ -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,57 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { tf004 } from "./tf004";
3
+ import { tf005 } from "./tf005";
4
+ import { loadFixture } from "./fixtures/load";
5
+
6
+ describe("TF005: git/hg module source is unpinned, or pinned to a mutable ref", () => {
7
+ test("check metadata", () => {
8
+ expect(tf005.id).toBe("TF005");
9
+ expect(tf005.description).toContain("Git/hg");
10
+ });
11
+
12
+ test("flags an unpinned git source", async () => {
13
+ const diags = tf005.check(await loadFixture("TF005", "positive"));
14
+ expect(diags).toHaveLength(1);
15
+ expect(diags[0].checkId).toBe("TF005");
16
+ expect(diags[0].message).toContain("no `?ref=`");
17
+ expect(diags[0].entity).toBe("TF005/module.vpc");
18
+ });
19
+
20
+ test("flags a ref pinned to a default branch", async () => {
21
+ const diags = tf005.check(await loadFixture("TF005", "positive-branch"));
22
+ expect(diags).toHaveLength(1);
23
+ expect(diags[0].message).toContain("mutable branch");
24
+ });
25
+
26
+ test("flags a ref that is neither a tag nor a full SHA (stricter than checkov's \\d\\.\\d)", async () => {
27
+ const diags = tf005.check(await loadFixture("TF005", "positive-non-semver"));
28
+ expect(diags).toHaveLength(1);
29
+ expect(diags[0].message).toContain("neither a tag nor a full commit SHA");
30
+ });
31
+
32
+ test("passes a ref pinned to a semver tag", async () => {
33
+ const diags = tf005.check(await loadFixture("TF005", "negative"));
34
+ expect(diags).toHaveLength(0);
35
+ });
36
+
37
+ test("passes a ref pinned to a full 40-hex commit SHA", async () => {
38
+ const diags = tf005.check(await loadFixture("TF005", "negative-sha"));
39
+ expect(diags).toHaveLength(0);
40
+ });
41
+
42
+ test("never fires on a local source", async () => {
43
+ const diags = tf005.check(await loadFixture("TF005", "negative-local"));
44
+ expect(diags).toHaveLength(0);
45
+ });
46
+
47
+ test("never fires on a registry source", async () => {
48
+ const diags = tf005.check(await loadFixture("TF005", "negative-registry"));
49
+ expect(diags).toHaveLength(0);
50
+ });
51
+
52
+ test("is exclusive of TF004 on a registry-sourced module block", async () => {
53
+ const ctx = await loadFixture("TF005", "negative-registry");
54
+ expect(tf005.check(ctx)).toHaveLength(0);
55
+ expect(tf004.check(ctx)).toHaveLength(1); // no version set, so TF004 fires instead
56
+ });
57
+ });