@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +555 -0
- package/bin/agentic-security.js +770 -49
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +117 -18
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +24 -6
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +187 -52
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +117 -18
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +27 -5
- package/dist/730.index.js +311 -0
- package/dist/736.index.js +301 -0
- package/dist/824.index.js +7 -0
- package/dist/905.index.js +88 -22
- package/dist/920.index.js +491 -0
- package/dist/970.index.js +173 -0
- package/dist/agentic-security.mjs +13 -13
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/calibration-seed.json +2 -0
- package/package.json +23 -9
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/index.js +18 -0
- package/src/dataflow/privacy-catalog.js +290 -0
- package/src/dataflow/privacy-deep-walker.js +515 -0
- package/src/dataflow/privacy-governance.js +126 -0
- package/src/dataflow/privacy-inventory.js +154 -0
- package/src/dataflow/privacy-sink-policy.js +125 -0
- package/src/dataflow/privacy-taint.js +115 -54
- package/src/dataflow/privacy-taxonomy.js +233 -0
- package/src/discovery/disprove.js +7 -3
- package/src/discovery/hunter.js +9 -5
- package/src/discovery/index.js +2 -2
- package/src/discovery/llm-invoke.js +69 -13
- package/src/egress/audit.js +147 -0
- package/src/egress/policy.js +313 -0
- package/src/egress/redact.js +180 -0
- package/src/engine.js +1048 -302
- package/src/fix/apply-fix-service.js +404 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +177 -50
- package/src/pipeline/analyzer-supervisor.js +93 -0
- package/src/pipeline/analyzer-worker.js +26 -0
- package/src/pipeline/annotator-runner.js +33 -0
- package/src/pipeline/assurance-mode.js +154 -0
- package/src/pipeline/cascade-worker-pool.js +172 -0
- package/src/pipeline/cascade-worker.js +43 -0
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/detector-runner.js +51 -0
- package/src/pipeline/enrichment-completion.js +58 -0
- package/src/pipeline/evidence-provenance.js +91 -0
- package/src/pipeline/finding-schema.js +108 -0
- package/src/pipeline/legacy-compat.js +101 -0
- package/src/pipeline/producer-collector.js +48 -0
- package/src/pipeline/producer-registry.js +112 -0
- package/src/pipeline/scan-health.js +144 -0
- package/src/posture/CLAUDE.md +123 -0
- package/src/posture/accuracy-scorecard.js +156 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +241 -0
- package/src/posture/auditor-walkthrough.js +186 -21
- package/src/posture/calibration-feedback.js +201 -0
- package/src/posture/calibration-seed.json +2 -0
- package/src/posture/calibration.js +25 -0
- package/src/posture/compliance-evidence-signing.js +131 -0
- package/src/posture/compliance-policy.js +324 -17
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/custom-rules.js +36 -0
- package/src/posture/deterministic.js +8 -1
- package/src/posture/encryption-provider.js +205 -0
- package/src/posture/evidence-grade-wording.js +71 -0
- package/src/posture/fix-history.js +137 -20
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +65 -8
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/git-history.js +13 -5
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +111 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +47 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +64 -5
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +248 -5
- package/src/report/oscal.js +7 -2
- package/src/runScan.js +34 -5
- package/src/sast/cpp.js +3 -14
- package/src/sast/rate-limit.js +33 -3
- package/src/sca/llm-function-extract.js +6 -0
- package/src/util/git-hardening.js +128 -0
package/dist/435.index.js
CHANGED
|
@@ -23,6 +23,12 @@ var external_node_url_ = __webpack_require__(3136);
|
|
|
23
23
|
var promises_ = __webpack_require__(1455);
|
|
24
24
|
// EXTERNAL MODULE: ./src/posture/fix-history.js
|
|
25
25
|
var fix_history = __webpack_require__(4407);
|
|
26
|
+
// EXTERNAL MODULE: ./src/fix/apply-fix-service.js
|
|
27
|
+
var apply_fix_service = __webpack_require__(7730);
|
|
28
|
+
// EXTERNAL MODULE: ./src/posture/material-change.js
|
|
29
|
+
var material_change = __webpack_require__(4629);
|
|
30
|
+
// EXTERNAL MODULE: ./src/fix/approver-registry.js
|
|
31
|
+
var approver_registry = __webpack_require__(437);
|
|
26
32
|
;// CONCATENATED MODULE: ./src/posture/deterministic-fix.js
|
|
27
33
|
// Deterministic fix synthesis (#1) — for the narrow set of vulnerability classes
|
|
28
34
|
// where a context-INDEPENDENT literal swap is a safe, correct fix, produce a
|
|
@@ -92,6 +98,8 @@ function synthesizeDeterministicPatch(finding, fileContent) {
|
|
|
92
98
|
|
|
93
99
|
// EXTERNAL MODULE: ./src/posture/integrity.js
|
|
94
100
|
var integrity = __webpack_require__(1130);
|
|
101
|
+
// EXTERNAL MODULE: ./src/posture/state-dir.js
|
|
102
|
+
var state_dir = __webpack_require__(1174);
|
|
95
103
|
// EXTERNAL MODULE: ./src/posture/cache-economics.js
|
|
96
104
|
var cache_economics = __webpack_require__(8752);
|
|
97
105
|
;// CONCATENATED MODULE: ./src/mcp/redact.js
|
|
@@ -197,10 +205,10 @@ function redactArgsBlob(s) {
|
|
|
197
205
|
return redactString(s);
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
// EXTERNAL MODULE: ./src/report/index.js +
|
|
201
|
-
var report = __webpack_require__(
|
|
202
|
-
// EXTERNAL MODULE: ./src/posture/
|
|
203
|
-
var
|
|
208
|
+
// EXTERNAL MODULE: ./src/report/index.js + 3 modules
|
|
209
|
+
var report = __webpack_require__(457);
|
|
210
|
+
// EXTERNAL MODULE: ./src/posture/provenance/schema.js
|
|
211
|
+
var schema = __webpack_require__(4594);
|
|
204
212
|
;// CONCATENATED MODULE: ./src/posture/agents-memory.js
|
|
205
213
|
// AGENTS.md — writable continual-learning memory (harness-anatomy #2).
|
|
206
214
|
//
|
|
@@ -499,6 +507,14 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
|
|
|
499
507
|
|
|
500
508
|
|
|
501
509
|
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
// Git-origin provenance (Finding Provenance M0/M1). Distinct from
|
|
514
|
+
// `finding.provenance` (AI-authorship) and from an SCA entry's `provenance`
|
|
515
|
+
// (Sigstore/SLSA attestation) — see report/index.js's import comment.
|
|
516
|
+
|
|
517
|
+
|
|
502
518
|
// Lazy-loaded: these transitively pull in npm packages (@babel/core and
|
|
503
519
|
// friends) that aren't available in the plugin-cache install path
|
|
504
520
|
// (no node_modules). Deferring keeps the MCP server bootable everywhere;
|
|
@@ -863,26 +879,44 @@ const scan_diff = {
|
|
|
863
879
|
// / MAX_TOTAL_SCAN_BYTES, so this does not turn scan_diff into a
|
|
864
880
|
// full-project deep scan).
|
|
865
881
|
const runScan = await getRunScan();
|
|
866
|
-
|
|
882
|
+
// FR-704 (assurance-hardening PRD): this tool's own description promises
|
|
883
|
+
// "runs scan in memory" — without this, runFullScan's own state writers
|
|
884
|
+
// (dpia.md, ropa.md, privacy-framework.json, threat-model.json, and
|
|
885
|
+
// others) fire unconditionally on every call, silently mutating the
|
|
886
|
+
// user's real project on every pre-write self-correction scan. Confirmed
|
|
887
|
+
// by direct execution before this fix (11 state artifacts written by a
|
|
888
|
+
// single scan_diff-shaped call).
|
|
889
|
+
const result = await (0,state_dir/* withStateWritesDisabled */.Ao)(() =>
|
|
890
|
+
runScan(sessionRoot, { network: false, fileContents, deep: true, deepInCi: true }));
|
|
867
891
|
const wantSet = new Set(Object.keys(fileContents));
|
|
868
892
|
const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
869
893
|
const min = sevRank[severity] ?? 0;
|
|
870
|
-
// Stage 6 correctness audit: this only
|
|
871
|
-
// (the SAST channel) — scan.secrets and
|
|
872
|
-
// arrays on the raw runScan() result
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
// the
|
|
880
|
-
|
|
894
|
+
// Stage 6 correctness audit (historical): this used to only read
|
|
895
|
+
// result.scan.findings (the SAST channel) — scan.secrets and
|
|
896
|
+
// scan.logicVulns are separate arrays on the raw runScan() result, and a
|
|
897
|
+
// hand-rolled 3-channel concat here was a second, divergent copy of the
|
|
898
|
+
// merge report/index.js's normalizeFindings() already does (four
|
|
899
|
+
// channels, plus per-channel defaulting and remediation-string
|
|
900
|
+
// resolution the old concat re-implemented separately and could drift
|
|
901
|
+
// from). Assurance-hardening PRD FR-105 ("JSON, SARIF, HTML, CSV, JUnit,
|
|
902
|
+
// and MCP outputs derive from the same validated object"): route through
|
|
903
|
+
// the same canonical merge every other output format uses.
|
|
904
|
+
//
|
|
905
|
+
// This closes the field-mapping/dedup divergence, but does NOT make
|
|
906
|
+
// scan_diff surface SCA/supply-chain findings end to end: this handler
|
|
907
|
+
// never builds a `depFileContents` map (everything a caller passes in
|
|
908
|
+
// `files`, manifests included, lands in `fileContents`), and manifest-
|
|
909
|
+
// based supply-chain detection in engine.js reads only `depFileContents`
|
|
910
|
+
// — so `result.scan.supplyChain` is always empty for this tool today
|
|
911
|
+
// regardless of this fix. That is a separate, real limitation (scan_diff
|
|
912
|
+
// was designed for pre-write code self-correction, not manifest
|
|
913
|
+
// scanning), left as-is rather than silently claimed fixed here.
|
|
914
|
+
const findings = (0,report.normalizeFindings)(result.scan)
|
|
881
915
|
.filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
|
|
882
916
|
.map(f => redactFinding({
|
|
883
917
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
884
|
-
title: f.
|
|
885
|
-
description: f.description, remediation:
|
|
918
|
+
title: f.vuln, cwe: f.cwe,
|
|
919
|
+
description: f.description, remediation: f.remediation,
|
|
886
920
|
}));
|
|
887
921
|
// Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
|
|
888
922
|
// The agent gets a head+tail preview plus a path it can page through;
|
|
@@ -1025,6 +1059,21 @@ const explain_finding = {
|
|
|
1025
1059
|
epssScore: typeof f.epssScore === 'number' ? f.epssScore : null,
|
|
1026
1060
|
epssPercentile: typeof f.epssPercentile === 'number' ? f.epssPercentile : null,
|
|
1027
1061
|
exploitedNow: !!f.exploitedNow,
|
|
1062
|
+
// Which commit introduced this finding. `includeEmail` stays at its
|
|
1063
|
+
// DEFAULT (false) unconditionally — unlike the JSON report there is no
|
|
1064
|
+
// operator-set env escape for it here, because the consumer is an
|
|
1065
|
+
// agent that has no business receiving a committer's email address.
|
|
1066
|
+
// `pseudonymize`, by contrast, IS read back from the same env var
|
|
1067
|
+
// report/index.js's `_normalizedProvenance` reads
|
|
1068
|
+
// (AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1 / --pseudonymize-authors) —
|
|
1069
|
+
// fix-round item 4: an operator who set that policy was still getting
|
|
1070
|
+
// raw committer names (and, via providerEnrichment, raw reviewer
|
|
1071
|
+
// logins/CODEOWNERS lines) through this MCP surface because this call
|
|
1072
|
+
// passed no options object at all, silently defeating their policy at
|
|
1073
|
+
// this one output boundary while report/index.js honoured it.
|
|
1074
|
+
findingProvenance: f.findingProvenance ? (0,schema/* redactFindingProvenance */.As)(f.findingProvenance, {
|
|
1075
|
+
pseudonymize: process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS === '1',
|
|
1076
|
+
}) : null,
|
|
1028
1077
|
};
|
|
1029
1078
|
},
|
|
1030
1079
|
};
|
|
@@ -1032,7 +1081,7 @@ const explain_finding = {
|
|
|
1032
1081
|
// ─── apply_fix ───────────────────────────────────────────────────────────────
|
|
1033
1082
|
const apply_fix = {
|
|
1034
1083
|
name: 'apply_fix',
|
|
1035
|
-
description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing.',
|
|
1084
|
+
description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing. On success, `verified:true` means verification passed but `verifiedFull:true` is the honest signal that every required leg (lint when configured, tests when a runner exists) genuinely ran — a false `verifiedFull` with `verified:true` means the pass is real but degraded (see `verify.degradedLegs`), not a full verification.',
|
|
1036
1085
|
inputSchema: {
|
|
1037
1086
|
type: 'object',
|
|
1038
1087
|
additionalProperties: false,
|
|
@@ -1073,6 +1122,27 @@ const apply_fix = {
|
|
|
1073
1122
|
partialSanitization: { type: 'boolean' },
|
|
1074
1123
|
},
|
|
1075
1124
|
},
|
|
1125
|
+
// FR-307/FR-1002: this schema had `additionalProperties: false`
|
|
1126
|
+
// and never declared `approval` — the property apply-fix-
|
|
1127
|
+
// service.js's high-impact-change gate has required since FR-307
|
|
1128
|
+
// was built. A real MCP caller supplying fixMeta.approval was
|
|
1129
|
+
// rejected by validate.js at the schema layer before the handler
|
|
1130
|
+
// ever ran, silently making the approval gate (and FR-1002's
|
|
1131
|
+
// identity check layered on it) unreachable from this tool's
|
|
1132
|
+
// only real production entry point. See D-0024.
|
|
1133
|
+
approval: {
|
|
1134
|
+
type: 'object',
|
|
1135
|
+
additionalProperties: false,
|
|
1136
|
+
properties: {
|
|
1137
|
+
approvedBy: { type: 'string', minLength: 1, maxLength: 200 },
|
|
1138
|
+
reason: { type: 'string', minLength: 1, maxLength: 1000 },
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
// FR-1003: separation-of-duties. Self-reported the same way
|
|
1142
|
+
// approvedBy is — this tool has no way to determine who actually
|
|
1143
|
+
// wrote a patch, so `author` is a claim, checked against a
|
|
1144
|
+
// configurable policy the same way `approval` is.
|
|
1145
|
+
author: { type: 'string', minLength: 1, maxLength: 200 },
|
|
1076
1146
|
},
|
|
1077
1147
|
},
|
|
1078
1148
|
},
|
|
@@ -1147,20 +1217,79 @@ const apply_fix = {
|
|
|
1147
1217
|
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
|
|
1148
1218
|
};
|
|
1149
1219
|
}
|
|
1220
|
+
// FR-307/FR-1002/D-0024: this caller-supplied-patch branch writes via
|
|
1221
|
+
// applyFixHistory() directly and never called applyVerifiedFix() — so
|
|
1222
|
+
// the high-impact-change approval gate (auth/authZ/crypto/PII/schema/
|
|
1223
|
+
// infra-privilege/public-API) built for the OTHER apply_fix branch
|
|
1224
|
+
// (stored fix.replacement) never ran here at all, for any input. Since
|
|
1225
|
+
// this is the branch the tool's own description calls the one that
|
|
1226
|
+
// covers "~100% of findings that ship only a template," that gap was
|
|
1227
|
+
// the larger of the two found this cycle. Same before/after content
|
|
1228
|
+
// shape `apply-fix-service.js` already uses — read-first-in-try/catch
|
|
1229
|
+
// (D-0012), never existsSync-then-readFileSync.
|
|
1230
|
+
const filesForMaterialClassification = {};
|
|
1231
|
+
for (const [rel, v] of Object.entries(confinedAbs)) {
|
|
1232
|
+
let before = '';
|
|
1233
|
+
try { before = await promises_.readFile(v.abs, 'utf8'); } catch { /* new file — before stays '' */ }
|
|
1234
|
+
filesForMaterialClassification[rel] = { before, after: v.content };
|
|
1235
|
+
}
|
|
1236
|
+
const materialClassification = (0,material_change/* classifyFixMaterialRisk */.kz)(filesForMaterialClassification);
|
|
1150
1237
|
if (dry_run) {
|
|
1151
|
-
return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary };
|
|
1238
|
+
return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
|
|
1239
|
+
}
|
|
1240
|
+
if (materialClassification.highImpactCategories.length) {
|
|
1241
|
+
const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
|
|
1242
|
+
const hasApprovalEvidence = !!(approval && typeof approval === 'object' &&
|
|
1243
|
+
typeof approval.approvedBy === 'string' && approval.approvedBy.trim().length > 0 &&
|
|
1244
|
+
typeof approval.reason === 'string' && approval.reason.trim().length > 0);
|
|
1245
|
+
if (!hasApprovalEvidence) {
|
|
1246
|
+
return {
|
|
1247
|
+
_meta: META, applied: false,
|
|
1248
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
|
|
1249
|
+
materialClassification,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
const approverRegistry = (0,approver_registry/* loadApproverRegistry */.P4)(ctx.sessionRoot);
|
|
1253
|
+
const requiredRoles = (0,approver_registry/* requiredRolesFor */.Kq)(approverRegistry, materialClassification.highImpactCategories);
|
|
1254
|
+
const identityCheck = (0,approver_registry/* verifyApprover */.$N)(approverRegistry, approval.approvedBy, requiredRoles);
|
|
1255
|
+
if (!identityCheck.verified) {
|
|
1256
|
+
return {
|
|
1257
|
+
_meta: META, applied: false,
|
|
1258
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
|
|
1259
|
+
materialClassification,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
// FR-1003: separation-of-duties, same no-op-unless-configured gate
|
|
1263
|
+
// as apply-fix-service.js's own copy — see approver-registry.js.
|
|
1264
|
+
const sodCheck = (0,approver_registry/* checkSeparationOfDuties */.I0)(approverRegistry, fixMeta?.author, approval.approvedBy);
|
|
1265
|
+
if (!sodCheck.ok) {
|
|
1266
|
+
return {
|
|
1267
|
+
_meta: META, applied: false,
|
|
1268
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
|
|
1269
|
+
materialClassification,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1152
1272
|
}
|
|
1153
1273
|
const written = [];
|
|
1154
1274
|
try {
|
|
1155
1275
|
for (const [rel, v] of Object.entries(confinedAbs)) {
|
|
1156
|
-
const
|
|
1276
|
+
const fileExisted = external_node_fs_.existsSync(v.abs);
|
|
1277
|
+
const originalContent = fileExisted ? await promises_.readFile(v.abs, 'utf8') : '';
|
|
1157
1278
|
const entry = await (0,fix_history/* applyFix */.oM)({
|
|
1158
|
-
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
|
|
1279
|
+
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content, fileExisted,
|
|
1159
1280
|
findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
|
|
1281
|
+
findingProvenance: f.findingProvenance || null,
|
|
1160
1282
|
});
|
|
1161
1283
|
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
1162
1284
|
}
|
|
1163
1285
|
} catch (e) {
|
|
1286
|
+
// FR-306: roll back every file THIS batch already wrote before the
|
|
1287
|
+
// failure — applyFixHistory already restored the one file that just
|
|
1288
|
+
// failed; this covers the rest, so a multi-file patch never leaves
|
|
1289
|
+
// some files patched and others not.
|
|
1290
|
+
for (const w of written) {
|
|
1291
|
+
try { await (0,fix_history/* revertEntryById */.rJ)(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
|
|
1292
|
+
}
|
|
1164
1293
|
if (e && e.name === 'FixAttemptBudgetExceededError') {
|
|
1165
1294
|
return { _meta: META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
|
|
1166
1295
|
}
|
|
@@ -1168,7 +1297,7 @@ const apply_fix = {
|
|
|
1168
1297
|
}
|
|
1169
1298
|
let acceptance = null;
|
|
1170
1299
|
try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* best-effort */ }
|
|
1171
|
-
return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance };
|
|
1300
|
+
return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
|
|
1172
1301
|
}
|
|
1173
1302
|
|
|
1174
1303
|
if (typeof f.fix?.replacement !== 'string') {
|
|
@@ -1205,41 +1334,47 @@ const apply_fix = {
|
|
|
1205
1334
|
};
|
|
1206
1335
|
}
|
|
1207
1336
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
maxAttempts: e.max,
|
|
1233
|
-
key: e.key,
|
|
1234
|
-
};
|
|
1337
|
+
// FR-301/A-08 (assurance-hardening PRD): this branch used to write
|
|
1338
|
+
// f.fix.replacement straight to disk with NO fresh verification — no
|
|
1339
|
+
// rescan, no lint, nothing confirming the stored replacement actually
|
|
1340
|
+
// closes the finding it claims to fix. The caller-patch branch above
|
|
1341
|
+
// already required this; there is no reason a STORED fix should be
|
|
1342
|
+
// trusted more than a caller-supplied one just because it shipped with
|
|
1343
|
+
// the finding. Routed through the same applyVerifiedFix() service the
|
|
1344
|
+
// CLI's `fix --apply` now also uses (src/fix/apply-fix-service.js) —
|
|
1345
|
+
// confinement/reserved-path are re-checked there too (harmless
|
|
1346
|
+
// redundancy with the dry_run preview above, kept for that preview's
|
|
1347
|
+
// size-diff shape) but the load-bearing addition is the verification
|
|
1348
|
+
// gate before the write.
|
|
1349
|
+
if (!f.stableId) {
|
|
1350
|
+
return { _meta: META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
|
|
1351
|
+
}
|
|
1352
|
+
const result = await (0,apply_fix_service/* applyVerifiedFix */.On)({
|
|
1353
|
+
scanRoot: ctx.sessionRoot,
|
|
1354
|
+
finding: f,
|
|
1355
|
+
files: { [f.file]: f.fix.replacement },
|
|
1356
|
+
fixMeta,
|
|
1357
|
+
});
|
|
1358
|
+
if (!result.ok) {
|
|
1359
|
+
if (result.budgetExceeded) {
|
|
1360
|
+
return { _meta: META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
|
|
1235
1361
|
}
|
|
1236
|
-
|
|
1362
|
+
return { _meta: META, applied: false, reason: result.reason, verify: result.verify || null };
|
|
1237
1363
|
}
|
|
1238
1364
|
// R25 (PRD §5): surface the running auto-fix acceptance rate after each
|
|
1239
1365
|
// applied fix, so the closed loop reports its own success metric.
|
|
1240
1366
|
let acceptance = null;
|
|
1241
1367
|
try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* metric is best-effort */ }
|
|
1242
|
-
|
|
1368
|
+
const entry = result.written[0];
|
|
1369
|
+
return {
|
|
1370
|
+
// FR-305: verifiedFull distinguishes "every required leg (lint, tests)
|
|
1371
|
+
// genuinely ran and passed" from "passed, but a required leg was
|
|
1372
|
+
// skipped or unavailable" — verified:true alone conflates them.
|
|
1373
|
+
_meta: META, applied: true, verified: true, verifiedFull: result.verifiedFull,
|
|
1374
|
+
historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
|
|
1375
|
+
integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
|
|
1376
|
+
verify: result.verify,
|
|
1377
|
+
};
|
|
1243
1378
|
},
|
|
1244
1379
|
};
|
|
1245
1380
|
|
package/dist/444.index.js
CHANGED
|
@@ -10,6 +10,7 @@ export const modules = {
|
|
|
10
10
|
/* harmony export */ });
|
|
11
11
|
/* unused harmony exports extractAddedLines, scanHistoryDiff */
|
|
12
12
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
13
|
+
/* harmony import */ var _util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8844);
|
|
13
14
|
// R15 (PRD §5) — git-history secret sweep.
|
|
14
15
|
//
|
|
15
16
|
// A secret removed from HEAD but present in any past commit is still
|
|
@@ -25,6 +26,7 @@ export const modules = {
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
|
|
28
30
|
// Pull the post-image (added) lines out of a unified diff: lines starting with
|
|
29
31
|
// a single '+' (not the '+++' file header). Returns reconstructed text.
|
|
30
32
|
function extractAddedLines(diffText) {
|
|
@@ -74,8 +76,15 @@ function sweepGitHistory(scanRoot, detectFn, { maxCommits = 50, timeoutMs = 2000
|
|
|
74
76
|
if (!scanRoot || typeof detectFn !== 'function') return [];
|
|
75
77
|
let out;
|
|
76
78
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// Second independent Finding Provenance PRD audit (FR-PROV-024): this
|
|
80
|
+
// scanRoot is a scanned repository, not this project's own trusted
|
|
81
|
+
// checkout. `--no-textconv` alone (the pre-existing hardening here) closes
|
|
82
|
+
// the .gitattributes textconv surface but NOT `core.fsmonitor` /
|
|
83
|
+
// `core.hooksPath` — this `log -p` call renders every historical commit's
|
|
84
|
+
// diff content, the same shape verified exploitable in
|
|
85
|
+
// provenance/git-evidence.js, so it gets the full hardening too.
|
|
86
|
+
out = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.execFileSync)('git', (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitArgs */ .Ax)(['-C', scanRoot, 'log', '-p', '-n', String(maxCommits), '--no-color', '--no-merges', '--no-textconv']),
|
|
87
|
+
{ encoding: 'utf8', maxBuffer: 96 * 1024 * 1024, timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], env: (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitEnv */ .Si)() });
|
|
79
88
|
} catch { return []; }
|
|
80
89
|
const parts = out.split(/^commit ([0-9a-f]{7,40})/m); // [pre, sha, body, sha, body, ...]
|
|
81
90
|
const findings = [];
|
package/dist/449.index.js
CHANGED
|
@@ -14,6 +14,7 @@ export const modules = {
|
|
|
14
14
|
/* harmony export */ });
|
|
15
15
|
/* unused harmony export findingsExceedingSLA */
|
|
16
16
|
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
|
|
17
|
+
/* harmony import */ var _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4594);
|
|
17
18
|
// 0.8.0 Feat-11: MTTR / finding-age tracking — per-finding firstSeenAt/lastSeenAt with SLA breach detection.
|
|
18
19
|
//
|
|
19
20
|
// Stamps every finding with `firstSeenAt` (preserved from the baseline if the
|
|
@@ -25,6 +26,29 @@ export const modules = {
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
|
|
30
|
+
// FR-PROV-019: "reports never show an age without its basis and confidence."
|
|
31
|
+
// FINDING_ORIGIN is the only basis backed by a resolved git commit for the
|
|
32
|
+
// finding's actual introduction; EARLIEST_OBSERVABLE is also git-derived but
|
|
33
|
+
// partial (weaker claim, no exact introduction commit). UNCOMMITTED and
|
|
34
|
+
// FIRST_OBSERVED are both wall-clock fallbacks — the age is a first-seen
|
|
35
|
+
// timestamp, never resolved against git history — and must say so honestly
|
|
36
|
+
// rather than read like a proven date.
|
|
37
|
+
function _ageBasisLabel(ageBasis, confidence) {
|
|
38
|
+
const level = confidence?.level && confidence.level !== 'unknown' ? confidence.level.toUpperCase() : null;
|
|
39
|
+
switch (ageBasis) {
|
|
40
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FINDING_ORIGIN:
|
|
41
|
+
return `proven origin${level ? `, ${level} confidence` : ''}`;
|
|
42
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.EARLIEST_OBSERVABLE:
|
|
43
|
+
return `earliest observable commit, partial history${level ? `, ${level} confidence` : ''}`;
|
|
44
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.UNCOMMITTED:
|
|
45
|
+
return 'uncommitted — first-seen fallback, origin not proven';
|
|
46
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED:
|
|
47
|
+
default:
|
|
48
|
+
return 'first-seen fallback — origin not proven';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
// Stable fingerprint for cross-scan finding identity. Mirrors the dedupe key.
|
|
29
53
|
// Exported so a caller can compute the "removed since baseline" (i.e. fixed)
|
|
30
54
|
// set that computeMTTR needs, using the exact same identity function
|
|
@@ -53,6 +77,25 @@ function stampFindingTimestamps(findings, baselineMap = new Map(), now = Date.no
|
|
|
53
77
|
f.lastSeenAt = nowIso;
|
|
54
78
|
const firstMs = Date.parse(f.firstSeenAt);
|
|
55
79
|
f.ageDays = Math.max(0, Math.floor((now - firstMs) / 86400000));
|
|
80
|
+
// FR-PROV-019: age/SLA basis. ageDays above stays pure wall-clock —
|
|
81
|
+
// every existing SLA/computeMTTR consumer keeps its current meaning.
|
|
82
|
+
// ageBasis + provenAgeDays are ADDITIVE: a report can show both and
|
|
83
|
+
// explain the discrepancy, never silently swap which number "age" means.
|
|
84
|
+
const status = f.findingProvenance?.status;
|
|
85
|
+
const origin = f.findingProvenance?.findingOrigin;
|
|
86
|
+
if (status === 'complete' && origin?.authorDate) {
|
|
87
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FINDING_ORIGIN;
|
|
88
|
+
f.provenAgeDays = Math.max(0, Math.floor((now - Date.parse(origin.authorDate)) / 86400000));
|
|
89
|
+
} else if (status === 'partial' && origin?.authorDate) {
|
|
90
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.EARLIEST_OBSERVABLE;
|
|
91
|
+
f.provenAgeDays = Math.max(0, Math.floor((now - Date.parse(origin.authorDate)) / 86400000));
|
|
92
|
+
} else if (status === 'uncommitted') {
|
|
93
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.UNCOMMITTED;
|
|
94
|
+
f.provenAgeDays = f.ageDays;
|
|
95
|
+
} else {
|
|
96
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED;
|
|
97
|
+
f.provenAgeDays = f.ageDays;
|
|
98
|
+
}
|
|
56
99
|
}
|
|
57
100
|
return findings;
|
|
58
101
|
}
|
|
@@ -84,28 +127,49 @@ function findingsExceedingSLA(findings, slaDays = null) {
|
|
|
84
127
|
});
|
|
85
128
|
}
|
|
86
129
|
|
|
87
|
-
// Median age (days) of the currently-open findings
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
// Median age (days) of the currently-open findings, PLUS the ageBasis/
|
|
131
|
+
// confidence of whichever finding landed on that median — a single-scan proxy
|
|
132
|
+
// for "how long has this debt been sitting". True MTTR (computeMTTR) needs the
|
|
133
|
+
// set of findings that were FIXED; this reports the open backlog's median age
|
|
134
|
+
// so a scan can show whether debt is getting older. Returns null on empty
|
|
135
|
+
// input. Local — surfaced only through renderSlaSummary (its sole consumer).
|
|
136
|
+
//
|
|
137
|
+
// FR-PROV-019: the days figure prefers `provenAgeDays` (git-derived when
|
|
138
|
+
// available) over the pure-wall-clock `ageDays`, and the ageBasis/confidence
|
|
139
|
+
// travel WITH the day count they describe — never a bare number with the
|
|
140
|
+
// basis looked up separately, which is how the original miss happened
|
|
141
|
+
// (ageBasis was stamped onto the finding but never reached the string that
|
|
142
|
+
// printed its age). Findings stamped by an older/test caller that never ran
|
|
143
|
+
// through stampFindingTimestamps (no ageBasis at all) degrade to the same
|
|
144
|
+
// honest "not proven" label FIRST_OBSERVED gets, never a false claim.
|
|
145
|
+
function medianOpenAge(findings) {
|
|
146
|
+
const entries = (findings || [])
|
|
147
|
+
.map(f => ({
|
|
148
|
+
days: f.provenAgeDays != null ? f.provenAgeDays : (f.ageDays || 0),
|
|
149
|
+
ageBasis: f.ageBasis || _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED,
|
|
150
|
+
confidence: f.findingProvenance?.confidence || null,
|
|
151
|
+
}))
|
|
152
|
+
.sort((a, b) => a.days - b.days);
|
|
153
|
+
if (!entries.length) return null;
|
|
154
|
+
return entries[Math.floor(entries.length / 2)];
|
|
96
155
|
}
|
|
97
156
|
|
|
98
157
|
// One-line SLA-breach summary for surfacing after a scan (#10). Returns null
|
|
99
|
-
// when nothing is past its per-severity SLA. Pairs with
|
|
158
|
+
// when nothing is past its per-severity SLA. Pairs with medianOpenAge for a
|
|
100
159
|
// "is my security debt aging" readout that the vibecoder can act on.
|
|
160
|
+
//
|
|
161
|
+
// FR-PROV-019: never prints the median age number without its basis and
|
|
162
|
+
// confidence alongside it — see medianOpenAge/_ageBasisLabel above.
|
|
101
163
|
function renderSlaSummary(findings, slaDays = null) {
|
|
102
164
|
const breached = findingsExceedingSLA(findings || [], slaDays);
|
|
103
165
|
if (!breached.length) return null;
|
|
104
166
|
const bySev = {};
|
|
105
167
|
for (const f of breached) bySev[f.severity] = (bySev[f.severity] || 0) + 1;
|
|
106
168
|
const parts = ['critical', 'high', 'medium', 'low', 'info'].filter(s => bySev[s]).map(s => `${bySev[s]} ${s}`);
|
|
107
|
-
const median =
|
|
108
|
-
const ageNote = median != null
|
|
169
|
+
const median = medianOpenAge(findings);
|
|
170
|
+
const ageNote = median != null
|
|
171
|
+
? ` (median open age ${median.days}d, ${_ageBasisLabel(median.ageBasis, median.confidence)})`
|
|
172
|
+
: '';
|
|
109
173
|
return `${breached.length} finding(s) past remediation SLA: ${parts.join(', ')}${ageNote}`;
|
|
110
174
|
}
|
|
111
175
|
|