terramend 0.2.0 → 0.2.1
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/dist/agents/claudePretoolGate.d.ts +2 -2
- package/dist/cli.mjs +16554 -8100
- package/dist/index.js +13484 -5037
- package/dist/internal.js +75 -11
- package/dist/mcp/assess.d.ts +86 -0
- package/dist/mcp/changeSummary.d.ts +50 -0
- package/dist/mcp/crosswalk.d.ts +5 -0
- package/dist/mcp/localContext.d.ts +1 -1
- package/dist/mcp/terraform/evidence.d.ts +99 -0
- package/dist/mcp/terraform/scanners.d.ts +38 -3
- package/dist/mcp/terraform/types.d.ts +16 -0
- package/dist/mcp/terraform/verification.d.ts +74 -0
- package/dist/mcp/terraform.d.ts +4 -0
- package/dist/modes.d.ts +1 -1
- package/dist/toolState.d.ts +1 -0
- package/dist/utils/moduleFetch.d.ts +42 -0
- package/dist/utils/payload.d.ts +4 -0
- package/dist/utils/remediationCommand.d.ts +3 -0
- package/dist/utils/terraformMcp.d.ts +2 -2
- package/dist/utils/terramendConfig.d.ts +51 -0
- package/dist/utils/toolLicensing.d.ts +56 -0
- package/dist/utils/toolSelection.d.ts +72 -0
- package/package.json +9 -8
- package/src/agents/claudePretoolGate.ts +3 -3
- package/src/mcp/assess.test.ts +135 -0
- package/src/mcp/assess.ts +341 -0
- package/src/mcp/changeSummary.test.ts +94 -0
- package/src/mcp/changeSummary.ts +145 -0
- package/src/mcp/crosswalk.ts +15 -1
- package/src/mcp/guardrails.ts +11 -6
- package/src/mcp/localContext.ts +7 -0
- package/src/mcp/localServer.test.ts +2 -0
- package/src/mcp/localServer.ts +14 -0
- package/src/mcp/server.ts +6 -0
- package/src/mcp/terraform/evidence.test.ts +72 -0
- package/src/mcp/terraform/evidence.ts +187 -0
- package/src/mcp/terraform/scanners.ts +86 -9
- package/src/mcp/terraform/tools.test.ts +96 -1
- package/src/mcp/terraform/tools.ts +115 -32
- package/src/mcp/terraform/types.ts +24 -0
- package/src/mcp/terraform/verification.test.ts +85 -0
- package/src/mcp/terraform/verification.ts +133 -0
- package/src/mcp/terraform.test.ts +108 -0
- package/src/mcp/terraform.ts +4 -0
- package/src/modes.test.ts +9 -1
- package/src/modes.ts +81 -11
- package/src/toolState.ts +6 -0
- package/src/utils/moduleFetch.test.ts +68 -0
- package/src/utils/moduleFetch.ts +86 -0
- package/src/utils/payload.test.ts +66 -1
- package/src/utils/payload.ts +39 -11
- package/src/utils/remediationCommand.test.ts +32 -0
- package/src/utils/remediationCommand.ts +11 -0
- package/src/utils/terraformMcp.ts +6 -5
- package/src/utils/terramendConfig.test.ts +98 -0
- package/src/utils/terramendConfig.ts +143 -0
- package/src/utils/toolLicensing.test.ts +54 -0
- package/src/utils/toolLicensing.ts +103 -0
- package/src/utils/toolSelection.test.ts +140 -0
- package/src/utils/toolSelection.ts +231 -0
package/dist/internal.js
CHANGED
|
@@ -751,7 +751,7 @@ Inline comments use the same severity framing as body \`### \` sections, scaled
|
|
|
751
751
|
- **Legacy headings REMOVED.** Do not use \`### Key changes\`, \`### Issues found\`, \`<b>TL;DR</b>\`, or \`<sub><b>Summary</b>\`. The new structure subsumes them.`;
|
|
752
752
|
var REMEDIATION_PR_FORMAT = `### Remediation PR format
|
|
753
753
|
|
|
754
|
-
**Minimum (ALWAYS include, even under tight budget):** a one-paragraph plain-English summary of *what was wrong and what you changed*, then a \`## What changed\` list with one *Was / Changed / Safe because* note per concern, then the \`## Validation
|
|
754
|
+
**Minimum (ALWAYS include, even under tight budget):** a one-paragraph plain-English summary of *what was wrong and what you changed*, then a \`## What changed\` list with one *Was / Changed / Safe because* note per concern, then the \`## Validation\` list. If you produce nothing else, produce these three \u2014 a PR a human can't understand from its body alone has failed its job. Everything below enriches this minimum; it does not replace it.
|
|
755
755
|
|
|
756
756
|
Build the PR body in this EXACT order. Every line is backed by a tool result \u2014 never write a status you didn't get from a tool. Omit a whole section only when its tool didn't run (e.g. no plan without cloud creds); never fabricate it. Keep a blank line between every block-level element (GitHub needs it to render).
|
|
757
757
|
|
|
@@ -789,19 +789,21 @@ One \`### \` subsection per resolved concern (or one per rule for a by-rule grou
|
|
|
789
789
|
|
|
790
790
|
Lead each heading with a severity emoji (\u{1F6A8} critical \xB7 \u26A0\uFE0F high \xB7 \u{1F512} security \xB7 \u2139\uFE0F low/info). Backtick-wrap every identifier. No raw diff dumps \u2014 the Files tab shows the diff.
|
|
791
791
|
|
|
792
|
-
#### 4. \`## Validation
|
|
792
|
+
#### 4. \`## Validation\`
|
|
793
793
|
|
|
794
|
-
Built ONLY from \`terraform_verify_remediation\`'s result \u2014 this is the proof, not a self-report. One line per id in \`resolved\`, then any still-open id honestly:
|
|
794
|
+
Built ONLY from \`terraform_verify_remediation\`'s result \u2014 this is the proof, not a self-report. One \u2705 line per id in \`resolved\`, then any still-open id honestly:
|
|
795
795
|
|
|
796
796
|
\`\`\`
|
|
797
|
-
## Validation
|
|
797
|
+
## Validation
|
|
798
798
|
|
|
799
|
-
- \
|
|
800
|
-
- \
|
|
799
|
+
- \u2705 \\\`trivy:AVD-AWS-0088\\\` resolved
|
|
800
|
+
- \u2705 \\\`checkov:CKV_AWS_19\\\` resolved
|
|
801
801
|
- \u26A0\uFE0F still open: \\\`tflint:...\\\` \u2014 {why it couldn't be cleared}
|
|
802
802
|
\`\`\`
|
|
803
803
|
|
|
804
|
-
|
|
804
|
+
**Resolved XOR still-open \u2014 never both.** Each concern appears on exactly one line: an \`id\` in the tool's \`resolved\` set gets a \`\u2705 \u2026 resolved\` line (the green check means cleared); an \`id\` in \`remaining\` gets a \`\u26A0\uFE0F still open: \u2026\` line. NEVER put a \u2705 on an unresolved concern (no \`\u2705 \u2026 re-flagged\`, no \`\u2705 \u2026 still open\`) \u2014 that is a false attestation and the single worst thing this body can do. A concern earns its \u2705 only if the tool returned its id in \`resolved\`; if it's in \`remaining\`, it is still-open, full stop.
|
|
805
|
+
|
|
806
|
+
If \`has_regressions\` is true, add a \`> [!CAUTION]\` **Regression** callout listing each id in the tool's \`regressions\` set BEFORE this list, and ensure the \`needs-human\` label is set. \`regressions\` are concerns the fix genuinely INTRODUCED (a new \`rule\`+\`file\` not present before) \u2014 they are computed line-independently, so a pre-existing concern that merely moved lines is NOT a regression; do not relabel a \`remaining\` concern as a regression.
|
|
805
807
|
|
|
806
808
|
#### 5. \`<details><summary>Plan</summary>\` (when \`terraform_plan\` ran)
|
|
807
809
|
|
|
@@ -1196,6 +1198,42 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
1196
1198
|
${REVIEW_FINDING_PRECEDENTS}
|
|
1197
1199
|
|
|
1198
1200
|
${PR_SUMMARY_FORMAT}`
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
name: "SummarizePr",
|
|
1204
|
+
description: "Summarize a pull request's changes in a single structured comment \u2014 what it does, the key changes, and any areas worth a closer look. Does NOT review, approve, or change code (use Review for a verdict).",
|
|
1205
|
+
prompt: `### Checklist
|
|
1206
|
+
|
|
1207
|
+
This mode posts ONE plain-English summary of what a PR does \u2014 an orientation aid, not a verdict. Do NOT approve, request changes, leave inline review comments, or modify any code. If a real review is wanted, that's the Review mode.
|
|
1208
|
+
|
|
1209
|
+
1. **task list**: create your task list for this run as your first action.
|
|
1210
|
+
|
|
1211
|
+
2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. Read the diff TOC so you understand the scope.
|
|
1212
|
+
|
|
1213
|
+
3. **Terraform anchor (when relevant)**: call \`${t("terraform_change_summary")}\` \u2014 it returns the DETERMINISTIC Terraform block changes (resource/module/data/variable/output addresses ADDED and REMOVED, plus the Terraform files touched) vs the base. It degrades green (\`ok: false\`) when git can't resolve the base \u2014 run \`${t("git_fetch")}\` on the base ref first and retry \u2014 or when the PR has no Terraform changes (then it's a general summary). Use its counts as the factual backbone of the Terraform part of your summary instead of counting by eye.
|
|
1214
|
+
|
|
1215
|
+
4. **read for intent**: read the \`diffPath\` (and related files as needed) to understand WHAT the PR does and WHY \u2014 not just the mechanics. Pull as much context as you need; you are the synthesizer.
|
|
1216
|
+
|
|
1217
|
+
5. **post the summary**: call \`${t("create_issue_comment")}\` ONCE on the PR with a structured summary in this shape (omit a section when it has nothing):
|
|
1218
|
+
|
|
1219
|
+
\`\`\`
|
|
1220
|
+
## Summary
|
|
1221
|
+
{1\u20132 sentences: what this PR does and why, in plain English.}
|
|
1222
|
+
|
|
1223
|
+
### Key changes
|
|
1224
|
+
- **{short title}** \u2014 {one sentence}; backtick-wrap files/identifiers you name.
|
|
1225
|
+
- ...
|
|
1226
|
+
|
|
1227
|
+
### Terraform changes
|
|
1228
|
+
{only when terraform_change_summary returned data \u2014 e.g. "Adds \\\`module.vpc\\\` and \\\`aws_s3_bucket.logs\\\`; removes \\\`aws_launch_configuration.web\\\`; edits 3 files." Use its real addresses/counts.}
|
|
1229
|
+
|
|
1230
|
+
### Worth a closer look (optional)
|
|
1231
|
+
- {non-blocking observations a reviewer might want to focus on \u2014 risk areas, sequencing, things the diff implies but doesn't address. Phrase as orientation, not findings \u2014 this is not a review.}
|
|
1232
|
+
\`\`\`
|
|
1233
|
+
|
|
1234
|
+
Keep it scannable: lead with intent, alternate prose with structure, backtick-wrap identifiers, no raw diff dumps, no \`+N/-M\` stats. NEVER fabricate a change \u2014 every claim must be in the diff (the Terraform counts come from the tool).
|
|
1235
|
+
|
|
1236
|
+
6. **finalize**: call \`${t("report_progress")}\` with a one-line note that the summary was posted (or the exact error if the comment failed). Do NOT call \`${t("create_pull_request_review")}\` \u2014 this mode summarizes, it does not review.`
|
|
1199
1237
|
},
|
|
1200
1238
|
{
|
|
1201
1239
|
name: "Plan",
|
|
@@ -1263,10 +1301,30 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
1263
1301
|
- \`git add . && git commit -m "resolve merge conflicts"\`
|
|
1264
1302
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
1265
1303
|
- Call \`${t("report_progress")}\` with a summary of what was resolved (or the exact push error if push failed)`
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
name: "Assess",
|
|
1307
|
+
description: "Read-only Terraform best-practice ASSESSMENT: scan with the deterministic check tools, map findings to compliance controls, and report the posture (clean / advisory / action-required) \u2014 without modifying any Terraform or opening a PR.",
|
|
1308
|
+
prompt: `### Checklist
|
|
1309
|
+
|
|
1310
|
+
This mode is **read-only** \u2014 the Assess half of Terramend's one-engine-two-modes design. It reports posture; it never fixes. Do NOT edit Terraform, commit, push, or open a PR/issue in this mode. If the findings warrant a fix, say so and recommend re-running in \`remediate\` mode.
|
|
1311
|
+
|
|
1312
|
+
1. **task list**: create your task list for this run as your first action.
|
|
1313
|
+
|
|
1314
|
+
2. **assess**: call \`${t("terraform_assess")}\`. It runs the scanners and returns a deterministic \`scorecard\` (overall \`posture\`, \`by_severity\` counts, \`top_risks\`, and an indicative compliance-crosswalk summary) plus a ready-to-post \`markdown\` report. This is the core deliverable \u2014 built from tool results, not your own judgement.
|
|
1315
|
+
|
|
1316
|
+
3. **optional lenses** (fold each into the report only when it actually ran):
|
|
1317
|
+
- \`${t("infracost_diff")}\` \u2014 current monthly cost (auto-skips without \`INFRACOST_API_KEY\`/the CLI).
|
|
1318
|
+
- \`${t("terraform_version_currency")}\` \u2014 provider/module pins that are outdated or unpinned.
|
|
1319
|
+
- \`${t("terraform_emit_sarif")}\` \u2014 when the workflow has a SARIF upload step, emit \`terramend.sarif\` so every concern also lands in the repo's Security tab (read-only, complementary).
|
|
1320
|
+
|
|
1321
|
+
4. **report**: call \`${t("report_progress")}\` once with the assessment. Use the \`markdown\` from \`${t("terraform_assess")}\` as the base (it carries the posture banner, severity counts, top risks, and the indicative-crosswalk note verbatim \u2014 do not soften or inflate it), then append one-line **Cost** / **Version currency** notes if those lenses ran. If \`${t("set_output")}\` is available (standalone runs), also emit the structured result (\`posture\`, \`total\`, \`by_severity\`, the touched \`frameworks\`) so a CI step can gate on \`posture\`.
|
|
1322
|
+
|
|
1323
|
+
5. **guardrails**: never modify \`*.tf\`/\`*.tfvars\`, never push, never open a PR or issue. The assessment is the only deliverable.`
|
|
1266
1324
|
},
|
|
1267
1325
|
{
|
|
1268
1326
|
name: "Remediate",
|
|
1269
|
-
description: "Bring a repository's Terraform up to best practice: scan with the deterministic check tools, then open one scoped, reviewable PR per concern that fixes it and proves
|
|
1327
|
+
description: "Bring a repository's Terraform up to best practice: scan with the deterministic check tools, then open one scoped, reviewable PR per concern that fixes it and proves each fix by re-scanning (\u2705).",
|
|
1270
1328
|
prompt: `### Checklist
|
|
1271
1329
|
|
|
1272
1330
|
1. **task list**: create your task list for this run as your first action.
|
|
@@ -1287,6 +1345,8 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
1287
1345
|
|
|
1288
1346
|
**Comment command (\xA73.12)**: when this run was triggered by a \`@terramend fix \u2026\` comment, the triggering body is in your prompt \u2014 honour the requested scope INSTEAD of "highest-severity group": \`fix #<concern-id>\` \u2192 act only on the group containing that concern id; \`fix all <severity>-severity\` \u2192 set the scan \`severity_threshold\` to that level and act on those groups (up to \`max_prs\`); \`fix <file>.tf\` \u2192 act on that file's group; \`fix all\` \u2192 act on the highest-severity groups up to \`max_prs\`. If the comment isn't a recognised fix command, fall back to the default scope. A **strategy suffix** \u2014 \`fix #<concern-id> with strategy B\` (or a bare \`strategy B\` reply on a proposal thread) \u2014 additionally tells you **which** fix to apply: see \xA726 in step 4.
|
|
1289
1347
|
|
|
1348
|
+
**Bulk remediation (\xA737 \u2014 \`fix rule <rule-id>\` / \`fix all rule <rule-id>\`)**: a request to fix ONE scanner rule everywhere it fires (e.g. \`@terramend fix rule CKV_AWS_23\` \u2014 "add a description to every security group", or \`fix rule terraform_required_version\`). Re-scan with \`group_by: "rule"\` (\xA73.11) so that rule becomes ONE group spanning every file, then act on the single group whose \`rule_ids\` include \`<rule-id>\` \u2014 apply the SAME minimal fix at every site in its \`files\` and open ONE coherent PR (not one per file). This is the sweep path; still honour \`max_prs\` and never batch a \`needs-human\` group. Cite each fixed site in the PR body.
|
|
1349
|
+
|
|
1290
1350
|
4. **for the chosen group**:
|
|
1291
1351
|
- **base branch**: this run's base branch is resolved deterministically \u2014 \`${t("create_pull_request")}\` targets the \`base_branch\` input if set, else the repository's default branch (\`main\`, or \`master\`). You do not choose it; just **omit** the \`base\` argument when opening the PR (below) and it is filled in.
|
|
1292
1352
|
- **idempotency**: the remediation branch is \`remediate/<group-id>\`. Before doing anything, check whether that branch or an open PR for it already exists (\`${t("git")}\` / \`${t("get_pull_request")}\`). If one exists, update it rather than opening a duplicate.
|
|
@@ -1294,6 +1354,10 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
1294
1354
|
- **honest refusal (\xA729 \u2014 decide BEFORE fixing)**: if the group's concerns appear in the scan's \`refusal_candidates\` (the fix needs a human decision \u2014 narrowing an IAM wildcard, a KMS key policy, a real ingress CIDR), do **not** guess a fix that could break the stack. Instead open a structured issue (\`${t("create_issue")}\`) describing the concern, why it isn't auto-fixed, and what a human should do, and skip the PR for that group. A proven fix or an honest refusal \u2014 never a guessed, unverifiable PR.
|
|
1295
1355
|
- **propose, then let me steer (\xA726 \u2014 when there's no single right fix)**: distinct from \xA729 (which refuses a fix a human must *decide*), \xA726 is for a finding with **2\u20133 genuinely distinct, defensible fixes** that differ in trade-offs, not correctness (e.g. encrypt with an AWS-managed key **vs** a customer-managed KMS key; a narrow security-group rule **vs** a prefix list **vs** a VPC endpoint). When such a fork exists **and the triggering comment did not already select a strategy**, do **not** silently pick for the reviewer: via \`${t("create_issue_comment")}\` post one short comment listing the options as **A / B / C** \u2014 each a single line (what it does + its trade-off) \u2014 and ask the reviewer to reply \`@terramend fix #<concern-id> with strategy <A|B|C>\`. Then **skip the PR for this group** this run and note it in your final report (it resumes when the reviewer replies). When the comment **did** select one (\`fix #<id> with strategy B\`, or a bare \`strategy B\` reply on the proposal thread), apply **exactly** that strategy \u2014 don't second-guess it. Reserve this for real forks in the road; a fix with one obvious correct answer just gets made.
|
|
1296
1356
|
- **fix**: edit the group's file(s), using your native file tools. For a by-file group that's the single \`file\`; for a **by-rule group (\xA73.11)** it's every entry in \`files\` (fix the one rule everywhere it fires). Resolve **every** concern in the group \u2014 when the scan's \`co_located\` shows several scanners flagged the same \`file:line\` (\xA730), they're one underlying defect: write ONE canonical fix and one explanation, not separate edits. **Only touch \`*.tf\` / \`*.tfvars\` files.** Make the smallest changes that clear the concerns \u2014 do NOT reformat or refactor unrelated code (see *SYSTEM* surgical-change rules). **Module-source awareness (\xA74.14):** call \`${t("terraform_module_graph")}\` first \u2014 if the concern's file is inside a \`local_module_dir\`, fix it ONCE at the module source (it propagates to all callers; note them in the PR); if the fix would require editing a registry/git/remote module, you can't fix it here \u2014 report it (open an issue naming the upstream module + version) instead. **Approved modules (\xA74.14):** call \`${t("list_modules")}\` and prefer a catalogue module (registry or house, pinned) when the fix is genuinely a module swap \u2014 but for a one-line fix on an existing raw resource, fix it in place. **Provider-major awareness (\xA74.15):** before introducing an argument or block, check \`terraform_validate\`'s \`providers\` list for the pinned \`major\` \u2014 argument names and valid blocks differ across majors. After the dir is init-ed (validate/plan ran), you can **verify an argument exists** for the installed provider with \`${t("terraform_provider_schema")}\` (pass the resource type + the arg names you added; it returns any \`unknown_args\` that would break \`plan\`). **Reusing a module?** call \`${t("terraform_module_interface")}\` on its dir to get its real \`variable\` names + which are required, so the \`module\` block you write is correct.
|
|
1357
|
+
- **fix QUALITY \u2014 a real fix, not a scanner-silencer (enterprise bar)**: the goal is infrastructure that is *actually* safer, not Terraform that merely stops tripping the scanner. Three rules:
|
|
1358
|
+
- **Secure defaults, never a hidden-insecure one.** When you parameterise a hardcoded value (\xA74.13), the new \`variable\`'s \`default\` must be the SECURE choice, or have **no \`default\`** (forcing the operator to set it). NEVER preserve the insecure value as the default \u2014 e.g. replacing \`cidr_blocks = ["0.0.0.0/0"]\` with \`var.allowed_cidr_blocks\` *defaulting to \`["0.0.0.0/0"]\`* is not a fix: the deployed behaviour is identical and you've just moved the problem somewhere a scanner may not see it. If the secure value genuinely needs a human decision, this is an honest-refusal (\xA729) / propose-then-steer (\xA726) case, not a default-to-insecure.
|
|
1359
|
+
- **Optional-input resources must be conditional.** If a fix adds a resource or block that only works when an OPTIONAL input is set (e.g. an HTTPS listener that needs \`var.ssl_certificate_arn\`, which defaults to \`null\`), gate it with \`count\`/\`for_each\` or a \`dynamic\` block so it is NOT emitted \u2014 and cannot break \`plan\`/\`apply\` \u2014 when the input is unset. Do not write an always-present resource that references a null/empty value, and never claim in the PR that something is "only active when set" unless the HCL actually makes it conditional. Remember \`${t("terraform_validate")}\` can pass on HCL that still fails at \`plan\`/\`apply\` \u2014 your claim of conditionality must be in the code, not just the prose.
|
|
1360
|
+
- **Modernise, don't perpetuate (\xA74.15).** Call \`${t("terraform_version_currency")}\` and, when you must add a \`required_providers\`/\`required_version\` block, pin a CURRENT supported major, not an ancient one chosen only to match legacy code. Flag (in the PR body, as a follow-up \u2014 not necessarily fixed in this scoped PR) deprecated patterns the scanners don't encode: the archived \`hashicorp/template\` provider + \`data "template_file"\` (modern: the built-in \`templatefile()\` function), \`aws_launch_configuration\` (\u2192 \`aws_launch_template\`), and any provider/module pin that is several majors behind. A best-practice fix should not entrench an EOL provider.
|
|
1297
1361
|
- **keep the module's tests/examples consistent (\xA728 \u2014 only when you fixed a reusable module)**: if the file(s) you changed live inside a \`local_module_dir\` (from \`${t("terraform_module_graph")}\`) AND your fix changed the module's public interface (added/removed/renamed a \`variable\`, tightened a type), call \`${t("terraform_module_tests")}\` with that module dir. It returns the module's existing \`examples/\` fixtures + \`terraform test\` (\`*.tftest.hcl\`) / Go Terratest files and the \`drift\` per asset \u2014 \`missing_required\` (a variable the asset must now set) and \`unknown_set\` (a variable the asset references that no longer exists). Update **exactly** the drifting assets so they match the new interface; **never weaken, delete, or comment out an assertion just to make a test pass** \u2014 a fix that breaks a module's contract is the test doing its job, so correct the fix or the fixture, not the assertion. \`examples/\` are \`*.tf\` (always within the push allow-list); native \`*.tftest.hcl\` / Go \`*_test.go\` files are only pushable when the \`terratest\` input is enabled \u2014 when it isn't and only those drift, note the needed test update in the PR body for a human rather than leaving the module's own tests broken. Skip this entirely for a one-off raw-resource fix that doesn't touch a module interface.
|
|
1298
1362
|
- **validate**: call \`${t("terraform_validate")}\`. If it does not pass, fix what it reports or abandon this group \u2014 **never open a PR whose validate did not pass**. Its \`providers\` field carries the pinned provider majors (use them as above). It also returns \`unknown_arguments\` (\xA74.15-next): arguments you wrote that are NOT in the installed provider's schema and would break \`plan\` \u2014 treat any entry as a must-fix (correct the argument for the pinned major) even though \`passed\` doesn't gate on it. \`schema_checked: false\` means the schema wasn't available (rely on \`${t("terraform_plan")}\` then).
|
|
1299
1363
|
- **policy gate (optional, \xA73.5)**: if the repo ships policy-as-code (a \`policy/\`, \`policies/\`, or \`.conftest\` dir of Rego), call \`${t("policy_check")}\` \u2014 it runs \`conftest\` against the plan JSON. It degrades green (\`ok: false\`) when conftest or a policy dir is absent. When it returns \`passed: false\`, treat it exactly like a failed validate: fix the violation (listed in \`failures\`) or label the PR \`needs-human\` and surface it \u2014 never push past a policy denial.
|
|
@@ -1305,7 +1369,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
1305
1369
|
- **full plan (\`plan_text\`, \xA71.2)**: when present, attach it to the PR body as a collapsed \`<details><summary>Plan</summary>\\n\\n\\\`\\\`\\\`\\n\u2026\\n\\\`\\\`\\\`\\n</details>\` block so a reviewer can see the exact planned change without re-running it.
|
|
1306
1370
|
- **commit + push**: \`git add\` only the file you changed, commit with a message naming the file and the key rules (e.g. \`fix(tf): harden main.tf \u2014 S3 encryption + block public access\`), then \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*).
|
|
1307
1371
|
- **open PR \u2014 with a COMPLETE body (MANDATORY)**: \`${t("create_pull_request")}\` (omit \`base\` \u2014 it resolves to the run's base branch above). The PR body is the primary deliverable a human reviews \u2014 open the PR **with a full body**, never a placeholder you intend to fill in later. At an absolute minimum the body MUST explain, in plain English: (a) **what was wrong** \u2014 each concern by \`rule_id\` + its \`evidence\`; and (b) **what you changed** to fix each one, and why it's safe. Build it with the **Remediation PR format** at the end of this checklist (status banner \u2192 title + badges \u2192 \`## What changed\` with the \xA75.17 *Was / Changed / Safe because* note per concern). \u26A0\uFE0F \`${t("report_progress")}\` writes the GitHub Actions **job summary**, which is NOT the PR body \u2014 a good job summary does **not** substitute for a complete PR body. If you only have time/budget for one, the PR body wins.
|
|
1308
|
-
- **prove it (
|
|
1372
|
+
- **prove it (re-scan)**: call \`${t("terraform_verify_remediation")}\` with the group's \`concern_ids\`. It re-runs the scanners and returns the authoritative \`resolved\` / \`remaining\` sets and a \`verified\` flag \u2014 this is the proof, do NOT eyeball a scan or self-report. Then \`${t("update_pull_request_body")}\` to add a "Validation" section built **from that result**: one \`\u2705 <rule_id> resolved\` line per id in \`resolved\`, and list every id in \`remaining\` honestly as still-open. Never put a \u2705 on a concern unless the tool returned it in \`resolved\`. Act on two more fields it returns:
|
|
1309
1373
|
- **regressions (\xA71.4)**: when \`has_regressions\` is true, the fix INTRODUCED new concerns (listed in \`regressions\`) that weren't there before \u2014 it traded one defect for another. Add a prominent **\u26A0\uFE0F Regression** callout listing them, add the \`needs-human\` label (\`${t("add_labels")}\`), and prefer reworking the fix to remove the regression before relying on the PR.
|
|
1310
1374
|
- **confidence (\xA75.19)**: render the returned \`confidence\` (high/medium/low) as a one-line badge in the PR body (e.g. \`Confidence: high\`) with its \`confidence_reasons\`. It is computed deterministically from the verification evidence (verified + no regressions + plan idempotency + blast radius + cost) \u2014 report it verbatim, do NOT inflate it.
|
|
1311
1375
|
- **per-finding explanation (\xA75.17)**: in the PR body, give each resolved concern a short three-line note \u2014 **Was** (what the scanner flagged, from its \`evidence\`), **Changed** (what your fix did), **Safe because** (why it's correct/non-breaking) \u2014 and hyperlink the \`rule_id\` to its documentation. The scan output carries a \`doc_url\` per concern (and \`doc_urls\` per group); use it, falling back to the concern's \`remediation_hint\` when no \`doc_url\` is present.
|
|
@@ -1315,7 +1379,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
1315
1379
|
|
|
1316
1380
|
5. **guardrails** (always): one scoped PR per group, never a mega-PR spanning multiple files; **never auto-merge** and always leave the PR for human review; never modify files outside \`*.tf\` / \`*.tfvars\`.
|
|
1317
1381
|
|
|
1318
|
-
6. **finalize**: call \`${t("report_progress")}\` once with a summary \u2014 which file/group was fixed, the PR link, and the
|
|
1382
|
+
6. **finalize**: call \`${t("report_progress")}\` once with a summary \u2014 which file/group was fixed, the PR link, and the validation result (resolved \u2705 / still-open) (or the exact tool error if push/PR creation failed).
|
|
1319
1383
|
|
|
1320
1384
|
**SARIF for code-scanning (optional, \xA73.5)**: when the workflow has a SARIF upload step (it grants \`security-events: write\` and runs \`github/codeql-action/upload-sarif\` on a \`terramend.sarif\`), call \`${t("terraform_emit_sarif")}\` once at the end so the full scan also lands in the repo's Security tab \u2014 complementary to the fix PR, not a replacement for it.
|
|
1321
1385
|
|
|
@@ -1380,7 +1444,7 @@ ${REMEDIATION_PR_FORMAT}`
|
|
|
1380
1444
|
|
|
1381
1445
|
8. **finalize**:
|
|
1382
1446
|
- confirm a clean working tree (only your new \`*.tf\`/\`*.tfvars\` files), then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*).
|
|
1383
|
-
- open a PR via \`${t("create_pull_request")}\` (omit \`base\` \u2014 it resolves to the run's base branch above). Use the **Remediation PR format** conventions (the same status banner + badge row + \`## What changed\` shape, and the body-wide rules) ADAPTED for generation: the body states the requirement, what was generated, the key best-practice choices (security defaults, parameters, modules, pinned versions) and any assumptions; the badge row carries \`Plan\`/\`Cost\` when those tools ran; and in place of the \`## Validation
|
|
1447
|
+
- open a PR via \`${t("create_pull_request")}\` (omit \`base\` \u2014 it resolves to the run's base branch above). Use the **Remediation PR format** conventions (the same status banner + badge row + \`## What changed\` shape, and the body-wide rules) ADAPTED for generation: the body states the requirement, what was generated, the key best-practice choices (security defaults, parameters, modules, pinned versions) and any assumptions; the badge row carries \`Plan\`/\`Cost\` when those tools ran; and in place of the \`## Validation\` proof list put a \`## Validation\` line stating \`terraform_validate\` passed and \`terraform_scan\` is clean (self-scan: 0 concerns).
|
|
1384
1448
|
- **never auto-merge** \u2014 leave the PR for human review.
|
|
1385
1449
|
- call \`${t("report_progress")}\` once with the PR link (or the exact tool error if push/PR creation failed).
|
|
1386
1450
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type CrosswalkReport } from "#app/mcp/crosswalk";
|
|
2
|
+
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
3
|
+
import { type Concern, type ScannerOutcome, type Severity } from "#app/mcp/terraform/types";
|
|
4
|
+
import { type VerificationSummary } from "#app/mcp/terraform/verification";
|
|
5
|
+
import { type ResolvedToolSelection } from "#app/utils/toolSelection";
|
|
6
|
+
/**
|
|
7
|
+
* Assess pillar — the read-only product (roadmap pillar 3). Terramend's scanner
|
|
8
|
+
* engine has two modes off ONE codebase: Remediate = engine + fix loop + verify;
|
|
9
|
+
* **Assess = engine, read-only**. This surfaces that read-only half as a
|
|
10
|
+
* first-class deliverable: run the deterministic scanners, normalise into the
|
|
11
|
+
* findings schema, map to the compliance crosswalk (§23), and produce a
|
|
12
|
+
* **scorecard** + an auditor-facing markdown report — WITHOUT touching the
|
|
13
|
+
* Terraform or opening a PR. No cloud credentials, no writes.
|
|
14
|
+
*
|
|
15
|
+
* The scorecard is deterministic (computed from tool results, never the model's
|
|
16
|
+
* word) so a CI gate can branch on `posture` and an assessor gets a reproducible,
|
|
17
|
+
* framework-mapped report.
|
|
18
|
+
*/
|
|
19
|
+
export type AssessPosture = "clean" | "advisory" | "action-required";
|
|
20
|
+
export interface AssessTopRisk {
|
|
21
|
+
rule_id: string;
|
|
22
|
+
severity: Severity;
|
|
23
|
+
file: string;
|
|
24
|
+
line: number | null;
|
|
25
|
+
evidence: string;
|
|
26
|
+
}
|
|
27
|
+
export interface AssessmentScorecard {
|
|
28
|
+
/** clean (0 concerns) · advisory (only medium/low/info) · action-required (≥1 critical/high). */
|
|
29
|
+
posture: AssessPosture;
|
|
30
|
+
total: number;
|
|
31
|
+
by_severity: Record<Severity, number>;
|
|
32
|
+
/** highest-severity concerns first, capped — the "what to look at first" list. */
|
|
33
|
+
top_risks: AssessTopRisk[];
|
|
34
|
+
compliance: {
|
|
35
|
+
/** frameworks this scan touched (from the crosswalk's by_framework index). */
|
|
36
|
+
frameworks: string[];
|
|
37
|
+
/** distinct controls touched across all frameworks. */
|
|
38
|
+
controls_touched: number;
|
|
39
|
+
/** concerns that mapped to ≥1 control vs none (honest coverage signal). */
|
|
40
|
+
mapped: number;
|
|
41
|
+
unmapped: number;
|
|
42
|
+
version: string;
|
|
43
|
+
reviewed: string;
|
|
44
|
+
};
|
|
45
|
+
/** five-status verification taxonomy: per-concern fail / not-code-verifiable +
|
|
46
|
+
* the scanner coverage (verified vs inconclusive). Keeps a "clean" posture
|
|
47
|
+
* honest — e.g. "clean, but tflint inconclusive (not run)". */
|
|
48
|
+
verification: VerificationSummary;
|
|
49
|
+
}
|
|
50
|
+
/** posture from the severity distribution: any critical/high ⇒ action-required;
|
|
51
|
+
* any lower-severity concern ⇒ advisory; nothing ⇒ clean. */
|
|
52
|
+
export declare function assessPosture(bySeverity: Record<Severity, number>): AssessPosture;
|
|
53
|
+
/**
|
|
54
|
+
* Build the deterministic assessment scorecard from a scan's concerns + their
|
|
55
|
+
* crosswalk report. Pure. `concerns` should be the severity-sorted, deduped,
|
|
56
|
+
* Terraform-only set; `crosswalk` is `buildCrosswalkReport(concerns)`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildAssessment(concerns: Concern[], crosswalk: CrosswalkReport, verification: VerificationSummary): AssessmentScorecard;
|
|
59
|
+
/**
|
|
60
|
+
* Render the scorecard as a deterministic, auditor-facing markdown report (the
|
|
61
|
+
* Assess deliverable). Built entirely from the scorecard so it's reproducible and
|
|
62
|
+
* model-independent. Pure.
|
|
63
|
+
*/
|
|
64
|
+
export declare function renderAssessmentMarkdown(s: AssessmentScorecard): string;
|
|
65
|
+
export declare const TerraformAssessParams: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
66
|
+
severity_threshold?: "critical" | "high" | "medium" | "low" | "info";
|
|
67
|
+
}, {}>;
|
|
68
|
+
/** the full read-only assessment pipeline: scan (honouring the §1.5 licence gate
|
|
69
|
+
* + module-fetch credential) → crosswalk → verification taxonomy → scorecard.
|
|
70
|
+
* Shared by `terraform_assess` and the evidence-bundle emitter so both report the
|
|
71
|
+
* identical posture from the identical toolchain. Pure-ish (only the scanners do
|
|
72
|
+
* I/O); no writes. */
|
|
73
|
+
export declare function runAssessmentPipeline(ctx: LocalToolContext, threshold: Severity): {
|
|
74
|
+
cwd: string;
|
|
75
|
+
selection: ResolvedToolSelection;
|
|
76
|
+
outcomes: ScannerOutcome[];
|
|
77
|
+
concerns: Concern[];
|
|
78
|
+
crosswalk: CrosswalkReport;
|
|
79
|
+
verification: VerificationSummary;
|
|
80
|
+
scorecard: AssessmentScorecard;
|
|
81
|
+
};
|
|
82
|
+
export declare function TerraformAssessTool(ctx: LocalToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
|
|
83
|
+
severity_threshold?: "critical" | "high" | "medium" | "low" | "info";
|
|
84
|
+
}, {
|
|
85
|
+
severity_threshold?: "critical" | "high" | "medium" | "low" | "info";
|
|
86
|
+
}>>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
2
|
+
/**
|
|
3
|
+
* §36 AI PR summaries — the deterministic Terraform-change anchor. A PR summary
|
|
4
|
+
* written purely by the model drifts (miscounts resources, invents changes). This
|
|
5
|
+
* parses the PR's unified diff for Terraform BLOCK changes (which resource /
|
|
6
|
+
* module / data / variable / output addresses were added or removed, which files
|
|
7
|
+
* were touched) so the human-readable summary is anchored to facts, not prose.
|
|
8
|
+
*
|
|
9
|
+
* Pure parser (`summarizeTerraformResourceDiff`) + a tool that runs the
|
|
10
|
+
* merge-base diff and feeds it in. Block ADDED/REMOVED is precise (a block header
|
|
11
|
+
* on a +/- line); in-place edits to an existing block surface as a touched FILE
|
|
12
|
+
* (attributing a sub-block edit to a specific address needs full-file parsing —
|
|
13
|
+
* we stay honest and report the file rather than guess).
|
|
14
|
+
*/
|
|
15
|
+
export interface TerraformChangeSummary {
|
|
16
|
+
/** addresses of blocks added in this diff (e.g. `aws_s3_bucket.logs`, `module.vpc`). */
|
|
17
|
+
added: string[];
|
|
18
|
+
/** addresses of blocks removed in this diff. */
|
|
19
|
+
removed: string[];
|
|
20
|
+
/** Terraform files touched (a superset signal — includes in-place edits). */
|
|
21
|
+
files: string[];
|
|
22
|
+
counts: {
|
|
23
|
+
added: number;
|
|
24
|
+
removed: number;
|
|
25
|
+
files: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a Terraform block header into its address, or null when the line is not a
|
|
30
|
+
* top-level block header. Handles two-label blocks (`resource`/`data`) and
|
|
31
|
+
* single-label blocks (`module`/`variable`/`output`/`provider`). The line is the
|
|
32
|
+
* raw HCL (diff +/- prefix already stripped). Pure.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseBlockAddress(line: string): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Summarise a unified `git diff` into added/removed Terraform block addresses +
|
|
37
|
+
* the touched Terraform files. Tracks the current file from `+++ b/<path>`
|
|
38
|
+
* headers and only considers `.tf`/`.tfvars` files. Pure; deterministic ordering
|
|
39
|
+
* (sorted, de-duplicated). A block counted as both added and removed (moved) is
|
|
40
|
+
* left in both lists — the prose can describe the move.
|
|
41
|
+
*/
|
|
42
|
+
export declare function summarizeTerraformResourceDiff(diff: string): TerraformChangeSummary;
|
|
43
|
+
export declare const TerraformChangeSummaryParams: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
44
|
+
base?: string;
|
|
45
|
+
}, {}>;
|
|
46
|
+
export declare function TerraformChangeSummaryTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
|
|
47
|
+
base?: string;
|
|
48
|
+
}, {
|
|
49
|
+
base?: string;
|
|
50
|
+
}>>;
|
package/dist/mcp/crosswalk.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolContext } from "#app/mcp/server";
|
|
2
|
+
import { type ConcernVerificationStatus } from "#app/mcp/terraform/verification";
|
|
2
3
|
/**
|
|
3
4
|
* Compliance crosswalk (§differentiator 23 — "explain like I'm the auditor", the
|
|
4
5
|
* seed of the Part-6 moat). Maps a best-practice concern → the control families
|
|
@@ -56,6 +57,10 @@ export interface CrosswalkEntry {
|
|
|
56
57
|
rule_id: string;
|
|
57
58
|
themes: string[];
|
|
58
59
|
controls: ControlRef[];
|
|
60
|
+
/** the five-status verdict for this control statement: `fail` (code-verified
|
|
61
|
+
* violation) or `not-code-verifiable` (a human-decision control the engine can
|
|
62
|
+
* flag but not prove). Lets an assessor read the crosswalk honestly. */
|
|
63
|
+
status: ConcernVerificationStatus;
|
|
59
64
|
}
|
|
60
65
|
export interface CrosswalkReport {
|
|
61
66
|
version: string;
|
|
@@ -13,7 +13,7 @@ import type { ResolvedPayload } from "#app/utils/payload";
|
|
|
13
13
|
* needs more (octokit, push, PR state) belongs on `ToolContext`, not here.
|
|
14
14
|
*/
|
|
15
15
|
export interface LocalToolContext {
|
|
16
|
-
payload: Pick<ResolvedPayload, "cwd" | "scanScope" | "severityThreshold" | "autonomyThreshold" | "costIncreaseBlockUsd" | "moduleCatalogue">;
|
|
16
|
+
payload: Pick<ResolvedPayload, "cwd" | "scanScope" | "severityThreshold" | "autonomyThreshold" | "costIncreaseBlockUsd" | "moduleCatalogue" | "toolsEnabled" | "gitleaks" | "terratest" | "terraformMcp" | "moduleFetchToken">;
|
|
17
17
|
toolState: ToolState;
|
|
18
18
|
tmpdir: string;
|
|
19
19
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type AssessmentScorecard } from "#app/mcp/assess";
|
|
2
|
+
import type { CrosswalkReport } from "#app/mcp/crosswalk";
|
|
3
|
+
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
4
|
+
import { type VerificationStatus, type VerificationSummary } from "#app/mcp/terraform/verification";
|
|
5
|
+
/**
|
|
6
|
+
* Backend-free compliance evidence bundle (the WS4a wedge — an auditor-facing
|
|
7
|
+
* artifact the OSS action can emit with **zero cloud and no backend**, committed
|
|
8
|
+
* to a `compliance/` path). It packages the read-only assessment — posture,
|
|
9
|
+
* per-control statements with their five-status verdict ([[verification]]), the
|
|
10
|
+
* crosswalk index, and the scanner coverage — into one deterministic JSON file.
|
|
11
|
+
*
|
|
12
|
+
* SCHEMA / HONESTY: this is Terramend's OWN structured schema, NOT OSCAL. A
|
|
13
|
+
* strict OSCAL (or compliance-trestle / C2P) emitter is a deliberate follow-up,
|
|
14
|
+
* gated on a buyer who actually needs OSCAL — emitting OSCAL nobody consumes is
|
|
15
|
+
* cost without value. The bundle is INDICATIVE alignment guidance, never an audit
|
|
16
|
+
* verdict, and it never claims `pass` for an unfired control (absence of a
|
|
17
|
+
* finding is not proof). Pure builder + a thin file-writing tool.
|
|
18
|
+
*/
|
|
19
|
+
export declare const EVIDENCE_SCHEMA: "terramend-evidence/v0.1";
|
|
20
|
+
export declare const DEFAULT_EVIDENCE_PATH = "compliance/terramend-evidence.json";
|
|
21
|
+
export interface EvidenceControlStatement {
|
|
22
|
+
concern_id: string;
|
|
23
|
+
rule_id: string;
|
|
24
|
+
/** the five-status verdict for this statement (fail / not-code-verifiable). */
|
|
25
|
+
status: VerificationStatus;
|
|
26
|
+
severity?: string | undefined;
|
|
27
|
+
file?: string | undefined;
|
|
28
|
+
line?: number | null | undefined;
|
|
29
|
+
controls: {
|
|
30
|
+
framework: string;
|
|
31
|
+
control: string;
|
|
32
|
+
title: string;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
export interface EvidenceBundle {
|
|
36
|
+
/** Terramend's own schema id — explicitly NOT OSCAL (see module note). */
|
|
37
|
+
schema: typeof EVIDENCE_SCHEMA;
|
|
38
|
+
/** caller-supplied ISO timestamp (kept out of the builder so it stays pure). */
|
|
39
|
+
generated_at: string;
|
|
40
|
+
tool: {
|
|
41
|
+
name: "terramend";
|
|
42
|
+
version: string;
|
|
43
|
+
};
|
|
44
|
+
subject: {
|
|
45
|
+
scanned_dir: string;
|
|
46
|
+
repo?: string | undefined;
|
|
47
|
+
ref?: string | undefined;
|
|
48
|
+
commit?: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
posture: AssessmentScorecard["posture"];
|
|
51
|
+
summary: {
|
|
52
|
+
total: number;
|
|
53
|
+
by_severity: AssessmentScorecard["by_severity"];
|
|
54
|
+
verification: VerificationSummary["counts"];
|
|
55
|
+
};
|
|
56
|
+
/** one statement per mapped concern, each carrying its status + controls. */
|
|
57
|
+
control_statements: EvidenceControlStatement[];
|
|
58
|
+
/** which scanners code-verified vs which were inconclusive (coverage gaps). */
|
|
59
|
+
coverage: VerificationSummary["coverage"];
|
|
60
|
+
crosswalk: {
|
|
61
|
+
version: string;
|
|
62
|
+
reviewed: string;
|
|
63
|
+
by_framework: CrosswalkReport["by_framework"];
|
|
64
|
+
};
|
|
65
|
+
/** the five-status legend, so the bundle is self-describing for an assessor. */
|
|
66
|
+
legend: Record<VerificationStatus, string>;
|
|
67
|
+
disclaimer: string;
|
|
68
|
+
}
|
|
69
|
+
export interface EvidenceSubject {
|
|
70
|
+
scanned_dir: string;
|
|
71
|
+
repo?: string | undefined;
|
|
72
|
+
ref?: string | undefined;
|
|
73
|
+
commit?: string | undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the evidence bundle from an assessment's scorecard + crosswalk. Pure —
|
|
77
|
+
* `generatedAt` and the subject identifiers are passed in so the same inputs
|
|
78
|
+
* always produce the same bytes (and tests don't need a clock). Control
|
|
79
|
+
* statements come from the crosswalk entries (which already carry the
|
|
80
|
+
* verification status), enriched with the concern's severity/location.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildEvidenceBundle(args: {
|
|
83
|
+
scorecard: AssessmentScorecard;
|
|
84
|
+
crosswalk: CrosswalkReport;
|
|
85
|
+
subject: EvidenceSubject;
|
|
86
|
+
generatedAt: string;
|
|
87
|
+
version?: string;
|
|
88
|
+
}): EvidenceBundle;
|
|
89
|
+
export declare const TerraformEmitEvidenceParams: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
90
|
+
output_path?: string;
|
|
91
|
+
severity_threshold?: "critical" | "high" | "medium" | "low" | "info";
|
|
92
|
+
}, {}>;
|
|
93
|
+
export declare function TerraformEmitEvidenceTool(ctx: LocalToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
|
|
94
|
+
output_path?: string;
|
|
95
|
+
severity_threshold?: "critical" | "high" | "medium" | "low" | "info";
|
|
96
|
+
}, {
|
|
97
|
+
output_path?: string;
|
|
98
|
+
severity_threshold?: "critical" | "high" | "medium" | "low" | "info";
|
|
99
|
+
}>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Concern, type ScannerOutcome } from "#app/mcp/terraform/types";
|
|
2
|
+
import { type ResolvedToolSelection } from "#app/utils/toolSelection";
|
|
2
3
|
export declare function scanFmt(cwd: string): ScannerOutcome;
|
|
3
4
|
/** `terraform fmt -check -list=true` prints one unformatted file path per line. */
|
|
4
5
|
export declare function parseFmtOutput(stdout: string, cwd?: string): Concern[];
|
|
@@ -8,7 +9,7 @@ export declare function parseFmtOutput(stdout: string, cwd?: string): Concern[];
|
|
|
8
9
|
* whole tree), so a multi-root repo only catches subdir-root validate errors
|
|
9
10
|
* when we visit each root.
|
|
10
11
|
*/
|
|
11
|
-
export declare function scanValidate(cwd: string): ScannerOutcome;
|
|
12
|
+
export declare function scanValidate(cwd: string, extraEnv?: Record<string, string>): ScannerOutcome;
|
|
12
13
|
/** parse `terraform validate -json`; keeps real errors, drops uninitialized-dir noise. */
|
|
13
14
|
export declare function parseValidateOutput(stdout: string, cwd?: string): Concern[];
|
|
14
15
|
export interface ProviderRequirement {
|
|
@@ -95,9 +96,20 @@ export declare function parseCheckovOutput(stdout: string, cwd?: string): Concer
|
|
|
95
96
|
* the base can't be determined (caller then falls back to a full scan).
|
|
96
97
|
*/
|
|
97
98
|
export declare function changedTerraformFiles(cwd: string): Set<string> | null;
|
|
99
|
+
export interface RunScannersOptions {
|
|
100
|
+
/** §1.5 — the resolved tool selection. A scanner whose tool is gated/disabled
|
|
101
|
+
* is reported as a `skipped` outcome (with the licence/disable reason) instead
|
|
102
|
+
* of running, so `terraform_scan` and the ✗→✓ verifier see the SAME toolchain. */
|
|
103
|
+
selection?: ResolvedToolSelection | undefined;
|
|
104
|
+
/** §1.5 — module-fetch credential env, threaded into `terraform validate`'s
|
|
105
|
+
* init so a private cross-repo `git::` module resolves during a scan. */
|
|
106
|
+
terraformEnv?: Record<string, string> | undefined;
|
|
107
|
+
}
|
|
98
108
|
/** run every scanner once over `cwd`. shared by `terraform_scan` and the
|
|
99
|
-
* deterministic remediation verifier so both see the identical toolchain.
|
|
100
|
-
|
|
109
|
+
* deterministic remediation verifier so both see the identical toolchain. A
|
|
110
|
+
* scanner the selection has turned off is emitted as `skipped` (never run), so
|
|
111
|
+
* the gate applies identically to the scan and its verification re-scan. */
|
|
112
|
+
export declare function runScanners(cwd: string, opts?: RunScannersOptions): ScannerOutcome[];
|
|
101
113
|
export interface RemediationVerdict {
|
|
102
114
|
/** true only when every original concern id is absent from the re-scan. */
|
|
103
115
|
verified: boolean;
|
|
@@ -129,3 +141,26 @@ export declare function computeRemediationVerdict(originalConcernIds: string[],
|
|
|
129
141
|
* Returns sorted ids for a stable PR body.
|
|
130
142
|
*/
|
|
131
143
|
export declare function computeRegressions(baselineConcernIds: Iterable<string>, currentConcernIds: Iterable<string>): string[];
|
|
144
|
+
/**
|
|
145
|
+
* Line-INDEPENDENT ✗→✓ partition (the integrity-preserving replacement for the
|
|
146
|
+
* raw-id `computeRemediationVerdict` when scan context is available). Each
|
|
147
|
+
* requested entry carries its display `id` and its `key` (see `concernKeyOf`);
|
|
148
|
+
* a concern is `remaining` iff its KEY still appears in the re-scan, else
|
|
149
|
+
* `resolved`. Because a fix that shifts lines keeps the same (source|rule|file)
|
|
150
|
+
* key, an unfixed concern can no longer be mis-reported as resolved. Pure.
|
|
151
|
+
*/
|
|
152
|
+
export declare function partitionByKey(requested: {
|
|
153
|
+
id: string;
|
|
154
|
+
key: string;
|
|
155
|
+
}[], currentKeys: Set<string>): RemediationVerdict;
|
|
156
|
+
/**
|
|
157
|
+
* Line-INDEPENDENT regression set: one representative current concern id per KEY
|
|
158
|
+
* present in the re-scan but absent from the pre-fix baseline keys. A pre-existing
|
|
159
|
+
* concern that merely shifted to a new line (same key) is NOT a regression — only
|
|
160
|
+
* a genuinely new (rule, file) defect is. Replaces the raw-id `computeRegressions`
|
|
161
|
+
* for the integrity path. Pure; returns sorted ids for a stable PR body.
|
|
162
|
+
*/
|
|
163
|
+
export declare function regressionIdsByKey(current: {
|
|
164
|
+
id: string;
|
|
165
|
+
key: string;
|
|
166
|
+
}[], baselineKeys: Set<string>): string[];
|
|
@@ -46,6 +46,22 @@ export declare const SEVERITIES: readonly ["critical", "high", "medium", "low",
|
|
|
46
46
|
export type Severity = (typeof SEVERITIES)[number];
|
|
47
47
|
export declare const SEVERITY_RANK: Record<Severity, number>;
|
|
48
48
|
export declare function concernId(source: string, ruleId: string, file: string, line: number | null): string;
|
|
49
|
+
/**
|
|
50
|
+
* A LINE-INDEPENDENT identity for a concern — which rule fires in which file,
|
|
51
|
+
* ignoring the exact line. Two instances of the same rule in the same file at
|
|
52
|
+
* different lines share a key.
|
|
53
|
+
*
|
|
54
|
+
* The full content `id` keys on the line so it's unique per instance (right for
|
|
55
|
+
* SARIF alerts + branch naming), but that makes it UNSTABLE under a fix: almost
|
|
56
|
+
* every fix adds or removes lines, shifting every concern below it to a new line
|
|
57
|
+
* → a new id. If ✗→✓ verification compared raw ids, a shifted-but-unfixed concern
|
|
58
|
+
* would look RESOLVED (old id gone) and simultaneously look like a REGRESSION
|
|
59
|
+
* (new id appeared) — a false attestation either way. `terraform_verify_remediation`
|
|
60
|
+
* compares on this key instead, so a line shift can't fabricate a resolution or a
|
|
61
|
+
* regression. Derived identically to `id` minus the line (same bare-rule
|
|
62
|
+
* normalization) so keys match across the original scan and the re-scan.
|
|
63
|
+
*/
|
|
64
|
+
export declare function concernKeyOf(c: Pick<Concern, "source" | "rule_id" | "location">): string;
|
|
49
65
|
/**
|
|
50
66
|
* Normalize a scanner-reported path to a repo-relative POSIX path. Each scanner
|
|
51
67
|
* reports the file differently — tflint gives `main.tf` (relative), trivy a
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Concern, ScannerOutcome } from "#app/mcp/terraform/types";
|
|
2
|
+
/**
|
|
3
|
+
* Five-status verification taxonomy (the auditor-credibility win the evidence
|
|
4
|
+
* pack + crosswalk both lean on). The point is HONESTY: never let "no finding"
|
|
5
|
+
* read as "compliant", and never claim the engine proved something it cannot see
|
|
6
|
+
* from code. Every assessment statement carries exactly one of these:
|
|
7
|
+
*
|
|
8
|
+
* - `pass` — a check ran and code-verified compliance.
|
|
9
|
+
* - `fail` — a check ran and code-verified a violation.
|
|
10
|
+
* - `not-applicable` — the control does not apply to the resources present.
|
|
11
|
+
* - `inconclusive` — a relevant check did NOT run (gated / not installed /
|
|
12
|
+
* unparseable). A coverage gap, never silently a pass.
|
|
13
|
+
* - `not-code-verifiable` — the control needs human / process evidence
|
|
14
|
+
* (governance, training, a key-policy decision); IaC
|
|
15
|
+
* scanning structurally cannot prove it either way.
|
|
16
|
+
*
|
|
17
|
+
* What this engine asserts today: `fail` and `not-code-verifiable` per concern,
|
|
18
|
+
* and `inconclusive` per scanner that didn't run. It deliberately does NOT
|
|
19
|
+
* fabricate `pass` / `not-applicable` for controls nothing fired on — absence of
|
|
20
|
+
* a finding is not proof, and over-claiming is exactly what costs credibility
|
|
21
|
+
* with an assessor. The two reserved statuses are part of the shared vocabulary
|
|
22
|
+
* for the evidence consumer (and a future full-framework crosswalk). Pure.
|
|
23
|
+
*/
|
|
24
|
+
export declare const VERIFICATION_STATUSES: readonly ["pass", "fail", "not-applicable", "inconclusive", "not-code-verifiable"];
|
|
25
|
+
export type VerificationStatus = (typeof VERIFICATION_STATUSES)[number];
|
|
26
|
+
/** one-line legend per status — for the report / evidence bundle. */
|
|
27
|
+
export declare const VERIFICATION_STATUS_LABEL: Record<VerificationStatus, string>;
|
|
28
|
+
/** the statuses the engine asserts per concern (a concern is always one or the
|
|
29
|
+
* other — it fired, the only question is whether code can prove the fix). */
|
|
30
|
+
export type ConcernVerificationStatus = Extract<VerificationStatus, "fail" | "not-code-verifiable">;
|
|
31
|
+
/**
|
|
32
|
+
* Classify one concern: a code-verified violation (`fail`) — UNLESS its
|
|
33
|
+
* remediation is a human decision the engine can flag but not prove from code
|
|
34
|
+
* (IAM least-privilege, a KMS key policy, a real CIDR — the §29 refusal set), in
|
|
35
|
+
* which case it is `not-code-verifiable`. Pure.
|
|
36
|
+
*/
|
|
37
|
+
export declare function concernVerificationStatus(concern: Pick<Concern, "rule_id" | "evidence">): {
|
|
38
|
+
status: ConcernVerificationStatus;
|
|
39
|
+
reason?: string;
|
|
40
|
+
};
|
|
41
|
+
export interface VerifiedConcern {
|
|
42
|
+
id: string;
|
|
43
|
+
status: ConcernVerificationStatus;
|
|
44
|
+
reason?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface VerificationSummary {
|
|
47
|
+
/** per-concern verification status. */
|
|
48
|
+
concerns: VerifiedConcern[];
|
|
49
|
+
counts: {
|
|
50
|
+
fail: number;
|
|
51
|
+
not_code_verifiable: number;
|
|
52
|
+
/** scanners that did not run (each is a coverage gap). */
|
|
53
|
+
inconclusive: number;
|
|
54
|
+
};
|
|
55
|
+
coverage: {
|
|
56
|
+
/** scanners that ran — their checks are code-verified for what they cover. */
|
|
57
|
+
verified: string[];
|
|
58
|
+
/** scanners that did NOT run — their checks are INCONCLUSIVE, never a pass. */
|
|
59
|
+
inconclusive: {
|
|
60
|
+
source: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
}[];
|
|
63
|
+
};
|
|
64
|
+
/** the honesty caveat an assessor should read alongside the statuses. */
|
|
65
|
+
note: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Roll a scan up into a verification summary: every concern classified
|
|
69
|
+
* (fail / not-code-verifiable) and every scanner partitioned into verified (ran)
|
|
70
|
+
* vs inconclusive (skipped — gated, not installed, or unparseable). Pure;
|
|
71
|
+
* `outcomes` is the raw `runScanners` result, `concerns` the deduped,
|
|
72
|
+
* Terraform-only set the assessment reports on.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildVerificationSummary(concerns: Pick<Concern, "id" | "rule_id" | "evidence">[], outcomes: ScannerOutcome[]): VerificationSummary;
|
package/dist/mcp/terraform.d.ts
CHANGED
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* findings — reviewer findings + SARIF ingest/emit
|
|
12
12
|
* plan — plan parsing + destroy/blast/stability/aggregation
|
|
13
13
|
* tools — the MCP Tool factories + their *Params schemas
|
|
14
|
+
* verification — the five-status taxonomy (fail / not-code-verifiable / …)
|
|
15
|
+
* evidence — the backend-free compliance evidence bundle + emitter
|
|
14
16
|
*/
|
|
15
17
|
export * from "#app/mcp/terraform/cost";
|
|
16
18
|
export * from "#app/mcp/terraform/currency";
|
|
17
19
|
export * from "#app/mcp/terraform/decisions";
|
|
20
|
+
export * from "#app/mcp/terraform/evidence";
|
|
18
21
|
export * from "#app/mcp/terraform/findings";
|
|
19
22
|
export * from "#app/mcp/terraform/plan";
|
|
20
23
|
export * from "#app/mcp/terraform/scanners";
|
|
21
24
|
export * from "#app/mcp/terraform/tools";
|
|
22
25
|
export * from "#app/mcp/terraform/types";
|
|
26
|
+
export * from "#app/mcp/terraform/verification";
|