@intentius/chant-lexicon-terraform 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +9 -0
  2. package/dist/composites/terraform-adopt-op.d.ts +131 -0
  3. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  4. package/dist/composites/terraform-apply-op.d.ts +70 -17
  5. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  6. package/dist/composites/terraform-watch-op.d.ts +104 -37
  7. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  8. package/dist/config.d.ts +49 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/describe-resources.d.ts +256 -16
  11. package/dist/describe-resources.d.ts.map +1 -1
  12. package/dist/hcl/descend.d.ts +98 -0
  13. package/dist/hcl/descend.d.ts.map +1 -0
  14. package/dist/hcl/marker.d.ts +130 -0
  15. package/dist/hcl/marker.d.ts.map +1 -0
  16. package/dist/hcl/parse.d.ts +156 -9
  17. package/dist/hcl/parse.d.ts.map +1 -1
  18. package/dist/hcl/references.d.ts +76 -0
  19. package/dist/hcl/references.d.ts.map +1 -0
  20. package/dist/hcl/roots.d.ts +22 -3
  21. package/dist/hcl/roots.d.ts.map +1 -1
  22. package/dist/hcl/suppressions.d.ts +54 -0
  23. package/dist/hcl/suppressions.d.ts.map +1 -0
  24. package/dist/hcl/value.d.ts +77 -0
  25. package/dist/hcl/value.d.ts.map +1 -0
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/integrity.json +34 -5
  29. package/dist/lint/audit-catalog.d.ts +3 -2
  30. package/dist/lint/audit-catalog.d.ts.map +1 -1
  31. package/dist/lint/audit-lineage.d.ts +95 -0
  32. package/dist/lint/audit-lineage.d.ts.map +1 -1
  33. package/dist/lint/post-synth/blocks.d.ts +53 -0
  34. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  36. package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/module-source.d.ts +38 -0
  39. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  40. package/dist/lint/post-synth/scope.d.ts +65 -0
  41. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  42. package/dist/lint/post-synth/tf001.d.ts +58 -6
  43. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  44. package/dist/lint/post-synth/tf002.d.ts +32 -0
  45. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  46. package/dist/lint/post-synth/tf003.d.ts +22 -0
  47. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  48. package/dist/lint/post-synth/tf004.d.ts +22 -0
  49. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  50. package/dist/lint/post-synth/tf005.d.ts +28 -0
  51. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf006.d.ts +21 -0
  53. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  54. package/dist/lint/post-synth/tf007.d.ts +21 -0
  55. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  56. package/dist/lint/post-synth/tf008.d.ts +22 -0
  57. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  58. package/dist/lint/post-synth/tf009.d.ts +24 -0
  59. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  60. package/dist/lint/post-synth/tf010.d.ts +19 -0
  61. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  62. package/dist/lint/post-synth/tf011.d.ts +18 -0
  63. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  64. package/dist/lint/post-synth/tf012.d.ts +17 -0
  65. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  66. package/dist/lint/post-synth/tf013.d.ts +21 -0
  67. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  68. package/dist/lint/post-synth/tf014.d.ts +25 -0
  69. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  70. package/dist/lint/post-synth/tf015.d.ts +30 -0
  71. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  72. package/dist/lint/post-synth/tf016.d.ts +30 -0
  73. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  74. package/dist/lint/post-synth/tf017.d.ts +19 -0
  75. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  76. package/dist/lint/post-synth/tf018.d.ts +20 -0
  77. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  78. package/dist/lint/post-synth/tf019.d.ts +25 -0
  79. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  80. package/dist/lint/post-synth/tf020.d.ts +38 -0
  81. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  82. package/dist/lint/post-synth/tf021.d.ts +41 -0
  83. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  84. package/dist/lint/post-synth/tf022.d.ts +24 -0
  85. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  86. package/dist/lint/post-synth/tf024.d.ts +24 -0
  87. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  88. package/dist/lint/post-synth/tf025.d.ts +29 -0
  89. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  90. package/dist/lint/post-synth/tf026.d.ts +28 -0
  91. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  92. package/dist/lint/post-synth/tf027.d.ts +30 -0
  93. package/dist/lint/post-synth/tf027.d.ts.map +1 -0
  94. package/dist/lint/post-synth/tf028.d.ts +33 -0
  95. package/dist/lint/post-synth/tf028.d.ts.map +1 -0
  96. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  97. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  98. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  99. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  100. package/dist/lint/secret-shape.d.ts +68 -0
  101. package/dist/lint/secret-shape.d.ts.map +1 -0
  102. package/dist/live-ownership.d.ts +55 -0
  103. package/dist/live-ownership.d.ts.map +1 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/manifest.json +1 -1
  106. package/dist/okf/index.md +26 -0
  107. package/dist/okf/rules/TF002.md +11 -0
  108. package/dist/okf/rules/TF003.md +11 -0
  109. package/dist/okf/rules/TF004.md +11 -0
  110. package/dist/okf/rules/TF005.md +11 -0
  111. package/dist/okf/rules/TF006.md +11 -0
  112. package/dist/okf/rules/TF007.md +11 -0
  113. package/dist/okf/rules/TF008.md +11 -0
  114. package/dist/okf/rules/TF009.md +11 -0
  115. package/dist/okf/rules/TF010.md +11 -0
  116. package/dist/okf/rules/TF011.md +11 -0
  117. package/dist/okf/rules/TF012.md +11 -0
  118. package/dist/okf/rules/TF013.md +11 -0
  119. package/dist/okf/rules/TF014.md +11 -0
  120. package/dist/okf/rules/TF015.md +11 -0
  121. package/dist/okf/rules/TF016.md +11 -0
  122. package/dist/okf/rules/TF017.md +11 -0
  123. package/dist/okf/rules/TF018.md +11 -0
  124. package/dist/okf/rules/TF019.md +11 -0
  125. package/dist/okf/rules/TF020.md +11 -0
  126. package/dist/okf/rules/TF021.md +11 -0
  127. package/dist/okf/rules/TF022.md +11 -0
  128. package/dist/okf/rules/TF024.md +11 -0
  129. package/dist/okf/rules/TF025.md +11 -0
  130. package/dist/okf/rules/TF026.md +11 -0
  131. package/dist/okf/rules/TF027.md +11 -0
  132. package/dist/okf/rules/TF028.md +11 -0
  133. package/dist/op/activities/index.d.ts +13 -6
  134. package/dist/op/activities/index.d.ts.map +1 -1
  135. package/dist/op/activities/live-detect.d.ts +51 -0
  136. package/dist/op/activities/live-detect.d.ts.map +1 -0
  137. package/dist/op/activities/terraform.d.ts +466 -17
  138. package/dist/op/activities/terraform.d.ts.map +1 -1
  139. package/dist/op/activity-contracts.d.ts +148 -0
  140. package/dist/op/activity-contracts.d.ts.map +1 -0
  141. package/dist/op/adoption.d.ts +118 -0
  142. package/dist/op/adoption.d.ts.map +1 -0
  143. package/dist/op/builders.d.ts +48 -6
  144. package/dist/op/builders.d.ts.map +1 -1
  145. package/dist/op/resolve-root-mode.d.ts +59 -0
  146. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  147. package/dist/plugin.d.ts.map +1 -1
  148. package/dist/rules/blocks.ts +109 -0
  149. package/dist/rules/module-source.ts +121 -0
  150. package/dist/rules/plan-before-apply.ts +11 -0
  151. package/dist/rules/scope.ts +107 -0
  152. package/dist/rules/tf001.ts +98 -18
  153. package/dist/rules/tf002.ts +151 -0
  154. package/dist/rules/tf003.ts +71 -0
  155. package/dist/rules/tf004.ts +63 -0
  156. package/dist/rules/tf005.ts +76 -0
  157. package/dist/rules/tf006.ts +53 -0
  158. package/dist/rules/tf007.ts +78 -0
  159. package/dist/rules/tf008.ts +75 -0
  160. package/dist/rules/tf009.ts +59 -0
  161. package/dist/rules/tf010.ts +51 -0
  162. package/dist/rules/tf011.ts +49 -0
  163. package/dist/rules/tf012.ts +47 -0
  164. package/dist/rules/tf013.ts +63 -0
  165. package/dist/rules/tf014.ts +72 -0
  166. package/dist/rules/tf015.ts +72 -0
  167. package/dist/rules/tf016.ts +75 -0
  168. package/dist/rules/tf017.ts +50 -0
  169. package/dist/rules/tf018.ts +72 -0
  170. package/dist/rules/tf019.ts +72 -0
  171. package/dist/rules/tf020.ts +123 -0
  172. package/dist/rules/tf021.ts +126 -0
  173. package/dist/rules/tf022.ts +61 -0
  174. package/dist/rules/tf024.ts +70 -0
  175. package/dist/rules/tf025.ts +101 -0
  176. package/dist/rules/tf026.ts +84 -0
  177. package/dist/rules/tf027.ts +74 -0
  178. package/dist/rules/tf028.ts +96 -0
  179. package/dist/skills/chant-terraform.md +1 -1
  180. package/dist/validate.d.ts.map +1 -1
  181. package/package.json +7 -2
  182. package/src/__fixtures__/ACCEPTANCE.md +181 -0
  183. package/src/__fixtures__/live/main.tf +28 -0
  184. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  185. package/src/__fixtures__/live-adopt/main.tf +21 -0
  186. package/src/__fixtures__/live-estate/README.md +58 -0
  187. package/src/__fixtures__/live-estate/compute.tf +10 -0
  188. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  189. package/src/__fixtures__/live-estate/logs.tf +8 -0
  190. package/src/__fixtures__/live-estate/network.tf +24 -0
  191. package/src/__fixtures__/live-estate/rule.tf +11 -0
  192. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  193. package/src/__fixtures__/live-estate/versions.tf +31 -0
  194. package/src/__fixtures__/live-ls.json +98 -0
  195. package/src/__fixtures__/live-plan.json +285 -0
  196. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  197. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  198. package/src/__fixtures__/module-tree/README.md +16 -0
  199. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  200. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  201. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  202. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  203. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  204. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  205. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  206. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  207. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  208. package/src/__fixtures__/references/main.tf +56 -0
  209. package/src/__fixtures__/remote-backend/main.tf +34 -0
  210. package/src/__fixtures__/with-backend/main.tf +7 -0
  211. package/src/codegen/lint-rules-page.test.ts +45 -0
  212. package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
  213. package/src/composites/terraform-adopt-op.test.ts +175 -0
  214. package/src/composites/terraform-adopt-op.ts +202 -0
  215. package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
  216. package/src/composites/terraform-apply-op.test.ts +179 -19
  217. package/src/composites/terraform-apply-op.ts +123 -30
  218. package/src/composites/terraform-watch-op.test.ts +169 -29
  219. package/src/composites/terraform-watch-op.ts +183 -75
  220. package/src/config.ts +32 -3
  221. package/src/describe-resources.live.test.ts +598 -0
  222. package/src/describe-resources.test.ts +53 -0
  223. package/src/describe-resources.ts +811 -28
  224. package/src/hcl/descend.test.ts +162 -0
  225. package/src/hcl/descend.ts +273 -0
  226. package/src/hcl/marker.test.ts +147 -0
  227. package/src/hcl/marker.ts +235 -0
  228. package/src/hcl/parse.test.ts +69 -0
  229. package/src/hcl/parse.ts +269 -27
  230. package/src/hcl/references.test.ts +116 -0
  231. package/src/hcl/references.ts +172 -0
  232. package/src/hcl/roots.ts +59 -5
  233. package/src/hcl/suppressions.test.ts +147 -0
  234. package/src/hcl/suppressions.ts +204 -0
  235. package/src/hcl/value.test.ts +73 -0
  236. package/src/hcl/value.ts +106 -0
  237. package/src/index.ts +31 -1
  238. package/src/lint/audit-catalog.ts +288 -3
  239. package/src/lint/audit-lineage.ts +384 -1
  240. package/src/lint/audit.test.ts +210 -3
  241. package/src/lint/post-synth/blocks.ts +109 -0
  242. package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
  243. package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
  244. package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
  245. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  246. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  247. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  248. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  249. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  250. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  251. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  252. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  253. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  254. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  255. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  256. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  257. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  258. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  259. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  260. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  261. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  262. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  263. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  264. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  265. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  266. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  267. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  268. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  269. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  270. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  271. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  272. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  273. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  274. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  275. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  276. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  277. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  278. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  279. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  280. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  281. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  282. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  283. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  284. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  285. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  286. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  287. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  288. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  289. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  290. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  291. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  292. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  293. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  294. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  295. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  296. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  297. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  298. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  299. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  300. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  301. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  302. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  303. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  304. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  305. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  306. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  307. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  309. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  310. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  311. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  312. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  313. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  314. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  315. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  316. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  317. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  318. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  319. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  320. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  321. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  322. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  323. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  324. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  325. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  326. package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
  327. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
  328. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
  329. package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
  330. package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
  331. package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
  332. package/src/lint/post-synth/fixtures/load.ts +108 -0
  333. package/src/lint/post-synth/index.ts +52 -0
  334. package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
  335. package/src/lint/post-synth/module-rules.test.ts +153 -0
  336. package/src/lint/post-synth/module-source.test.ts +75 -0
  337. package/src/lint/post-synth/module-source.ts +121 -0
  338. package/src/lint/post-synth/post-synth.test.ts +73 -16
  339. package/src/lint/post-synth/scope.ts +107 -0
  340. package/src/lint/post-synth/single-block.test.ts +263 -0
  341. package/src/lint/post-synth/suppression.test.ts +110 -0
  342. package/src/lint/post-synth/tf001.ts +98 -18
  343. package/src/lint/post-synth/tf002.test.ts +93 -0
  344. package/src/lint/post-synth/tf002.ts +151 -0
  345. package/src/lint/post-synth/tf003.test.ts +75 -0
  346. package/src/lint/post-synth/tf003.ts +71 -0
  347. package/src/lint/post-synth/tf004.test.ts +47 -0
  348. package/src/lint/post-synth/tf004.ts +63 -0
  349. package/src/lint/post-synth/tf005.test.ts +57 -0
  350. package/src/lint/post-synth/tf005.ts +76 -0
  351. package/src/lint/post-synth/tf006.ts +53 -0
  352. package/src/lint/post-synth/tf007.ts +78 -0
  353. package/src/lint/post-synth/tf008.ts +75 -0
  354. package/src/lint/post-synth/tf009.ts +59 -0
  355. package/src/lint/post-synth/tf010.ts +51 -0
  356. package/src/lint/post-synth/tf011.ts +49 -0
  357. package/src/lint/post-synth/tf012.ts +47 -0
  358. package/src/lint/post-synth/tf013.ts +63 -0
  359. package/src/lint/post-synth/tf014.ts +72 -0
  360. package/src/lint/post-synth/tf015.ts +72 -0
  361. package/src/lint/post-synth/tf016.ts +75 -0
  362. package/src/lint/post-synth/tf017.ts +50 -0
  363. package/src/lint/post-synth/tf018.ts +72 -0
  364. package/src/lint/post-synth/tf019.ts +72 -0
  365. package/src/lint/post-synth/tf020.ts +123 -0
  366. package/src/lint/post-synth/tf021.ts +126 -0
  367. package/src/lint/post-synth/tf022.ts +61 -0
  368. package/src/lint/post-synth/tf024.test.ts +42 -0
  369. package/src/lint/post-synth/tf024.ts +70 -0
  370. package/src/lint/post-synth/tf025.test.ts +46 -0
  371. package/src/lint/post-synth/tf025.ts +101 -0
  372. package/src/lint/post-synth/tf026.test.ts +56 -0
  373. package/src/lint/post-synth/tf026.ts +84 -0
  374. package/src/lint/post-synth/tf027.test.ts +62 -0
  375. package/src/lint/post-synth/tf027.ts +74 -0
  376. package/src/lint/post-synth/tf028.test.ts +92 -0
  377. package/src/lint/post-synth/tf028.ts +96 -0
  378. package/src/lint/presets.test.ts +72 -0
  379. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  380. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  381. package/src/lint/rules/fixtures/load.ts +53 -0
  382. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  383. package/src/lint/rules/plan-before-apply.ts +11 -0
  384. package/src/lint/secret-shape.test.ts +150 -0
  385. package/src/lint/secret-shape.ts +159 -0
  386. package/src/live-ownership.ts +60 -0
  387. package/src/lsp/completions.test.ts +3 -2
  388. package/src/lsp/option-keys.ts +12 -6
  389. package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
  390. package/src/op/activities/choudoufu.test.ts +725 -0
  391. package/src/op/activities/index.ts +40 -4
  392. package/src/op/activities/live-detect.test.ts +143 -0
  393. package/src/op/activities/live-detect.ts +162 -0
  394. package/src/op/activities/registry.test.ts +11 -3
  395. package/src/op/activities/terraform.test.ts +7 -3
  396. package/src/op/activities/terraform.ts +861 -69
  397. package/src/op/activity-contracts.test.ts +147 -0
  398. package/src/op/activity-contracts.ts +178 -0
  399. package/src/op/adoption.test.ts +170 -0
  400. package/src/op/adoption.ts +241 -0
  401. package/src/op/builders.test.ts +60 -5
  402. package/src/op/builders.ts +95 -5
  403. package/src/op/resolve-root-mode.test.ts +85 -0
  404. package/src/op/resolve-root-mode.ts +86 -0
  405. package/src/plugin.test.ts +106 -0
  406. package/src/plugin.ts +186 -29
  407. package/src/skills/chant-terraform.md +1 -1
  408. package/src/validate.ts +10 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared post-synth fixture loader.
3
+ *
4
+ * KICS lays its own query fixtures out as `positive.tf`/`negative.tf` pairs
5
+ * beside the query that tests them (survey, #2107): every query is
6
+ * externally auditable because a rule and its evidence live in the same
7
+ * directory. This lexicon adopts that layout: `fixtures/<id>/positive.tf`
8
+ * triggers the check, `fixtures/<id>/negative.tf` does not, and a rule with
9
+ * more than one triggering condition adds `positive-<case>.tf` beside them.
10
+ *
11
+ * KICS itself scans a whole fixture directory in one pass and tells its
12
+ * fixtures apart by the file each finding came from. That doesn't transfer
13
+ * here: TF001 (and most post-synth checks with per-root state, not
14
+ * per-resource) fire once per *root*, deduplicated by root name, so parsing
15
+ * `positive.tf` and `negative.tf` together as one root would let whichever
16
+ * file's terraform block is read first decide the outcome for both, silently
17
+ * discarding the other. `loadFixture` therefore reads exactly the one named
18
+ * file and parses it alone, through the same `blocksToEntities` pass
19
+ * `parseTerraformRootDir` wraps for a real root directory, so sibling
20
+ * fixtures never mix into one build.
21
+ */
22
+ import { readFileSync } from "node:fs";
23
+ import { dirname, join } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
26
+ import { blocksToEntities } from "../../../hcl/parse";
27
+ import type { CallModuleType } from "../../../hcl/descend";
28
+ import { renderTerraformRoots } from "../../../hcl/roots";
29
+
30
+ const FIXTURES_DIR = dirname(fileURLToPath(import.meta.url));
31
+
32
+ /**
33
+ * Parse `fixtures/<checkId>/<name>.tf` into a {@link PostSynthContext} whose
34
+ * `entities` are exactly what a real root module of that one file would
35
+ * produce. `root` (the entity key prefix and the `TerraformEntity.props.root`
36
+ * value) defaults to `checkId`; pass a distinct value to load two fixtures
37
+ * into one context under different root names.
38
+ *
39
+ * Three lines per fixture in a test:
40
+ * ```ts
41
+ * const diags = tf001.check(await loadFixture("TF001", "positive"));
42
+ * expect(diags).toHaveLength(1);
43
+ * ```
44
+ */
45
+ export async function loadFixture(
46
+ checkId: string,
47
+ name: string,
48
+ root: string = checkId,
49
+ ): Promise<PostSynthContext> {
50
+ const file = `${name}.tf`;
51
+ const source = readFileSync(join(FIXTURES_DIR, checkId, file), "utf-8");
52
+ const entities = await blocksToEntities([{ name: file, source }], root);
53
+ return {
54
+ outputs: new Map(),
55
+ entities,
56
+ buildResult: {
57
+ outputs: new Map(),
58
+ entities,
59
+ warnings: [],
60
+ errors: [],
61
+ sourceFileCount: 1,
62
+ },
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Parse a whole fixture DIRECTORY, `fixtures/<checkId>/<name>/`, the way
68
+ * `buildRoots()` parses a configured root: its own `.tf` files, then every
69
+ * local `module` call followed into its directory (chant #2112). A rule about
70
+ * child modules (TF014, TF015) or about a whole scope's references (TF020)
71
+ * cannot be shown a single file and still be tested honestly, so its fixture
72
+ * is a tree:
73
+ *
74
+ * ```
75
+ * fixtures/TF014/positive/main.tf # module "cdn" { source = "./modules/cdn" }
76
+ * fixtures/TF014/positive/modules/cdn/main.tf # the provider block the rule reports
77
+ * ```
78
+ *
79
+ * The fixture directory is also the project root for the descent, so a
80
+ * fixture can exercise the outside-the-project refusal with a `../` source
81
+ * without reaching into the repository around it. `warnings` from the render
82
+ * lands on `buildResult.warnings`, where the descent's own refusals are
83
+ * readable by a test.
84
+ */
85
+ export async function loadTreeFixture(
86
+ checkId: string,
87
+ name: string,
88
+ root: string = checkId,
89
+ callModuleType?: CallModuleType,
90
+ ): Promise<PostSynthContext> {
91
+ const dir = join(FIXTURES_DIR, checkId, name);
92
+ const { entities, warnings } = await renderTerraformRoots({
93
+ projectRoot: dir,
94
+ roots: { [root]: { dir } },
95
+ ...(callModuleType ? { callModuleType } : {}),
96
+ });
97
+ return {
98
+ outputs: new Map(),
99
+ entities,
100
+ buildResult: {
101
+ outputs: new Map(),
102
+ entities,
103
+ warnings,
104
+ errors: [],
105
+ sourceFileCount: entities.size,
106
+ },
107
+ };
108
+ }
@@ -1,7 +1,59 @@
1
1
  // Code generated by chant generate. DO NOT EDIT.
2
2
  import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
3
3
  import { tf001 } from "./tf001";
4
+ import { tf002 } from "./tf002";
5
+ import { tf003 } from "./tf003";
6
+ import { tf004 } from "./tf004";
7
+ import { tf005 } from "./tf005";
8
+ import { tf006 } from "./tf006";
9
+ import { tf007 } from "./tf007";
10
+ import { tf008 } from "./tf008";
11
+ import { tf009 } from "./tf009";
12
+ import { tf010 } from "./tf010";
13
+ import { tf011 } from "./tf011";
14
+ import { tf012 } from "./tf012";
15
+ import { tf013 } from "./tf013";
16
+ import { tf014 } from "./tf014";
17
+ import { tf015 } from "./tf015";
18
+ import { tf016 } from "./tf016";
19
+ import { tf017 } from "./tf017";
20
+ import { tf018 } from "./tf018";
21
+ import { tf019 } from "./tf019";
22
+ import { tf020 } from "./tf020";
23
+ import { tf021 } from "./tf021";
24
+ import { tf022 } from "./tf022";
25
+ import { tf024 } from "./tf024";
26
+ import { tf025 } from "./tf025";
27
+ import { tf026 } from "./tf026";
28
+ import { tf027 } from "./tf027";
29
+ import { tf028 } from "./tf028";
4
30
 
5
31
  export const postSynthChecks: PostSynthCheck[] = [
6
32
  tf001,
33
+ tf002,
34
+ tf003,
35
+ tf004,
36
+ tf005,
37
+ tf006,
38
+ tf007,
39
+ tf008,
40
+ tf009,
41
+ tf010,
42
+ tf011,
43
+ tf012,
44
+ tf013,
45
+ tf014,
46
+ tf015,
47
+ tf016,
48
+ tf017,
49
+ tf018,
50
+ tf019,
51
+ tf020,
52
+ tf021,
53
+ tf022,
54
+ tf024,
55
+ tf025,
56
+ tf026,
57
+ tf027,
58
+ tf028,
7
59
  ];
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The two live-root refusals against both config file formats (#2216).
3
+ *
4
+ * `TerraformApplyOp` and `TerraformWatchOp` each resolve a root's mode
5
+ * synchronously when they are built, and `resolveRootModeSync` reads a
6
+ * `chant.config.json` and nothing else: a `chant.config.ts` is project-
7
+ * authored code it will not evaluate. Every project in this repository is on
8
+ * a `chant.config.ts`, so a refusal that lives only in a composite has never
9
+ * fired for anyone. TF027 and TF028 read the mode the build stamps onto the
10
+ * parsed HCL instead, which is the same either way, and these tests
11
+ * materialise one project in each format to prove it.
12
+ *
13
+ * The build path here is the plugin's own `buildRoots` hook (what `chant
14
+ * build` calls to turn `terraform.roots` into entities) followed by the
15
+ * checks the plugin contributes, so the entities under test are the ones a
16
+ * real build produces rather than a hand-assembled map.
17
+ */
18
+
19
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { afterEach, describe, expect, test } from "vitest";
23
+ import type { Declarable } from "@intentius/chant/declarable";
24
+ import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
25
+ import { terraformPlugin } from "../../plugin";
26
+ import { TerraformApplyOp } from "../../composites/terraform-apply-op";
27
+ import { TerraformWatchOp } from "../../composites/terraform-watch-op";
28
+
29
+ const UNTAGGED_DELETE_ROOT = `terraform {
30
+ required_version = ">= 1.5.0"
31
+
32
+ live {
33
+ estate = "prod-estate"
34
+
35
+ policy {
36
+ undeclared_untagged = "delete"
37
+ }
38
+ }
39
+ }
40
+
41
+ resource "null_resource" "first" {}
42
+ `;
43
+
44
+ const LIVE_ROOT = `terraform {
45
+ required_version = ">= 1.5.0"
46
+
47
+ live {
48
+ estate = "prod-estate"
49
+ }
50
+ }
51
+
52
+ resource "null_resource" "first" {}
53
+ `;
54
+
55
+ const CONFIG = {
56
+ lexicons: ["terraform"],
57
+ terraform: { binary: "choudoufu", roots: { estate: { dir: "./estate" } } },
58
+ };
59
+
60
+ const dirs: string[] = [];
61
+
62
+ afterEach(() => {
63
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
64
+ });
65
+
66
+ /** A project whose one root is `mainTf`, its config written in `format`. */
67
+ function project(format: "ts" | "json", mainTf: string): string {
68
+ const dir = mkdtempSync(join(tmpdir(), `chant-tf-2216-${format}-`));
69
+ dirs.push(dir);
70
+ mkdirSync(join(dir, "estate"), { recursive: true });
71
+ writeFileSync(join(dir, "estate", "main.tf"), mainTf);
72
+ writeFileSync(
73
+ join(dir, `chant.config.${format}`),
74
+ format === "json"
75
+ ? JSON.stringify(CONFIG, null, 2)
76
+ : `import type { ChantConfig } from "@intentius/chant/config";\n\nexport default ${JSON.stringify(CONFIG, null, 2)} satisfies ChantConfig;\n`,
77
+ );
78
+ return dir;
79
+ }
80
+
81
+ /** `chant build`'s own path: the plugin's `buildRoots`, then the checks it contributes. */
82
+ async function checkProject(dir: string, ops: Map<string, Declarable> = new Map()): Promise<PostSynthDiagnostic[]> {
83
+ const { entities } = await terraformPlugin.buildRoots!({ projectRoot: dir, config: CONFIG, entities: new Map() });
84
+ for (const [name, op] of ops) entities.set(name, op);
85
+ const ctx = { outputs: new Map(), entities } as unknown as PostSynthContext;
86
+ return (terraformPlugin.postSynthChecks?.() ?? []).flatMap((check) => check.check(ctx));
87
+ }
88
+
89
+ describe('TF027 fires on undeclared_untagged = "delete" whatever the config format (#2216)', () => {
90
+ for (const format of ["ts", "json"] as const) {
91
+ test(`chant.config.${format}: the build reports the root and the setting`, async () => {
92
+ const diags = await checkProject(project(format, UNTAGGED_DELETE_ROOT));
93
+ const tf027 = diags.filter((d) => d.checkId === "TF027");
94
+ expect(tf027).toHaveLength(1);
95
+ expect(tf027[0].severity).toBe("error");
96
+ expect(tf027[0].message).toContain('Root module "estate"');
97
+ expect(tf027[0].message).toContain('undeclared_untagged = "delete"');
98
+ });
99
+ }
100
+
101
+ test("the same project with no such policy setting is clean", async () => {
102
+ const diags = await checkProject(project("ts", LIVE_ROOT));
103
+ expect(diags.filter((d) => d.checkId === "TF027")).toEqual([]);
104
+ });
105
+
106
+ test("the composite's own refusal fires only on the format it can read, which is why the check exists", () => {
107
+ const json = project("json", UNTAGGED_DELETE_ROOT);
108
+ expect(() => TerraformApplyOp({ name: "estate-apply", root: "estate", cwd: json })).toThrow(
109
+ /undeclared_untagged = "delete"/,
110
+ );
111
+
112
+ const ts = project("ts", UNTAGGED_DELETE_ROOT);
113
+ expect(() => TerraformApplyOp({ name: "estate-apply", root: "estate", cwd: ts })).not.toThrow();
114
+ });
115
+ });
116
+
117
+ describe("TF028 reports a stock-mode watch Op on a live root whatever the config format (#2216)", () => {
118
+ test("chant.config.ts: the composite builds, and the build reports the Op", async () => {
119
+ const dir = project("ts", LIVE_ROOT);
120
+ const { op } = TerraformWatchOp({ name: "estate-watch", root: "estate", cwd: dir });
121
+ const diags = await checkProject(dir, new Map([["estate-watch", op as unknown as Declarable]]));
122
+ const tf028 = diags.filter((d) => d.checkId === "TF028");
123
+ expect(tf028).toHaveLength(1);
124
+ expect(tf028[0].message).toContain('Op "estate-watch"');
125
+ expect(tf028[0].message).toContain('root module "estate"');
126
+ });
127
+
128
+ test("chant.config.json: the composite refuses before there is an Op to check", () => {
129
+ const dir = project("json", LIVE_ROOT);
130
+ expect(() => TerraformWatchOp({ name: "estate-watch", root: "estate", cwd: dir })).toThrow(
131
+ /runs choudoufu with a declared estate/,
132
+ );
133
+ });
134
+
135
+ test("live: true on the same root is clean in both formats", async () => {
136
+ for (const format of ["ts", "json"] as const) {
137
+ const dir = project(format, LIVE_ROOT);
138
+ const { op } = TerraformWatchOp({ name: "estate-watch", root: "estate", live: true, cwd: dir });
139
+ const diags = await checkProject(dir, new Map([["estate-watch", op as unknown as Declarable]]));
140
+ expect(diags.filter((d) => d.checkId === "TF028")).toEqual([]);
141
+ }
142
+ });
143
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The four rules module descent made possible (chant #2112): TF014 and TF015,
3
+ * which are about what a child module may contain, and TF020 and TF021, which
4
+ * need a whole scope's references or a resource's whole body.
5
+ *
6
+ * TF014, TF015 and the scoped half of TF020 read tree fixtures
7
+ * (`fixtures/<id>/<case>/`, parsed through `renderTerraformRoots` by
8
+ * `loadTreeFixture`), because a rule about a child module cannot be shown one
9
+ * file and still be tested honestly. TF020's own conditions and TF021 read
10
+ * single-file fixtures the way every rule before them does.
11
+ */
12
+
13
+ import { describe, expect, it } from "vitest";
14
+ import { loadFixture, loadTreeFixture } from "./fixtures/load";
15
+ import { tf001 } from "./tf001";
16
+ import { tf014 } from "./tf014";
17
+ import { tf015 } from "./tf015";
18
+ import { tf020 } from "./tf020";
19
+ import { tf021 } from "./tf021";
20
+ import { withCallersChain } from "./scope";
21
+
22
+ describe("TF014: a child module configures a provider", () => {
23
+ it("reports a provider block that sets anything beyond alias", async () => {
24
+ const diags = tf014.check(await loadTreeFixture("TF014", "positive", "app"));
25
+ expect(diags).toHaveLength(1);
26
+ expect(diags[0].entity).toBe("app/module.cdn/provider.aws");
27
+ expect(diags[0].message).toContain("profile, region");
28
+ expect(diags[0].message).toContain("providers = { ... }");
29
+ });
30
+
31
+ it("leaves an alias-only block, and the root's own provider, alone", async () => {
32
+ const diags = tf014.check(await loadTreeFixture("TF014", "negative", "app"));
33
+ expect(diags).toEqual([]);
34
+ });
35
+
36
+ it("never fires on a root module's provider block", async () => {
37
+ // The positive fixture's ROOT configures a region too; only the child's
38
+ // block is reported.
39
+ const ctx = await loadTreeFixture("TF014", "positive", "app");
40
+ expect(ctx.entities.has("app/provider.aws")).toBe(true);
41
+ expect(tf014.check(ctx).map((d) => d.entity)).toEqual(["app/module.cdn/provider.aws"]);
42
+ });
43
+ });
44
+
45
+ describe("TF015: a child module declares a backend or cloud block", () => {
46
+ it("reports the backend block in the child", async () => {
47
+ const diags = tf015.check(await loadTreeFixture("TF015", "positive", "app"));
48
+ expect(diags).toHaveLength(1);
49
+ expect(diags[0].entity).toBe("app/module.cdn/terraform");
50
+ expect(diags[0].message).toContain("declares a backend block");
51
+ expect(diags[0].message).toContain("Only a root module may");
52
+ });
53
+
54
+ it("leaves a child's required_version and required_providers alone", async () => {
55
+ expect(tf015.check(await loadTreeFixture("TF015", "negative", "app"))).toEqual([]);
56
+ });
57
+
58
+ it("is TF001's mirror: the root keeps its backend and neither rule fires twice", async () => {
59
+ const ctx = await loadTreeFixture("TF015", "positive", "app");
60
+ expect(tf001.check(ctx)).toEqual([]);
61
+ expect(tf015.check(ctx)).toHaveLength(1);
62
+ });
63
+ });
64
+
65
+ describe("TF001 still fires only on roots (#2112)", () => {
66
+ it("does not report a child module that has no backend", async () => {
67
+ // The root of the TF014 fixture declares one; the child declares none.
68
+ const ctx = await loadTreeFixture("TF014", "positive", "app");
69
+ expect(ctx.entities.has("app/module.cdn/aws_s3_bucket.assets")).toBe(true);
70
+ expect(tf001.check(ctx)).toEqual([]);
71
+ });
72
+
73
+ it("still reports a root that has none, descent or no descent", async () => {
74
+ const ctx = await loadTreeFixture("TF020", "scoped", "app");
75
+ expect(tf001.check(ctx)).toEqual([]); // this root has a remote backend block
76
+
77
+ const bare = await loadFixture("TF001", "positive", "app");
78
+ expect(tf001.check(bare)).toHaveLength(1);
79
+ });
80
+ });
81
+
82
+ describe("TF020: a declaration nothing in its scope references", () => {
83
+ it("reports an unused variable, local, data source and aliased provider", async () => {
84
+ const diags = tf020.check(await loadFixture("TF020", "positive"));
85
+ expect(diags.map((d) => d.entity).sort()).toEqual([
86
+ "TF020/data.aws_ami.ubuntu",
87
+ "TF020/locals",
88
+ "TF020/provider.aws",
89
+ "TF020/var.retention_days",
90
+ ]);
91
+ expect(diags.find((d) => d.entity === "TF020/locals")?.message).toContain('"local.common_tags"');
92
+ expect(diags.find((d) => d.entity === "TF020/data.aws_ami.ubuntu")?.message).toContain("every plan");
93
+ });
94
+
95
+ it("does not count a variable read only by its own validation block as used", async () => {
96
+ const diags = tf020.check(await loadFixture("TF020", "positive"));
97
+ expect(diags.some((d) => d.message.includes('"var.retention_days"'))).toBe(true);
98
+ });
99
+
100
+ it("reports nothing when every declaration is referenced", async () => {
101
+ expect(tf020.check(await loadFixture("TF020", "negative"))).toEqual([]);
102
+ });
103
+
104
+ it("judges each module scope on its own", async () => {
105
+ const diags = tf020.check(await loadTreeFixture("TF020", "scoped", "app"));
106
+ // The root's `var.region` is referenced by the module call; the child's
107
+ // `var.unused_in_child` is referenced by nothing in the child.
108
+ expect(diags.map((d) => d.entity)).toEqual(["app/module.cdn/var.unused_in_child"]);
109
+ });
110
+ });
111
+
112
+ describe("TF021: count where for_each is safer", () => {
113
+ it("reports a numeric count building an identity from count.index", async () => {
114
+ const diags = tf021.check(await loadFixture("TF021", "positive"));
115
+ expect(diags).toHaveLength(1);
116
+ expect(diags[0].message).toContain("`count = 3`");
117
+ expect(diags[0].message).toContain("tags.Name");
118
+ expect(diags[0].message).toContain("for_each");
119
+ });
120
+
121
+ it("reports a length() count the same way", async () => {
122
+ const diags = tf021.check(await loadFixture("TF021", "positive-length"));
123
+ expect(diags).toHaveLength(1);
124
+ expect(diags[0].message).toContain("length(var.subnets)");
125
+ expect(diags[0].message).toContain("bucket");
126
+ });
127
+
128
+ it("leaves a conditional count, a positional-only count.index and for_each alone", async () => {
129
+ expect(tf021.check(await loadFixture("TF021", "negative"))).toEqual([]);
130
+ });
131
+ });
132
+
133
+ describe("the Callers chain on a finding inside a child module", () => {
134
+ it("names the root and every call site, with the file and line of each", async () => {
135
+ const ctx = await loadTreeFixture("TF014", "positive", "app");
136
+ const [wrapped] = withCallersChain([tf014]);
137
+ const [diag] = wrapped.check(ctx);
138
+ expect(diag.message).toContain("Callers: app -> module.cdn (main.tf:13).");
139
+ });
140
+
141
+ it("chains one hop per level", async () => {
142
+ const ctx = await loadTreeFixture("TF015", "positive", "app");
143
+ const [wrapped] = withCallersChain([tf015]);
144
+ const [diag] = wrapped.check(ctx);
145
+ expect(diag.message).toContain("Callers: app -> module.cdn (main.tf:11).");
146
+ });
147
+
148
+ it("says nothing on a finding in the root module", async () => {
149
+ const [wrapped] = withCallersChain([tf001]);
150
+ const [diag] = wrapped.check(await loadFixture("TF001", "positive", "app"));
151
+ expect(diag.message).not.toContain("Callers:");
152
+ });
153
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { classifyModuleSource, isDefaultBranch, isPinnedRef } from "./module-source";
3
+
4
+ describe("classifyModuleSource", () => {
5
+ test("local paths", () => {
6
+ expect(classifyModuleSource("./modules/local").kind).toBe("local");
7
+ expect(classifyModuleSource("../modules/local").kind).toBe("local");
8
+ });
9
+
10
+ test("registry addresses, three and four parts", () => {
11
+ expect(classifyModuleSource("terraform-aws-modules/vpc/aws").kind).toBe("registry");
12
+ expect(classifyModuleSource("app.terraform.io/example-corp/k8s-cluster/azurerm").kind).toBe("registry");
13
+ });
14
+
15
+ test("a github.com or bitbucket.org host is never a registry address, even with four parts", () => {
16
+ expect(classifyModuleSource("github.com/hashicorp/example/extra").kind).not.toBe("registry");
17
+ expect(classifyModuleSource("bitbucket.org/hashicorp/example/extra").kind).not.toBe("registry");
18
+ });
19
+
20
+ test("git sources: explicit force prefix, github/bitbucket shorthand, scp-style, .git suffix", () => {
21
+ expect(classifyModuleSource("git::https://example.com/vpc.git").kind).toBe("git");
22
+ expect(classifyModuleSource("hg::http://example.com/vpc").kind).toBe("git");
23
+ expect(classifyModuleSource("github.com/hashicorp/example").kind).toBe("git");
24
+ expect(classifyModuleSource("bitbucket.org/foo/bar").kind).toBe("git");
25
+ expect(classifyModuleSource("git@github.com:org/repo.git").kind).toBe("git");
26
+ expect(classifyModuleSource("https://example.com/vpc.git").kind).toBe("git");
27
+ expect(classifyModuleSource("https://example.com/vpc.git?ref=main").kind).toBe("git");
28
+ });
29
+
30
+ test("extracts ref, falling back to rev", () => {
31
+ expect(classifyModuleSource("git::https://example.com/vpc.git?ref=v1.2.0").ref).toBe("v1.2.0");
32
+ expect(classifyModuleSource("git::https://example.com/vpc.git?rev=v1.2.0").ref).toBe("v1.2.0");
33
+ expect(classifyModuleSource("git::https://example.com/vpc.git?ref=v1.2.0&rev=other").ref).toBe("v1.2.0");
34
+ expect(classifyModuleSource("git::https://example.com/vpc.git").ref).toBeUndefined();
35
+ });
36
+
37
+ test("a source that parses as neither local, registry nor git is other", () => {
38
+ expect(classifyModuleSource("s3::https://bucket.s3.amazonaws.com/vpc.zip").kind).toBe("other");
39
+ expect(classifyModuleSource("not a valid source at all").kind).toBe("other");
40
+ });
41
+ });
42
+
43
+ describe("isPinnedRef", () => {
44
+ test("accepts semver-shaped tags, with or without a leading v", () => {
45
+ expect(isPinnedRef("v1.2.0")).toBe(true);
46
+ expect(isPinnedRef("1.2.0")).toBe(true);
47
+ expect(isPinnedRef("v1.2.0-rc.1")).toBe(true);
48
+ });
49
+
50
+ test("accepts a full 40-hex commit SHA", () => {
51
+ expect(isPinnedRef("abcdef1234567890abcdef1234567890abcdef12")).toBe(true);
52
+ });
53
+
54
+ test("rejects a short SHA", () => {
55
+ expect(isPinnedRef("abcdef1")).toBe(false);
56
+ });
57
+
58
+ test("rejects checkov's CKV_TF_2-passing but non-semver v1.2-dev (chant is stricter)", () => {
59
+ expect(isPinnedRef("v1.2-dev")).toBe(false);
60
+ });
61
+
62
+ test("rejects an arbitrary branch-shaped name", () => {
63
+ expect(isPinnedRef("some-feature")).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe("isDefaultBranch", () => {
68
+ test("recognizes the four known mutable branch names", () => {
69
+ for (const b of ["main", "master", "develop", "trunk"]) expect(isDefaultBranch(b)).toBe(true);
70
+ });
71
+
72
+ test("does not flag an arbitrary branch name as a default branch", () => {
73
+ expect(isDefaultBranch("some-feature")).toBe(false);
74
+ });
75
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Classify a `module` block's `source` string, shared by TF004 (registry
3
+ * module without a version) and TF005 (git/hg module unpinned or pinned to
4
+ * a branch). Neither rule evaluates HCL: both read `source` as the literal
5
+ * string hcl2json hands back, so a source built from a variable or a
6
+ * function call is never classified (`"other"`), matching tflint's own
7
+ * `SourceKnown` guard on `module.Source`.
8
+ *
9
+ * `registry` mirrors `hashicorp/terraform-registry-address`'s
10
+ * `ParseModuleSource` (MPL-2.0,
11
+ * https://github.com/hashicorp/terraform-registry-address/blob/main/module.go):
12
+ * three slash-separated parts (`namespace/name/target-system`), or four with
13
+ * a leading hostname that contains a dot and is not `github.com` or
14
+ * `bitbucket.org` (those are reserved for direct VCS installs, never a
15
+ * registry). `git` covers the sources tflint's `terraform_module_pinned_source`
16
+ * treats as git/hg: an explicit `git::`/`hg::` force prefix, the `github.com`
17
+ * and `bitbucket.org` shorthands its `GitHubDetector`/`BitBucketDetector`
18
+ * recognize, scp-style `git@host:path` addresses, and any source ending in
19
+ * `.git`. `local` is `./` or `../` (checkov's `UNKNOWN` case for module
20
+ * pinning: a local path cannot be pinned to anything and is never flagged).
21
+ */
22
+
23
+ export type ModuleSourceKind = "local" | "registry" | "git" | "other";
24
+
25
+ export interface ModuleSourceClassification {
26
+ kind: ModuleSourceKind;
27
+ /** The `?ref=`/`&ref=` value (falling back to `rev`), when `kind` is `"git"`. */
28
+ ref?: string;
29
+ }
30
+
31
+ const REGISTRY_NAME = /^[0-9A-Za-z](?:[0-9A-Za-z_-]{0,62}[0-9A-Za-z])?$/;
32
+ const REGISTRY_TARGET_SYSTEM = /^[0-9a-z]{1,64}$/;
33
+ const RESERVED_VCS_HOSTS = new Set(["github.com", "bitbucket.org"]);
34
+
35
+ /** Split a raw query string (no leading `?`) into its key/value pairs, last write wins. */
36
+ function parseQuery(query: string): Map<string, string> {
37
+ const params = new Map<string, string>();
38
+ for (const pair of query.split("&")) {
39
+ if (pair === "") continue;
40
+ const eq = pair.indexOf("=");
41
+ const key = eq === -1 ? pair : pair.slice(0, eq);
42
+ const value = eq === -1 ? "" : decodeURIComponent(pair.slice(eq + 1));
43
+ params.set(decodeURIComponent(key), value);
44
+ }
45
+ return params;
46
+ }
47
+
48
+ /** Strip a `//<subdir>` package-subdirectory suffix, ignoring one embedded in the scheme (`https://`). */
49
+ function stripSubdir(base: string): string {
50
+ const schemeEnd = base.indexOf("://");
51
+ const searchFrom = schemeEnd === -1 ? 0 : schemeEnd + 3;
52
+ const idx = base.indexOf("//", searchFrom);
53
+ return idx === -1 ? base : base.slice(0, idx);
54
+ }
55
+
56
+ function isRegistryAddress(rest: string): boolean {
57
+ const parts = stripSubdir(rest).split("/");
58
+ let namespace: string, name: string, targetSystem: string;
59
+ if (parts.length === 3) {
60
+ [namespace, name, targetSystem] = parts;
61
+ } else if (parts.length === 4) {
62
+ const host = parts[0];
63
+ if (!host.includes(".") || RESERVED_VCS_HOSTS.has(host.toLowerCase())) return false;
64
+ [, namespace, name, targetSystem] = parts;
65
+ } else {
66
+ return false;
67
+ }
68
+ return REGISTRY_NAME.test(namespace) && REGISTRY_NAME.test(name) && REGISTRY_TARGET_SYSTEM.test(targetSystem);
69
+ }
70
+
71
+ /** Is `rest` (source with any `git::`/`hg::` force prefix already stripped) a git/hg-shaped source? */
72
+ function isGitShaped(rest: string): boolean {
73
+ if (/^[\w.-]+@[^:/]+:/.test(rest)) return true; // scp-style, e.g. git@github.com:org/repo.git
74
+ const withoutScheme = rest.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
75
+ if (/^(github\.com|bitbucket\.org)\//i.test(withoutScheme)) return true;
76
+ if (/\.git($|[/?])/i.test(withoutScheme)) return true;
77
+ return false;
78
+ }
79
+
80
+ export function classifyModuleSource(source: string): ModuleSourceClassification {
81
+ const trimmed = source.trim();
82
+ if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith(".\\") || trimmed.startsWith("..\\")) {
83
+ return { kind: "local" };
84
+ }
85
+
86
+ const queryIdx = trimmed.indexOf("?");
87
+ const base = queryIdx === -1 ? trimmed : trimmed.slice(0, queryIdx);
88
+ const query = queryIdx === -1 ? "" : trimmed.slice(queryIdx + 1);
89
+
90
+ const forced = /^(git|hg)::/i.exec(base);
91
+ const rest = forced ? base.slice(forced[0].length) : base;
92
+
93
+ if (forced || isGitShaped(rest)) {
94
+ const params = parseQuery(query);
95
+ const ref = params.get("ref") || params.get("rev") || undefined;
96
+ return { kind: "git", ref };
97
+ }
98
+
99
+ if (isRegistryAddress(rest)) return { kind: "registry" };
100
+
101
+ return { kind: "other" };
102
+ }
103
+
104
+ const TAG_LIKE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
105
+ const FULL_SHA = /^[0-9a-f]{40}$/i;
106
+ const DEFAULT_BRANCHES = new Set(["main", "master", "develop", "trunk"]);
107
+
108
+ /**
109
+ * Is `ref` pinned tightly enough for TF005: a semver-shaped tag or a full
110
+ * 40-hex commit SHA. Stricter than checkov's `CKV_TF_1`/`CKV_TF_2`, which
111
+ * accept any `?ref=` containing `\d\.\d` (so `v1.2-dev` passes there); the
112
+ * page documents the difference.
113
+ */
114
+ export function isPinnedRef(ref: string): boolean {
115
+ return TAG_LIKE.test(ref) || FULL_SHA.test(ref);
116
+ }
117
+
118
+ /** Is `ref` one of the well-known mutable branch names tflint's `flexible` style rejects, plus `trunk`. */
119
+ export function isDefaultBranch(ref: string): boolean {
120
+ return DEFAULT_BRANCHES.has(ref);
121
+ }