@clear-capabilities/agentic-security-scanner 0.124.1 → 0.128.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/CHANGELOG.md +206 -0
- package/bin/agentic-security.js +75 -2
- package/dist/11.index.js +353 -0
- package/dist/113.index.js +525 -0
- package/dist/178.index.js +1 -1
- package/dist/220.index.js +193 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +2406 -0
- package/dist/449.index.js +135 -0
- package/dist/637.index.js +1 -1
- package/dist/752.index.js +7 -4
- package/dist/801.index.js +87 -0
- package/dist/826.index.js +4 -1
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +1 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -6
- package/src/engine.js +31 -1
- package/src/integrations/tickets.js +9 -3
- package/src/ir/CLAUDE.md +22 -17
- package/src/llm-validator/index.js +47 -12
- package/src/mcp/tools.js +108 -3
- package/src/posture/CLAUDE.md +10 -1
- package/src/posture/cache-economics.js +7 -4
- package/src/posture/deterministic-fix.js +65 -0
- package/src/posture/entrypoint-inventory.js +248 -0
- package/src/posture/falsification.js +121 -0
- package/src/posture/fix-honesty-gate.js +175 -0
- package/src/posture/fix-verify.js +18 -3
- package/src/posture/model-routing.js +126 -0
- package/src/posture/mttr.js +25 -0
- package/src/posture/provider-catalog.js +108 -0
- package/src/posture/root-cause-sweep.js +262 -0
- package/src/posture/secret-live-check.js +71 -0
- package/src/pr-comment.js +3 -1
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/api-authz.js +36 -0
- package/src/sast/file-upload.js +118 -0
- package/src/sast/llm-cost-advisor.js +88 -0
- package/src/util/untrusted.js +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,211 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.128.1 — patch dependency vulnerabilities (11 Dependabot alerts → 0)
|
|
4
|
+
|
|
5
|
+
Security maintenance. Cleared all 11 open Dependabot alerts by updating the two lockfiles to
|
|
6
|
+
patched versions (all within-major bumps, no breakage):
|
|
7
|
+
|
|
8
|
+
- **`scanner/`** — `js-yaml` 4.1 → 4.3.0 (GHSA-h67p-54hq-rp68, quadratic-complexity DoS via merge
|
|
9
|
+
keys). `js-yaml` is inlined into the shipped bundle, so `dist/agentic-security.mjs` was rebuilt;
|
|
10
|
+
full gate re-run green (`npm test` 1695/0, cve-replay 185/185).
|
|
11
|
+
- **`ide/vscode/`** — `undici` → 7.28.0 (incl. one high), `form-data` → 4.0.6 (high),
|
|
12
|
+
`markdown-it` → 14.3.0, `esbuild` → 0.28.1, `js-yaml` → 4.3.0. All transitive under
|
|
13
|
+
`@vscode/vsce`/`esbuild`; lockfile-only, `npm audit` now reports 0.
|
|
14
|
+
|
|
15
|
+
`npm audit` is clean (0 vulnerabilities) in both packages.
|
|
16
|
+
|
|
17
|
+
## 0.128.0 — the agentic methodology layer + a simpler command surface
|
|
18
|
+
|
|
19
|
+
Two things landed together this release: a set of default-on **methodology annotators** that
|
|
20
|
+
layer agentic-hunter discipline on top of the deterministic engine, and a **consolidation** of
|
|
21
|
+
the command and skill surface so there's less to remember.
|
|
22
|
+
|
|
23
|
+
**Methodology layer (7 additions, all v1, all tested — see `docs/AGENTIC_METHODOLOGY_PRD.md`):**
|
|
24
|
+
|
|
25
|
+
- **Default falsification pass** (`posture/falsification.js`) — for each taint finding it tries
|
|
26
|
+
to *disprove* the finding by locating a context-matched control on the path, and demotes +
|
|
27
|
+
quarantines the ones it can block. Recall-preserving (never removes a finding, never touches
|
|
28
|
+
severity — like the proof gate); genuine cve-replay `pre` vulns still fire (0 false blocks,
|
|
29
|
+
corpus 185/185 intact). Opt out with `AGENTIC_SECURITY_NO_FALSIFICATION=1`.
|
|
30
|
+
- **Attack-surface completeness inventory** (`posture/entrypoint-inventory.js`) — enumerates
|
|
31
|
+
every entry point (HTTP / queue / cron / CLI / env / upload / webhook) with a disposition
|
|
32
|
+
each, on `scan.entrypointInventory`.
|
|
33
|
+
- **Root-cause sweep** (`posture/root-cause-sweep.js`) — from a confirmed finding, searches the
|
|
34
|
+
repo for sibling instances detectors missed, with a `found = candidates + mitigated`
|
|
35
|
+
accounting invariant, on `scan.rootCauseSweep`.
|
|
36
|
+
- **Meta-security hardening** (`util/untrusted.js` + `docs/AGENT_THREAT_MODEL.md`) — a tested
|
|
37
|
+
threat model treating attacker-authored finding text as untrusted input; escaping wired into
|
|
38
|
+
the PR/issue/ticket render paths.
|
|
39
|
+
- **Capability-based model routing** (`posture/model-routing.js`) — stamps `finding.dispatchModel`
|
|
40
|
+
(strongest for crypto/auth/critical, mid for injection, cheapest for low-sev hardening) for
|
|
41
|
+
cost-sensitive subagent dispatch.
|
|
42
|
+
- **Self-improving recall harness** (`bench/realworld-recall/`) — LLM-judged (offline-degrading)
|
|
43
|
+
recall on real repos + a miss-analyzer that names the pipeline stage that dropped a finding
|
|
44
|
+
and proposes the fix. Bench-only; never in the product scan path.
|
|
45
|
+
- **Deterministic fix-honesty gates** (`posture/fix-honesty-gate.js`) — a residual-risk
|
|
46
|
+
hand-wave guard, a cited-file:line requirement for FP/safe verdicts, and FULL/MITIGATION/
|
|
47
|
+
WORKAROUND completeness tiers; the previously-orphaned test loop is now wired into
|
|
48
|
+
`apply_fix` behind `AGENTIC_SECURITY_FIX_RUN_TESTS=1`.
|
|
49
|
+
|
|
50
|
+
**Simpler surface (no functionality removed — everything folds to a mode + alias):**
|
|
51
|
+
|
|
52
|
+
- Commands **12 → 10**: `/ci` folded into `/setup --ci` (+ new `/setup --predeploy`), and
|
|
53
|
+
`/three-agent-review` into `/triage --deep`. Old names still resolve via the
|
|
54
|
+
legacy-alias-redirect hook.
|
|
55
|
+
- Skills **11 → 7**: the four write-time guards merged into `secure-coding-guard`, and the two
|
|
56
|
+
explainers into `security-explain`.
|
|
57
|
+
|
|
58
|
+
**Docs:** README + ARCHITECTURE + HARNESS_COMPATIBILITY refreshed with accurate surface counts
|
|
59
|
+
(17 MCP tools, 10 commands, 7 skills, 5 hook events, 9 sub-agents); the SAST/SCA improvement PRD
|
|
60
|
+
audited and marked (16 of 25 shipped, 9 partial). Full `npm test` green (1695 tests); cve-replay
|
|
61
|
+
corpus 185/185, no drift.
|
|
62
|
+
|
|
63
|
+
## 0.127.0 — cost advisor: an actual choice, not just a tip
|
|
64
|
+
|
|
65
|
+
The model-cost advisor (`hooks/model-cost-advisor.js`) has always been advisory
|
|
66
|
+
only — a `systemMessage` tip you read and act on yourself, by design, because a
|
|
67
|
+
`UserPromptSubmit` hook cannot pause for interactive input or change the model/
|
|
68
|
+
effort (verified against the current Claude Code hooks reference before writing
|
|
69
|
+
any of this, not assumed from the hook's own pre-existing comments). This
|
|
70
|
+
release adds a real choice on top of that hard limit, without giving up the
|
|
71
|
+
zero-token guarantee for anyone who doesn't opt in.
|
|
72
|
+
|
|
73
|
+
- **New opt-in `"interactive": true`** (`.agentic-security/model-optimizer.json`,
|
|
74
|
+
default `false`). On a qualifying prompt, the hook now additionally emits
|
|
75
|
+
`hookSpecificOutput.additionalContext` — billed as input tokens, unlike the
|
|
76
|
+
free `systemMessage` — directing Claude to call `AskUserQuestion` with three
|
|
77
|
+
options: keep current defaults; get the exact `/model`/`/effort` command to
|
|
78
|
+
run yourself (Claude cannot switch its own running model — no exception
|
|
79
|
+
exists); or apply the cheaper model/effort to Claude's own delegated
|
|
80
|
+
Task/Agent-tool sub-agent dispatches for the rest of the session — the one
|
|
81
|
+
axis Claude can genuinely act on directly. Every other install keeps today's
|
|
82
|
+
zero-token behavior unchanged; only projects that explicitly opt in pay for
|
|
83
|
+
the interactive path, and only on prompts that already qualify for a tip.
|
|
84
|
+
- The chosen sub-agent override is **sticky for the session** — persisted to
|
|
85
|
+
`.agentic-security/model-optimizer-state.json` (extending the existing state
|
|
86
|
+
file rather than adding a new one) and cleared at the next `SessionStart`,
|
|
87
|
+
which already does a bare overwrite of that file. A cooldown
|
|
88
|
+
(`interactiveCooldownTurns`, default 3) stops the directive from re-firing
|
|
89
|
+
its token cost if Claude doesn't act on it (e.g. a non-interactive/scripted
|
|
90
|
+
invocation).
|
|
91
|
+
- Fixed a real bug the new shape would otherwise have hit: `dispatch-user-
|
|
92
|
+
prompt.js`'s `mergeOutputs` previously let the legacy-alias-redirect's
|
|
93
|
+
`additionalContext` and the advisor's new `additionalContext` clobber each
|
|
94
|
+
other if both fired on the same prompt (independent triggers, so this can
|
|
95
|
+
genuinely co-occur) — now joined instead of last-write-wins.
|
|
96
|
+
- `commands/setup.md`'s `--model-optimizer` gained a matching `--interactive`
|
|
97
|
+
flag; `CLAUDE.md` documents the durable instruction for applying a saved
|
|
98
|
+
sub-agent override on future dispatches, respecting existing static
|
|
99
|
+
`model:` frontmatter pins (`security-triager.md`, `sca-triager.md`) and the
|
|
100
|
+
"task needs more capability" carve-out.
|
|
101
|
+
|
|
102
|
+
Also added `SECURITY.md` (vulnerability disclosure policy).
|
|
103
|
+
|
|
104
|
+
## 0.126.0 — closing the find→fix loop: verified auto-fix, CWE-434/306, Fable 5 pricing
|
|
105
|
+
|
|
106
|
+
The largest remediation-side release to date. Prior to this, findings could be
|
|
107
|
+
*found* at scale but not *fixed* at scale — of the 677 findings on this repo's own
|
|
108
|
+
scan, zero could be mechanically applied. This release closes that loop end to end
|
|
109
|
+
while adding the detection classes and workflow polish the closed loop needed.
|
|
110
|
+
Full detail and rationale: `docs/FIND_AND_FIX_LOOP_PRD.md` (implemented; the file
|
|
111
|
+
itself is removed post-implementation per its own instructions).
|
|
112
|
+
|
|
113
|
+
**Remediation — a verified patch path for every finding, not just the ones with a
|
|
114
|
+
stored replacement.**
|
|
115
|
+
- `apply_fix` (MCP) now accepts a caller-supplied `patch` (a files map) and
|
|
116
|
+
**re-verifies it inline** — rescan-clean, no new ≥medium finding, lint-clean —
|
|
117
|
+
before writing. Previously `apply_fix` refused any finding without a stored
|
|
118
|
+
`fix.replacement`; now a template-only or description-only finding (the vast
|
|
119
|
+
majority) can be fixed, because the bytes are proven safe at write time instead
|
|
120
|
+
of trusted at synthesis time.
|
|
121
|
+
- **Deterministic zero-LLM patch synthesis** (`posture/deterministic-fix.js`) for
|
|
122
|
+
safe, context-independent classes — weak hash (md5/sha1→sha256), TLS
|
|
123
|
+
verify-off→on — materialized on demand by `synthesize_fix` and still gated by
|
|
124
|
+
the same inline verifier before `apply_fix` writes it.
|
|
125
|
+
- Regression tests are wired into the fix loop: `synthesize_fix` surfaces the
|
|
126
|
+
scan's existing PoC-derived `regression_test` so a fix ships with a test that
|
|
127
|
+
fails pre-fix and passes post-fix.
|
|
128
|
+
- `/fix --all` and `/find-and-fix-everything` now run independent findings in
|
|
129
|
+
**parallel** (serializing only same-file findings), never halt on the first
|
|
130
|
+
test failure, and publish the running **auto-fix acceptance rate**.
|
|
131
|
+
- `/fix --sca` surfaces its **upgrade break-rate** (build/test-verified upgrades
|
|
132
|
+
are the default; the break-rate is how often an "available" upgrade actually
|
|
133
|
+
wasn't safe to take).
|
|
134
|
+
- MTTR / SLA tracking is now live: every scan stamps `firstSeenAt`/`ageDays` and
|
|
135
|
+
surfaces an SLA-breach summary (`critical` 7d / `high` 30d / …).
|
|
136
|
+
- The Layer-3 LLM validator gained a first-class Anthropic preset
|
|
137
|
+
(`AGENTIC_SECURITY_LLM_PRESET=anthropic`) — previously BYO-endpoint only, so the
|
|
138
|
+
FP-suppression layer was reachable with just a key instead of a hand-built
|
|
139
|
+
endpoint.
|
|
140
|
+
|
|
141
|
+
**Detection.**
|
|
142
|
+
- New **CWE-434 unrestricted file-upload detector** (JS/Python) — a whole CWE
|
|
143
|
+
class that had zero coverage: unguarded Multer configs and writes that use the
|
|
144
|
+
client-supplied filename as the destination.
|
|
145
|
+
- New **CWE-306 missing-authentication** rule: an unauthenticated destructive
|
|
146
|
+
route (DELETE, or id-taking PUT/PATCH) fires only when the app authenticates
|
|
147
|
+
elsewhere in the codebase — so auth-detection is proven to work before the rule
|
|
148
|
+
trusts a "no auth found" signal, keeping it high-precision even on all-public
|
|
149
|
+
files.
|
|
150
|
+
- Corrected stale documentation in `scanner/src/ir/CLAUDE.md`: the Python CST
|
|
151
|
+
parser's `match`-case bodies, walrus bindings, destructuring, and comprehension
|
|
152
|
+
filters were already fully lowered and taint-propagating — verified end-to-end
|
|
153
|
+
with new flow tests, not just parser-shape tests.
|
|
154
|
+
- The independent-eval gate (`bench/independent-eval/`) is now **active**
|
|
155
|
+
(`aggregateF1`/`perFamilyRecall` floors instead of `null`) — proven to fail on a
|
|
156
|
+
deliberate regression and pass at the current corpus result.
|
|
157
|
+
|
|
158
|
+
**Hooks & cost.**
|
|
159
|
+
- Pricing tables across all five rate-table copies now include **Fable 5
|
|
160
|
+
($10/$50 per MTok)** and Sonnet 5 — previously a Fable 5 session wasn't even
|
|
161
|
+
priced by the cache-economics reporter, and the cost advisor couldn't reason
|
|
162
|
+
about the flagship model at all.
|
|
163
|
+
- Two mechanical subagents (`security-triager`, `sca-triager`) pinned to Haiku.
|
|
164
|
+
- The two `UserPromptSubmit` hooks and three `PreToolUse` (Edit) hooks were each
|
|
165
|
+
consolidated into a single dispatcher process — halving/thirding the per-turn
|
|
166
|
+
node spawns. The security-critical bodyguard block is proven to survive
|
|
167
|
+
consolidation (a dedicated regression test asserts the exit-2 deny still
|
|
168
|
+
fires first and short-circuits the advisory hooks).
|
|
169
|
+
- The post-edit hook now offers a one-tap auto-fix inline when a fresh finding is
|
|
170
|
+
mechanically fixable, instead of only pointing at `/fix-all`.
|
|
171
|
+
- The model-cost optimizer now ships **on by default** (`mode: "advise"`) with a
|
|
172
|
+
predicted-vs-realized savings ledger.
|
|
173
|
+
|
|
174
|
+
**Workflow.**
|
|
175
|
+
- `scan --watch` wires the existing (previously unwired) watch-mode daemon:
|
|
176
|
+
continuous incremental re-scan on file change with a risk-delta status line.
|
|
177
|
+
- Opt-in, offline-degrading **live-secret validation**
|
|
178
|
+
(`--validate-secrets`) — labels a detected secret `live`/`dead`/`unknown` via a
|
|
179
|
+
read-only provider "whoami" check (GitHub, Stripe, OpenAI, SendGrid), so "this
|
|
180
|
+
key is LIVE" is distinguished from "you have a high-entropy string."
|
|
181
|
+
`--secret-history` (git-history sweep) was already wired; this release adds
|
|
182
|
+
the liveness half.
|
|
183
|
+
- Diff-scoped scans (`--pr` / `--changed-since`) now default `--incremental` on,
|
|
184
|
+
since a changed-file set is exactly the incremental cache's designed case.
|
|
185
|
+
|
|
186
|
+
## 0.125.0 — multi-provider LLM cost + prompt-cache linter (Cache Economics v2, phase 1)
|
|
187
|
+
|
|
188
|
+
First slice of the Cache Economics v2 PRD (`docs/CACHE_ECONOMICS_V2_PRD.md`) — the
|
|
189
|
+
foundation (P1 provider detection, P2 provider catalog) + the flagship analyzer (F1
|
|
190
|
+
cache-hygiene + P3 per-provider model/depth recommendation). The optimizer now reaches
|
|
191
|
+
beyond this Claude Code session into the user's **own AI-app code**, across providers.
|
|
192
|
+
|
|
193
|
+
- **Provider catalog** (`scanner/src/posture/provider-catalog.js`): a dated, no-network
|
|
194
|
+
snapshot of Anthropic / OpenAI / Google Gemini / xAI — model ladder ($/1M rates), the
|
|
195
|
+
"depth" knob (effort / reasoning_effort / thinkingBudget), and the cache model
|
|
196
|
+
(explicit vs automatic vs implicit). Prices are sourced, not hardcoded-in-logic, with a
|
|
197
|
+
`SOURCED_AT` staleness date.
|
|
198
|
+
- **LLM cost/cache detector** (`scanner/src/sast/llm-cost-advisor.js`, `scanLlmCost`):
|
|
199
|
+
provider-aware, gated on detecting an LLM SDK (low FP), emits **advisory** findings —
|
|
200
|
+
(1) *prompt-cache killer*: a timestamp/UUID/random value baked into a prompt prefix
|
|
201
|
+
that defeats caching; (2) *over-provisioned*: a flagship model at high depth, with the
|
|
202
|
+
catalog's cheaper model + lower depth **in that provider's framework** as the fix
|
|
203
|
+
(e.g. an OpenAI app → "gpt-5.4 at reasoning_effort=low"). Severity `low`/`info` so it
|
|
204
|
+
never inflates security counts.
|
|
205
|
+
|
|
206
|
+
Remaining PRD phases (F2 measured cost, F3 TTL, F4 compaction, F5 pre-warm, F6 cross-
|
|
207
|
+
session warmth, F7 self-tuning; full P4 per-provider economics) ship next.
|
|
208
|
+
|
|
3
209
|
## 0.124.1 — fix: narration no longer prints a broken location
|
|
4
210
|
|
|
5
211
|
Now that v0.124.0 surfaces `narration` prominently inline, a pre-existing template
|
package/bin/agentic-security.js
CHANGED
|
@@ -113,6 +113,7 @@ Options:
|
|
|
113
113
|
--since-baseline Only show findings NOT in the saved baseline
|
|
114
114
|
--hide-proven-safe Drop findings discharged by a flow proof (provably safe)
|
|
115
115
|
--secret-history Also sweep recent git history for committed secrets
|
|
116
|
+
--validate-secrets Label each detected secret live/dead/unknown (opt-in, network, offline-degrading)
|
|
116
117
|
(removed from HEAD but recoverable from .git)
|
|
117
118
|
--history-depth <n> Commits to sweep with --secret-history (default 50)
|
|
118
119
|
--no-epss Skip EPSS exploit-prediction enrichment (default: enabled)
|
|
@@ -331,8 +332,40 @@ async function cmdScan(args) {
|
|
|
331
332
|
}
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
// #21 — --watch : continuous incremental re-scan on file change. Each change
|
|
336
|
+
// re-scans (incrementally) and writes a risk-delta to
|
|
337
|
+
// .agentic-security/watch-status.md that a statusline / /posture poll surfaces
|
|
338
|
+
// inline — so "did my edit add risk?" is answered without a manual re-scan.
|
|
339
|
+
// Blocks until Ctrl-C, like `jest --watch`. Opt-in, so it never affects a
|
|
340
|
+
// normal one-shot scan.
|
|
341
|
+
if (args.flags['watch']) {
|
|
342
|
+
process.env.AGENTIC_SECURITY_INCREMENTAL = '1';
|
|
343
|
+
const { watchProject, computeDelta, persistStatus, renderStatusLine } = await import('../src/posture/watch-mode.js');
|
|
344
|
+
process.stderr.write(`[watch] scanning ${targetAbs} on change — Ctrl-C to stop. Status → .agentic-security/watch-status.md\n`);
|
|
345
|
+
const seed = await runScan(targetAbs, {});
|
|
346
|
+
let prevFindings = seed.scan.findings || [];
|
|
347
|
+
await watchProject(targetAbs, async () => {
|
|
348
|
+
try {
|
|
349
|
+
const { scan } = await runScan(targetAbs, {});
|
|
350
|
+
const curr = scan.findings || [];
|
|
351
|
+
const delta = computeDelta(prevFindings, curr);
|
|
352
|
+
persistStatus(targetAbs, delta);
|
|
353
|
+
process.stderr.write('[watch] ' + renderStatusLine(delta) + '\n');
|
|
354
|
+
prevFindings = curr;
|
|
355
|
+
} catch (e) {
|
|
356
|
+
process.stderr.write(`[watch] rescan failed: ${e.message}\n`);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return 0; // watchProject blocks until aborted
|
|
360
|
+
}
|
|
361
|
+
|
|
334
362
|
// --incremental : reuse taint summaries from prior scans for faster deep mode.
|
|
335
|
-
|
|
363
|
+
// #23 — default incremental ON for diff-scoped scans (--pr / --changed-since):
|
|
364
|
+
// that's the cache's designed use (small changed set → its callers), so the
|
|
365
|
+
// PR-native path is fast by default. A full-tree scan stays non-incremental
|
|
366
|
+
// unless explicitly requested (blanket-default flip needs broader validation).
|
|
367
|
+
const _diffScoped = !!(args.flags['pr'] || args.flags['changed-since']);
|
|
368
|
+
if (args.flags['incremental'] || process.env.AGENTIC_SECURITY_INCREMENTAL === '1' || _diffScoped) {
|
|
336
369
|
process.env.AGENTIC_SECURITY_INCREMENTAL = '1';
|
|
337
370
|
}
|
|
338
371
|
|
|
@@ -566,6 +599,46 @@ async function cmdScan(args) {
|
|
|
566
599
|
if (_isSafeStateDir(stateDir)) {
|
|
567
600
|
await fsp.mkdir(stateDir, { recursive: true });
|
|
568
601
|
const persistedScan = toJSON(scan, meta);
|
|
602
|
+
// #10 — MTTR: stamp firstSeenAt/lastSeenAt/ageDays from the PREVIOUS scan so
|
|
603
|
+
// every finding carries an age, SLA breaches can be surfaced, and the fix
|
|
604
|
+
// loop can report time-to-clean. Best-effort; skipped under --deterministic
|
|
605
|
+
// so deterministic state stays byte-identical run-to-run.
|
|
606
|
+
if (!args.flags.deterministic) {
|
|
607
|
+
try {
|
|
608
|
+
const { stampFindingTimestamps, buildBaselineMap, renderSlaSummary } = await import('../src/posture/mttr.js');
|
|
609
|
+
let baselineMap = new Map();
|
|
610
|
+
try {
|
|
611
|
+
const prev = JSON.parse(await fsp.readFile(path.join(stateDir, 'last-scan.json'), 'utf8'));
|
|
612
|
+
baselineMap = buildBaselineMap(prev);
|
|
613
|
+
} catch { /* first run — empty baseline, everything is firstSeen now */ }
|
|
614
|
+
const now = Date.now();
|
|
615
|
+
stampFindingTimestamps(persistedScan.findings || [], baselineMap, now);
|
|
616
|
+
stampFindingTimestamps(persistedScan.secrets || [], baselineMap, now);
|
|
617
|
+
stampFindingTimestamps((persistedScan.supplyChain || []).filter(s => s.type === 'vulnerable_dep'), baselineMap, now);
|
|
618
|
+
// Surface the SLA-breach line on human-readable formats (not JSON/CI pipes).
|
|
619
|
+
const isJson = format === 'json' || format === 'sarif' || format === 'cyclonedx' || format === 'sbom' || format === 'spdx' || format === 'vex' || format === 'openvex' || format === 'pbom' || format === 'aibom';
|
|
620
|
+
if (!isJson) {
|
|
621
|
+
const sla = renderSlaSummary(persistedScan.findings || []);
|
|
622
|
+
if (sla) process.stderr.write(`⏰ agentic-security: ${sla}\n`);
|
|
623
|
+
}
|
|
624
|
+
} catch { /* MTTR is best-effort — never block a scan write */ }
|
|
625
|
+
}
|
|
626
|
+
// #22 — live-secret validation (opt-in, offline-degrading). Label each
|
|
627
|
+
// detected secret live | dead | unknown via a read-only provider "whoami".
|
|
628
|
+
// "This key is LIVE and was committed N commits ago" is the P0 that matters.
|
|
629
|
+
if (args.flags['validate-secrets'] || process.env.AGENTIC_SECURITY_VALIDATE_SECRETS === '1') {
|
|
630
|
+
try {
|
|
631
|
+
const { checkSecretLive } = await import('../src/posture/secret-live-check.js');
|
|
632
|
+
let live = 0;
|
|
633
|
+
for (const s of (persistedScan.secrets || [])) {
|
|
634
|
+
const { verdict, provider } = await checkSecretLive(s);
|
|
635
|
+
s.liveVerdict = verdict;
|
|
636
|
+
if (provider) s.liveProvider = provider;
|
|
637
|
+
if (verdict === 'live') live++;
|
|
638
|
+
}
|
|
639
|
+
if (live > 0) process.stderr.write(`🔴 agentic-security: ${live} LIVE secret(s) validated — rotate immediately (even if already removed from HEAD).\n`);
|
|
640
|
+
} catch { /* best-effort, offline-degrading — never block a scan */ }
|
|
641
|
+
}
|
|
569
642
|
const lastScanBody = JSON.stringify(persistedScan, null, 2);
|
|
570
643
|
await fsp.writeFile(path.join(stateDir, 'last-scan.json'), lastScanBody);
|
|
571
644
|
try {
|
|
@@ -1535,7 +1608,7 @@ description: Remediate every finding at or above a severity threshold (default:
|
|
|
1535
1608
|
argument-hint: "[--severity critical|high|medium]"
|
|
1536
1609
|
---
|
|
1537
1610
|
|
|
1538
|
-
Read \`.agentic-security/last-scan.json\`. For every finding at or above \`\${1:-critical}\` severity, dispatch the security-fixer subagent
|
|
1611
|
+
Read \`.agentic-security/last-scan.json\`. For every finding at or above \`\${1:-critical}\` severity, dispatch the security-fixer subagent — independent findings in parallel, serializing only findings that share a file. Each fix is inline-verified by apply_fix before it lands (finding gone + no new ≥medium + lint). Do NOT halt on the first failure: record each finding's outcome and continue, then report the full list and the auto-fix acceptance rate. Re-run \`/security-scan-all\` to confirm.
|
|
1539
1612
|
`,
|
|
1540
1613
|
'security-report.md': `---
|
|
1541
1614
|
description: Generate an HTML security report (or JSON / Markdown / SARIF).
|
package/dist/11.index.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
export const id = 11;
|
|
2
|
+
export const ids = [11];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 11:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
// ESM COMPAT FLAG
|
|
9
|
+
__webpack_require__.r(__webpack_exports__);
|
|
10
|
+
|
|
11
|
+
// EXPORTS
|
|
12
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
13
|
+
runProjectLinter: () => (/* binding */ runProjectLinter),
|
|
14
|
+
verifyFix: () => (/* binding */ verifyFix),
|
|
15
|
+
verifyPatch: () => (/* binding */ verifyPatch)
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// EXTERNAL MODULE: external "node:child_process"
|
|
19
|
+
var external_node_child_process_ = __webpack_require__(1421);
|
|
20
|
+
// EXTERNAL MODULE: external "node:fs"
|
|
21
|
+
var external_node_fs_ = __webpack_require__(3024);
|
|
22
|
+
// EXTERNAL MODULE: external "node:path"
|
|
23
|
+
var external_node_path_ = __webpack_require__(6760);
|
|
24
|
+
// EXTERNAL MODULE: ./src/engine.js + 524 modules
|
|
25
|
+
var engine = __webpack_require__(8215);
|
|
26
|
+
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
27
|
+
// Deterministic honesty gates on fix / finding output (#7).
|
|
28
|
+
//
|
|
29
|
+
// The project's verification discipline (scanner/CLAUDE.md) exists because
|
|
30
|
+
// several releases shipped broken or false because work was reported as done
|
|
31
|
+
// without confirming the artifact changed. Two of those failure modes are
|
|
32
|
+
// *textual* — they live in the prose an agent emits alongside a fix — and can
|
|
33
|
+
// be caught deterministically, with no LLM and no network:
|
|
34
|
+
//
|
|
35
|
+
// 1. Hand-wave residual-risk prose. "The input is adequately handled",
|
|
36
|
+
// "future work", "tbd", "later" — vague assurances that claim safety
|
|
37
|
+
// without naming a concrete remaining vector. A residual you can't name
|
|
38
|
+
// is a residual you're guessing about; reject the guess.
|
|
39
|
+
//
|
|
40
|
+
// 2. An unbacked "this is a false positive / provably safe" verdict. Marking
|
|
41
|
+
// a finding safe is a coverage *reduction* — it must cite a `file:line`
|
|
42
|
+
// that shows why, exactly like the rules-override gate refuses to silently
|
|
43
|
+
// shrink coverage.
|
|
44
|
+
//
|
|
45
|
+
// Plus a conservative fix-tier classifier so a partial remediation can never be
|
|
46
|
+
// labelled FULL: any workaround-only signal (rate-limit, docs, log-without-
|
|
47
|
+
// reject) is WORKAROUND; anything short of (sink signature changed + all callers
|
|
48
|
+
// routed + a discriminating test) is at most MITIGATION; only the full set with
|
|
49
|
+
// no partial-sanitization caveat earns FULL.
|
|
50
|
+
//
|
|
51
|
+
// Pure functions, no side effects, no throwing — safe to call from a command,
|
|
52
|
+
// a hook, or the MCP verify_fix path.
|
|
53
|
+
|
|
54
|
+
// Vague-assurance phrases that a real residual must never hide behind. Matched
|
|
55
|
+
// case-insensitively with word boundaries so "later" doesn't trip on
|
|
56
|
+
// "collateral" and "tbd" doesn't trip on a longer token.
|
|
57
|
+
const BANNED_RESIDUAL_PHRASES = Object.freeze([
|
|
58
|
+
'adequately handled',
|
|
59
|
+
'adequately handles',
|
|
60
|
+
'properly validated',
|
|
61
|
+
'properly handled',
|
|
62
|
+
'handled properly',
|
|
63
|
+
'handled safely',
|
|
64
|
+
'future work',
|
|
65
|
+
'more work needed',
|
|
66
|
+
'to be done',
|
|
67
|
+
'tbd',
|
|
68
|
+
'later',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// A citation shaped like `file:line` — one or more non-space, non-colon chars,
|
|
72
|
+
// a colon, then digits. Unanchored: it need only appear somewhere in the item.
|
|
73
|
+
const CITATION_RE = /[^\s:]+:\d+/;
|
|
74
|
+
|
|
75
|
+
// Verdicts that assert the finding is not real and therefore demand a citation.
|
|
76
|
+
// Compared after normalizing separators (`_`/space → `-`) and lowercasing, so
|
|
77
|
+
// FALSE_POSITIVE, false-positive, and "provably safe" all land here.
|
|
78
|
+
const FP_VERDICTS = Object.freeze(new Set(['false-positive', 'provably-safe', 'safe']));
|
|
79
|
+
|
|
80
|
+
function _escapeRe(s) {
|
|
81
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reject vague-assurance / hand-wave residual-risk prose.
|
|
86
|
+
*
|
|
87
|
+
* An empty or whitespace-only residual is ok — there is no residual to lie
|
|
88
|
+
* about. A non-empty residual is rejected when it contains any banned phrase;
|
|
89
|
+
* each match yields one violation naming the offending phrase.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} residualText
|
|
92
|
+
* @returns {{ ok: boolean, violations: string[] }}
|
|
93
|
+
*/
|
|
94
|
+
function checkResidualHonesty(residualText) {
|
|
95
|
+
const text = typeof residualText === 'string' ? residualText : '';
|
|
96
|
+
if (text.trim() === '') return { ok: true, violations: [] };
|
|
97
|
+
|
|
98
|
+
const violations = [];
|
|
99
|
+
for (const phrase of BANNED_RESIDUAL_PHRASES) {
|
|
100
|
+
const re = new RegExp(`\\b${_escapeRe(phrase)}\\b`, 'i');
|
|
101
|
+
if (re.test(text)) {
|
|
102
|
+
violations.push(`vague-assurance phrase: "${phrase}"`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { ok: violations.length === 0, violations };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _isCitation(item) {
|
|
109
|
+
if (typeof item === 'string') return CITATION_RE.test(item);
|
|
110
|
+
if (item && typeof item === 'object' && typeof item.location === 'string') {
|
|
111
|
+
return CITATION_RE.test(item.location);
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _normalizeVerdict(verdict) {
|
|
117
|
+
return String(verdict).trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Require a file:line citation behind a "this is not real" verdict.
|
|
122
|
+
*
|
|
123
|
+
* For a false-positive / provably-safe / safe verdict (case-insensitive; also
|
|
124
|
+
* accepts FALSE_POSITIVE), at least one evidence item must be a `file:line`
|
|
125
|
+
* citation — either a string matching /[^\s:]+:\d+/ or an object
|
|
126
|
+
* `{ location: "file:line" }`. Any other verdict passes unconditionally.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} verdict
|
|
129
|
+
* @param {Array|string|object} evidence
|
|
130
|
+
* @returns {{ ok: boolean, violations: string[] }}
|
|
131
|
+
*/
|
|
132
|
+
function requireCitedEvidence(verdict, evidence) {
|
|
133
|
+
if (typeof verdict !== 'string' || !FP_VERDICTS.has(_normalizeVerdict(verdict))) {
|
|
134
|
+
return { ok: true, violations: [] };
|
|
135
|
+
}
|
|
136
|
+
const items = Array.isArray(evidence)
|
|
137
|
+
? evidence
|
|
138
|
+
: evidence == null
|
|
139
|
+
? []
|
|
140
|
+
: [evidence];
|
|
141
|
+
if (items.some(_isCitation)) return { ok: true, violations: [] };
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
violations: ['false-positive/safe verdict requires a file:line citation'],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Classify a fix into FULL | MITIGATION | WORKAROUND, conservative-first.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} signals
|
|
152
|
+
* @param {boolean} signals.sinkSignatureChanged
|
|
153
|
+
* @param {boolean} signals.allCallersRouted
|
|
154
|
+
* @param {boolean} signals.testDiscriminates - a test that fails pre-fix, passes post-fix
|
|
155
|
+
* @param {boolean} [signals.rateLimitOnly]
|
|
156
|
+
* @param {boolean} [signals.docsOnly]
|
|
157
|
+
* @param {boolean} [signals.logOnlyNoReject]
|
|
158
|
+
* @param {boolean} [signals.partialSanitization]
|
|
159
|
+
* @returns {'FULL'|'MITIGATION'|'WORKAROUND'}
|
|
160
|
+
*/
|
|
161
|
+
function computeFixTier(signals) {
|
|
162
|
+
const s = signals && typeof signals === 'object' ? signals : {};
|
|
163
|
+
if (s.rateLimitOnly || s.docsOnly || s.logOnlyNoReject) return 'WORKAROUND';
|
|
164
|
+
const complete = s.sinkSignatureChanged && s.allCallersRouted && s.testDiscriminates;
|
|
165
|
+
if (s.partialSanitization || !complete) return 'MITIGATION';
|
|
166
|
+
return 'FULL';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compose the three gates for a single fix's output.
|
|
171
|
+
*
|
|
172
|
+
* ok = residual-honesty ok AND evidence-citation ok, further constrained by the
|
|
173
|
+
* tier/residual consistency invariant:
|
|
174
|
+
* - a FULL tier must NOT carry a residual (a full fix has nothing left);
|
|
175
|
+
* - a non-FULL tier MUST document a residual (say what's still open).
|
|
176
|
+
*
|
|
177
|
+
* @param {{ residual?: string, verdict?: string, evidence?: any, signals?: object }} input
|
|
178
|
+
* @returns {{ ok: boolean, tier: string, violations: string[] }}
|
|
179
|
+
*/
|
|
180
|
+
function gateFixOutput({ residual, verdict, evidence, signals } = {}) {
|
|
181
|
+
const tier = computeFixTier(signals);
|
|
182
|
+
const residualCheck = checkResidualHonesty(residual);
|
|
183
|
+
const evidenceCheck = requireCitedEvidence(verdict, evidence);
|
|
184
|
+
|
|
185
|
+
const violations = [...residualCheck.violations, ...evidenceCheck.violations];
|
|
186
|
+
let ok = residualCheck.ok && evidenceCheck.ok;
|
|
187
|
+
|
|
188
|
+
const residualEmpty = typeof residual !== 'string' || residual.trim() === '';
|
|
189
|
+
if (tier === 'FULL' && !residualEmpty) {
|
|
190
|
+
violations.push('FULL tier cannot carry a residual');
|
|
191
|
+
ok = false;
|
|
192
|
+
}
|
|
193
|
+
if (tier !== 'FULL' && residualEmpty) {
|
|
194
|
+
violations.push('non-FULL tier must document a residual');
|
|
195
|
+
ok = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { ok, tier, violations };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERDICTS });
|
|
202
|
+
|
|
203
|
+
;// CONCATENATED MODULE: ./src/posture/fix-verify.js
|
|
204
|
+
// Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
|
|
205
|
+
//
|
|
206
|
+
// Given a candidate patch (the new file content + the finding stableId being
|
|
207
|
+
// fixed), verify it:
|
|
208
|
+
//
|
|
209
|
+
// 1. The original finding's stableId no longer fires on the patched file.
|
|
210
|
+
// 2. No new findings at severity ≥ medium were introduced by the patch.
|
|
211
|
+
// 3. The project's existing linter (when present) passes on the patched file.
|
|
212
|
+
//
|
|
213
|
+
// If any of those fail, the caller is expected to NOT apply the patch and
|
|
214
|
+
// instead surface a "fix plan" — a numbered list of steps the engineer can
|
|
215
|
+
// follow — rather than dump a broken patch on the user.
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
224
|
+
|
|
225
|
+
// Run a focused re-scan over just the patched file(s) using the in-memory
|
|
226
|
+
// engine. No filesystem write needed — we hand the new content in via the
|
|
227
|
+
// fileContents map.
|
|
228
|
+
async function verifyPatch({
|
|
229
|
+
scanRoot,
|
|
230
|
+
originalFindingStableId,
|
|
231
|
+
files, // { [relPath]: newContent }
|
|
232
|
+
depFileContents = {},
|
|
233
|
+
} = {}) {
|
|
234
|
+
if (!files || typeof files !== 'object') return { ok: false, reason: 'no-files-provided' };
|
|
235
|
+
const fileContents = { ...files };
|
|
236
|
+
let scan;
|
|
237
|
+
try {
|
|
238
|
+
scan = await (0,engine/* runFullScan */.wW)({ fileContents, depFileContents, scanRoot }, () => {});
|
|
239
|
+
} catch (e) {
|
|
240
|
+
return { ok: false, reason: 'rescan-failed', error: e.message };
|
|
241
|
+
}
|
|
242
|
+
const findings = (scan && scan.findings) || [];
|
|
243
|
+
const stillHasOriginal = !!originalFindingStableId &&
|
|
244
|
+
findings.some(f => f.stableId === originalFindingStableId);
|
|
245
|
+
if (stillHasOriginal) {
|
|
246
|
+
return { ok: false, reason: 'original-finding-still-present', stableId: originalFindingStableId };
|
|
247
|
+
}
|
|
248
|
+
const introducedHighOrAbove = findings.filter(f =>
|
|
249
|
+
(SEVERITY_RANK[f.severity] ?? 9) <= SEVERITY_RANK.medium);
|
|
250
|
+
// Don't count findings on lines outside the patched files — but our
|
|
251
|
+
// fileContents map IS the patched files, so every finding is in-scope.
|
|
252
|
+
return {
|
|
253
|
+
ok: introducedHighOrAbove.length === 0,
|
|
254
|
+
reason: introducedHighOrAbove.length === 0 ? 'verified' : 'introduced-new-findings',
|
|
255
|
+
introduced: introducedHighOrAbove.map(f => ({
|
|
256
|
+
vuln: f.vuln, file: f.file, line: f.line, severity: f.severity,
|
|
257
|
+
stableId: f.stableId,
|
|
258
|
+
})),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Detect which linter the project uses and run it on the patched files.
|
|
263
|
+
// Returns { ok, runner, output } or { ok: true, runner: 'none' } when no
|
|
264
|
+
// linter is configured (silent pass).
|
|
265
|
+
function runProjectLinter(scanRoot, filePaths) {
|
|
266
|
+
if (!scanRoot || !Array.isArray(filePaths) || filePaths.length === 0) {
|
|
267
|
+
return { ok: true, runner: 'none' };
|
|
268
|
+
}
|
|
269
|
+
const has = (p) => { try { return external_node_fs_.existsSync(external_node_path_.join(scanRoot, p)); } catch { return false; } };
|
|
270
|
+
// Pick the linter by config file present in the repo root.
|
|
271
|
+
const jsFiles = filePaths.filter(f => /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(f));
|
|
272
|
+
const pyFiles = filePaths.filter(f => /\.py$/i.test(f));
|
|
273
|
+
const goFiles = filePaths.filter(f => /\.go$/i.test(f));
|
|
274
|
+
const javaFiles = filePaths.filter(f => /\.java$/i.test(f));
|
|
275
|
+
|
|
276
|
+
if (jsFiles.length && (has('.eslintrc') || has('.eslintrc.json') || has('.eslintrc.js') || has('eslint.config.js') || has('eslint.config.mjs'))) {
|
|
277
|
+
return runLinter(scanRoot, 'eslint', ['--no-error-on-unmatched-pattern', ...jsFiles]);
|
|
278
|
+
}
|
|
279
|
+
if (pyFiles.length && (has('pyproject.toml') || has('ruff.toml') || has('.ruff.toml'))) {
|
|
280
|
+
return runLinter(scanRoot, 'ruff', ['check', ...pyFiles]);
|
|
281
|
+
}
|
|
282
|
+
if (pyFiles.length && has('.flake8')) {
|
|
283
|
+
return runLinter(scanRoot, 'flake8', pyFiles);
|
|
284
|
+
}
|
|
285
|
+
if (goFiles.length && (has('.golangci.yml') || has('.golangci.yaml'))) {
|
|
286
|
+
return runLinter(scanRoot, 'golangci-lint', ['run', ...goFiles]);
|
|
287
|
+
}
|
|
288
|
+
if (javaFiles.length && has('checkstyle.xml')) {
|
|
289
|
+
return runLinter(scanRoot, 'checkstyle', ['-c', 'checkstyle.xml', ...javaFiles]);
|
|
290
|
+
}
|
|
291
|
+
return { ok: true, runner: 'none' };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function runLinter(cwd, cmd, args) {
|
|
295
|
+
let r;
|
|
296
|
+
try {
|
|
297
|
+
r = (0,external_node_child_process_.spawnSync)(cmd, args, { cwd, encoding: 'utf8', timeout: 60_000 });
|
|
298
|
+
} catch (e) {
|
|
299
|
+
return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing', error: e.message };
|
|
300
|
+
}
|
|
301
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
302
|
+
return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing' };
|
|
303
|
+
}
|
|
304
|
+
if (r.status === null) {
|
|
305
|
+
return { ok: false, runner: cmd, reason: 'timed-out', output: (r.stderr || r.stdout || '').slice(-2000) };
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
ok: r.status === 0,
|
|
309
|
+
runner: cmd,
|
|
310
|
+
exitCode: r.status,
|
|
311
|
+
output: ((r.stderr || '') + (r.stdout || '')).slice(-2000),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Top-level verify: re-scan + lint. Returns the combined verdict + a
|
|
316
|
+
// human-readable summary string suitable for surfacing to the user.
|
|
317
|
+
// Addition #7 — deterministic honesty gates on fix output. When the caller
|
|
318
|
+
// supplies `fixMeta` ({ residual, verdict, evidence, signals }) — e.g. the
|
|
319
|
+
// security-fixer agent's residual-risk text + completeness signals — the fix's
|
|
320
|
+
// claims are checked mechanically (no hand-wave residual prose, a cited
|
|
321
|
+
// file:line for any FP/safe verdict, and a FULL/MITIGATION/WORKAROUND tier). A
|
|
322
|
+
// dishonest or over-claiming fix fails the gate. When `fixMeta` is absent
|
|
323
|
+
// (the deterministic MCP write path, which has no claims to check) the honesty
|
|
324
|
+
// gate is skipped and behavior is unchanged.
|
|
325
|
+
async function verifyFix({
|
|
326
|
+
scanRoot,
|
|
327
|
+
originalFindingStableId,
|
|
328
|
+
files,
|
|
329
|
+
depFileContents,
|
|
330
|
+
fixMeta,
|
|
331
|
+
} = {}) {
|
|
332
|
+
const rescan = await verifyPatch({ scanRoot, originalFindingStableId, files, depFileContents });
|
|
333
|
+
const lint = runProjectLinter(scanRoot, Object.keys(files || {}));
|
|
334
|
+
let honesty = null;
|
|
335
|
+
if (fixMeta && typeof fixMeta === 'object') {
|
|
336
|
+
try { honesty = gateFixOutput(fixMeta); } catch { honesty = null; }
|
|
337
|
+
}
|
|
338
|
+
const ok = rescan.ok && (lint.ok || lint.skipped) && (honesty ? honesty.ok : true);
|
|
339
|
+
const summary = [
|
|
340
|
+
`re-scan: ${rescan.ok ? 'PASS' : 'FAIL — ' + rescan.reason}`,
|
|
341
|
+
`linter: ${lint.runner === 'none' ? 'skipped (no linter config)'
|
|
342
|
+
: lint.skipped ? `${lint.runner} not installed`
|
|
343
|
+
: lint.ok ? `${lint.runner} PASS`
|
|
344
|
+
: `${lint.runner} FAIL (exit ${lint.exitCode})`}`,
|
|
345
|
+
honesty ? `honesty: ${honesty.ok ? `PASS (${honesty.tier})` : 'FAIL — ' + honesty.violations.join('; ')}` : null,
|
|
346
|
+
].filter(Boolean).join('\n');
|
|
347
|
+
return { ok, rescan, lint, honesty, summary };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
/***/ })
|
|
352
|
+
|
|
353
|
+
};
|