@intentius/chant-lexicon-terraform 0.101.0 → 0.104.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 (374) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
  2. package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
  3. package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
  4. package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
  5. package/dist/change-set.d.ts +67 -0
  6. package/dist/change-set.d.ts.map +1 -0
  7. package/dist/changed-roots.d.ts +30 -0
  8. package/dist/changed-roots.d.ts.map +1 -0
  9. package/dist/codegen/package.d.ts +1 -1
  10. package/dist/commands.d.ts +26 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/components/capability-plugin.d.ts +54 -0
  13. package/dist/components/capability-plugin.d.ts.map +1 -0
  14. package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
  15. package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
  16. package/dist/config.d.ts +3 -3
  17. package/dist/hcl/descend.d.ts +1 -1
  18. package/dist/hcl/descend.d.ts.map +1 -1
  19. package/dist/hcl/edges.d.ts +2 -2
  20. package/dist/hcl/edges.d.ts.map +1 -1
  21. package/dist/hcl/jsonencode.d.ts +106 -0
  22. package/dist/hcl/jsonencode.d.ts.map +1 -0
  23. package/dist/hcl/marker.d.ts.map +1 -1
  24. package/dist/hcl/parse.d.ts +29 -1
  25. package/dist/hcl/parse.d.ts.map +1 -1
  26. package/dist/hcl/roots.d.ts +1 -1
  27. package/dist/hcl/roots.d.ts.map +1 -1
  28. package/dist/hcl/suppressions.d.ts.map +1 -1
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/integrity.json +22 -4
  32. package/dist/lint/audit-catalog.d.ts.map +1 -1
  33. package/dist/lint/audit-lineage.d.ts +1 -1
  34. package/dist/lint/audit-lineage.d.ts.map +1 -1
  35. package/dist/lint/post-synth/flat-attributes.d.ts +69 -0
  36. package/dist/lint/post-synth/flat-attributes.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 +27 -1
  39. package/dist/lint/post-synth/module-source.d.ts.map +1 -1
  40. package/dist/lint/post-synth/terragrunt.d.ts +37 -0
  41. package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
  42. package/dist/lint/post-synth/tf002.d.ts +3 -1
  43. package/dist/lint/post-synth/tf002.d.ts.map +1 -1
  44. package/dist/lint/post-synth/tf030.d.ts +51 -0
  45. package/dist/lint/post-synth/tf030.d.ts.map +1 -0
  46. package/dist/lint/post-synth/tf031.d.ts +55 -0
  47. package/dist/lint/post-synth/tf031.d.ts.map +1 -0
  48. package/dist/lint/post-synth/tf032.d.ts +71 -0
  49. package/dist/lint/post-synth/tf032.d.ts.map +1 -0
  50. package/dist/lint/post-synth/tf033.d.ts +50 -0
  51. package/dist/lint/post-synth/tf033.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf034.d.ts +25 -0
  53. package/dist/lint/post-synth/tf034.d.ts.map +1 -0
  54. package/dist/lint/post-synth/tf035.d.ts +33 -0
  55. package/dist/lint/post-synth/tf035.d.ts.map +1 -0
  56. package/dist/lint/post-synth/tf036.d.ts +33 -0
  57. package/dist/lint/post-synth/tf036.d.ts.map +1 -0
  58. package/dist/lint/post-synth/tf037.d.ts +31 -0
  59. package/dist/lint/post-synth/tf037.d.ts.map +1 -0
  60. package/dist/lint/post-synth/tf038.d.ts +22 -0
  61. package/dist/lint/post-synth/tf038.d.ts.map +1 -0
  62. package/dist/lint/post-synth/tf039.d.ts +23 -0
  63. package/dist/lint/post-synth/tf039.d.ts.map +1 -0
  64. package/dist/lint/post-synth/tf040.d.ts +30 -0
  65. package/dist/lint/post-synth/tf040.d.ts.map +1 -0
  66. package/dist/lint/post-synth/tf041.d.ts +20 -0
  67. package/dist/lint/post-synth/tf041.d.ts.map +1 -0
  68. package/dist/lint/post-synth/tf042.d.ts +18 -0
  69. package/dist/lint/post-synth/tf042.d.ts.map +1 -0
  70. package/dist/lint/post-synth/tf043.d.ts +17 -0
  71. package/dist/lint/post-synth/tf043.d.ts.map +1 -0
  72. package/dist/lint/post-synth/tf044.d.ts +17 -0
  73. package/dist/lint/post-synth/tf044.d.ts.map +1 -0
  74. package/dist/lint/post-synth/tf045.d.ts +22 -0
  75. package/dist/lint/post-synth/tf045.d.ts.map +1 -0
  76. package/dist/manifest.json +1 -1
  77. package/dist/okf/index.md +16 -0
  78. package/dist/okf/rules/TF030.md +11 -0
  79. package/dist/okf/rules/TF031.md +11 -0
  80. package/dist/okf/rules/TF032.md +11 -0
  81. package/dist/okf/rules/TF033.md +11 -0
  82. package/dist/okf/rules/TF034.md +11 -0
  83. package/dist/okf/rules/TF035.md +11 -0
  84. package/dist/okf/rules/TF036.md +11 -0
  85. package/dist/okf/rules/TF037.md +11 -0
  86. package/dist/okf/rules/TF038.md +11 -0
  87. package/dist/okf/rules/TF039.md +11 -0
  88. package/dist/okf/rules/TF040.md +11 -0
  89. package/dist/okf/rules/TF041.md +11 -0
  90. package/dist/okf/rules/TF042.md +11 -0
  91. package/dist/okf/rules/TF043.md +11 -0
  92. package/dist/okf/rules/TF044.md +11 -0
  93. package/dist/okf/rules/TF045.md +11 -0
  94. package/dist/op/activities/index.d.ts +4 -0
  95. package/dist/op/activities/index.d.ts.map +1 -1
  96. package/dist/op/activities/pin-rollout.d.ts +46 -0
  97. package/dist/op/activities/pin-rollout.d.ts.map +1 -0
  98. package/dist/op/activities/terraform.d.ts +24 -50
  99. package/dist/op/activities/terraform.d.ts.map +1 -1
  100. package/dist/op/activity-contracts.d.ts +66 -18
  101. package/dist/op/activity-contracts.d.ts.map +1 -1
  102. package/dist/op/builders.d.ts +9 -0
  103. package/dist/op/builders.d.ts.map +1 -1
  104. package/dist/pin/edit-ts.d.ts +21 -0
  105. package/dist/pin/edit-ts.d.ts.map +1 -0
  106. package/dist/pin/edit.d.ts +52 -0
  107. package/dist/pin/edit.d.ts.map +1 -0
  108. package/dist/pin/forge.d.ts +39 -0
  109. package/dist/pin/forge.d.ts.map +1 -0
  110. package/dist/pin/index.d.ts +22 -0
  111. package/dist/pin/index.d.ts.map +1 -0
  112. package/dist/pin/rollout.d.ts +116 -0
  113. package/dist/pin/rollout.d.ts.map +1 -0
  114. package/dist/pin/scan.d.ts +54 -0
  115. package/dist/pin/scan.d.ts.map +1 -0
  116. package/dist/pin/source.d.ts +93 -0
  117. package/dist/pin/source.d.ts.map +1 -0
  118. package/dist/pin/waves.d.ts +66 -0
  119. package/dist/pin/waves.d.ts.map +1 -0
  120. package/dist/plan-digest.d.ts +59 -0
  121. package/dist/plan-digest.d.ts.map +1 -0
  122. package/dist/plugin.d.ts.map +1 -1
  123. package/dist/rules/flat-attributes.ts +146 -0
  124. package/dist/rules/module-source.ts +56 -1
  125. package/dist/rules/terragrunt.ts +76 -0
  126. package/dist/rules/tf002.ts +1 -1
  127. package/dist/rules/tf030.ts +290 -0
  128. package/dist/rules/tf031.ts +370 -0
  129. package/dist/rules/tf032.ts +226 -0
  130. package/dist/rules/tf033.ts +160 -0
  131. package/dist/rules/tf034.ts +63 -0
  132. package/dist/rules/tf035.ts +74 -0
  133. package/dist/rules/tf036.ts +119 -0
  134. package/dist/rules/tf037.ts +101 -0
  135. package/dist/rules/tf038.ts +71 -0
  136. package/dist/rules/tf039.ts +64 -0
  137. package/dist/rules/tf040.ts +76 -0
  138. package/dist/rules/tf041.ts +56 -0
  139. package/dist/rules/tf042.ts +42 -0
  140. package/dist/rules/tf043.ts +49 -0
  141. package/dist/rules/tf044.ts +51 -0
  142. package/dist/rules/tf045.ts +47 -0
  143. package/dist/terragrunt/affected.d.ts +119 -0
  144. package/dist/terragrunt/affected.d.ts.map +1 -0
  145. package/dist/terragrunt/index.d.ts +11 -0
  146. package/dist/terragrunt/index.d.ts.map +1 -0
  147. package/dist/terragrunt/mocks.d.ts +147 -0
  148. package/dist/terragrunt/mocks.d.ts.map +1 -0
  149. package/dist/terragrunt/run.d.ts +130 -0
  150. package/dist/terragrunt/run.d.ts.map +1 -0
  151. package/dist/terragrunt/units.d.ts +103 -0
  152. package/dist/terragrunt/units.d.ts.map +1 -0
  153. package/dist/terragrunt/wave.d.ts +89 -0
  154. package/dist/terragrunt/wave.d.ts.map +1 -0
  155. package/package.json +13 -3
  156. package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
  157. package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
  158. package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
  159. package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
  160. package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
  161. package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
  162. package/src/__fixtures__/change-set/terraform.plan.json +589 -0
  163. package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
  164. package/src/__fixtures__/change-set/tofu.plan.json +588 -0
  165. package/src/__fixtures__/change-set/v1.tf.txt +21 -0
  166. package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
  167. package/src/__fixtures__/change-set/v2.tf.txt +21 -0
  168. package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
  169. package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
  170. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
  171. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
  172. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
  173. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
  174. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
  175. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
  176. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
  177. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
  178. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
  179. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
  180. package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
  181. package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
  182. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
  183. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
  184. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
  185. package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
  186. package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
  187. package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
  188. package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
  189. package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
  190. package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
  191. package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
  192. package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
  193. package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
  194. package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
  195. package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
  196. package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
  197. package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
  198. package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
  199. package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
  200. package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
  201. package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
  202. package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
  203. package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
  204. package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
  205. package/src/__fixtures__/terragrunt/record.ts +56 -0
  206. package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
  207. package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
  208. package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
  209. package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
  210. package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
  211. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
  212. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
  213. package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
  214. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
  215. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
  216. package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
  217. package/src/change-set-combined-run.test.ts +156 -0
  218. package/src/change-set.test.ts +151 -0
  219. package/src/change-set.ts +273 -0
  220. package/src/changed-roots.test.ts +58 -0
  221. package/src/changed-roots.ts +102 -0
  222. package/src/commands.ts +112 -0
  223. package/src/components/capability-plugin.test.ts +91 -0
  224. package/src/components/capability-plugin.ts +131 -0
  225. package/src/components/pr-loop.tofu.test.ts +244 -0
  226. package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
  227. package/src/composites/terraform-pin-rollout-op.ts +60 -0
  228. package/src/hcl/descend.ts +1 -1
  229. package/src/hcl/edges.ts +2 -2
  230. package/src/hcl/jsonencode.test.ts +226 -0
  231. package/src/hcl/jsonencode.ts +454 -0
  232. package/src/hcl/parse.test.ts +25 -0
  233. package/src/hcl/parse.ts +91 -6
  234. package/src/hcl/roots.ts +1 -1
  235. package/src/hcl/suppressions.ts +8 -2
  236. package/src/index.ts +18 -0
  237. package/src/lint/audit-catalog.ts +230 -0
  238. package/src/lint/audit-lineage.ts +155 -1
  239. package/src/lint/audit.test.ts +3 -1
  240. package/src/lint/post-synth/estate-2284.test.ts +68 -0
  241. package/src/lint/post-synth/fixtures/TF030/negative-rules.tf +45 -0
  242. package/src/lint/post-synth/fixtures/TF030/negative.tf +55 -0
  243. package/src/lint/post-synth/fixtures/TF030/not-determined-port.tf +28 -0
  244. package/src/lint/post-synth/fixtures/TF030/not-determined-standalone.tf +32 -0
  245. package/src/lint/post-synth/fixtures/TF030/not-determined-vpc-rule.tf +21 -0
  246. package/src/lint/post-synth/fixtures/TF030/not-determined.tf +39 -0
  247. package/src/lint/post-synth/fixtures/TF030/positive-ipv6.tf +44 -0
  248. package/src/lint/post-synth/fixtures/TF030/positive-mixed.tf +36 -0
  249. package/src/lint/post-synth/fixtures/TF030/positive-standalone.tf +25 -0
  250. package/src/lint/post-synth/fixtures/TF030/positive.tf +43 -0
  251. package/src/lint/post-synth/fixtures/TF031/negative.tf +89 -0
  252. package/src/lint/post-synth/fixtures/TF031/not-determined.tf +139 -0
  253. package/src/lint/post-synth/fixtures/TF031/positive-data-source.tf +57 -0
  254. package/src/lint/post-synth/fixtures/TF031/positive-heredoc.tf +149 -0
  255. package/src/lint/post-synth/fixtures/TF031/positive-jsonencode.tf +115 -0
  256. package/src/lint/post-synth/fixtures/TF032/negative-heredoc.tf +20 -0
  257. package/src/lint/post-synth/fixtures/TF032/negative-references.tf +214 -0
  258. package/src/lint/post-synth/fixtures/TF032/negative.tf +99 -0
  259. package/src/lint/post-synth/fixtures/TF032/positive-heredoc.tf +22 -0
  260. package/src/lint/post-synth/fixtures/TF032/positive-sidecar.tf +197 -0
  261. package/src/lint/post-synth/fixtures/TF032/positive.tf +54 -0
  262. package/src/lint/post-synth/fixtures/TF033/negative.tf +39 -0
  263. package/src/lint/post-synth/fixtures/TF033/not-determined-aurora.tf +25 -0
  264. package/src/lint/post-synth/fixtures/TF033/positive-absent.tf +93 -0
  265. package/src/lint/post-synth/fixtures/TF033/positive.tf +50 -0
  266. package/src/lint/post-synth/fixtures/TF034/negative.tf +24 -0
  267. package/src/lint/post-synth/fixtures/TF034/positive-absent.tf +36 -0
  268. package/src/lint/post-synth/fixtures/TF034/positive.tf +14 -0
  269. package/src/lint/post-synth/fixtures/TF035/negative-absent.tf +43 -0
  270. package/src/lint/post-synth/fixtures/TF035/negative.tf +54 -0
  271. package/src/lint/post-synth/fixtures/TF035/positive.tf +37 -0
  272. package/src/lint/post-synth/fixtures/TF036/negative.tf +61 -0
  273. package/src/lint/post-synth/fixtures/TF036/not-determined.tf +28 -0
  274. package/src/lint/post-synth/fixtures/TF036/positive.tf +38 -0
  275. package/src/lint/post-synth/fixtures/TF037/negative-exclusion.tf +63 -0
  276. package/src/lint/post-synth/fixtures/TF037/negative.tf +27 -0
  277. package/src/lint/post-synth/fixtures/TF037/not-determined.tf +27 -0
  278. package/src/lint/post-synth/fixtures/TF037/positive-absent.tf +15 -0
  279. package/src/lint/post-synth/fixtures/TF037/positive.tf +26 -0
  280. package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
  281. package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
  282. package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
  283. package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
  284. package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
  285. package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
  286. package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
  287. package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
  288. package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
  289. package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
  290. package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
  291. package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
  292. package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
  293. package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
  294. package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
  295. package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
  296. package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
  297. package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
  298. package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
  299. package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
  300. package/src/lint/post-synth/fixtures/estate-2284/clean/main.tf +132 -0
  301. package/src/lint/post-synth/fixtures/estate-2284/violating/main.tf +112 -0
  302. package/src/lint/post-synth/flat-attributes.ts +146 -0
  303. package/src/lint/post-synth/index.ts +32 -0
  304. package/src/lint/post-synth/module-source.test.ts +42 -1
  305. package/src/lint/post-synth/module-source.ts +56 -1
  306. package/src/lint/post-synth/terragrunt.ts +76 -0
  307. package/src/lint/post-synth/tf002.ts +1 -1
  308. package/src/lint/post-synth/tf030.test.ts +219 -0
  309. package/src/lint/post-synth/tf030.ts +290 -0
  310. package/src/lint/post-synth/tf031.test.ts +229 -0
  311. package/src/lint/post-synth/tf031.ts +370 -0
  312. package/src/lint/post-synth/tf032.test.ts +127 -0
  313. package/src/lint/post-synth/tf032.ts +226 -0
  314. package/src/lint/post-synth/tf033.test.ts +126 -0
  315. package/src/lint/post-synth/tf033.ts +160 -0
  316. package/src/lint/post-synth/tf034.test.ts +71 -0
  317. package/src/lint/post-synth/tf034.ts +63 -0
  318. package/src/lint/post-synth/tf035.test.ts +68 -0
  319. package/src/lint/post-synth/tf035.ts +74 -0
  320. package/src/lint/post-synth/tf036.test.ts +87 -0
  321. package/src/lint/post-synth/tf036.ts +119 -0
  322. package/src/lint/post-synth/tf037.test.ts +88 -0
  323. package/src/lint/post-synth/tf037.ts +101 -0
  324. package/src/lint/post-synth/tf038.test.ts +51 -0
  325. package/src/lint/post-synth/tf038.ts +71 -0
  326. package/src/lint/post-synth/tf039.test.ts +40 -0
  327. package/src/lint/post-synth/tf039.ts +64 -0
  328. package/src/lint/post-synth/tf040.test.ts +55 -0
  329. package/src/lint/post-synth/tf040.ts +76 -0
  330. package/src/lint/post-synth/tf041.test.ts +50 -0
  331. package/src/lint/post-synth/tf041.ts +56 -0
  332. package/src/lint/post-synth/tf042.test.ts +19 -0
  333. package/src/lint/post-synth/tf042.ts +42 -0
  334. package/src/lint/post-synth/tf043.test.ts +65 -0
  335. package/src/lint/post-synth/tf043.ts +49 -0
  336. package/src/lint/post-synth/tf044.test.ts +27 -0
  337. package/src/lint/post-synth/tf044.ts +51 -0
  338. package/src/lint/post-synth/tf045.test.ts +30 -0
  339. package/src/lint/post-synth/tf045.ts +47 -0
  340. package/src/op/activities/index.ts +4 -0
  341. package/src/op/activities/pin-rollout.ts +77 -0
  342. package/src/op/activities/terraform.test.ts +13 -0
  343. package/src/op/activities/terraform.ts +46 -86
  344. package/src/op/activity-contracts.test.ts +4 -1
  345. package/src/op/activity-contracts.ts +42 -0
  346. package/src/op/builders.ts +13 -0
  347. package/src/pin/bundle.test.ts +53 -0
  348. package/src/pin/edit-ts.test.ts +39 -0
  349. package/src/pin/edit-ts.ts +77 -0
  350. package/src/pin/edit.test.ts +199 -0
  351. package/src/pin/edit.ts +157 -0
  352. package/src/pin/forge.ts +85 -0
  353. package/src/pin/index.ts +22 -0
  354. package/src/pin/rollout.test.ts +232 -0
  355. package/src/pin/rollout.ts +451 -0
  356. package/src/pin/scan.ts +309 -0
  357. package/src/pin/source.ts +206 -0
  358. package/src/pin/waves.test.ts +101 -0
  359. package/src/pin/waves.ts +125 -0
  360. package/src/plan-digest.ts +99 -0
  361. package/src/plan-summary.test.ts +154 -0
  362. package/src/plugin.ts +25 -2
  363. package/src/terragrunt/affected.acceptance.test.ts +83 -0
  364. package/src/terragrunt/affected.test.ts +177 -0
  365. package/src/terragrunt/affected.ts +348 -0
  366. package/src/terragrunt/index.ts +11 -0
  367. package/src/terragrunt/mocks.acceptance.test.ts +105 -0
  368. package/src/terragrunt/mocks.test.ts +309 -0
  369. package/src/terragrunt/mocks.ts +324 -0
  370. package/src/terragrunt/run.ts +369 -0
  371. package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
  372. package/src/terragrunt/terragrunt.test.ts +332 -0
  373. package/src/terragrunt/units.ts +235 -0
  374. package/src/terragrunt/wave.ts +181 -0
@@ -0,0 +1,127 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
3
+ import { tf032 } from "./tf032";
4
+ import { loadFixture } from "./fixtures/load";
5
+ import { blocksToEntities } from "../../hcl/parse";
6
+
7
+ async function ctxOf(source: string): Promise<PostSynthContext> {
8
+ const entities = await blocksToEntities([{ name: "main.tf", source }], "TF032");
9
+ return { outputs: new Map(), entities } as unknown as PostSynthContext;
10
+ }
11
+
12
+ const errors = (d: PostSynthDiagnostic[]) => d.filter((x) => x.severity === "error");
13
+ const infos = (d: PostSynthDiagnostic[]) => d.filter((x) => x.severity === "info");
14
+
15
+ /** One task definition around a container_definitions expression. */
16
+ const task = (defs: string) => `resource "aws_ecs_task_definition" "t" {\n family = "t"\n container_definitions = ${defs}\n}\n`;
17
+
18
+ describe("TF032: plaintext credential in an ECS container definition", () => {
19
+ test("fires on each literal credential in a jsonencode'd environment (edstem paylink-platform)", async () => {
20
+ const diags = tf032.check(await loadFixture("TF032", "positive"));
21
+ expect(errors(diags).map((d) => d.message.match(/passes "([^"]+)"/)?.[1])).toEqual(["DB_PASSWORD", "PAYMENT_API_KEY"]);
22
+ // `image = "${var.ecr_repo}:latest"` is an unknown leaf, not a reason to give up on the container.
23
+ expect(infos(diags)).toHaveLength(0);
24
+ for (const d of errors(diags)) {
25
+ expect(d.checkId).toBe("TF032");
26
+ expect(d.entity).toBe("TF032/aws_ecs_task_definition.api");
27
+ expect(d.message).toContain("redacted");
28
+ expect(d.message).not.toContain("Pl-Pr0d-Db-2025!");
29
+ expect(d.message).not.toContain("pk_live_");
30
+ }
31
+ });
32
+
33
+ test("reads a heredoc with no interpolation as JSON (trivy-checks' failing example)", async () => {
34
+ const diags = tf032.check(await loadFixture("TF032", "positive-heredoc"));
35
+ expect(errors(diags)).toHaveLength(1);
36
+ expect(errors(diags)[0].message).toContain('"DATABASE_PASSWORD"');
37
+ expect(errors(diags)[0].message).toContain('container "my_service"');
38
+ });
39
+
40
+ test("fires on a literal sidecar password and reports the local-fed environments as not determined (govuk forms-admin)", async () => {
41
+ const diags = tf032.check(await loadFixture("TF032", "positive-sidecar"));
42
+ expect(errors(diags)).toHaveLength(1);
43
+ expect(errors(diags)[0].message).toContain('container "postgres" passes "POSTGRES_PASSWORD"');
44
+ const nd = infos(diags);
45
+ expect(nd).toHaveLength(2);
46
+ for (const d of nd) {
47
+ expect(d.message.startsWith("Not determined:")).toBe(true);
48
+ expect(d.message).toContain("`environment`");
49
+ }
50
+ expect(nd.map((d) => d.message.match(/container "([^"]+)"/)?.[1])).toEqual(["forms-admin", "forms-admin-seeding"]);
51
+ });
52
+
53
+ test("is silent when credentials come through secrets (vapor penny-bot)", async () => {
54
+ expect(tf032.check(await loadFixture("TF032", "negative"))).toEqual([]);
55
+ });
56
+
57
+ test("is silent on trivy-checks' passing example", async () => {
58
+ expect(tf032.check(await loadFixture("TF032", "negative-heredoc"))).toEqual([]);
59
+ });
60
+
61
+ test("a reference value is not a literal, a container with no environment is not insecure (testdrivenio)", async () => {
62
+ const diags = tf032.check(await loadFixture("TF032", "negative-references"));
63
+ expect(errors(diags)).toEqual([]);
64
+ // The one task whose definitions are `data.template_file.app.rendered`.
65
+ expect(infos(diags)).toHaveLength(1);
66
+ expect(infos(diags)[0].entity).toBe("TF032/aws_ecs_task_definition.app");
67
+ expect(infos(diags)[0].message).toMatch(/^Not determined: .*data\.template_file\.app\.rendered/);
68
+ });
69
+
70
+ describe("not determined, never a warning", () => {
71
+ test.each([
72
+ ["file()", 'file("service.json")', /call to file\(\)/],
73
+ ["templatefile()", 'templatefile("defs.tpl", { image = var.image })', /call to templatefile\(\)/],
74
+ ["a local", "local.container_definitions", /reference `local\.container_definitions`/],
75
+ ["jsonencode(concat(...))", "jsonencode(concat([{ name = \"a\" }], var.extra))", /jsonencode argument holds/],
76
+ ["a heredoc with interpolation", '<<EOT\n[{"name":"a","image":"${var.image}"}]\nEOT', /interpolation/],
77
+ ])("%s", async (_label, defs, reason) => {
78
+ const diags = tf032.check(await ctxOf(task(defs)));
79
+ expect(diags).toHaveLength(1);
80
+ expect(diags[0].severity).toBe("info");
81
+ expect(diags[0].message.startsWith("Not determined:")).toBe(true);
82
+ expect(diags[0].message).toMatch(reason);
83
+ });
84
+
85
+ test("an environment entry that is itself an expression", async () => {
86
+ const diags = tf032.check(
87
+ await ctxOf(task('jsonencode([{ name = "a", environment = [{ name = "MODE", value = "api" }, local.extra_env] }])')),
88
+ );
89
+ expect(diags).toHaveLength(1);
90
+ expect(diags[0].severity).toBe("info");
91
+ expect(diags[0].message).toContain('container "a"');
92
+ });
93
+ });
94
+
95
+ test("the same name through secrets does not fire; a secret-named literal in environment does", async () => {
96
+ const viaSecrets = task(
97
+ 'jsonencode([{ name = "a", secrets = [{ name = "DB_PASSWORD", valueFrom = "arn:aws:ssm:eu-west-1:123456789012:parameter/db" }] }])',
98
+ );
99
+ expect(tf032.check(await ctxOf(viaSecrets))).toEqual([]);
100
+ const viaEnv = task('jsonencode([{ name = "a", environment = [{ name = "DB_PASSWORD", value = "s3cr3t-Pa55word" }] }])');
101
+ expect(errors(tf032.check(await ctxOf(viaEnv)))).toHaveLength(1);
102
+ });
103
+
104
+ test("short values, placeholders and lookalike names are not credentials", async () => {
105
+ const defs = task(
106
+ 'jsonencode([{ name = "a", environment = [' +
107
+ '{ name = "PASSWORD_MIN_LENGTH", value = "8" },' +
108
+ '{ name = "DB_PASSWORD", value = "changeme-please" },' +
109
+ '{ name = "TOKENIZER", value = "wordpiece-uncased" },' +
110
+ '{ name = "DB_SECRET_ARN", value = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:db" }' +
111
+ "] }])",
112
+ );
113
+ expect(tf032.check(await ctxOf(defs))).toEqual([]);
114
+ });
115
+
116
+ test("a credential-shaped value fires whatever it is called", async () => {
117
+ const defs = task('jsonencode([{ name = "a", environment = [{ name = "UPSTREAM", value = "AKIAIOSFODNN7EXAMPLF" }] }])');
118
+ const diags = errors(tf032.check(await ctxOf(defs)));
119
+ expect(diags).toHaveLength(1);
120
+ expect(diags[0].message).toContain("shape of a credential");
121
+ });
122
+
123
+ test("only aws_ecs_task_definition is read", async () => {
124
+ const src = 'resource "aws_ecs_service" "s" {\n name = "s"\n container_definitions = local.x\n}\n';
125
+ expect(tf032.check(await ctxOf(src))).toEqual([]);
126
+ });
127
+ });
@@ -0,0 +1,226 @@
1
+ /**
2
+ * TF032: an ECS container definition passes a credential as a plaintext
3
+ * environment value.
4
+ *
5
+ * The Terraform counterpart of the aws lexicon's WAW046. A value under a
6
+ * container's `environment` is stored verbatim in the task definition: it is
7
+ * readable by anyone with `ecs:DescribeTaskDefinition`, it shows in the
8
+ * console, and it is in Terraform state and plan output. ECS has a field for
9
+ * exactly this, `secrets`, whose `valueFrom` is a Secrets Manager or SSM
10
+ * Parameter Store ARN that the agent resolves at task start. A credential
11
+ * belongs there.
12
+ *
13
+ * Why this is a native rule and not WAW046 through a bridge
14
+ * (docs/design/waw-hcl-fidelity-probe.md): `container_definitions` is a JSON
15
+ * string. In HCL it is almost always `jsonencode([...])`, which hcl2json hands
16
+ * back as the call's unevaluated source text, so the structure WAW046 reads
17
+ * does not exist in the parse until something reads that text.
18
+ * `../../hcl/jsonencode.ts` does, for literal structure only, and never
19
+ * evaluates anything.
20
+ *
21
+ * The three answers, per task definition:
22
+ *
23
+ * - Readable. `container_definitions` is `jsonencode([...])` whose list and
24
+ * containers are literal, or a plain JSON string (a heredoc with no
25
+ * interpolation is one). A reference or function call in a leaf
26
+ * (`image = var.image`, `value = var.db_password`) does not stop the read:
27
+ * that leaf is unknown and the literal fields beside it are still read. An
28
+ * environment entry whose value is a reference is skipped, since a
29
+ * reference is not a committed literal (TF007 and TF022 judge where the
30
+ * referenced value comes from).
31
+ * - Violation. A readable environment entry whose `value` is a literal string
32
+ * and is secret-shaped by `../secret-shape.ts`: a credential-looking name
33
+ * with a literal of at least 8 characters that is not a placeholder, or a
34
+ * value that is a credential on its own (a vendor key shape, a JWT, a PEM
35
+ * key, a high-entropy token) whatever it is called.
36
+ * - Not determined. `container_definitions` is a reference
37
+ * (`local.defs`, `data.template_file.x.rendered`), `file()`,
38
+ * `templatefile()`, a heredoc or string with interpolation, a jsonencode
39
+ * argument that is not a literal list (`jsonencode(concat(...))`,
40
+ * `jsonencode(local.containers)`), or a container whose `environment` is an
41
+ * expression (`environment = local.env`). One `info` diagnostic starting
42
+ * `Not determined:` per task definition or container, never a warning.
43
+ *
44
+ * Absence: a container with no `environment` is not insecure, and a task
45
+ * definition with no `container_definitions` is not reported (the attribute
46
+ * is required, so that is a plan error, not this rule's finding). A name in
47
+ * `secrets` is never reported: `secrets` is not read at all.
48
+ *
49
+ * Findings never carry the value, only its length.
50
+ */
51
+
52
+ import type {
53
+ PostSynthCheck,
54
+ PostSynthContext,
55
+ PostSynthDiagnostic,
56
+ } from "@intentius/chant/lint/post-synth";
57
+ import { RESOURCE_TYPE } from "../../hcl/parse";
58
+ import { attr } from "../../hcl/value";
59
+ import { isUnknown, readJsonencode } from "../../hcl/jsonencode";
60
+ import { secretShapedAssignment } from "../secret-shape";
61
+ import { blocksOfType, type TerraformBlock } from "./blocks";
62
+
63
+ const TASK_DEFINITION = "aws_ecs_task_definition";
64
+
65
+ /** The container definitions of one task definition, or why they cannot be read. */
66
+ export type ContainerDefinitionsRead =
67
+ | { kind: "readable"; containers: unknown }
68
+ | { kind: "not-determined"; reason: string }
69
+ | { kind: "absent" };
70
+
71
+ /**
72
+ * Read `container_definitions` as structure without evaluating anything.
73
+ * `jsonencode(...)` goes through the shared reader with unknown leaves; a
74
+ * plain string with no interpolation is JSON (a heredoc arrives as one);
75
+ * everything else is named in the not-determined reason.
76
+ */
77
+ export function readContainerDefinitions(body: Record<string, unknown>): ContainerDefinitionsRead {
78
+ const raw = body.container_definitions;
79
+ if (raw === undefined || raw === null) return { kind: "absent" };
80
+
81
+ const encoded = readJsonencode(raw, { unknownLeaves: true });
82
+ if (encoded.kind === "literal") return { kind: "readable", containers: encoded.value };
83
+ if (encoded.kind === "not-determined") {
84
+ return { kind: "not-determined", reason: `its jsonencode argument holds ${encoded.reason}` };
85
+ }
86
+
87
+ if (typeof raw !== "string") return { kind: "not-determined", reason: "it is not a string" };
88
+ // attr() alone is not enough to call a string literal: its interpolation
89
+ // pattern tolerates one level of nested braces, so a deeper expression can
90
+ // read as `literal`. Any `${` at all means the parse did not hand us text.
91
+ if (!raw.includes("${")) {
92
+ try {
93
+ return { kind: "readable", containers: JSON.parse(raw) };
94
+ } catch {
95
+ return { kind: "not-determined", reason: "it is a string that is not valid JSON" };
96
+ }
97
+ }
98
+ return { kind: "not-determined", reason: describeExpression(raw) };
99
+ }
100
+
101
+ /** Name the expression an unreadable `container_definitions` holds, for the message. */
102
+ function describeExpression(raw: string): string {
103
+ const a = attr({ v: raw }, "v");
104
+ if (a.kind === "reference" && a.refs?.[0] !== undefined) {
105
+ const expr = a.refs[0];
106
+ const call = /^([a-z_][a-z0-9_]*)\s*\(/i.exec(expr);
107
+ if (call) return `it is a call to ${call[1]}(), which this rule does not evaluate`;
108
+ return `it is the reference \`${expr}\`, whose value is not in this module's source`;
109
+ }
110
+ return "it is a string or heredoc with interpolation, which this rule does not evaluate";
111
+ }
112
+
113
+ /** A container's display name: its literal `name`, else its position. */
114
+ function containerName(container: Record<string, unknown>, index: number): string {
115
+ return typeof container.name === "string" ? container.name : `#${index}`;
116
+ }
117
+
118
+ function notDetermined(block: TerraformBlock, what: string, reason: string): PostSynthDiagnostic {
119
+ return {
120
+ checkId: "TF032",
121
+ severity: "info",
122
+ message:
123
+ `Not determined: "${block.address}" ${what}: ${reason}. TF032 reads only literal structure, ` +
124
+ "so a plaintext credential in it can be neither found nor ruled out.",
125
+ entity: block.key,
126
+ lexicon: "terraform",
127
+ };
128
+ }
129
+
130
+ export function checkEcsPlaintextCredentials(ctx: PostSynthContext): PostSynthDiagnostic[] {
131
+ const diagnostics: PostSynthDiagnostic[] = [];
132
+
133
+ for (const block of blocksOfType(ctx, RESOURCE_TYPE)) {
134
+ if (!block.address.startsWith(`${TASK_DEFINITION}.`)) continue;
135
+
136
+ const read = readContainerDefinitions(block.body);
137
+ if (read.kind === "absent") continue;
138
+ if (read.kind === "not-determined") {
139
+ diagnostics.push(notDetermined(block, "`container_definitions`", read.reason));
140
+ continue;
141
+ }
142
+ // A literal that is not a list is not a valid container_definitions; the
143
+ // provider rejects it at plan, and there is no environment to read.
144
+ if (!Array.isArray(read.containers)) continue;
145
+
146
+ read.containers.forEach((container, index) => {
147
+ if (isUnknown(container)) {
148
+ diagnostics.push(
149
+ notDetermined(block, `container #${index} of \`container_definitions\``, `it is ${container.reason}`),
150
+ );
151
+ return;
152
+ }
153
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
154
+ const c = container as Record<string, unknown>;
155
+ const name = containerName(c, index);
156
+
157
+ const env = c.environment;
158
+ if (env === undefined || env === null) return;
159
+ if (isUnknown(env)) {
160
+ diagnostics.push(notDetermined(block, `container "${name}" \`environment\``, `it is ${env.reason}`));
161
+ return;
162
+ }
163
+ if (!Array.isArray(env)) return;
164
+
165
+ const unreadable: string[] = [];
166
+ for (const entry of env) {
167
+ if (isUnknown(entry)) {
168
+ unreadable.push(entry.reason);
169
+ continue;
170
+ }
171
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
172
+ const e = entry as Record<string, unknown>;
173
+ if (isUnknown(e.name)) {
174
+ // A computed variable name could be anything, so the entry is
175
+ // unreadable only when it also carries a literal value to judge.
176
+ if (typeof e.value === "string") unreadable.push(e.name.reason);
177
+ continue;
178
+ }
179
+ if (typeof e.name !== "string") continue;
180
+ // A reference value is the remediation's neighbour, not a literal:
181
+ // skipped. Numbers and booleans are not credentials.
182
+ if (typeof e.value !== "string") continue;
183
+
184
+ const shape = secretShapedAssignment(e.name, e.value);
185
+ if (!shape) continue;
186
+ const why =
187
+ shape.reason === "name"
188
+ ? "its name reads like a credential"
189
+ : "the value has the shape of a credential";
190
+ diagnostics.push({
191
+ checkId: "TF032",
192
+ severity: "error",
193
+ message:
194
+ `"${block.address}" container "${name}" passes "${e.name}" as a plaintext environment value ` +
195
+ `(${shape.length} characters, redacted here; ${why}). It is stored in the task definition, ` +
196
+ "visible to anyone who can describe it, and in state and plan output. Move it to the " +
197
+ "container's `secrets` with a `valueFrom` naming a Secrets Manager secret or SSM parameter, " +
198
+ "and rotate the committed value.",
199
+ entity: block.key,
200
+ lexicon: "terraform",
201
+ });
202
+ }
203
+
204
+ if (unreadable.length > 0) {
205
+ diagnostics.push(
206
+ notDetermined(
207
+ block,
208
+ `container "${name}" \`environment\``,
209
+ `${unreadable.length} entr${unreadable.length === 1 ? "y is" : "ies are"} not literal (${unreadable[0]})`,
210
+ ),
211
+ );
212
+ }
213
+ });
214
+ }
215
+
216
+ return diagnostics;
217
+ }
218
+
219
+ export const tf032: PostSynthCheck = {
220
+ id: "TF032",
221
+ description: "ECS container definition passes a credential as a plaintext environment value",
222
+
223
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
224
+ return checkEcsPlaintextCredentials(ctx);
225
+ },
226
+ };
@@ -0,0 +1,126 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
3
+ import { blocksToEntities } from "../../hcl/parse";
4
+ import { loadFixture } from "./fixtures/load";
5
+ import { tf033 } from "./tf033";
6
+
7
+ async function inline(source: string): Promise<PostSynthContext> {
8
+ const entities = await blocksToEntities([{ name: "main.tf", source }], "app");
9
+ return { outputs: new Map(), entities } as unknown as PostSynthContext;
10
+ }
11
+
12
+ const verdicts = (d: PostSynthDiagnostic[]) => d.map((x) => `${x.severity} ${x.entity}`).sort();
13
+
14
+ describe("TF033 over vendored real-world fixtures", () => {
15
+ test("storage_encrypted = true is silent (tmknom/example-pragmatic-terraform)", async () => {
16
+ expect(tf033.check(await loadFixture("TF033", "negative"))).toEqual([]);
17
+ });
18
+
19
+ test("storage_encrypted = false is an error (bridgecrewio/terragoat)", async () => {
20
+ const diags = tf033.check(await loadFixture("TF033", "positive"));
21
+ expect(verdicts(diags)).toEqual(["error TF033/aws_db_instance.default"]);
22
+ expect(diags[0]).toMatchObject({ checkId: "TF033", lexicon: "terraform" });
23
+ expect(diags[0].message).toContain("sets `storage_encrypted = false`");
24
+ });
25
+
26
+ test("an instance that does not set storage_encrypted is an error; snapshot_identifier = \"\" is no source (zoitech)", async () => {
27
+ const diags = tf033.check(await loadFixture("TF033", "positive-absent"));
28
+ expect(verdicts(diags)).toEqual(["error TF033/aws_db_instance.postgres"]);
29
+ expect(diags[0].message).toContain("the provider default is `false`");
30
+ });
31
+
32
+ test("an Aurora cluster without storage_encrypted is not determined (bridgecrewio/terragoat)", async () => {
33
+ const diags = tf033.check(await loadFixture("TF033", "not-determined-aurora"));
34
+ expect(verdicts(diags)).toEqual(["info TF033/aws_rds_cluster.app1-rds-cluster"]);
35
+ expect(diags[0].message).toMatch(/^Not determined: /);
36
+ expect(diags[0].message).toContain("2026-02-16");
37
+ });
38
+ });
39
+
40
+ describe("TF033 cases the fixtures do not isolate", () => {
41
+ test("an instance created from a source is not determined unless storage_encrypted = true", async () => {
42
+ const diags = tf033.check(
43
+ await inline(`
44
+ resource "aws_db_instance" "replica" {
45
+ replicate_source_db = aws_db_instance.primary.identifier
46
+ }
47
+ resource "aws_db_instance" "restored" {
48
+ snapshot_identifier = "rds:prod-2026-01-01"
49
+ storage_encrypted = false
50
+ }
51
+ resource "aws_db_instance" "pitr" {
52
+ restore_to_point_in_time {
53
+ use_latest_restorable_time = true
54
+ }
55
+ }
56
+ resource "aws_db_instance" "explicit" {
57
+ snapshot_identifier = "rds:prod-2026-01-01"
58
+ storage_encrypted = true
59
+ }`),
60
+ );
61
+ expect(verdicts(diags)).toEqual([
62
+ "info app/aws_db_instance.pitr",
63
+ "info app/aws_db_instance.replica",
64
+ "info app/aws_db_instance.restored",
65
+ ]);
66
+ expect(diags.find((d) => d.entity === "app/aws_db_instance.replica")!.message).toContain("`replicate_source_db`");
67
+ });
68
+
69
+ test("a storage_encrypted expression is not determined; the string \"true\" is true", async () => {
70
+ const diags = tf033.check(
71
+ await inline(`
72
+ resource "aws_db_instance" "v" {
73
+ storage_encrypted = var.encrypted
74
+ }
75
+ resource "aws_db_instance" "s" {
76
+ storage_encrypted = "true"
77
+ }`),
78
+ );
79
+ expect(verdicts(diags)).toEqual(["info app/aws_db_instance.v"]);
80
+ expect(diags[0].message).toContain("`storage_encrypted` is an expression (var.encrypted)");
81
+ });
82
+
83
+ test("a Multi-AZ DB cluster defaults to unencrypted; Aurora false and an engine expression are not determined", async () => {
84
+ const diags = tf033.check(
85
+ await inline(`
86
+ resource "aws_rds_cluster" "multi_az" {
87
+ engine = "postgres"
88
+ db_cluster_instance_class = "db.r6gd.large"
89
+ }
90
+ resource "aws_rds_cluster" "multi_az_off" {
91
+ engine = "mysql"
92
+ storage_encrypted = false
93
+ }
94
+ resource "aws_rds_cluster" "aurora_off" {
95
+ engine = "aurora-postgresql"
96
+ storage_encrypted = false
97
+ }
98
+ resource "aws_rds_cluster" "engine_var" {
99
+ engine = var.engine
100
+ }
101
+ resource "aws_rds_cluster" "restored" {
102
+ engine = "postgres"
103
+ snapshot_identifier = "arn:aws:rds:eu-west-1:111122223333:cluster-snapshot:x"
104
+ }
105
+ resource "aws_rds_cluster" "serverless_v1" {
106
+ engine = "aurora-mysql"
107
+ engine_mode = "serverless"
108
+ }
109
+ resource "aws_rds_cluster" "on" {
110
+ engine = "postgres"
111
+ storage_encrypted = true
112
+ }`),
113
+ );
114
+ expect(verdicts(diags)).toEqual([
115
+ "error app/aws_rds_cluster.multi_az",
116
+ "error app/aws_rds_cluster.multi_az_off",
117
+ "info app/aws_rds_cluster.aurora_off",
118
+ "info app/aws_rds_cluster.engine_var",
119
+ "info app/aws_rds_cluster.restored",
120
+ ]);
121
+ });
122
+
123
+ test("aws_rds_cluster_instance is never read", async () => {
124
+ expect(tf033.check(await inline(`resource "aws_rds_cluster_instance" "i" { cluster_identifier = "c" }`))).toEqual([]);
125
+ });
126
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * TF033: an RDS DB instance or cluster does not encrypt its storage
3
+ * (chant #2288, epic #2284).
4
+ *
5
+ * The Terraform counterpart of the aws lexicon's WAW021, written natively.
6
+ * WAW021 reads a template chant synthesized, where `StorageEncrypted` absent
7
+ * means false. Reading somebody else's HCL, absence means different things
8
+ * for the two resources and for how the database is created, so each case is
9
+ * decided here against the provider schema and the AWS default.
10
+ *
11
+ * `aws_db_instance` (provider `internal/service/rds/instance.go`, v6.67.0):
12
+ * `storage_encrypted` is optional, not computed, and the docs say "The
13
+ * default is `false` if not specified". The plain create path and `s3_import`
14
+ * send `StorageEncrypted: d.Get("storage_encrypted")`, so absent means false
15
+ * and is reported. The replica (`replicate_source_db`), snapshot
16
+ * (`snapshot_identifier`) and point-in-time (`restore_to_point_in_time`)
17
+ * paths never send it: the new instance takes its encryption from the source
18
+ * (and a cross-region replica from `kms_key_id`). For those the rule cannot
19
+ * know, and says so, unless `storage_encrypted = true` is written down.
20
+ *
21
+ * `aws_rds_cluster` (`cluster.go`): `storage_encrypted` is optional and
22
+ * computed, sent only when set, and "Terraform will only perform drift
23
+ * detection if a configuration value is provided". Two engine families:
24
+ *
25
+ * - Aurora (`engine` `aurora-mysql`/`aurora-postgresql`, or absent, which
26
+ * provider v5 defaulted to `aurora`). Since 2026-02-16 AWS encrypts every
27
+ * new Aurora cluster at rest with an AWS-owned key whatever
28
+ * `StorageEncrypted` says, and reports `StorageEncrypted: false` with
29
+ * `StorageEncryptionType: sse-rds` for it. A cluster created before that
30
+ * date with storage encryption off is unencrypted, and the configuration
31
+ * does not say when its cluster was created. So an Aurora cluster with
32
+ * `storage_encrypted` absent or `false` is not determined, not a finding.
33
+ * `engine_mode = "serverless"` (Serverless v1) was always encrypted and
34
+ * is not reported at all.
35
+ * - Multi-AZ DB clusters (`engine` `mysql`/`postgres`): the default is
36
+ * `false` and nothing changed it, so absent or `false` is a finding,
37
+ * unless the cluster is restored or replicated from a source
38
+ * (`snapshot_identifier`, `restore_to_point_in_time`,
39
+ * `replication_source_identifier`, `global_cluster_identifier`), which
40
+ * again decides the encryption.
41
+ *
42
+ * Not checked: `aws_rds_cluster_instance` (its `storage_encrypted` is
43
+ * computed-only and never appears in a body), `aws_rds_global_cluster`, and
44
+ * whether the key is customer managed.
45
+ *
46
+ * Scope: root and child modules alike (#2112).
47
+ */
48
+
49
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
50
+ import type { BlockBody } from "../../hcl/parse";
51
+ import type { TerraformBlock } from "./blocks";
52
+ import { finding, isGiven, notDetermined, readBool, readString, resourcesOfType, type Read } from "./flat-attributes";
53
+
54
+ const ID = "TF033";
55
+
56
+ const NOT_DETERMINED_TAIL =
57
+ "TF033 does not evaluate expressions or read the source database, so it cannot tell whether this storage is encrypted.";
58
+
59
+ /** The first source argument a body gives, if any. */
60
+ function sourceOf(body: BlockBody, names: readonly string[]): string | undefined {
61
+ return names.find((n) => isGiven(body, n));
62
+ }
63
+
64
+ const INSTANCE_SOURCES = ["replicate_source_db", "snapshot_identifier", "restore_to_point_in_time"] as const;
65
+ const CLUSTER_SOURCES = [
66
+ "snapshot_identifier",
67
+ "restore_to_point_in_time",
68
+ "replication_source_identifier",
69
+ "global_cluster_identifier",
70
+ ] as const;
71
+
72
+ function unencrypted(block: TerraformBlock, encrypted: Read<boolean>, what: string): PostSynthDiagnostic {
73
+ const how =
74
+ encrypted.kind === "absent"
75
+ ? "does not set `storage_encrypted`, and the provider default is `false`"
76
+ : "sets `storage_encrypted = false`";
77
+ return finding(
78
+ ID,
79
+ "error",
80
+ block,
81
+ `${how}, so the ${what} stores its data unencrypted. Set \`storage_encrypted = true\` (and \`kms_key_id\` for a ` +
82
+ "customer managed key). Encryption cannot be turned on in place: an existing database is migrated by restoring " +
83
+ "an encrypted copy of a snapshot.",
84
+ );
85
+ }
86
+
87
+ function checkInstance(block: TerraformBlock): PostSynthDiagnostic | undefined {
88
+ const encrypted = readBool(block.body, "storage_encrypted");
89
+ if (encrypted.kind === "known" && encrypted.value) return undefined;
90
+ if (encrypted.kind === "unknown") return notDetermined(ID, block, encrypted.reason, NOT_DETERMINED_TAIL);
91
+ const source = sourceOf(block.body, INSTANCE_SOURCES);
92
+ if (source) {
93
+ return notDetermined(
94
+ ID,
95
+ block,
96
+ `is created from a source (\`${source}\`), and the provider does not send \`storage_encrypted\` on that path: ` +
97
+ "the instance takes its encryption from the source",
98
+ NOT_DETERMINED_TAIL,
99
+ );
100
+ }
101
+ return unencrypted(block, encrypted, "DB instance");
102
+ }
103
+
104
+ function checkCluster(block: TerraformBlock): PostSynthDiagnostic | undefined {
105
+ const encrypted = readBool(block.body, "storage_encrypted");
106
+ if (encrypted.kind === "known" && encrypted.value) return undefined;
107
+ if (encrypted.kind === "unknown") return notDetermined(ID, block, encrypted.reason, NOT_DETERMINED_TAIL);
108
+
109
+ const engine = readString(block.body, "engine");
110
+ if (engine.kind === "unknown") {
111
+ return notDetermined(
112
+ ID,
113
+ block,
114
+ `${engine.reason}, and whether an unset \`storage_encrypted\` means unencrypted depends on the engine`,
115
+ NOT_DETERMINED_TAIL,
116
+ );
117
+ }
118
+ const aurora = engine.kind === "absent" || engine.value.startsWith("aurora");
119
+ if (aurora) {
120
+ const mode = readString(block.body, "engine_mode");
121
+ if (mode.kind === "known" && mode.value === "serverless") return undefined;
122
+ const set = encrypted.kind === "absent" ? "does not set `storage_encrypted`" : "sets `storage_encrypted = false`";
123
+ return notDetermined(
124
+ ID,
125
+ block,
126
+ `is an Aurora cluster that ${set}. AWS encrypts every Aurora cluster created on or after 2026-02-16 with an ` +
127
+ "AWS-owned key whatever this says; one created earlier with storage encryption off is unencrypted, and the " +
128
+ "configuration does not say when the cluster was created",
129
+ "Set `storage_encrypted = true` to make the answer explicit.",
130
+ );
131
+ }
132
+ const source = sourceOf(block.body, CLUSTER_SOURCES);
133
+ if (source) {
134
+ return notDetermined(
135
+ ID,
136
+ block,
137
+ `is created from a source (\`${source}\`), which decides its encryption`,
138
+ NOT_DETERMINED_TAIL,
139
+ );
140
+ }
141
+ return unencrypted(block, encrypted, "Multi-AZ DB cluster");
142
+ }
143
+
144
+ export const tf033: PostSynthCheck = {
145
+ id: "TF033",
146
+ description: "RDS DB instance or cluster storage is not encrypted",
147
+
148
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
149
+ const out: PostSynthDiagnostic[] = [];
150
+ for (const block of resourcesOfType(ctx, "aws_db_instance")) {
151
+ const d = checkInstance(block);
152
+ if (d) out.push(d);
153
+ }
154
+ for (const block of resourcesOfType(ctx, "aws_rds_cluster")) {
155
+ const d = checkCluster(block);
156
+ if (d) out.push(d);
157
+ }
158
+ return out;
159
+ },
160
+ };