@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
@@ -15,7 +15,390 @@
15
15
  * `terraform_workspace_remote` (a `terraform.workspace` compatibility check
16
16
  * against remote execution, a different condition than "no backend is
17
17
  * configured at all"). Ship with no lineage rather than invent a credit.
18
+ *
19
+ * TF101 (plan-before-apply, `lint/rules/plan-before-apply.ts`) is a source-level
20
+ * lint rule over chant's own Op builders, not a post-synth check, so it carries
21
+ * no `RuleMeta`/`Lineage` entry here at all: this file's `Record` only ever
22
+ * keys post-synth check ids. Its own page section (docs/pages/lint-rules.mdx)
23
+ * says the same thing directly: no tool anywhere models "this plan file traces
24
+ * back to a specific preceding plan step", because that is a question about
25
+ * chant's own DSL, not about a Terraform root.
26
+ *
27
+ * ## The survey registry
28
+ *
29
+ * #2107 ran three research passes over every serious Terraform lint, scan and
30
+ * policy tool (1,888 lines, 222 cited URLs, attached to that issue as
31
+ * comments). The tools and documents below are what the survey found worth
32
+ * crediting from across that research. #2108 added none of them to
33
+ * `PRIOR_ART`: `packages/core/src/audit/catalog.test.ts`'s "every registered
34
+ * tool is credited" test fails on an entry no shipped rule cites yet, and
35
+ * neither TF001 nor TF101 cites anything. #2109 (TF002-TF005) is the first to
36
+ * draw from this list: `tflint-ruleset-terraform`, `terraform-sentinel-policies`,
37
+ * `hashicorp-style-guide` and `aws-terraform-prescriptive-guidance` are now in
38
+ * `PRIOR_ART`, spelled exactly as below. `checkov` and `kics` were already
39
+ * registered (other lexicons cite them); TF004 and TF005 are the first
40
+ * Terraform rules to. Each later rule issue adds the remaining entries it
41
+ * needs the first time a shipped rule cites them: #2112 (TF014, TF015, TF020,
42
+ * TF021) added `gcp-terraform-best-practices`, the last row of the table below
43
+ * that no shipped rule had cited yet.
44
+ *
45
+ * Each row's Sweep note says whether `scripts/prior-art-sweep.ts` can index the
46
+ * source and, when it cannot, why (#2219). Five of these are indexed as of
47
+ * 2026-09-07: the three tflint rulesets, tfsec and the Sentinel policy set,
48
+ * 311 upstream rule ids in `scripts/prior-art/snapshot.json`. The rest are
49
+ * prose or code, so the tools they credit are re-read by hand when their
50
+ * documents change.
51
+ *
52
+ * | Key | Name | URL | Licence | Kind | Note |
53
+ * |---|---|---|---|---|---|
54
+ * | `tflint` | tflint | https://github.com/terraform-linters/tflint | MPL-2.0 (its `terraform` package is BUSL-1.1; binaries are bound by both) | scanner | The plugin host; ships no language rules of its own. Sweep: not registered in `PRIOR_ART` (no rule cites it), so it is not swept. |
55
+ * | `tflint-ruleset-terraform` | tflint-ruleset-terraform | https://github.com/terraform-linters/tflint-ruleset-terraform | MPL-2.0 | scanner | The official language-level ruleset, bundled into tflint. Sweep: indexed from `docs/rules/README.md`, whose table links each rule's page (20 rules). |
56
+ * | `tflint-ruleset-redeploy` | tflint-ruleset-redeploy | https://github.com/RedeployAB/tflint-ruleset-redeploy | 0BSD | scanner | Community ruleset; richest source of language-level rules outside the official set. Sweep: indexed from `docs/rules/README.md`, same table shape (33 rules). |
57
+ * | `tflint-ruleset-avm` | tflint-ruleset-avm | https://github.com/Azure/tflint-ruleset-avm | MIT | scanner | Azure Verified Modules conformance; two of its 33 rules are provider-agnostic. Sweep: indexed from the repository-root `RULES.md`, since `docs/` holds pages for only two rules (33 rules). |
58
+ * | `tfsec` | tfsec | https://github.com/aquasecurity/tfsec | MIT | scanner | Historical: not archived but effectively frozen (no release since 2025-05); its docs site 404s above v0.61.x, so cite pinned repository paths (`github.com/aquasecurity/tfsec/blob/master/docs/checks/...`), not the docs site. Sweep: indexed from the root `rules.md` on `master`, a flat table of all 152 check ids, because `docs/checks` nests a directory per provider, service and check. |
59
+ * | `trivy-checks` | trivy-checks | https://github.com/aquasecurity/trivy-checks | MIT (not the trivy scanner's own Apache-2.0; cite the checks repo's licence, not the engine's) | scanner | Where tfsec's rules live today (tfsec -> defsec -> trivy + trivy-checks). Cite rules by `long_id` (stable, human-readable); `AVD-*` ids are aliases now. Sweep: not registered in `PRIOR_ART` (no rule cites it yet) and its `long_id`s live in Rego metadata under `checks/<kind>/<provider>/<service>/`, with no flat index. |
60
+ * | `terraform-sentinel-policies` | HashiCorp reference Sentinel policies | https://github.com/hashicorp/terraform-sentinel-policies | MPL-2.0 | specification | HashiCorp's own example policy set; ships zero rules baked into HCP Terraform itself, so treat a cited policy as a documented pattern (`specification`) unless a future entry runs it as a real check (`scanner`); the `kind` union allows either. Sweep: indexed by listing the five per-cloud directories, whose `.sentinel` filenames are the policy ids credits cite (73 policies); the repository publishes no index page. |
61
+ * | `choudoufu` | choudoufu | https://github.com/INTENTIUS/choudoufu | MPL-2.0 | scanner | Its `internal/live/lint` package; experimental, AWS-only. Sweep: unsweepable, its rule ids are Go constants in `internal/live/lint/issue.go` and it publishes no rule index. |
62
+ * | `hashicorp-style-guide` | HashiCorp Terraform style guide | https://developer.hashicorp.com/terraform/language/style | n/a | specification | Sweep: unsweepable, one prose page of recommendations with no rule ids; a credit quotes the sentence and links its fragment. |
63
+ * | `gcp-terraform-best-practices` | Google Cloud Terraform best-practices series | https://docs.cloud.google.com/docs/terraform/best-practices/general-style-structure | n/a | specification | A multi-page series; cite the specific page's fragment. Sweep: unsweepable, prose across several pages with no rule ids. |
64
+ * | `aws-terraform-prescriptive-guidance` | AWS prescriptive guidance for Terraform | https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/ | n/a | specification | Sweep: unsweepable, a prose guide whose sections carry headings rather than rule ids. |
65
+ *
66
+ * Deliberately excluded, per the survey: Terrascan and regula (both archived,
67
+ * do not build on dead projects); Snyk IaC (a product, not a rule source);
68
+ * semgrep (no SPDX id: its engine is LGPL-2.1 but its rules ship under the
69
+ * proprietary Semgrep Rules License v1.0, which forbids redistribution; cite
70
+ * it by name in prose if a rule's idea comes from it, never register it in
71
+ * `PRIOR_ART` with an SPDX licence). TF008 credits semgrep by name under that
72
+ * rule, so `PRIOR_ART` does carry the entry, with the licence spelled out
73
+ * rather than as an SPDX id. Sweep: unsweepable, the registry publishes no
74
+ * index and a rule id is the path of its YAML file under
75
+ * `terraform/<provider>/<category>/`, which no single directory listing covers.
76
+ *
77
+ * TF024 (#2103, live root declares a backend/cloud block) credits choudoufu's
78
+ * own `RuleStateBackend`, `internal/live/lint/issue.go` of
79
+ * github.com/INTENTIUS/choudoufu: the identical condition, checked the same
80
+ * way this fork checks it at `init` before any command runs. TF025 (a live
81
+ * root's non-default `terraform.workspace`) has no entry: its workspace half
82
+ * is refused at the CLI argument-parsing layer there
83
+ * (`internal/command/live_mode.go`), not by a dedicated lint rule under
84
+ * `internal/live/lint`, so there is no `Rule*` constant to point at.
85
+ *
86
+ * TF026 (#2106, a live root's `delete: "never"` against its `policy` block's
87
+ * `undeclared_tagged` verb) has no entry either, the same reason TF001 has
88
+ * none: choudoufu's own `internal/live/lint` package checks a policy block
89
+ * for internal validity (`RulePolicyVerb`: an unrecognized or wrong-quadrant
90
+ * verb; `RulePolicyScope`: `undeclared_untagged = "delete"` with no `scope`
91
+ * block), never against a `delete: "never" | "owned-only" | "gated"`
92
+ * classification, because choudoufu has no such classification — that
93
+ * vocabulary is chant's own, shared across lexicons (`lexicons/k8s/src/op/
94
+ * activities/kubectl.ts`'s `ApplyDeleteMode`, `packages/core/src/op/
95
+ * activities/apply.ts`'s `DeleteMode`), applied here to a tool that does not
96
+ * itself carry the concept. Ship with no lineage rather than invent a credit.
97
+ *
98
+ * TF027 (#2216, a live root's `policy` block setting `undeclared_untagged =
99
+ * "delete"`) does have one: choudoufu's `RulePolicyScope`
100
+ * (`internal/live/lint/policy.go`) refuses the same assignment, and issue #67
101
+ * there makes it a lint refusal rather than a default. The relation is
102
+ * `extends`, because choudoufu refuses it only when no `scope` block narrows
103
+ * the purge, while chant refuses it either way: a `scope` block bounds an
104
+ * account-wide sweep to a service, type or region, and every resource inside
105
+ * that boundary is still one this estate never marked. "Bounded" is not
106
+ * "owned", and owning it is the condition chant applies.
107
+ *
108
+ * TF028 (#2216, a live root watched by a `TerraformWatchOp` built without
109
+ * `live: true`) has no entry. It reads a chant Op's own configuration against
110
+ * a root's mode, and no surveyed tool has an Op model to read: choudoufu lints
111
+ * HCL, and every tflint ruleset, tfsec/trivy-checks and the Sentinel policies
112
+ * do the same. There is nothing upstream to credit.
18
113
  */
19
114
  import type { Lineage } from "@intentius/chant/audit/catalog";
20
115
 
21
- export const terraformAuditLineage: Record<string, Lineage[]> = {};
116
+ const TFSEC_V061 = "https://github.com/aquasecurity/tfsec/blob/v0.61.3/docs/checks/general/secrets";
117
+ /** tflint's official language ruleset. */
118
+ const TFLINT_RULES = "https://github.com/terraform-linters/tflint-ruleset-terraform/blob/main/docs/rules";
119
+ /** The community ruleset with the richest set of language rules outside the official one. */
120
+ const REDEPLOY_RULES = "https://github.com/RedeployAB/tflint-ruleset-redeploy/blob/main/docs/rules";
121
+
122
+ export const terraformAuditLineage: Record<string, Lineage[]> = {
123
+ TF027: [
124
+ {
125
+ tool: "choudoufu",
126
+ rule: "RulePolicyScope",
127
+ url: "https://github.com/INTENTIUS/choudoufu/blob/main/internal/live/lint/policy.go",
128
+ relation: "extends",
129
+ },
130
+ ],
131
+ TF024: [
132
+ {
133
+ tool: "choudoufu",
134
+ rule: "RuleStateBackend",
135
+ url: "https://github.com/INTENTIUS/choudoufu/blob/main/internal/live/lint/issue.go",
136
+ relation: "equivalent",
137
+ },
138
+ ],
139
+ TF002: [
140
+ {
141
+ tool: "tflint-ruleset-terraform",
142
+ rule: "terraform_required_providers",
143
+ url: "https://github.com/terraform-linters/tflint-ruleset-terraform/blob/main/docs/rules/terraform_required_providers.md",
144
+ relation: "equivalent",
145
+ },
146
+ {
147
+ tool: "terraform-sentinel-policies",
148
+ rule: "require-all-providers-have-version-constraint",
149
+ url: "https://github.com/hashicorp/terraform-sentinel-policies/tree/main/cloud-agnostic",
150
+ relation: "equivalent",
151
+ },
152
+ {
153
+ tool: "aws-terraform-prescriptive-guidance",
154
+ rule: "Add automated version checks",
155
+ url: "https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/version.html#add-automated-version-checks",
156
+ relation: "equivalent",
157
+ },
158
+ ],
159
+ // Sentinel's restrict-terraform-versions checks that required_version is
160
+ // within an allowed range, not that it is present at all: a narrower
161
+ // question than TF003's, so "overlaps" rather than "equivalent" (#2109).
162
+ TF003: [
163
+ {
164
+ tool: "tflint-ruleset-terraform",
165
+ rule: "terraform_required_version",
166
+ url: "https://github.com/terraform-linters/tflint-ruleset-terraform/blob/main/docs/rules/terraform_required_version.md",
167
+ relation: "equivalent",
168
+ },
169
+ {
170
+ tool: "terraform-sentinel-policies",
171
+ rule: "restrict-terraform-versions",
172
+ url: "https://github.com/hashicorp/terraform-sentinel-policies/tree/main/cloud-agnostic",
173
+ relation: "overlaps",
174
+ },
175
+ {
176
+ tool: "hashicorp-style-guide",
177
+ rule: "Version pinning",
178
+ url: "https://developer.hashicorp.com/terraform/language/style#version-pinning",
179
+ relation: "overlaps",
180
+ },
181
+ ],
182
+ TF004: [
183
+ {
184
+ tool: "tflint-ruleset-terraform",
185
+ rule: "terraform_module_version",
186
+ url: "https://github.com/terraform-linters/tflint-ruleset-terraform/blob/main/docs/rules/terraform_module_version.md",
187
+ relation: "equivalent",
188
+ },
189
+ {
190
+ tool: "checkov",
191
+ rule: "CKV_TF_2",
192
+ url: "https://github.com/bridgecrewio/checkov/blob/main/checkov/terraform/checks/module/generic/RevisionVersionTag.py",
193
+ relation: "equivalent",
194
+ },
195
+ ],
196
+ // checkov's CKV_TF_1 accepts any `?ref=`/`&ref=` matching `[?&](ref=).*(\d\.\d).*`,
197
+ // which a branch literally named `v1.2-dev` passes; TF005 requires a full
198
+ // semver tag or a 40-hex SHA, so "extends" (a strict superset), not
199
+ // "equivalent". KICS's query only matches the `git::` prefix, not the
200
+ // github.com/bitbucket.org shorthands or scp-style sources TF005 also
201
+ // covers, so "overlaps" (#2109).
202
+ TF005: [
203
+ {
204
+ tool: "tflint-ruleset-terraform",
205
+ rule: "terraform_module_pinned_source",
206
+ url: "https://github.com/terraform-linters/tflint-ruleset-terraform/blob/main/docs/rules/terraform_module_pinned_source.md",
207
+ relation: "equivalent",
208
+ },
209
+ {
210
+ tool: "checkov",
211
+ rule: "CKV_TF_1",
212
+ url: "https://github.com/bridgecrewio/checkov/blob/main/checkov/terraform/checks/module/generic/RevisionHash.py",
213
+ relation: "extends",
214
+ },
215
+ {
216
+ tool: "kics",
217
+ rule: "3a81fc06-566f-492a-91dd-7448e409e2cd",
218
+ url: "https://docs.kics.io/latest/queries/terraform-queries/3a81fc06-566f-492a-91dd-7448e409e2cd/",
219
+ relation: "overlaps",
220
+ },
221
+ ],
222
+ TF006: [
223
+ {
224
+ tool: "tflint-ruleset-avm",
225
+ rule: "avm_terraform_sensitive_variable_default_disallowed",
226
+ url: "https://github.com/Azure/tflint-ruleset-avm/blob/main/docs/basic/avm_terraform_sensitive_variable_default_disallowed.md",
227
+ relation: "equivalent",
228
+ },
229
+ ],
230
+ TF007: [
231
+ { tool: "tfsec", rule: "general-secrets-sensitive-in-variable", url: `${TFSEC_V061}/sensitive-in-variable.md`, relation: "equivalent" },
232
+ { tool: "tfsec", rule: "general-secrets-sensitive-in-local", url: `${TFSEC_V061}/sensitive-in-local.md`, relation: "equivalent" },
233
+ ],
234
+ // #2107's rule table credits TF008 with Snyk `SNYK-CC-TF-74` ("Credentials
235
+ // are configured via provider attributes") alongside these three. That
236
+ // credit is deliberately absent here, and the absence is the epic's own
237
+ // decision rather than an oversight: Snyk IaC is a product, not a rule
238
+ // source, which is why it sits in this file's "deliberately excluded" list
239
+ // above and has no `PRIOR_ART` entry to cite. The rules have no open
240
+ // repository behind them, and the public index (security.snyk.io/rules/
241
+ // cloud) does not even enumerate the legacy `SNYK-CC-TF-*` ids, so a
242
+ // credit here could not be swept the way every other one in this file is
243
+ // (scripts/prior-art-sweep.ts). The condition loses nothing by it:
244
+ // `SNYK-CC-TF-74`, checkov's `CKV_AWS_41` and semgrep's
245
+ // `aws-provider-static-credentials` are the same check three times over,
246
+ // and the other two are credited below (#2220).
247
+ TF008: [
248
+ { tool: "checkov", rule: "CKV_AWS_41", url: "https://www.checkov.io/5.Policy%20Index/terraform.html", relation: "overlaps" },
249
+ { tool: "kics", rule: "d7b9d850-3e06-4a75-852f-c46c2e92240b", url: "https://docs.kics.io/latest/queries/terraform-queries/aws/d7b9d850-3e06-4a75-852f-c46c2e92240b/", relation: "overlaps" },
250
+ {
251
+ tool: "semgrep",
252
+ rule: "terraform.aws.security.aws-provider-static-credentials",
253
+ url: "https://github.com/semgrep/semgrep-rules/blob/develop/terraform/aws/security/aws-provider-static-credentials.yaml",
254
+ relation: "overlaps",
255
+ },
256
+ ],
257
+ TF009: [
258
+ {
259
+ tool: "hashicorp-style-guide",
260
+ rule: "Variables: for sensitive variables, such as passwords and private keys, set the sensitive parameter to true",
261
+ url: "https://developer.hashicorp.com/terraform/language/style#variables",
262
+ relation: "equivalent",
263
+ },
264
+ ],
265
+ TF010: [
266
+ { tool: "tflint-ruleset-terraform", rule: "terraform_typed_variables", url: `${TFLINT_RULES}/terraform_typed_variables.md`, relation: "equivalent" },
267
+ { tool: "kics", rule: "fc5109bf-01fd-49fb-8bde-4492b543c34a", url: "https://docs.kics.io/latest/queries/terraform-queries/fc5109bf-01fd-49fb-8bde-4492b543c34a/", relation: "equivalent" },
268
+ ],
269
+ TF011: [
270
+ { tool: "tflint-ruleset-terraform", rule: "terraform_documented_variables", url: `${TFLINT_RULES}/terraform_documented_variables.md`, relation: "equivalent" },
271
+ { tool: "kics", rule: "2a153952-2544-4687-bcc9-cc8fea814a9b", url: "https://docs.kics.io/latest/queries/terraform-queries/2a153952-2544-4687-bcc9-cc8fea814a9b/", relation: "equivalent" },
272
+ {
273
+ tool: "terraform-sentinel-policies",
274
+ rule: "validate-variables-have-descriptions",
275
+ url: "https://github.com/hashicorp/terraform-sentinel-policies/blob/main/cloud-agnostic/validate-variables-have-descriptions.sentinel",
276
+ relation: "equivalent",
277
+ },
278
+ ],
279
+ TF012: [
280
+ { tool: "tflint-ruleset-terraform", rule: "terraform_documented_outputs", url: `${TFLINT_RULES}/terraform_documented_outputs.md`, relation: "equivalent" },
281
+ { tool: "kics", rule: "59312e8a-a64e-41e7-a252-618533dd1ea8", url: "https://docs.kics.io/latest/queries/terraform-queries/59312e8a-a64e-41e7-a252-618533dd1ea8/", relation: "equivalent" },
282
+ ],
283
+ TF013: [
284
+ { tool: "tflint-ruleset-redeploy", rule: "terraform_ignore_changes_all", url: `${REDEPLOY_RULES}/terraform_ignore_changes_all.md`, relation: "equivalent" },
285
+ ],
286
+ // The two vendor documents below (and the one on TF015, and the HashiCorp
287
+ // style guide on TF021) are `Lineage` entries, not `RuleMeta.authority`
288
+ // ones, though #2107's table calls them "authority". `authority` is not a
289
+ // free-standing "this document says so" field: an authority citation forces
290
+ // `category: "security"` (`packages/core/src/audit/catalog.ts`), and
291
+ // `catalog.test.ts` binds the two together in both directions, "an
292
+ // authority citation always means security" and "authority citations only
293
+ // attach to merge-worthy entries". A provider block in a child module is a
294
+ // structural rule, not a security finding, so citing these guides as
295
+ // authority would have relabelled three rules as security to get a citation
296
+ // rendered. Lineage carries the same URLs and never touches tier or
297
+ // category, so that is where they live (#2112, recorded here by #2220).
298
+ TF014: [
299
+ {
300
+ tool: "tflint-ruleset-avm",
301
+ rule: "avm_terraform_provider_block_disallowed",
302
+ url: "https://github.com/Azure/tflint-ruleset-avm/blob/main/docs/basic/avm_terraform_provider_block_disallowed.md",
303
+ relation: "equivalent",
304
+ },
305
+ {
306
+ tool: "gcp-terraform-best-practices",
307
+ rule: "Build reusable modules: modules must not configure providers",
308
+ url: "https://docs.cloud.google.com/docs/terraform/best-practices/general-style-structure#reusable-modules",
309
+ relation: "equivalent",
310
+ },
311
+ {
312
+ tool: "aws-terraform-prescriptive-guidance",
313
+ rule: "Structure: declare provider configurations in the root module",
314
+ url: "https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/structure.html",
315
+ relation: "equivalent",
316
+ },
317
+ ],
318
+ // TF015 has no scanner to credit: no ruleset in the survey checks for a
319
+ // backend or cloud block inside a child module.
320
+ // tflint-ruleset-terraform's index has nothing about backends at all,
321
+ // Redeploy's has nothing either, and AVM's module rules stop at the
322
+ // provider block TF014 credits. What states the rule is Google Cloud's
323
+ // reusable-modules guide, so that document is the single credit here, and
324
+ // the page section says plainly that the credit is a document rather than
325
+ // a tool (#2112). It is lineage rather than `authority` for the reason on
326
+ // TF014 above: an authority citation would force `category: "security"` on
327
+ // a rule about where state configuration belongs.
328
+ TF015: [
329
+ {
330
+ tool: "gcp-terraform-best-practices",
331
+ rule: "Build reusable modules: state configuration belongs to the root module",
332
+ url: "https://docs.cloud.google.com/docs/terraform/best-practices/general-style-structure#reusable-modules",
333
+ relation: "equivalent",
334
+ },
335
+ ],
336
+ TF016: [
337
+ // `overlaps`, not `equivalent`: tflint reports the same `x = "${y}"` form
338
+ // and also the object-key case (`"${var.k}" = v`), which this rule leaves
339
+ // alone. See tf016.ts.
340
+ { tool: "tflint-ruleset-terraform", rule: "terraform_deprecated_interpolation", url: `${TFLINT_RULES}/terraform_deprecated_interpolation.md`, relation: "overlaps" },
341
+ ],
342
+ TF017: [
343
+ { tool: "tflint-ruleset-redeploy", rule: "terraform_module_depends_on", url: `${REDEPLOY_RULES}/terraform_module_depends_on.md`, relation: "equivalent" },
344
+ ],
345
+ TF018: [
346
+ { tool: "tflint-ruleset-redeploy", rule: "terraform_output_resource", url: `${REDEPLOY_RULES}/terraform_output_resource.md`, relation: "equivalent" },
347
+ ],
348
+ TF019: [
349
+ { tool: "tflint-ruleset-redeploy", rule: "terraform_redundant_default", url: `${REDEPLOY_RULES}/terraform_redundant_default.md`, relation: "equivalent" },
350
+ ],
351
+ TF020: [
352
+ // `overlaps`: tflint evaluates expressions and reports the same four
353
+ // kinds of declaration; chant's index is a string scan over hcl2json's
354
+ // preserved expression strings, so it is generous where tflint is exact
355
+ // (see hcl/references.ts and tf020.ts).
356
+ {
357
+ tool: "tflint-ruleset-terraform",
358
+ rule: "terraform_unused_declarations",
359
+ url: `${TFLINT_RULES}/terraform_unused_declarations.md`,
360
+ relation: "overlaps",
361
+ },
362
+ ],
363
+ TF021: [
364
+ // Redeploy reports every `count` over a collection; TF021 reports only
365
+ // the ones that build an identity out of `count.index`, which is a
366
+ // strict subset, hence `overlaps` on both credits rather than
367
+ // `equivalent`. choudoufu's own rule is the survey's deepest reading of
368
+ // the identity-bearing question (#2112). The HashiCorp style guide entry
369
+ // is lineage rather than `authority` for the reason recorded on TF014: an
370
+ // authority citation forces `category: "security"`, and `count` versus
371
+ // `for_each` is not a security finding.
372
+ {
373
+ tool: "tflint-ruleset-redeploy",
374
+ rule: "terraform_prefer_for_each",
375
+ url: `${REDEPLOY_RULES}/terraform_prefer_for_each.md`,
376
+ relation: "overlaps",
377
+ },
378
+ {
379
+ tool: "choudoufu",
380
+ rule: "RuleCountIndex",
381
+ url: "https://github.com/INTENTIUS/choudoufu/blob/main/internal/live/lint/count_index.go",
382
+ relation: "overlaps",
383
+ },
384
+ {
385
+ tool: "hashicorp-style-guide",
386
+ rule: "Resources: use for_each for a dynamic resource count",
387
+ url: "https://developer.hashicorp.com/terraform/language/style#dynamic-resource-count",
388
+ relation: "overlaps",
389
+ },
390
+ ],
391
+ TF022: [
392
+ // tfsec consolidated its three `general/secrets` checks into
393
+ // `no-plaintext-exposure` in v1, which is the one still in the repository's
394
+ // `master` docs; `overlaps` because that consolidation also covers the
395
+ // variable and locals cases TF007 reports separately.
396
+ {
397
+ tool: "tfsec",
398
+ rule: "general-secrets-no-plaintext-exposure",
399
+ url: "https://github.com/aquasecurity/tfsec/blob/master/docs/checks/general/secrets/no-plaintext-exposure/index.md",
400
+ relation: "overlaps",
401
+ },
402
+ { tool: "kics", rule: "a88baa34-e2ad-44ea-ad6f-8cac87bc7c71", url: "https://docs.kics.io/latest/secrets/", relation: "overlaps" },
403
+ ],
404
+ };
@@ -1,9 +1,16 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
3
5
  import { describe, expect, test } from "vitest";
4
- import { auditFiles, type ChecksProvider, type EntitiesProvider } from "@intentius/chant/audit/core";
5
- import { discoverByDetection, type DetectPlugin } from "@intentius/chant/audit/discover";
6
+ import { auditFiles, CROSS_FILE, type AuditFinding, type ChecksProvider, type EntitiesProvider } from "@intentius/chant/audit/core";
7
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
8
+ import { collectCandidates, discoverByDetection, type DetectPlugin } from "@intentius/chant/audit/discover";
9
+ import { auditTerraformState } from "@intentius/chant/audit/terraform-state";
6
10
  import { terraformPlugin } from "../plugin";
11
+ import { AUDIT_ROOT_NAME } from "../hcl/parse";
12
+ import { renderTerraformRoots } from "../hcl/roots";
13
+ import { postSynthChecks } from "./post-synth";
7
14
 
8
15
  /**
9
16
  * End-to-end audit test (#2085 acceptance): `chant audit` (`auditFiles` on top
@@ -35,11 +42,211 @@ describe("chant audit against a discovered terraform root module", () => {
35
42
  expect(tf001[0]!.lexicon).toBe("terraform");
36
43
  });
37
44
 
38
- test("reports nothing for the with-backend root", async () => {
45
+ // #2218: `backend "local"` is the local backend named rather than fallen
46
+ // back into, so it is the same finding as no backend at all. The audit path
47
+ // gets its own case because it parses joined file content rather than a
48
+ // directory, and the label has to survive that parse too.
49
+ test('reports TF001 for a root whose only backend is `backend "local"`', async () => {
39
50
  const inputs = discoverByDetection(join(fixtures, "with-backend"), [terraformDetectPlugin]);
40
51
  expect(inputs).toHaveLength(1);
41
52
 
53
+ const findings = await auditFiles(inputs, { checksProvider, entitiesProvider });
54
+ const tf001 = findings.filter((f) => f.checkId === "TF001");
55
+ expect(tf001).toHaveLength(1);
56
+ expect(tf001[0]!.message).toContain('the backend it declares is `backend "local"`');
57
+ });
58
+
59
+ test("reports nothing for the remote-backend root", async () => {
60
+ const inputs = discoverByDetection(join(fixtures, "remote-backend"), [terraformDetectPlugin]);
61
+ expect(inputs).toHaveLength(1);
62
+
42
63
  const findings = await auditFiles(inputs, { checksProvider, entitiesProvider });
43
64
  expect(findings.filter((f) => f.checkId === "TF001")).toHaveLength(0);
44
65
  });
45
66
  });
67
+
68
+ /**
69
+ * TF023 (#2110) is audit-only: it reads the file list `chant audit` discovers,
70
+ * never a parsed root module, so it lives in core
71
+ * (`packages/core/src/audit/terraform-state.ts`) beside the other
72
+ * lexicon-independent families. These run the two halves it depends on, the
73
+ * real walk and the real check, over the committed fixture roots.
74
+ */
75
+ describe("TF023: Terraform state committed to the repository", () => {
76
+ const tf023 = join(dirname(fileURLToPath(import.meta.url)), "post-synth", "fixtures", "TF023");
77
+
78
+ test("fires on a root whose state file is committed beside it", () => {
79
+ // The fixture ships its state as `state.json`: a tracked `terraform.tfstate`
80
+ // is exactly what this rule reports, and the repository's own push rules
81
+ // refuse the name. The test materialises the root the rule is written for.
82
+ const root = mkdtempSync(join(tmpdir(), "chant-tf023-"));
83
+ copyFileSync(join(tf023, "positive", "main.tf"), join(root, "main.tf"));
84
+ copyFileSync(join(tf023, "positive", "state.json"), join(root, "terraform.tfstate"));
85
+ const findings = auditTerraformState(collectCandidates(root));
86
+ rmSync(root, { recursive: true, force: true });
87
+ expect(findings).toHaveLength(1);
88
+ expect(findings[0].checkId).toBe("TF023");
89
+ expect(findings[0].file).toBe("terraform.tfstate");
90
+ expect(findings[0].lexicon).toBe("terraform");
91
+ });
92
+
93
+ test("says nothing about a root that keeps its state remote", () => {
94
+ expect(auditTerraformState(collectCandidates(join(tf023, "negative")))).toEqual([]);
95
+ });
96
+
97
+ test("never runs during a build: it is not one of this lexicon's post-synth checks", () => {
98
+ expect(postSynthChecks.map((c) => c.id)).not.toContain("TF023");
99
+ expect(terraformPlugin.auditCatalog?.()).not.toHaveProperty("TF023");
100
+ });
101
+ });
102
+
103
+ /**
104
+ * #2217: `chant audit` used to hand the plugin one directory's joined text and
105
+ * no directory, so the parse stopped at the root module's own files and every
106
+ * discovered directory parsed under the same fixed scope name. A root and its
107
+ * `modules/*` then merged into one entity map, which produced a finding keyed
108
+ * `<scope>/<address>#2` against the file `(cross-file)` and left the two rules
109
+ * about child modules (TF014, TF015) with no child to see. These run the real
110
+ * discovery and the real audit over the tree fixtures those rules were written
111
+ * for.
112
+ */
113
+ describe("chant audit descends a root module's local modules (#2217)", () => {
114
+ const treeFixtures = join(dirname(fileURLToPath(import.meta.url)), "post-synth", "fixtures");
115
+
116
+ async function audit(dir: string): Promise<AuditFinding[]> {
117
+ return auditFiles(discoverByDetection(dir, [terraformDetectPlugin]), { checksProvider, entitiesProvider });
118
+ }
119
+
120
+ test("a directory the root calls as a module is not audited as a root of its own", () => {
121
+ const inputs = discoverByDetection(join(treeFixtures, "TF014", "positive"), [terraformDetectPlugin]);
122
+ expect(inputs.map((i) => i.path)).toEqual(["."]);
123
+ expect(inputs[0]!.dir).toBe(join(treeFixtures, "TF014", "positive"));
124
+ });
125
+
126
+ test("TF014 fires on the positive fixture, named by its call chain", async () => {
127
+ const findings = await audit(join(treeFixtures, "TF014", "positive"));
128
+ const tf014 = findings.filter((f) => f.checkId === "TF014");
129
+ expect(tf014).toHaveLength(1);
130
+ expect(tf014[0]!.entity).toBe("audit-root/module.cdn/provider.aws");
131
+ expect(tf014[0]!.message).toContain("Callers: audit-root -> module.cdn (main.tf:13).");
132
+ expect(tf014[0]!.file).toBe(".");
133
+ });
134
+
135
+ test("nothing is attributed to a synthetic cross-file entity any more", async () => {
136
+ const findings = await audit(join(treeFixtures, "TF014", "positive"));
137
+ expect(findings.map((f) => f.file)).not.toContain(CROSS_FILE);
138
+ expect(findings.filter((f) => /#\d+$/.test(f.entity ?? ""))).toEqual([]);
139
+ // The root's own provider block is still the root's: TF002 reports it
140
+ // against the root scope, not against a merge of the two directories.
141
+ const tf002 = findings.filter((f) => f.checkId === "TF002");
142
+ expect(tf002.map((f) => f.entity)).toEqual(["audit-root/provider.aws"]);
143
+ });
144
+
145
+ test("TF014 says nothing about the negative fixture's alias-only block", async () => {
146
+ const findings = await audit(join(treeFixtures, "TF014", "negative"));
147
+ expect(findings.filter((f) => f.checkId === "TF014")).toEqual([]);
148
+ expect(findings.map((f) => f.file)).not.toContain(CROSS_FILE);
149
+ });
150
+
151
+ test("TF015 fires on a child module that declares a backend", async () => {
152
+ const findings = await audit(join(treeFixtures, "TF015", "positive"));
153
+ expect(findings.map((f) => `${f.checkId} ${f.entity}`)).toEqual(["TF015 audit-root/module.cdn/terraform"]);
154
+ expect(findings[0]!.message).toContain("Callers: audit-root -> module.cdn");
155
+ });
156
+
157
+ test("TF015 leaves a child's required_version and required_providers alone", async () => {
158
+ expect(await audit(join(treeFixtures, "TF015", "negative"))).toEqual([]);
159
+ });
160
+
161
+ test("TF001 stays root-only: the child module without a backend is not reported", async () => {
162
+ const findings = await audit(join(treeFixtures, "TF014", "positive"));
163
+ expect(findings.filter((f) => f.checkId === "TF001")).toEqual([]);
164
+ });
165
+
166
+ test("two roots in one repository are two scopes, each named after its directory", async () => {
167
+ const repo = mkdtempSync(join(tmpdir(), "chant-2217-"));
168
+ for (const env of ["prod", "dev"]) {
169
+ mkdirSync(join(repo, "envs", env), { recursive: true });
170
+ writeFileSync(join(repo, "envs", env, "main.tf"), 'terraform {\n required_version = ">= 1.5.0"\n}\n');
171
+ }
172
+ const findings = await audit(repo);
173
+ rmSync(repo, { recursive: true, force: true });
174
+
175
+ const tf001 = findings.filter((f) => f.checkId === "TF001");
176
+ expect(tf001.map((f) => f.missing?.scope).sort()).toEqual(["envs.dev", "envs.prod"]);
177
+ expect(tf001.map((f) => f.file).sort()).toEqual(["envs/dev", "envs/prod"]);
178
+ expect(findings.map((f) => f.file)).not.toContain(CROSS_FILE);
179
+ });
180
+ });
181
+
182
+ /**
183
+ * The regression net for #2217: for every fixture root in the family, the
184
+ * verdict `chant audit` reaches through real discovery is the verdict
185
+ * `chant build` reaches through `renderTerraformRoots`. The two paths parse
186
+ * the same directories with the same checks, so any rule whose behaviour
187
+ * depends on scope (every module-descent rule, and every root-only rule that
188
+ * must NOT follow one) is pinned on both paths at once.
189
+ *
190
+ * The build side renders with no `binary`, so a `live` fixture (TF024 to
191
+ * TF026) is stock on both sides: the audit path does not detect live mode
192
+ * (#2103), and this asserts the two agree, not that either sees an estate.
193
+ */
194
+ describe("every fixture root gets the same verdict from chant audit as from chant build (#2217)", () => {
195
+ const fixtureRoot = join(dirname(fileURLToPath(import.meta.url)), "post-synth", "fixtures");
196
+
197
+ function verdictOf(findings: Array<{ checkId: string; entity?: string }>): string[] {
198
+ return findings.map((f) => `${f.checkId} ${f.entity ?? ""}`).sort();
199
+ }
200
+
201
+ async function buildVerdict(dir: string): Promise<string[]> {
202
+ const { entities, warnings } = await renderTerraformRoots({
203
+ projectRoot: dir,
204
+ roots: { [AUDIT_ROOT_NAME]: { dir } },
205
+ });
206
+ const ctx = {
207
+ outputs: new Map(),
208
+ entities,
209
+ buildResult: { outputs: new Map(), entities, warnings, errors: [], sourceFileCount: entities.size },
210
+ } as unknown as PostSynthContext;
211
+ return verdictOf((terraformPlugin.postSynthChecks?.() ?? []).flatMap((check) => check.check(ctx)));
212
+ }
213
+
214
+ async function auditVerdict(dir: string): Promise<string[]> {
215
+ const inputs = discoverByDetection(dir, [terraformDetectPlugin]);
216
+ return verdictOf(await auditFiles(inputs, { checksProvider, entitiesProvider }));
217
+ }
218
+
219
+ /** Every fixture case: a `<id>/<case>/` tree as it stands, and a `<id>/<case>.tf` copied into a directory of its own. */
220
+ function cases(): Array<{ name: string; materialize: (into: string) => string }> {
221
+ const out: Array<{ name: string; materialize: (into: string) => string }> = [];
222
+ for (const id of readdirSync(fixtureRoot).filter((e) => statSync(join(fixtureRoot, e)).isDirectory()).sort()) {
223
+ for (const entry of readdirSync(join(fixtureRoot, id)).sort()) {
224
+ const full = join(fixtureRoot, id, entry);
225
+ if (statSync(full).isDirectory()) {
226
+ out.push({ name: `${id}/${entry}`, materialize: () => full });
227
+ } else if (entry.endsWith(".tf")) {
228
+ out.push({
229
+ name: `${id}/${entry}`,
230
+ materialize: (into) => {
231
+ copyFileSync(full, join(into, entry));
232
+ return into;
233
+ },
234
+ });
235
+ }
236
+ }
237
+ }
238
+ return out;
239
+ }
240
+
241
+ for (const { name, materialize } of cases()) {
242
+ test(name, async () => {
243
+ const scratch = mkdtempSync(join(tmpdir(), "chant-2217-fx-"));
244
+ const dir = materialize(scratch);
245
+ try {
246
+ expect(await auditVerdict(dir)).toEqual(await buildVerdict(dir));
247
+ } finally {
248
+ rmSync(scratch, { recursive: true, force: true });
249
+ }
250
+ });
251
+ }
252
+ });