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,127 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { CommitInfoTool } from "#app/mcp/commitInfo";
|
|
6
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
7
|
+
|
|
8
|
+
type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
|
|
9
|
+
|
|
10
|
+
async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
|
|
11
|
+
const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
|
|
12
|
+
return exec(params, {});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const fullCommit = {
|
|
16
|
+
sha: "abc1234def5678",
|
|
17
|
+
html_url: "https://gh/commit/abc1234",
|
|
18
|
+
commit: {
|
|
19
|
+
message: "fix: tighten bucket policy",
|
|
20
|
+
author: { date: "2026-01-01T00:00:00Z" },
|
|
21
|
+
committer: { date: "2026-01-02T00:00:00Z" },
|
|
22
|
+
},
|
|
23
|
+
author: { login: "alice" },
|
|
24
|
+
committer: null,
|
|
25
|
+
parents: [{ sha: "p1" }, { sha: "p2" }],
|
|
26
|
+
stats: { additions: 3, deletions: 1, total: 4 },
|
|
27
|
+
files: [
|
|
28
|
+
{
|
|
29
|
+
filename: "main.tf",
|
|
30
|
+
patch: "@@ -1,2 +1,3 @@\n resource\n+ versioning = true\n }",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function makeCtx(commitData: Record<string, unknown>) {
|
|
36
|
+
const getCommit = vi.fn(async (_p: unknown) => ({ data: commitData }));
|
|
37
|
+
const ctx = {
|
|
38
|
+
octokit: { rest: { repos: { getCommit } } },
|
|
39
|
+
repo: { owner: "octo", name: "repo" },
|
|
40
|
+
} as unknown as ToolContext;
|
|
41
|
+
return { ctx, getCommit };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("CommitInfoTool", () => {
|
|
45
|
+
let tempDir: string;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
tempDir = mkdtempSync(join(tmpdir(), "terramend-commitinfo-"));
|
|
49
|
+
vi.stubEnv("TERRAMEND_TEMP_DIR", tempDir);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.unstubAllEnvs();
|
|
54
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns commit metadata and writes the formatted diff to disk", async () => {
|
|
58
|
+
const { ctx, getCommit } = makeCtx(fullCommit);
|
|
59
|
+
const result = await runTool(CommitInfoTool(ctx), { sha: "abc1234def5678" });
|
|
60
|
+
|
|
61
|
+
expect(result.isError).toBeUndefined();
|
|
62
|
+
expect(getCommit).toHaveBeenCalledWith({ owner: "octo", repo: "repo", ref: "abc1234def5678" });
|
|
63
|
+
|
|
64
|
+
const text = result.content[0].text;
|
|
65
|
+
expect(text).toContain("fix: tighten bucket policy");
|
|
66
|
+
expect(text).toContain("alice");
|
|
67
|
+
expect(text).toContain("2026-01-01T00:00:00Z");
|
|
68
|
+
expect(text).toContain("fileCount: 1");
|
|
69
|
+
expect(text).toContain("commit-abc1234.diff");
|
|
70
|
+
|
|
71
|
+
const diff = readFileSync(join(tempDir, "commit-abc1234.diff"), "utf-8");
|
|
72
|
+
expect(diff).toContain("main.tf");
|
|
73
|
+
expect(diff).toContain("versioning = true");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("falls back to committer date and zeroed stats when fields are missing", async () => {
|
|
77
|
+
const { ctx } = makeCtx({
|
|
78
|
+
...fullCommit,
|
|
79
|
+
author: undefined,
|
|
80
|
+
committer: { login: "bot" },
|
|
81
|
+
commit: { message: "chore", committer: { date: "2026-02-02T00:00:00Z" } },
|
|
82
|
+
stats: undefined,
|
|
83
|
+
files: undefined,
|
|
84
|
+
parents: [],
|
|
85
|
+
});
|
|
86
|
+
const result = await runTool(CommitInfoTool(ctx), { sha: "abc1234def5678" });
|
|
87
|
+
|
|
88
|
+
expect(result.isError).toBeUndefined();
|
|
89
|
+
const text = result.content[0].text;
|
|
90
|
+
expect(text).toContain("author: null");
|
|
91
|
+
expect(text).toContain("bot");
|
|
92
|
+
expect(text).toContain("2026-02-02T00:00:00Z");
|
|
93
|
+
expect(text).toContain("fileCount: 0");
|
|
94
|
+
expect(text).toContain("additions: 0");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("renders an empty date when neither author nor committer dates exist", async () => {
|
|
98
|
+
const { ctx } = makeCtx({
|
|
99
|
+
...fullCommit,
|
|
100
|
+
commit: { message: "chore" },
|
|
101
|
+
files: [{ filename: "image.png" }],
|
|
102
|
+
});
|
|
103
|
+
const result = await runTool(CommitInfoTool(ctx), { sha: "abc1234def5678" });
|
|
104
|
+
|
|
105
|
+
expect(result.isError).toBeUndefined();
|
|
106
|
+
const diff = readFileSync(join(tempDir, "commit-abc1234.diff"), "utf-8");
|
|
107
|
+
expect(diff).toContain("(binary file or no changes)");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("errors when TERRAMEND_TEMP_DIR is not set", async () => {
|
|
111
|
+
vi.stubEnv("TERRAMEND_TEMP_DIR", "");
|
|
112
|
+
const { ctx } = makeCtx(fullCommit);
|
|
113
|
+
const result = await runTool(CommitInfoTool(ctx), { sha: "abc1234def5678" });
|
|
114
|
+
|
|
115
|
+
expect(result.isError).toBe(true);
|
|
116
|
+
expect(result.content[0].text).toContain("TERRAMEND_TEMP_DIR not set");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("surfaces API failures as tool errors", async () => {
|
|
120
|
+
const { ctx, getCommit } = makeCtx(fullCommit);
|
|
121
|
+
getCommit.mockRejectedValueOnce(new Error("No commit found"));
|
|
122
|
+
const result = await runTool(CommitInfoTool(ctx), { sha: "deadbeef" });
|
|
123
|
+
|
|
124
|
+
expect(result.isError).toBe(true);
|
|
125
|
+
expect(result.content[0].text).toContain("No commit found");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { type } from "arktype";
|
|
4
|
+
import { formatFilesWithLineNumbers } from "#app/mcp/checkout";
|
|
5
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
6
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
7
|
+
import { log } from "#app/utils/cli";
|
|
8
|
+
|
|
9
|
+
export const CommitInfo = type({
|
|
10
|
+
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function CommitInfoTool(ctx: ToolContext) {
|
|
14
|
+
return tool({
|
|
15
|
+
name: "get_commit_info",
|
|
16
|
+
description:
|
|
17
|
+
"Retrieve commit metadata and diff via GitHub API. Use this instead of git show for reviewing commits - " +
|
|
18
|
+
"it works with shallow clones and shows the actual changes in the commit. Returns diffPath pointing to formatted diff file. " +
|
|
19
|
+
'Example: `get_commit_info({ sha: "2a6ab5d" })`.',
|
|
20
|
+
parameters: CommitInfo,
|
|
21
|
+
execute: execute(async ({ sha }) => {
|
|
22
|
+
const response = await ctx.octokit.rest.repos.getCommit({
|
|
23
|
+
owner: ctx.repo.owner,
|
|
24
|
+
repo: ctx.repo.name,
|
|
25
|
+
ref: sha,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const data = response.data;
|
|
29
|
+
const files = data.files ?? [];
|
|
30
|
+
|
|
31
|
+
// format diff with line numbers and write to file
|
|
32
|
+
const formatResult = formatFilesWithLineNumbers(files);
|
|
33
|
+
const tempDir = process.env.TERRAMEND_TEMP_DIR;
|
|
34
|
+
if (!tempDir) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"TERRAMEND_TEMP_DIR not set - get_commit_info must run in terramend action context",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const diffFile = join(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
40
|
+
writeFileSync(diffFile, formatResult.content);
|
|
41
|
+
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
sha: data.sha,
|
|
45
|
+
message: data.commit.message,
|
|
46
|
+
author: data.author?.login ?? null,
|
|
47
|
+
committer: data.committer?.login ?? null,
|
|
48
|
+
date: data.commit.author?.date ?? data.commit.committer?.date ?? "",
|
|
49
|
+
url: data.html_url,
|
|
50
|
+
parents: data.parents.map((p) => p.sha),
|
|
51
|
+
stats: {
|
|
52
|
+
additions: data.stats?.additions ?? 0,
|
|
53
|
+
deletions: data.stats?.deletions ?? 0,
|
|
54
|
+
total: data.stats?.total ?? 0,
|
|
55
|
+
},
|
|
56
|
+
fileCount: files.length,
|
|
57
|
+
diffFile,
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildCrosswalkReport,
|
|
4
|
+
ComplianceCrosswalkTool,
|
|
5
|
+
mapConcernToControls,
|
|
6
|
+
} from "#app/mcp/crosswalk";
|
|
7
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
8
|
+
|
|
9
|
+
describe("mapConcernToControls", () => {
|
|
10
|
+
it("maps an encryption concern to encryption controls", () => {
|
|
11
|
+
const { themes, controls } = mapConcernToControls({
|
|
12
|
+
rule_id: "trivy:AVD-AWS-0088",
|
|
13
|
+
evidence: "S3 bucket does not have server-side encryption enabled",
|
|
14
|
+
});
|
|
15
|
+
expect(themes).toContain("encryption-at-rest");
|
|
16
|
+
expect(controls.some((c) => c.framework === "CIS Controls v8")).toBe(true);
|
|
17
|
+
expect(controls.some((c) => c.framework === "NHS DSPT")).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("maps a public-access concern to firewall/network controls", () => {
|
|
21
|
+
const { themes, controls } = mapConcernToControls({
|
|
22
|
+
rule_id: "checkov:CKV_AWS_260",
|
|
23
|
+
evidence: "Security group allows ingress from 0.0.0.0/0 to port 22",
|
|
24
|
+
});
|
|
25
|
+
expect(themes).toContain("public-exposure");
|
|
26
|
+
expect(controls.some((c) => c.framework === "Cyber Essentials")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("dedupes identical control refs across matched themes", () => {
|
|
30
|
+
// matches both encryption-in-transit (tls) and public-exposure (public)
|
|
31
|
+
const { controls } = mapConcernToControls({
|
|
32
|
+
rule_id: "x",
|
|
33
|
+
evidence: "public endpoint without TLS",
|
|
34
|
+
});
|
|
35
|
+
const keys = controls.map((c) => `${c.framework}|${c.control}`);
|
|
36
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns empty for an unmappable concern (honest, not forced)", () => {
|
|
40
|
+
const { themes, controls } = mapConcernToControls({
|
|
41
|
+
rule_id: "terraform-fmt:format",
|
|
42
|
+
evidence: "file is not canonically formatted",
|
|
43
|
+
});
|
|
44
|
+
expect(themes).toEqual([]);
|
|
45
|
+
expect(controls).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("buildCrosswalkReport", () => {
|
|
50
|
+
it("builds per-concern entries, a by-framework index, and an unmapped list", () => {
|
|
51
|
+
const report = buildCrosswalkReport([
|
|
52
|
+
{ id: "aaa", rule_id: "trivy:AVD-AWS-0088", evidence: "bucket not encrypted at rest" },
|
|
53
|
+
{ id: "bbb", rule_id: "terraform-fmt:format", evidence: "not formatted" },
|
|
54
|
+
]);
|
|
55
|
+
expect(report.entries.map((e) => e.concern_id)).toEqual(["aaa"]);
|
|
56
|
+
expect(report.unmapped_concern_ids).toEqual(["bbb"]);
|
|
57
|
+
expect(Object.keys(report.by_framework).length).toBeGreaterThan(0);
|
|
58
|
+
// version + review date are carried for reproducibility.
|
|
59
|
+
expect(report.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
60
|
+
expect(report.reviewed).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("by_framework controls are deduped and sorted", () => {
|
|
64
|
+
const report = buildCrosswalkReport([
|
|
65
|
+
{ id: "a", rule_id: "r1", evidence: "encrypt at rest" },
|
|
66
|
+
{ id: "b", rule_id: "r2", evidence: "encryption kms missing" },
|
|
67
|
+
]);
|
|
68
|
+
for (const controls of Object.values(report.by_framework)) {
|
|
69
|
+
const ids = controls.map((c) => c.control);
|
|
70
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
71
|
+
expect([...ids]).toEqual([...ids].sort());
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("is deterministic for the same input", () => {
|
|
76
|
+
const input = [{ id: "a", rule_id: "r", evidence: "public ingress without encryption" }];
|
|
77
|
+
expect(buildCrosswalkReport(input)).toEqual(buildCrosswalkReport(input));
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("ComplianceCrosswalkTool", () => {
|
|
82
|
+
it("wraps the report in the ok envelope with the indicative-crosswalk note", async () => {
|
|
83
|
+
const tool = ComplianceCrosswalkTool({} as unknown as ToolContext);
|
|
84
|
+
const exec = tool.execute as (
|
|
85
|
+
p: unknown,
|
|
86
|
+
c: unknown,
|
|
87
|
+
) => Promise<{ content: [{ type: "text"; text: string }]; isError?: boolean }>;
|
|
88
|
+
const result = await exec(
|
|
89
|
+
{
|
|
90
|
+
concerns: [
|
|
91
|
+
{ id: "a", rule_id: "trivy:AVD-AWS-0088", evidence: "bucket lacks encryption" },
|
|
92
|
+
{ id: "z", rule_id: "none", evidence: "completely unrelated text" },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const text = result.content[0].text;
|
|
99
|
+
expect(result.isError).toBeUndefined();
|
|
100
|
+
expect(text).toContain("ok: true");
|
|
101
|
+
expect(text).toContain("concern_id: a");
|
|
102
|
+
expect(text).toContain("unmapped_concern_ids[1]: z");
|
|
103
|
+
expect(text).toContain("Indicative crosswalk");
|
|
104
|
+
expect(text).toContain("not an audit verdict");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
3
|
+
import { execute, tool, toolOk } from "#app/mcp/shared";
|
|
4
|
+
import { log } from "#app/utils/cli";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compliance crosswalk (§differentiator 23 — "explain like I'm the auditor", the
|
|
8
|
+
* seed of the Part-6 moat). Maps a best-practice concern → the control families
|
|
9
|
+
* it touches across UK public-sector + general frameworks, so a remediation can
|
|
10
|
+
* be narrated to an assessor in their own language ("this closes NCSC Cloud
|
|
11
|
+
* Principle 2 / Cyber Essentials Secure Configuration") rather than as a raw
|
|
12
|
+
* scanner rule id.
|
|
13
|
+
*
|
|
14
|
+
* SCOPE / HONESTY: this is a deterministic STARTER rule-pack keyed on the
|
|
15
|
+
* defect's THEME (encryption, public exposure, least-privilege, logging, …),
|
|
16
|
+
* not a certified control-by-control mapping. The durable product is a
|
|
17
|
+
* versioned, framework-revision-pinned crosswalk (Part 6) — so every mapping
|
|
18
|
+
* carries the pack version + date and is labelled indicative, never an audit
|
|
19
|
+
* verdict. No open crosswalk to UK frameworks exists; this is the wedge.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const CROSSWALK_VERSION = "0.1.0";
|
|
23
|
+
/** the date this rule-pack's framework references were last reviewed (absolute). */
|
|
24
|
+
export const CROSSWALK_REVIEWED = "2026-06-07";
|
|
25
|
+
|
|
26
|
+
export interface ControlRef {
|
|
27
|
+
/** the framework, e.g. "NCSC Cloud Security Principles". */
|
|
28
|
+
framework: string;
|
|
29
|
+
/** the control id within that framework, e.g. "Principle 2". */
|
|
30
|
+
control: string;
|
|
31
|
+
/** the control's short title. */
|
|
32
|
+
title: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** a theme of defect, the signals that identify it, and the controls it maps to. */
|
|
36
|
+
interface CrosswalkRule {
|
|
37
|
+
theme: string;
|
|
38
|
+
/** lowercased substrings that, if any appears in the rule_id/evidence, match. */
|
|
39
|
+
signals: string[];
|
|
40
|
+
controls: ControlRef[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The starter rule-pack. Themes are matched against the concern's rule_id +
|
|
44
|
+
// evidence (lowercased). Ordered most-specific first; a concern can match
|
|
45
|
+
// several themes (their controls are unioned). Kept deliberately small and
|
|
46
|
+
// auditable — each control ref is a real, citable control family.
|
|
47
|
+
const CROSSWALK: CrosswalkRule[] = [
|
|
48
|
+
{
|
|
49
|
+
theme: "encryption-at-rest",
|
|
50
|
+
// NB avoid the bare token "sse" — it's a substring of "essential"/"asset"
|
|
51
|
+
// and would false-match; use the explicit phrases instead.
|
|
52
|
+
signals: [
|
|
53
|
+
"encrypt",
|
|
54
|
+
"kms",
|
|
55
|
+
"server-side encryption",
|
|
56
|
+
"server_side_encryption",
|
|
57
|
+
"sse_algorithm",
|
|
58
|
+
"at rest",
|
|
59
|
+
"at-rest",
|
|
60
|
+
"unencrypted",
|
|
61
|
+
],
|
|
62
|
+
controls: [
|
|
63
|
+
{
|
|
64
|
+
framework: "NCSC Cloud Security Principles",
|
|
65
|
+
control: "Principle 1",
|
|
66
|
+
title: "Data in transit protection",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
framework: "Cyber Essentials",
|
|
70
|
+
control: "Secure Configuration",
|
|
71
|
+
title: "Secure configuration",
|
|
72
|
+
},
|
|
73
|
+
{ framework: "CIS Controls v8", control: "3.11", title: "Encrypt sensitive data at rest" },
|
|
74
|
+
{
|
|
75
|
+
framework: "NHS DSPT",
|
|
76
|
+
control: "Standard 1",
|
|
77
|
+
title: "Personal confidential data protected",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
theme: "encryption-in-transit",
|
|
83
|
+
signals: ["tls", "ssl", "https", "in transit", "in-transit", "insecure protocol", "http "],
|
|
84
|
+
controls: [
|
|
85
|
+
{
|
|
86
|
+
framework: "NCSC Cloud Security Principles",
|
|
87
|
+
control: "Principle 1",
|
|
88
|
+
title: "Data in transit protection",
|
|
89
|
+
},
|
|
90
|
+
{ framework: "CIS Controls v8", control: "3.10", title: "Encrypt sensitive data in transit" },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
theme: "public-exposure",
|
|
95
|
+
signals: ["public", "0.0.0.0/0", "publicly", "anonymous", "open to the internet", "ingress"],
|
|
96
|
+
controls: [
|
|
97
|
+
{
|
|
98
|
+
framework: "NCSC Cloud Security Principles",
|
|
99
|
+
control: "Principle 9",
|
|
100
|
+
title: "Secure user management / network separation",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
framework: "Cyber Essentials",
|
|
104
|
+
control: "Firewalls",
|
|
105
|
+
title: "Boundary firewalls and internet gateways",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
framework: "CIS Controls v8",
|
|
109
|
+
control: "4.4",
|
|
110
|
+
title: "Implement and manage a firewall on servers",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
framework: "Secure by Design",
|
|
114
|
+
control: "SbD-7",
|
|
115
|
+
title: "Protect data in transit and at rest",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
theme: "least-privilege",
|
|
121
|
+
signals: [
|
|
122
|
+
"iam",
|
|
123
|
+
"wildcard",
|
|
124
|
+
"least privilege",
|
|
125
|
+
"least-privilege",
|
|
126
|
+
"policy",
|
|
127
|
+
"admin",
|
|
128
|
+
"privilege",
|
|
129
|
+
"*:*",
|
|
130
|
+
"role",
|
|
131
|
+
],
|
|
132
|
+
controls: [
|
|
133
|
+
{
|
|
134
|
+
framework: "NCSC Cloud Security Principles",
|
|
135
|
+
control: "Principle 9",
|
|
136
|
+
title: "Secure user management",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
framework: "Cyber Essentials",
|
|
140
|
+
control: "User Access Control",
|
|
141
|
+
title: "User access control",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
framework: "CIS Controls v8",
|
|
145
|
+
control: "6.8",
|
|
146
|
+
title: "Define and maintain role-based access control",
|
|
147
|
+
},
|
|
148
|
+
{ framework: "NHS DSPT", control: "Standard 4", title: "Managing access" },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
theme: "logging-audit",
|
|
153
|
+
// NB avoid the bare token "log" — it's a substring of "catalog"/"blog" and
|
|
154
|
+
// would false-match; use the explicit phrases/longer tokens instead.
|
|
155
|
+
signals: [
|
|
156
|
+
"logging",
|
|
157
|
+
"log group",
|
|
158
|
+
"audit",
|
|
159
|
+
"cloudtrail",
|
|
160
|
+
"flow log",
|
|
161
|
+
"access log",
|
|
162
|
+
"monitoring",
|
|
163
|
+
"cloudwatch",
|
|
164
|
+
],
|
|
165
|
+
controls: [
|
|
166
|
+
{
|
|
167
|
+
framework: "NCSC Cloud Security Principles",
|
|
168
|
+
control: "Principle 13",
|
|
169
|
+
title: "Audit information and alerting",
|
|
170
|
+
},
|
|
171
|
+
{ framework: "CIS Controls v8", control: "8.2", title: "Collect audit logs" },
|
|
172
|
+
{ framework: "NHS DSPT", control: "Standard 7", title: "Continuity planning / monitoring" },
|
|
173
|
+
{ framework: "SOC 2", control: "CC7.2", title: "Security monitoring" },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
theme: "backup-resilience",
|
|
178
|
+
signals: ["versioning", "backup", "snapshot", "retention", "deletion protection", "multi-az"],
|
|
179
|
+
controls: [
|
|
180
|
+
{
|
|
181
|
+
framework: "NCSC Cloud Security Principles",
|
|
182
|
+
control: "Principle 2",
|
|
183
|
+
title: "Asset protection and resilience",
|
|
184
|
+
},
|
|
185
|
+
{ framework: "NHS DSPT", control: "Standard 7", title: "Continuity planning" },
|
|
186
|
+
{ framework: "CIS Controls v8", control: "11.2", title: "Perform automated backups" },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
theme: "secrets-management",
|
|
191
|
+
signals: ["secret", "credential", "password", "hardcoded", "plaintext", "access key", "token"],
|
|
192
|
+
controls: [
|
|
193
|
+
{
|
|
194
|
+
framework: "NCSC Cloud Security Principles",
|
|
195
|
+
control: "Principle 10",
|
|
196
|
+
title: "Identity and authentication",
|
|
197
|
+
},
|
|
198
|
+
{ framework: "CIS Controls v8", control: "16.4", title: "Securely store credentials" },
|
|
199
|
+
{
|
|
200
|
+
framework: "Cyber Essentials",
|
|
201
|
+
control: "Secure Configuration",
|
|
202
|
+
title: "Secure configuration",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Map a single concern to the indicative control references it touches. Matches
|
|
210
|
+
* the concern's `rule_id` + `evidence` (and an optional `category`) against the
|
|
211
|
+
* crosswalk themes; unions the controls of every theme that fires. Pure.
|
|
212
|
+
* Returns an empty array when nothing matches (honest — better than a forced
|
|
213
|
+
* mapping). De-duplicates identical control refs.
|
|
214
|
+
*/
|
|
215
|
+
export function mapConcernToControls(concern: {
|
|
216
|
+
rule_id: string;
|
|
217
|
+
evidence: string;
|
|
218
|
+
category?: string;
|
|
219
|
+
}): { themes: string[]; controls: ControlRef[] } {
|
|
220
|
+
const haystack = `${concern.rule_id} ${concern.evidence}`.toLowerCase();
|
|
221
|
+
const themes: string[] = [];
|
|
222
|
+
const controls: ControlRef[] = [];
|
|
223
|
+
const seen = new Set<string>();
|
|
224
|
+
for (const rule of CROSSWALK) {
|
|
225
|
+
if (!rule.signals.some((s) => haystack.includes(s))) continue;
|
|
226
|
+
themes.push(rule.theme);
|
|
227
|
+
for (const c of rule.controls) {
|
|
228
|
+
const key = `${c.framework}|${c.control}`;
|
|
229
|
+
if (seen.has(key)) continue;
|
|
230
|
+
seen.add(key);
|
|
231
|
+
controls.push(c);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { themes, controls };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ConcernForCrosswalk {
|
|
238
|
+
id: string;
|
|
239
|
+
rule_id: string;
|
|
240
|
+
evidence: string;
|
|
241
|
+
category?: string;
|
|
242
|
+
severity?: string;
|
|
243
|
+
location?: { file: string; line: number | null };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface CrosswalkEntry {
|
|
247
|
+
concern_id: string;
|
|
248
|
+
rule_id: string;
|
|
249
|
+
themes: string[];
|
|
250
|
+
controls: ControlRef[];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface CrosswalkReport {
|
|
254
|
+
version: string;
|
|
255
|
+
reviewed: string;
|
|
256
|
+
/** per-concern control mappings (only concerns that mapped to ≥1 control). */
|
|
257
|
+
entries: CrosswalkEntry[];
|
|
258
|
+
/** framework → the distinct controls this scan touched, for an auditor index. */
|
|
259
|
+
by_framework: Record<string, { control: string; title: string }[]>;
|
|
260
|
+
/** concerns that did not map to any control (honest coverage signal). */
|
|
261
|
+
unmapped_concern_ids: string[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Build the auditor crosswalk for a set of concerns: per-concern control refs
|
|
266
|
+
* plus a `by_framework` index (which controls this scan touched, deduped) and an
|
|
267
|
+
* honest `unmapped` list. Pure + deterministic. Carries the pack version + date
|
|
268
|
+
* so the report is reproducible and clearly indicative.
|
|
269
|
+
*/
|
|
270
|
+
export function buildCrosswalkReport(concerns: ConcernForCrosswalk[]): CrosswalkReport {
|
|
271
|
+
const entries: CrosswalkEntry[] = [];
|
|
272
|
+
const unmapped: string[] = [];
|
|
273
|
+
const byFramework = new Map<string, Map<string, string>>();
|
|
274
|
+
for (const c of concerns) {
|
|
275
|
+
const { themes, controls } = mapConcernToControls(c);
|
|
276
|
+
if (controls.length === 0) {
|
|
277
|
+
unmapped.push(c.id);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
entries.push({ concern_id: c.id, rule_id: c.rule_id, themes, controls });
|
|
281
|
+
for (const ctl of controls) {
|
|
282
|
+
const map = byFramework.get(ctl.framework) ?? new Map<string, string>();
|
|
283
|
+
if (!map.has(ctl.control)) map.set(ctl.control, ctl.title);
|
|
284
|
+
byFramework.set(ctl.framework, map);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const by_framework: Record<string, { control: string; title: string }[]> = {};
|
|
288
|
+
for (const [framework, controls] of [...byFramework.entries()].sort((a, b) =>
|
|
289
|
+
a[0].localeCompare(b[0]),
|
|
290
|
+
)) {
|
|
291
|
+
by_framework[framework] = [...controls.entries()]
|
|
292
|
+
.map(([control, title]) => ({ control, title }))
|
|
293
|
+
.sort((a, b) => a.control.localeCompare(b.control));
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
version: CROSSWALK_VERSION,
|
|
297
|
+
reviewed: CROSSWALK_REVIEWED,
|
|
298
|
+
entries,
|
|
299
|
+
by_framework,
|
|
300
|
+
unmapped_concern_ids: unmapped,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const ComplianceCrosswalkParams = type({
|
|
305
|
+
concerns: type({
|
|
306
|
+
id: type.string,
|
|
307
|
+
rule_id: type.string,
|
|
308
|
+
evidence: type.string,
|
|
309
|
+
"category?": type.string,
|
|
310
|
+
"severity?": type.string,
|
|
311
|
+
})
|
|
312
|
+
.array()
|
|
313
|
+
.describe("the concerns to map (pass the `concerns` array from terraform_scan/read_findings)."),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
export function ComplianceCrosswalkTool(ctx: ToolContext) {
|
|
317
|
+
void ctx;
|
|
318
|
+
return tool({
|
|
319
|
+
name: "terraform_compliance_crosswalk",
|
|
320
|
+
description:
|
|
321
|
+
"Map a scan's concerns to the UK public-sector + general compliance controls they touch (§23) — NCSC " +
|
|
322
|
+
"Cloud Security Principles, Cyber Essentials, NHS DSPT, Secure by Design, CIS Controls, SOC 2 — so a " +
|
|
323
|
+
"remediation PR can be narrated to an ASSESSOR in their own framework. Pass the `concerns` from " +
|
|
324
|
+
"terraform_scan/read_findings; returns a per-concern control mapping plus a `by_framework` index and an " +
|
|
325
|
+
"honest `unmapped_concern_ids` list. The mapping is an INDICATIVE starter rule-pack (version + review " +
|
|
326
|
+
"date included) keyed on the defect theme — cite it as 'indicative alignment', never an audit verdict.",
|
|
327
|
+
parameters: ComplianceCrosswalkParams,
|
|
328
|
+
execute: execute(async ({ concerns }) => {
|
|
329
|
+
const report = buildCrosswalkReport(concerns as ConcernForCrosswalk[]);
|
|
330
|
+
log.info(
|
|
331
|
+
`» terraform_compliance_crosswalk: ${report.entries.length} mapped / ${report.unmapped_concern_ids.length} unmapped across ${Object.keys(report.by_framework).length} framework(s)`,
|
|
332
|
+
);
|
|
333
|
+
return toolOk({
|
|
334
|
+
...report,
|
|
335
|
+
note: `Indicative crosswalk v${report.version} (reviewed ${report.reviewed}) — alignment guidance, not an audit verdict.`,
|
|
336
|
+
});
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
}
|