@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
@@ -1,12 +1,16 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
  import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
- import { tf001 } from "./tf001";
3
+ import { backendTypes, hasRemoteBackend, tf001 } from "./tf001";
4
+ import { loadFixture } from "./fixtures/load";
4
5
  import { TERRAFORM_TYPE, terraformEntity } from "../../hcl/parse";
5
6
 
6
7
  /**
7
- * The local context builder the k3s checks use. `createPostSynthContext` from
8
- * the test-utils package hands back an empty entity map, and every check here
9
- * reads `ctx.entities`.
8
+ * The local context builder for the edge cases below that are about the
9
+ * *collection* (several roots or several blocks sharing one root), not about
10
+ * one root's parsed shape, which is what real HCL fixtures represent. The four
11
+ * headline cases (a backendless root, a `backend "local"` root, a remote
12
+ * backend, a `cloud` block) go through `fixtures/TF001/` and `loadFixture`
13
+ * instead; see that module's doc comment for why.
10
14
  */
11
15
  function makeCtx(entities: Record<string, ReturnType<typeof terraformEntity>>): PostSynthContext {
12
16
  return {
@@ -21,23 +25,37 @@ function terraformBlock(root: string, body: Record<string, unknown>): ReturnType
21
25
  }
22
26
 
23
27
  describe("TF001: root module declares no remote backend", () => {
24
- test("flags a root whose terraform block has no backend", () => {
25
- const diags = tf001.check(
26
- makeCtx({ "legacy/terraform": terraformBlock("legacy", { required_version: ">= 1.5.0" }) }),
27
- );
28
+ test("flags a root whose terraform block has no backend", async () => {
29
+ const diags = tf001.check(await loadFixture("TF001", "positive"));
28
30
  expect(diags).toHaveLength(1);
29
31
  expect(diags[0].checkId).toBe("TF001");
30
- expect(diags[0].entity).toBe("legacy/terraform");
32
+ expect(diags[0].entity).toBe("TF001/terraform");
31
33
  expect(diags[0].lexicon).toBe("terraform");
32
- expect(diags[0].message).toContain("legacy");
34
+ expect(diags[0].message).toContain("TF001");
35
+ expect(diags[0].message).toContain("there is no backend block at all");
36
+ expect(diags[0].missing).toEqual({ kind: "backend", scope: "TF001" });
33
37
  });
34
38
 
35
- test("passes a root with a backend block", () => {
36
- const diags = tf001.check(
37
- makeCtx({
38
- "app/terraform": terraformBlock("app", { backend: { local: [{ path: "terraform.tfstate" }] } }),
39
- }),
40
- );
39
+ // #2218: the check reads the backend block's type label, so the local
40
+ // backend is the same finding whether it is fallen back into or asked for
41
+ // by name, and the message says which one it found.
42
+ test('flags a root whose backend is `backend "local"`', async () => {
43
+ const diags = tf001.check(await loadFixture("TF001", "positive-local"));
44
+ expect(diags).toHaveLength(1);
45
+ expect(diags[0].checkId).toBe("TF001");
46
+ expect(diags[0].entity).toBe("TF001/terraform");
47
+ expect(diags[0].message).toContain('the backend it declares is `backend "local"`');
48
+ expect(diags[0].message).not.toContain("no backend block");
49
+ expect(diags[0].missing).toEqual({ kind: "backend", scope: "TF001" });
50
+ });
51
+
52
+ test("passes a root with a remote backend block", async () => {
53
+ const diags = tf001.check(await loadFixture("TF001", "negative"));
54
+ expect(diags).toHaveLength(0);
55
+ });
56
+
57
+ test("passes a root whose cloud block is the whole state configuration", async () => {
58
+ const diags = tf001.check(await loadFixture("TF001", "negative-cloud"));
41
59
  expect(diags).toHaveLength(0);
42
60
  });
43
61
 
@@ -92,4 +110,43 @@ describe("TF001: root module declares no remote backend", () => {
92
110
  const diags = tf001.check(makeCtx({ "app/terraform": terraformBlock("app", { backend: {} }) }));
93
111
  expect(diags).toHaveLength(1);
94
112
  });
113
+
114
+ test('flags a hand-built body carrying `backend "local"`', () => {
115
+ const diags = tf001.check(
116
+ makeCtx({
117
+ "app/terraform": terraformBlock("app", { backend: { local: [{ path: "terraform.tfstate" }] } }),
118
+ }),
119
+ );
120
+ expect(diags).toHaveLength(1);
121
+ expect(diags[0].message).toContain('the backend it declares is `backend "local"`');
122
+ });
123
+ });
124
+
125
+ /**
126
+ * The label read itself (#2218). hcl2json nests a labelled block one level
127
+ * deeper than an unlabelled one, and the type only survives the parse as a
128
+ * key of that inner object, so the shape is worth pinning down apart from the
129
+ * check that reads it.
130
+ */
131
+ describe("backendTypes / hasRemoteBackend", () => {
132
+ test("reads the type label out of the nested object hcl2json builds", () => {
133
+ expect(backendTypes({ backend: { local: [{ path: "terraform.tfstate" }] } })).toEqual(["local"]);
134
+ expect(backendTypes({ backend: { s3: [{ bucket: "tfstate" }] } })).toEqual(["s3"]);
135
+ });
136
+
137
+ test("has no labels to read when there is no backend block, or a malformed one", () => {
138
+ expect(backendTypes({})).toEqual([]);
139
+ expect(backendTypes({ backend: [{ bucket: "tfstate" }] })).toEqual([]);
140
+ expect(backendTypes({ backend: "s3" })).toEqual([]);
141
+ });
142
+
143
+ test("counts every backend but `local` as remote, and counts `cloud`", () => {
144
+ expect(hasRemoteBackend({ backend: { local: [{}] } })).toBe(false);
145
+ expect(hasRemoteBackend({})).toBe(false);
146
+ expect(hasRemoteBackend({ backend: { s3: [{}] } })).toBe(true);
147
+ expect(hasRemoteBackend({ backend: { gcs: [{}] } })).toBe(true);
148
+ expect(hasRemoteBackend({ backend: { azurerm: [{}] } })).toBe(true);
149
+ expect(hasRemoteBackend({ backend: { http: [{}] } })).toBe(true);
150
+ expect(hasRemoteBackend({ cloud: [{ organization: "acme" }] })).toBe(true);
151
+ });
95
152
  });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Root scope or child scope, and the `Callers:` chain that says which call
3
+ * reached a finding (chant #2112).
4
+ *
5
+ * Since the parse descends into local modules, `ctx.entities` holds two kinds
6
+ * of block: the root module's own, keyed `<root>/<address>`, and a child
7
+ * module's, keyed `<root>/module.<name>/<address>` with the call chain on
8
+ * `props.callers`. Every check has to say which it applies to, and the three
9
+ * shapes that answer live here:
10
+ *
11
+ * - {@link isRootScoped} for a check about how a ROOT is assembled (TF001's
12
+ * backend, TF002's `required_providers`): a child module has no backend and
13
+ * no provider requirements of its own to judge, and reading one as if it
14
+ * were the root's is how a root with a perfectly good backend gets flagged
15
+ * because a module beneath it has none.
16
+ * - {@link isChildScoped} for the mirror, the two rules that are ONLY about
17
+ * child modules (TF014, TF015).
18
+ * - {@link callersNote} for the sentence appended to a finding inside a
19
+ * child, naming each call site with the file and line it sits on. tflint
20
+ * prints the same thing as a `Callers:` block under the diagnostic
21
+ * (https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/calling-modules.md);
22
+ * a `PostSynthDiagnostic` carries no ranges, so the chain goes in the
23
+ * message, where a reader of any output format sees it.
24
+ *
25
+ * A check with no opinion (most of them: an unset `type` on a variable is the
26
+ * same finding wherever the variable lives) needs nothing from this module and
27
+ * runs over both scopes, which is what tflint does with its own language rules
28
+ * under `--call-module-type=local`.
29
+ */
30
+
31
+ import type { PostSynthCheck, PostSynthContext } from "@intentius/chant/lint/post-synth";
32
+ import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
33
+ import { callersOfKey, type TerraformEntity } from "../../hcl/parse";
34
+
35
+ /** The `module.<name>` chain on an entity, outermost first. Empty for a root block. */
36
+ export function callersOf(entity: Declarable): readonly string[] {
37
+ if (!isResourceDeclarable(entity)) return [];
38
+ const props = (entity as TerraformEntity).props as Partial<TerraformEntity["props"]>;
39
+ return Array.isArray(props.callers) ? props.callers : [];
40
+ }
41
+
42
+ /** Was this block declared in the root module itself? */
43
+ export function isRootScoped(entity: Declarable): boolean {
44
+ return callersOf(entity).length === 0;
45
+ }
46
+
47
+ /** Was this block declared in a child module the root calls? */
48
+ export function isChildScoped(entity: Declarable): boolean {
49
+ return callersOf(entity).length > 0;
50
+ }
51
+
52
+ /**
53
+ * The `Callers:` sentence for a finding on `key`, or `""` when the finding is
54
+ * in the root module.
55
+ *
56
+ * The call sites are the key's own prefixes: a finding on
57
+ * `app/module.cdn/module.bucket/aws_s3_bucket.assets` was reached through
58
+ * `app/module.cdn` and then `app/module.cdn/module.bucket`, both of them
59
+ * `module` block entities in `ctx.entities`, so the file and line each was
60
+ * parsed from are already recorded. A prefix with no entity (a hand-built
61
+ * context in a test) renders as the bare call name.
62
+ */
63
+ export function callersNote(ctx: PostSynthContext, key: string): string {
64
+ const callers = callersOfKey(key);
65
+ if (callers.length === 0) return "";
66
+
67
+ const root = key.slice(0, key.indexOf("/"));
68
+ const hops = callers.map((caller, i) => {
69
+ const callSite = [root, ...callers.slice(0, i + 1)].join("/");
70
+ const entity = ctx.entities.get(callSite);
71
+ if (!entity || !isResourceDeclarable(entity)) return caller;
72
+ const props = (entity as TerraformEntity).props as Partial<TerraformEntity["props"]>;
73
+ const file = typeof props.file === "string" ? props.file : "";
74
+ if (file === "") return caller;
75
+ return typeof props.line === "number" ? `${caller} (${file}:${props.line})` : `${caller} (${file})`;
76
+ });
77
+
78
+ return ` Callers: ${[root, ...hops].join(" -> ")}.`;
79
+ }
80
+
81
+ /**
82
+ * Wrap every check so a finding inside a child module carries its
83
+ * {@link callersNote}, whichever rule produced it.
84
+ *
85
+ * Done once, here, rather than in each rule's message: a reader of a finding
86
+ * on `module.cdn`'s bucket needs to know which call reached that code no
87
+ * matter which of the twenty-odd checks reported it, and a rule that forgot
88
+ * to append the chain would be a silent gap. The wrapper is what
89
+ * `terraformPlugin.postSynthChecks()` returns; a rule's own `check()` is
90
+ * unchanged, which is what its unit tests call.
91
+ *
92
+ * A message that already names the chain is left alone, so a rule with
93
+ * something more specific to say about its callers can say it.
94
+ */
95
+ export function withCallersChain(checks: readonly PostSynthCheck[]): PostSynthCheck[] {
96
+ return checks.map((check) => ({
97
+ ...check,
98
+ check(ctx: PostSynthContext) {
99
+ return check.check(ctx).map((diagnostic) => {
100
+ if (typeof diagnostic.entity !== "string") return diagnostic;
101
+ if (diagnostic.message.includes("Callers:")) return diagnostic;
102
+ const note = callersNote(ctx, diagnostic.entity);
103
+ return note === "" ? diagnostic : { ...diagnostic, message: diagnostic.message + note };
104
+ });
105
+ },
106
+ }));
107
+ }
@@ -0,0 +1,263 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { loadFixture } from "./fixtures/load";
3
+ import { tf006 } from "./tf006";
4
+ import { tf007 } from "./tf007";
5
+ import { tf008 } from "./tf008";
6
+ import { tf009 } from "./tf009";
7
+ import { tf010 } from "./tf010";
8
+ import { tf011 } from "./tf011";
9
+ import { tf012 } from "./tf012";
10
+ import { tf013 } from "./tf013";
11
+ import { tf016 } from "./tf016";
12
+ import { tf017 } from "./tf017";
13
+ import { tf018 } from "./tf018";
14
+ import { tf019 } from "./tf019";
15
+ import { tf022 } from "./tf022";
16
+
17
+ /**
18
+ * The single-block rules (#2110): TF006 to TF013, TF016 to TF019 and TF022,
19
+ * every one of them reading one entity of one type. Each rule's headline pair
20
+ * runs through `fixtures/<id>/positive.tf` and `negative.tf` and the shared
21
+ * loader, so the HCL a test asserts on is the HCL the page shows; the extra
22
+ * cases below each pair are the ones that are about a shape rather than a
23
+ * file, and are written as small fixtures of their own through the same
24
+ * loader rather than as hand-built entity maps.
25
+ *
26
+ * TF001 keeps its own file (./post-synth.test.ts) and TF023 is not a
27
+ * post-synth check at all: it reads the audit's file list, so it is tested in
28
+ * ../audit.test.ts against a real directory.
29
+ */
30
+
31
+ describe("TF006: sensitive variable with a default", () => {
32
+ test("flags a sensitive variable that carries a default", async () => {
33
+ const diags = tf006.check(await loadFixture("TF006", "positive"));
34
+ expect(diags).toHaveLength(1);
35
+ expect(diags[0].checkId).toBe("TF006");
36
+ expect(diags[0].entity).toBe("TF006/var.deploy_token");
37
+ expect(diags[0].severity).toBe("error");
38
+ });
39
+
40
+ test("passes a sensitive variable with no default", async () => {
41
+ expect(tf006.check(await loadFixture("TF006", "negative"))).toHaveLength(0);
42
+ });
43
+
44
+ test("an empty or null default still counts", async () => {
45
+ const diags = tf006.check(await loadFixture("TF006", "positive-empty-default"));
46
+ expect(diags.map((d) => d.entity).sort()).toEqual(["TF006/var.blank_token", "TF006/var.null_token"]);
47
+ });
48
+ });
49
+
50
+ describe("TF007: secret-shaped literal in a variable default or a locals value", () => {
51
+ test("flags a credential-named default and a credential-shaped local", async () => {
52
+ const diags = tf007.check(await loadFixture("TF007", "positive"));
53
+ expect(diags.map((d) => d.entity).sort()).toEqual(["TF007/locals.ci_token", "TF007/var.db_password"]);
54
+ expect(diags.every((d) => d.checkId === "TF007")).toBe(true);
55
+ });
56
+
57
+ test("never repeats the value in the message", async () => {
58
+ const diags = tf007.check(await loadFixture("TF007", "positive"));
59
+ for (const d of diags) {
60
+ expect(d.message).not.toContain("hunter2-prod-db");
61
+ expect(d.message).not.toContain("ghp_");
62
+ }
63
+ });
64
+
65
+ test("passes a sensitive variable with no default and a local that references one", async () => {
66
+ expect(tf007.check(await loadFixture("TF007", "negative"))).toHaveLength(0);
67
+ });
68
+ });
69
+
70
+ describe("TF008: hardcoded credentials in a provider block", () => {
71
+ test("flags both literal credentials, whatever the provider is", async () => {
72
+ const diags = tf008.check(await loadFixture("TF008", "positive"));
73
+ expect(diags.map((d) => d.entity).sort()).toEqual([
74
+ "TF008/provider.aws.access_key",
75
+ "TF008/provider.aws.secret_key",
76
+ ]);
77
+ });
78
+
79
+ test("passes a provider with no credential and one pointed at a key file", async () => {
80
+ expect(tf008.check(await loadFixture("TF008", "negative"))).toHaveLength(0);
81
+ });
82
+
83
+ test("reads attribute names, not provider types, so a non-AWS provider is covered", async () => {
84
+ const diags = tf008.check(await loadFixture("TF008", "positive-other-provider"));
85
+ expect(diags).toHaveLength(2);
86
+ expect(diags.map((d) => d.entity).sort()).toEqual([
87
+ "TF008/provider.postgresql.password",
88
+ "TF008/provider.vault.token",
89
+ ]);
90
+ });
91
+
92
+ test("a credential passed as a variable is the fix, not a finding", async () => {
93
+ expect(tf008.check(await loadFixture("TF008", "negative-variable"))).toHaveLength(0);
94
+ });
95
+ });
96
+
97
+ describe("TF009: credential-named variable not marked sensitive", () => {
98
+ test("flags a password variable with no sensitive flag", async () => {
99
+ const diags = tf009.check(await loadFixture("TF009", "positive"));
100
+ expect(diags).toHaveLength(1);
101
+ expect(diags[0].entity).toBe("TF009/var.db_password");
102
+ });
103
+
104
+ test("passes a sensitive one, and an ARN that only points at a secret", async () => {
105
+ expect(tf009.check(await loadFixture("TF009", "negative"))).toHaveLength(0);
106
+ });
107
+ });
108
+
109
+ describe("TF010: variable without a type", () => {
110
+ test("flags an untyped variable", async () => {
111
+ const diags = tf010.check(await loadFixture("TF010", "positive"));
112
+ expect(diags).toHaveLength(1);
113
+ expect(diags[0].entity).toBe("TF010/var.instance_count");
114
+ expect(diags[0].severity).toBe("info");
115
+ });
116
+
117
+ test("passes a typed one", async () => {
118
+ expect(tf010.check(await loadFixture("TF010", "negative"))).toHaveLength(0);
119
+ });
120
+ });
121
+
122
+ describe("TF011: variable without a description", () => {
123
+ test("flags an undocumented variable", async () => {
124
+ const diags = tf011.check(await loadFixture("TF011", "positive"));
125
+ expect(diags).toHaveLength(1);
126
+ expect(diags[0].entity).toBe("TF011/var.region");
127
+ });
128
+
129
+ test("passes a documented one", async () => {
130
+ expect(tf011.check(await loadFixture("TF011", "negative"))).toHaveLength(0);
131
+ });
132
+
133
+ test("a blank description is no description", async () => {
134
+ expect(tf011.check(await loadFixture("TF011", "positive-blank"))).toHaveLength(1);
135
+ });
136
+ });
137
+
138
+ describe("TF012: output without a description", () => {
139
+ test("flags an undocumented output", async () => {
140
+ const diags = tf012.check(await loadFixture("TF012", "positive"));
141
+ expect(diags).toHaveLength(1);
142
+ expect(diags[0].entity).toBe("TF012/output.bucket_name");
143
+ });
144
+
145
+ test("passes a documented one", async () => {
146
+ expect(tf012.check(await loadFixture("TF012", "negative"))).toHaveLength(0);
147
+ });
148
+ });
149
+
150
+ describe("TF013: ignore_changes = all", () => {
151
+ test("flags a lifecycle block that ignores everything", async () => {
152
+ const diags = tf013.check(await loadFixture("TF013", "positive"));
153
+ expect(diags).toHaveLength(1);
154
+ expect(diags[0].entity).toBe("TF013/aws_instance.web");
155
+ expect(diags[0].severity).toBe("warning");
156
+ });
157
+
158
+ test("passes a named list of attributes", async () => {
159
+ expect(tf013.check(await loadFixture("TF013", "negative"))).toHaveLength(0);
160
+ });
161
+
162
+ test("a data source's lifecycle block counts too", async () => {
163
+ const diags = tf013.check(await loadFixture("TF013", "positive-data"));
164
+ expect(diags.map((d) => d.entity)).toEqual(["TF013/data.aws_ami.base"]);
165
+ });
166
+ });
167
+
168
+ describe("TF016: interpolation-only attribute values", () => {
169
+ test("flags each quoted interpolation, with its file and line", async () => {
170
+ const diags = tf016.check(await loadFixture("TF016", "positive"));
171
+ expect(diags).toHaveLength(2);
172
+ expect(diags.map((d) => d.entity)).toEqual(["TF016/positive.tf:7", "TF016/positive.tf:12"]);
173
+ expect(diags[0].message).toContain("bucket");
174
+ });
175
+
176
+ test("passes bare references, which the parse cannot tell apart from quoted ones", async () => {
177
+ // The point of reading the source: `bucket = var.assets_bucket` parses to
178
+ // exactly the same `"${var.assets_bucket}"` string as the positive fixture.
179
+ expect(tf016.check(await loadFixture("TF016", "negative"))).toHaveLength(0);
180
+ });
181
+
182
+ test("passes a template that carries literal text as well", async () => {
183
+ expect(tf016.check(await loadFixture("TF016", "negative-template"))).toHaveLength(0);
184
+ });
185
+
186
+ test("scans a file once however many blocks it holds", async () => {
187
+ const diags = tf016.check(await loadFixture("TF016", "positive-shared-file"));
188
+ expect(diags).toHaveLength(1);
189
+ });
190
+ });
191
+
192
+ describe("TF017: depends_on on a module block", () => {
193
+ test("flags the module call", async () => {
194
+ const diags = tf017.check(await loadFixture("TF017", "positive"));
195
+ expect(diags).toHaveLength(1);
196
+ expect(diags[0].entity).toBe("TF017/module.cdn");
197
+ });
198
+
199
+ test("passes a module wired through its inputs", async () => {
200
+ expect(tf017.check(await loadFixture("TF017", "negative"))).toHaveLength(0);
201
+ });
202
+ });
203
+
204
+ describe("TF018: output of a whole resource", () => {
205
+ test("flags an output that returns the resource itself", async () => {
206
+ const diags = tf018.check(await loadFixture("TF018", "positive"));
207
+ expect(diags).toHaveLength(1);
208
+ expect(diags[0].entity).toBe("TF018/output.web_instance");
209
+ expect(diags[0].message).toContain("aws_instance.web.id");
210
+ });
211
+
212
+ test("passes an attribute, and a whole module (its outputs are already curated)", async () => {
213
+ expect(tf018.check(await loadFixture("TF018", "negative"))).toHaveLength(0);
214
+ });
215
+
216
+ test("a whole data source counts, a data source attribute does not", async () => {
217
+ const diags = tf018.check(await loadFixture("TF018", "positive-data"));
218
+ expect(diags.map((d) => d.entity)).toEqual(["TF018/output.base_ami"]);
219
+ });
220
+ });
221
+
222
+ describe("TF019: a meta-argument set to its default false", () => {
223
+ test("flags the variable flag and both lifecycle flags", async () => {
224
+ const diags = tf019.check(await loadFixture("TF019", "positive"));
225
+ expect(diags.map((d) => d.entity).sort()).toEqual([
226
+ "TF019/aws_instance.web.create_before_destroy",
227
+ "TF019/aws_instance.web.prevent_destroy",
228
+ "TF019/var.log_level.sensitive",
229
+ ]);
230
+ });
231
+
232
+ test("passes a root with no redundant defaults, and an expression is not a literal false", async () => {
233
+ expect(tf019.check(await loadFixture("TF019", "negative"))).toHaveLength(0);
234
+ });
235
+
236
+ test("an output's ephemeral flag counts too", async () => {
237
+ const diags = tf019.check(await loadFixture("TF019", "positive-output"));
238
+ expect(diags.map((d) => d.entity)).toEqual(["TF019/output.endpoint.ephemeral"]);
239
+ });
240
+ });
241
+
242
+ describe("TF022: credential-named resource attribute holding a literal", () => {
243
+ test("flags the plaintext password", async () => {
244
+ const diags = tf022.check(await loadFixture("TF022", "positive"));
245
+ expect(diags).toHaveLength(1);
246
+ expect(diags[0].entity).toBe("TF022/aws_db_instance.app.password");
247
+ expect(diags[0].message).not.toContain("Pr0dDbP4ssw0rd");
248
+ expect(diags[0].message).toContain("reference");
249
+ });
250
+
251
+ test("passes a password read from a data source", async () => {
252
+ expect(tf022.check(await loadFixture("TF022", "negative"))).toHaveLength(0);
253
+ });
254
+
255
+ test("descends into a nested block", async () => {
256
+ const diags = tf022.check(await loadFixture("TF022", "positive-nested"));
257
+ expect(diags.map((d) => d.entity)).toEqual(["TF022/aws_instance.web.connection.password"]);
258
+ });
259
+
260
+ test("a locator attribute is the remediation, not the finding", async () => {
261
+ expect(tf022.check(await loadFixture("TF022", "negative-locator"))).toHaveLength(0);
262
+ });
263
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * End-to-end suppression (chant #2111): real `.tf` text, through
3
+ * `blocksToEntities` (the exact parse `chant build`'s `buildRoots()` and
4
+ * `chant audit`'s `auditEntities()` both call), into TF001, then through
5
+ * `applyInlineSuppressions`. TF001 is the issue's named test subject because
6
+ * it is the one rule with nothing to anchor a finding to but the entity that
7
+ * is missing something, and proving both `# chant-ignore` and
8
+ * `# chant-ignore-block` reach it validates the block-anchored form for
9
+ * exactly the case it exists for.
10
+ */
11
+ import { describe, test, expect } from "vitest";
12
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
13
+ import { applyInlineSuppressions, SUPPRESSION_EXPIRED_ID, SUPPRESSION_MISPLACED_FILE_ID, SUPPRESSION_UNIGNORABLE_ID } from "@intentius/chant/lint/suppressions";
14
+ import type { RuleConfig } from "@intentius/chant/lint/rule";
15
+ import { tf001 } from "./tf001";
16
+ import { blocksToEntities } from "../../hcl/parse";
17
+
18
+ async function ctxFor(source: string): Promise<PostSynthContext> {
19
+ const entities = await blocksToEntities([{ name: "main.tf", source }], "app");
20
+ return { outputs: new Map(), entities } as unknown as PostSynthContext;
21
+ }
22
+
23
+ /** A `terraform` block with no backend: TF001's positive case, undecorated. */
24
+ const BACKENDLESS = `terraform {\n required_version = ">= 1.5.0"\n}\n`;
25
+
26
+ describe("TF001 through inline suppression", () => {
27
+ test("chant-ignore-block suppresses TF001 on the terraform block", async () => {
28
+ const ctx = await ctxFor(`# chant-ignore-block: TF001\n${BACKENDLESS}`);
29
+ const diags = tf001.check(ctx);
30
+ expect(diags).toHaveLength(1);
31
+ const { diagnostics, suppressed } = applyInlineSuppressions(diags, ctx.entities);
32
+ expect(diagnostics).toHaveLength(0);
33
+ expect(suppressed).toHaveLength(1);
34
+ });
35
+
36
+ test("a plain chant-ignore on the same block also suppresses TF001", async () => {
37
+ const ctx = await ctxFor(`# chant-ignore: TF001\n${BACKENDLESS}`);
38
+ const diags = tf001.check(ctx);
39
+ const { diagnostics, suppressed } = applyInlineSuppressions(diags, ctx.entities);
40
+ expect(diagnostics).toHaveLength(0);
41
+ expect(suppressed).toHaveLength(1);
42
+ });
43
+
44
+ test('"all" suppresses TF001 too, not just an explicit id', async () => {
45
+ const ctx = await ctxFor(`# chant-ignore-block: all\n${BACKENDLESS}`);
46
+ const diags = tf001.check(ctx);
47
+ const { diagnostics, suppressed } = applyInlineSuppressions(diags, ctx.entities);
48
+ expect(diagnostics).toHaveLength(0);
49
+ expect(suppressed).toHaveLength(1);
50
+ });
51
+
52
+ test("a directive naming a different rule id does not suppress TF001", async () => {
53
+ const ctx = await ctxFor(`# chant-ignore-block: TF010\n${BACKENDLESS}`);
54
+ const diags = tf001.check(ctx);
55
+ const { diagnostics, suppressed } = applyInlineSuppressions(diags, ctx.entities);
56
+ expect(diagnostics).toHaveLength(1);
57
+ expect(suppressed).toHaveLength(0);
58
+ });
59
+
60
+ test("chant-ignore-file suppresses TF001 for the whole root when placed at the top", async () => {
61
+ const ctx = await ctxFor(`# chant-ignore-file: TF001\n${BACKENDLESS}`);
62
+ const diags = tf001.check(ctx);
63
+ const { diagnostics, suppressed } = applyInlineSuppressions(diags, ctx.entities);
64
+ expect(diagnostics).toHaveLength(0);
65
+ expect(suppressed).toHaveLength(1);
66
+ });
67
+
68
+ test("a chant-ignore-file NOT on the first non-blank line does not suppress, and is reported", async () => {
69
+ const ctx = await ctxFor(`# a header\n# chant-ignore-file: TF001\n${BACKENDLESS}`);
70
+ const diags = tf001.check(ctx);
71
+ const { diagnostics, suppressed, meta } = applyInlineSuppressions(diags, ctx.entities);
72
+ expect(diagnostics).toHaveLength(1); // TF001 still fires: the misplaced directive has no effect
73
+ expect(suppressed).toHaveLength(0);
74
+ expect(meta.map((m) => m.checkId)).toEqual([SUPPRESSION_MISPLACED_FILE_ID]);
75
+ });
76
+
77
+ test("an expired directive no longer suppresses TF001, and is reported once", async () => {
78
+ const ctx = await ctxFor(`# chant-ignore-block: TF001 exp:2020-01-01\n${BACKENDLESS}`);
79
+ const diags = tf001.check(ctx);
80
+ const { diagnostics, suppressed, meta } = applyInlineSuppressions(diags, ctx.entities, undefined, new Date("2026-01-01"));
81
+ expect(diagnostics).toHaveLength(1);
82
+ expect(suppressed).toHaveLength(0);
83
+ expect(meta.map((m) => m.checkId)).toEqual([SUPPRESSION_EXPIRED_ID]);
84
+ });
85
+
86
+ test("a not-yet-expired directive still suppresses TF001", async () => {
87
+ const ctx = await ctxFor(`# chant-ignore-block: TF001 exp:2099-01-01\n${BACKENDLESS}`);
88
+ const diags = tf001.check(ctx);
89
+ const { diagnostics, suppressed, meta } = applyInlineSuppressions(diags, ctx.entities, undefined, new Date("2026-01-01"));
90
+ expect(diagnostics).toHaveLength(0);
91
+ expect(suppressed).toHaveLength(1);
92
+ expect(meta).toHaveLength(0);
93
+ });
94
+
95
+ test("ignorable: false denies an explicit chant-ignore of TF001, and reports the attempt", async () => {
96
+ const rules: Record<string, RuleConfig> = { TF001: ["warning", { ignorable: false }] };
97
+ const ctx = await ctxFor(`# chant-ignore-block: TF001\n${BACKENDLESS}`);
98
+ const diags = tf001.check(ctx);
99
+ const { diagnostics, suppressed, meta } = applyInlineSuppressions(diags, ctx.entities, rules);
100
+ expect(diagnostics).toHaveLength(1); // TF001 still fires
101
+ expect(suppressed).toHaveLength(0);
102
+ expect(meta.map((m) => m.checkId)).toEqual([SUPPRESSION_UNIGNORABLE_ID]);
103
+ });
104
+
105
+ test("TF001 with a backend block never fires, suppressed or not", async () => {
106
+ const withBackend = `terraform {\n backend "s3" {\n bucket = "tfstate"\n }\n}\n`;
107
+ const ctx = await ctxFor(withBackend);
108
+ expect(tf001.check(ctx)).toHaveLength(0);
109
+ });
110
+ });