infraweaver 0.3.10 → 0.3.11
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/README.md +1 -1
- package/dist/agents/quotaTokens.d.ts +8 -0
- package/dist/cli.mjs +1843 -1601
- package/dist/index.js +421 -182
- package/dist/internal.js +6 -4
- package/dist/mcp/crosswalk-digest.d.ts +1 -1
- package/dist/mcp/git.d.ts +19 -3
- package/dist/mcp/localContext.d.ts +9 -1
- package/dist/mcp/shell.d.ts +3 -3
- package/dist/mcp/staleFix.d.ts +13 -1
- package/dist/mcp/terraform/hardcodedSecrets.d.ts +49 -0
- package/dist/mcp/terraform/nativeRules.d.ts +1 -0
- package/dist/mcp/terraform/refactorScope.d.ts +12 -0
- package/dist/mcp/terraform/tools/scan.d.ts +1 -0
- package/dist/mcp/terraform/types.d.ts +3 -0
- package/dist/utils/config/payload.d.ts +2 -0
- package/package.json +1 -1
- package/src/agents/claude.ts +2 -1
- package/src/agents/opencode.ts +2 -1
- package/src/agents/quotaTokens.ts +25 -0
- package/src/mcp/crosswalk-digest.ts +1 -1
- package/src/mcp/crosswalk.ts +2 -0
- package/src/mcp/git.ts +44 -3
- package/src/mcp/localContext.ts +8 -0
- package/src/mcp/localServer.ts +3 -0
- package/src/mcp/moduleExtraction.ts +4 -1
- package/src/mcp/shell.ts +2 -2
- package/src/mcp/staleFix.ts +21 -2
- package/src/mcp/terraform/concernResult.ts +24 -4
- package/src/mcp/terraform/hardcodedSecrets.ts +202 -0
- package/src/mcp/terraform/nativeRuleScanners.ts +8 -0
- package/src/mcp/terraform/nativeRules.ts +14 -0
- package/src/mcp/terraform/refactorScope.ts +86 -0
- package/src/mcp/terraform/standardsReport.ts +2 -0
- package/src/mcp/terraform/tools/consolidationCandidates.ts +5 -1
- package/src/mcp/terraform/tools/normalizationCandidates.ts +5 -1
- package/src/mcp/terraform/types.ts +3 -0
- package/src/modes/refactor.ts +2 -0
- package/src/modes/refresh-remediation.ts +1 -1
- package/src/modes/remediate-and-refactor.ts +1 -1
- package/src/modes/remediate.ts +2 -2
- package/src/utils/config/payload.ts +4 -0
package/dist/internal.js
CHANGED
|
@@ -1781,6 +1781,8 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
|
|
|
1781
1781
|
- \`${t("terraform_normalization_candidates")}\` \u2192 in-place idiomatic cleanups that change SYNTAX, not behaviour (redundant whole-string \`"\${expr}"\` interpolation; legacy HCL0.11 map-argument block syntax like \`vars {\` on a \`template_file\`). These relocate NOTHING \u2014 zero \`moved {}\` blocks \u2014 and are proven by the same equivalence check.
|
|
1782
1782
|
- \`${t("terraform_consolidation_candidates")}\` \u2192 the SAME resource shape declared in several root directories (\`env/dev\` + \`env/staging\` + \`env/prod\`), with the single parameterised module they could all call already derived: which attributes are identical (module content) and which differ (per-environment inputs). A member root of a group will ALSO show up as a zero-candidate cluster in \`${t("module_extraction_candidates")}\` \u2014 prefer the consolidation, which does that work once for every environment instead of once per environment. Its \`near_groups\` are the opposite case: roots recognisably the same stack that DIFFER (one carries a resource the other does not), where folding them together is a design decision this mode has no basis for. Do NOT extract a near-group root's shared resources into a module on its own either \u2014 a module one twin calls and the other does not widens exactly the drift the near-group reports. Leave those roots alone and name the near-group and its \`differences\` in your report.
|
|
1783
1783
|
|
|
1784
|
+
**A result marked \`report_only\` is an observation, not work.** The repository is a module other configurations call (\`report_only_reason\` says why), and restructuring it is churn for every caller: name those candidates in your \`${t("report_progress")}\` summary and open no PR for them. \`examples/\` roots are left out of every candidate list unless the operator set \`refactor_examples\`.
|
|
1785
|
+
|
|
1784
1786
|
**Before picking, check the interface.** \`${t("module_extraction_candidates")}\` reports \`unmapped_attributes\` per candidate and a top-level \`partial_interfaces\` list: arguments the raw resources SET that the target module does not. Adopting such a module DROPS them silently \u2014 and the equivalence check cannot catch it, because once the argument is gone it is absent from both sides. Two of these are unrecoverable rather than merely wrong (\`object_lock_enabled\` cannot be set after creation; \`force_destroy\` changes deletion semantics). So: **decline the candidate, or extend the module to expose the argument and say so in the PR body.** Deleting the argument to make the shapes match is never the fix. A candidate whose \`unmapped_attributes\` is \`null\` was NOT checked \u2014 the module is external and its code is not in this repo \u2014 which is not the same as clean.
|
|
1785
1787
|
|
|
1786
1788
|
Pick the highest-value refactor across ALL THREE detectors. Only when they ALL come back empty \u2014 or all that remains is a cluster in a near-group root \u2014 is there genuinely nothing to do: call \`${t("report_progress")}\` with an ACCURATE message \u2014 e.g. "No behaviour-preserving refactor found: no extraction clusters and no idiomatic-normalisation candidates." \u2014 and **stop**. Two anti-patterns to avoid in that message: (i) never call a repo "already idiomatic" while normalisation candidates remain; (ii) when what's left is behaviour-CHANGING modernisation \u2014 a deprecated-resource swap (\`aws_launch_configuration\`\u2192\`aws_launch_template\`, \`aws_elb\`\u2192\`aws_lb\`, \`template_file\`\u2192\`templatefile()\`) \u2014 do NOT report "nothing to refactor": that is real work, but it ALTERS the resource set, so it belongs to Remediate. Say so explicitly rather than implying the code is clean.
|
|
@@ -1867,7 +1869,7 @@ This mode keeps already-open ${PRODUCT_NAME} remediation PRs healthy. A remediat
|
|
|
1867
1869
|
- **resolved-on-base? \u2192 close it**: look for a group in step 3's scan whose \`id\` equals the PR's \`group_id\` (for a by-rule/batch PR, match on the concern ids it covered). If **no** current group/concern corresponds, the concern was already fixed on the base (a human fix, or a base change removed the file) \u2014 the PR is redundant. Call \`${t("close_pull_request")}\` with a one-line \`comment\` explaining it's resolved on the base. Do not push anything for this PR.
|
|
1868
1870
|
- **still present \u2192 re-derive the fix on the current base**:
|
|
1869
1871
|
- **read WHY the CI failed, before changing anything** (\`checks_failing\` PRs only): when step 2 gave the PR a \`failing_check_suite_id\`, call \`${t("get_check_suite_logs")}\` with that id FIRST. Re-deriving without reading the failure is exactly how attempt 2 and attempt 3 become identical repeats of attempt 1 \u2014 same fix, same red check, three times, until the retry cap escalates a PR nobody learned anything about. Use the returned \`failed_steps\` and log excerpt to decide what to do DIFFERENTLY this attempt. If the log shows the failure is not ours \u2014 a broken base, a missing credential, an unrelated flaky job \u2014 do not spend an attempt re-deriving: add \`needs-human\` (\`${t("add_labels")}\`), post ONE comment naming the failing step, and move on. Record the cause in the PR body/comment you write below, so the next attempt and any human start from what you found. When the id is absent, or the tool reports no failed runs, say so in the comment and continue with the plain re-derive rather than guessing.
|
|
1870
|
-
- **branch**: recreate the remediation branch at the current base HEAD via \`${t("git")}\` (\`git checkout -B
|
|
1872
|
+
- **branch**: recreate the remediation branch at the current base HEAD via \`${t("git")}\` (\`git({ command: "checkout", args: ["-B", <the PR's branch>] })\`, the \`branch\` step 2 returned) \u2014 \`-B\` force-resets it to the just-scanned base so the diff is only your fix.
|
|
1871
1873
|
- **fix \u2192 validate \u2192 plan \u2192 keep tests consistent \u2192 prove it**: apply the minimal fix for that group exactly as in **Remediate** step 4 (same \`${t("terraform_validate")}\`, \`${t("terraform_plan")}\`, \`${t("terraform_module_tests")}\`, and \`${t("terraform_verify_remediation")}\` gates, and the same guardrails \u2014 never open/keep a PR whose validate didn't pass, abandon a group that would destroy a stateful resource, etc.).
|
|
1872
1874
|
- **force-update the PR branch**: \`${t("push_branch")}\` with \`force: true\` (the PR already exists; force-updating its branch refreshes it in place \u2014 do NOT open a second PR). The Terraform-only / secret / destroy guardrails still run at push time.
|
|
1873
1875
|
- **refresh the body**: \`${t("update_pull_request_body")}\` rebuilt from the fresh \`${t("terraform_verify_remediation")}\` result (the Remediation PR format below), and add a one-line note that it was rebased onto the current base (\`<short-sha>\`) on this run.
|
|
@@ -1912,8 +1914,8 @@ function remediateMode(t) {
|
|
|
1912
1914
|
|
|
1913
1915
|
4. **for the chosen group**:
|
|
1914
1916
|
- **base branch**: this run's base branch is resolved deterministically \u2014 \`${t("create_pull_request")}\` targets the \`base_branch\` input if set, else the branch the run started on, 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.
|
|
1915
|
-
- **idempotency**: the remediation branch is \`remediate/<group-id
|
|
1916
|
-
- **branch**: create
|
|
1917
|
+
- **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch) \u2014 use it exactly, never a name of your own. 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.
|
|
1918
|
+
- **branch**: create the group's \`branch\` from the **current HEAD** (the checkout that was just scanned) via \`${t("git")}\` (\`git({ command: "checkout", args: ["-b", <branch>] })\`). Do NOT switch to a different base first \u2014 branching from the scanned checkout keeps the PR diff to exactly your fix.
|
|
1917
1919
|
- **honest refusal (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.
|
|
1918
1920
|
- **propose, then let me steer (when there's no single right fix)**: distinct from honest refusal (which refuses a fix a human must *decide*), this 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 \`${COMMENT_COMMAND} 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.
|
|
1919
1921
|
- **reuse a proven fix (optional, do this BEFORE editing)**: call \`${t("terraform_fix_memory")}\`. When proven-fix memory is enabled it returns any patterns whose \`finding_type\` matches this group's concerns \u2014 each a transformation that already passed a \`\u2717\u2192\u2713\` proof on this repo or elsewhere in the fleet, with its \`remediation_hint\` + a \`before_example\`. Use a matching pattern as your STARTING POINT (it saves you rediscovering the approach), but ADAPT it to this repo's own resources \u2014 it is a prior, never a patch to paste. You MUST still run \`${t("terraform_verify_remediation")}\` to prove \`\u2717\u2192\u2713\` here; a pattern is never blind-applied and never substitutes for the per-target proof. Returns an empty list (never an error) when memory is off or nothing matches \u2014 then fix from first principles as usual.
|
|
@@ -1973,7 +1975,7 @@ This mode composes the **Remediate** and **Refactor** verbs into a single run. I
|
|
|
1973
1975
|
- **commit the fix** (\`git add\` only the changed \`*.tf\`/\`*.tfvars\`, a \`fix(tf): \u2026\` message). Do NOT push yet. This committed state is the refactor's equivalence BASELINE.
|
|
1974
1976
|
|
|
1975
1977
|
3. **REFACTOR (phase 2) \u2014 modularise on top of the committed fix**: follow the **Refactor** mode's flow:
|
|
1976
|
-
- \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor. Respect a pinned \`refactor_source\` if one was supplied (it constrains which module source you may use; the behaviour-altering b.4 path is only available when the operator pinned \`third-party\`).
|
|
1978
|
+
- \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor; a result marked \`report_only\` (the repository is a module others call) is named in the summary, never proposed. Respect a pinned \`refactor_source\` if one was supplied (it constrains which module source you may use; the behaviour-altering b.4 path is only available when the operator pinned \`third-party\`).
|
|
1977
1979
|
- resolve the module source, wire the \`module\` call against its real interface (\`${t("terraform_module_interface")}\` for a local module dir, \`${t("terraform_module_lookup")}\` for a registry-sourced one), and emit a \`moved {}\` block for EVERY relocated address (\`${t("terraform_generate_moved")}\`).
|
|
1978
1980
|
- \`terraform fmt\` + \`${t("terraform_validate")}\`, then \u2014 **with the refactor edits still UNCOMMITTED** \u2014 call \`${t("terraform_equivalence_check")}\`. In this mode it diffs the working tree against the COMMITTED fix (not the run-start commit), so it measures only the refactor. It must return \`equivalent: true\` (zero uncovered moves, resource set + arguments preserved, validate + fmt clean) before you proceed; \`${t("push_branch")}\` hard-blocks an unproven one \u2014 and, once this run has added a \`moved {}\` block, it also hard-blocks a push that never ran the check at all, so abandoning the refactor means REVERTING its edits rather than leaving them in with the fix. If it can't be proven equivalent, abandon the refactor and ship the fix alone.
|
|
1979
1981
|
- **document the module (only when the \`docs\` input is enabled)**: after the equivalence check passes, call \`${t("terraform_module_docs")}\` for the module you created/adopted (pass the relocated addresses as \`moves\`). Do not hand-write docs \u2014 the tool builds from the interface, moved blocks, and equivalence verdict.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const EXPECTED_CROSSWALK_DIGEST = "
|
|
1
|
+
export declare const EXPECTED_CROSSWALK_DIGEST = "f583001997c4c69f84f189a14e69e4f6eb2c52bddc04ca7f404286b5280d1ff4";
|
package/dist/mcp/git.d.ts
CHANGED
|
@@ -44,14 +44,30 @@ export declare const NOSHELL_BLOCKED_WRITE_ARGS: string[];
|
|
|
44
44
|
* `-o`, or attached `-o<path>`. */
|
|
45
45
|
export declare function isBlockedWriteArg(arg: string): boolean;
|
|
46
46
|
export declare const Git: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
47
|
-
command
|
|
47
|
+
command?: string;
|
|
48
48
|
args?: string[];
|
|
49
49
|
}, {}>;
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The subcommand and arguments of a call, however the model wrote it: the
|
|
52
|
+
* subcommand alone (the documented form), a whole `git …` command line, or the
|
|
53
|
+
* subcommand as the first of `args`. Models wrote the other two often enough
|
|
54
|
+
* to cost a turn per run, and each is unambiguous. The subcommand is checked
|
|
55
|
+
* against the same pattern the schema uses, so a flag (`-c`, `-C`,
|
|
56
|
+
* `--exec-path`) can no more arrive through `args[0]` or a command line than
|
|
57
|
+
* through `command`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function normalizeGitCall(params: {
|
|
60
|
+
command?: string | undefined;
|
|
61
|
+
args?: string[] | undefined;
|
|
62
|
+
}): {
|
|
51
63
|
command: string;
|
|
64
|
+
args: string[];
|
|
65
|
+
};
|
|
66
|
+
export declare function GitTool(ctx: GitToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
|
|
67
|
+
command?: string;
|
|
52
68
|
args?: string[];
|
|
53
69
|
}, {
|
|
54
|
-
command
|
|
70
|
+
command?: string;
|
|
55
71
|
args?: string[];
|
|
56
72
|
}>> & {
|
|
57
73
|
mutates?: boolean;
|
|
@@ -13,7 +13,7 @@ import type { ResolvedPayload } from "../utils/config/payload.js";
|
|
|
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" | "iacLanguages" | "severityThreshold" | "grouping" | "autonomyThreshold" | "repeatedLiteralThreshold" | "varFiles" | "costIncreaseBlockUsd" | "moduleCatalogue" | "allowReplace" | "allowMajorUpgrade" | "toolsEnabled" | "allowedPaths" | "toolsNetwork" | "gitleaks" | "terratest" | "docs" | "terraformMcp" | "moduleFetchToken" | "moduleFetchSshKey" | "registryToken" | "registryHost" | "opentofuMcp" | "fixMemory">;
|
|
16
|
+
payload: Pick<ResolvedPayload, "cwd" | "scanScope" | "iacLanguages" | "severityThreshold" | "grouping" | "autonomyThreshold" | "repeatedLiteralThreshold" | "varFiles" | "costIncreaseBlockUsd" | "moduleCatalogue" | "allowReplace" | "allowMajorUpgrade" | "toolsEnabled" | "allowedPaths" | "toolsNetwork" | "gitleaks" | "terratest" | "docs" | "terraformMcp" | "moduleFetchToken" | "moduleFetchSshKey" | "registryToken" | "registryHost" | "opentofuMcp" | "fixMemory" | "baseBranch" | "refactorExamples">;
|
|
17
17
|
toolState: ToolState;
|
|
18
18
|
tmpdir: string;
|
|
19
19
|
/** resolved upstream model specifier driving this run's agent (e.g.
|
|
@@ -23,4 +23,12 @@ export interface LocalToolContext {
|
|
|
23
23
|
* bundle's PROVENANCE block — recording WHO produced the evidence, not
|
|
24
24
|
* just what was found. */
|
|
25
25
|
resolvedModel?: string | undefined;
|
|
26
|
+
/** the repository, for its default branch. The Action's full `ToolContext`
|
|
27
|
+
* carries it; a local session has none, so its remediation branches keep the
|
|
28
|
+
* plain `remediate/<id>` name. */
|
|
29
|
+
repo?: {
|
|
30
|
+
data: {
|
|
31
|
+
default_branch?: string | null;
|
|
32
|
+
};
|
|
33
|
+
} | undefined;
|
|
26
34
|
}
|
package/dist/mcp/shell.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ToolContext } from "./server.js";
|
|
2
2
|
export declare const ShellParams: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
3
3
|
command: string;
|
|
4
|
-
description
|
|
4
|
+
description?: string;
|
|
5
5
|
timeout?: number;
|
|
6
6
|
working_directory?: string;
|
|
7
7
|
background?: boolean;
|
|
@@ -38,13 +38,13 @@ export declare function createCappedBuffer(limit: number): {
|
|
|
38
38
|
};
|
|
39
39
|
export declare function ShellTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
|
|
40
40
|
command: string;
|
|
41
|
-
description
|
|
41
|
+
description?: string;
|
|
42
42
|
timeout?: number;
|
|
43
43
|
working_directory?: string;
|
|
44
44
|
background?: boolean;
|
|
45
45
|
}, {
|
|
46
46
|
command: string;
|
|
47
|
-
description
|
|
47
|
+
description?: string;
|
|
48
48
|
timeout?: number;
|
|
49
49
|
working_directory?: string;
|
|
50
50
|
background?: boolean;
|
package/dist/mcp/staleFix.d.ts
CHANGED
|
@@ -2,8 +2,20 @@ import type { GitHubToolContext } from "./capabilityContext.js";
|
|
|
2
2
|
/** true when `branch` is a Infraweaver remediation/generation branch. */
|
|
3
3
|
export declare function isRemediationBranch(branch: string): boolean;
|
|
4
4
|
/** the `<group-id>` of a `remediate/<group-id>` branch (the scan group id that
|
|
5
|
-
* keys the fix), or null for a generation branch / non-remediation branch.
|
|
5
|
+
* keys the fix), or null for a generation branch / non-remediation branch. A
|
|
6
|
+
* `--<base>` suffix (see [[remediationBranchSuffix]]) is not part of the id. */
|
|
6
7
|
export declare function groupIdFromBranch(branch: string): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* What a remediation branch name carries after its group id: nothing when the
|
|
10
|
+
* PR targets the repository default branch, `--<base>` otherwise.
|
|
11
|
+
*
|
|
12
|
+
* The group id is a hash of the file (or rule) alone, so the same fix aimed at
|
|
13
|
+
* two base branches — or at two repos whose roots are both `main.tf` — named
|
|
14
|
+
* the same branch, and a run that found it taken improvised another name the
|
|
15
|
+
* refresh and concurrency tools could not see. A default-branch fix keeps the
|
|
16
|
+
* plain name, so the PRs already open under it are still found and refreshed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function remediationBranchSuffix(base: string | undefined, defaultBranch: string | undefined): string;
|
|
7
19
|
/** true when a commit-author LOGIN is Infraweaver's bot (so a commit by it is NOT
|
|
8
20
|
* a human edit). A null/absent login is treated as non-human here for backward
|
|
9
21
|
* compatibility of this login-only helper — but prefer {@link isBotCommit}, which
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential written into the configuration as a string literal, by the
|
|
3
|
+
* name of the argument that holds it.
|
|
4
|
+
*
|
|
5
|
+
* WHY BY NAME. The secret scanners look at the VALUE — a known key format, or a
|
|
6
|
+
* string with enough entropy — so a hand-made password (`"cg-flagshop-Pr0d!…"`)
|
|
7
|
+
* slips past all of them. Terraform says more than a text scanner can see: the
|
|
8
|
+
* value sits in an argument, a map key or a variable whose name says what it
|
|
9
|
+
* holds. A literal in `password`, `client_secret` or a Lambda environment
|
|
10
|
+
* variable called `auth` is in the repository whatever it looks like.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IS READ. Every `name = "…"` line inside a `resource`, `data`,
|
|
13
|
+
* `provider`, `module` or `locals` block, at any depth, so
|
|
14
|
+
* `environment { variables = { auth = "…" } }` and `jsonencode({ … })` objects
|
|
15
|
+
* are reached; and the `default` of a `variable` whose name is a secret's. Not
|
|
16
|
+
* read: comments, heredoc bodies, a name/value pair split across two keys
|
|
17
|
+
* (`[{ name = "PASSWORD", value = "…" }]`), and a value that starts on a later
|
|
18
|
+
* line than its name.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS NOT A FINDING. A reference, a function call, an interpolation, an
|
|
21
|
+
* empty string, or a placeholder made only of `*` / `x` / `<…>`. Neither is an
|
|
22
|
+
* argument whose name only mentions a secret (`password_length`, `secret_name`,
|
|
23
|
+
* `token_validity_units`, `creation_token`): the last word of the name decides.
|
|
24
|
+
*
|
|
25
|
+
* Arguments the provider can take write-only (`aws_db_instance.password`) may
|
|
26
|
+
* also be reported by `secret-persisted-to-state`, which says something else:
|
|
27
|
+
* that the value reaches state. This one says it is in git.
|
|
28
|
+
*/
|
|
29
|
+
import type { SourceFile } from "./tree.js";
|
|
30
|
+
import { type Concern } from "./types.js";
|
|
31
|
+
/** True when an argument, map key or variable called `name` holds a secret. */
|
|
32
|
+
export declare function isSecretName(name: string): boolean;
|
|
33
|
+
export interface HardcodedSecret {
|
|
34
|
+
/** the argument, map key or variable name. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** what holds it: `resource "aws_lambda_function" "x"`, `variable "db_password"`, … */
|
|
37
|
+
holder: string;
|
|
38
|
+
line: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Every secret-named literal in one file. Pure.
|
|
42
|
+
*
|
|
43
|
+
* One pass over the lines rather than a block parse: every assignment line is
|
|
44
|
+
* the same question whatever block it sits in, and the native pack's block
|
|
45
|
+
* parser is shared by every scanner and budgeted (see nativeScan.perf.test).
|
|
46
|
+
*/
|
|
47
|
+
export declare function findHardcodedSecrets(hcl: string): HardcodedSecret[];
|
|
48
|
+
/** Scan an already-read tree into native concerns. Pure; no I/O of its own. */
|
|
49
|
+
export declare function scanHardcodedSecrets(files: readonly SourceFile[]): Concern[];
|
|
@@ -20,6 +20,7 @@ export interface NativeRule {
|
|
|
20
20
|
}
|
|
21
21
|
export declare const SECRET_PERSISTED_TO_STATE = "secret-persisted-to-state";
|
|
22
22
|
export declare const COMMITTED_STATE_FILE = "committed-state-file";
|
|
23
|
+
export declare const HARDCODED_SECRET_ARGUMENT = "hardcoded-secret-argument";
|
|
23
24
|
export declare const IMPERATIVE_ACTION_TRIGGER = "imperative-action-trigger";
|
|
24
25
|
export declare const DYNAMIC_DESTROY_PROTECTION = "dynamic-destroy-protection";
|
|
25
26
|
export declare const DEPRECATED_STATE_LOCKING = "deprecated-state-locking";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** true for a path inside an `examples/` (or `example/`) directory, at any depth. */
|
|
2
|
+
export declare function isExamplePath(path: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Why the repository at `cwd` reads as a reusable module rather than a
|
|
5
|
+
* deployment, or null when it does not.
|
|
6
|
+
*/
|
|
7
|
+
export declare function libraryRepoReason(cwd: string): string | null;
|
|
8
|
+
/** what a detector tool adds to its result when the repository is a library. */
|
|
9
|
+
export declare function reportOnly(reason: string | null): {
|
|
10
|
+
report_only?: true;
|
|
11
|
+
report_only_reason?: string;
|
|
12
|
+
};
|
|
@@ -242,6 +242,9 @@ export interface ConcernGroup {
|
|
|
242
242
|
/** stable id — the remediation branch/PR key (`remediate/<id>`). Derived from
|
|
243
243
|
* the file (by-file grouping) or the rule (by-rule grouping). */
|
|
244
244
|
id: string;
|
|
245
|
+
/** the branch its fix is pushed to: `remediate/<id>`, plus `--<base>` when the
|
|
246
|
+
* PR targets a branch other than the repository default. */
|
|
247
|
+
branch?: string;
|
|
245
248
|
/** the group's primary file (by-file) or a human label like "3 files"
|
|
246
249
|
* (by-rule); `files` carries the full list for by-rule groups. */
|
|
247
250
|
file: string;
|
|
@@ -64,6 +64,7 @@ export declare const Inputs: import("arktype/internal/variants/object.ts").Objec
|
|
|
64
64
|
registry_token?: string | undefined;
|
|
65
65
|
registry_host?: string | undefined;
|
|
66
66
|
refactor_source?: string | undefined;
|
|
67
|
+
refactor_examples?: string | undefined;
|
|
67
68
|
review_instructions?: string | undefined;
|
|
68
69
|
fp_filtering_instructions?: string | undefined;
|
|
69
70
|
remediation_instructions?: string | undefined;
|
|
@@ -151,6 +152,7 @@ managedCredentials?: {
|
|
|
151
152
|
registryToken: string | undefined;
|
|
152
153
|
registryHost: string | undefined;
|
|
153
154
|
refactorSource: import("../prompt/promptDirectives.js").RefactorSource | undefined;
|
|
155
|
+
refactorExamples: boolean;
|
|
154
156
|
remediationCommand: import("../github/remediationCommand.js").RemediationCommand | null;
|
|
155
157
|
baseBranch: string | undefined;
|
|
156
158
|
allowReplace: string[] | undefined;
|
package/package.json
CHANGED
package/src/agents/claude.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
getUnsubmittedReview,
|
|
61
61
|
} from "#app/agents/postRun";
|
|
62
62
|
import { buildGracePrompt, buildGraceQuotas, shouldRunGraceTurn } from "#app/agents/quotaReserve";
|
|
63
|
+
import { quotaTokens } from "#app/agents/quotaTokens";
|
|
63
64
|
import { REVIEWER_AGENT_NAME, REVIEWER_SYSTEM_PROMPT } from "#app/agents/reviewer";
|
|
64
65
|
import { ORCHESTRATOR_LABEL, SessionLabeler, withLabel } from "#app/agents/sessionLabeler";
|
|
65
66
|
import {
|
|
@@ -637,7 +638,7 @@ function timerFor(state: ClaudeRunState, label: string): ThinkingTimer {
|
|
|
637
638
|
function chargeTokenQuota(state: ClaudeRunState): void {
|
|
638
639
|
const quota = state.params.tokenQuota;
|
|
639
640
|
if (!quota || state.stopRun.signal.aborted) return;
|
|
640
|
-
if (!quota.observe(state.quotaSource, state.accumulatedTokens)) return;
|
|
641
|
+
if (!quota.observe(state.quotaSource, quotaTokens(state.accumulatedTokens))) return;
|
|
641
642
|
log.info(`» ${state.params.label}: ${quota.gap()}`);
|
|
642
643
|
state.stopRun.abort(new TokenQuotaExceededError(quota.gap()));
|
|
643
644
|
}
|
package/src/agents/opencode.ts
CHANGED
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
runPostRunRetryLoop,
|
|
86
86
|
shouldRunReflection,
|
|
87
87
|
} from "#app/agents/postRun";
|
|
88
|
+
import { quotaTokens } from "#app/agents/quotaTokens";
|
|
88
89
|
import { REVIEWER_AGENT_NAME } from "#app/agents/reviewer";
|
|
89
90
|
import { ORCHESTRATOR_LABEL, SessionLabeler, withLabel } from "#app/agents/sessionLabeler";
|
|
90
91
|
import {
|
|
@@ -458,7 +459,7 @@ export function newTurn(): TurnAccumulator {
|
|
|
458
459
|
export function chargeTokenQuota(ctx: RunnerContext): void {
|
|
459
460
|
const quota = ctx.tokenQuota;
|
|
460
461
|
if (!quota || !ctx.currentTurn || ctx.tokenQuotaExceeded) return;
|
|
461
|
-
if (!quota.observe(ctx.currentTurn.quotaSource, ctx.currentTurn.tokens)) return;
|
|
462
|
+
if (!quota.observe(ctx.currentTurn.quotaSource, quotaTokens(ctx.currentTurn.tokens))) return;
|
|
462
463
|
ctx.tokenQuotaExceeded = true;
|
|
463
464
|
log.info(`» ${ctx.label}: ${quota.gap()}`);
|
|
464
465
|
ctx.abortRun?.(new TokenQuotaExceededError(quota.gap()));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one run's token usage counts against its token quota.
|
|
3
|
+
*
|
|
4
|
+
* Every turn re-reads the whole conversation from the prompt cache, so on a
|
|
5
|
+
* long-context run cache reads are almost all of the volume: a Remediate run on
|
|
6
|
+
* a 68 KB `main.tf` reached 8.5M tokens in four minutes of ordinary progress,
|
|
7
|
+
* ~95% of it cache reads. Counted 1:1, the quota stopped legitimate work long
|
|
8
|
+
* before it could stop a runaway. A cache read is billed at a tenth of an input
|
|
9
|
+
* token, so it is weighted the same here: a loop that keeps re-reading a large
|
|
10
|
+
* context still reaches the ceiling, and its cost — what the quota exists to
|
|
11
|
+
* bound — is measured as it is billed.
|
|
12
|
+
*/
|
|
13
|
+
const CACHE_READ_WEIGHT = 0.1;
|
|
14
|
+
|
|
15
|
+
export interface TokenUsage {
|
|
16
|
+
input: number;
|
|
17
|
+
output: number;
|
|
18
|
+
cacheRead: number;
|
|
19
|
+
cacheWrite: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The pre-summed total the run's token quota observes. */
|
|
23
|
+
export function quotaTokens(t: TokenUsage): number {
|
|
24
|
+
return t.input + t.output + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT;
|
|
25
|
+
}
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// verified fail-closed before any compliance verdict is produced; see crosswalk.ts.
|
|
4
4
|
// Regenerate this file whenever the crosswalk tables in crosswalk.ts change intentionally.
|
|
5
5
|
export const EXPECTED_CROSSWALK_DIGEST =
|
|
6
|
-
"
|
|
6
|
+
"f583001997c4c69f84f189a14e69e4f6eb2c52bddc04ca7f404286b5280d1ff4";
|
package/src/mcp/crosswalk.ts
CHANGED
|
@@ -939,6 +939,8 @@ const THEME_LOCKED: ReadonlyMap<string, string> = new Map([
|
|
|
939
939
|
// does NOT also cite `encryption-at-rest`: whether the repository is encrypted is
|
|
940
940
|
// not what the finding establishes.
|
|
941
941
|
["committed-state-file", "secrets-management"],
|
|
942
|
+
// a credential in the code is the same CIS 16.4 failure one step earlier than state in git
|
|
943
|
+
["hardcoded-secret-argument", "secrets-management"],
|
|
942
944
|
]);
|
|
943
945
|
|
|
944
946
|
/** the bare rule id, with any `<source>:` namespace stripped. */
|
package/src/mcp/git.ts
CHANGED
|
@@ -936,11 +936,53 @@ const subcommandPattern = regex("^[a-z][a-z0-9-]*$");
|
|
|
936
936
|
|
|
937
937
|
// exported so the security tests validate against the REAL schema rather than a
|
|
938
938
|
// second copy of the pattern that could drift from it.
|
|
939
|
+
// a whole command line in `command` (`git checkout -b fix`), the way the shell
|
|
940
|
+
// tool takes one. Its subcommand is held to the same pattern once split.
|
|
941
|
+
const commandLinePattern = regex("^git\\s+[a-z][a-z0-9-]*(?:\\s.*)?$");
|
|
942
|
+
|
|
939
943
|
export const Git = type({
|
|
940
|
-
command: type(subcommandPattern)
|
|
944
|
+
command: type(subcommandPattern)
|
|
945
|
+
.or(type(commandLinePattern))
|
|
946
|
+
.describe("Git command (e.g., 'status', 'log', 'diff')")
|
|
947
|
+
.optional(),
|
|
941
948
|
args: type.string.array().describe("Additional arguments for the git command").optional(),
|
|
942
949
|
});
|
|
943
950
|
|
|
951
|
+
/** a command line split into words, honouring single and double quotes. */
|
|
952
|
+
function splitWords(line: string): string[] {
|
|
953
|
+
return [...line.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* The subcommand and arguments of a call, however the model wrote it: the
|
|
958
|
+
* subcommand alone (the documented form), a whole `git …` command line, or the
|
|
959
|
+
* subcommand as the first of `args`. Models wrote the other two often enough
|
|
960
|
+
* to cost a turn per run, and each is unambiguous. The subcommand is checked
|
|
961
|
+
* against the same pattern the schema uses, so a flag (`-c`, `-C`,
|
|
962
|
+
* `--exec-path`) can no more arrive through `args[0]` or a command line than
|
|
963
|
+
* through `command`.
|
|
964
|
+
*/
|
|
965
|
+
export function normalizeGitCall(params: {
|
|
966
|
+
command?: string | undefined;
|
|
967
|
+
args?: string[] | undefined;
|
|
968
|
+
}): {
|
|
969
|
+
command: string;
|
|
970
|
+
args: string[];
|
|
971
|
+
} {
|
|
972
|
+
let words: string[];
|
|
973
|
+
if (params.command === undefined) words = [...(params.args ?? [])];
|
|
974
|
+
else if (/^git\s/.test(params.command))
|
|
975
|
+
words = [...splitWords(params.command).slice(1), ...(params.args ?? [])];
|
|
976
|
+
else return { command: params.command, args: params.args ?? [] };
|
|
977
|
+
const [command, ...args] = words[0] === "git" ? words.slice(1) : words;
|
|
978
|
+
if (command === undefined || !/^[a-z][a-z0-9-]*$/.test(command)) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`command must be a Git subcommand such as 'status' or 'checkout' (was "${command ?? ""}")`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return { command, args };
|
|
984
|
+
}
|
|
985
|
+
|
|
944
986
|
export function GitTool(ctx: GitToolContext) {
|
|
945
987
|
return tool({
|
|
946
988
|
name: "git",
|
|
@@ -958,8 +1000,7 @@ export function GitTool(ctx: GitToolContext) {
|
|
|
958
1000
|
"git pull is not available — use git_fetch then this tool with command 'merge'.",
|
|
959
1001
|
parameters: Git,
|
|
960
1002
|
execute: execute(async (params) => {
|
|
961
|
-
const command = params
|
|
962
|
-
const args = params.args ?? [];
|
|
1003
|
+
const { command, args } = normalizeGitCall(params);
|
|
963
1004
|
|
|
964
1005
|
// guard: {command:"status",args:["status"]} → `git status status`, where
|
|
965
1006
|
// git silently treats args[0] as a pathspec. when nothing matches the
|
package/src/mcp/localContext.ts
CHANGED
|
@@ -62,6 +62,10 @@ export interface LocalToolContext {
|
|
|
62
62
|
// the local stdio MCP server too, so a CLI/IDE session benefits from a
|
|
63
63
|
// committed `.infraweaver.fix-library.json`.
|
|
64
64
|
| "fixMemory"
|
|
65
|
+
// the PR base a remediation branch is named for (see remediationBranchSuffix)
|
|
66
|
+
| "baseBranch"
|
|
67
|
+
// whether Refactor may propose changes inside `examples/` (refactorScope)
|
|
68
|
+
| "refactorExamples"
|
|
65
69
|
>;
|
|
66
70
|
toolState: ToolState;
|
|
67
71
|
tmpdir: string;
|
|
@@ -72,4 +76,8 @@ export interface LocalToolContext {
|
|
|
72
76
|
* bundle's PROVENANCE block — recording WHO produced the evidence, not
|
|
73
77
|
* just what was found. */
|
|
74
78
|
resolvedModel?: string | undefined;
|
|
79
|
+
/** the repository, for its default branch. The Action's full `ToolContext`
|
|
80
|
+
* carries it; a local session has none, so its remediation branches keep the
|
|
81
|
+
* plain `remediate/<id>` name. */
|
|
82
|
+
repo?: { data: { default_branch?: string | null } } | undefined;
|
|
75
83
|
}
|
package/src/mcp/localServer.ts
CHANGED
|
@@ -111,6 +111,9 @@ export function buildLocalContext(options: LocalMcpOptions): LocalToolContext {
|
|
|
111
111
|
// blocked_destructive informationally with push_will_block: false.
|
|
112
112
|
allowReplace: undefined,
|
|
113
113
|
allowMajorUpgrade: undefined,
|
|
114
|
+
// no PR surface, so remediation branches keep their plain names
|
|
115
|
+
baseBranch: undefined,
|
|
116
|
+
refactorExamples: false,
|
|
114
117
|
moduleCatalogue: options.moduleCatalogue,
|
|
115
118
|
toolsEnabled: parseToolSelection(options.toolsEnabled),
|
|
116
119
|
// no allowed_paths input on the local server, so the one writing tool
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
readFetchedGitModules,
|
|
43
43
|
readFetchedRegistryModuleDirs,
|
|
44
44
|
} from "#app/mcp/terraform/refactor/workspace";
|
|
45
|
+
import { isExamplePath, libraryRepoReason, reportOnly } from "#app/mcp/terraform/refactorScope";
|
|
45
46
|
import { scanSessionOf } from "#app/mcp/terraform/scanSession";
|
|
46
47
|
import { log } from "#app/utils/cli";
|
|
47
48
|
import { pushInto } from "#app/utils/collect";
|
|
@@ -915,7 +916,8 @@ export function ModuleExtractionCandidatesTool(ctx: LocalToolContext) {
|
|
|
915
916
|
cwd,
|
|
916
917
|
ctx.payload.moduleCatalogue,
|
|
917
918
|
ctx.toolState.orgModuleHcl,
|
|
918
|
-
);
|
|
919
|
+
).filter((c) => ctx.payload.refactorExamples || !isExamplePath(c.cluster.file));
|
|
920
|
+
const library = libraryRepoReason(cwd);
|
|
919
921
|
const matched = candidates.filter((c) => c.candidates.length > 0);
|
|
920
922
|
// Record every candidate's reason, keyed by source, for create_pull_request:
|
|
921
923
|
// when a refactor PR's diff adopts one of these modules and the agent left
|
|
@@ -943,6 +945,7 @@ export function ModuleExtractionCandidatesTool(ctx: LocalToolContext) {
|
|
|
943
945
|
: ""),
|
|
944
946
|
);
|
|
945
947
|
return toolOk({
|
|
948
|
+
...reportOnly(library),
|
|
946
949
|
cluster_count: candidates.length,
|
|
947
950
|
matched_count: matched.length,
|
|
948
951
|
candidates,
|
package/src/mcp/shell.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { log } from "#app/utils/log";
|
|
|
17
17
|
|
|
18
18
|
export const ShellParams = type({
|
|
19
19
|
command: "string",
|
|
20
|
-
description: "string",
|
|
20
|
+
"description?": "string",
|
|
21
21
|
"timeout?": type.number.describe(
|
|
22
22
|
"Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately.",
|
|
23
23
|
),
|
|
@@ -455,7 +455,7 @@ export function ShellTool(ctx: ToolContext) {
|
|
|
455
455
|
return tool({
|
|
456
456
|
name: "shell",
|
|
457
457
|
timeoutMs: 120_000,
|
|
458
|
-
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
458
|
+
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets. NOT for git: any command running \`git\` is refused here — use the git tool (and push_branch / git_fetch).
|
|
459
459
|
|
|
460
460
|
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
461
461
|
|
package/src/mcp/staleFix.ts
CHANGED
|
@@ -38,9 +38,28 @@ export function isRemediationBranch(branch: string): boolean {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** the `<group-id>` of a `remediate/<group-id>` branch (the scan group id that
|
|
41
|
-
* keys the fix), or null for a generation branch / non-remediation branch.
|
|
41
|
+
* keys the fix), or null for a generation branch / non-remediation branch. A
|
|
42
|
+
* `--<base>` suffix (see [[remediationBranchSuffix]]) is not part of the id. */
|
|
42
43
|
export function groupIdFromBranch(branch: string): string | null {
|
|
43
|
-
return branch.match(/^remediate\/(
|
|
44
|
+
return branch.match(/^remediate\/(.+?)(?:--.+)?$/)?.[1] ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What a remediation branch name carries after its group id: nothing when the
|
|
49
|
+
* PR targets the repository default branch, `--<base>` otherwise.
|
|
50
|
+
*
|
|
51
|
+
* The group id is a hash of the file (or rule) alone, so the same fix aimed at
|
|
52
|
+
* two base branches — or at two repos whose roots are both `main.tf` — named
|
|
53
|
+
* the same branch, and a run that found it taken improvised another name the
|
|
54
|
+
* refresh and concurrency tools could not see. A default-branch fix keeps the
|
|
55
|
+
* plain name, so the PRs already open under it are still found and refreshed.
|
|
56
|
+
*/
|
|
57
|
+
export function remediationBranchSuffix(
|
|
58
|
+
base: string | undefined,
|
|
59
|
+
defaultBranch: string | undefined,
|
|
60
|
+
): string {
|
|
61
|
+
if (!base || !defaultBranch || base === defaultBranch) return "";
|
|
62
|
+
return `--${base.replace(/[^A-Za-z0-9._-]+/g, "-")}`;
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
/** core matcher: a NON-EMPTY identity string (a GitHub login or a git author
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
2
|
+
import { remediationBranchSuffix } from "#app/mcp/staleFix";
|
|
2
3
|
import {
|
|
3
4
|
capSeededConcerns,
|
|
4
5
|
type PrioritySignals,
|
|
@@ -65,6 +66,18 @@ export function annotateConcerns(
|
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** The branch this run's PRs target, as `create_pull_request` resolves it: the
|
|
70
|
+
* `base_branch` input, else the branch the run started on, else the default. */
|
|
71
|
+
function remediationBaseOf(ctx: LocalToolContext): string | undefined {
|
|
72
|
+
const head = ctx.toolState.initialHead;
|
|
73
|
+
return (
|
|
74
|
+
ctx.payload.baseBranch?.trim() ||
|
|
75
|
+
(head?.kind === "branch" ? head.name : undefined) ||
|
|
76
|
+
ctx.repo?.data.default_branch?.trim() ||
|
|
77
|
+
undefined
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
/**
|
|
69
82
|
* The shared concerns→result pipeline behind `terraform_scan` and `read_findings`:
|
|
70
83
|
* capture the line-pinned + line-independent baseline (so verify's regression diff
|
|
@@ -141,10 +154,17 @@ export function buildConcernResult(
|
|
|
141
154
|
active,
|
|
142
155
|
opts.autonomyThreshold,
|
|
143
156
|
);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
const suffix = remediationBranchSuffix(
|
|
158
|
+
remediationBaseOf(ctx),
|
|
159
|
+
ctx.repo?.data.default_branch?.trim() || undefined,
|
|
160
|
+
);
|
|
161
|
+
const groups = (
|
|
162
|
+
opts.priority ? prioritizeGroups(annotatedGroups, active, opts.priority) : annotatedGroups
|
|
163
|
+
).map((g) => ({ ...g, branch: `remediate/${g.id}${suffix}` }));
|
|
164
|
+
const planned = planBatches(groups);
|
|
165
|
+
const batchPlan = planned.batch_branch
|
|
166
|
+
? { ...planned, batch_branch: `${planned.batch_branch}${suffix}` }
|
|
167
|
+
: planned;
|
|
148
168
|
|
|
149
169
|
const bySeverity: Record<string, number> = {};
|
|
150
170
|
for (const c of active) bySeverity[c.severity] = (bySeverity[c.severity] ?? 0) + 1;
|