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,886 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §5.2 — plain-language standard → REVIEWED policy-as-code.
|
|
3
|
+
*
|
|
4
|
+
* The feature a compliance officer asks for is "paste our internal standard, get
|
|
5
|
+
* a policy". The feature that is safe to ship is narrower, and the difference is
|
|
6
|
+
* the whole design.
|
|
7
|
+
*
|
|
8
|
+
* WHY NOT "MODEL WRITES REGO". Rego's worst failure mode is silent: a rule that
|
|
9
|
+
* references a field name the plan JSON does not have is not an error, it is
|
|
10
|
+
* *vacuously satisfied* — it never denies. A generated policy that looks
|
|
11
|
+
* enforced and passes everything is strictly worse than no policy, because a
|
|
12
|
+
* control owner will tick the box. Free-form generation cannot rule that out,
|
|
13
|
+
* and neither can reading the diff.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THIS DOES INSTEAD. The model's job is reduced to filling in a CLOSED
|
|
16
|
+
* intermediate representation ([[PolicyRuleSpec]]) — one resource type, one
|
|
17
|
+
* attribute path, one operator drawn from a fixed set. Everything downstream is
|
|
18
|
+
* deterministic:
|
|
19
|
+
*
|
|
20
|
+
* 1. `emitRego` / `emitPolicyHcl` / `emitPolicyTestHcl` are pure functions of
|
|
21
|
+
* the spec. The same spec always produces the same bytes.
|
|
22
|
+
* 2. `selfTestSpec` evaluates the spec IN PROCESS against three generated
|
|
23
|
+
* fixtures — violating, compliant, and attribute-absent — and REJECTS a
|
|
24
|
+
* spec that does not deny the violating case. That is the vacuous-rule
|
|
25
|
+
* check, and it runs before a human is ever shown the policy.
|
|
26
|
+
* 3. The crosswalk mapping is not generated at all: the spec names one of the
|
|
27
|
+
* existing themes, so the control citation comes from the reviewed
|
|
28
|
+
* [[crosswalk]] table by the same mechanical hop every other finding uses.
|
|
29
|
+
*
|
|
30
|
+
* WHAT IT DELIBERATELY DOES NOT CLAIM. We do not ship OPA or tfpolicy, so this
|
|
31
|
+
* module cannot assert that the EMITTED text behaves like the spec — only that
|
|
32
|
+
* the spec itself is non-vacuous and that the emission is deterministic. The
|
|
33
|
+
* caller reports that distinction (`rego_verified`, `policy_hcl_fidelity`)
|
|
34
|
+
* rather than papering over it; `policy_check` (conftest) and the customer's own
|
|
35
|
+
* `tfpolicy validate`/`test` are where the emitted artifacts get executed. The
|
|
36
|
+
* `.policytest.hcl` exists precisely so that step is one command, not a project.
|
|
37
|
+
*
|
|
38
|
+
* ONE MORE HONESTY POINT, AND IT IS THE SUBTLE ONE. "The attribute is missing"
|
|
39
|
+
* is not a verdict a policy engine can guess. `acl != "public-read"` on a bucket
|
|
40
|
+
* with no `acl` is compliant (the default is private); `sse_algorithm == "AES256"`
|
|
41
|
+
* on a bucket with no encryption block is a violation. Same shape, opposite
|
|
42
|
+
* answer. So `when_absent` is a REQUIRED field with no default — the author
|
|
43
|
+
* states it, the self-test exercises it, and the reviewing human sees it.
|
|
44
|
+
*/
|
|
45
|
+
import { PRODUCT_NAME } from "#app/brand";
|
|
46
|
+
import { THEME_TO_NIST } from "#app/mcp/crosswalk";
|
|
47
|
+
import { SEVERITY_RANK, type Severity } from "#app/mcp/terraform/types";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The namespace an authored rule's id carries. Deliberately NOT the `infraweaver:`
|
|
51
|
+
* native-source prefix: these findings are produced by the customer's conftest
|
|
52
|
+
* running the customer's committed policy. We helped write it; we did not find
|
|
53
|
+
* it, and the label should not imply otherwise.
|
|
54
|
+
*/
|
|
55
|
+
export const AUTHORED_RULE_PREFIX = "policy";
|
|
56
|
+
|
|
57
|
+
/** The fully-qualified rule id for an authored spec. */
|
|
58
|
+
export function authoredRuleId(id: string): string {
|
|
59
|
+
return `${AUTHORED_RULE_PREFIX}:${id}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The closed predicate set. Anything not expressible here is not authorable —
|
|
63
|
+
* which is the point: an operator we cannot evaluate in process is an operator
|
|
64
|
+
* whose generated policy we cannot self-test. */
|
|
65
|
+
export const POLICY_OPERATORS = [
|
|
66
|
+
"equals",
|
|
67
|
+
"not_equals",
|
|
68
|
+
"in",
|
|
69
|
+
"not_in",
|
|
70
|
+
"present",
|
|
71
|
+
"absent",
|
|
72
|
+
"is_true",
|
|
73
|
+
"is_false",
|
|
74
|
+
"matches",
|
|
75
|
+
"gte",
|
|
76
|
+
"lte",
|
|
77
|
+
"non_empty",
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
export type PolicyOperator = (typeof POLICY_OPERATORS)[number];
|
|
81
|
+
|
|
82
|
+
/** Operators that compare against a scalar operand. */
|
|
83
|
+
const SCALAR_OPERAND: ReadonlySet<PolicyOperator> = new Set([
|
|
84
|
+
"equals",
|
|
85
|
+
"not_equals",
|
|
86
|
+
"matches",
|
|
87
|
+
"gte",
|
|
88
|
+
"lte",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/** Operators that compare against a list operand. */
|
|
92
|
+
const LIST_OPERAND: ReadonlySet<PolicyOperator> = new Set(["in", "not_in"]);
|
|
93
|
+
|
|
94
|
+
/** Operators that take no operand at all. */
|
|
95
|
+
const NO_OPERAND: ReadonlySet<PolicyOperator> = new Set([
|
|
96
|
+
"present",
|
|
97
|
+
"absent",
|
|
98
|
+
"is_true",
|
|
99
|
+
"is_false",
|
|
100
|
+
"non_empty",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
/** Operators whose operand must be numeric. */
|
|
104
|
+
const NUMERIC_OPERAND: ReadonlySet<PolicyOperator> = new Set(["gte", "lte"]);
|
|
105
|
+
|
|
106
|
+
export type PolicyScalar = string | number | boolean;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What the rule concludes when the attribute path resolves to nothing.
|
|
110
|
+
* Required, and deliberately not defaulted — see the module note.
|
|
111
|
+
*/
|
|
112
|
+
export type WhenAbsent = "violation" | "compliant";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The reviewed intermediate representation. This — not Rego — is what the model
|
|
116
|
+
* produces from prose, and what a human reviews.
|
|
117
|
+
*/
|
|
118
|
+
export interface PolicyRuleSpec {
|
|
119
|
+
/** kebab-case, unique within the authored pack. Becomes the rule id in every emitted artifact. */
|
|
120
|
+
id: string;
|
|
121
|
+
/** one-line human title. */
|
|
122
|
+
title: string;
|
|
123
|
+
/** the sentence of the SOURCE STANDARD this rule was derived from, quoted. The
|
|
124
|
+
* audit trail from prose to policy; carried into every emitted artifact. */
|
|
125
|
+
source_requirement: string;
|
|
126
|
+
/** the Terraform resource type the rule applies to, e.g. `aws_s3_bucket`. */
|
|
127
|
+
resource_type: string;
|
|
128
|
+
/** dotted path into the resource's planned values, e.g.
|
|
129
|
+
* `server_side_encryption_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm`.
|
|
130
|
+
* A segment that resolves to an array is flattened — every element contributes
|
|
131
|
+
* a candidate value, and the rule must hold for ALL of them. */
|
|
132
|
+
attribute: string;
|
|
133
|
+
operator: PolicyOperator;
|
|
134
|
+
/** the operand. Scalar for equals/not_equals/matches/gte/lte, a list for
|
|
135
|
+
* in/not_in, omitted for the rest. */
|
|
136
|
+
value?: PolicyScalar | PolicyScalar[];
|
|
137
|
+
when_absent: WhenAbsent;
|
|
138
|
+
severity: Severity;
|
|
139
|
+
/** one of the reviewed crosswalk themes — this is what earns the control citation. */
|
|
140
|
+
theme: string;
|
|
141
|
+
/** the violation message a reviewer and an engineer both read. */
|
|
142
|
+
message: string;
|
|
143
|
+
/** a compliant value for the generated fixtures. REQUIRED for `matches`, where
|
|
144
|
+
* no value can be synthesised from the pattern; optional elsewhere. */
|
|
145
|
+
example_compliant?: PolicyScalar;
|
|
146
|
+
/** a violating value for the generated fixtures. REQUIRED for `matches`. */
|
|
147
|
+
example_violating?: PolicyScalar;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── validation ───────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
153
|
+
const RESOURCE_TYPE_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
154
|
+
const ATTRIBUTE_RE = /^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Structural validation of a spec. Returns the list of problems — empty means
|
|
158
|
+
* valid. Kept separate from the self-test: this catches "the spec is malformed",
|
|
159
|
+
* the self-test catches "the spec is well-formed but says nothing".
|
|
160
|
+
*/
|
|
161
|
+
export function validateSpec(spec: PolicyRuleSpec): string[] {
|
|
162
|
+
const errs: string[] = [];
|
|
163
|
+
if (!ID_RE.test(spec.id))
|
|
164
|
+
errs.push(`id '${spec.id}' must be kebab-case (a-z, 0-9, hyphens)`);
|
|
165
|
+
if (!spec.title.trim()) errs.push("title is empty");
|
|
166
|
+
if (!spec.source_requirement.trim())
|
|
167
|
+
errs.push(
|
|
168
|
+
"source_requirement is empty — every rule must cite the prose it came from",
|
|
169
|
+
);
|
|
170
|
+
if (!spec.message.trim()) errs.push("message is empty");
|
|
171
|
+
if (!RESOURCE_TYPE_RE.test(spec.resource_type))
|
|
172
|
+
errs.push(
|
|
173
|
+
`resource_type '${spec.resource_type}' is not a Terraform resource type`,
|
|
174
|
+
);
|
|
175
|
+
if (!ATTRIBUTE_RE.test(spec.attribute))
|
|
176
|
+
errs.push(`attribute '${spec.attribute}' is not a dotted attribute path`);
|
|
177
|
+
if (!(spec.theme in THEME_TO_NIST))
|
|
178
|
+
errs.push(
|
|
179
|
+
`theme '${spec.theme}' is not a crosswalk theme — pick one of: ${Object.keys(THEME_TO_NIST).sort().join(", ")}`,
|
|
180
|
+
);
|
|
181
|
+
if (!(spec.severity in SEVERITY_RANK))
|
|
182
|
+
errs.push(`severity '${spec.severity}' is not a severity`);
|
|
183
|
+
|
|
184
|
+
const op = spec.operator;
|
|
185
|
+
if (!POLICY_OPERATORS.includes(op)) {
|
|
186
|
+
errs.push(`operator '${op}' is not supported`);
|
|
187
|
+
return errs;
|
|
188
|
+
}
|
|
189
|
+
const v = spec.value;
|
|
190
|
+
if (NO_OPERAND.has(op)) {
|
|
191
|
+
if (v !== undefined) errs.push(`operator '${op}' takes no value`);
|
|
192
|
+
} else if (LIST_OPERAND.has(op)) {
|
|
193
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
194
|
+
errs.push(`operator '${op}' needs a non-empty list value`);
|
|
195
|
+
} else if (SCALAR_OPERAND.has(op)) {
|
|
196
|
+
if (v === undefined || Array.isArray(v))
|
|
197
|
+
errs.push(`operator '${op}' needs a scalar value`);
|
|
198
|
+
else if (NUMERIC_OPERAND.has(op) && typeof v !== "number")
|
|
199
|
+
errs.push(`operator '${op}' needs a numeric value`);
|
|
200
|
+
else if (op === "matches") {
|
|
201
|
+
if (typeof v !== "string")
|
|
202
|
+
errs.push("operator 'matches' needs a string regex");
|
|
203
|
+
else {
|
|
204
|
+
try {
|
|
205
|
+
new RegExp(v);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
errs.push(
|
|
208
|
+
`operator 'matches' regex does not compile: ${(e as Error).message}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// No value can be synthesised from an arbitrary pattern, so the author
|
|
213
|
+
// supplies both sides. Without them the fixtures — and therefore the
|
|
214
|
+
// vacuity check — would be guesswork.
|
|
215
|
+
if (
|
|
216
|
+
spec.example_compliant === undefined ||
|
|
217
|
+
spec.example_violating === undefined
|
|
218
|
+
)
|
|
219
|
+
errs.push(
|
|
220
|
+
"operator 'matches' requires both example_compliant and example_violating (a fixture value cannot be derived from a regex)",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return errs;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── evaluation (the semantics every emitter mirrors) ─────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Resolve a dotted path against a planned-values object, flattening arrays: at
|
|
231
|
+
* each segment, an array contributes each of its elements as a candidate parent.
|
|
232
|
+
* Returns every leaf value the path reaches. An empty array means the path
|
|
233
|
+
* resolved to nothing — which is what `when_absent` decides on.
|
|
234
|
+
*
|
|
235
|
+
* `null` leaves are dropped: Terraform writes an unset optional argument as
|
|
236
|
+
* `null`, and "present but null" is the same fact as "absent" to a reader.
|
|
237
|
+
*/
|
|
238
|
+
export function resolvePath(root: unknown, path: string): unknown[] {
|
|
239
|
+
let level: unknown[] = [root];
|
|
240
|
+
for (const segment of path.split(".")) {
|
|
241
|
+
const next: unknown[] = [];
|
|
242
|
+
for (const node of level) {
|
|
243
|
+
for (const member of node !== null && Array.isArray(node)
|
|
244
|
+
? node
|
|
245
|
+
: [node]) {
|
|
246
|
+
if (member === null || typeof member !== "object") continue;
|
|
247
|
+
const child = (member as Record<string, unknown>)[segment];
|
|
248
|
+
if (child === undefined) continue;
|
|
249
|
+
next.push(child);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
level = next;
|
|
253
|
+
}
|
|
254
|
+
// flatten a terminal array (e.g. a list attribute) into its elements, and drop nulls
|
|
255
|
+
const out: unknown[] = [];
|
|
256
|
+
for (const node of level) {
|
|
257
|
+
for (const member of Array.isArray(node) ? node : [node]) {
|
|
258
|
+
if (member === null) continue;
|
|
259
|
+
out.push(member);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Does one resolved value satisfy the rule? Pure; mirrored by the emitted Rego. */
|
|
266
|
+
export function valueSatisfies(spec: PolicyRuleSpec, value: unknown): boolean {
|
|
267
|
+
const v = spec.value;
|
|
268
|
+
switch (spec.operator) {
|
|
269
|
+
case "equals":
|
|
270
|
+
return value === v;
|
|
271
|
+
case "not_equals":
|
|
272
|
+
return value !== v;
|
|
273
|
+
case "in":
|
|
274
|
+
return Array.isArray(v) && v.some((x) => x === value);
|
|
275
|
+
case "not_in":
|
|
276
|
+
return Array.isArray(v) && !v.some((x) => x === value);
|
|
277
|
+
// `present` is satisfied by any resolved value — `resolvePath` has already
|
|
278
|
+
// dropped nulls, so reaching here means something real is there.
|
|
279
|
+
case "present":
|
|
280
|
+
return true;
|
|
281
|
+
// ...and `absent` is therefore violated by any resolved value.
|
|
282
|
+
case "absent":
|
|
283
|
+
return false;
|
|
284
|
+
case "is_true":
|
|
285
|
+
return value === true;
|
|
286
|
+
case "is_false":
|
|
287
|
+
return value === false;
|
|
288
|
+
case "matches":
|
|
289
|
+
return (
|
|
290
|
+
typeof value === "string" &&
|
|
291
|
+
typeof v === "string" &&
|
|
292
|
+
new RegExp(v).test(value)
|
|
293
|
+
);
|
|
294
|
+
case "gte":
|
|
295
|
+
return typeof value === "number" && typeof v === "number" && value >= v;
|
|
296
|
+
case "lte":
|
|
297
|
+
return typeof value === "number" && typeof v === "number" && value <= v;
|
|
298
|
+
case "non_empty":
|
|
299
|
+
if (typeof value === "string" || Array.isArray(value))
|
|
300
|
+
return value.length > 0;
|
|
301
|
+
if (value !== null && typeof value === "object")
|
|
302
|
+
return Object.keys(value as object).length > 0;
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface SpecVerdict {
|
|
308
|
+
violates: boolean;
|
|
309
|
+
/** why — the sentence a test failure prints. */
|
|
310
|
+
reason: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Evaluate a spec against ONE resource's planned values. Universal over the
|
|
315
|
+
* resolved values: every matched value must satisfy the rule, so a list where
|
|
316
|
+
* one element is wrong is a violation.
|
|
317
|
+
*/
|
|
318
|
+
export function evaluateSpec(
|
|
319
|
+
spec: PolicyRuleSpec,
|
|
320
|
+
values: unknown,
|
|
321
|
+
): SpecVerdict {
|
|
322
|
+
const resolved = resolvePath(values, spec.attribute);
|
|
323
|
+
if (resolved.length === 0) {
|
|
324
|
+
return spec.when_absent === "violation"
|
|
325
|
+
? {
|
|
326
|
+
violates: true,
|
|
327
|
+
reason: `${spec.attribute} is absent and when_absent=violation`,
|
|
328
|
+
}
|
|
329
|
+
: {
|
|
330
|
+
violates: false,
|
|
331
|
+
reason: `${spec.attribute} is absent and when_absent=compliant`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const bad = resolved.filter((v) => !valueSatisfies(spec, v));
|
|
335
|
+
if (bad.length === 0)
|
|
336
|
+
return {
|
|
337
|
+
violates: false,
|
|
338
|
+
reason: `all ${resolved.length} value(s) satisfy ${spec.operator}`,
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
violates: true,
|
|
342
|
+
reason: `${bad.length} of ${resolved.length} value(s) fail ${spec.operator}: ${bad
|
|
343
|
+
.map((v) => JSON.stringify(v))
|
|
344
|
+
.join(", ")}`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── fixtures ─────────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
/** A sentinel that no real configuration produces, used where any non-matching
|
|
351
|
+
* value will do. Distinctive on purpose: if it ever shows up in a customer's
|
|
352
|
+
* plan output, something generated has leaked into real config. */
|
|
353
|
+
const SENTINEL = "__infraweaver_fixture_noncompliant__";
|
|
354
|
+
|
|
355
|
+
/** A compliant value for the spec's operator, or the author's override. */
|
|
356
|
+
function compliantValue(spec: PolicyRuleSpec): PolicyScalar {
|
|
357
|
+
if (spec.example_compliant !== undefined) return spec.example_compliant;
|
|
358
|
+
const v = spec.value;
|
|
359
|
+
switch (spec.operator) {
|
|
360
|
+
case "equals":
|
|
361
|
+
return v as PolicyScalar;
|
|
362
|
+
case "not_equals":
|
|
363
|
+
return SENTINEL;
|
|
364
|
+
case "in":
|
|
365
|
+
return (v as PolicyScalar[])[0] as PolicyScalar;
|
|
366
|
+
case "not_in":
|
|
367
|
+
return SENTINEL;
|
|
368
|
+
case "present":
|
|
369
|
+
case "non_empty":
|
|
370
|
+
return "infraweaver-fixture-value";
|
|
371
|
+
case "is_true":
|
|
372
|
+
return true;
|
|
373
|
+
case "is_false":
|
|
374
|
+
return false;
|
|
375
|
+
case "gte":
|
|
376
|
+
return v as number;
|
|
377
|
+
case "lte":
|
|
378
|
+
return v as number;
|
|
379
|
+
// `absent` has no compliant *value* — the compliant fixture is the empty one.
|
|
380
|
+
// `matches` is unreachable: validateSpec requires the author's example.
|
|
381
|
+
default:
|
|
382
|
+
return SENTINEL;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** A violating value for the spec's operator, or the author's override. */
|
|
387
|
+
function violatingValue(spec: PolicyRuleSpec): PolicyScalar {
|
|
388
|
+
if (spec.example_violating !== undefined) return spec.example_violating;
|
|
389
|
+
const v = spec.value;
|
|
390
|
+
switch (spec.operator) {
|
|
391
|
+
case "equals":
|
|
392
|
+
return SENTINEL;
|
|
393
|
+
case "not_equals":
|
|
394
|
+
return v as PolicyScalar;
|
|
395
|
+
case "in":
|
|
396
|
+
return SENTINEL;
|
|
397
|
+
case "not_in":
|
|
398
|
+
return (v as PolicyScalar[])[0] as PolicyScalar;
|
|
399
|
+
case "present":
|
|
400
|
+
// handled by the absent fixture; keep the shape total
|
|
401
|
+
return SENTINEL;
|
|
402
|
+
case "absent":
|
|
403
|
+
return "infraweaver-fixture-value";
|
|
404
|
+
case "is_true":
|
|
405
|
+
return false;
|
|
406
|
+
case "is_false":
|
|
407
|
+
return true;
|
|
408
|
+
case "gte":
|
|
409
|
+
return (v as number) - 1;
|
|
410
|
+
case "lte":
|
|
411
|
+
return (v as number) + 1;
|
|
412
|
+
case "non_empty":
|
|
413
|
+
return "";
|
|
414
|
+
default:
|
|
415
|
+
return SENTINEL;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Build the nested object a dotted path implies, with `leaf` at the end. */
|
|
420
|
+
export function nestValue(
|
|
421
|
+
path: string,
|
|
422
|
+
leaf: unknown,
|
|
423
|
+
): Record<string, unknown> {
|
|
424
|
+
const segs = path.split(".");
|
|
425
|
+
let node: unknown = leaf;
|
|
426
|
+
for (let i = segs.length - 1; i >= 0; i--)
|
|
427
|
+
node = { [segs[i] as string]: node };
|
|
428
|
+
return node as Record<string, unknown>;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface PolicyFixture {
|
|
432
|
+
/** `compliant` | `violating` | `absent` — the mock's role in the self-test. */
|
|
433
|
+
kind: "compliant" | "violating" | "absent";
|
|
434
|
+
/** the resource's planned values. */
|
|
435
|
+
values: Record<string, unknown>;
|
|
436
|
+
/** what the rule MUST conclude about it. */
|
|
437
|
+
expect_violation: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The three fixtures every authored rule is tested against. The `absent` case is
|
|
442
|
+
* the one that matters most: it is both the commonest real shape (an optional
|
|
443
|
+
* block simply not written) and the case a hand-written policy most often gets
|
|
444
|
+
* wrong.
|
|
445
|
+
*/
|
|
446
|
+
export function buildFixtures(spec: PolicyRuleSpec): PolicyFixture[] {
|
|
447
|
+
const fixtures: PolicyFixture[] = [];
|
|
448
|
+
// `absent` as an OPERATOR means "this must not be set" — no present value can
|
|
449
|
+
// satisfy it, so there is no compliant-value fixture to build.
|
|
450
|
+
if (spec.operator !== "absent") {
|
|
451
|
+
fixtures.push({
|
|
452
|
+
kind: "compliant",
|
|
453
|
+
values: nestValue(spec.attribute, compliantValue(spec)),
|
|
454
|
+
expect_violation: false,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// Symmetrically, `present` is satisfied by ANY value, so no violating-value
|
|
458
|
+
// fixture exists — its only failure mode is the absent case below.
|
|
459
|
+
if (spec.operator !== "present") {
|
|
460
|
+
fixtures.push({
|
|
461
|
+
kind: "violating",
|
|
462
|
+
values: nestValue(spec.attribute, violatingValue(spec)),
|
|
463
|
+
expect_violation: true,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
fixtures.push({
|
|
467
|
+
kind: "absent",
|
|
468
|
+
values: {},
|
|
469
|
+
expect_violation: spec.when_absent === "violation",
|
|
470
|
+
});
|
|
471
|
+
return fixtures;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export interface SelfTestCase {
|
|
475
|
+
kind: PolicyFixture["kind"];
|
|
476
|
+
expected: boolean;
|
|
477
|
+
actual: boolean;
|
|
478
|
+
passed: boolean;
|
|
479
|
+
reason: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export interface SelfTestReport {
|
|
483
|
+
/** every case behaved as its fixture demanded AND the rule denies something.
|
|
484
|
+
* Both, deliberately: a rule that agrees with all three fixtures by never
|
|
485
|
+
* firing has passed nothing. */
|
|
486
|
+
passed: boolean;
|
|
487
|
+
cases: SelfTestCase[];
|
|
488
|
+
/** true when the rule denied at least one fixture — the vacuity check. A rule
|
|
489
|
+
* that denies nothing is not a rule. */
|
|
490
|
+
denies_something: boolean;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Run the spec against its own fixtures. A spec that fails here is never
|
|
495
|
+
* emitted: the caller refuses it rather than hand a human a policy that does
|
|
496
|
+
* not do what its own description says.
|
|
497
|
+
*/
|
|
498
|
+
export function selfTestSpec(spec: PolicyRuleSpec): SelfTestReport {
|
|
499
|
+
const cases: SelfTestCase[] = [];
|
|
500
|
+
for (const fixture of buildFixtures(spec)) {
|
|
501
|
+
const verdict = evaluateSpec(spec, fixture.values);
|
|
502
|
+
cases.push({
|
|
503
|
+
kind: fixture.kind,
|
|
504
|
+
expected: fixture.expect_violation,
|
|
505
|
+
actual: verdict.violates,
|
|
506
|
+
passed: verdict.violates === fixture.expect_violation,
|
|
507
|
+
reason: verdict.reason,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const denies_something = cases.some((c) => c.actual);
|
|
511
|
+
return {
|
|
512
|
+
passed: cases.every((c) => c.passed) && denies_something,
|
|
513
|
+
cases,
|
|
514
|
+
denies_something,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ── Rego emission ────────────────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
/** A Rego identifier derived from the rule id. */
|
|
521
|
+
function regoIdent(id: string): string {
|
|
522
|
+
return id.replace(/-/g, "_");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** A Rego (JSON-compatible) literal. */
|
|
526
|
+
function lit(v: unknown): string {
|
|
527
|
+
return JSON.stringify(v);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* The shared helper every emitted policy imports by inclusion. Kept as ONE
|
|
532
|
+
* reviewed block rather than inlined per rule, because the array-flattening
|
|
533
|
+
* semantics are the part a reader must check — and they should only have to
|
|
534
|
+
* check it once.
|
|
535
|
+
*
|
|
536
|
+
* Rego forbids recursion, so per-rule path traversal is unrolled at generation
|
|
537
|
+
* time (see `pathComprehension`); `_members` is the non-recursive primitive that
|
|
538
|
+
* makes each step uniform.
|
|
539
|
+
*/
|
|
540
|
+
export const REGO_PRELUDE = `# Shared helpers for ${PRODUCT_NAME}-authored policies. Reviewed once, used by every rule.
|
|
541
|
+
# _members normalises a node to a list so an array-valued block and a single
|
|
542
|
+
# block traverse identically — the same rule ${PRODUCT_NAME}'s own evaluator applies.
|
|
543
|
+
_members(x) := x if is_array(x)
|
|
544
|
+
_members(x) := [x] if not is_array(x)
|
|
545
|
+
|
|
546
|
+
# Only plan actions that produce or change configuration are judged. A delete
|
|
547
|
+
# leaves no "after" to assert on, and a no-op has already been judged.
|
|
548
|
+
_relevant(rc) if {
|
|
549
|
+
some action in rc.change.actions
|
|
550
|
+
action in {"create", "update"}
|
|
551
|
+
}
|
|
552
|
+
`;
|
|
553
|
+
|
|
554
|
+
/** The unrolled comprehension resolving `path` against `root`, mirroring `resolvePath`. */
|
|
555
|
+
function pathComprehension(path: string, root: string): string {
|
|
556
|
+
const segs = path.split(".");
|
|
557
|
+
const lines: string[] = [];
|
|
558
|
+
let cur = root;
|
|
559
|
+
for (let i = 0; i < segs.length; i++) {
|
|
560
|
+
const parent = `p${i}`;
|
|
561
|
+
lines.push(`\tsome ${parent} in _members(${cur})`);
|
|
562
|
+
lines.push(`\tis_object(${parent})`);
|
|
563
|
+
const child = `c${i}`;
|
|
564
|
+
lines.push(`\t${child} := ${parent}[${lit(segs[i])}]`);
|
|
565
|
+
cur = child;
|
|
566
|
+
}
|
|
567
|
+
// terminal flatten + null drop, matching resolvePath's tail
|
|
568
|
+
lines.push(`\tsome leaf in _members(${cur})`);
|
|
569
|
+
lines.push(`\tleaf != null`);
|
|
570
|
+
return `[leaf |\n${lines.join("\n")}\n]`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* The alternative bodies of the per-value predicate, each a list of Rego
|
|
575
|
+
* expressions ANDed together. Multiple bodies become multiple definitions of the
|
|
576
|
+
* same function, which Rego ORs.
|
|
577
|
+
*
|
|
578
|
+
* The TYPE GUARDS are not decoration. Rego's comparison operators order ACROSS
|
|
579
|
+
* types rather than erroring — `"5" >= 1` is true, because numbers sort before
|
|
580
|
+
* strings — so an unguarded `gte` would pass a string where the in-process
|
|
581
|
+
* evaluator (and any reasonable reading of the rule) fails it. Every operator
|
|
582
|
+
* whose meaning depends on the operand's type states that type explicitly, so
|
|
583
|
+
* the emitted Rego and `valueSatisfies` cannot disagree.
|
|
584
|
+
*/
|
|
585
|
+
function regoPredicateBodies(spec: PolicyRuleSpec, v: string): string[][] {
|
|
586
|
+
const val = spec.value;
|
|
587
|
+
switch (spec.operator) {
|
|
588
|
+
case "equals":
|
|
589
|
+
return [[`${v} == ${lit(val)}`]];
|
|
590
|
+
case "not_equals":
|
|
591
|
+
return [[`${v} != ${lit(val)}`]];
|
|
592
|
+
case "in":
|
|
593
|
+
return [[`${v} in ${lit(val)}`]];
|
|
594
|
+
case "not_in":
|
|
595
|
+
return [[`not ${v} in ${lit(val)}`]];
|
|
596
|
+
case "present":
|
|
597
|
+
return [[`${v} != null`]];
|
|
598
|
+
case "absent":
|
|
599
|
+
return [[`${v} == null`]];
|
|
600
|
+
case "is_true":
|
|
601
|
+
return [[`${v} == true`]];
|
|
602
|
+
case "is_false":
|
|
603
|
+
return [[`${v} == false`]];
|
|
604
|
+
case "matches":
|
|
605
|
+
return [[`is_string(${v})`, `regex.match(${lit(val)}, ${v})`]];
|
|
606
|
+
case "gte":
|
|
607
|
+
return [[`is_number(${v})`, `${v} >= ${lit(val)}`]];
|
|
608
|
+
case "lte":
|
|
609
|
+
return [[`is_number(${v})`, `${v} <= ${lit(val)}`]];
|
|
610
|
+
// `count` accepts strings, arrays and objects and errors on anything else;
|
|
611
|
+
// one body per accepted type keeps a number or a boolean from being "empty".
|
|
612
|
+
case "non_empty":
|
|
613
|
+
return [
|
|
614
|
+
[`is_string(${v})`, `count(${v}) > 0`],
|
|
615
|
+
[`is_array(${v})`, `count(${v}) > 0`],
|
|
616
|
+
[`is_object(${v})`, `count(${v}) > 0`],
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Emit conftest-ready Rego for one spec. Pure and byte-stable — the same spec
|
|
623
|
+
* always yields the same file, so a re-run produces no diff and a diff means the
|
|
624
|
+
* spec changed.
|
|
625
|
+
*
|
|
626
|
+
* Targets `terraform show -json` output (`input.resource_changes`), which is what
|
|
627
|
+
* `policy_check` already feeds conftest.
|
|
628
|
+
*/
|
|
629
|
+
export function emitRego(
|
|
630
|
+
spec: PolicyRuleSpec,
|
|
631
|
+
opts: { packageName?: string } = {},
|
|
632
|
+
): string {
|
|
633
|
+
const pkg = opts.packageName ?? "main";
|
|
634
|
+
const ident = regoIdent(spec.id);
|
|
635
|
+
const ruleId = authoredRuleId(spec.id);
|
|
636
|
+
const header = [
|
|
637
|
+
`# ${spec.title}`,
|
|
638
|
+
`# rule: ${ruleId} severity: ${spec.severity} theme: ${spec.theme}`,
|
|
639
|
+
`# source requirement: ${spec.source_requirement.replace(/\s+/g, " ").trim()}`,
|
|
640
|
+
"#",
|
|
641
|
+
"# GENERATED BY INFRAWEAVER — review before committing. The generator self-tested",
|
|
642
|
+
"# this rule's SEMANTICS against violating/compliant/absent fixtures; it did not",
|
|
643
|
+
`# execute this Rego. Run \`conftest test\` (or ${PRODUCT_NAME}'s policy_check) to do that.`,
|
|
644
|
+
].join("\n");
|
|
645
|
+
|
|
646
|
+
const okRule = regoPredicateBodies(spec, "v")
|
|
647
|
+
.map(
|
|
648
|
+
(body) =>
|
|
649
|
+
`_ok_${ident}(v) if {\n${body.map((line) => `\t${line}`).join("\n")}\n}`,
|
|
650
|
+
)
|
|
651
|
+
.join("\n\n");
|
|
652
|
+
const valsRule = `_vals_${ident}(root) := ${pathComprehension(spec.attribute, "root")}`;
|
|
653
|
+
|
|
654
|
+
const msg = `sprintf("[%s] %s: %s", [${lit(ruleId)}, rc.address, ${lit(spec.message)}])`;
|
|
655
|
+
|
|
656
|
+
const denyValue = [
|
|
657
|
+
"deny contains msg if {",
|
|
658
|
+
"\tsome rc in input.resource_changes",
|
|
659
|
+
`\trc.type == ${lit(spec.resource_type)}`,
|
|
660
|
+
"\t_relevant(rc)",
|
|
661
|
+
`\tvals := _vals_${ident}(rc.change.after)`,
|
|
662
|
+
"\tcount(vals) > 0",
|
|
663
|
+
"\tsome v in vals",
|
|
664
|
+
`\tnot _ok_${ident}(v)`,
|
|
665
|
+
`\tmsg := ${msg}`,
|
|
666
|
+
"}",
|
|
667
|
+
].join("\n");
|
|
668
|
+
|
|
669
|
+
const denyAbsent =
|
|
670
|
+
spec.when_absent === "violation"
|
|
671
|
+
? [
|
|
672
|
+
"",
|
|
673
|
+
"# when_absent = violation: nothing to assert on is itself the finding.",
|
|
674
|
+
"deny contains msg if {",
|
|
675
|
+
"\tsome rc in input.resource_changes",
|
|
676
|
+
`\trc.type == ${lit(spec.resource_type)}`,
|
|
677
|
+
"\t_relevant(rc)",
|
|
678
|
+
`\tcount(_vals_${ident}(rc.change.after)) == 0`,
|
|
679
|
+
`\tmsg := ${msg}`,
|
|
680
|
+
"}",
|
|
681
|
+
].join("\n")
|
|
682
|
+
: "";
|
|
683
|
+
|
|
684
|
+
return `${header}\n\npackage ${pkg}\n\nimport rego.v1\n\n${REGO_PRELUDE}\n${valsRule}\n\n${okRule}\n\n${denyValue}\n${denyAbsent}\n`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ── tfpolicy (.policy.hcl) emission ──────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* How faithful the `.policy.hcl` translation is to the spec.
|
|
691
|
+
*
|
|
692
|
+
* `exact` — a single-segment attribute path, where `attrs.<name>` means exactly
|
|
693
|
+
* what our resolver means.
|
|
694
|
+
* `approximate` — a nested path. tfpolicy's `attrs` traversal does not flatten
|
|
695
|
+
* arrays the way our resolver does, so a nested block that Terraform represents
|
|
696
|
+
* as a list needs `[0]` indexing and the translation asserts on the FIRST
|
|
697
|
+
* element rather than all of them. We emit it, we say so in the file, and the
|
|
698
|
+
* generated `.policytest.hcl` is how the customer confirms it before relying
|
|
699
|
+
* on it.
|
|
700
|
+
*
|
|
701
|
+
* This axis is about PATH SHAPE only. A second, unavoidable difference applies
|
|
702
|
+
* to every rule and is stated in the emitted header instead: HCL converts
|
|
703
|
+
* between types where it can, so a comparison there can succeed on an operand
|
|
704
|
+
* the Rego — which is explicitly type-guarded — rejects.
|
|
705
|
+
*/
|
|
706
|
+
export type PolicyHclFidelity = "exact" | "approximate";
|
|
707
|
+
|
|
708
|
+
export function policyHclFidelity(spec: PolicyRuleSpec): PolicyHclFidelity {
|
|
709
|
+
return spec.attribute.includes(".") ? "approximate" : "exact";
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** An HCL literal for a scalar or list operand. */
|
|
713
|
+
function hcl(v: unknown): string {
|
|
714
|
+
if (Array.isArray(v)) return `[${v.map(hcl).join(", ")}]`;
|
|
715
|
+
return JSON.stringify(v);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** The tfpolicy accessor for the spec's path — `core::try` guarded, single line. */
|
|
719
|
+
function hclAccessor(spec: PolicyRuleSpec): string {
|
|
720
|
+
return `core::try(attrs.${spec.attribute}, null)`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** The tfpolicy predicate over the accessor, as a single-line boolean expression. */
|
|
724
|
+
function hclPredicate(spec: PolicyRuleSpec, acc: string): string {
|
|
725
|
+
const v = spec.value;
|
|
726
|
+
switch (spec.operator) {
|
|
727
|
+
case "equals":
|
|
728
|
+
return `${acc} == ${hcl(v)}`;
|
|
729
|
+
case "not_equals":
|
|
730
|
+
return `${acc} != ${hcl(v)}`;
|
|
731
|
+
case "in":
|
|
732
|
+
return `core::contains(${hcl(v)}, ${acc})`;
|
|
733
|
+
case "not_in":
|
|
734
|
+
return `!core::contains(${hcl(v)}, ${acc})`;
|
|
735
|
+
case "present":
|
|
736
|
+
return `${acc} != null`;
|
|
737
|
+
case "absent":
|
|
738
|
+
return `${acc} == null`;
|
|
739
|
+
case "is_true":
|
|
740
|
+
return `${acc} == true`;
|
|
741
|
+
case "is_false":
|
|
742
|
+
return `${acc} == false`;
|
|
743
|
+
case "matches":
|
|
744
|
+
return `core::try(core::regex(${hcl(v)}, ${acc}) != null, false)`;
|
|
745
|
+
case "gte":
|
|
746
|
+
return `${acc} >= ${hcl(v)}`;
|
|
747
|
+
case "lte":
|
|
748
|
+
return `${acc} <= ${hcl(v)}`;
|
|
749
|
+
case "non_empty":
|
|
750
|
+
return `core::length(core::try(${acc}, [])) > 0`;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** Severity → tfpolicy enforcement level. Only a code-verified `critical`/`high`
|
|
755
|
+
* earns `mandatory`; the rest advise, matching the band doctrine in
|
|
756
|
+
* [[nativeRules]] — a generated rule does not get to hard-block on day one. */
|
|
757
|
+
function enforcementLevel(severity: Severity): "mandatory" | "advisory" {
|
|
758
|
+
return severity === "critical" || severity === "high"
|
|
759
|
+
? "mandatory"
|
|
760
|
+
: "advisory";
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Emit a HashiCorp tfpolicy `.policy.hcl` for one spec.
|
|
765
|
+
*
|
|
766
|
+
* We AUTHOR this dialect; we do not enforce it. `tfpolicy` has no
|
|
767
|
+
* credential-free local plan evaluation and its binary is source-closed, so it
|
|
768
|
+
* cannot replace the OPA gate — the value is that an HCP customer gets a native
|
|
769
|
+
* artifact they can validate and run in their own pipeline.
|
|
770
|
+
*/
|
|
771
|
+
export function emitPolicyHcl(spec: PolicyRuleSpec): string {
|
|
772
|
+
const acc = hclAccessor(spec);
|
|
773
|
+
const predicate = hclPredicate(spec, acc);
|
|
774
|
+
// `when_absent` is expressed structurally rather than left to core::try's
|
|
775
|
+
// default, so the absent case reads the same way it does in the spec.
|
|
776
|
+
const condition =
|
|
777
|
+
spec.when_absent === "compliant"
|
|
778
|
+
? `${acc} == null || (${predicate})`
|
|
779
|
+
: `${acc} != null && (${predicate})`;
|
|
780
|
+
const fidelity = policyHclFidelity(spec);
|
|
781
|
+
const caveat =
|
|
782
|
+
fidelity === "approximate"
|
|
783
|
+
? [
|
|
784
|
+
"# FIDELITY: approximate. This rule's attribute path is nested, and tfpolicy's",
|
|
785
|
+
`# attrs traversal does not flatten list-shaped blocks the way ${PRODUCT_NAME}'s`,
|
|
786
|
+
"# resolver does — a block Terraform represents as a list may need [0] indexing,",
|
|
787
|
+
"# and this condition then asserts on the first element only. Run the generated",
|
|
788
|
+
"# .policytest.hcl with `tfpolicy test` before relying on it.",
|
|
789
|
+
].join("\n")
|
|
790
|
+
: "# FIDELITY: exact. Single-segment attribute path — no traversal ambiguity.";
|
|
791
|
+
|
|
792
|
+
return `# ${spec.title}
|
|
793
|
+
# rule: ${authoredRuleId(spec.id)} severity: ${spec.severity} theme: ${spec.theme}
|
|
794
|
+
# source requirement: ${spec.source_requirement.replace(/\s+/g, " ").trim()}
|
|
795
|
+
#
|
|
796
|
+
# GENERATED BY INFRAWEAVER — review before committing. ${PRODUCT_NAME} authors this
|
|
797
|
+
# dialect but does not execute it: validate with \`tfpolicy validate\` and run the
|
|
798
|
+
# generated .policytest.hcl with \`tfpolicy test\`.
|
|
799
|
+
#
|
|
800
|
+
# TYPES: HCL converts between types where it can, so this condition may accept an
|
|
801
|
+
# operand the companion Rego rejects — that one is explicitly type-guarded. Where
|
|
802
|
+
# the two must agree exactly, the Rego is the reference.
|
|
803
|
+
${caveat}
|
|
804
|
+
|
|
805
|
+
resource_policy ${hcl(spec.resource_type)} ${hcl(regoIdent(spec.id))} {
|
|
806
|
+
enforcement_level = ${hcl(enforcementLevel(spec.severity))}
|
|
807
|
+
|
|
808
|
+
enforce {
|
|
809
|
+
condition = ${condition}
|
|
810
|
+
error_message = ${hcl(spec.message)}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Render a fixture's values as a tfpolicy `attrs = { … }` body. */
|
|
817
|
+
function hclAttrs(values: Record<string, unknown>, indent: string): string {
|
|
818
|
+
const entries = Object.entries(values);
|
|
819
|
+
if (entries.length === 0) return "{}";
|
|
820
|
+
const inner = entries
|
|
821
|
+
.map(([k, v]) => {
|
|
822
|
+
const rendered =
|
|
823
|
+
v !== null && typeof v === "object" && !Array.isArray(v)
|
|
824
|
+
? hclAttrs(v as Record<string, unknown>, `${indent} `)
|
|
825
|
+
: hcl(v);
|
|
826
|
+
return `${indent} ${k} = ${rendered}`;
|
|
827
|
+
})
|
|
828
|
+
.join("\n");
|
|
829
|
+
return `{\n${inner}\n${indent}}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Emit the `.policytest.hcl` companion — the same three fixtures the in-process
|
|
834
|
+
* self-test used, expressed as tfpolicy mocks. This is what turns "we authored
|
|
835
|
+
* it" into something the customer can verify in one command.
|
|
836
|
+
*/
|
|
837
|
+
export function emitPolicyTestHcl(spec: PolicyRuleSpec): string {
|
|
838
|
+
const blocks = buildFixtures(spec).map((f) => {
|
|
839
|
+
const expect = f.expect_violation ? "\n expect_failure = true" : "";
|
|
840
|
+
return `resource ${hcl(spec.resource_type)} ${hcl(f.kind)} {${expect}\n attrs = ${hclAttrs(f.values, " ")}\n}`;
|
|
841
|
+
});
|
|
842
|
+
return `# Fixtures for ${authoredRuleId(spec.id)} — the same violating / compliant / absent
|
|
843
|
+
# cases ${PRODUCT_NAME} self-tested the rule's semantics against. Run with:
|
|
844
|
+
# tfpolicy test --policies=. --tests=.
|
|
845
|
+
|
|
846
|
+
policytest {
|
|
847
|
+
targets = [${hcl(policyFileName(spec))}]
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
${blocks.join("\n\n")}
|
|
851
|
+
`;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// ── plan-JSON fixture (for conftest / policy_check) ──────────────────────────
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* The generated fixtures as a `terraform show -json`-shaped document, so the
|
|
858
|
+
* emitted Rego can be executed against them by conftest without a real plan.
|
|
859
|
+
* One resource per fixture, named for its role.
|
|
860
|
+
*/
|
|
861
|
+
export function emitPlanFixture(spec: PolicyRuleSpec): string {
|
|
862
|
+
const resource_changes = buildFixtures(spec).map((f) => ({
|
|
863
|
+
address: `${spec.resource_type}.${f.kind}`,
|
|
864
|
+
mode: "managed",
|
|
865
|
+
type: spec.resource_type,
|
|
866
|
+
name: f.kind,
|
|
867
|
+
provider_name: `registry.terraform.io/hashicorp/${spec.resource_type.split("_")[0]}`,
|
|
868
|
+
change: { actions: ["create"], before: null, after: f.values },
|
|
869
|
+
}));
|
|
870
|
+
return `${JSON.stringify({ format_version: "1.2", resource_changes }, null, 2)}\n`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ── file naming ──────────────────────────────────────────────────────────────
|
|
874
|
+
|
|
875
|
+
export function regoFileName(spec: PolicyRuleSpec): string {
|
|
876
|
+
return `${spec.id}.rego`;
|
|
877
|
+
}
|
|
878
|
+
export function policyFileName(spec: PolicyRuleSpec): string {
|
|
879
|
+
return `${spec.id}.policy.hcl`;
|
|
880
|
+
}
|
|
881
|
+
export function policyTestFileName(spec: PolicyRuleSpec): string {
|
|
882
|
+
return `${spec.id}.policytest.hcl`;
|
|
883
|
+
}
|
|
884
|
+
export function planFixtureFileName(spec: PolicyRuleSpec): string {
|
|
885
|
+
return `${spec.id}.fixture.plan.json`;
|
|
886
|
+
}
|