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.
- package/LICENSE +661 -0
- package/README.md +244 -0
- package/dist/agents/claude.d.ts +113 -0
- package/dist/agents/claudePretoolGate.d.ts +137 -0
- package/dist/agents/dispatchMetrics.d.ts +77 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +46 -0
- package/dist/agents/opencode.d.ts +284 -0
- package/dist/agents/opencodePlugin.d.ts +96 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +126 -0
- package/dist/agents/reviewer.d.ts +41 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +246 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/tokenQuota.d.ts +118 -0
- package/dist/agents/writeGateSource.d.ts +22 -0
- package/dist/agents/writePolicy.d.ts +79 -0
- package/dist/brand.d.ts +67 -0
- package/dist/cli.mjs +247846 -0
- package/dist/external.d.ts +227 -0
- package/dist/i18n/scaffolding.d.ts +59 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +247061 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +2398 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/assess.d.ts +157 -0
- package/dist/mcp/capabilityContext.d.ts +71 -0
- package/dist/mcp/changeSummary.d.ts +52 -0
- package/dist/mcp/checkSuite.d.ts +27 -0
- package/dist/mcp/checkout.d.ts +92 -0
- package/dist/mcp/comment.d.ts +127 -0
- package/dist/mcp/commitInfo.d.ts +11 -0
- package/dist/mcp/crosswalk.d.ts +178 -0
- package/dist/mcp/crosswalkDigest.d.ts +1 -0
- package/dist/mcp/cyberEssentials.d.ts +24 -0
- package/dist/mcp/dashboard.d.ts +125 -0
- package/dist/mcp/dependencies.d.ts +12 -0
- package/dist/mcp/frameworks.d.ts +74 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +60 -0
- package/dist/mcp/guardrails.d.ts +222 -0
- package/dist/mcp/issue.d.ts +20 -0
- package/dist/mcp/issueComments.d.ts +11 -0
- package/dist/mcp/issueEvents.d.ts +11 -0
- package/dist/mcp/issueInfo.d.ts +11 -0
- package/dist/mcp/labels.d.ts +14 -0
- package/dist/mcp/localContext.d.ts +26 -0
- package/dist/mcp/moduleExtraction.d.ts +79 -0
- package/dist/mcp/moduleTests.d.ts +106 -0
- package/dist/mcp/modules.d.ts +198 -0
- package/dist/mcp/output.d.ts +16 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +50 -0
- package/dist/mcp/pr.d.ts +64 -0
- package/dist/mcp/prInfo.d.ts +11 -0
- package/dist/mcp/providerSchema.d.ts +52 -0
- package/dist/mcp/review.d.ts +212 -0
- package/dist/mcp/reviewComments.d.ts +245 -0
- package/dist/mcp/roots.d.ts +60 -0
- package/dist/mcp/scope.d.ts +26 -0
- package/dist/mcp/selectMode.d.ts +20 -0
- package/dist/mcp/server.d.ts +61 -0
- package/dist/mcp/shared.d.ts +62 -0
- package/dist/mcp/shell.d.ts +58 -0
- package/dist/mcp/staleFix.d.ts +138 -0
- package/dist/mcp/terraform/azurePrices.d.ts +8 -0
- package/dist/mcp/terraform/baseline.d.ts +57 -0
- package/dist/mcp/terraform/concernResult.d.ts +38 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/cspm.d.ts +80 -0
- package/dist/mcp/terraform/currency.d.ts +110 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/eidas.d.ts +100 -0
- package/dist/mcp/terraform/evidence.d.ts +343 -0
- package/dist/mcp/terraform/findings.d.ts +76 -0
- package/dist/mcp/terraform/fixMemory.d.ts +81 -0
- package/dist/mcp/terraform/governance.d.ts +56 -0
- package/dist/mcp/terraform/hcl.d.ts +115 -0
- package/dist/mcp/terraform/iacLanguages.d.ts +120 -0
- package/dist/mcp/terraform/idleFloor.d.ts +78 -0
- package/dist/mcp/terraform/keyless.d.ts +68 -0
- package/dist/mcp/terraform/moduleDocs.d.ts +56 -0
- package/dist/mcp/terraform/nativeRules.d.ts +38 -0
- package/dist/mcp/terraform/nativeScan.d.ts +11 -0
- package/dist/mcp/terraform/noiseProfile.d.ts +100 -0
- package/dist/mcp/terraform/normalization.d.ts +52 -0
- package/dist/mcp/terraform/oscal.d.ts +103 -0
- package/dist/mcp/terraform/packs/azurerm-3-4.d.ts +2 -0
- package/dist/mcp/terraform/packs/cloudflare-4-5.d.ts +2 -0
- package/dist/mcp/terraform/paths.d.ts +28 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/planDrift.d.ts +199 -0
- package/dist/mcp/terraform/policyAuthor.d.ts +191 -0
- package/dist/mcp/terraform/prowlerOcsf.d.ts +136 -0
- package/dist/mcp/terraform/refactor.d.ts +183 -0
- package/dist/mcp/terraform/registry.d.ts +108 -0
- package/dist/mcp/terraform/risk.d.ts +41 -0
- package/dist/mcp/terraform/scanSession.d.ts +116 -0
- package/dist/mcp/terraform/scannerCache.d.ts +15 -0
- package/dist/mcp/terraform/scannerEgress.d.ts +72 -0
- package/dist/mcp/terraform/scannerJson.d.ts +13 -0
- package/dist/mcp/terraform/scanners.d.ts +205 -0
- package/dist/mcp/terraform/signing.d.ts +61 -0
- package/dist/mcp/terraform/stateLocking.d.ts +18 -0
- package/dist/mcp/terraform/subprocess.d.ts +43 -0
- package/dist/mcp/terraform/suppressions.d.ts +90 -0
- package/dist/mcp/terraform/taxonomy.d.ts +39 -0
- package/dist/mcp/terraform/tfquery.d.ts +47 -0
- package/dist/mcp/terraform/toolchainIntegrity.d.ts +82 -0
- package/dist/mcp/terraform/tools/authorPolicy.d.ts +59 -0
- package/dist/mcp/terraform/tools/detectPlanDrift.d.ts +20 -0
- package/dist/mcp/terraform/tools/emitCost.d.ts +11 -0
- package/dist/mcp/terraform/tools/emitSarif.d.ts +14 -0
- package/dist/mcp/terraform/tools/fixMemory.d.ts +19 -0
- package/dist/mcp/terraform/tools/infracostDiff.d.ts +22 -0
- package/dist/mcp/terraform/tools/ingestExternalFindings.d.ts +21 -0
- package/dist/mcp/terraform/tools/moduleLookup.d.ts +14 -0
- package/dist/mcp/terraform/tools/moduleSearch.d.ts +23 -0
- package/dist/mcp/terraform/tools/plan.d.ts +59 -0
- package/dist/mcp/terraform/tools/providerUpgrade.d.ts +55 -0
- package/dist/mcp/terraform/tools/readFindings.d.ts +17 -0
- package/dist/mcp/terraform/tools/scan.d.ts +114 -0
- package/dist/mcp/terraform/tools/validate.d.ts +31 -0
- package/dist/mcp/terraform/tools/verifyRemediation.d.ts +31 -0
- package/dist/mcp/terraform/tools/versionCurrency.d.ts +5 -0
- package/dist/mcp/terraform/tools.d.ts +16 -0
- package/dist/mcp/terraform/transformPacks.d.ts +267 -0
- package/dist/mcp/terraform/types.d.ts +226 -0
- package/dist/mcp/terraform/verification.d.ts +81 -0
- package/dist/mcp/terraform/vex.d.ts +115 -0
- package/dist/mcp/terraform/writeOnly.d.ts +67 -0
- package/dist/mcp/terraform.d.ts +51 -0
- package/dist/mcp/terratest.d.ts +85 -0
- package/dist/mcp/untrustedContent.d.ts +76 -0
- package/dist/mcp/upload.d.ts +8 -0
- package/dist/models.d.ts +181 -0
- package/dist/modes/address-reviews.d.ts +2 -0
- package/dist/modes/assess.d.ts +2 -0
- package/dist/modes/build.d.ts +2 -0
- package/dist/modes/compliance-audit.d.ts +2 -0
- package/dist/modes/cost-optimization.d.ts +2 -0
- package/dist/modes/drift-detect.d.ts +2 -0
- package/dist/modes/fix.d.ts +2 -0
- package/dist/modes/incremental-review.d.ts +2 -0
- package/dist/modes/index.d.ts +24 -0
- package/dist/modes/modernize-deprecated.d.ts +2 -0
- package/dist/modes/plan.d.ts +2 -0
- package/dist/modes/policy-gate.d.ts +2 -0
- package/dist/modes/prFormats.d.ts +3 -0
- package/dist/modes/refactor.d.ts +2 -0
- package/dist/modes/refresh-remediation.d.ts +2 -0
- package/dist/modes/remediate-and-refactor.d.ts +2 -0
- package/dist/modes/remediate.d.ts +2 -0
- package/dist/modes/resolve-conflicts.d.ts +2 -0
- package/dist/modes/review.d.ts +2 -0
- package/dist/modes/summarize-pr.d.ts +2 -0
- package/dist/modes/task.d.ts +2 -0
- package/dist/modes/terraform-code-review.d.ts +2 -0
- package/dist/modes/types.d.ts +9 -0
- package/dist/modes/update-dependencies.d.ts +2 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +107 -0
- package/dist/skills/terraform-best-practices/SKILL.md +379 -0
- package/dist/toolState.d.ts +123 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +47 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/annotations.d.ts +17 -0
- package/dist/utils/apiFetch.d.ts +11 -0
- package/dist/utils/apiKeys.d.ts +48 -0
- package/dist/utils/apiUrl.d.ts +28 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/baseRefConfig.d.ts +121 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildInfraweaverFooter.d.ts +29 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/changeImpact.d.ts +101 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/cloudClient.d.ts +33 -0
- package/dist/utils/cloudFetchCore.d.ts +42 -0
- package/dist/utils/cloudReport.d.ts +114 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/env.d.ts +46 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +104 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/infraweaverConfig.d.ts +56 -0
- package/dist/utils/install.d.ts +37 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/keylessOidc.d.ts +28 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/modelResolver.d.ts +63 -0
- package/dist/utils/moduleFetch.d.ts +39 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/openaiCompatible.d.ts +26 -0
- package/dist/utils/opentofuMcp.d.ts +50 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +249 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/presets.d.ts +36 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/proxyModel.d.ts +76 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/redact.d.ts +33 -0
- package/dist/utils/registryAuth.d.ts +52 -0
- package/dist/utils/remediationCommand.d.ts +58 -0
- package/dist/utils/retry.d.ts +38 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +112 -0
- package/dist/utils/runContextData.d.ts +42 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +89 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +34 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +44 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +81 -0
- package/dist/utils/terraformMcp.d.ts +46 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +52 -0
- package/dist/utils/toolLicensing.d.ts +56 -0
- package/dist/utils/toolSelection.d.ts +73 -0
- package/dist/utils/toon.d.ts +16 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +132 -0
- package/src/agents/claude.ts +1588 -0
- package/src/agents/claudePretoolGate.ts +286 -0
- package/src/agents/dispatchMetrics.ts +162 -0
- package/src/agents/gateServer.ts +178 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +164 -0
- package/src/agents/opencode.ts +1600 -0
- package/src/agents/opencodePlugin.ts +304 -0
- package/src/agents/opencodeShared.ts +134 -0
- package/src/agents/postRun.ts +615 -0
- package/src/agents/reviewer.ts +134 -0
- package/src/agents/sessionLabeler.ts +185 -0
- package/src/agents/shared.ts +377 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/tokenQuota.ts +203 -0
- package/src/agents/writeGateSource.ts +148 -0
- package/src/agents/writePolicy.ts +132 -0
- package/src/brand.ts +84 -0
- package/src/cli.ts +117 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +126 -0
- package/src/commands/verifyEvidence.ts +100 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.ts +109 -0
- package/src/external.ts +308 -0
- package/src/i18n/scaffolding.ts +191 -0
- package/src/index.ts +7 -0
- package/src/internal/index.ts +74 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.ts +815 -0
- package/src/mcp/__fixtures__/infraweaver-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/infraweaver-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/infraweaver-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/assess.ts +723 -0
- package/src/mcp/capabilityContext.ts +80 -0
- package/src/mcp/changeSummary.ts +145 -0
- package/src/mcp/checkSuite.ts +261 -0
- package/src/mcp/checkout.ts +1015 -0
- package/src/mcp/comment.ts +683 -0
- package/src/mcp/commitInfo.ts +57 -0
- package/src/mcp/crosswalk.ts +1253 -0
- package/src/mcp/crosswalkDigest.ts +5 -0
- package/src/mcp/cyberEssentials.ts +60 -0
- package/src/mcp/dashboard.ts +232 -0
- package/src/mcp/dependencies.ts +190 -0
- package/src/mcp/frameworks.ts +236 -0
- package/src/mcp/geminiSanitizer.ts +212 -0
- package/src/mcp/git.ts +1116 -0
- package/src/mcp/guardrails.ts +771 -0
- package/src/mcp/issue.ts +74 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.ts +35 -0
- package/src/mcp/localContext.ts +65 -0
- package/src/mcp/localServer.ts +217 -0
- package/src/mcp/moduleExtraction.ts +368 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.ts +752 -0
- package/src/mcp/output.ts +71 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.ts +226 -0
- package/src/mcp/pr.ts +238 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.ts +1081 -0
- package/src/mcp/reviewComments.ts +1137 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.ts +82 -0
- package/src/mcp/selectMode.ts +207 -0
- package/src/mcp/server.ts +498 -0
- package/src/mcp/shared.ts +120 -0
- package/src/mcp/shell.ts +631 -0
- package/src/mcp/staleFix.ts +557 -0
- package/src/mcp/terraform/__snapshots__/moduleDocs.test.ts.snap +40 -0
- package/src/mcp/terraform/azurePrices.ts +38 -0
- package/src/mcp/terraform/baseline.ts +172 -0
- package/src/mcp/terraform/concernResult.ts +111 -0
- package/src/mcp/terraform/cost.ts +175 -0
- package/src/mcp/terraform/cspm.ts +326 -0
- package/src/mcp/terraform/currency.ts +354 -0
- package/src/mcp/terraform/decisions.ts +590 -0
- package/src/mcp/terraform/eidas.ts +134 -0
- package/src/mcp/terraform/evidence.ts +771 -0
- package/src/mcp/terraform/findings.ts +383 -0
- package/src/mcp/terraform/fixMemory.ts +253 -0
- package/src/mcp/terraform/governance.ts +231 -0
- package/src/mcp/terraform/hcl.ts +273 -0
- package/src/mcp/terraform/iacLanguages.ts +431 -0
- package/src/mcp/terraform/idleFloor.ts +304 -0
- package/src/mcp/terraform/keyless.ts +123 -0
- package/src/mcp/terraform/moduleDocs.ts +303 -0
- package/src/mcp/terraform/nativeRules.ts +152 -0
- package/src/mcp/terraform/nativeScan.ts +67 -0
- package/src/mcp/terraform/noiseProfile.ts +183 -0
- package/src/mcp/terraform/normalization.ts +265 -0
- package/src/mcp/terraform/oscal.ts +283 -0
- package/src/mcp/terraform/packs/azurerm-3-4.ts +382 -0
- package/src/mcp/terraform/packs/cloudflare-4-5.ts +114 -0
- package/src/mcp/terraform/paths.ts +59 -0
- package/src/mcp/terraform/plan.ts +377 -0
- package/src/mcp/terraform/planDrift.ts +520 -0
- package/src/mcp/terraform/policyAuthor.ts +886 -0
- package/src/mcp/terraform/prowlerOcsf.ts +380 -0
- package/src/mcp/terraform/refactor.ts +879 -0
- package/src/mcp/terraform/registry.ts +317 -0
- package/src/mcp/terraform/risk.ts +99 -0
- package/src/mcp/terraform/scanSession.ts +242 -0
- package/src/mcp/terraform/scannerCache.ts +86 -0
- package/src/mcp/terraform/scannerEgress.ts +141 -0
- package/src/mcp/terraform/scannerJson.ts +22 -0
- package/src/mcp/terraform/scanners.ts +1134 -0
- package/src/mcp/terraform/signing.ts +151 -0
- package/src/mcp/terraform/stateLocking.ts +106 -0
- package/src/mcp/terraform/subprocess.ts +102 -0
- package/src/mcp/terraform/suppressions.ts +551 -0
- package/src/mcp/terraform/taxonomy.ts +0 -0
- package/src/mcp/terraform/tfquery.ts +159 -0
- package/src/mcp/terraform/toolchainIntegrity.ts +151 -0
- package/src/mcp/terraform/tools/authorPolicy.ts +185 -0
- package/src/mcp/terraform/tools/detectPlanDrift.ts +279 -0
- package/src/mcp/terraform/tools/emitCost.ts +130 -0
- package/src/mcp/terraform/tools/emitSarif.ts +108 -0
- package/src/mcp/terraform/tools/fixMemory.ts +82 -0
- package/src/mcp/terraform/tools/infracostDiff.ts +110 -0
- package/src/mcp/terraform/tools/ingestExternalFindings.ts +259 -0
- package/src/mcp/terraform/tools/moduleLookup.ts +86 -0
- package/src/mcp/terraform/tools/moduleSearch.ts +46 -0
- package/src/mcp/terraform/tools/plan.ts +384 -0
- package/src/mcp/terraform/tools/providerUpgrade.ts +217 -0
- package/src/mcp/terraform/tools/readFindings.ts +138 -0
- package/src/mcp/terraform/tools/scan.ts +303 -0
- package/src/mcp/terraform/tools/validate.ts +160 -0
- package/src/mcp/terraform/tools/verifyRemediation.ts +190 -0
- package/src/mcp/terraform/tools/versionCurrency.ts +64 -0
- package/src/mcp/terraform/tools.ts +62 -0
- package/src/mcp/terraform/transformPacks.ts +594 -0
- package/src/mcp/terraform/types.ts +382 -0
- package/src/mcp/terraform/verification.ts +150 -0
- package/src/mcp/terraform/vex.ts +367 -0
- package/src/mcp/terraform/writeOnly.ts +252 -0
- package/src/mcp/terraform.ts +52 -0
- package/src/mcp/terratest.ts +215 -0
- package/src/mcp/untrustedContent.ts +126 -0
- package/src/mcp/upload.ts +123 -0
- package/src/models.ts +753 -0
- package/src/modes/address-reviews.ts +35 -0
- package/src/modes/assess.ts +27 -0
- package/src/modes/build.ts +86 -0
- package/src/modes/compliance-audit.ts +25 -0
- package/src/modes/cost-optimization.ts +36 -0
- package/src/modes/drift-detect.ts +31 -0
- package/src/modes/fix.ts +29 -0
- package/src/modes/incremental-review.ts +103 -0
- package/src/modes/index.ts +94 -0
- package/src/modes/modernize-deprecated.ts +36 -0
- package/src/modes/plan.ts +21 -0
- package/src/modes/policy-gate.ts +27 -0
- package/src/modes/prFormats.ts +325 -0
- package/src/modes/refactor.ts +74 -0
- package/src/modes/refresh-remediation.ts +40 -0
- package/src/modes/remediate-and-refactor.ts +42 -0
- package/src/modes/remediate.ts +78 -0
- package/src/modes/resolve-conflicts.ts +32 -0
- package/src/modes/review.ts +135 -0
- package/src/modes/summarize-pr.ts +42 -0
- package/src/modes/task.ts +26 -0
- package/src/modes/terraform-code-review.ts +68 -0
- package/src/modes/types.ts +17 -0
- package/src/modes/update-dependencies.ts +44 -0
- package/src/prep/index.ts +96 -0
- package/src/prep/installNodeDependencies.ts +251 -0
- package/src/prep/installPythonDependencies.ts +235 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.ts +237 -0
- package/src/runCli.ts +335 -0
- package/src/skills/terraform-best-practices/SKILL.md +379 -0
- package/src/toolState.ts +241 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.ts +245 -0
- package/src/utils/agentHangReport.ts +180 -0
- package/src/utils/annotations.ts +56 -0
- package/src/utils/apiFetch.ts +19 -0
- package/src/utils/apiKeys.ts +305 -0
- package/src/utils/apiUrl.ts +42 -0
- package/src/utils/assets.ts +123 -0
- package/src/utils/baseRefConfig.ts +254 -0
- package/src/utils/billingErrors.ts +210 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildInfraweaverFooter.ts +104 -0
- package/src/utils/byokFallback.ts +137 -0
- package/src/utils/changeImpact.ts +330 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +36 -0
- package/src/utils/cloudClient.ts +57 -0
- package/src/utils/cloudFetchCore.ts +139 -0
- package/src/utils/cloudReport.ts +316 -0
- package/src/utils/codexHome.ts +204 -0
- package/src/utils/codexOAuth.ts +154 -0
- package/src/utils/codexRefreshDetect.ts +36 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/env.ts +84 -0
- package/src/utils/errorReport.ts +94 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.ts +268 -0
- package/src/utils/gitAuthServer.ts +186 -0
- package/src/utils/github.ts +676 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.ts +198 -0
- package/src/utils/infraweaverConfig.ts +202 -0
- package/src/utils/install.ts +279 -0
- package/src/utils/instructions.ts +640 -0
- package/src/utils/keylessOidc.ts +47 -0
- package/src/utils/leapingComment.ts +20 -0
- package/src/utils/learnings.ts +145 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.ts +452 -0
- package/src/utils/modelResolver.ts +105 -0
- package/src/utils/moduleFetch.ts +138 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +86 -0
- package/src/utils/openaiCompatible.ts +52 -0
- package/src/utils/opentofuMcp.ts +61 -0
- package/src/utils/overrides.ts +100 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.ts +173 -0
- package/src/utils/payload.ts +1206 -0
- package/src/utils/prSummary.ts +148 -0
- package/src/utils/presets.ts +105 -0
- package/src/utils/progressComment.ts +266 -0
- package/src/utils/providerErrors.ts +189 -0
- package/src/utils/proxyModel.ts +239 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/redact.ts +86 -0
- package/src/utils/registryAuth.ts +115 -0
- package/src/utils/remediationCommand.ts +144 -0
- package/src/utils/retry.ts +180 -0
- package/src/utils/reviewCleanup.ts +116 -0
- package/src/utils/run.ts +100 -0
- package/src/utils/runContext.ts +276 -0
- package/src/utils/runContextData.ts +121 -0
- package/src/utils/runErrorRenderer.ts +282 -0
- package/src/utils/runFixture.ts +91 -0
- package/src/utils/runLifecycle.ts +360 -0
- package/src/utils/runStartupLog.ts +67 -0
- package/src/utils/secrets.ts +208 -0
- package/src/utils/setup.ts +368 -0
- package/src/utils/shell.ts +132 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.ts +474 -0
- package/src/utils/terraformMcp.ts +99 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.ts +168 -0
- package/src/utils/token.ts +294 -0
- package/src/utils/toolLicensing.ts +152 -0
- package/src/utils/toolSelection.ts +239 -0
- package/src/utils/toon.ts +76 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +94 -0
- 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
|
+
}
|