infraweaver 0.1.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 (524) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +244 -0
  3. package/dist/agents/claude.d.ts +113 -0
  4. package/dist/agents/claudePretoolGate.d.ts +137 -0
  5. package/dist/agents/dispatchMetrics.d.ts +77 -0
  6. package/dist/agents/gateServer.d.ts +7 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/nativeFsDenies.d.ts +46 -0
  9. package/dist/agents/opencode.d.ts +284 -0
  10. package/dist/agents/opencodePlugin.d.ts +96 -0
  11. package/dist/agents/opencodeShared.d.ts +40 -0
  12. package/dist/agents/postRun.d.ts +126 -0
  13. package/dist/agents/reviewer.d.ts +41 -0
  14. package/dist/agents/sessionLabeler.d.ts +97 -0
  15. package/dist/agents/shared.d.ts +246 -0
  16. package/dist/agents/subagentModels.d.ts +19 -0
  17. package/dist/agents/tokenQuota.d.ts +118 -0
  18. package/dist/agents/writeGateSource.d.ts +22 -0
  19. package/dist/agents/writePolicy.d.ts +79 -0
  20. package/dist/brand.d.ts +67 -0
  21. package/dist/cli.mjs +247846 -0
  22. package/dist/external.d.ts +227 -0
  23. package/dist/i18n/scaffolding.d.ts +59 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +247061 -0
  26. package/dist/internal/index.d.ts +18 -0
  27. package/dist/internal.js +2398 -0
  28. package/dist/lifecycle.d.ts +2 -0
  29. package/dist/main.d.ts +8 -0
  30. package/dist/mcp/arkConfig.d.ts +1 -0
  31. package/dist/mcp/assess.d.ts +157 -0
  32. package/dist/mcp/capabilityContext.d.ts +71 -0
  33. package/dist/mcp/changeSummary.d.ts +52 -0
  34. package/dist/mcp/checkSuite.d.ts +27 -0
  35. package/dist/mcp/checkout.d.ts +92 -0
  36. package/dist/mcp/comment.d.ts +127 -0
  37. package/dist/mcp/commitInfo.d.ts +11 -0
  38. package/dist/mcp/crosswalk.d.ts +178 -0
  39. package/dist/mcp/crosswalkDigest.d.ts +1 -0
  40. package/dist/mcp/cyberEssentials.d.ts +24 -0
  41. package/dist/mcp/dashboard.d.ts +125 -0
  42. package/dist/mcp/dependencies.d.ts +12 -0
  43. package/dist/mcp/frameworks.d.ts +74 -0
  44. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  45. package/dist/mcp/git.d.ts +60 -0
  46. package/dist/mcp/guardrails.d.ts +222 -0
  47. package/dist/mcp/issue.d.ts +20 -0
  48. package/dist/mcp/issueComments.d.ts +11 -0
  49. package/dist/mcp/issueEvents.d.ts +11 -0
  50. package/dist/mcp/issueInfo.d.ts +11 -0
  51. package/dist/mcp/labels.d.ts +14 -0
  52. package/dist/mcp/localContext.d.ts +26 -0
  53. package/dist/mcp/moduleExtraction.d.ts +79 -0
  54. package/dist/mcp/moduleTests.d.ts +106 -0
  55. package/dist/mcp/modules.d.ts +198 -0
  56. package/dist/mcp/output.d.ts +16 -0
  57. package/dist/mcp/pathSafety.d.ts +14 -0
  58. package/dist/mcp/policy.d.ts +50 -0
  59. package/dist/mcp/pr.d.ts +64 -0
  60. package/dist/mcp/prInfo.d.ts +11 -0
  61. package/dist/mcp/providerSchema.d.ts +52 -0
  62. package/dist/mcp/review.d.ts +212 -0
  63. package/dist/mcp/reviewComments.d.ts +245 -0
  64. package/dist/mcp/roots.d.ts +60 -0
  65. package/dist/mcp/scope.d.ts +26 -0
  66. package/dist/mcp/selectMode.d.ts +20 -0
  67. package/dist/mcp/server.d.ts +61 -0
  68. package/dist/mcp/shared.d.ts +62 -0
  69. package/dist/mcp/shell.d.ts +58 -0
  70. package/dist/mcp/staleFix.d.ts +138 -0
  71. package/dist/mcp/terraform/azurePrices.d.ts +8 -0
  72. package/dist/mcp/terraform/baseline.d.ts +57 -0
  73. package/dist/mcp/terraform/concernResult.d.ts +38 -0
  74. package/dist/mcp/terraform/cost.d.ts +55 -0
  75. package/dist/mcp/terraform/cspm.d.ts +80 -0
  76. package/dist/mcp/terraform/currency.d.ts +110 -0
  77. package/dist/mcp/terraform/decisions.d.ts +178 -0
  78. package/dist/mcp/terraform/eidas.d.ts +100 -0
  79. package/dist/mcp/terraform/evidence.d.ts +343 -0
  80. package/dist/mcp/terraform/findings.d.ts +76 -0
  81. package/dist/mcp/terraform/fixMemory.d.ts +81 -0
  82. package/dist/mcp/terraform/governance.d.ts +56 -0
  83. package/dist/mcp/terraform/hcl.d.ts +115 -0
  84. package/dist/mcp/terraform/iacLanguages.d.ts +120 -0
  85. package/dist/mcp/terraform/idleFloor.d.ts +78 -0
  86. package/dist/mcp/terraform/keyless.d.ts +68 -0
  87. package/dist/mcp/terraform/moduleDocs.d.ts +56 -0
  88. package/dist/mcp/terraform/nativeRules.d.ts +38 -0
  89. package/dist/mcp/terraform/nativeScan.d.ts +11 -0
  90. package/dist/mcp/terraform/noiseProfile.d.ts +100 -0
  91. package/dist/mcp/terraform/normalization.d.ts +52 -0
  92. package/dist/mcp/terraform/oscal.d.ts +103 -0
  93. package/dist/mcp/terraform/packs/azurerm-3-4.d.ts +2 -0
  94. package/dist/mcp/terraform/packs/cloudflare-4-5.d.ts +2 -0
  95. package/dist/mcp/terraform/paths.d.ts +28 -0
  96. package/dist/mcp/terraform/plan.d.ts +157 -0
  97. package/dist/mcp/terraform/planDrift.d.ts +199 -0
  98. package/dist/mcp/terraform/policyAuthor.d.ts +191 -0
  99. package/dist/mcp/terraform/prowlerOcsf.d.ts +136 -0
  100. package/dist/mcp/terraform/refactor.d.ts +183 -0
  101. package/dist/mcp/terraform/registry.d.ts +108 -0
  102. package/dist/mcp/terraform/risk.d.ts +41 -0
  103. package/dist/mcp/terraform/scanSession.d.ts +116 -0
  104. package/dist/mcp/terraform/scannerCache.d.ts +15 -0
  105. package/dist/mcp/terraform/scannerEgress.d.ts +72 -0
  106. package/dist/mcp/terraform/scannerJson.d.ts +13 -0
  107. package/dist/mcp/terraform/scanners.d.ts +205 -0
  108. package/dist/mcp/terraform/signing.d.ts +61 -0
  109. package/dist/mcp/terraform/stateLocking.d.ts +18 -0
  110. package/dist/mcp/terraform/subprocess.d.ts +43 -0
  111. package/dist/mcp/terraform/suppressions.d.ts +90 -0
  112. package/dist/mcp/terraform/taxonomy.d.ts +39 -0
  113. package/dist/mcp/terraform/tfquery.d.ts +47 -0
  114. package/dist/mcp/terraform/toolchainIntegrity.d.ts +82 -0
  115. package/dist/mcp/terraform/tools/authorPolicy.d.ts +59 -0
  116. package/dist/mcp/terraform/tools/detectPlanDrift.d.ts +20 -0
  117. package/dist/mcp/terraform/tools/emitCost.d.ts +11 -0
  118. package/dist/mcp/terraform/tools/emitSarif.d.ts +14 -0
  119. package/dist/mcp/terraform/tools/fixMemory.d.ts +19 -0
  120. package/dist/mcp/terraform/tools/infracostDiff.d.ts +22 -0
  121. package/dist/mcp/terraform/tools/ingestExternalFindings.d.ts +21 -0
  122. package/dist/mcp/terraform/tools/moduleLookup.d.ts +14 -0
  123. package/dist/mcp/terraform/tools/moduleSearch.d.ts +23 -0
  124. package/dist/mcp/terraform/tools/plan.d.ts +59 -0
  125. package/dist/mcp/terraform/tools/providerUpgrade.d.ts +55 -0
  126. package/dist/mcp/terraform/tools/readFindings.d.ts +17 -0
  127. package/dist/mcp/terraform/tools/scan.d.ts +114 -0
  128. package/dist/mcp/terraform/tools/validate.d.ts +31 -0
  129. package/dist/mcp/terraform/tools/verifyRemediation.d.ts +31 -0
  130. package/dist/mcp/terraform/tools/versionCurrency.d.ts +5 -0
  131. package/dist/mcp/terraform/tools.d.ts +16 -0
  132. package/dist/mcp/terraform/transformPacks.d.ts +267 -0
  133. package/dist/mcp/terraform/types.d.ts +226 -0
  134. package/dist/mcp/terraform/verification.d.ts +81 -0
  135. package/dist/mcp/terraform/vex.d.ts +115 -0
  136. package/dist/mcp/terraform/writeOnly.d.ts +67 -0
  137. package/dist/mcp/terraform.d.ts +51 -0
  138. package/dist/mcp/terratest.d.ts +85 -0
  139. package/dist/mcp/untrustedContent.d.ts +76 -0
  140. package/dist/mcp/upload.d.ts +8 -0
  141. package/dist/models.d.ts +181 -0
  142. package/dist/modes/address-reviews.d.ts +2 -0
  143. package/dist/modes/assess.d.ts +2 -0
  144. package/dist/modes/build.d.ts +2 -0
  145. package/dist/modes/compliance-audit.d.ts +2 -0
  146. package/dist/modes/cost-optimization.d.ts +2 -0
  147. package/dist/modes/drift-detect.d.ts +2 -0
  148. package/dist/modes/fix.d.ts +2 -0
  149. package/dist/modes/incremental-review.d.ts +2 -0
  150. package/dist/modes/index.d.ts +24 -0
  151. package/dist/modes/modernize-deprecated.d.ts +2 -0
  152. package/dist/modes/plan.d.ts +2 -0
  153. package/dist/modes/policy-gate.d.ts +2 -0
  154. package/dist/modes/prFormats.d.ts +3 -0
  155. package/dist/modes/refactor.d.ts +2 -0
  156. package/dist/modes/refresh-remediation.d.ts +2 -0
  157. package/dist/modes/remediate-and-refactor.d.ts +2 -0
  158. package/dist/modes/remediate.d.ts +2 -0
  159. package/dist/modes/resolve-conflicts.d.ts +2 -0
  160. package/dist/modes/review.d.ts +2 -0
  161. package/dist/modes/summarize-pr.d.ts +2 -0
  162. package/dist/modes/task.d.ts +2 -0
  163. package/dist/modes/terraform-code-review.d.ts +2 -0
  164. package/dist/modes/types.d.ts +9 -0
  165. package/dist/modes/update-dependencies.d.ts +2 -0
  166. package/dist/prep/index.d.ts +7 -0
  167. package/dist/prep/installNodeDependencies.d.ts +2 -0
  168. package/dist/prep/installPythonDependencies.d.ts +2 -0
  169. package/dist/prep/types.d.ts +31 -0
  170. package/dist/reviewQuality.d.ts +107 -0
  171. package/dist/skills/terraform-best-practices/SKILL.md +379 -0
  172. package/dist/toolState.d.ts +123 -0
  173. package/dist/utils/activity.d.ts +40 -0
  174. package/dist/utils/agent.d.ts +47 -0
  175. package/dist/utils/agentHangReport.d.ts +38 -0
  176. package/dist/utils/annotations.d.ts +17 -0
  177. package/dist/utils/apiFetch.d.ts +11 -0
  178. package/dist/utils/apiKeys.d.ts +48 -0
  179. package/dist/utils/apiUrl.d.ts +28 -0
  180. package/dist/utils/assets.d.ts +8 -0
  181. package/dist/utils/baseRefConfig.d.ts +121 -0
  182. package/dist/utils/billingErrors.d.ts +85 -0
  183. package/dist/utils/body.d.ts +34 -0
  184. package/dist/utils/buildInfraweaverFooter.d.ts +29 -0
  185. package/dist/utils/byokFallback.d.ts +85 -0
  186. package/dist/utils/changeImpact.d.ts +101 -0
  187. package/dist/utils/claudeSubscription.d.ts +30 -0
  188. package/dist/utils/cli.d.ts +10 -0
  189. package/dist/utils/cloudClient.d.ts +33 -0
  190. package/dist/utils/cloudFetchCore.d.ts +42 -0
  191. package/dist/utils/cloudReport.d.ts +114 -0
  192. package/dist/utils/codexHome.d.ts +29 -0
  193. package/dist/utils/codexOAuth.d.ts +60 -0
  194. package/dist/utils/diffCoverage.d.ts +63 -0
  195. package/dist/utils/env.d.ts +46 -0
  196. package/dist/utils/errorReport.d.ts +17 -0
  197. package/dist/utils/exitHandler.d.ts +8 -0
  198. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  199. package/dist/utils/gitAuth.d.ts +84 -0
  200. package/dist/utils/gitAuthServer.d.ts +24 -0
  201. package/dist/utils/github.d.ts +104 -0
  202. package/dist/utils/globals.d.ts +3 -0
  203. package/dist/utils/infraweaverConfig.d.ts +56 -0
  204. package/dist/utils/install.d.ts +37 -0
  205. package/dist/utils/instructions.d.ts +48 -0
  206. package/dist/utils/keylessOidc.d.ts +28 -0
  207. package/dist/utils/leapingComment.d.ts +11 -0
  208. package/dist/utils/learnings.d.ts +62 -0
  209. package/dist/utils/learningsTruncate.d.ts +25 -0
  210. package/dist/utils/lifecycle.d.ts +57 -0
  211. package/dist/utils/log.d.ts +111 -0
  212. package/dist/utils/modelResolver.d.ts +63 -0
  213. package/dist/utils/moduleFetch.d.ts +39 -0
  214. package/dist/utils/normalizeEnv.d.ts +30 -0
  215. package/dist/utils/openCodeModels.d.ts +11 -0
  216. package/dist/utils/openaiCompatible.d.ts +26 -0
  217. package/dist/utils/opentofuMcp.d.ts +50 -0
  218. package/dist/utils/overrides.d.ts +40 -0
  219. package/dist/utils/packageManager.d.ts +49 -0
  220. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  221. package/dist/utils/payload.d.ts +249 -0
  222. package/dist/utils/prSummary.d.ts +61 -0
  223. package/dist/utils/presets.d.ts +36 -0
  224. package/dist/utils/progressComment.d.ts +146 -0
  225. package/dist/utils/providerErrors.d.ts +31 -0
  226. package/dist/utils/proxyModel.d.ts +76 -0
  227. package/dist/utils/rangeDiff.d.ts +51 -0
  228. package/dist/utils/redact.d.ts +33 -0
  229. package/dist/utils/registryAuth.d.ts +52 -0
  230. package/dist/utils/remediationCommand.d.ts +58 -0
  231. package/dist/utils/retry.d.ts +38 -0
  232. package/dist/utils/reviewCleanup.d.ts +14 -0
  233. package/dist/utils/run.d.ts +9 -0
  234. package/dist/utils/runContext.d.ts +112 -0
  235. package/dist/utils/runContextData.d.ts +42 -0
  236. package/dist/utils/runErrorRenderer.d.ts +64 -0
  237. package/dist/utils/runLifecycle.d.ts +89 -0
  238. package/dist/utils/runStartupLog.d.ts +15 -0
  239. package/dist/utils/secrets.d.ts +34 -0
  240. package/dist/utils/setup.d.ts +90 -0
  241. package/dist/utils/shell.d.ts +44 -0
  242. package/dist/utils/skills.d.ts +10 -0
  243. package/dist/utils/subprocess.d.ts +81 -0
  244. package/dist/utils/terraformMcp.d.ts +46 -0
  245. package/dist/utils/time.d.ts +15 -0
  246. package/dist/utils/timer.d.ts +23 -0
  247. package/dist/utils/todoTracking.d.ts +16 -0
  248. package/dist/utils/token.d.ts +52 -0
  249. package/dist/utils/toolLicensing.d.ts +56 -0
  250. package/dist/utils/toolSelection.d.ts +73 -0
  251. package/dist/utils/toon.d.ts +16 -0
  252. package/dist/utils/version.d.ts +2 -0
  253. package/dist/utils/versioning.d.ts +7 -0
  254. package/dist/utils/vertex.d.ts +16 -0
  255. package/dist/utils/workflow.d.ts +13 -0
  256. package/package.json +132 -0
  257. package/src/agents/claude.ts +1588 -0
  258. package/src/agents/claudePretoolGate.ts +286 -0
  259. package/src/agents/dispatchMetrics.ts +162 -0
  260. package/src/agents/gateServer.ts +178 -0
  261. package/src/agents/index.ts +10 -0
  262. package/src/agents/nativeFsDenies.ts +164 -0
  263. package/src/agents/opencode.ts +1600 -0
  264. package/src/agents/opencodePlugin.ts +304 -0
  265. package/src/agents/opencodeShared.ts +134 -0
  266. package/src/agents/postRun.ts +615 -0
  267. package/src/agents/reviewer.ts +134 -0
  268. package/src/agents/sessionLabeler.ts +185 -0
  269. package/src/agents/shared.ts +377 -0
  270. package/src/agents/subagentModels.ts +40 -0
  271. package/src/agents/tokenQuota.ts +203 -0
  272. package/src/agents/writeGateSource.ts +148 -0
  273. package/src/agents/writePolicy.ts +132 -0
  274. package/src/brand.ts +84 -0
  275. package/src/cli.ts +117 -0
  276. package/src/commands/gha.ts +188 -0
  277. package/src/commands/mcp.ts +126 -0
  278. package/src/commands/verifyEvidence.ts +100 -0
  279. package/src/entry.ts +7 -0
  280. package/src/entryPost.ts +109 -0
  281. package/src/external.ts +308 -0
  282. package/src/i18n/scaffolding.ts +191 -0
  283. package/src/index.ts +7 -0
  284. package/src/internal/index.ts +74 -0
  285. package/src/lifecycle.ts +2 -0
  286. package/src/main.ts +815 -0
  287. package/src/mcp/__fixtures__/infraweaver-scratch-pr-49-review-3485940013.json +110 -0
  288. package/src/mcp/__fixtures__/infraweaver-scratch-pr-64-review-3531000326.json +14 -0
  289. package/src/mcp/__fixtures__/infraweaver-test-repo-pr-1.diff.json +67 -0
  290. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  291. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  292. package/src/mcp/arkConfig.ts +7 -0
  293. package/src/mcp/assess.ts +723 -0
  294. package/src/mcp/capabilityContext.ts +80 -0
  295. package/src/mcp/changeSummary.ts +145 -0
  296. package/src/mcp/checkSuite.ts +261 -0
  297. package/src/mcp/checkout.ts +1015 -0
  298. package/src/mcp/comment.ts +683 -0
  299. package/src/mcp/commitInfo.ts +57 -0
  300. package/src/mcp/crosswalk.ts +1253 -0
  301. package/src/mcp/crosswalkDigest.ts +5 -0
  302. package/src/mcp/cyberEssentials.ts +60 -0
  303. package/src/mcp/dashboard.ts +232 -0
  304. package/src/mcp/dependencies.ts +190 -0
  305. package/src/mcp/frameworks.ts +236 -0
  306. package/src/mcp/geminiSanitizer.ts +212 -0
  307. package/src/mcp/git.ts +1116 -0
  308. package/src/mcp/guardrails.ts +771 -0
  309. package/src/mcp/issue.ts +74 -0
  310. package/src/mcp/issueComments.ts +48 -0
  311. package/src/mcp/issueEvents.ts +100 -0
  312. package/src/mcp/issueInfo.ts +72 -0
  313. package/src/mcp/labels.ts +35 -0
  314. package/src/mcp/localContext.ts +65 -0
  315. package/src/mcp/localServer.ts +217 -0
  316. package/src/mcp/moduleExtraction.ts +368 -0
  317. package/src/mcp/moduleTests.ts +421 -0
  318. package/src/mcp/modules.ts +752 -0
  319. package/src/mcp/output.ts +71 -0
  320. package/src/mcp/pathSafety.ts +28 -0
  321. package/src/mcp/policy.ts +226 -0
  322. package/src/mcp/pr.ts +238 -0
  323. package/src/mcp/prInfo.ts +91 -0
  324. package/src/mcp/providerSchema.ts +175 -0
  325. package/src/mcp/review.ts +1081 -0
  326. package/src/mcp/reviewComments.ts +1137 -0
  327. package/src/mcp/roots.ts +217 -0
  328. package/src/mcp/scope.ts +82 -0
  329. package/src/mcp/selectMode.ts +207 -0
  330. package/src/mcp/server.ts +498 -0
  331. package/src/mcp/shared.ts +120 -0
  332. package/src/mcp/shell.ts +631 -0
  333. package/src/mcp/staleFix.ts +557 -0
  334. package/src/mcp/terraform/__snapshots__/moduleDocs.test.ts.snap +40 -0
  335. package/src/mcp/terraform/azurePrices.ts +38 -0
  336. package/src/mcp/terraform/baseline.ts +172 -0
  337. package/src/mcp/terraform/concernResult.ts +111 -0
  338. package/src/mcp/terraform/cost.ts +175 -0
  339. package/src/mcp/terraform/cspm.ts +326 -0
  340. package/src/mcp/terraform/currency.ts +354 -0
  341. package/src/mcp/terraform/decisions.ts +590 -0
  342. package/src/mcp/terraform/eidas.ts +134 -0
  343. package/src/mcp/terraform/evidence.ts +771 -0
  344. package/src/mcp/terraform/findings.ts +383 -0
  345. package/src/mcp/terraform/fixMemory.ts +253 -0
  346. package/src/mcp/terraform/governance.ts +231 -0
  347. package/src/mcp/terraform/hcl.ts +273 -0
  348. package/src/mcp/terraform/iacLanguages.ts +431 -0
  349. package/src/mcp/terraform/idleFloor.ts +304 -0
  350. package/src/mcp/terraform/keyless.ts +123 -0
  351. package/src/mcp/terraform/moduleDocs.ts +303 -0
  352. package/src/mcp/terraform/nativeRules.ts +152 -0
  353. package/src/mcp/terraform/nativeScan.ts +67 -0
  354. package/src/mcp/terraform/noiseProfile.ts +183 -0
  355. package/src/mcp/terraform/normalization.ts +265 -0
  356. package/src/mcp/terraform/oscal.ts +283 -0
  357. package/src/mcp/terraform/packs/azurerm-3-4.ts +382 -0
  358. package/src/mcp/terraform/packs/cloudflare-4-5.ts +114 -0
  359. package/src/mcp/terraform/paths.ts +59 -0
  360. package/src/mcp/terraform/plan.ts +377 -0
  361. package/src/mcp/terraform/planDrift.ts +520 -0
  362. package/src/mcp/terraform/policyAuthor.ts +886 -0
  363. package/src/mcp/terraform/prowlerOcsf.ts +380 -0
  364. package/src/mcp/terraform/refactor.ts +879 -0
  365. package/src/mcp/terraform/registry.ts +317 -0
  366. package/src/mcp/terraform/risk.ts +99 -0
  367. package/src/mcp/terraform/scanSession.ts +242 -0
  368. package/src/mcp/terraform/scannerCache.ts +86 -0
  369. package/src/mcp/terraform/scannerEgress.ts +141 -0
  370. package/src/mcp/terraform/scannerJson.ts +22 -0
  371. package/src/mcp/terraform/scanners.ts +1134 -0
  372. package/src/mcp/terraform/signing.ts +151 -0
  373. package/src/mcp/terraform/stateLocking.ts +106 -0
  374. package/src/mcp/terraform/subprocess.ts +102 -0
  375. package/src/mcp/terraform/suppressions.ts +551 -0
  376. package/src/mcp/terraform/taxonomy.ts +0 -0
  377. package/src/mcp/terraform/tfquery.ts +159 -0
  378. package/src/mcp/terraform/toolchainIntegrity.ts +151 -0
  379. package/src/mcp/terraform/tools/authorPolicy.ts +185 -0
  380. package/src/mcp/terraform/tools/detectPlanDrift.ts +279 -0
  381. package/src/mcp/terraform/tools/emitCost.ts +130 -0
  382. package/src/mcp/terraform/tools/emitSarif.ts +108 -0
  383. package/src/mcp/terraform/tools/fixMemory.ts +82 -0
  384. package/src/mcp/terraform/tools/infracostDiff.ts +110 -0
  385. package/src/mcp/terraform/tools/ingestExternalFindings.ts +259 -0
  386. package/src/mcp/terraform/tools/moduleLookup.ts +86 -0
  387. package/src/mcp/terraform/tools/moduleSearch.ts +46 -0
  388. package/src/mcp/terraform/tools/plan.ts +384 -0
  389. package/src/mcp/terraform/tools/providerUpgrade.ts +217 -0
  390. package/src/mcp/terraform/tools/readFindings.ts +138 -0
  391. package/src/mcp/terraform/tools/scan.ts +303 -0
  392. package/src/mcp/terraform/tools/validate.ts +160 -0
  393. package/src/mcp/terraform/tools/verifyRemediation.ts +190 -0
  394. package/src/mcp/terraform/tools/versionCurrency.ts +64 -0
  395. package/src/mcp/terraform/tools.ts +62 -0
  396. package/src/mcp/terraform/transformPacks.ts +594 -0
  397. package/src/mcp/terraform/types.ts +382 -0
  398. package/src/mcp/terraform/verification.ts +150 -0
  399. package/src/mcp/terraform/vex.ts +367 -0
  400. package/src/mcp/terraform/writeOnly.ts +252 -0
  401. package/src/mcp/terraform.ts +52 -0
  402. package/src/mcp/terratest.ts +215 -0
  403. package/src/mcp/untrustedContent.ts +126 -0
  404. package/src/mcp/upload.ts +123 -0
  405. package/src/models.ts +753 -0
  406. package/src/modes/address-reviews.ts +35 -0
  407. package/src/modes/assess.ts +27 -0
  408. package/src/modes/build.ts +86 -0
  409. package/src/modes/compliance-audit.ts +25 -0
  410. package/src/modes/cost-optimization.ts +36 -0
  411. package/src/modes/drift-detect.ts +31 -0
  412. package/src/modes/fix.ts +29 -0
  413. package/src/modes/incremental-review.ts +103 -0
  414. package/src/modes/index.ts +94 -0
  415. package/src/modes/modernize-deprecated.ts +36 -0
  416. package/src/modes/plan.ts +21 -0
  417. package/src/modes/policy-gate.ts +27 -0
  418. package/src/modes/prFormats.ts +325 -0
  419. package/src/modes/refactor.ts +74 -0
  420. package/src/modes/refresh-remediation.ts +40 -0
  421. package/src/modes/remediate-and-refactor.ts +42 -0
  422. package/src/modes/remediate.ts +78 -0
  423. package/src/modes/resolve-conflicts.ts +32 -0
  424. package/src/modes/review.ts +135 -0
  425. package/src/modes/summarize-pr.ts +42 -0
  426. package/src/modes/task.ts +26 -0
  427. package/src/modes/terraform-code-review.ts +68 -0
  428. package/src/modes/types.ts +17 -0
  429. package/src/modes/update-dependencies.ts +44 -0
  430. package/src/prep/index.ts +96 -0
  431. package/src/prep/installNodeDependencies.ts +251 -0
  432. package/src/prep/installPythonDependencies.ts +235 -0
  433. package/src/prep/types.ts +38 -0
  434. package/src/reviewQuality.ts +237 -0
  435. package/src/runCli.ts +335 -0
  436. package/src/skills/terraform-best-practices/SKILL.md +379 -0
  437. package/src/toolState.ts +241 -0
  438. package/src/utils/activity.ts +210 -0
  439. package/src/utils/agent.ts +245 -0
  440. package/src/utils/agentHangReport.ts +180 -0
  441. package/src/utils/annotations.ts +56 -0
  442. package/src/utils/apiFetch.ts +19 -0
  443. package/src/utils/apiKeys.ts +305 -0
  444. package/src/utils/apiUrl.ts +42 -0
  445. package/src/utils/assets.ts +123 -0
  446. package/src/utils/baseRefConfig.ts +254 -0
  447. package/src/utils/billingErrors.ts +210 -0
  448. package/src/utils/body.ts +168 -0
  449. package/src/utils/buildInfraweaverFooter.ts +104 -0
  450. package/src/utils/byokFallback.ts +137 -0
  451. package/src/utils/changeImpact.ts +330 -0
  452. package/src/utils/claudeSubscription.ts +93 -0
  453. package/src/utils/cli.ts +36 -0
  454. package/src/utils/cloudClient.ts +57 -0
  455. package/src/utils/cloudFetchCore.ts +139 -0
  456. package/src/utils/cloudReport.ts +316 -0
  457. package/src/utils/codexHome.ts +204 -0
  458. package/src/utils/codexOAuth.ts +154 -0
  459. package/src/utils/codexRefreshDetect.ts +36 -0
  460. package/src/utils/diffCoverage.ts +404 -0
  461. package/src/utils/env.ts +84 -0
  462. package/src/utils/errorReport.ts +94 -0
  463. package/src/utils/exitHandler.ts +35 -0
  464. package/src/utils/fixDoubleEscapedString.ts +9 -0
  465. package/src/utils/ghaCore.ts +13 -0
  466. package/src/utils/gitAuth.ts +268 -0
  467. package/src/utils/gitAuthServer.ts +186 -0
  468. package/src/utils/github.ts +676 -0
  469. package/src/utils/globals.ts +9 -0
  470. package/src/utils/humanEditCapture.ts +198 -0
  471. package/src/utils/infraweaverConfig.ts +202 -0
  472. package/src/utils/install.ts +279 -0
  473. package/src/utils/instructions.ts +640 -0
  474. package/src/utils/keylessOidc.ts +47 -0
  475. package/src/utils/leapingComment.ts +20 -0
  476. package/src/utils/learnings.ts +145 -0
  477. package/src/utils/learningsTruncate.ts +42 -0
  478. package/src/utils/lifecycle.ts +198 -0
  479. package/src/utils/log.ts +452 -0
  480. package/src/utils/modelResolver.ts +105 -0
  481. package/src/utils/moduleFetch.ts +138 -0
  482. package/src/utils/normalizeEnv.ts +106 -0
  483. package/src/utils/openCodeModels.ts +86 -0
  484. package/src/utils/openaiCompatible.ts +52 -0
  485. package/src/utils/opentofuMcp.ts +61 -0
  486. package/src/utils/overrides.ts +100 -0
  487. package/src/utils/packageManager.ts +257 -0
  488. package/src/utils/patchWorkflowRunFields.ts +173 -0
  489. package/src/utils/payload.ts +1206 -0
  490. package/src/utils/prSummary.ts +148 -0
  491. package/src/utils/presets.ts +105 -0
  492. package/src/utils/progressComment.ts +266 -0
  493. package/src/utils/providerErrors.ts +189 -0
  494. package/src/utils/proxyModel.ts +239 -0
  495. package/src/utils/rangeDiff.ts +182 -0
  496. package/src/utils/redact.ts +86 -0
  497. package/src/utils/registryAuth.ts +115 -0
  498. package/src/utils/remediationCommand.ts +144 -0
  499. package/src/utils/retry.ts +180 -0
  500. package/src/utils/reviewCleanup.ts +116 -0
  501. package/src/utils/run.ts +100 -0
  502. package/src/utils/runContext.ts +276 -0
  503. package/src/utils/runContextData.ts +121 -0
  504. package/src/utils/runErrorRenderer.ts +282 -0
  505. package/src/utils/runFixture.ts +91 -0
  506. package/src/utils/runLifecycle.ts +360 -0
  507. package/src/utils/runStartupLog.ts +67 -0
  508. package/src/utils/secrets.ts +208 -0
  509. package/src/utils/setup.ts +368 -0
  510. package/src/utils/shell.ts +132 -0
  511. package/src/utils/skills.ts +67 -0
  512. package/src/utils/subprocess.ts +474 -0
  513. package/src/utils/terraformMcp.ts +99 -0
  514. package/src/utils/time.ts +59 -0
  515. package/src/utils/timer.ts +72 -0
  516. package/src/utils/todoTracking.ts +168 -0
  517. package/src/utils/token.ts +294 -0
  518. package/src/utils/toolLicensing.ts +152 -0
  519. package/src/utils/toolSelection.ts +239 -0
  520. package/src/utils/toon.ts +76 -0
  521. package/src/utils/version.ts +10 -0
  522. package/src/utils/versioning.ts +44 -0
  523. package/src/utils/vertex.ts +94 -0
  524. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,1134 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { type } from "arktype";
4
+ import {
5
+ MAX_TF_TOTAL_BYTES,
6
+ readTextCapped,
7
+ walkTfFiles,
8
+ } from "#app/mcp/modules";
9
+ import {
10
+ loadProvidersSchema,
11
+ unknownArgsForResource,
12
+ } from "#app/mcp/providerSchema";
13
+ import { matchBraceBody, skipNonCode } from "#app/mcp/terraform/hcl";
14
+ import {
15
+ checkovTypeToLanguage,
16
+ trivyTypeToLanguage,
17
+ } from "#app/mcp/terraform/iacLanguages";
18
+ import { scanNative } from "#app/mcp/terraform/nativeScan";
19
+ import {
20
+ type ResolvedRoot,
21
+ resolveBaseRef,
22
+ resolveRoots,
23
+ toRepoRelative,
24
+ } from "#app/mcp/terraform/paths";
25
+ import {
26
+ egressEnvFor,
27
+ type ToolsNetworkDirective,
28
+ } from "#app/mcp/terraform/scannerEgress";
29
+ import { parseScannerJson } from "#app/mcp/terraform/scannerJson";
30
+ import { run, runTf } from "#app/mcp/terraform/subprocess";
31
+ import { assertToolchainIntegrity } from "#app/mcp/terraform/toolchainIntegrity";
32
+ import {
33
+ buildConcern,
34
+ type Concern,
35
+ dedupe,
36
+ lowerSeverity,
37
+ rebaseConcern,
38
+ type ScannerOutcome,
39
+ type Severity,
40
+ skipped,
41
+ } from "#app/mcp/terraform/types";
42
+ import { log } from "#app/utils/cli";
43
+ import {
44
+ type ResolvedToolSelection,
45
+ scannerToolId,
46
+ } from "#app/utils/toolSelection";
47
+
48
+ // dirs already `terraform init`-ed this process, so repeated scans don't re-init.
49
+ const initedDirs = new Set<string>();
50
+
51
+ /**
52
+ * Run `terraform init -backend=false` once per dir so `terraform validate` has
53
+ * provider schemas to check against (Bug 3 / gap B). Without init, validate only
54
+ * emits "missing required provider" — which VALIDATE_NOISE drops — so it was
55
+ * effectively inert. `-backend=false` avoids needing real backend credentials;
56
+ * `-input=false` keeps it non-interactive. Network-dependent and best-effort: if
57
+ * it fails (offline, private module, etc.) validate still runs, just shallow.
58
+ */
59
+ function ensureTerraformInit(
60
+ cwd: string,
61
+ extraEnv?: Record<string, string>,
62
+ ): void {
63
+ if (initedDirs.has(cwd)) return;
64
+ // `extraEnv` carries the optional module-fetch credential so init can
65
+ // resolve a private cross-repo `git::` module; absent for the common case.
66
+ // runTf falls back to `tofu` when `terraform` isn't installed.
67
+ const r = runTf(
68
+ ["init", "-backend=false", "-input=false", "-no-color"],
69
+ cwd,
70
+ extraEnv,
71
+ );
72
+ // mark done even on non-zero: a failed init won't succeed on retry within the
73
+ // same run, and we don't want to re-run it for every scanner call.
74
+ initedDirs.add(cwd);
75
+ if (r.status !== 0 && !r.missing) {
76
+ log.info(
77
+ `» terraform init (for validate) did not complete cleanly — validate may be shallow`,
78
+ );
79
+ }
80
+ }
81
+
82
+ // --- terraform fmt -------------------------------------------------------
83
+
84
+ export function scanFmt(cwd: string): ScannerOutcome {
85
+ // `terraform` when installed, else `tofu` — OpenTofu-only runners keep fmt
86
+ // coverage (identical CLI surface; the source label stays "terraform-fmt" so
87
+ // rule ids and content hashes never fork on the binary name).
88
+ const r = runTf(["fmt", "-check", "-recursive", "-list=true"], cwd);
89
+ if (r.missing)
90
+ return skipped("terraform-fmt", "terraform (or tofu) not installed");
91
+ // exit 0 = all formatted; exit 3 = files need formatting (lists them on stdout);
92
+ // other non-zero = real error (e.g. parse failure) — surface nothing, validate covers it.
93
+ if (r.status === 0)
94
+ return { source: "terraform-fmt", ran: true, concerns: [] };
95
+ return {
96
+ source: "terraform-fmt",
97
+ ran: true,
98
+ concerns: parseFmtOutput(r.stdout, cwd),
99
+ };
100
+ }
101
+
102
+ /** `terraform fmt -check -list=true` prints one unformatted file path per line. */
103
+ export function parseFmtOutput(stdout: string, cwd = ""): Concern[] {
104
+ const files = stdout
105
+ .split("\n")
106
+ .map((l) => l.trim())
107
+ .filter(Boolean);
108
+ return files.map<Concern>((raw) =>
109
+ buildConcern({
110
+ source: "terraform-fmt",
111
+ rule: "unformatted",
112
+ file: toRepoRelative(raw, cwd),
113
+ line: null,
114
+ severity: "low",
115
+ category: "style",
116
+ evidence: "File does not match `terraform fmt` canonical style.",
117
+ remediation_hint: "Run `terraform fmt` to apply canonical formatting.",
118
+ }),
119
+ );
120
+ }
121
+
122
+ // --- terraform validate ---------------------------------------------------
123
+
124
+ // diagnostics that are environmental (the dir isn't initialized, or a provider
125
+ // plugin failed to install/launch) rather than a real best-practice issue.
126
+ // dropped so a scan can't emit false positives from toolchain hiccups — e.g.
127
+ // after `terraform init` (Bug 3), a crashed provider plugin surfaces as
128
+ // "Failed to load plugin schemas", which is noise, not a defect in the HCL.
129
+ const VALIDATE_NOISE = [
130
+ "terraform init",
131
+ "missing required provider",
132
+ "module not installed",
133
+ "module is not yet installed",
134
+ "required plugins are not installed",
135
+ "uninitialized",
136
+ "failed to load plugin",
137
+ "plugin did not respond",
138
+ "could not load plugin",
139
+ ];
140
+
141
+ /** run `terraform validate` in one root and return concerns re-based onto cwd. */
142
+ function scanValidateRoot(
143
+ root: ResolvedRoot,
144
+ extraEnv?: Record<string, string>,
145
+ ): ScannerOutcome {
146
+ ensureTerraformInit(root.absDir, extraEnv);
147
+ const r = runTf(["validate", "-json"], root.absDir);
148
+ if (r.missing)
149
+ return skipped("terraform-validate", "terraform (or tofu) not installed");
150
+ try {
151
+ const concerns = parseValidateOutput(r.stdout, root.absDir).map((c) =>
152
+ rebaseConcern(c, root.relDir),
153
+ );
154
+ return { source: "terraform-validate", ran: true, concerns };
155
+ } catch {
156
+ // terraform ran but emitted output we couldn't parse — a real CLI-level
157
+ // failure (corrupted .terraform, a crash, an ancient terraform without
158
+ // `-json`), NOT a clean tree. Flag it as unvalidated so the tool fails
159
+ // closed rather than reporting this root as passing.
160
+ return {
161
+ ...skipped(
162
+ "terraform-validate",
163
+ "could not parse `terraform validate -json` output",
164
+ ),
165
+ unvalidated: 1,
166
+ };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Run `terraform validate` across EVERY root and aggregate. `validate` is the
172
+ * one scanner that's per-root (fmt/tflint/trivy/checkov are recursive over the
173
+ * whole tree), so a multi-root repo only catches subdir-root validate errors
174
+ * when we visit each root.
175
+ */
176
+ export function scanValidate(
177
+ cwd: string,
178
+ extraEnv?: Record<string, string>,
179
+ ): ScannerOutcome {
180
+ const roots = resolveRoots(cwd);
181
+ const concerns: Concern[] = [];
182
+ let anyRan = false;
183
+ let sawMissing = false;
184
+ let unvalidated = 0;
185
+ for (const root of roots) {
186
+ const outcome = scanValidateRoot(root, extraEnv);
187
+ unvalidated += outcome.unvalidated ?? 0;
188
+ if (outcome.ran) {
189
+ anyRan = true;
190
+ concerns.push(...outcome.concerns);
191
+ } else if (outcome.skipped_reason?.includes("not installed")) {
192
+ sawMissing = true;
193
+ }
194
+ }
195
+ if (!anyRan) {
196
+ return sawMissing
197
+ ? skipped("terraform-validate", "terraform not installed")
198
+ : {
199
+ ...skipped(
200
+ "terraform-validate",
201
+ "could not parse `terraform validate -json` output",
202
+ ),
203
+ unvalidated,
204
+ };
205
+ }
206
+ return {
207
+ source: "terraform-validate",
208
+ ran: true,
209
+ concerns: dedupe(concerns),
210
+ unvalidated,
211
+ };
212
+ }
213
+
214
+ /** parse `terraform validate -json`; keeps real errors, drops uninitialized-dir noise. */
215
+ export function parseValidateOutput(stdout: string, cwd = ""): Concern[] {
216
+ const parsed = parseScannerJson(ValidateOutput, stdout, "terraform validate");
217
+ const diags = (parsed.diagnostics ?? []).filter(
218
+ (d) => d.severity === "error",
219
+ );
220
+ const concerns: Concern[] = [];
221
+ for (const d of diags) {
222
+ const text = `${d.summary ?? ""} ${d.detail ?? ""}`.toLowerCase();
223
+ if (VALIDATE_NOISE.some((n) => text.includes(n))) continue;
224
+ concerns.push(
225
+ buildConcern({
226
+ source: "terraform-validate",
227
+ rule: d.summary ?? "error",
228
+ file: toRepoRelative(d.range?.filename, cwd),
229
+ line: d.range?.start?.line ?? null,
230
+ severity: "high",
231
+ category: "correctness",
232
+ evidence: [d.summary, d.detail].filter(Boolean).join(" — "),
233
+ remediation_hint: null,
234
+ }),
235
+ );
236
+ }
237
+ return concerns;
238
+ }
239
+
240
+ const ValidateDiagnostic = type({
241
+ "severity?": "string",
242
+ "summary?": "string",
243
+ "detail?": "string",
244
+ "range?": { "filename?": "string", "start?": { "line?": "number" } },
245
+ });
246
+ const ValidateOutput = type({
247
+ "diagnostics?": ValidateDiagnostic.array().or("null"),
248
+ });
249
+
250
+ // --- provider-version awareness ------------------------------------
251
+
252
+ export interface ProviderRequirement {
253
+ /** local name, e.g. `aws`. */
254
+ name: string;
255
+ /** registry source, e.g. `hashicorp/aws`, or null (legacy string form). */
256
+ source: string | null;
257
+ /** raw version constraint, e.g. `~> 5.0`, or null when unconstrained. */
258
+ version: string | null;
259
+ /** the pinned MAJOR (the lower-bound major of the constraint) — the number a
260
+ * fix must target, since argument schemas differ across provider majors. */
261
+ major: number | null;
262
+ }
263
+
264
+ /** the lower-bound major version from a constraint string (`~> 5.0` → 5,
265
+ * `>= 3.1, < 4.0` → 3, `5` → 5). null when no number is present. */
266
+ function majorOf(version: string | null): number | null {
267
+ if (!version) return null;
268
+ const m = version.match(/(\d+)\s*\.\s*\d+/) ?? version.match(/(\d+)/);
269
+ return m ? Number(m[1]) : null;
270
+ }
271
+
272
+ /**
273
+ * Parse every `required_providers { … }` block in some HCL text into the pinned
274
+ * provider requirements. Handles the modern object form
275
+ * (`aws = { source = "hashicorp/aws", version = "~> 5.0" }`) and the legacy
276
+ * string form (`aws = "~> 5.0"`). A repo's "correct" fix depends on the provider
277
+ * MAJOR — argument names and valid blocks differ across AWS/Azure majors — so
278
+ * surfacing the pinned major lets a fix target the right schema instead of
279
+ * breaking `plan`. Brace-matched (not a fragile single regex) so nested objects
280
+ * don't confuse it. First declaration of a name wins (dedup across files).
281
+ */
282
+ export function parseRequiredProviders(hcl: string): ProviderRequirement[] {
283
+ const out: ProviderRequirement[] = [];
284
+ const seen = new Set<string>();
285
+ let searchFrom = 0;
286
+ for (;;) {
287
+ const idx = hcl.indexOf("required_providers", searchFrom);
288
+ if (idx === -1) break;
289
+ const braceStart = hcl.indexOf("{", idx);
290
+ if (braceStart === -1) break;
291
+ let depth = 0;
292
+ let end = -1;
293
+ for (let i = braceStart; i < hcl.length; i++) {
294
+ if (hcl[i] === "{") depth++;
295
+ else if (hcl[i] === "}") {
296
+ depth--;
297
+ if (depth === 0) {
298
+ end = i;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ if (end === -1) break;
304
+ const body = hcl.slice(braceStart + 1, end);
305
+ searchFrom = end + 1;
306
+
307
+ // object form: name = { source = "…", version = "…" }
308
+ const objRe = /([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*\{([^}]*)\}/g;
309
+ let m: RegExpExecArray | null;
310
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex-exec iteration
311
+ while ((m = objRe.exec(body)) !== null) {
312
+ // destructure with defaults — `name` is a required group, `inner` is `[^}]*`
313
+ // (may be empty); the "" defaults only stand in for a non-match that can't happen.
314
+ const [, name = "", inner = ""] = m;
315
+ if (seen.has(name)) continue;
316
+ seen.add(name);
317
+ const source = inner.match(/source\s*=\s*"([^"]+)"/)?.[1] ?? null;
318
+ const version = inner.match(/version\s*=\s*"([^"]+)"/)?.[1] ?? null;
319
+ out.push({ name, source, version, major: majorOf(version) });
320
+ }
321
+ // legacy string form: name = "version" — run on the body with object blocks
322
+ // stripped so an object's inner `source =`/`version =` lines aren't matched.
323
+ const bodyNoObjects = body.replace(
324
+ /([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*\{[^}]*\}/g,
325
+ "",
326
+ );
327
+ const strRe = /([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*"([^"]+)"/g;
328
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex-exec iteration
329
+ while ((m = strRe.exec(bodyNoObjects)) !== null) {
330
+ const [, name = "", version = ""] = m;
331
+ if (seen.has(name)) continue;
332
+ seen.add(name);
333
+ out.push({ name, source: null, version, major: majorOf(version) });
334
+ }
335
+ }
336
+ return out;
337
+ }
338
+
339
+ /** read the repo's `*.tf` files (recursively — root + subdir roots + nested
340
+ * modules) and parse their pinned provider requirements (best-effort; an
341
+ * unreadable tree yields none). First declaration of a provider wins. */
342
+ export function collectProviderRequirements(
343
+ cwd: string,
344
+ ): ProviderRequirement[] {
345
+ let text = "";
346
+ for (const f of walkTfFiles(cwd)) {
347
+ // byte-capped per file and in aggregate: a hostile repo with one multi-GB
348
+ // `.tf` would otherwise OOM the run near V8's string limit.
349
+ if (text.length >= MAX_TF_TOTAL_BYTES) break;
350
+ const t = readTextCapped(join(cwd, f));
351
+ if (t !== null) text += `${t}\n`;
352
+ }
353
+ return parseRequiredProviders(text);
354
+ }
355
+
356
+ // --- argument-vs-schema validation ------------------------------
357
+
358
+ /** the top-level arguments of a single `resource` block. */
359
+ export interface ResourceArguments {
360
+ resourceType: string;
361
+ /** the resource's local name (`resource "aws_s3_bucket" "<name>"`). */
362
+ name: string;
363
+ /** top-level attribute + nested-block names (meta-arguments excluded). */
364
+ args: string[];
365
+ }
366
+
367
+ // Terraform meta-arguments are valid on EVERY resource and never appear in a
368
+ // provider's schema — exclude them so they're not flagged as unknown. `dynamic`
369
+ // is handled specially (its quoted label is the real block name).
370
+ const RESOURCE_META_ARGUMENTS: ReadonlySet<string> = new Set([
371
+ "count",
372
+ "for_each",
373
+ "provider",
374
+ "depends_on",
375
+ "lifecycle",
376
+ "provisioner",
377
+ "connection",
378
+ ]);
379
+
380
+ /**
381
+ * Parse every `resource "<type>" "<name>" { … }` block's TOP-LEVEL argument
382
+ * names (attributes assigned with `=` and nested block labels) from some HCL.
383
+ * Conservative by design — it skips `"…"` strings and `#`/`//` line comments so
384
+ * an interpolation's braces or a commented line can't corrupt the brace depth or
385
+ * fabricate an argument, and only reports depth-0 names. A `dynamic "x"` block
386
+ * contributes `x` (the generated block type). Used to cross-check written
387
+ * arguments against the installed provider schema; pure.
388
+ */
389
+ export function parseResourceArguments(hcl: string): ResourceArguments[] {
390
+ const out: ResourceArguments[] = [];
391
+ const re = /(?:^|\n)\s*resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/g;
392
+ let m: RegExpExecArray | null;
393
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex-exec iteration
394
+ while ((m = re.exec(hcl)) !== null) {
395
+ const [, resourceType = "", name = ""] = m;
396
+ const braceStart = hcl.indexOf("{", m.index);
397
+ if (braceStart === -1) break;
398
+ const body = matchBraceBody(hcl, braceStart);
399
+ if (!body) break;
400
+ re.lastIndex = body.end + 1;
401
+ out.push({ resourceType, name, args: topLevelArgNames(body.text) });
402
+ }
403
+ return out;
404
+ }
405
+
406
+ /** extract the depth-0 argument names from a resource block body (string-,
407
+ * comment-, and heredoc-aware), excluding meta-arguments. */
408
+ function topLevelArgNames(body: string): string[] {
409
+ const names = new Set<string>();
410
+ let depth = 0;
411
+ let atStmtStart = true;
412
+ for (let i = 0; i < body.length; i++) {
413
+ const span = skipNonCode(body, i);
414
+ if (span) {
415
+ i = span.end;
416
+ // a comment/heredoc ends a line (next token is a fresh statement); a
417
+ // string ends mid-line (still inside the current statement).
418
+ atStmtStart = span.endsLine;
419
+ continue;
420
+ }
421
+ const ch = body[i]!;
422
+ if (ch === "{") {
423
+ depth++;
424
+ atStmtStart = false;
425
+ continue;
426
+ }
427
+ if (ch === "}") {
428
+ depth--;
429
+ atStmtStart = false;
430
+ continue;
431
+ }
432
+ if (ch === "\n") {
433
+ atStmtStart = true;
434
+ continue;
435
+ }
436
+ if (ch === " " || ch === "\t" || ch === "\r") continue;
437
+ // a non-space char that starts a statement at depth 0 → read an identifier.
438
+ if (depth === 0 && atStmtStart && /[A-Za-z_]/.test(ch)) {
439
+ let j = i;
440
+ while (j < body.length && /[A-Za-z0-9_-]/.test(body[j]!)) j++;
441
+ const ident = body.slice(i, j);
442
+ // skip whitespace after the identifier to classify it.
443
+ let k = j;
444
+ while (k < body.length && (body[k] === " " || body[k] === "\t")) k++;
445
+ const next = body[k];
446
+ if (next === "=" && body[k + 1] !== "=") {
447
+ // attribute assignment: `name = …`
448
+ if (!RESOURCE_META_ARGUMENTS.has(ident)) names.add(ident);
449
+ } else if (next === "{") {
450
+ // nested block: `name { … }`
451
+ if (!RESOURCE_META_ARGUMENTS.has(ident)) names.add(ident);
452
+ } else if (next === '"') {
453
+ // labeled block: `dynamic "x" {` → the generated block is `x`;
454
+ // other labeled blocks (`provisioner "remote-exec"`) are meta.
455
+ if (ident === "dynamic") {
456
+ const labelEnd = body.indexOf('"', k + 1);
457
+ if (labelEnd !== -1) names.add(body.slice(k + 1, labelEnd));
458
+ }
459
+ }
460
+ i = j - 1;
461
+ atStmtStart = false;
462
+ continue;
463
+ }
464
+ atStmtStart = false;
465
+ }
466
+ return [...names];
467
+ }
468
+
469
+ export interface UnknownArgument {
470
+ resource_type: string;
471
+ /** the resource's local name. */
472
+ name: string;
473
+ /** repo-relative file the resource is declared in. */
474
+ file: string;
475
+ /** the argument names not present in the installed provider's schema. */
476
+ unknown: string[];
477
+ }
478
+
479
+ /**
480
+ * Cross-check every resource's written arguments against the
481
+ * INSTALLED provider's schema, so an argument that's invalid for the pinned
482
+ * provider major (a "correct" fix for the wrong version) is caught at validate
483
+ * time, not as a later `plan` failure. Degrades green: returns
484
+ * `{ checked: false }` when the schema is unavailable (terraform not installed /
485
+ * dir not init-ed). A resource type absent from the schema is skipped (can't
486
+ * judge), never flagged.
487
+ */
488
+ export function checkArgumentsAgainstSchema(cwd: string): {
489
+ checked: boolean;
490
+ unknown_arguments: UnknownArgument[];
491
+ } {
492
+ const schema = loadProvidersSchema(cwd);
493
+ if (!schema) return { checked: false, unknown_arguments: [] };
494
+ const out: UnknownArgument[] = [];
495
+ for (const f of walkTfFiles(cwd)) {
496
+ const text = readTextCapped(join(cwd, f));
497
+ if (text === null) continue;
498
+ for (const block of parseResourceArguments(text)) {
499
+ const verdict = unknownArgsForResource(
500
+ schema,
501
+ block.resourceType,
502
+ block.args,
503
+ );
504
+ if (!verdict.unknownResourceType && verdict.unknown.length > 0) {
505
+ out.push({
506
+ resource_type: block.resourceType,
507
+ name: block.name,
508
+ file: f,
509
+ unknown: verdict.unknown,
510
+ });
511
+ }
512
+ }
513
+ }
514
+ return { checked: true, unknown_arguments: out };
515
+ }
516
+
517
+ /** A parsed resource block plus the file it came from. `parseResourceArguments`
518
+ * works on one file's text and so cannot carry a location; the transform-pack
519
+ * matcher (TC-2.2) needs one to tell the agent WHERE to edit. */
520
+ export interface LocatedResource extends ResourceArguments {
521
+ /** repo-relative file the resource is declared in. */
522
+ file: string;
523
+ }
524
+
525
+ /** Every `resource` block under `cwd`, with its file. Best-effort in the same
526
+ * way as `collectProviderRequirements` — an unreadable file is skipped rather
527
+ * than failing the walk. */
528
+ export function collectResourceArguments(cwd: string): LocatedResource[] {
529
+ const out: LocatedResource[] = [];
530
+ for (const f of walkTfFiles(cwd)) {
531
+ const text = readTextCapped(join(cwd, f));
532
+ if (text === null) continue;
533
+ for (const block of parseResourceArguments(text))
534
+ out.push({ ...block, file: f });
535
+ }
536
+ return out;
537
+ }
538
+
539
+ // --- tflint ---------------------------------------------------------------
540
+
541
+ function tflintSeverity(s: string | undefined): Severity {
542
+ switch ((s ?? "").toLowerCase()) {
543
+ case "error":
544
+ return "high";
545
+ case "warning":
546
+ return "medium";
547
+ default:
548
+ return "low";
549
+ }
550
+ }
551
+
552
+ // dirs we've already attempted `tflint --init` in, so repeated scans don't re-init.
553
+ const tflintInitedDirs = new Set<string>();
554
+
555
+ /**
556
+ * Install tflint's provider ruleset plugins via `tflint --init` when the dir has
557
+ * a `.tflint.hcl` declaring them. Core `tflint --recursive` runs only the
558
+ * built-in rules; the high-value provider rules (deprecated args, invalid
559
+ * instance types, missing-tag policies, etc.) live in the aws/azurerm/google
560
+ * plugins, which must be installed first. Opt-in by design — we only init when
561
+ * the repo ships a `.tflint.hcl`, so we don't force AWS rules onto an Azure/GCP
562
+ * repo. Best-effort and network-dependent: a failed init just leaves tflint
563
+ * running its core rules, exactly as before.
564
+ */
565
+ function ensureTflintInit(
566
+ cwd: string,
567
+ extraEnv?: Record<string, string>,
568
+ ): void {
569
+ if (tflintInitedDirs.has(cwd)) return;
570
+ // mark first: a failed init won't succeed on retry within the same run, and
571
+ // we don't want to re-attempt the network fetch on every scanner call.
572
+ tflintInitedDirs.add(cwd);
573
+ if (!existsSync(join(cwd, ".tflint.hcl"))) return;
574
+ // under egress denial the overlay still admits GitHub's release hosts, so
575
+ // ruleset plugins install; everything else dies at the blackhole proxy.
576
+ const r = run("tflint", ["--init"], cwd, extraEnv);
577
+ if (r.status !== 0 && !r.missing) {
578
+ log.info(
579
+ "» tflint --init did not complete cleanly — provider ruleset plugins may be unavailable",
580
+ );
581
+ }
582
+ }
583
+
584
+ export function scanTflint(
585
+ cwd: string,
586
+ network?: ToolsNetworkDirective,
587
+ ): ScannerOutcome {
588
+ assertToolchainIntegrity("tflint", cwd);
589
+ const egress = egressEnvFor("tflint", network);
590
+ ensureTflintInit(cwd, egress ?? undefined);
591
+ const r = run(
592
+ "tflint",
593
+ ["--format", "json", "--recursive"],
594
+ cwd,
595
+ egress ?? undefined,
596
+ );
597
+ if (r.missing) return skipped("tflint", "tflint not installed");
598
+ try {
599
+ return {
600
+ source: "tflint",
601
+ ran: true,
602
+ concerns: parseTflintOutput(r.stdout, cwd),
603
+ ...(egress ? { egress_isolation: "partial" as const } : {}),
604
+ };
605
+ } catch {
606
+ return skipped("tflint", "could not parse tflint json output");
607
+ }
608
+ }
609
+
610
+ /** parse `tflint --format json` output into concerns. */
611
+ export function parseTflintOutput(stdout: string, cwd = ""): Concern[] {
612
+ const parsed = parseScannerJson(TflintOutput, stdout, "tflint");
613
+ return (parsed.issues ?? []).map<Concern>((issue) => {
614
+ const rule = issue.rule?.name ?? "issue";
615
+ return buildConcern({
616
+ source: "tflint",
617
+ rule,
618
+ file: toRepoRelative(issue.range?.filename, cwd),
619
+ line: issue.range?.start?.line ?? null,
620
+ severity: tflintSeverity(issue.rule?.severity),
621
+ category: "style",
622
+ evidence: issue.message ?? rule,
623
+ remediation_hint: issue.rule?.link ?? null,
624
+ });
625
+ });
626
+ }
627
+
628
+ const TflintIssue = type({
629
+ "rule?": { "name?": "string", "severity?": "string", "link?": "string" },
630
+ "message?": "string",
631
+ "range?": { "filename?": "string", "start?": { "line?": "number" } },
632
+ });
633
+ const TflintOutput = type({ "issues?": TflintIssue.array().or("null") });
634
+
635
+ // --- trivy ----------------------------------------------------------------
636
+
637
+ // tfsec was archived by Aqua and folded into Trivy; `trivy config` is its
638
+ // maintained successor with a larger ruleset (the AVD-* checks). `--quiet`
639
+ // keeps Trivy's progress chatter off stdout so the JSON parses cleanly.
640
+ function scanTrivy(
641
+ cwd: string,
642
+ network?: ToolsNetworkDirective,
643
+ ): ScannerOutcome {
644
+ // Supply-chain gate: refuse to execute a known-compromised trivy build
645
+ // (GHSA-69fq-xp46-6x23). Throws — a backdoored scanner is not a 'skip'.
646
+ assertToolchainIntegrity("trivy", cwd);
647
+ const egress = egressEnvFor("trivy", network);
648
+ const r = run(
649
+ "trivy",
650
+ ["config", "--format", "json", "--quiet", "."],
651
+ cwd,
652
+ egress ?? undefined,
653
+ );
654
+ if (r.missing) return skipped("trivy", "trivy not installed");
655
+ try {
656
+ return {
657
+ source: "trivy",
658
+ ran: true,
659
+ concerns: parseTrivyOutput(r.stdout, cwd),
660
+ ...(egress ? { egress_isolation: "partial" as const } : {}),
661
+ };
662
+ } catch {
663
+ return skipped("trivy", "could not parse trivy json output");
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Parse `trivy config --format json` output into concerns. Trivy nests
669
+ * misconfigurations under `Results[].Misconfigurations[]`, keyed to the result's
670
+ * `Target` file. `trivy config` reports only failures by default, but we
671
+ * defensively drop any `Status: "PASS"` entry so an `--include-non-failures`
672
+ * run can't leak passing checks into the concern set. Each concern carries the
673
+ * IaC language from the result's `Type` (terraform / cloudformation / kubernetes
674
+ * / helm / dockerfile / azure-arm), which is what the read-only surfaces scope on
675
+ * — trivy scans every format it finds, and the language tag keeps that honest.
676
+ */
677
+ export function parseTrivyOutput(stdout: string, cwd = ""): Concern[] {
678
+ const parsed = parseScannerJson(TrivyOutput, stdout, "trivy");
679
+ const concerns: Concern[] = [];
680
+ for (const result of parsed.Results ?? []) {
681
+ const file = toRepoRelative(result.Target, cwd);
682
+ const language = trivyTypeToLanguage(result.Type);
683
+ for (const m of result.Misconfigurations ?? []) {
684
+ if (m.Status === "PASS") continue;
685
+ const rule = m.AVDID || m.ID || "issue";
686
+ const start = m.CauseMetadata?.StartLine;
687
+ concerns.push(
688
+ buildConcern({
689
+ source: "trivy",
690
+ rule,
691
+ file,
692
+ line: typeof start === "number" && start > 0 ? start : null,
693
+ severity: lowerSeverity(m.Severity),
694
+ category: "security",
695
+ evidence: m.Message || m.Description || m.Title || rule,
696
+ code_snippet: trivyCodeSnippet(m.CauseMetadata?.Code?.Lines),
697
+ remediation_hint: m.Resolution || m.References?.[0] || null,
698
+ language,
699
+ }),
700
+ );
701
+ }
702
+ }
703
+ return concerns;
704
+ }
705
+
706
+ const TrivyCodeLine = type({
707
+ "Number?": "number",
708
+ "Content?": "string",
709
+ "IsCause?": "boolean",
710
+ "Truncated?": "boolean",
711
+ });
712
+ type TrivyCodeLine = typeof TrivyCodeLine.infer;
713
+
714
+ const TrivyMisconfiguration = type({
715
+ "ID?": "string",
716
+ "AVDID?": "string",
717
+ "Title?": "string",
718
+ "Description?": "string",
719
+ "Message?": "string",
720
+ "Resolution?": "string",
721
+ "Severity?": "string",
722
+ "References?": "string[]",
723
+ "Status?": "string",
724
+ "CauseMetadata?": {
725
+ "StartLine?": "number",
726
+ "EndLine?": "number",
727
+ "Code?": { "Lines?": TrivyCodeLine.array().or("null") },
728
+ },
729
+ });
730
+
731
+ // Max source lines (and chars) we keep for a finding's snippet — enough to show
732
+ // the offending block, bounded so a pathological scan can't bloat the report.
733
+ const SNIPPET_MAX_LINES = 8;
734
+ const SNIPPET_MAX_CHARS = 1000;
735
+
736
+ /** The offending source from a trivy finding's `CauseMetadata.Code.Lines`:
737
+ * prefer the lines trivy marked as the cause, else the whole shown window.
738
+ * Returns null when trivy attached no code (older trivy / `--quiet` edge). */
739
+ function trivyCodeSnippet(
740
+ lines: TrivyCodeLine[] | null | undefined,
741
+ ): string | null {
742
+ if (!lines?.length) return null;
743
+ const cause = lines.filter((l) => l.IsCause && !l.Truncated);
744
+ const picked = (
745
+ cause.length ? cause : lines.filter((l) => !l.Truncated)
746
+ ).slice(0, SNIPPET_MAX_LINES);
747
+ return tidySnippet(picked.map((l) => l.Content ?? ""));
748
+ }
749
+
750
+ /**
751
+ * Best-effort fallback so EVERY located finding shows its code, not only the
752
+ * trivy/checkov ones whose scanner emits a source window. For any concern that
753
+ * has a line but no scanner-attached snippet (terraform-validate, tflint, …),
754
+ * read the flagged line from disk. Files are read once and cached; a missing or
755
+ * unreadable file just leaves the snippet null. Pure-ish (read-only I/O).
756
+ */
757
+ export function attachSourceSnippets(
758
+ concerns: Concern[],
759
+ cwd: string,
760
+ ): Concern[] {
761
+ const cache = new Map<string, string[] | null>();
762
+ const fileLines = (file: string): string[] | null => {
763
+ const cached = cache.get(file);
764
+ if (cached !== undefined) return cached;
765
+ let lines: string[] | null = null;
766
+ try {
767
+ const abs = join(cwd, file);
768
+ if (existsSync(abs)) lines = readTextCapped(abs)?.split(/\r?\n/) ?? null;
769
+ } catch {
770
+ lines = null;
771
+ }
772
+ cache.set(file, lines);
773
+ return lines;
774
+ };
775
+ return concerns.map((c) => {
776
+ if (c.code_snippet || c.location.line == null || !c.location.file) return c;
777
+ const raw = fileLines(c.location.file)?.[c.location.line - 1];
778
+ if (raw == null) return c;
779
+ const snippet = tidySnippet([raw]);
780
+ return snippet ? { ...c, code_snippet: snippet } : c;
781
+ });
782
+ }
783
+
784
+ /** Join source lines into a clipped snippet, dropping a common leading indent so
785
+ * the code reads cleanly in the narrow finding panel. Returns null when empty. */
786
+ function tidySnippet(rawLines: string[]): string | null {
787
+ const lines = rawLines.map((l) => l.replace(/\s+$/, ""));
788
+ while (lines.length && lines[0]?.trim() === "") lines.shift();
789
+ while (lines.length && lines[lines.length - 1]?.trim() === "") lines.pop();
790
+ if (!lines.length) return null;
791
+ const indent = Math.min(
792
+ ...lines
793
+ .filter((l) => l.trim() !== "")
794
+ .map((l) => l.length - l.trimStart().length),
795
+ );
796
+ const text = lines.map((l) => l.slice(indent)).join("\n");
797
+ return text.trim() ? text.slice(0, SNIPPET_MAX_CHARS) : null;
798
+ }
799
+
800
+ const TrivyResult = type({
801
+ "Target?": "string",
802
+ "Class?": "string",
803
+ "Type?": "string",
804
+ "Misconfigurations?": TrivyMisconfiguration.array(),
805
+ });
806
+ const TrivyOutput = type({ "Results?": TrivyResult.array().or("null") });
807
+
808
+ // --- checkov --------------------------------------------------------------
809
+
810
+ function scanCheckov(
811
+ cwd: string,
812
+ downloadExternalModules = false,
813
+ frameworks: string[] = ["terraform", "terraform_json"],
814
+ network?: ToolsNetworkDirective,
815
+ ): ScannerOutcome {
816
+ // `--framework` pins checkov to EXACTLY the languages this run scopes to
817
+ // (derived from `iac_languages` ∩ what the tree contains — see
818
+ // checkovFrameworksFor). The default stays Terraform-only so the fix loop's
819
+ // scan never widens; the read-only Assess path passes the detected set.
820
+ // Explicit is deliberate: checkov's own default is `all`, which also scans
821
+ // CI configs / secrets / SAST — noise the crosswalk isn't tuned for.
822
+ // NB: no `--compact` — it omits `code_block` from the JSON, which is exactly
823
+ // the offending source we attach to each finding (the detail's "flagged
824
+ // location"). `--quiet` already suppresses the progress banner.
825
+ //
826
+ // `--download-external-modules true` (off by default in checkov, and in
827
+ // Infraweaver) makes checkov resolve and scan INSIDE registry/git modules. The
828
+ // refactor `b.4` path (adopting a third-party module) MUST turn this on: a
829
+ // naive re-scan with it off stops looking inside the adopted module, so a pile
830
+ // of findings would vanish without being fixed — a false ✗→✓ ("finding-
831
+ // laundering"). Off by default keeps the normal scan fast and offline.
832
+ // Same integrity gate as trivy/tflint. No checkov advisory exists today, so
833
+ // this is a no-op denylist check — but it also probes the version, which the
834
+ // evidence bundle's provenance block records (A.3): the signed bundle names
835
+ // the exact scanner builds that produced its findings.
836
+ assertToolchainIntegrity("checkov", cwd);
837
+ const egress = egressEnvFor("checkov", network);
838
+ const args = [
839
+ "-d",
840
+ ".",
841
+ "--framework",
842
+ ...frameworks,
843
+ "-o",
844
+ "json",
845
+ "--quiet",
846
+ ];
847
+ if (downloadExternalModules) args.push("--download-external-modules", "true");
848
+ const r = run("checkov", args, cwd, egress ?? undefined);
849
+ if (r.missing) return skipped("checkov", "checkov not installed");
850
+ try {
851
+ return {
852
+ source: "checkov",
853
+ ran: true,
854
+ concerns: parseCheckovOutput(r.stdout, cwd),
855
+ ...(egress ? { egress_isolation: "partial" as const } : {}),
856
+ };
857
+ } catch {
858
+ return skipped("checkov", "could not parse checkov json output");
859
+ }
860
+ }
861
+
862
+ /** parse `checkov -o json` output (object for one framework, array for several).
863
+ * Each block's `check_type` names the framework that produced it (terraform /
864
+ * cloudformation / kubernetes / …) — carried onto every concern as its language. */
865
+ export function parseCheckovOutput(stdout: string, cwd = ""): Concern[] {
866
+ const parsed = parseScannerJson(CheckovOutputs, stdout, "checkov");
867
+ const blocks = Array.isArray(parsed) ? parsed : [parsed];
868
+ const concerns: Concern[] = [];
869
+ for (const block of blocks) {
870
+ const language = checkovTypeToLanguage(block.check_type);
871
+ for (const check of block.results?.failed_checks ?? []) {
872
+ // checkov emits 0 for "no specific line"; normalize to null (matching the
873
+ // trivy parser and the reviewer's findings.json) so the content id is
874
+ // stable and a reviewer-loaded checkov concern re-verifies ✗→✓.
875
+ const startLine = check.file_line_range?.[0];
876
+ const rule = check.check_id ?? "issue";
877
+ concerns.push(
878
+ buildConcern({
879
+ source: "checkov",
880
+ rule,
881
+ file: toRepoRelative(check.file_path, cwd),
882
+ line:
883
+ typeof startLine === "number" && startLine > 0 ? startLine : null,
884
+ severity: lowerSeverity(check.severity ?? undefined),
885
+ category: "security",
886
+ evidence: check.check_name ?? rule,
887
+ code_snippet: checkovCodeSnippet(check.code_block),
888
+ remediation_hint: check.guideline ?? null,
889
+ language,
890
+ }),
891
+ );
892
+ }
893
+ }
894
+ return concerns;
895
+ }
896
+
897
+ /** The offending source from a checkov finding's `code_block` — an array of
898
+ * `[lineNumber, content]` pairs for the flagged resource block. */
899
+ function checkovCodeSnippet(
900
+ block: [number, string][] | null | undefined,
901
+ ): string | null {
902
+ if (!block?.length) return null;
903
+ return tidySnippet(
904
+ block.slice(0, SNIPPET_MAX_LINES).map(([, content]) => content ?? ""),
905
+ );
906
+ }
907
+
908
+ const CheckovFailedCheck = type({
909
+ "check_id?": "string",
910
+ "check_name?": "string",
911
+ "severity?": "string | null",
912
+ "file_path?": "string",
913
+ "file_line_range?": "number[]",
914
+ "code_block?": type(["number", "string"]).array(),
915
+ "guideline?": "string",
916
+ });
917
+ const CheckovOutput = type({
918
+ "check_type?": "string",
919
+ "results?": { "failed_checks?": CheckovFailedCheck.array() },
920
+ });
921
+ // object for one framework, array for several — see parseCheckovOutput.
922
+ const CheckovOutputs = CheckovOutput.or(CheckovOutput.array());
923
+
924
+ /**
925
+ * Terraform files changed on the current branch vs the base. Returns null when
926
+ * the base can't be determined (caller then falls back to a full scan).
927
+ */
928
+ export function changedTerraformFiles(cwd: string): Set<string> | null {
929
+ const base = resolveBaseRef(cwd);
930
+ if (!base) return null;
931
+ const mergeBase = run("git", ["merge-base", base, "HEAD"], cwd);
932
+ const from =
933
+ mergeBase.status === 0 && mergeBase.stdout.trim()
934
+ ? mergeBase.stdout.trim()
935
+ : base;
936
+ const diff = run("git", ["diff", "--name-only", from, "HEAD"], cwd);
937
+ if (diff.status !== 0) return null;
938
+ // `git diff` reports paths relative to the repo ROOT, but a concern's
939
+ // `location.file` is relative to the scan `cwd` (toRepoRelative). When `cwd`
940
+ // is a repo SUBDIRECTORY (the `cwd` action input resolved under
941
+ // GITHUB_WORKSPACE) the two path spaces disagree — e.g. git says
942
+ // `infra/main.tf` while the concern says `main.tf` — and the in-scope check
943
+ // would silently drop every concern. Re-base the diff paths onto `cwd` by
944
+ // stripping the cwd→root prefix and discarding anything outside it, so both
945
+ // sides are cwd-relative.
946
+ const prefixResult = run("git", ["rev-parse", "--show-prefix"], cwd);
947
+ const prefix =
948
+ prefixResult.status === 0
949
+ ? prefixResult.stdout.trim().replace(/\\/g, "/")
950
+ : "";
951
+ const files: string[] = [];
952
+ for (const raw of diff.stdout.split("\n")) {
953
+ let f = raw.trim().replace(/\\/g, "/").replace(/^\.\//, "");
954
+ if (!f) continue;
955
+ if (prefix) {
956
+ if (!f.startsWith(prefix)) continue; // changed file lives outside the scanned subdir
957
+ f = f.slice(prefix.length);
958
+ }
959
+ if (f.endsWith(".tf") || f.endsWith(".tfvars")) files.push(f);
960
+ }
961
+ return new Set(files);
962
+ }
963
+
964
+ export interface RunScannersOptions {
965
+ /** the resolved tool selection. A scanner whose tool is gated/disabled
966
+ * is reported as a `skipped` outcome (with the licence/disable reason) instead
967
+ * of running, so `terraform_scan` and the ✗→✓ verifier see the SAME toolchain. */
968
+ selection?: ResolvedToolSelection | undefined;
969
+ /** module-fetch credential env, threaded into `terraform validate`'s
970
+ * init so a private cross-repo `git::` module resolves during a scan. */
971
+ terraformEnv?: Record<string, string> | undefined;
972
+ /** when true, checkov resolves and scans INSIDE registry/git modules
973
+ * (`--download-external-modules true`). Required by the refactor b.4 path so
974
+ * adopting a third-party module can't launder its internal findings; off by
975
+ * default to keep the normal scan fast and offline. */
976
+ downloadExternalModules?: boolean | undefined;
977
+ /** checkov `--framework` values (see checkovFrameworksFor). Defaults to the
978
+ * Terraform-only pair — the fix loop never widens; the read-only Assess path
979
+ * passes the language-scope-derived set for multi-language breadth. */
980
+ checkovFrameworks?: string[] | undefined;
981
+ /** per-scanner egress policy (supply-chain 0.2, the `tools_network` input).
982
+ * A denied tool runs under the blackhole-proxy env overlay and its outcome
983
+ * records `egress_isolation: "partial"`. Absent ⇒ open egress (today's
984
+ * behaviour) — deny is opt-in. */
985
+ toolsNetwork?: ToolsNetworkDirective | undefined;
986
+ }
987
+
988
+ /** run every scanner once over `cwd`. shared by `terraform_scan` and the
989
+ * deterministic remediation verifier so both see the identical toolchain. A
990
+ * scanner the selection has turned off is emitted as `skipped` (never run), so
991
+ * the gate applies identically to the scan and its verification re-scan. */
992
+ export function runScanners(
993
+ cwd: string,
994
+ opts: RunScannersOptions = {},
995
+ ): ScannerOutcome[] {
996
+ const {
997
+ selection,
998
+ terraformEnv,
999
+ downloadExternalModules,
1000
+ checkovFrameworks,
1001
+ toolsNetwork,
1002
+ } = opts;
1003
+ const gate = (
1004
+ source: Concern["source"],
1005
+ runner: () => ScannerOutcome,
1006
+ ): ScannerOutcome => {
1007
+ if (!selection) return runner();
1008
+ const id = scannerToolId(source);
1009
+ if (id && !selection.enabled(id)) {
1010
+ return skipped(
1011
+ source,
1012
+ selection.offReason(id) ?? "disabled by tools_enabled",
1013
+ );
1014
+ }
1015
+ return runner();
1016
+ };
1017
+ const upstream = [
1018
+ // validate runs first so its `terraform init` has happened by the time the
1019
+ // native pack asks for the provider schema — otherwise the persisted-secret
1020
+ // rule would report a coverage gap on a tree that could in fact be judged.
1021
+ gate("terraform-fmt", () => scanFmt(cwd)),
1022
+ gate("terraform-validate", () => scanValidate(cwd, terraformEnv)),
1023
+ gate("tflint", () => scanTflint(cwd, toolsNetwork)),
1024
+ gate("trivy", () => scanTrivy(cwd, toolsNetwork)),
1025
+ gate("checkov", () =>
1026
+ scanCheckov(
1027
+ cwd,
1028
+ downloadExternalModules,
1029
+ checkovFrameworks,
1030
+ toolsNetwork,
1031
+ ),
1032
+ ),
1033
+ ];
1034
+ // The native pack is never gated: it is our own code, carries no third-party
1035
+ // licence, and shells out to nothing (`scannerToolId` returns null for it).
1036
+ return [...upstream, scanNative(cwd)];
1037
+ }
1038
+
1039
+ export interface RemediationVerdict {
1040
+ /** true only when every original concern id is absent from the re-scan. */
1041
+ verified: boolean;
1042
+ /** original ids no longer present (the fix cleared them). */
1043
+ resolved: string[];
1044
+ /** original ids still present (the fix did NOT clear them). */
1045
+ remaining: string[];
1046
+ }
1047
+
1048
+ /**
1049
+ * Deterministic ✗→✓ check: partition the group's original `concern_ids` into
1050
+ * those gone from a fresh scan (`resolved`) and those still present
1051
+ * (`remaining`). Concern ids are content hashes (`sha1(source|rule|file|line)`),
1052
+ * so a missing id means that exact concern is gone — the correct primitive for
1053
+ * "did the fix clear it", independent of severity/scope filtering. This is the
1054
+ * code-level replacement for the agent eyeballing a re-scan and self-reporting.
1055
+ */
1056
+ export function computeRemediationVerdict(
1057
+ originalConcernIds: string[],
1058
+ currentConcernIds: Set<string>,
1059
+ ): RemediationVerdict {
1060
+ const resolved: string[] = [];
1061
+ const remaining: string[] = [];
1062
+ for (const id of originalConcernIds) {
1063
+ if (currentConcernIds.has(id)) remaining.push(id);
1064
+ else resolved.push(id);
1065
+ }
1066
+ return { verified: remaining.length === 0, resolved, remaining };
1067
+ }
1068
+
1069
+ /**
1070
+ * Regression guard. The full re-scan (`terraform_verify_remediation`)
1071
+ * already sees the whole workspace, so a concern the fix *introduced* shows up
1072
+ * in the current scan. Regressions are exactly the content ids present after the
1073
+ * fix that were not in the pre-fix baseline — `current − baseline`. A non-empty
1074
+ * result means the fix traded one defect for another (e.g. an encryption block
1075
+ * that trips a different tflint rule) and must downgrade the PR to needs-human.
1076
+ *
1077
+ * Both id sets are computed the same way (the deduped union of every scanner's
1078
+ * concern ids, unfiltered by severity) so the diff is apples-to-apples — a
1079
+ * regression at ANY severity is caught, not just ones above the run threshold.
1080
+ * Returns sorted ids for a stable PR body.
1081
+ */
1082
+ export function computeRegressions(
1083
+ baselineConcernIds: Iterable<string>,
1084
+ currentConcernIds: Iterable<string>,
1085
+ ): string[] {
1086
+ const baseline = new Set(baselineConcernIds);
1087
+ const regressions = new Set<string>();
1088
+ for (const id of currentConcernIds) {
1089
+ if (!baseline.has(id)) regressions.add(id);
1090
+ }
1091
+ return [...regressions].sort();
1092
+ }
1093
+
1094
+ /**
1095
+ * Line-INDEPENDENT ✗→✓ partition (the integrity-preserving replacement for the
1096
+ * raw-id `computeRemediationVerdict` when scan context is available). Each
1097
+ * requested entry carries its display `id` and its `key` (see `concernKeyOf`);
1098
+ * a concern is `remaining` iff its KEY still appears in the re-scan, else
1099
+ * `resolved`. Because a fix that shifts lines keeps the same (source|rule|file)
1100
+ * key, an unfixed concern can no longer be mis-reported as resolved. Pure.
1101
+ */
1102
+ export function partitionByKey(
1103
+ requested: { id: string; key: string }[],
1104
+ currentKeys: Set<string>,
1105
+ ): RemediationVerdict {
1106
+ const resolved: string[] = [];
1107
+ const remaining: string[] = [];
1108
+ for (const r of requested) {
1109
+ if (currentKeys.has(r.key)) remaining.push(r.id);
1110
+ else resolved.push(r.id);
1111
+ }
1112
+ return { verified: remaining.length === 0, resolved, remaining };
1113
+ }
1114
+
1115
+ /**
1116
+ * Line-INDEPENDENT regression set: one representative current concern id per KEY
1117
+ * present in the re-scan but absent from the pre-fix baseline keys. A pre-existing
1118
+ * concern that merely shifted to a new line (same key) is NOT a regression — only
1119
+ * a genuinely new (rule, file) defect is. Replaces the raw-id `computeRegressions`
1120
+ * for the integrity path. Pure; returns sorted ids for a stable PR body.
1121
+ */
1122
+ export function regressionIdsByKey(
1123
+ current: { id: string; key: string }[],
1124
+ baselineKeys: Set<string>,
1125
+ ): string[] {
1126
+ const seen = new Set<string>();
1127
+ const out: string[] = [];
1128
+ for (const c of current) {
1129
+ if (baselineKeys.has(c.key) || seen.has(c.key)) continue;
1130
+ seen.add(c.key);
1131
+ out.push(c.id);
1132
+ }
1133
+ return out.sort();
1134
+ }