@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.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 +390 -0
- package/bin/agentic-security.js +3813 -83
- package/dist/1122.index.js +702 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +13 -4
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/2271.index.js +165 -0
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +307 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +14 -6
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +14 -6
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +67 -3
- package/dist/5051.index.js +770 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +29 -7
- package/dist/{449.index.js → 5830.index.js} +78 -14
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +26 -8
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +580 -187
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/package.json +23 -10
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +2 -2
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +580 -30
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +22 -1
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +140 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +76 -0
- package/src/posture/auditor-walkthrough.js +192 -13
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/compliance-policy.js +12 -2
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/fix-history.js +25 -2
- package/src/posture/fix-verify.js +9 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/git-history.js +13 -5
- package/src/posture/material-change.js +21 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +14 -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/remediation-ledger.js +337 -0
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +38 -14
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/report/index.js +197 -3
- package/src/runScan.js +34 -5
- package/src/sast/rate-limit.js +33 -3
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +139 -0
- package/src/util/git-hardening.js +128 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
export const id = 5561;
|
|
2
|
+
export const ids = [5561];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 5561:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ evaluateVerificationEvidence: () => (/* binding */ evaluateVerificationEvidence),
|
|
10
|
+
/* harmony export */ foldRemediationLedger: () => (/* binding */ foldRemediationLedger),
|
|
11
|
+
/* harmony export */ validateOpenPayload: () => (/* binding */ validateOpenPayload),
|
|
12
|
+
/* harmony export */ validateTransition: () => (/* binding */ validateTransition)
|
|
13
|
+
/* harmony export */ });
|
|
14
|
+
/* unused harmony exports REMEDIATION_STATES, REMEDIATION_EVENT_TYPES, ACCEPTED_RISK_REQUIRED_FIELDS, foldRemediationItem */
|
|
15
|
+
// remediation.js — M5 deliverable #6 (Blast-Radius: Remediation Command
|
|
16
|
+
// Center, FR-507 + AC-31): the pure RemediationItem contract, its event
|
|
17
|
+
// fold, its state machine, and the verification-evidence decision.
|
|
18
|
+
//
|
|
19
|
+
// Pure by contract — zero imports, no fs, no I/O of any kind, never
|
|
20
|
+
// throws. The ledger layer (`posture/remediation-ledger.js`, Task 2 of
|
|
21
|
+
// this same sub-project) owns every side effect: file locking, append,
|
|
22
|
+
// tolerant read, hash chaining. This module mirrors `governance-edit.js`'s
|
|
23
|
+
// own split (that module's own header makes the identical claim for its
|
|
24
|
+
// own CLI-write boundary) and the "pure data + pure functions, boundary-
|
|
25
|
+
// tested" precedent `flow-grade.js`/`obligation-mapping.js` already set
|
|
26
|
+
// in this package.
|
|
27
|
+
//
|
|
28
|
+
// ── The `verified`-unreachable-from-`state_changed` rule (AC-31) ──────
|
|
29
|
+
//
|
|
30
|
+
// AC-31's own `then`-clause requires that marking remediation work
|
|
31
|
+
// "done" can NEVER, by itself, mark a finding verified — only a real
|
|
32
|
+
// scan re-confirming the flow is gone (`scan_verification`) or an
|
|
33
|
+
// explicitly-permitted, explicitly-attributed manual attestation
|
|
34
|
+
// (`manual_attestation`) may transition an item into `verified`. PRD
|
|
35
|
+
// line 171 states the same rule as an explicit non-goal: this deliverable
|
|
36
|
+
// must never let "I fixed it" (a human claim) substitute for "the scanner
|
|
37
|
+
// re-observed the flow is gone" (an evidence-backed claim) without an
|
|
38
|
+
// explicit, auditable manual-attestation opt-in. `validateTransition`
|
|
39
|
+
// enforces this by rejecting `state_changed` with `state: 'verified'`
|
|
40
|
+
// FIRST, before even looking at the item's current state — the rejection
|
|
41
|
+
// is therefore genuinely unconditional, not just unreached from the
|
|
42
|
+
// states this module happens to reach today.
|
|
43
|
+
//
|
|
44
|
+
// ── Deliberate scope narrowing in evaluateVerificationEvidence ────────
|
|
45
|
+
//
|
|
46
|
+
// A required-evidence flow id is treated as SATISFIED only when it
|
|
47
|
+
// appears in `diff.removed.flows` with `causeClassification ===
|
|
48
|
+
// 'application_change'` — i.e. the flow is genuinely gone from the graph
|
|
49
|
+
// and its disappearance is attributable to an application change, not to
|
|
50
|
+
// the scanner simply seeing less than it used to. A flow that is still
|
|
51
|
+
// PRESENT in the graph but has merely become MORE PROTECTED (e.g. its
|
|
52
|
+
// `protectionSummary` moved from `unprotected` to `protected` — visible
|
|
53
|
+
// only as a `changed.flows` entry, never a `removed.flows` entry) is
|
|
54
|
+
// explicitly OUT OF SCOPE for this first cut: recognizing an "improving
|
|
55
|
+
// transition" as evidence of a fix needs its own deliberate, disclosed
|
|
56
|
+
// heuristic (what counts as "protected enough", whether a policy verdict
|
|
57
|
+
// change alone should count, etc.) that this module does not attempt.
|
|
58
|
+
// A future increment could add it; until then, such a flow reports
|
|
59
|
+
// `flows_still_present`, same as a flow with no change at all.
|
|
60
|
+
//
|
|
61
|
+
// `possible_coverage_regression` is computed DIFF-WIDE by
|
|
62
|
+
// `computeGraphDiff` (see `graph-diff.js:328`'s own
|
|
63
|
+
// `coverageRegressionReasons`/`flowRemovalCause` — every removed flow in
|
|
64
|
+
// one diff shares the SAME cause once any completeness signal regressed)
|
|
65
|
+
// — which is why a coverage-regression hit on any of the item's OWN
|
|
66
|
+
// required-evidence flows refuses the WHOLE verification immediately (PRD
|
|
67
|
+
// line 1975), rather than letting other, seemingly-clean required-
|
|
68
|
+
// evidence flows in the same diff verify: an incomplete scan cannot be
|
|
69
|
+
// trusted to have honestly seen everything it claims to have not seen.
|
|
70
|
+
// (The loop below only ever inspects `diff.removed.flows` entries for
|
|
71
|
+
// flows actually named in `requiredEvidenceFlowIds` — an unrelated
|
|
72
|
+
// removed flow elsewhere in the same diff is never consulted.)
|
|
73
|
+
|
|
74
|
+
const REMEDIATION_STATES = Object.freeze([
|
|
75
|
+
'open', 'in_progress', 'awaiting_verification', 'verified', 'accepted_risk', 'reopened',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const REMEDIATION_EVENT_TYPES = Object.freeze([
|
|
79
|
+
'opened', 'state_changed', 'scan_verification', 'manual_attestation', 'accepted_risk', 'reopened',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// PRD line 572's exact four-field list (scoping doc §4.3).
|
|
83
|
+
const ACCEPTED_RISK_REQUIRED_FIELDS = Object.freeze(['approver', 'reason', 'scope', 'expiration']);
|
|
84
|
+
|
|
85
|
+
function _isNonEmptyString(v) {
|
|
86
|
+
return typeof v === 'string' && v.length > 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _isPlainObject(v) {
|
|
90
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── foldRemediationItem / foldRemediationLedger ────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Folds an ordered event list for ONE remediation item into its current
|
|
97
|
+
* shape, or `null` for an empty/malformed/non-`opened`-first list. Never
|
|
98
|
+
* throws.
|
|
99
|
+
*/
|
|
100
|
+
function foldRemediationItem(events) {
|
|
101
|
+
if (!Array.isArray(events) || events.length === 0) return null;
|
|
102
|
+
const openEvent = events[0];
|
|
103
|
+
if (!openEvent || openEvent.type !== 'opened') return null;
|
|
104
|
+
|
|
105
|
+
const item = {
|
|
106
|
+
id: openEvent.id,
|
|
107
|
+
state: 'open',
|
|
108
|
+
owner: openEvent.owner,
|
|
109
|
+
dueDate: openEvent.dueDate,
|
|
110
|
+
recommendedControl: openEvent.recommendedControl,
|
|
111
|
+
assessment: openEvent.assessment,
|
|
112
|
+
affectedFlowIds: openEvent.affectedFlowIds,
|
|
113
|
+
affectedNodeIds: openEvent.affectedNodeIds,
|
|
114
|
+
affectedEdgeIds: openEvent.affectedEdgeIds,
|
|
115
|
+
requiredEvidence: openEvent.requiredEvidence,
|
|
116
|
+
manualAttestationPermitted: openEvent.manualAttestationPermitted,
|
|
117
|
+
approvals: [],
|
|
118
|
+
exceptions: [],
|
|
119
|
+
verificationSnapshotId: null,
|
|
120
|
+
history: events,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (let i = 1; i < events.length; i++) {
|
|
124
|
+
const ev = events[i];
|
|
125
|
+
if (!ev || typeof ev !== 'object') continue;
|
|
126
|
+
switch (ev.type) {
|
|
127
|
+
case 'state_changed':
|
|
128
|
+
item.state = ev.state;
|
|
129
|
+
break;
|
|
130
|
+
case 'scan_verification':
|
|
131
|
+
if (ev.outcome === 'verified') {
|
|
132
|
+
item.state = 'verified';
|
|
133
|
+
item.verificationSnapshotId = ev.snapshotId;
|
|
134
|
+
}
|
|
135
|
+
// an 'unverifiable' outcome changes neither state nor snapshot.
|
|
136
|
+
break;
|
|
137
|
+
case 'manual_attestation':
|
|
138
|
+
item.state = 'verified';
|
|
139
|
+
item.approvals.push({
|
|
140
|
+
approver: ev.approver, reason: ev.reason, at: ev.at, evidenceKind: 'manual',
|
|
141
|
+
});
|
|
142
|
+
// Records a real baseline (final-review fix round 1, Blocking-3):
|
|
143
|
+
// without this, a manually-attested item keeps whatever STALE
|
|
144
|
+
// `verificationSnapshotId` it happened to carry (or `null`,
|
|
145
|
+
// falling back to an even less defensible baseline) forever, and
|
|
146
|
+
// `reopen-check` keeps diffing from that stale anchor — reopening
|
|
147
|
+
// a just-permitted attestation on the very next run even though
|
|
148
|
+
// nothing changed. `ev.snapshotId` is optional (a manual
|
|
149
|
+
// attestation before any lineage scan has ever run is legitimate).
|
|
150
|
+
if (ev.snapshotId) item.verificationSnapshotId = ev.snapshotId;
|
|
151
|
+
break;
|
|
152
|
+
case 'accepted_risk':
|
|
153
|
+
item.state = 'accepted_risk';
|
|
154
|
+
item.exceptions.push({
|
|
155
|
+
approver: ev.approver, reason: ev.reason, scope: ev.scope, expiration: ev.expiration, at: ev.at,
|
|
156
|
+
});
|
|
157
|
+
break;
|
|
158
|
+
case 'reopened':
|
|
159
|
+
item.state = 'reopened';
|
|
160
|
+
// Retires the stale anchor (final-review fix round 1, Blocking-3)
|
|
161
|
+
// so it cannot outlive the verification it belonged to — the next
|
|
162
|
+
// verification (scan or manual) must establish its own baseline.
|
|
163
|
+
item.verificationSnapshotId = null;
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
// an unrecognized event type is ignored by the fold — validation
|
|
167
|
+
// of proposed events is validateTransition's job, not this one's.
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return item;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Groups a flat, interleaved event stream by item id (`ev.itemId`,
|
|
177
|
+
* falling back to `ev.id` on an `opened` event that carries only `id`),
|
|
178
|
+
* preserving per-group insertion order, and folds each group. Events with
|
|
179
|
+
* no resolvable item id are skipped. Groups that fold to `null` are
|
|
180
|
+
* dropped. Never throws.
|
|
181
|
+
*/
|
|
182
|
+
function foldRemediationLedger(allEvents) {
|
|
183
|
+
const result = {};
|
|
184
|
+
if (!Array.isArray(allEvents)) return result;
|
|
185
|
+
|
|
186
|
+
const groups = new Map();
|
|
187
|
+
for (const ev of allEvents) {
|
|
188
|
+
if (!ev || typeof ev !== 'object') continue;
|
|
189
|
+
const itemId = ev.itemId ?? (ev.type === 'opened' ? ev.id : undefined);
|
|
190
|
+
if (!_isNonEmptyString(itemId)) continue;
|
|
191
|
+
if (!groups.has(itemId)) groups.set(itemId, []);
|
|
192
|
+
groups.get(itemId).push(ev);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const [itemId, events] of groups) {
|
|
196
|
+
const item = foldRemediationItem(events);
|
|
197
|
+
if (item !== null) result[itemId] = item;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── validateOpenPayload ─────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
const _REQUIRED_ASSESSMENT_FIELDS = [
|
|
206
|
+
'assessmentId', 'targetId', 'targetKind', 'traceKind', 'scope', 'graphId', 'graphDigest', 'snapshotId',
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const _DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validates an `opened`-event-shaped payload. Returns `{valid, errors}`,
|
|
213
|
+
* `errors` as `[{field, message}]`. Never throws.
|
|
214
|
+
*/
|
|
215
|
+
function validateOpenPayload(payload) {
|
|
216
|
+
const errors = [];
|
|
217
|
+
const err = (field, message) => errors.push({ field, message });
|
|
218
|
+
|
|
219
|
+
if (!_isPlainObject(payload)) {
|
|
220
|
+
err('(payload)', 'payload must be an object');
|
|
221
|
+
return { valid: false, errors };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const field of ['id', 'owner', 'recommendedControl']) {
|
|
225
|
+
if (!_isNonEmptyString(payload[field])) err(field, `${field} is required and must be a non-empty string`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!_isNonEmptyString(payload.dueDate) || !_DATE_RE.test(payload.dueDate)) {
|
|
229
|
+
err('dueDate', 'dueDate is required and must be a YYYY-MM-DD date');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!_isPlainObject(payload.assessment)) {
|
|
233
|
+
err('assessment', 'assessment is required and must be an object');
|
|
234
|
+
} else {
|
|
235
|
+
for (const field of _REQUIRED_ASSESSMENT_FIELDS) {
|
|
236
|
+
if (!_isNonEmptyString(payload.assessment[field])) {
|
|
237
|
+
err(`assessment.${field}`, `assessment.${field} is required and must be a non-empty string`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// assessmentPath is the one OPTIONAL inline field — no check.
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const evidence = payload.requiredEvidence;
|
|
244
|
+
if (!Array.isArray(evidence) || evidence.length === 0 || !evidence.every(_isNonEmptyString)) {
|
|
245
|
+
err('requiredEvidence', 'requiredEvidence is required and must be a non-empty array of non-empty strings');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { valid: errors.length === 0, errors };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── validateTransition — AC-31's own state machine ──────────────────────
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Validates a proposed event against a remediation item's current state
|
|
255
|
+
* (or `null` for no item yet). Returns `{valid, errors}`, `errors` as
|
|
256
|
+
* `[{field, message}]`. Never throws.
|
|
257
|
+
*/
|
|
258
|
+
function validateTransition(item, proposedEvent) {
|
|
259
|
+
const errors = [];
|
|
260
|
+
const err = (field, message) => errors.push({ field, message });
|
|
261
|
+
|
|
262
|
+
if (!_isPlainObject(proposedEvent)) {
|
|
263
|
+
err('(event)', 'proposed event must be an object');
|
|
264
|
+
return { valid: false, errors };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const type = proposedEvent.type;
|
|
268
|
+
|
|
269
|
+
if (type === 'opened') {
|
|
270
|
+
if (item !== null && item !== undefined) {
|
|
271
|
+
err('type', 'a remediation item with this id already exists — opened is only valid when no item exists');
|
|
272
|
+
return { valid: false, errors };
|
|
273
|
+
}
|
|
274
|
+
return { valid: true, errors: [] };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// AC-31's own load-bearing rule: state_changed can never reach
|
|
278
|
+
// `verified`. Checked FIRST, before even the "item must exist" check
|
|
279
|
+
// below, so the rejection is genuinely unconditional — reachable from
|
|
280
|
+
// no state at all, including no item — and cannot be routed around by
|
|
281
|
+
// any current state. See this file's own header.
|
|
282
|
+
if (type === 'state_changed' && proposedEvent.state === 'verified') {
|
|
283
|
+
err('state', 'state_changed can never reach verified — only scan_verification or manual_attestation may');
|
|
284
|
+
return { valid: false, errors };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Every other event type requires an existing item.
|
|
288
|
+
if (!_isPlainObject(item)) {
|
|
289
|
+
err('type', `${type} is not valid against no item — open the item first`);
|
|
290
|
+
return { valid: false, errors };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
switch (type) {
|
|
294
|
+
case 'state_changed': {
|
|
295
|
+
if (proposedEvent.state === 'in_progress') {
|
|
296
|
+
if (item.state !== 'open' && item.state !== 'reopened') {
|
|
297
|
+
err('state', `state_changed to in_progress is only valid from open or reopened, not ${item.state}`);
|
|
298
|
+
}
|
|
299
|
+
} else if (proposedEvent.state === 'awaiting_verification') {
|
|
300
|
+
if (item.state !== 'in_progress') {
|
|
301
|
+
err('state', `state_changed to awaiting_verification is only valid from in_progress, not ${item.state}`);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
err('state', `state_changed to ${JSON.stringify(proposedEvent.state)} is not a recognized transition`);
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case 'scan_verification': {
|
|
310
|
+
if (item.state !== 'awaiting_verification') {
|
|
311
|
+
err('type', `scan_verification is only valid from awaiting_verification, not ${item.state}`);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
if (proposedEvent.outcome === 'verified') {
|
|
315
|
+
if (!_isNonEmptyString(proposedEvent.snapshotId)) {
|
|
316
|
+
err('snapshotId', 'a verified outcome requires a snapshotId');
|
|
317
|
+
}
|
|
318
|
+
} else if (proposedEvent.outcome === 'unverifiable') {
|
|
319
|
+
// no further requirement beyond the outcome itself.
|
|
320
|
+
} else {
|
|
321
|
+
err('outcome', `outcome ${JSON.stringify(proposedEvent.outcome)} is not a recognized scan_verification outcome`);
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case 'manual_attestation': {
|
|
327
|
+
if (item.state !== 'awaiting_verification') {
|
|
328
|
+
err('type', `manual_attestation is only valid from awaiting_verification, not ${item.state}`);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
if (!item.manualAttestationPermitted) {
|
|
332
|
+
err('manualAttestationPermitted', 'manual attestation is not permitted for this item — it must be opened with --allow-manual-attestation to allow one (open a new item if this one predates that need)');
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (!_isNonEmptyString(proposedEvent.approver)) err('approver', 'manual_attestation requires a non-empty approver');
|
|
336
|
+
if (!_isNonEmptyString(proposedEvent.reason)) err('reason', 'manual_attestation requires a non-empty reason');
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case 'accepted_risk': {
|
|
341
|
+
if (item.state === 'verified' || item.state === 'accepted_risk') {
|
|
342
|
+
err('type', `accepted_risk is not valid from ${item.state}`);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
for (const field of ACCEPTED_RISK_REQUIRED_FIELDS) {
|
|
346
|
+
if (!_isNonEmptyString(proposedEvent[field])) {
|
|
347
|
+
err(field, `accepted_risk requires a non-empty ${field}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'reopened': {
|
|
354
|
+
if (item.state !== 'verified') {
|
|
355
|
+
err('type', `reopened is only valid from verified, not ${item.state}`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
if (!_isNonEmptyString(proposedEvent.reason)) {
|
|
359
|
+
err('reason', 'reopened must always carry a non-empty reason naming what triggered it');
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
default:
|
|
365
|
+
err('type', `${JSON.stringify(type)} is not a recognized remediation event type`);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { valid: errors.length === 0, errors };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── evaluateVerificationEvidence ────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Decides whether a GraphDiff's real, checkable evidence satisfies every
|
|
376
|
+
* flow id a remediation item required evidence for. See this file's own
|
|
377
|
+
* header for the deliberate scope narrowing and the coverage-regression
|
|
378
|
+
* whole-verification-refusal rule. Never throws.
|
|
379
|
+
*/
|
|
380
|
+
function evaluateVerificationEvidence(diff, requiredEvidenceFlowIds) {
|
|
381
|
+
if (!Array.isArray(requiredEvidenceFlowIds) || requiredEvidenceFlowIds.length === 0) {
|
|
382
|
+
return { outcome: 'unverifiable', reason: 'no_required_evidence' };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const removedFlows = diff?.removed?.flows;
|
|
386
|
+
const removedById = new Map();
|
|
387
|
+
if (Array.isArray(removedFlows)) {
|
|
388
|
+
for (const entry of removedFlows) {
|
|
389
|
+
if (entry && typeof entry.id === 'string') removedById.set(entry.id, entry);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const unsatisfiedFlowIds = [];
|
|
394
|
+
|
|
395
|
+
for (const flowId of requiredEvidenceFlowIds) {
|
|
396
|
+
const entry = removedById.get(flowId);
|
|
397
|
+
const cause = entry?.causeClassification;
|
|
398
|
+
|
|
399
|
+
if (cause === 'possible_coverage_regression') {
|
|
400
|
+
return {
|
|
401
|
+
outcome: 'unverifiable',
|
|
402
|
+
reason: 'possible_coverage_regression',
|
|
403
|
+
flowId,
|
|
404
|
+
coverageRegressionReasons: entry.coverageRegressionReasons,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (cause === 'reidentified') {
|
|
409
|
+
return {
|
|
410
|
+
outcome: 'unverifiable',
|
|
411
|
+
reason: 'reidentified',
|
|
412
|
+
flowId,
|
|
413
|
+
reidentifiedTo: entry.reidentifiedTo,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (cause === 'application_change') {
|
|
418
|
+
continue; // satisfied
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Absent from removed.flows entirely, or an unrecognized
|
|
422
|
+
// classification — either way, not satisfied.
|
|
423
|
+
unsatisfiedFlowIds.push(flowId);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (unsatisfiedFlowIds.length > 0) {
|
|
427
|
+
return { outcome: 'unverifiable', reason: 'flows_still_present', unsatisfiedFlowIds };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return { outcome: 'verified' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
/***/ })
|
|
435
|
+
|
|
436
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const id =
|
|
2
|
-
export const ids = [
|
|
1
|
+
export const id = 5637;
|
|
2
|
+
export const ids = [5637];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
5
|
/***/ 5637:
|
|
@@ -11,7 +11,8 @@ export const modules = {
|
|
|
11
11
|
/* harmony export */ renderPrDeltaText: () => (/* binding */ renderPrDeltaText)
|
|
12
12
|
/* harmony export */ });
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
|
-
/* harmony import */ var
|
|
14
|
+
/* harmony import */ var _util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8844);
|
|
15
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9753);
|
|
15
16
|
// Shadowscan / security-DELTA on PR (v0.72).
|
|
16
17
|
//
|
|
17
18
|
// Most SAST PR-comment integrations show absolute counts — "12 findings
|
|
@@ -40,16 +41,25 @@ export const modules = {
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
|
|
43
45
|
const FILE_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|java|cs|kt|go|rb|php|sol|swift|rs|tf|yml|yaml|json|toml|md)$/i;
|
|
44
46
|
const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
45
47
|
|
|
48
|
+
// `root` is the PR's repository — a scan target, not this project's own
|
|
49
|
+
// trusted checkout — hardened per FR-PROV-024 / the second Finding
|
|
50
|
+
// Provenance PRD audit (same exposure class as
|
|
51
|
+
// provenance/git-evidence.js's `_run`).
|
|
46
52
|
function _git(root, args) {
|
|
47
|
-
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
53
|
+
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)('git', (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitArgs */ .Ax)(args), { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitEnv */ .Si)() });
|
|
48
54
|
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function _readFileAtRef(root, ref, file) {
|
|
52
|
-
|
|
58
|
+
// `--no-textconv`: this blob-cat form of `show` (`<ref>:<file>`, not a
|
|
59
|
+
// diff) was verified NOT reachable via a hostile textconv driver in
|
|
60
|
+
// current git, same as git-evidence.js's getBlobAtCommit — kept for
|
|
61
|
+
// defense-in-depth/uniformity.
|
|
62
|
+
const r = _git(root, ['show', '--no-textconv', `${ref}:${file}`]);
|
|
53
63
|
return r.ok ? r.stdout : null;
|
|
54
64
|
}
|
|
55
65
|
|
|
@@ -70,7 +80,12 @@ async function _scanAtRef(root, ref) {
|
|
|
70
80
|
const c = _readFileAtRef(root, ref, f);
|
|
71
81
|
if (c != null) fileContents[f] = c;
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
// `provenance:false` — a base-ref snapshot, not the current working state.
|
|
84
|
+
// Beyond the wasted git walks, updateLifecycle marks every open stableId
|
|
85
|
+
// absent from the finding set it is handed as `remediated`; running the PR
|
|
86
|
+
// delta gate would silently rewrite the project's lifecycle store from the
|
|
87
|
+
// base ref's findings.
|
|
88
|
+
return (0,_engine_js__WEBPACK_IMPORTED_MODULE_2__/* .runFullScan */ .wW)({ fileContents, scanRoot: root, provenance: false }, () => {});
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
function _summary(findings) {
|
|
@@ -85,7 +100,14 @@ function _summary(findings) {
|
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
function _changedFiles(root, baseRef, headRef) {
|
|
88
|
-
|
|
103
|
+
// `--no-ext-diff`: found during the second-audit-remediation sweep — this
|
|
104
|
+
// is a `git diff` call site (`--name-only`, no content rendered, so not
|
|
105
|
+
// itself a verified exploit path today, same as runScan.js's
|
|
106
|
+
// `changedSince`), but every `diff` invocation in this codebase gets it
|
|
107
|
+
// uniformly per the live exploit VERIFIED in material-change.js's
|
|
108
|
+
// classifyGitDiff (external diff drivers fire on `git diff` even with
|
|
109
|
+
// `--no-textconv`, which is a surface `--no-textconv` alone does not close).
|
|
110
|
+
const r = _git(root, ['diff', '--name-only', '--no-ext-diff', `${baseRef}...${headRef}`]);
|
|
89
111
|
if (!r.ok) return new Set();
|
|
90
112
|
return new Set(r.stdout.trim().split('\n').filter(Boolean));
|
|
91
113
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const id =
|
|
2
|
-
export const ids = [
|
|
1
|
+
export const id = 5830;
|
|
2
|
+
export const ids = [5830];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
5
|
/***/ 5830:
|
|
@@ -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
|
|