terramend 0.2.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 +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -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/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -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 +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -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/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 +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -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/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -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 +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -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 +39 -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 +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-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/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -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.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review-quality controls for the Review / IncrementalReview modes: a curated
|
|
3
|
+
* false-positive precedents list and an adversarial per-finding verification
|
|
4
|
+
* pass. Both are embedded into the mode prompts (modes.ts) — the precedents
|
|
5
|
+
* also travel verbatim inside every verification dispatch, since subagents
|
|
6
|
+
* never see the orchestrator's prompt.
|
|
7
|
+
*
|
|
8
|
+
* Provenance: adapted from Anthropic's claude-code-security-review action
|
|
9
|
+
* (MIT) — its hard-exclusion rules, its LLM-judge "PRECEDENTS" list, and the
|
|
10
|
+
* /security-review slash command's refute-subagent pattern. See
|
|
11
|
+
* CLAUDE-CODE-SECURITY-REVIEW-VS-TERRAMEND.md (workspace root) §5.1–5.2 for
|
|
12
|
+
* the comparison that motivated this. Deliberate divergences from upstream:
|
|
13
|
+
* - CCSR judges findings via sequential direct API calls; we dispatch
|
|
14
|
+
* parallel read-only reviewfrog subagents (machine-gated, see
|
|
15
|
+
* agents/subagentToolGates.ts) so verification adds one round of wall
|
|
16
|
+
* time regardless of finding count.
|
|
17
|
+
* - CCSR's judge gates on a bare confidence number; we gate on an explicit
|
|
18
|
+
* verdict (confirmed/refuted/uncertain) plus confidence, because the
|
|
19
|
+
* verdict is what the orchestrator acts on and the number alone invites
|
|
20
|
+
* anchoring.
|
|
21
|
+
* - CCSR drops "secrets stored on disk" findings (handled by a separate
|
|
22
|
+
* pipeline there). Terramend reviews IaC where a hardcoded credential is
|
|
23
|
+
* a core finding, so that exclusion is intentionally NOT inherited.
|
|
24
|
+
* Kept as-is from upstream: fail-open semantics (a broken verifier must
|
|
25
|
+
* never silently swallow a true positive) and suppression auditability
|
|
26
|
+
* (excluded findings are listed, never deleted).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { REVIEWER_AGENT_NAME } from "#app/agents/reviewer";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* False-positive precedents applied at aggregation time and inside every
|
|
33
|
+
* verification dispatch. Each entry encodes a recurring FP class; a candidate
|
|
34
|
+
* matching one needs specific evidence that the precedent does not apply, or
|
|
35
|
+
* it gets dropped. Ordered: hard exclusions (never post) → general code
|
|
36
|
+
* precedents → Terraform/IaC precedents → the final signal-quality bar.
|
|
37
|
+
*/
|
|
38
|
+
export const REVIEW_FINDING_PRECEDENTS = `### Finding precedents (false-positive control)
|
|
39
|
+
|
|
40
|
+
Apply these when deciding whether a candidate finding is worth posting, and include this whole section verbatim in every verification dispatch. Each precedent encodes a recurring false-positive class: a candidate that matches one is dropped unless you have specific evidence the precedent does not apply here.
|
|
41
|
+
|
|
42
|
+
**Hard exclusions — never post:**
|
|
43
|
+
|
|
44
|
+
- Denial-of-service / resource-exhaustion concerns without a concrete, cheap-to-trigger attack path: missing rate limiting, "unbounded" loops over trusted input, "could exhaust memory/CPU".
|
|
45
|
+
- Theoretical race conditions or timing attacks. Post a race only when it is concretely reachable and concretely harmful.
|
|
46
|
+
- Memory-safety findings (buffer overflow, use-after-free, OOB) in memory-safe languages — Rust, Go, JS/TS, Python, Java, HCL.
|
|
47
|
+
- Security findings whose anchor is a documentation file — a code snippet in \`.md\`/\`.mdx\` is not an attack surface. (Stale or incorrect docs remain valid *impact* findings; this exclusion is only for treating doc content as exploitable.)
|
|
48
|
+
- "Lack of hardening" with no vulnerability: code is not required to implement every best practice, only to avoid concrete flaws.
|
|
49
|
+
- Vulnerable-dependency reports based on version strings alone — dependency scanning is a separate pipeline with its own remediation flow.
|
|
50
|
+
|
|
51
|
+
**General precedents:**
|
|
52
|
+
|
|
53
|
+
- Environment variables, CLI flags, and workflow-dispatch inputs are operator-trusted. An attack that requires controlling them is invalid.
|
|
54
|
+
- A missing permission/auth check in client-side code is not a finding; the server is the enforcement boundary. The same applies to client-side input validation.
|
|
55
|
+
- React/Angular-class frameworks escape output by default — an XSS claim needs \`dangerouslySetInnerHTML\`, \`bypassSecurityTrustHtml\`, or an equivalent unsafe API in the diff.
|
|
56
|
+
- SSRF requires control of host or protocol; path-only control is not SSRF. Neither SSRF nor path traversal applies to purely client-side code.
|
|
57
|
+
- Command injection in shell scripts needs a named untrusted-input path; developer-invoked scripts taking developer-supplied arguments don't qualify.
|
|
58
|
+
- Un-sanitized user input reaching logs is log spoofing, not a vulnerability. A logging finding is valid only when it exposes secrets, credentials, or PII.
|
|
59
|
+
- UUIDs are unguessable; an attack that requires guessing one is invalid.
|
|
60
|
+
|
|
61
|
+
**Terraform / IaC precedents:**
|
|
62
|
+
|
|
63
|
+
- \`0.0.0.0/0\` **egress** is common and usually intentional — flag open **ingress**, or egress only with a concrete exfiltration concern attached.
|
|
64
|
+
- Values from \`*.tfvars\`, \`locals\`, and module input variables are operator-trusted; "what if this variable is malicious" is invalid without naming an untrusted writer.
|
|
65
|
+
- Missing encryption / versioning / access-logging on resources that demonstrably hold no sensitive data (short-retention log groups, test fixtures, scratch buckets) is ℹ️ at most, never 🚨.
|
|
66
|
+
- Unpinned provider or module versions are style feedback for first-party modules; ⚠️ only for third-party module sources, where the unpinned ref is supply-chain surface.
|
|
67
|
+
- Do not infer state drift, plan outcomes, or "this will destroy/replace the database" from static HCL — only \`terraform plan\` evidence supports those claims. Without plan evidence, phrase the concern as an open question, not a finding.
|
|
68
|
+
- Missing tags and naming-convention deviations are nitpicks, not findings.
|
|
69
|
+
- When a deterministic scanner rule covers the same issue (trivy \`AVD-*\`, checkov \`CKV_*\`, tflint), cite the rule id in the finding — that makes it ✗→✓ verifiable downstream instead of an unverifiable reviewer opinion.
|
|
70
|
+
|
|
71
|
+
**Signal-quality bar** — a surviving candidate must still answer yes to all three: Is there a concrete failure or attack path? Is it a real risk rather than a theoretical best practice? Could the author act on it exactly as written?`;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Adversarial verification pass, spliced into the aggregation step of Review
|
|
75
|
+
* and IncrementalReview (between the non-anchored-concern hunt and comment
|
|
76
|
+
* drafting). The 0-or-2+ lens rule does not apply here — that rule buys
|
|
77
|
+
* independence between discovery perspectives; verification is a per-claim
|
|
78
|
+
* judgment with no orthogonality to purchase, so one finding = one dispatch
|
|
79
|
+
* is correct even when there is exactly one finding.
|
|
80
|
+
*/
|
|
81
|
+
export const FINDING_VERIFICATION_PASS = `**Adversarial verification — required before posting any 🚨/⚠️ finding.** A candidate finding is a hypothesis until an independent pass has tried to kill it; your own trace is not independent, because you found it. For every candidate you intend to post at 🚨 critical or ⚠️ important, dispatch one \`${REVIEWER_AGENT_NAME}\` verification subagent — ALL of them in a single assistant turn as parallel Task tool_use blocks. One candidate = one subagent, and dispatching exactly one is fine here: the 0-or-2+ rule governs discovery lenses, where independence between perspectives is the point; verification is per-claim judgment with no orthogonality to buy. Skip verification only for:
|
|
82
|
+
|
|
83
|
+
- ℹ️ informational findings and nitpicks (post on your own judgment), and
|
|
84
|
+
- findings whose evidence is deterministic tool output — a scanner concern id, a failing test, a compiler/type error. Those re-verify mechanically and need no judge.
|
|
85
|
+
|
|
86
|
+
Each verification dispatch contains, in order:
|
|
87
|
+
- the absolute \`diffPath\` (and \`incrementalDiffPath\` when available) named verbatim — the reviewer's baked-in system prompt selects its first action on this token;
|
|
88
|
+
- the single finding under test: file, line, intended severity, the claim, and the evidence you collected;
|
|
89
|
+
- the **Finding precedents** section — plus any \`### Finding precedents — org addendum\` section from your instructions — included verbatim (the subagent cannot see your prompt);
|
|
90
|
+
- this charge: "Attempt to REFUTE this finding. Read the actual code — do not trust the claim's description of it. Apply the finding precedents. Report a verdict (\`confirmed\` / \`refuted\` / \`uncertain\`), a confidence score 1–10, and a 2–3 sentence justification quoting the code that decides it. When the attack or failure path is theoretical rather than demonstrated, bias toward \`refuted\`."
|
|
91
|
+
|
|
92
|
+
Set the Task \`description\` to \`verify:<file>:<line>\` so parallel verifications are distinguishable in CI logs. Asking for a verdict schema is correct here and does not violate the discovery-lens "no finding schema" discipline — the subagent is judging one claim, not exploring.
|
|
93
|
+
|
|
94
|
+
Gate on what comes back:
|
|
95
|
+
- \`refuted\` at confidence ≥ 7 → suppress the finding and record it for the audit trail.
|
|
96
|
+
- \`uncertain\`, or \`refuted\` at lower confidence → re-read the decisive code yourself; either downgrade to ℹ️ with the uncertainty stated, or suppress. Do not post it at 🚨/⚠️.
|
|
97
|
+
- \`confirmed\` → post it; fold the verifier's justification into the comment's technical-details block when it adds evidence.
|
|
98
|
+
- errored / timed out / nothing usable → retry once; if it still fails, KEEP the finding and add \`verification unavailable\` to its technical details. Fail open: a broken verifier must never silently swallow a true positive, and must never block the review.
|
|
99
|
+
|
|
100
|
+
Suppressed findings are recorded, never silently deleted — list every one in the \`Suppressed findings\` block at the bottom of the review body (shape defined in the format below): severity, \`file:line\`, the claim in a few words, the refutation in a few words. An unaudited filter eats true positives invisibly; the audit trail is what lets a human catch the filter being wrong.`;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Heading under which `fp_filtering_instructions` (the action input carrying
|
|
104
|
+
* org-specific FP precedents) is appended to the Review mode instructions.
|
|
105
|
+
* FINDING_VERIFICATION_PASS names this heading when telling the orchestrator
|
|
106
|
+
* what to include verbatim in each verification dispatch — the two strings
|
|
107
|
+
* are a contract; change them together.
|
|
108
|
+
*/
|
|
109
|
+
export const FP_PRECEDENTS_ADDENDUM_HEADING = "### Finding precedents — org addendum";
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Merge the §5.5 action inputs into the per-mode user instructions that
|
|
113
|
+
* `select_mode` appends to the mode prompt (see buildOrchestratorGuidance in
|
|
114
|
+
* mcp/selectMode.ts). Both land on the "Review" key — IncrementalReview
|
|
115
|
+
* inherits Review's instructions via modeInstructionParent. Composes with
|
|
116
|
+
* (never replaces) backend-provided instructions: hosted settings and
|
|
117
|
+
* workflow-file inputs are both repo-owner-controlled surfaces.
|
|
118
|
+
*/
|
|
119
|
+
export function mergeReviewModeInstructions(
|
|
120
|
+
base: Record<string, string>,
|
|
121
|
+
inputs: { reviewInstructions?: string | undefined; fpFilteringInstructions?: string | undefined },
|
|
122
|
+
): Record<string, string> {
|
|
123
|
+
const review = inputs.reviewInstructions?.trim();
|
|
124
|
+
const fp = inputs.fpFilteringInstructions?.trim();
|
|
125
|
+
if (!review && !fp) return base;
|
|
126
|
+
|
|
127
|
+
const sections = [base.Review, review];
|
|
128
|
+
if (fp) {
|
|
129
|
+
sections.push(
|
|
130
|
+
`${FP_PRECEDENTS_ADDENDUM_HEADING}\n\nApply these alongside the built-in Finding precedents, and include this whole section verbatim in every verification dispatch.\n\n${fp}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return { ...base, Review: sections.filter(Boolean).join("\n\n") };
|
|
134
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { accessSync, existsSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { runTerramendCli } from "#app/runCli";
|
|
6
|
+
import actionPackageJson from "#package.json" with { type: "json" };
|
|
7
|
+
|
|
8
|
+
vi.mock("node:child_process", async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import("node:child_process")>();
|
|
10
|
+
return { ...actual, execFileSync: vi.fn() };
|
|
11
|
+
});
|
|
12
|
+
vi.mock("node:fs", async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import("node:fs")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
accessSync: vi.fn(),
|
|
17
|
+
existsSync: vi.fn(() => true),
|
|
18
|
+
mkdtempSync: vi.fn(() => "/tmp/terramend-bootstrap-xyz"),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const nodeBinDir = dirname(process.execPath);
|
|
23
|
+
|
|
24
|
+
/** make accessSync succeed only for paths matching the given predicate. */
|
|
25
|
+
function allowExecutables(predicate: (path: string) => boolean): void {
|
|
26
|
+
vi.mocked(accessSync).mockImplementation((path) => {
|
|
27
|
+
if (!predicate(String(path))) {
|
|
28
|
+
throw new Error("EACCES");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function execCall(index = 0): {
|
|
34
|
+
command: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
options: Record<string, unknown>;
|
|
37
|
+
} {
|
|
38
|
+
const call = vi.mocked(execFileSync).mock.calls[index];
|
|
39
|
+
if (!call) throw new Error(`execFileSync call ${index} missing`);
|
|
40
|
+
return {
|
|
41
|
+
command: String(call[0]),
|
|
42
|
+
args: (call[1] ?? []) as string[],
|
|
43
|
+
options: (call[2] ?? {}) as Record<string, unknown>,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
allowExecutables(() => true);
|
|
49
|
+
// pin the Windows executable-extension list so candidate paths are deterministic
|
|
50
|
+
vi.stubEnv("PATHEXT", ".CMD");
|
|
51
|
+
vi.stubEnv("GITHUB_WORKSPACE", "");
|
|
52
|
+
vi.stubEnv("GITHUB_ACTION_REF", "");
|
|
53
|
+
vi.stubEnv("GITHUB_ACTION_REPOSITORY", "");
|
|
54
|
+
vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.unstubAllEnvs();
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
vi.clearAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("runTerramendCli – npx bootstrap path", () => {
|
|
64
|
+
it("runs the exact-pinned npm package via npx in a fresh tmpdir", () => {
|
|
65
|
+
runTerramendCli({ cliArgs: ["gha", "token"] });
|
|
66
|
+
|
|
67
|
+
const { command, args, options } = execCall();
|
|
68
|
+
expect(command).toContain("npx");
|
|
69
|
+
expect(args).toEqual(["--yes", `terramend@${actionPackageJson.version}`, "gha", "token"]);
|
|
70
|
+
expect(options.cwd).toBe("/tmp/terramend-bootstrap-xyz");
|
|
71
|
+
expect(mkdtempSync).toHaveBeenCalledWith(expect.stringContaining("terramend-bootstrap-"));
|
|
72
|
+
|
|
73
|
+
const env = options.env as NodeJS.ProcessEnv;
|
|
74
|
+
expect(env.npm_config_registry).toBe("https://registry.npmjs.org");
|
|
75
|
+
expect(env.COREPACK_NPM_REGISTRY).toBe("https://registry.npmjs.org");
|
|
76
|
+
expect(env.npm_config_min_release_age).toBe("0");
|
|
77
|
+
expect(env.pnpm_config_minimum_release_age).toBe("0");
|
|
78
|
+
expect(env.PATH).toContain(nodeBinDir);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("falls back to corepack pnpm dlx when npx is missing", () => {
|
|
82
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
83
|
+
allowExecutables((path) => path.includes("corepack"));
|
|
84
|
+
|
|
85
|
+
runTerramendCli({ cliArgs: ["gha"] });
|
|
86
|
+
|
|
87
|
+
expect(warnSpy).toHaveBeenCalledWith("» npx not found, using corepack pnpm dlx");
|
|
88
|
+
const { command, args } = execCall();
|
|
89
|
+
expect(command).toContain("corepack");
|
|
90
|
+
expect(args).toEqual(["pnpm", "dlx", `terramend@${actionPackageJson.version}`, "gha"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("throws when neither npx nor corepack can be found", () => {
|
|
94
|
+
allowExecutables(() => false);
|
|
95
|
+
|
|
96
|
+
expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow(
|
|
97
|
+
/could not find npx or corepack on PATH/,
|
|
98
|
+
);
|
|
99
|
+
expect(execFileSync).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("ignores PATH entries inside the customer workspace and relative entries", () => {
|
|
103
|
+
const workspace = join(nodeBinDir, "workspace-checkout");
|
|
104
|
+
vi.stubEnv("GITHUB_WORKSPACE", workspace);
|
|
105
|
+
vi.stubEnv("PATH", `${join(workspace, "bin")}${process.platform === "win32" ? ";" : ":"}bin`);
|
|
106
|
+
// every candidate is "accessible" — only the untrusted-path filter can reject
|
|
107
|
+
allowExecutables((path) => path.startsWith(workspace) || !path.includes(nodeBinDir));
|
|
108
|
+
|
|
109
|
+
expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow(
|
|
110
|
+
/could not find npx or corepack on PATH/,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("runTerramendCli – local CLI path", () => {
|
|
116
|
+
it("runs the checked-out cli.ts when TERRAMEND_FORCE_LOCAL_CLI=1", () => {
|
|
117
|
+
vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "1");
|
|
118
|
+
|
|
119
|
+
runTerramendCli({ cliArgs: ["gha"] });
|
|
120
|
+
|
|
121
|
+
const { command, args, options } = execCall();
|
|
122
|
+
expect(command).toBe(process.execPath);
|
|
123
|
+
expect(args).toEqual(["cli.ts", "gha"]);
|
|
124
|
+
expect(String(options.cwd)).toContain("src");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("runs locally when the action ref is main on terramend/terramend", () => {
|
|
128
|
+
vi.stubEnv("GITHUB_ACTION_REF", "main");
|
|
129
|
+
vi.stubEnv("GITHUB_ACTION_REPOSITORY", "terramend/terramend");
|
|
130
|
+
|
|
131
|
+
runTerramendCli({ cliArgs: ["gha"] });
|
|
132
|
+
|
|
133
|
+
expect(execCall().command).toBe(process.execPath);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("installs action dependencies via corepack pnpm when node_modules is missing", () => {
|
|
137
|
+
vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "1");
|
|
138
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
139
|
+
|
|
140
|
+
runTerramendCli({ cliArgs: ["gha"] });
|
|
141
|
+
|
|
142
|
+
expect(execFileSync).toHaveBeenCalledTimes(2);
|
|
143
|
+
const install = execCall(0);
|
|
144
|
+
expect(install.command).toContain("corepack");
|
|
145
|
+
expect(install.args).toEqual(["pnpm", "install", "--frozen-lockfile", "--ignore-scripts"]);
|
|
146
|
+
expect(execCall(1).args).toEqual(["cli.ts", "gha"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("throws a descriptive error when corepack is required but missing", () => {
|
|
150
|
+
vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "1");
|
|
151
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
152
|
+
allowExecutables(() => false);
|
|
153
|
+
|
|
154
|
+
expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow(
|
|
155
|
+
/could not find corepack on PATH \(needed to install action dependencies via pnpm\)/,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
function mockProcessExit() {
|
|
161
|
+
return vi.spyOn(process, "exit").mockImplementation(((code?: number | string | null) => {
|
|
162
|
+
throw new Error(`process.exit:${code}`);
|
|
163
|
+
}) as never);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
describe("runTerramendCli – child exit propagation", () => {
|
|
167
|
+
let exitSpy: ReturnType<typeof mockProcessExit>;
|
|
168
|
+
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
exitSpy = mockProcessExit();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("propagates a numeric child exit status silently", () => {
|
|
174
|
+
const childError = Object.assign(new Error("Command failed"), { status: 3 });
|
|
175
|
+
vi.mocked(execFileSync).mockImplementationOnce(() => {
|
|
176
|
+
throw childError;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow("process.exit:3");
|
|
180
|
+
expect(exitSpy).toHaveBeenCalledWith(3);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("exits 1 when the child was killed by a signal", () => {
|
|
184
|
+
const childError = Object.assign(new Error("Command failed"), {
|
|
185
|
+
status: null,
|
|
186
|
+
signal: "SIGTERM",
|
|
187
|
+
});
|
|
188
|
+
vi.mocked(execFileSync).mockImplementationOnce(() => {
|
|
189
|
+
throw childError;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow("process.exit:1");
|
|
193
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("rethrows genuine spawn failures", () => {
|
|
197
|
+
vi.mocked(execFileSync).mockImplementationOnce(() => {
|
|
198
|
+
throw new Error("spawn ENOENT");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow("spawn ENOENT");
|
|
202
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("swallows errors and warns when swallowErrors is set", () => {
|
|
206
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
207
|
+
vi.mocked(execFileSync).mockImplementationOnce(() => {
|
|
208
|
+
throw new Error("cleanup blew up");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(() => runTerramendCli({ cliArgs: ["gha"], swallowErrors: true })).not.toThrow();
|
|
212
|
+
expect(warnSpy).toHaveBeenCalledWith("» terramend cleanup bootstrap failed: cleanup blew up");
|
|
213
|
+
});
|
|
214
|
+
});
|
package/src/runCli.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import actionPackageJson from "#package.json" with { type: "json" };
|
|
7
|
+
|
|
8
|
+
interface RunTerramendCliParams {
|
|
9
|
+
cliArgs: string[];
|
|
10
|
+
swallowErrors?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RuntimeContext {
|
|
14
|
+
actionRef: string | undefined;
|
|
15
|
+
actionRepository: string | undefined;
|
|
16
|
+
actionRoot: string;
|
|
17
|
+
nodeBinDir: string;
|
|
18
|
+
env: NodeJS.ProcessEnv;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
22
|
+
// Pin the EXACT version baked into this action ref, not a `^` range. A consumer
|
|
23
|
+
// who SHA-pins `uses: terramend/terramend@<sha>` is pinning this file; resolving
|
|
24
|
+
// `^x.y.z` at runtime would silently run a newer npm publish than the pinned
|
|
25
|
+
// ref, so a single malicious/compromised publish would reach every consumer
|
|
26
|
+
// despite their pin. Exact-pinning makes the executed code match the vetted ref.
|
|
27
|
+
const FALLBACK_PACKAGE_SPEC = `terramend@${actionPackageJson.version}`;
|
|
28
|
+
|
|
29
|
+
function getErrorMessage(error: unknown): string {
|
|
30
|
+
return error instanceof Error ? error.message : String(error);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function canAccessExecutable(path: string): boolean {
|
|
34
|
+
try {
|
|
35
|
+
accessSync(path, constants.X_OK);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
if (process.platform !== "win32") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
accessSync(path, constants.F_OK);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// reject PATH entries that an attacker can plausibly write to before terramend
|
|
52
|
+
// runs. specifically: relative entries (., bin, etc., which resolve against
|
|
53
|
+
// cwd), and anything inside the customer's checkout. an attacker who can land
|
|
54
|
+
// a malicious `npx` in the repo and prepend `$GITHUB_WORKSPACE/bin` to
|
|
55
|
+
// `GITHUB_PATH` from a prior workflow step would otherwise get full code
|
|
56
|
+
// execution under our action token.
|
|
57
|
+
//
|
|
58
|
+
// on Windows the filesystem is case-insensitive but `resolve()` preserves
|
|
59
|
+
// input case, so we lowercase both sides before comparing — otherwise an
|
|
60
|
+
// attacker can bypass the filter by varying the case of GITHUB_WORKSPACE in
|
|
61
|
+
// their injected PATH entry (`d:\a\repo` vs `D:\a\repo`).
|
|
62
|
+
function normalizePathForCompare(path: string): string {
|
|
63
|
+
return process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isUntrustedPathEntry(entry: string, untrustedRoots: string[]): boolean {
|
|
67
|
+
if (!isAbsolute(entry)) return true;
|
|
68
|
+
const normalized = normalizePathForCompare(entry);
|
|
69
|
+
for (const root of untrustedRoots) {
|
|
70
|
+
if (normalized === root) return true;
|
|
71
|
+
if (normalized.startsWith(root + sep)) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getUntrustedPathRoots(env: NodeJS.ProcessEnv): string[] {
|
|
77
|
+
const roots: string[] = [];
|
|
78
|
+
const workspace = env.GITHUB_WORKSPACE;
|
|
79
|
+
if (workspace && isAbsolute(workspace)) roots.push(normalizePathForCompare(workspace));
|
|
80
|
+
return roots;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveExecutable(params: { command: string; env: NodeJS.ProcessEnv }): string | null {
|
|
84
|
+
const pathValue = params.env.PATH ?? "";
|
|
85
|
+
const untrustedRoots = getUntrustedPathRoots(params.env);
|
|
86
|
+
const pathEntries = pathValue
|
|
87
|
+
.split(delimiter)
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.filter((entry) => !isUntrustedPathEntry(entry, untrustedRoots));
|
|
90
|
+
const extensions =
|
|
91
|
+
process.platform === "win32"
|
|
92
|
+
? (params.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
|
|
93
|
+
: [""];
|
|
94
|
+
|
|
95
|
+
for (const pathEntry of pathEntries) {
|
|
96
|
+
for (const extension of extensions) {
|
|
97
|
+
const candidate = join(pathEntry, `${params.command}${extension.toLowerCase()}`);
|
|
98
|
+
if (canAccessExecutable(candidate)) {
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createRuntimeContext(): RuntimeContext {
|
|
108
|
+
const actionRoot = dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
const nodeBinDir = dirname(process.execPath);
|
|
110
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
111
|
+
env.npm_config_registry = NPM_REGISTRY;
|
|
112
|
+
env.COREPACK_NPM_REGISTRY = NPM_REGISTRY;
|
|
113
|
+
// bypass customer-side release-age gates (npm's `min-release-age`, pnpm's
|
|
114
|
+
// `minimumReleaseAge`) so our bootstrap can resolve the latest publish.
|
|
115
|
+
// terramend's npm version is server-stamped from a SHA-pinned action ref the
|
|
116
|
+
// customer already vets at the action layer — not a customer-vetted dep, so
|
|
117
|
+
// the gate is the wrong affordance here. env beats .npmrc in both tools.
|
|
118
|
+
// npm uses `npm_config_*`; pnpm v11+ requires `pnpm_config_*` (the v10→v11
|
|
119
|
+
// migration renamed the prefix). tracked: #713
|
|
120
|
+
env.npm_config_min_release_age = "0";
|
|
121
|
+
env.pnpm_config_minimum_release_age = "0";
|
|
122
|
+
const currentPath = process.env.PATH ?? "";
|
|
123
|
+
env.PATH = currentPath ? `${nodeBinDir}${delimiter}${currentPath}` : nodeBinDir;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
actionRef: process.env.GITHUB_ACTION_REF,
|
|
127
|
+
actionRepository: process.env.GITHUB_ACTION_REPOSITORY,
|
|
128
|
+
actionRoot,
|
|
129
|
+
nodeBinDir,
|
|
130
|
+
env,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// $GITHUB_WORKSPACE is the customer's repo. running `npx --yes terramend@…`
|
|
135
|
+
// there makes npm read THEIR `package.json` first, which on npm v11+ enforces
|
|
136
|
+
// `devEngines.packageManager` and aborts the bootstrap with EBADDEVENGINES
|
|
137
|
+
// before the agent ever boots. our bootstrap doesn't need anything from the
|
|
138
|
+
// customer's tree — a freshly-created tmpdir is package.json-free and
|
|
139
|
+
// parent-less, so npm walks up to `/` finding nothing. see #837.
|
|
140
|
+
//
|
|
141
|
+
// `mkdtempSync` (vs raw `tmpdir()`): `$TMPDIR` is overridable from a prior
|
|
142
|
+
// `$GITHUB_ENV` step, and a customer-authored or compromised prior step
|
|
143
|
+
// could plant `node_modules/terramend/` in the resolved tmpdir to hijack
|
|
144
|
+
// `npx --yes terramend@<version>` resolution. a fresh per-invocation
|
|
145
|
+
// subdirectory is mode 0700 and not pre-writable by anything earlier in
|
|
146
|
+
// the job.
|
|
147
|
+
function runCommand(params: { context: RuntimeContext; command: string; args: string[] }): void {
|
|
148
|
+
execFileSync(params.command, params.args, {
|
|
149
|
+
cwd: mkdtempSync(join(tmpdir(), "terramend-bootstrap-")),
|
|
150
|
+
stdio: "inherit",
|
|
151
|
+
env: params.context.env,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// resolve a launcher binary by walking PATH (which already has the action
|
|
156
|
+
// runtime's nodeBinDir prepended). some hosted Node 24 runner pools ship
|
|
157
|
+
// `node` at `externals/node24/bin/node` without the sibling `npx`/`corepack`,
|
|
158
|
+
// so a hardcoded sibling path can't be relied on — fall back to whatever the
|
|
159
|
+
// runner image provides on PATH.
|
|
160
|
+
function requireExecutable(params: {
|
|
161
|
+
context: RuntimeContext;
|
|
162
|
+
command: string;
|
|
163
|
+
purpose: string;
|
|
164
|
+
}): string {
|
|
165
|
+
const resolved = resolveExecutable({ command: params.command, env: params.context.env });
|
|
166
|
+
if (!resolved) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`could not find ${params.command} on PATH (needed to ${params.purpose}); ` +
|
|
169
|
+
`runtime PATH was: ${params.context.env.PATH ?? "<empty>"}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return resolved;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runPackageCli(context: RuntimeContext, packageSpec: string, cliArgs: string[]): void {
|
|
176
|
+
const npxPath = resolveExecutable({ command: "npx", env: context.env });
|
|
177
|
+
if (npxPath) {
|
|
178
|
+
runCommand({ context, command: npxPath, args: ["--yes", packageSpec, ...cliArgs] });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const corepackPath = resolveExecutable({ command: "corepack", env: context.env });
|
|
183
|
+
if (corepackPath) {
|
|
184
|
+
console.warn("» npx not found, using corepack pnpm dlx");
|
|
185
|
+
runCommand({ context, command: corepackPath, args: ["pnpm", "dlx", packageSpec, ...cliArgs] });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
`could not find npx or corepack on PATH to run ${packageSpec}; ` +
|
|
191
|
+
`runtime PATH was: ${context.env.PATH ?? "<empty>"}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ensureActionDependencies(context: RuntimeContext): void {
|
|
196
|
+
const nodeModulesPath = join(context.actionRoot, "node_modules");
|
|
197
|
+
if (existsSync(nodeModulesPath)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const corepackPath = requireExecutable({
|
|
202
|
+
context,
|
|
203
|
+
command: "corepack",
|
|
204
|
+
purpose: "install action dependencies via pnpm",
|
|
205
|
+
});
|
|
206
|
+
const adjacentCorepack = join(
|
|
207
|
+
context.nodeBinDir,
|
|
208
|
+
process.platform === "win32" ? "corepack.cmd" : "corepack",
|
|
209
|
+
);
|
|
210
|
+
if (corepackPath !== adjacentCorepack) {
|
|
211
|
+
// bad-runner case: GitHub's externals/node24/bin/ is missing the corepack
|
|
212
|
+
// sibling, so we resolved via PATH instead. logging this lets us correlate
|
|
213
|
+
// bootstrap path to runner pool when validating the fix.
|
|
214
|
+
console.warn(
|
|
215
|
+
`» nodeBinDir corepack missing (${adjacentCorepack}); using PATH-resolved ${corepackPath}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
execFileSync(corepackPath, ["pnpm", "install", "--frozen-lockfile", "--ignore-scripts"], {
|
|
219
|
+
cwd: context.actionRoot,
|
|
220
|
+
stdio: "inherit",
|
|
221
|
+
env: context.env,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function runLocalCli(context: RuntimeContext, cliArgs: string[]): void {
|
|
226
|
+
ensureActionDependencies(context);
|
|
227
|
+
execFileSync(process.execPath, ["cli.ts", ...cliArgs], {
|
|
228
|
+
cwd: context.actionRoot,
|
|
229
|
+
stdio: "inherit",
|
|
230
|
+
env: context.env,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function runTerramendCliInner(context: RuntimeContext, cliArgs: string[]): void {
|
|
235
|
+
if (process.env.TERRAMEND_FORCE_LOCAL_CLI === "1") {
|
|
236
|
+
runLocalCli(context, cliArgs);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (context.actionRef === "main" && context.actionRepository === "terramend/terramend") {
|
|
241
|
+
runLocalCli(context, cliArgs);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
runPackageCli(context, FALLBACK_PACKAGE_SPEC, cliArgs);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// the inner CLI and the bootstrap install run with `stdio: "inherit"`, so on a
|
|
249
|
+
// non-zero exit they've already printed their own `##[error]` line. node turns
|
|
250
|
+
// that exit into a thrown `Error: Command failed…`; letting it bubble crashes
|
|
251
|
+
// the outer bootstrap with a `node:internal/errors` stack trace that buries the
|
|
252
|
+
// real failure (#862, #867). propagate the child's exit code silently instead;
|
|
253
|
+
// only genuine spawn failures (ENOENT, missing npx, …) still surface.
|
|
254
|
+
function propagateChildExit(error: unknown): never {
|
|
255
|
+
if (error instanceof Error && "status" in error && typeof error.status === "number") {
|
|
256
|
+
process.exit(error.status);
|
|
257
|
+
}
|
|
258
|
+
if (error instanceof Error && "signal" in error && error.signal != null) {
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function runTerramendCli(params: RunTerramendCliParams): void {
|
|
265
|
+
const context = createRuntimeContext();
|
|
266
|
+
|
|
267
|
+
if (params.swallowErrors) {
|
|
268
|
+
try {
|
|
269
|
+
runTerramendCliInner(context, params.cliArgs);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn(`» terramend cleanup bootstrap failed: ${getErrorMessage(error)}`);
|
|
272
|
+
// best-effort cleanup
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
runTerramendCliInner(context, params.cliArgs);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
propagateChildExit(error);
|
|
281
|
+
}
|
|
282
|
+
}
|