@intentius/chant-lexicon-terraform 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +9 -0
  2. package/dist/composites/terraform-adopt-op.d.ts +131 -0
  3. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  4. package/dist/composites/terraform-apply-op.d.ts +70 -17
  5. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  6. package/dist/composites/terraform-watch-op.d.ts +104 -37
  7. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  8. package/dist/config.d.ts +49 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/describe-resources.d.ts +256 -16
  11. package/dist/describe-resources.d.ts.map +1 -1
  12. package/dist/hcl/descend.d.ts +98 -0
  13. package/dist/hcl/descend.d.ts.map +1 -0
  14. package/dist/hcl/marker.d.ts +130 -0
  15. package/dist/hcl/marker.d.ts.map +1 -0
  16. package/dist/hcl/parse.d.ts +156 -9
  17. package/dist/hcl/parse.d.ts.map +1 -1
  18. package/dist/hcl/references.d.ts +76 -0
  19. package/dist/hcl/references.d.ts.map +1 -0
  20. package/dist/hcl/roots.d.ts +22 -3
  21. package/dist/hcl/roots.d.ts.map +1 -1
  22. package/dist/hcl/suppressions.d.ts +54 -0
  23. package/dist/hcl/suppressions.d.ts.map +1 -0
  24. package/dist/hcl/value.d.ts +77 -0
  25. package/dist/hcl/value.d.ts.map +1 -0
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/integrity.json +34 -5
  29. package/dist/lint/audit-catalog.d.ts +3 -2
  30. package/dist/lint/audit-catalog.d.ts.map +1 -1
  31. package/dist/lint/audit-lineage.d.ts +95 -0
  32. package/dist/lint/audit-lineage.d.ts.map +1 -1
  33. package/dist/lint/post-synth/blocks.d.ts +53 -0
  34. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  36. package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/module-source.d.ts +38 -0
  39. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  40. package/dist/lint/post-synth/scope.d.ts +65 -0
  41. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  42. package/dist/lint/post-synth/tf001.d.ts +58 -6
  43. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  44. package/dist/lint/post-synth/tf002.d.ts +32 -0
  45. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  46. package/dist/lint/post-synth/tf003.d.ts +22 -0
  47. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  48. package/dist/lint/post-synth/tf004.d.ts +22 -0
  49. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  50. package/dist/lint/post-synth/tf005.d.ts +28 -0
  51. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf006.d.ts +21 -0
  53. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  54. package/dist/lint/post-synth/tf007.d.ts +21 -0
  55. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  56. package/dist/lint/post-synth/tf008.d.ts +22 -0
  57. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  58. package/dist/lint/post-synth/tf009.d.ts +24 -0
  59. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  60. package/dist/lint/post-synth/tf010.d.ts +19 -0
  61. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  62. package/dist/lint/post-synth/tf011.d.ts +18 -0
  63. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  64. package/dist/lint/post-synth/tf012.d.ts +17 -0
  65. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  66. package/dist/lint/post-synth/tf013.d.ts +21 -0
  67. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  68. package/dist/lint/post-synth/tf014.d.ts +25 -0
  69. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  70. package/dist/lint/post-synth/tf015.d.ts +30 -0
  71. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  72. package/dist/lint/post-synth/tf016.d.ts +30 -0
  73. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  74. package/dist/lint/post-synth/tf017.d.ts +19 -0
  75. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  76. package/dist/lint/post-synth/tf018.d.ts +20 -0
  77. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  78. package/dist/lint/post-synth/tf019.d.ts +25 -0
  79. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  80. package/dist/lint/post-synth/tf020.d.ts +38 -0
  81. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  82. package/dist/lint/post-synth/tf021.d.ts +41 -0
  83. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  84. package/dist/lint/post-synth/tf022.d.ts +24 -0
  85. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  86. package/dist/lint/post-synth/tf024.d.ts +24 -0
  87. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  88. package/dist/lint/post-synth/tf025.d.ts +29 -0
  89. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  90. package/dist/lint/post-synth/tf026.d.ts +28 -0
  91. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  92. package/dist/lint/post-synth/tf027.d.ts +30 -0
  93. package/dist/lint/post-synth/tf027.d.ts.map +1 -0
  94. package/dist/lint/post-synth/tf028.d.ts +33 -0
  95. package/dist/lint/post-synth/tf028.d.ts.map +1 -0
  96. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  97. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  98. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  99. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  100. package/dist/lint/secret-shape.d.ts +68 -0
  101. package/dist/lint/secret-shape.d.ts.map +1 -0
  102. package/dist/live-ownership.d.ts +55 -0
  103. package/dist/live-ownership.d.ts.map +1 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/manifest.json +1 -1
  106. package/dist/okf/index.md +26 -0
  107. package/dist/okf/rules/TF002.md +11 -0
  108. package/dist/okf/rules/TF003.md +11 -0
  109. package/dist/okf/rules/TF004.md +11 -0
  110. package/dist/okf/rules/TF005.md +11 -0
  111. package/dist/okf/rules/TF006.md +11 -0
  112. package/dist/okf/rules/TF007.md +11 -0
  113. package/dist/okf/rules/TF008.md +11 -0
  114. package/dist/okf/rules/TF009.md +11 -0
  115. package/dist/okf/rules/TF010.md +11 -0
  116. package/dist/okf/rules/TF011.md +11 -0
  117. package/dist/okf/rules/TF012.md +11 -0
  118. package/dist/okf/rules/TF013.md +11 -0
  119. package/dist/okf/rules/TF014.md +11 -0
  120. package/dist/okf/rules/TF015.md +11 -0
  121. package/dist/okf/rules/TF016.md +11 -0
  122. package/dist/okf/rules/TF017.md +11 -0
  123. package/dist/okf/rules/TF018.md +11 -0
  124. package/dist/okf/rules/TF019.md +11 -0
  125. package/dist/okf/rules/TF020.md +11 -0
  126. package/dist/okf/rules/TF021.md +11 -0
  127. package/dist/okf/rules/TF022.md +11 -0
  128. package/dist/okf/rules/TF024.md +11 -0
  129. package/dist/okf/rules/TF025.md +11 -0
  130. package/dist/okf/rules/TF026.md +11 -0
  131. package/dist/okf/rules/TF027.md +11 -0
  132. package/dist/okf/rules/TF028.md +11 -0
  133. package/dist/op/activities/index.d.ts +13 -6
  134. package/dist/op/activities/index.d.ts.map +1 -1
  135. package/dist/op/activities/live-detect.d.ts +51 -0
  136. package/dist/op/activities/live-detect.d.ts.map +1 -0
  137. package/dist/op/activities/terraform.d.ts +466 -17
  138. package/dist/op/activities/terraform.d.ts.map +1 -1
  139. package/dist/op/activity-contracts.d.ts +148 -0
  140. package/dist/op/activity-contracts.d.ts.map +1 -0
  141. package/dist/op/adoption.d.ts +118 -0
  142. package/dist/op/adoption.d.ts.map +1 -0
  143. package/dist/op/builders.d.ts +48 -6
  144. package/dist/op/builders.d.ts.map +1 -1
  145. package/dist/op/resolve-root-mode.d.ts +59 -0
  146. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  147. package/dist/plugin.d.ts.map +1 -1
  148. package/dist/rules/blocks.ts +109 -0
  149. package/dist/rules/module-source.ts +121 -0
  150. package/dist/rules/plan-before-apply.ts +11 -0
  151. package/dist/rules/scope.ts +107 -0
  152. package/dist/rules/tf001.ts +98 -18
  153. package/dist/rules/tf002.ts +151 -0
  154. package/dist/rules/tf003.ts +71 -0
  155. package/dist/rules/tf004.ts +63 -0
  156. package/dist/rules/tf005.ts +76 -0
  157. package/dist/rules/tf006.ts +53 -0
  158. package/dist/rules/tf007.ts +78 -0
  159. package/dist/rules/tf008.ts +75 -0
  160. package/dist/rules/tf009.ts +59 -0
  161. package/dist/rules/tf010.ts +51 -0
  162. package/dist/rules/tf011.ts +49 -0
  163. package/dist/rules/tf012.ts +47 -0
  164. package/dist/rules/tf013.ts +63 -0
  165. package/dist/rules/tf014.ts +72 -0
  166. package/dist/rules/tf015.ts +72 -0
  167. package/dist/rules/tf016.ts +75 -0
  168. package/dist/rules/tf017.ts +50 -0
  169. package/dist/rules/tf018.ts +72 -0
  170. package/dist/rules/tf019.ts +72 -0
  171. package/dist/rules/tf020.ts +123 -0
  172. package/dist/rules/tf021.ts +126 -0
  173. package/dist/rules/tf022.ts +61 -0
  174. package/dist/rules/tf024.ts +70 -0
  175. package/dist/rules/tf025.ts +101 -0
  176. package/dist/rules/tf026.ts +84 -0
  177. package/dist/rules/tf027.ts +74 -0
  178. package/dist/rules/tf028.ts +96 -0
  179. package/dist/skills/chant-terraform.md +1 -1
  180. package/dist/validate.d.ts.map +1 -1
  181. package/package.json +7 -2
  182. package/src/__fixtures__/ACCEPTANCE.md +181 -0
  183. package/src/__fixtures__/live/main.tf +28 -0
  184. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  185. package/src/__fixtures__/live-adopt/main.tf +21 -0
  186. package/src/__fixtures__/live-estate/README.md +58 -0
  187. package/src/__fixtures__/live-estate/compute.tf +10 -0
  188. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  189. package/src/__fixtures__/live-estate/logs.tf +8 -0
  190. package/src/__fixtures__/live-estate/network.tf +24 -0
  191. package/src/__fixtures__/live-estate/rule.tf +11 -0
  192. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  193. package/src/__fixtures__/live-estate/versions.tf +31 -0
  194. package/src/__fixtures__/live-ls.json +98 -0
  195. package/src/__fixtures__/live-plan.json +285 -0
  196. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  197. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  198. package/src/__fixtures__/module-tree/README.md +16 -0
  199. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  200. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  201. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  202. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  203. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  204. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  205. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  206. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  207. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  208. package/src/__fixtures__/references/main.tf +56 -0
  209. package/src/__fixtures__/remote-backend/main.tf +34 -0
  210. package/src/__fixtures__/with-backend/main.tf +7 -0
  211. package/src/codegen/lint-rules-page.test.ts +45 -0
  212. package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
  213. package/src/composites/terraform-adopt-op.test.ts +175 -0
  214. package/src/composites/terraform-adopt-op.ts +202 -0
  215. package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
  216. package/src/composites/terraform-apply-op.test.ts +179 -19
  217. package/src/composites/terraform-apply-op.ts +123 -30
  218. package/src/composites/terraform-watch-op.test.ts +169 -29
  219. package/src/composites/terraform-watch-op.ts +183 -75
  220. package/src/config.ts +32 -3
  221. package/src/describe-resources.live.test.ts +598 -0
  222. package/src/describe-resources.test.ts +53 -0
  223. package/src/describe-resources.ts +811 -28
  224. package/src/hcl/descend.test.ts +162 -0
  225. package/src/hcl/descend.ts +273 -0
  226. package/src/hcl/marker.test.ts +147 -0
  227. package/src/hcl/marker.ts +235 -0
  228. package/src/hcl/parse.test.ts +69 -0
  229. package/src/hcl/parse.ts +269 -27
  230. package/src/hcl/references.test.ts +116 -0
  231. package/src/hcl/references.ts +172 -0
  232. package/src/hcl/roots.ts +59 -5
  233. package/src/hcl/suppressions.test.ts +147 -0
  234. package/src/hcl/suppressions.ts +204 -0
  235. package/src/hcl/value.test.ts +73 -0
  236. package/src/hcl/value.ts +106 -0
  237. package/src/index.ts +31 -1
  238. package/src/lint/audit-catalog.ts +288 -3
  239. package/src/lint/audit-lineage.ts +384 -1
  240. package/src/lint/audit.test.ts +210 -3
  241. package/src/lint/post-synth/blocks.ts +109 -0
  242. package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
  243. package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
  244. package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
  245. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  246. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  247. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  248. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  249. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  250. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  251. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  252. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  253. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  254. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  255. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  256. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  257. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  258. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  259. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  260. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  261. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  262. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  263. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  264. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  265. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  266. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  267. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  268. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  269. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  270. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  271. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  272. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  273. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  274. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  275. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  276. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  277. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  278. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  279. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  280. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  281. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  282. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  283. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  284. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  285. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  286. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  287. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  288. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  289. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  290. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  291. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  292. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  293. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  294. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  295. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  296. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  297. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  298. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  299. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  300. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  301. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  302. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  303. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  304. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  305. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  306. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  307. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  309. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  310. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  311. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  312. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  313. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  314. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  315. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  316. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  317. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  318. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  319. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  320. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  321. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  322. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  323. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  324. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  325. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  326. package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
  327. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
  328. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
  329. package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
  330. package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
  331. package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
  332. package/src/lint/post-synth/fixtures/load.ts +108 -0
  333. package/src/lint/post-synth/index.ts +52 -0
  334. package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
  335. package/src/lint/post-synth/module-rules.test.ts +153 -0
  336. package/src/lint/post-synth/module-source.test.ts +75 -0
  337. package/src/lint/post-synth/module-source.ts +121 -0
  338. package/src/lint/post-synth/post-synth.test.ts +73 -16
  339. package/src/lint/post-synth/scope.ts +107 -0
  340. package/src/lint/post-synth/single-block.test.ts +263 -0
  341. package/src/lint/post-synth/suppression.test.ts +110 -0
  342. package/src/lint/post-synth/tf001.ts +98 -18
  343. package/src/lint/post-synth/tf002.test.ts +93 -0
  344. package/src/lint/post-synth/tf002.ts +151 -0
  345. package/src/lint/post-synth/tf003.test.ts +75 -0
  346. package/src/lint/post-synth/tf003.ts +71 -0
  347. package/src/lint/post-synth/tf004.test.ts +47 -0
  348. package/src/lint/post-synth/tf004.ts +63 -0
  349. package/src/lint/post-synth/tf005.test.ts +57 -0
  350. package/src/lint/post-synth/tf005.ts +76 -0
  351. package/src/lint/post-synth/tf006.ts +53 -0
  352. package/src/lint/post-synth/tf007.ts +78 -0
  353. package/src/lint/post-synth/tf008.ts +75 -0
  354. package/src/lint/post-synth/tf009.ts +59 -0
  355. package/src/lint/post-synth/tf010.ts +51 -0
  356. package/src/lint/post-synth/tf011.ts +49 -0
  357. package/src/lint/post-synth/tf012.ts +47 -0
  358. package/src/lint/post-synth/tf013.ts +63 -0
  359. package/src/lint/post-synth/tf014.ts +72 -0
  360. package/src/lint/post-synth/tf015.ts +72 -0
  361. package/src/lint/post-synth/tf016.ts +75 -0
  362. package/src/lint/post-synth/tf017.ts +50 -0
  363. package/src/lint/post-synth/tf018.ts +72 -0
  364. package/src/lint/post-synth/tf019.ts +72 -0
  365. package/src/lint/post-synth/tf020.ts +123 -0
  366. package/src/lint/post-synth/tf021.ts +126 -0
  367. package/src/lint/post-synth/tf022.ts +61 -0
  368. package/src/lint/post-synth/tf024.test.ts +42 -0
  369. package/src/lint/post-synth/tf024.ts +70 -0
  370. package/src/lint/post-synth/tf025.test.ts +46 -0
  371. package/src/lint/post-synth/tf025.ts +101 -0
  372. package/src/lint/post-synth/tf026.test.ts +56 -0
  373. package/src/lint/post-synth/tf026.ts +84 -0
  374. package/src/lint/post-synth/tf027.test.ts +62 -0
  375. package/src/lint/post-synth/tf027.ts +74 -0
  376. package/src/lint/post-synth/tf028.test.ts +92 -0
  377. package/src/lint/post-synth/tf028.ts +96 -0
  378. package/src/lint/presets.test.ts +72 -0
  379. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  380. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  381. package/src/lint/rules/fixtures/load.ts +53 -0
  382. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  383. package/src/lint/rules/plan-before-apply.ts +11 -0
  384. package/src/lint/secret-shape.test.ts +150 -0
  385. package/src/lint/secret-shape.ts +159 -0
  386. package/src/live-ownership.ts +60 -0
  387. package/src/lsp/completions.test.ts +3 -2
  388. package/src/lsp/option-keys.ts +12 -6
  389. package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
  390. package/src/op/activities/choudoufu.test.ts +725 -0
  391. package/src/op/activities/index.ts +40 -4
  392. package/src/op/activities/live-detect.test.ts +143 -0
  393. package/src/op/activities/live-detect.ts +162 -0
  394. package/src/op/activities/registry.test.ts +11 -3
  395. package/src/op/activities/terraform.test.ts +7 -3
  396. package/src/op/activities/terraform.ts +861 -69
  397. package/src/op/activity-contracts.test.ts +147 -0
  398. package/src/op/activity-contracts.ts +178 -0
  399. package/src/op/adoption.test.ts +170 -0
  400. package/src/op/adoption.ts +241 -0
  401. package/src/op/builders.test.ts +60 -5
  402. package/src/op/builders.ts +95 -5
  403. package/src/op/resolve-root-mode.test.ts +85 -0
  404. package/src/op/resolve-root-mode.ts +86 -0
  405. package/src/plugin.test.ts +106 -0
  406. package/src/plugin.ts +186 -29
  407. package/src/skills/chant-terraform.md +1 -1
  408. package/src/validate.ts +10 -1
@@ -0,0 +1,92 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { describe, expect, test } from "vitest";
4
+ import type { Declarable } from "@intentius/chant/declarable";
5
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
6
+ import { tf028 } from "./tf028";
7
+ import { parseTerraformRootDir } from "../../hcl/parse";
8
+ import { TerraformWatchOp, type TerraformWatchOpConfig } from "../../composites/terraform-watch-op";
9
+ import { TerraformApplyOp } from "../../composites/terraform-apply-op";
10
+
11
+ const fixtures = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "TF028");
12
+
13
+ /** The entities of one fixture root, parsed the way `buildRoots` parses a configured root. */
14
+ async function rootEntities(fixture: string, root: string, binary = "choudoufu"): Promise<Map<string, Declarable>> {
15
+ return parseTerraformRootDir(join(fixtures, fixture), root, undefined, { binary });
16
+ }
17
+
18
+ /** A `Chant::Op` entity from the real composite, keyed the way a discovered Op is. */
19
+ function watchOp(config: TerraformWatchOpConfig): [string, Declarable] {
20
+ const { op } = TerraformWatchOp(config);
21
+ return [config.name, op as unknown as Declarable];
22
+ }
23
+
24
+ function ctxOf(...maps: Array<Map<string, Declarable> | [string, Declarable]>): PostSynthContext {
25
+ const entities = new Map<string, Declarable>();
26
+ for (const m of maps) {
27
+ if (Array.isArray(m)) entities.set(m[0], m[1]);
28
+ else for (const [k, v] of m) entities.set(k, v);
29
+ }
30
+ return { outputs: new Map(), entities } as unknown as PostSynthContext;
31
+ }
32
+
33
+ describe("TF028: a live root planned by a stock terraformPlan step", () => {
34
+ test("fires on a TerraformWatchOp built without live: true", async () => {
35
+ const diags = tf028.check(
36
+ ctxOf(await rootEntities("live", "estate"), watchOp({ name: "estate-watch", root: "estate" })),
37
+ );
38
+ expect(diags).toHaveLength(1);
39
+ expect(diags[0].checkId).toBe("TF028");
40
+ expect(diags[0].severity).toBe("error");
41
+ expect(diags[0].lexicon).toBe("terraform");
42
+ expect(diags[0].entity).toBe("estate-watch");
43
+ expect(diags[0].message).toContain('Op "estate-watch"');
44
+ expect(diags[0].message).toContain('root module "estate"');
45
+ expect(diags[0].message).toContain("live: true");
46
+ });
47
+
48
+ test("passes when the same Op is built with live: true", async () => {
49
+ const diags = tf028.check(
50
+ ctxOf(
51
+ await rootEntities("live", "estate"),
52
+ watchOp({ name: "estate-watch", root: "estate", live: true }),
53
+ ),
54
+ );
55
+ expect(diags).toEqual([]);
56
+ });
57
+
58
+ test("passes on a stock root, which is what terraformPlan is for", async () => {
59
+ const diags = tf028.check(
60
+ ctxOf(await rootEntities("stock", "app", "terraform"), watchOp({ name: "app-watch", root: "app" })),
61
+ );
62
+ expect(diags).toEqual([]);
63
+ });
64
+
65
+ test("names the Op that plans the live root, not a sibling Op on another root", async () => {
66
+ const diags = tf028.check(
67
+ ctxOf(
68
+ await rootEntities("live", "estate"),
69
+ await rootEntities("stock", "app", "terraform"),
70
+ watchOp({ name: "estate-watch", root: "estate" }),
71
+ watchOp({ name: "app-watch", root: "app" }),
72
+ ),
73
+ );
74
+ expect(diags).toHaveLength(1);
75
+ expect(diags[0].entity).toBe("estate-watch");
76
+ });
77
+
78
+ test("does not fire on a TerraformApplyOp, whose stock plan step is correct on a live root", async () => {
79
+ const { op } = TerraformApplyOp({ name: "estate-apply", root: "estate" });
80
+ const diags = tf028.check(
81
+ ctxOf(await rootEntities("live", "estate"), ["estate-apply", op as unknown as Declarable]),
82
+ );
83
+ expect(diags).toEqual([]);
84
+ });
85
+
86
+ test("ignores a build with no live root at all", async () => {
87
+ const diags = tf028.check(
88
+ ctxOf(await rootEntities("stock", "estate", "terraform"), watchOp({ name: "estate-watch", root: "estate" })),
89
+ );
90
+ expect(diags).toEqual([]);
91
+ });
92
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * TF028: a live root is watched by a `TerraformWatchOp` built in stock mode.
3
+ *
4
+ * `TerraformWatchOp`'s `live` flag is declared, not detected: a composite is
5
+ * built by `chant build` without reading a root module's `.tf` files, so the
6
+ * Op author says which shape the Plan phase takes and, until #2216, nothing
7
+ * checked the answer against the root. Leaving it off a live root is silent
8
+ * and lossy. `live: true` swaps the Plan step for `choudoufuLivePlan`, whose
9
+ * single read of the live system answers three questions (drift, how many
10
+ * live resources sit at a declared identity carrying no marker, and how many
11
+ * of those an exact content match makes claimable); a stock `terraformPlan`
12
+ * step answers the first alone, so the watch reports drift and stays quiet
13
+ * about every unowned and adoptable resource in the estate.
14
+ *
15
+ * The check runs over the whole built graph because that is where both halves
16
+ * of the question are: the root's mode is stamped on every parsed entity by
17
+ * the build (`props.mode`, #2103), and the Op is a `Chant::Op` entity carrying
18
+ * the composite's own labels. `TerraformWatchOp` itself cross-checks the flag
19
+ * where it can (`../../op/resolve-root-mode.ts`), but that read is a
20
+ * `chant.config.json`-only one, so on a `chant.config.ts` project, which is
21
+ * every project in this repository, this check is what actually fires.
22
+ *
23
+ * Read off the labels the composite stamps rather than off the emitted step,
24
+ * since a stock `terraformPlan -out` step against a live root is correct in
25
+ * `TerraformApplyOp`: choudoufu v0.13.0 accepts `plan -out` and re-plans
26
+ * against the file at apply (#2157), so the apply Op builds one shape for
27
+ * both kinds of root and the step alone says nothing about a mistake.
28
+ *
29
+ * One diagnostic per Op, fired from the Op's entity.
30
+ */
31
+
32
+ import type {
33
+ PostSynthCheck,
34
+ PostSynthContext,
35
+ PostSynthDiagnostic,
36
+ } from "@intentius/chant/lint/post-synth";
37
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
38
+ import { isOpEntity } from "@intentius/chant/op/resource";
39
+
40
+ /** The labels `TerraformWatchOp` stamps on the Op it builds (`../../composites/terraform-watch-op.ts`). */
41
+ interface WatchOpLabels {
42
+ Watch?: unknown;
43
+ TerraformRoot?: unknown;
44
+ TerraformMode?: unknown;
45
+ }
46
+
47
+ /** Every root name the build parsed as live: `terraform.binary: "choudoufu"` plus a declared estate. */
48
+ function liveRootsOf(ctx: PostSynthContext): Set<string> {
49
+ const roots = new Set<string>();
50
+ for (const entity of ctx.entities.values()) {
51
+ if (!isResourceDeclarable(entity)) continue;
52
+ const props = entity.props as { root?: unknown; mode?: unknown };
53
+ if (props.mode !== "live" || typeof props.root !== "string") continue;
54
+ roots.add(props.root);
55
+ }
56
+ return roots;
57
+ }
58
+
59
+ export const tf028: PostSynthCheck = {
60
+ id: "TF028",
61
+ description: "A live root is watched by a TerraformWatchOp built without live: true",
62
+
63
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
64
+ const liveRoots = liveRootsOf(ctx);
65
+ if (liveRoots.size === 0) return [];
66
+
67
+ const diagnostics: PostSynthDiagnostic[] = [];
68
+
69
+ for (const [name, entity] of ctx.entities) {
70
+ if (!isOpEntity(entity)) continue;
71
+ const props = (entity as { props?: Record<string, unknown> }).props;
72
+ if (!props || typeof props.name !== "string") continue;
73
+ const labels = (typeof props.labels === "object" && props.labels !== null
74
+ ? props.labels
75
+ : {}) as WatchOpLabels;
76
+ if (labels.Watch !== "true") continue;
77
+ if (labels.TerraformMode === "live") continue;
78
+ const root = labels.TerraformRoot;
79
+ if (typeof root !== "string" || !liveRoots.has(root)) continue;
80
+
81
+ diagnostics.push({
82
+ checkId: "TF028",
83
+ severity: "error",
84
+ message:
85
+ `Op "${props.name}" watches root module "${root}" in stock mode, but that root runs choudoufu ` +
86
+ "with a declared estate. A stock plan reports drift alone and stays quiet about every unowned " +
87
+ `and adoptable resource in the estate. Set live: true on this TerraformWatchOp, or drop the ` +
88
+ "root's estate if it is not a live root.",
89
+ entity: name,
90
+ lexicon: "terraform",
91
+ });
92
+ }
93
+
94
+ return diagnostics;
95
+ },
96
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `terraformPlugin.lintPresets()` (chant #2113): `recommended`/`all`, and
3
+ * that `lint.presets` + core's `resolvePresetIds`/`applyConfiguredPreset`
4
+ * filter a terraform post-synth finding end to end: a report-only rule is
5
+ * not reported under `recommended` and is reported under `all`.
6
+ *
7
+ * The terraform lexicon ships no report-only post-synth check yet (TF001,
8
+ * TF024, TF025 are all merge-worthy. The report-only rules land in
9
+ * #2109/#2110/#2112, in parallel with this issue), so the "report-only rule
10
+ * excluded under recommended, included under all" half uses a synthetic
11
+ * diagnostic and a preset id set widened with a made-up id alongside the
12
+ * plugin's real `recommended` set, rather than inventing a permanent public
13
+ * TF id this issue doesn't own.
14
+ */
15
+ import { describe, test, expect } from "vitest";
16
+ import type { PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
17
+ import { resolvePresetIds, applyConfiguredPreset } from "@intentius/chant/lint/config";
18
+ import { terraformPlugin } from "../plugin";
19
+
20
+ describe("terraformPlugin.lintPresets()", () => {
21
+ const presets = terraformPlugin.lintPresets?.();
22
+
23
+ test("is defined and exports recommended + all", () => {
24
+ expect(presets).toBeDefined();
25
+ expect(presets?.recommended).toBeDefined();
26
+ expect(presets?.all).toBeDefined();
27
+ });
28
+
29
+ test("recommended is exactly the catalog's merge-worthy ids, and all is exactly every catalog id (derived, not hand-kept)", async () => {
30
+ const { terraformAuditCatalog } = await import("./audit-catalog");
31
+ const expectedRecommended = Object.values(terraformAuditCatalog)
32
+ .filter((m) => m.tier === "merge-worthy")
33
+ .map((m) => m.id)
34
+ .sort();
35
+ expect(presets?.recommended.slice().sort()).toEqual(expectedRecommended);
36
+ expect(presets?.all.slice().sort()).toEqual(Object.keys(terraformAuditCatalog).sort());
37
+ });
38
+
39
+ test("all is a superset of recommended", () => {
40
+ for (const id of presets?.recommended ?? []) {
41
+ expect(presets?.all).toContain(id);
42
+ }
43
+ });
44
+ });
45
+
46
+ describe("lint.presets end to end: a report-only rule is excluded under recommended, included under all (chant #2113)", () => {
47
+ const recommendedIds = terraformPlugin.lintPresets?.().recommended ?? [];
48
+ // Widened with a made-up report-only id, not a real TF id this issue owns,
49
+ // see this file's header comment.
50
+ const allIds = [...(terraformPlugin.lintPresets?.().all ?? []), "TFZZZ"];
51
+
52
+ function diags(): PostSynthDiagnostic[] {
53
+ return [
54
+ { checkId: "TF001", severity: "warning", message: "a real merge-worthy TF001 finding", lexicon: "terraform" },
55
+ { checkId: "TFZZZ", severity: "info", message: "a hypothetical report-only finding", lexicon: "terraform" },
56
+ ];
57
+ }
58
+
59
+ test("under `recommended`, the report-only finding is not reported (merge-worthy TF001 still is)", () => {
60
+ const presetIds = resolvePresetIds({ recommended: recommendedIds, all: allIds }, "recommended");
61
+ const result = applyConfiguredPreset(diags(), presetIds, undefined);
62
+ expect(result.diagnostics.map((d) => d.checkId)).toEqual(["TF001"]);
63
+ expect(result.suppressed.map((d) => d.checkId)).toEqual(["TFZZZ"]);
64
+ });
65
+
66
+ test("under `all`, the report-only finding IS reported", () => {
67
+ const presetIds = resolvePresetIds({ recommended: recommendedIds, all: allIds }, "all");
68
+ const result = applyConfiguredPreset(diags(), presetIds, undefined);
69
+ expect(result.diagnostics.map((d) => d.checkId).sort()).toEqual(["TF001", "TFZZZ"]);
70
+ expect(result.suppressed).toEqual([]);
71
+ });
72
+ });
@@ -0,0 +1,2 @@
1
+ const plan = terraformPlan("app", { id: "plan" });
2
+ const apply = terraformApply("app", { planFile: stepOutput(plan, "planFile") });
@@ -0,0 +1,2 @@
1
+ const plan = terraformPlan("app", { id: "plan" });
2
+ const apply = terraformApply("app", { planFile: "/tmp/plan.out" });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared lint-rule fixture loader, the `lint/rules/` sibling of
3
+ * `../../post-synth/fixtures/load.ts`.
4
+ *
5
+ * A post-synth check's fixtures are `.tf`, a real Terraform root. A lint
6
+ * rule's fixtures are Op-shaped source, the kind `chant lint` runs a rule's
7
+ * `check(context)` against directly. TF101's live under
8
+ * `fixtures/TF101/positive-op.ts` (triggers) and `negative-op.ts` (passes),
9
+ * deliberately unimported and unresolved at the identifier level: TF101
10
+ * itself matches purely on callee name (see `plan-before-apply.ts`'s doc
11
+ * comment), so a fixture that only needs to parse, not typecheck or run as a
12
+ * standalone program, stays honest about what the rule actually reads.
13
+ *
14
+ * On disk these are named `<name>-op.ts`, not `<name>.op.ts`: a real
15
+ * `*.op.ts` file is exactly what `packages/core/src/op/discover.ts`'s
16
+ * `discoverOps()` walks the whole repository (down from the git root, absent
17
+ * a `chant.config.*`) to find and `import()`, and it does that unconditionally
18
+ * in tests like `generate-pipeline.test.ts` that exercise real discovery. A
19
+ * fixture named `positive.op.ts` would be swept into that scan, imported,
20
+ * and fail at the top-level `const plan = terraformPlan(...)` reference,
21
+ * turning into a discovery error that breaks an unrelated, already-passing
22
+ * test. `loadRuleFixture` still hands the rule a logical `.op.ts` filename
23
+ * (below) so parsing and diagnostics see the file the way a real one would
24
+ * appear; only the on-disk name dodges the scan. Both physical files are
25
+ * also excluded from every tsconfig that would otherwise try to compile them
26
+ * as real source (root `tsconfig.json`, `tsconfig.typecheck.json`, and this
27
+ * lexicon's `tsconfig.build.json`).
28
+ */
29
+ import { readFileSync } from "node:fs";
30
+ import { dirname, join } from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+ import * as ts from "typescript";
33
+ import type { LintContext } from "@intentius/chant/lint/rule";
34
+
35
+ const FIXTURES_DIR = dirname(fileURLToPath(import.meta.url));
36
+
37
+ /**
38
+ * Parse `fixtures/<ruleId>/<name>-op.ts` into a {@link LintContext} whose
39
+ * `filePath` is the logical `<name>.op.ts` a real Op file would carry.
40
+ *
41
+ * Three lines per fixture in a test:
42
+ * ```ts
43
+ * const diags = planBeforeApplyRule.check(loadRuleFixture("TF101", "positive"));
44
+ * expect(diags).toHaveLength(1);
45
+ * ```
46
+ */
47
+ export function loadRuleFixture(ruleId: string, name: string): LintContext {
48
+ const onDisk = `${name}-op.ts`;
49
+ const logical = `${name}.op.ts`;
50
+ const code = readFileSync(join(FIXTURES_DIR, ruleId, onDisk), "utf-8");
51
+ const sourceFile = ts.createSourceFile(logical, code, ts.ScriptTarget.Latest, true);
52
+ return { sourceFile, entities: [], filePath: logical };
53
+ }
@@ -1,7 +1,11 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, test } from "vitest";
2
5
  import * as ts from "typescript";
3
6
  import type { LintContext } from "@intentius/chant/lint/rule";
4
7
  import { planBeforeApplyRule } from "./plan-before-apply";
8
+ import { loadRuleFixture } from "./fixtures/load";
5
9
 
6
10
  function createContext(code: string, fileName = "infra.op.ts"): LintContext {
7
11
  const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
@@ -10,18 +14,18 @@ function createContext(code: string, fileName = "infra.op.ts"): LintContext {
10
14
 
11
15
  describe("TF101: plan-before-apply", () => {
12
16
  test("flags a literal plan path", () => {
13
- const diags = planBeforeApplyRule.check(
14
- createContext(`
15
- const plan = terraformPlan({ root: "app" });
16
- const apply = terraformApply({ planFile: "/tmp/plan.out" });
17
- `),
18
- );
17
+ const diags = planBeforeApplyRule.check(loadRuleFixture("TF101", "positive"));
19
18
  expect(diags).toHaveLength(1);
20
19
  expect(diags[0].ruleId).toBe("TF101");
21
20
  expect(diags[0].severity).toBe("error");
22
21
  expect(diags[0].message).toContain("literal path");
23
22
  });
24
23
 
24
+ test("passes a step-output reference to the preceding plan (fixtures/TF101/negative.op.ts)", () => {
25
+ const diags = planBeforeApplyRule.check(loadRuleFixture("TF101", "negative"));
26
+ expect(diags).toHaveLength(0);
27
+ });
28
+
25
29
  test("flags a template literal plan path", () => {
26
30
  const diags = planBeforeApplyRule.check(
27
31
  createContext(`
@@ -109,3 +113,104 @@ describe("TF101: plan-before-apply", () => {
109
113
  expect(diags).toHaveLength(0);
110
114
  });
111
115
  });
116
+
117
+ describe("TF101: a live root is checked like any other (#2106 follow-up)", () => {
118
+ const dirs: string[] = [];
119
+
120
+ afterEach(() => {
121
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
122
+ });
123
+
124
+ /** A throwaway project with one `terraform.roots` entry, live or stock. */
125
+ function project(opts: { binary?: string; live?: boolean }): string {
126
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf101-mode-"));
127
+ dirs.push(dir);
128
+ mkdirSync(join(dir, "root"), { recursive: true });
129
+ writeFileSync(
130
+ join(dir, "root", "main.tf"),
131
+ opts.live
132
+ ? ["terraform {", " live {", ' estate = "fixture-estate"', " }", "}", ""].join("\n")
133
+ : 'resource "null_resource" "x" {}\n',
134
+ );
135
+ writeFileSync(
136
+ join(dir, "chant.config.json"),
137
+ JSON.stringify({ terraform: { binary: opts.binary ?? "terraform", roots: { app: { dir: "./root" } } } }),
138
+ );
139
+ return dir;
140
+ }
141
+
142
+ /** A `LintContext` whose file lives inside `project`, so a config walk would find it if the rule did one. */
143
+ function contextIn(project: string, code: string): LintContext {
144
+ const filePath = join(project, "ops", "app.op.ts");
145
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
146
+ return { sourceFile, entities: [], filePath };
147
+ }
148
+
149
+ test("fires on a terraformApply call over a live root, where #2106 exempted it", () => {
150
+ const dir = project({ binary: "choudoufu", live: true });
151
+ const diags = planBeforeApplyRule.check(
152
+ contextIn(
153
+ dir,
154
+ `
155
+ const apply = terraformApply("app", { planFile: "/tmp/plan.out" });
156
+ `,
157
+ ),
158
+ );
159
+ expect(diags).toHaveLength(1);
160
+ expect(diags[0].message).toContain("literal path");
161
+ });
162
+
163
+ test("fires on a stock root in the same shape", () => {
164
+ const dir = project({ binary: "terraform", live: false });
165
+ const diags = planBeforeApplyRule.check(
166
+ contextIn(
167
+ dir,
168
+ `
169
+ const apply = terraformApply("app", { planFile: "/tmp/plan.out" });
170
+ `,
171
+ ),
172
+ );
173
+ expect(diags).toHaveLength(1);
174
+ });
175
+
176
+ test("a live root's paired call still passes", () => {
177
+ const dir = project({ binary: "choudoufu", live: true });
178
+ const diags = planBeforeApplyRule.check(
179
+ contextIn(
180
+ dir,
181
+ `
182
+ const plan = terraformPlan("app", { id: "plan" });
183
+ const apply = terraformApply("app", { planFile: plan.out.planFile });
184
+ `,
185
+ ),
186
+ );
187
+ expect(diags).toHaveLength(0);
188
+ });
189
+
190
+ test("the object-literal-only call form is checked the same way", () => {
191
+ const dir = project({ binary: "choudoufu", live: true });
192
+ const diags = planBeforeApplyRule.check(
193
+ contextIn(
194
+ dir,
195
+ `
196
+ const apply = terraformApply({ root: "app", planFile: "/tmp/plan.out" });
197
+ `,
198
+ ),
199
+ );
200
+ expect(diags).toHaveLength(1);
201
+ });
202
+
203
+ test("no project config at all changes nothing: the rule reads none", () => {
204
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf101-mode-"));
205
+ dirs.push(dir);
206
+ const diags = planBeforeApplyRule.check(
207
+ contextIn(
208
+ dir,
209
+ `
210
+ const apply = terraformApply("app", { planFile: "/tmp/plan.out" });
211
+ `,
212
+ ),
213
+ );
214
+ expect(diags).toHaveLength(1);
215
+ });
216
+ });
@@ -27,6 +27,17 @@ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lin
27
27
  * silent — the same deliberately shallow "bail rather than guess" posture
28
28
  * `validateStepOutputRefs` takes, since the runtime brand on a real
29
29
  * `StepOutputRef` is invisible to a source-level AST match.
30
+ *
31
+ * Fires on every `terraformApply` call, live root or stock. #2106 exempted
32
+ * live roots for one release, because choudoufu refused `-out` and
33
+ * `apply <planfile>` and the activity ran a bare `apply -auto-approve` with no
34
+ * plan file to pair. choudoufu v0.13.0 admits both under a `live` block
35
+ * ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878)), and
36
+ * there the pairing matters more than on a stock root, not less: the apply
37
+ * re-plans the live system and compares its fresh plan against the file it was
38
+ * handed, so a plan file spelled out as a literal is an approval for a run
39
+ * nobody can trace back to a plan step. The exemption is gone with the
40
+ * condition that produced it, and this rule reads no project config at all.
30
41
  */
31
42
  export const planBeforeApplyRule: LintRule = {
32
43
  id: "TF101",
@@ -0,0 +1,150 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ isCredentialShapedValue,
4
+ isLiteralString,
5
+ isPlaceholderValue,
6
+ isSecretName,
7
+ secretShapedAssignment,
8
+ } from "./secret-shape";
9
+
10
+ describe("the name heuristic", () => {
11
+ test.each([
12
+ "password",
13
+ "db_password",
14
+ "PASSWORD",
15
+ "passwd",
16
+ "admin_passwd",
17
+ "secret",
18
+ "client_secret",
19
+ "secret_key",
20
+ "token",
21
+ "deploy_token",
22
+ "api_key",
23
+ "openai_api_key",
24
+ "private_key",
25
+ "access_key",
26
+ ])("%s reads like a credential", (name) => {
27
+ expect(isSecretName(name)).toBe(true);
28
+ });
29
+
30
+ test.each([
31
+ "region",
32
+ "instance_count",
33
+ "keystore",
34
+ "tokenizer",
35
+ "passwordless_sudo_enabled",
36
+ "bucket",
37
+ ])("%s does not", (name) => {
38
+ expect(isSecretName(name)).toBe(false);
39
+ });
40
+
41
+ test.each([
42
+ "secret_arn",
43
+ "db_password_file",
44
+ "api_key_id",
45
+ "private_key_path",
46
+ "deploy_token_name",
47
+ ])("%s is a locator, not the credential itself", (name) => {
48
+ expect(isSecretName(name)).toBe(false);
49
+ });
50
+
51
+ test("a locator suffix only excludes when it is the suffix", () => {
52
+ expect(isSecretName("arn_password")).toBe(true);
53
+ expect(isSecretName("file_secret")).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe("the value heuristic", () => {
58
+ test("an AWS access key id is credential-shaped, through core's own detector", () => {
59
+ expect(isCredentialShapedValue("AKIA2E0XYZ4PQRSTUVWX")).toBe(true);
60
+ });
61
+
62
+ test("a GitHub token is, for the same reason", () => {
63
+ expect(isCredentialShapedValue("ghp_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8")).toBe(true);
64
+ });
65
+
66
+ test("a JWT is", () => {
67
+ expect(
68
+ isCredentialShapedValue("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc2NH0.9mAcYbBqp4Ck2XjLwr7QeR1sT3uVwXyZ"),
69
+ ).toBe(true);
70
+ });
71
+
72
+ test("a PEM header is, even without the closing block", () => {
73
+ expect(isCredentialShapedValue("-----BEGIN RSA PRIVATE KEY-----\nMIIEow...")).toBe(true);
74
+ });
75
+
76
+ test("a long high-entropy token is", () => {
77
+ expect(isCredentialShapedValue("8Jd0hVn2XqB7sLtR4mYcE1zPgWfKa5UiQ3oNrTbA")).toBe(true);
78
+ });
79
+
80
+ test.each(["eu-west-1", "t3.micro", "postgres", "app-prod"])("%s is not", (value) => {
81
+ expect(isCredentialShapedValue(value)).toBe(false);
82
+ });
83
+
84
+ test("a model id or image ref is not, through core's identifier-slug filter", () => {
85
+ expect(isCredentialShapedValue("registry.example.com/team/app-server")).toBe(false);
86
+ });
87
+
88
+ test("a value shorter than eight characters is never judged", () => {
89
+ expect(isCredentialShapedValue("aB3$xY")).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe("placeholders", () => {
94
+ test.each(["changeme", "CHANGE_ME", "change-me", "YOUR_API_KEY", "example-secret", "0000000000000000"])(
95
+ "%s is a placeholder",
96
+ (value) => {
97
+ expect(isPlaceholderValue(value)).toBe(true);
98
+ },
99
+ );
100
+
101
+ test("a placeholder is never a finding, whatever the attribute is called", () => {
102
+ expect(secretShapedAssignment("db_password", "CHANGE_ME")).toBeUndefined();
103
+ expect(secretShapedAssignment("api_key", "your-api-key")).toBeUndefined();
104
+ });
105
+ });
106
+
107
+ describe("literals versus references", () => {
108
+ test("an hcl2json expression is not a literal", () => {
109
+ // Both `x = var.y` and `x = "${var.y}"` parse to this string.
110
+ expect(isLiteralString("${var.db_password}")).toBe(false);
111
+ expect(isLiteralString("${data.aws_secretsmanager_secret_version.db.secret_string}")).toBe(false);
112
+ });
113
+
114
+ test("a template with an interpolation in it is not a literal either", () => {
115
+ expect(isLiteralString("prefix-${var.env}-suffix")).toBe(false);
116
+ });
117
+
118
+ test("a plain string is", () => {
119
+ expect(isLiteralString("hunter2-prod-db")).toBe(true);
120
+ });
121
+
122
+ test("a non-string is not", () => {
123
+ expect(isLiteralString(3)).toBe(false);
124
+ expect(isLiteralString(true)).toBe(false);
125
+ expect(isLiteralString(["a"])).toBe(false);
126
+ });
127
+ });
128
+
129
+ describe("secretShapedAssignment", () => {
130
+ test("fires on the name when the value is an unremarkable literal", () => {
131
+ expect(secretShapedAssignment("db_password", "hunter2-prod-db")).toEqual({ reason: "name", length: 15 });
132
+ });
133
+
134
+ test("fires on the value when the name says nothing", () => {
135
+ const shape = secretShapedAssignment("bootstrap", "AKIA2E0XYZ4PQRSTUVWX");
136
+ expect(shape?.reason).toBe("value");
137
+ });
138
+
139
+ test("prefers the value reason when both hold", () => {
140
+ expect(secretShapedAssignment("api_key", "ghp_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8")?.reason).toBe("value");
141
+ });
142
+
143
+ test("declines a reference, which is the fix rather than the finding", () => {
144
+ expect(secretShapedAssignment("db_password", "${var.db_password}")).toBeUndefined();
145
+ });
146
+
147
+ test("declines a short value", () => {
148
+ expect(secretShapedAssignment("password", "abc123")).toBeUndefined();
149
+ });
150
+ });