@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +555 -0
  2. package/bin/agentic-security.js +770 -49
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +117 -18
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +24 -6
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/271.index.js +165 -0
  10. package/dist/384.index.js +1 -1
  11. package/dist/435.index.js +187 -52
  12. package/dist/444.index.js +11 -2
  13. package/dist/449.index.js +76 -12
  14. package/dist/526.index.js +117 -18
  15. package/dist/552.index.js +97 -0
  16. package/dist/637.index.js +27 -5
  17. package/dist/730.index.js +311 -0
  18. package/dist/736.index.js +301 -0
  19. package/dist/824.index.js +7 -0
  20. package/dist/905.index.js +88 -22
  21. package/dist/920.index.js +491 -0
  22. package/dist/970.index.js +173 -0
  23. package/dist/agentic-security.mjs +13 -13
  24. package/dist/agentic-security.mjs.sha256 +1 -1
  25. package/dist/calibration-seed.json +2 -0
  26. package/package.json +23 -9
  27. package/src/compare.js +6 -1
  28. package/src/dataflow/CLAUDE.md +1 -1
  29. package/src/dataflow/index.js +18 -0
  30. package/src/dataflow/privacy-catalog.js +290 -0
  31. package/src/dataflow/privacy-deep-walker.js +515 -0
  32. package/src/dataflow/privacy-governance.js +126 -0
  33. package/src/dataflow/privacy-inventory.js +154 -0
  34. package/src/dataflow/privacy-sink-policy.js +125 -0
  35. package/src/dataflow/privacy-taint.js +115 -54
  36. package/src/dataflow/privacy-taxonomy.js +233 -0
  37. package/src/discovery/disprove.js +7 -3
  38. package/src/discovery/hunter.js +9 -5
  39. package/src/discovery/index.js +2 -2
  40. package/src/discovery/llm-invoke.js +69 -13
  41. package/src/egress/audit.js +147 -0
  42. package/src/egress/policy.js +313 -0
  43. package/src/egress/redact.js +180 -0
  44. package/src/engine.js +1048 -302
  45. package/src/fix/apply-fix-service.js +404 -0
  46. package/src/fix/approver-registry.js +157 -0
  47. package/src/history-scan.js +22 -5
  48. package/src/ir/CLAUDE.md +1 -1
  49. package/src/llm-validator/index.js +86 -9
  50. package/src/llm-validator/model-status.js +66 -0
  51. package/src/lsp/server.js +49 -2
  52. package/src/mcp/tools.js +177 -50
  53. package/src/pipeline/analyzer-supervisor.js +93 -0
  54. package/src/pipeline/analyzer-worker.js +26 -0
  55. package/src/pipeline/annotator-runner.js +33 -0
  56. package/src/pipeline/assurance-mode.js +154 -0
  57. package/src/pipeline/cascade-worker-pool.js +172 -0
  58. package/src/pipeline/cascade-worker.js +43 -0
  59. package/src/pipeline/coverage-ledger.js +0 -0
  60. package/src/pipeline/detector-runner.js +51 -0
  61. package/src/pipeline/enrichment-completion.js +58 -0
  62. package/src/pipeline/evidence-provenance.js +91 -0
  63. package/src/pipeline/finding-schema.js +108 -0
  64. package/src/pipeline/legacy-compat.js +101 -0
  65. package/src/pipeline/producer-collector.js +48 -0
  66. package/src/pipeline/producer-registry.js +112 -0
  67. package/src/pipeline/scan-health.js +144 -0
  68. package/src/posture/CLAUDE.md +123 -0
  69. package/src/posture/accuracy-scorecard.js +156 -1
  70. package/src/posture/adversary-agent.js +15 -3
  71. package/src/posture/artifact-registry.js +241 -0
  72. package/src/posture/auditor-walkthrough.js +186 -21
  73. package/src/posture/calibration-feedback.js +201 -0
  74. package/src/posture/calibration-seed.json +2 -0
  75. package/src/posture/calibration.js +25 -0
  76. package/src/posture/compliance-evidence-signing.js +131 -0
  77. package/src/posture/compliance-policy.js +324 -17
  78. package/src/posture/cross-repo-memory.js +7 -2
  79. package/src/posture/custom-rules.js +36 -0
  80. package/src/posture/deterministic.js +8 -1
  81. package/src/posture/encryption-provider.js +205 -0
  82. package/src/posture/evidence-grade-wording.js +71 -0
  83. package/src/posture/fix-history.js +137 -20
  84. package/src/posture/fix-honesty-gate.js +47 -6
  85. package/src/posture/fix-verify.js +65 -8
  86. package/src/posture/fleet.js +0 -0
  87. package/src/posture/flow-narration.js +7 -2
  88. package/src/posture/git-history.js +13 -5
  89. package/src/posture/legal-hold.js +140 -0
  90. package/src/posture/llm-redteam.js +10 -1
  91. package/src/posture/material-change.js +111 -2
  92. package/src/posture/mttr.js +75 -12
  93. package/src/posture/policy-bundle.js +274 -0
  94. package/src/posture/pre-incident-archaeology.js +39 -7
  95. package/src/posture/privacy-framework.js +47 -6
  96. package/src/posture/production-feedback.js +179 -0
  97. package/src/posture/provenance/ai-authorship.js +68 -0
  98. package/src/posture/provenance/branch-entry.js +80 -0
  99. package/src/posture/provenance/cache.js +143 -0
  100. package/src/posture/provenance/confidence.js +36 -0
  101. package/src/posture/provenance/coordinator.js +786 -0
  102. package/src/posture/provenance/dag-walk.js +249 -0
  103. package/src/posture/provenance/evidence-attribution.js +59 -0
  104. package/src/posture/provenance/git-evidence.js +310 -0
  105. package/src/posture/provenance/lifecycle.js +208 -0
  106. package/src/posture/provenance/missing-control-resolver.js +137 -0
  107. package/src/posture/provenance/origin-resolver.js +342 -0
  108. package/src/posture/provenance/predicate-replay.js +133 -0
  109. package/src/posture/provenance/providers/config.js +39 -0
  110. package/src/posture/provenance/providers/github.js +62 -0
  111. package/src/posture/provenance/providers/gitlab.js +58 -0
  112. package/src/posture/provenance/repo-lineage.js +74 -0
  113. package/src/posture/provenance/sca-origin.js +139 -0
  114. package/src/posture/provenance/schema.js +255 -0
  115. package/src/posture/provenance/transitive-sca.js +147 -0
  116. package/src/posture/provenance/validate.js +30 -0
  117. package/src/posture/provenance-evidence-bundle.js +144 -0
  118. package/src/posture/retention-policy.js +132 -0
  119. package/src/posture/risk-dollars.js +216 -26
  120. package/src/posture/sbom-diff.js +15 -2
  121. package/src/posture/scan-checkpoint.js +176 -31
  122. package/src/posture/secret-history.js +10 -2
  123. package/src/posture/state-dir.js +64 -5
  124. package/src/posture/state-lifecycle-report.js +77 -0
  125. package/src/posture/suppressions.js +59 -3
  126. package/src/posture/vuln-archaeology.js +8 -2
  127. package/src/pr-delta.js +25 -4
  128. package/src/privacy/ir-adapter.js +380 -0
  129. package/src/report/index.js +248 -5
  130. package/src/report/oscal.js +7 -2
  131. package/src/runScan.js +34 -5
  132. package/src/sast/cpp.js +3 -14
  133. package/src/sast/rate-limit.js +33 -3
  134. package/src/sca/llm-function-extract.js +6 -0
  135. package/src/util/git-hardening.js +128 -0
@@ -0,0 +1,491 @@
1
+ export const id = 920;
2
+ export const ids = [920,238];
3
+ export const modules = {
4
+
5
+ /***/ 2238:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ I3: () => (/* binding */ recordFixAttempt),
10
+ /* harmony export */ fixDurationReport: () => (/* binding */ fixDurationReport),
11
+ /* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary),
12
+ /* harmony export */ sU: () => (/* binding */ loadFixAttempts)
13
+ /* harmony export */ });
14
+ /* unused harmony exports FIX_STAGES, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
15
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
16
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
17
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
18
+ // Time-to-validated-fix (R5, the reporting half).
19
+ //
20
+ // `verifyFix` already RUNS the stages and `test-runner.js` already times the
21
+ // slowest one. What did not exist was anything durable to read afterwards, so
22
+ // "how long does a fix actually take to validate" had no answer from real runs
23
+ // — only an estimate (`time-to-fix.js` guesses engineering hours from family
24
+ // and patch shape, before anything runs). This module is the opposite: it
25
+ // records what the pipeline observed and reports the distribution.
26
+ //
27
+ // THE HONESTY RULES, which are most of why this file is longer than a mean:
28
+ //
29
+ // 1. A failed attempt is NOT a data point about how long a fix takes. Fixes
30
+ // that fail verification fail fast (a re-scan that still sees the finding
31
+ // never reaches the test suite), so blending them into one average makes
32
+ // the pipeline look faster the worse it performs. Validated and failed
33
+ // attempts are summarised separately and never merged.
34
+ //
35
+ // 2. "Tests skipped" is not "tests passed". A project with no detectable
36
+ // suite can reach `ok:true` having run only the re-scan and the linter.
37
+ // That is a weaker claim than a fix whose suite executed, and it is also
38
+ // much faster, so counting the two together would quietly deflate the
39
+ // headline. They get their own bucket: `validated` means the suite ran and
40
+ // passed, `validatedWithoutTests` means there was no suite to run.
41
+ //
42
+ // 3. Every figure carries its `n`, and a percentile computed from too few
43
+ // samples is labelled unreliable rather than omitted or silently reported.
44
+ // Same precedent as the accuracy scorecard's `{n, d}` rates: a number
45
+ // without its denominator is not a measurement.
46
+ //
47
+ // Storage is append-only JSONL at `<scanRoot>/.agentic-security/fix-metrics.jsonl`,
48
+ // one record per verification attempt. Nothing here throws (posture
49
+ // convention) — an unwritable or corrupt log degrades to "no metrics", never
50
+ // to a failed verification.
51
+
52
+
53
+
54
+
55
+
56
+ const LOG_FILE = 'fix-metrics.jsonl';
57
+
58
+ // Below this many samples a percentile is an artifact of the sample, not a
59
+ // property of the pipeline. Reported anyway (hiding it invites re-deriving it
60
+ // wrong downstream) but flagged, so a caller cannot quote it as settled.
61
+ const RELIABLE_N = 10;
62
+
63
+ // The stages verifyFix runs, in execution order. Kept here so the recorder and
64
+ // the summariser cannot drift apart on stage naming.
65
+ const FIX_STAGES = Object.freeze(['rescan', 'lint', 'tests', 'honesty', 'poc']);
66
+
67
+ function _logPath(scanRoot) {
68
+ return (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, LOG_FILE);
69
+ }
70
+
71
+ /**
72
+ * Append one verification attempt. Best-effort and silent on failure: metrics
73
+ * must never be able to fail a fix that otherwise verified.
74
+ *
75
+ * @returns {boolean} whether the record was written (for tests, not callers).
76
+ */
77
+ function recordFixAttempt(scanRoot, record) {
78
+ if (!scanRoot || !record || typeof record !== 'object') return false;
79
+ try {
80
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
81
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return false;
82
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return false;
83
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
84
+ // One writeSync of one newline-terminated line: a concurrent reader sees
85
+ // whole records or nothing, and a torn tail is dropped on read.
86
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.appendFileSync(_logPath(scanRoot), JSON.stringify(record) + '\n', 'utf8');
87
+ return true;
88
+ } catch { return false; }
89
+ }
90
+
91
+ /**
92
+ * Read every well-formed attempt. A line that does not parse is skipped, not
93
+ * fatal — the last line of an interrupted write is the expected case.
94
+ */
95
+ function loadFixAttempts(scanRoot) {
96
+ try {
97
+ const raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(_logPath(scanRoot), 'utf8');
98
+ const out = [];
99
+ for (const line of raw.split('\n')) {
100
+ if (!line.trim()) continue;
101
+ try {
102
+ const rec = JSON.parse(line);
103
+ if (rec && typeof rec === 'object' && typeof rec.totalMs === 'number') out.push(rec);
104
+ } catch { /* torn or hand-edited line — drop it, keep the rest */ }
105
+ }
106
+ return out;
107
+ } catch { return []; }
108
+ }
109
+
110
+ // Nearest-rank percentile over an ascending array. Nearest-rank rather than
111
+ // interpolated because these are observed durations, and an interpolated p50
112
+ // reports a duration that no run actually took.
113
+ function _pct(sorted, p) {
114
+ if (!sorted.length) return null;
115
+ const rank = Math.ceil((p / 100) * sorted.length);
116
+ return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
117
+ }
118
+
119
+ function _dist(values) {
120
+ const v = values.filter(x => typeof x === 'number' && Number.isFinite(x) && x >= 0).sort((a, b) => a - b);
121
+ if (!v.length) return { n: 0, minMs: null, p50Ms: null, p90Ms: null, maxMs: null, meanMs: null, reliable: false };
122
+ const sum = v.reduce((a, b) => a + b, 0);
123
+ return {
124
+ n: v.length,
125
+ minMs: v[0],
126
+ p50Ms: _pct(v, 50),
127
+ p90Ms: _pct(v, 90),
128
+ maxMs: v[v.length - 1],
129
+ meanMs: Math.round(sum / v.length),
130
+ // Says whether the percentiles above may be quoted, not whether the count
131
+ // is real. n and min/max/mean are exact at any sample size.
132
+ reliable: v.length >= RELIABLE_N,
133
+ };
134
+ }
135
+
136
+ // Which bucket an attempt belongs to. Deliberately total: every attempt lands
137
+ // in exactly one, so the bucket counts always sum to the attempt count and a
138
+ // mis-shaped record cannot silently vanish from the denominator.
139
+ function bucketOf(a) {
140
+ if (!a?.ok) return 'failed';
141
+ return a.testsRan ? 'validated' : 'validatedWithoutTests';
142
+ }
143
+
144
+ /**
145
+ * Summarise a set of attempts into the reported distribution.
146
+ *
147
+ * `validated` is the headline: attempts that verified AND whose test suite
148
+ * actually ran and passed. The other two buckets exist so that headline cannot
149
+ * be inflated by counting weaker or faster outcomes inside it.
150
+ */
151
+ function summarizeFixDurations(attempts) {
152
+ const all = Array.isArray(attempts) ? attempts : [];
153
+ const buckets = { validated: [], validatedWithoutTests: [], failed: [] };
154
+ for (const a of all) buckets[bucketOf(a)].push(a);
155
+
156
+ const byStage = {};
157
+ for (const stage of FIX_STAGES) {
158
+ // Per-stage timings come from validated runs only. A stage's duration in a
159
+ // failed run is truncated by the failure (the pipeline stops), so mixing
160
+ // them in would understate every stage after the first failure point.
161
+ byStage[stage] = _dist(buckets.validated.map(a => a?.stages?.[stage]));
162
+ }
163
+
164
+ return {
165
+ attempts: all.length,
166
+ counts: {
167
+ validated: buckets.validated.length,
168
+ validatedWithoutTests: buckets.validatedWithoutTests.length,
169
+ failed: buckets.failed.length,
170
+ },
171
+ timeToValidatedFix: _dist(buckets.validated.map(a => a.totalMs)),
172
+ timeToValidatedFixWithoutTests: _dist(buckets.validatedWithoutTests.map(a => a.totalMs)),
173
+ timeToFailure: _dist(buckets.failed.map(a => a.totalMs)),
174
+ byStage,
175
+ reliableAtOrAbove: RELIABLE_N,
176
+ };
177
+ }
178
+
179
+ /** Read + summarise in one step. */
180
+ function fixDurationReport(scanRoot) {
181
+ return summarizeFixDurations(loadFixAttempts(scanRoot));
182
+ }
183
+
184
+ function _ms(v) {
185
+ if (v == null) return '—';
186
+ return v >= 1000 ? `${(v / 1000).toFixed(1)}s` : `${v}ms`;
187
+ }
188
+
189
+ /**
190
+ * One-paragraph human summary. Returns null when there is nothing measured —
191
+ * callers print nothing rather than printing an empty table.
192
+ */
193
+ function renderFixDurationSummary(sum) {
194
+ if (!sum || !sum.attempts) return null;
195
+ const d = sum.timeToValidatedFix;
196
+ const parts = [];
197
+ if (d.n) {
198
+ parts.push(
199
+ `time-to-validated-fix: median ${_ms(d.p50Ms)}, p90 ${_ms(d.p90Ms)} `
200
+ + `(n=${d.n}${d.reliable ? '' : `, below ${sum.reliableAtOrAbove} — percentiles not yet reliable`})`,
201
+ );
202
+ } else {
203
+ parts.push('time-to-validated-fix: no fix has both verified and had its test suite run yet');
204
+ }
205
+ if (sum.counts.validatedWithoutTests) {
206
+ parts.push(`${sum.counts.validatedWithoutTests} verified with no detectable test suite (excluded from the median above)`);
207
+ }
208
+ if (sum.counts.failed) {
209
+ parts.push(`${sum.counts.failed} failed verification, median ${_ms(sum.timeToFailure.p50Ms)} (counted separately)`);
210
+ }
211
+ return parts.join('; ') + '.';
212
+ }
213
+
214
+ const _internals = { _dist, _pct, RELIABLE_N };
215
+
216
+
217
+ // ── PRD F6.1 — score fixes on THREE AXES, not one ──────────────────────────
218
+ //
219
+ // The three axes the PRD names:
220
+ // (a) does the finding disappear — the rescan leg
221
+ // (b) does the project's own suite pass — the tests leg
222
+ // (c) does an independent verifier agree — the PoC re-check leg
223
+ //
224
+ // All three were already computed by verifyFixCore and then collapsed into one
225
+ // boolean, which is the problem: **(a) alone is satisfiable by deleting code.**
226
+ // A patch that removes the vulnerable function passes the rescan, has nothing
227
+ // left to fail, and — on a project with no detectable test suite — reaches
228
+ // ok:true having proven only that the detector went quiet.
229
+ //
230
+ // Reporting the axes separately makes that visible. `aOnly` is the number that
231
+ // matters most and the one nobody was publishing: attempts that satisfied ONLY
232
+ // the disappearance axis. A high aOnly with a high headline is the shape of a
233
+ // remediation feature that is deleting code and calling it a fix.
234
+ function summarizeFixAxes(attempts) {
235
+ const list = Array.isArray(attempts) ? attempts.filter(Boolean) : [];
236
+ const d = list.length;
237
+
238
+ const rate = (pred) => ({ n: list.filter(pred).length, d });
239
+
240
+ // Each axis is judged INDEPENDENTLY of the overall verdict, so a leg that
241
+ // passed inside a failed attempt still counts for its own axis. Reading them
242
+ // off `ok` would make the three axes three copies of the same number.
243
+ const findingDisappeared = rate((a) => a.rescanOk === true || (a.ok === true && a.rescanOk !== false));
244
+ const testsStillPass = rate((a) => a.testsRan === true && a.testsOk !== false);
245
+ const verifierAgrees = rate((a) => a.pocOk === true);
246
+
247
+ const satisfiesAll = rate((a) =>
248
+ (a.rescanOk === true || (a.ok === true && a.rescanOk !== false))
249
+ && a.testsRan === true && a.testsOk !== false
250
+ && a.pocOk === true);
251
+
252
+ // The honesty number: disappearance WITHOUT either corroborating axis.
253
+ const aOnly = rate((a) => {
254
+ const disappeared = a.rescanOk === true || (a.ok === true && a.rescanOk !== false);
255
+ const corroborated = (a.testsRan === true && a.testsOk !== false) || a.pocOk === true;
256
+ return disappeared && !corroborated;
257
+ });
258
+
259
+ return {
260
+ total: d,
261
+ findingDisappeared,
262
+ testsStillPass,
263
+ verifierAgrees,
264
+ satisfiesAll,
265
+ aOnly,
266
+ meaning:
267
+ 'findingDisappeared = the detector went quiet; testsStillPass = the project suite ran AND passed; '
268
+ + 'verifierAgrees = an independent PoC re-check confirmed the hole is shut. '
269
+ + 'aOnly counts attempts that satisfied ONLY disappearance — the shape a code-deleting "fix" produces.',
270
+ caveat: d === 0
271
+ ? 'no attempts recorded; every rate is 0/0 and means nothing'
272
+ : 'rates carry {n,d}; a small d is indicative, not settled',
273
+ };
274
+ }
275
+
276
+ /** Markdown for a report. Denominators always attached. */
277
+ function renderFixAxes(sum) {
278
+ if (!sum || !sum.total) return '_No fix attempts recorded._\n';
279
+ const row = (label, r, note) => `| ${label} | ${r.n}/${r.d} | ${note} |`;
280
+ return [
281
+ '| Axis | Rate | Meaning |',
282
+ '|---|---|---|',
283
+ row('(a) finding disappeared', sum.findingDisappeared, 'the detector went quiet'),
284
+ row('(b) project tests pass', sum.testsStillPass, 'the suite RAN and passed'),
285
+ row('(c) verifier agrees', sum.verifierAgrees, 'an independent PoC re-check confirmed it'),
286
+ row('all three', sum.satisfiesAll, 'the only row that means "fixed"'),
287
+ row('(a) ALONE', sum.aOnly, 'satisfiable by deleting code — watch this number'),
288
+ '',
289
+ ].join('\n');
290
+ }
291
+
292
+
293
+ /***/ }),
294
+
295
+ /***/ 1920:
296
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
297
+
298
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
299
+ /* harmony export */ productionFeedbackReport: () => (/* binding */ productionFeedbackReport),
300
+ /* harmony export */ renderProductionFeedbackSummary: () => (/* binding */ renderProductionFeedbackSummary)
301
+ /* harmony export */ });
302
+ /* unused harmony exports CATEGORIES, collectFeedbackEvents, summarizeFeedbackTrend, _internals */
303
+ /* harmony import */ var _suppressions_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9277);
304
+ /* harmony import */ var _triage_memory_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1905);
305
+ /* harmony import */ var _sca_policy_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(123);
306
+ /* harmony import */ var _fix_history_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4407);
307
+ /* harmony import */ var _fix_metrics_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(2238);
308
+ // FR-907 (assurance-hardening PRD): "Add longitudinal production feedback
309
+ // measurement | Metrics separate user suppression, accepted risk, invalid
310
+ // finding, fixed finding, and verification outcome."
311
+ //
312
+ // Read-only aggregation over 5 ALREADY-BUILT, separate mechanisms — this
313
+ // module invents no new storage of its own, only a unified view:
314
+ //
315
+ // user-suppression -> triage-memory.jsonl (decision:'wont-fix'),
316
+ // accepted.json (vibecoder soft-accept, dated),
317
+ // suppressions.yml (pro exception, undated)
318
+ // invalid-finding -> triage-memory.jsonl (decision:'false-positive')
319
+ // accepted-risk -> sca-policy.yml's accept-risk[] (undated snapshot
320
+ // — the policy file has no per-entry creation
321
+ // timestamp, only an optional future `expires`)
322
+ // fixed-finding -> fix-history/log.json (dated via `appliedAt`,
323
+ // status-tagged: applied/pending/reverted/failed)
324
+ // verification-outcome -> fix-metrics.jsonl (dated via `at`, ok-tagged)
325
+ //
326
+ // "Longitudinal" means: every event that HAS a real timestamp is usable in
327
+ // a time-bucketed trend; every event that does NOT (accept-risk entries,
328
+ // pro suppressions — neither schema records when the entry was added) is
329
+ // still counted in its category total but reported separately as
330
+ // "undated" rather than silently smeared across the time window or
331
+ // silently dropped. Same disclosed-gap discipline this codebase already
332
+ // uses elsewhere (privacy-framework.js's engine-gap bucket, accuracy-
333
+ // scorecard.js's excluded-from-denominator entries) — a number without
334
+ // its caveat is not a measurement.
335
+ //
336
+ // Never throws (posture convention): each of the 5 reads is wrapped
337
+ // independently, so a missing or malformed source degrades only that ONE
338
+ // category to empty, never blocks the other four. Every underlying reader
339
+ // (loadSoftAccepted, loadProSuppressions, loadMemory, loadScaPolicy,
340
+ // readLog, loadFixAttempts) already degrades gracefully on its own; the
341
+ // wrapping here is defense in depth against a reader whose contract
342
+ // changes later, not a claim that today's readers can throw.
343
+
344
+
345
+
346
+
347
+
348
+
349
+
350
+ const MS_PER_DAY = 86400000;
351
+
352
+ // The 5 categories named verbatim in FR-907's own acceptance criterion.
353
+ const CATEGORIES = Object.freeze([
354
+ 'user-suppression', 'accepted-risk', 'invalid-finding', 'fixed-finding', 'verification-outcome',
355
+ ]);
356
+
357
+ function _safe(fn) {
358
+ try { return fn(); } catch { return []; }
359
+ }
360
+
361
+ /**
362
+ * One unified event per underlying record, tagged with which of the 5
363
+ * PRD-named categories it belongs to. `at` is an ISO timestamp or null
364
+ * when the source schema has no per-entry creation date. `raw` keeps the
365
+ * original record for drill-down — never re-derived from the unified
366
+ * shape, so nothing is lost in translation.
367
+ */
368
+ function collectFeedbackEvents(scanRoot) {
369
+ const events = [];
370
+
371
+ for (const e of _safe(() => (0,_suppressions_js__WEBPACK_IMPORTED_MODULE_0__/* .loadSoftAccepted */ .gv)(scanRoot))) {
372
+ events.push({
373
+ category: 'user-suppression', source: 'suppressions.js:accepted.json',
374
+ at: e.accepted_at || null, findingId: e.id || null, file: e.file || null,
375
+ line: e.line ?? null, outcome: 'soft-accepted', raw: e,
376
+ });
377
+ }
378
+ for (const e of _safe(() => (0,_suppressions_js__WEBPACK_IMPORTED_MODULE_0__/* .loadProSuppressions */ .KM)(scanRoot))) {
379
+ events.push({
380
+ category: 'user-suppression', source: 'suppressions.js:suppressions.yml',
381
+ at: null, findingId: e.finding_id || null, file: e.file || null,
382
+ line: null, outcome: 'pro-exception', raw: e,
383
+ });
384
+ }
385
+ for (const e of _safe(() => (0,_triage_memory_js__WEBPACK_IMPORTED_MODULE_1__/* .loadMemory */ .ab)(scanRoot))) {
386
+ if (e.decision === 'wont-fix') {
387
+ events.push({
388
+ category: 'user-suppression', source: 'triage-memory.js',
389
+ at: e.at || null, findingId: e.id || null, file: e.file || null,
390
+ line: e.line ?? null, outcome: 'wont-fix', raw: e,
391
+ });
392
+ } else if (e.decision === 'false-positive') {
393
+ events.push({
394
+ category: 'invalid-finding', source: 'triage-memory.js',
395
+ at: e.at || null, findingId: e.id || null, file: e.file || null,
396
+ line: e.line ?? null, outcome: 'false-positive', raw: e,
397
+ });
398
+ }
399
+ }
400
+ const scaPolicy = _safe(() => (0,_sca_policy_js__WEBPACK_IMPORTED_MODULE_2__/* .loadScaPolicy */ .IN)(scanRoot));
401
+ const acceptRisk = scaPolicy && Array.isArray(scaPolicy.acceptRisk) ? scaPolicy.acceptRisk : [];
402
+ for (const e of acceptRisk) {
403
+ events.push({
404
+ category: 'accepted-risk', source: 'sca-policy.js',
405
+ at: null, findingId: e.cve || e.package || null, file: null,
406
+ line: null, outcome: 'accept-risk', raw: e,
407
+ });
408
+ }
409
+ for (const e of _safe(() => (0,_fix_history_js__WEBPACK_IMPORTED_MODULE_3__/* .readLog */ .x6)(scanRoot))) {
410
+ events.push({
411
+ category: 'fixed-finding', source: 'fix-history.js',
412
+ at: e.appliedAt || null, findingId: e.findingId || e.stableId || null,
413
+ file: e.file || null, line: null, outcome: e.status || 'unknown', raw: e,
414
+ });
415
+ }
416
+ for (const e of _safe(() => (0,_fix_metrics_js__WEBPACK_IMPORTED_MODULE_4__/* .loadFixAttempts */ .sU)(scanRoot))) {
417
+ events.push({
418
+ category: 'verification-outcome', source: 'fix-metrics.js',
419
+ at: e.at || null, findingId: e.stableId || null, file: null,
420
+ line: null, outcome: e.ok ? 'verified' : 'not-verified', raw: e,
421
+ });
422
+ }
423
+
424
+ return events;
425
+ }
426
+
427
+ /**
428
+ * Bucket events by day within a rolling `sinceDays`-day window — the same
429
+ * cutoff-window shape as posture/triage.js's own trend(). An event with no
430
+ * timestamp (or an unparseable one) cannot be placed on a time axis: it is
431
+ * counted once in `undated` per category rather than silently dropped or
432
+ * silently smeared into the window.
433
+ */
434
+ function summarizeFeedbackTrend(events, { sinceDays = 30, now = Date.now() } = {}) {
435
+ const cutoff = now - sinceDays * MS_PER_DAY;
436
+ const byCategory = {};
437
+ for (const cat of CATEGORIES) byCategory[cat] = { total: 0, inWindow: 0, undated: 0 };
438
+
439
+ const dayBuckets = new Map(); // 'YYYY-MM-DD' -> { category: count }
440
+ for (const ev of Array.isArray(events) ? events : []) {
441
+ if (!ev || !CATEGORIES.includes(ev.category)) continue;
442
+ byCategory[ev.category].total++;
443
+ const t = ev.at ? Date.parse(ev.at) : NaN;
444
+ if (!Number.isFinite(t)) { byCategory[ev.category].undated++; continue; }
445
+ if (t < cutoff) continue;
446
+ byCategory[ev.category].inWindow++;
447
+ const dayKey = new Date(t).toISOString().slice(0, 10);
448
+ if (!dayBuckets.has(dayKey)) dayBuckets.set(dayKey, {});
449
+ const bucket = dayBuckets.get(dayKey);
450
+ bucket[ev.category] = (bucket[ev.category] || 0) + 1;
451
+ }
452
+
453
+ const series = [...dayBuckets.entries()]
454
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
455
+ .map(([day, counts]) => ({ day, counts }));
456
+ return { sinceDays, byCategory, series };
457
+ }
458
+
459
+ /**
460
+ * Convenience wrapper: collect + summarize in one call — same pattern as
461
+ * fix-metrics.js's fixDurationReport(scanRoot).
462
+ */
463
+ function productionFeedbackReport(scanRoot, opts) {
464
+ const events = collectFeedbackEvents(scanRoot);
465
+ return { events, ...summarizeFeedbackTrend(events, opts) };
466
+ }
467
+
468
+ /**
469
+ * One block of human-readable summary, or null when nothing was measured
470
+ * at all — same "null when nothing measured" contract as
471
+ * renderFixDurationSummary, so a caller can skip the section entirely
472
+ * rather than print an empty header.
473
+ */
474
+ function renderProductionFeedbackSummary(report) {
475
+ if (!report || !Array.isArray(report.events) || !report.events.length) return null;
476
+ const lines = [`Production feedback (last ${report.sinceDays}d):`];
477
+ for (const cat of CATEGORIES) {
478
+ const c = report.byCategory[cat];
479
+ if (!c || c.total === 0) continue;
480
+ const undatedNote = c.undated ? `, ${c.undated} undated` : '';
481
+ lines.push(` ${cat}: ${c.inWindow} in window / ${c.total} total${undatedNote}`);
482
+ }
483
+ return lines.length > 1 ? lines.join('\n') : null;
484
+ }
485
+
486
+ const _internals = { MS_PER_DAY };
487
+
488
+
489
+ /***/ })
490
+
491
+ };
@@ -0,0 +1,173 @@
1
+ export const id = 970;
2
+ export const ids = [970];
3
+ export const modules = {
4
+
5
+ /***/ 4970:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ ASSURANCE_MODES: () => (/* binding */ ASSURANCE_MODES),
10
+ /* harmony export */ DEFAULT_ASSURANCE_MODE: () => (/* binding */ DEFAULT_ASSURANCE_MODE),
11
+ /* harmony export */ evaluateAssuranceMode: () => (/* binding */ evaluateAssuranceMode)
12
+ /* harmony export */ });
13
+ /* unused harmony export _internals */
14
+ /* harmony import */ var _posture_provenance_schema_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4594);
15
+ // Assurance modes (assurance-hardening PRD FR-204).
16
+ //
17
+ // "Add assurance modes: advisory, standard, and strict | Strict mode fails
18
+ // when a required analyzer fails, times out, is unavailable, or is
19
+ // silently skipped." Section 12.1 names the CLI surface:
20
+ // `--assurance advisory|standard|strict`.
21
+ //
22
+ // Built directly on FR-203's coverage ledger (pipeline/coverage-ledger.js)
23
+ // and FR-206's scan-health.js -- this module adds no new signal collection
24
+ // of its own, only a POLICY over signals that already exist. That is
25
+ // deliberate: assurance modes decide how strict to be about incomplete
26
+ // analysis; they must never be the thing that DEFINES what "incomplete"
27
+ // means, or the two concepts would drift out of sync.
28
+ //
29
+ // THREE MODES:
30
+ // advisory — never fails the gate over scan health, regardless of what
31
+ // scanHealth reports. Purely informational (surfaced in scanHealth /
32
+ // toShipVerdict / `ci`'s stderr, per FR-206's fix -- this mode does not
33
+ // suppress that, it just does not ADD a build-failing consequence).
34
+ // standard (the DEFAULT, matching this codebase's behavior before and
35
+ // after FR-204) — same as advisory for gate purposes: an incomplete
36
+ // scan is surfaced, never silently hidden, but does not itself fail a
37
+ // build independent of the ordinary --fail-on severity threshold. The
38
+ // distinction from advisory is one of INTENT/reporting emphasis, not
39
+ // mechanism -- see the module-level note below on why this codebase
40
+ // does not invent a mechanical difference the PRD text does not ask
41
+ // for.
42
+ // strict — an incomplete scan (any analyzer failed, timed out, or was
43
+ // skipped by policy -- the ledger's own three non-"completed" outcomes)
44
+ // is a HARD FAILURE, independent of and IN ADDITION TO --fail-on,
45
+ // mirroring cmdCi's own existing precedent for its --policy gate
46
+ // ("Policy runs ALONGSIDE the --fail-on threshold; either gate can
47
+ // fail the build").
48
+ //
49
+ // WHY ADVISORY AND STANDARD ARE MECHANICALLY IDENTICAL HERE: the PRD's own
50
+ // one-line acceptance criterion only specifies STRICT mode's behavior in
51
+ // full ("fails when..."); it does not name a distinct mechanical
52
+ // consequence for standard beyond "the default, not strict." Inventing an
53
+ // intermediate failure condition neither named in the PRD nor requested
54
+ // would be exactly the kind of unrequested scope-widening this codebase's
55
+ // own conventions warn against. If a future requirement needs standard
56
+ // mode to behave differently from advisory, that is a deliberate, separate
57
+ // decision -- not something to guess at here.
58
+
59
+
60
+
61
+ const ASSURANCE_MODES = Object.freeze(['advisory', 'standard', 'strict']);
62
+ const DEFAULT_ASSURANCE_MODE = 'standard';
63
+
64
+ function _isValidMode(mode) {
65
+ return ASSURANCE_MODES.includes(mode);
66
+ }
67
+
68
+ /**
69
+ * @param {string} mode - one of ASSURANCE_MODES; invalid/missing degrades to the default.
70
+ * @param {object|null} scanHealth - the engine's computed scan.scanHealth (FR-206).
71
+ * @returns {{ok: boolean, mode: string, reason: string|null, conditions: string[]}}
72
+ * ok:false only ever happens in strict mode; advisory/standard always ok:true
73
+ * (they report, they do not gate).
74
+ */
75
+ function evaluateAssuranceMode(mode, scanHealth, findings = []) {
76
+ const effectiveMode = _isValidMode(mode) ? mode : DEFAULT_ASSURANCE_MODE;
77
+ const conditions = Array.isArray(scanHealth?.conditions) ? scanHealth.conditions : [];
78
+
79
+ if (effectiveMode !== 'strict') {
80
+ return { ok: true, mode: effectiveMode, reason: null, conditions };
81
+ }
82
+
83
+ // Strict: any of the ledger's three non-"completed" analyzer outcomes,
84
+ // OR an annotator error / deep-mode failure (scanHealth's OTHER,
85
+ // non-analyzer conditions) -- "a required analyzer... is unavailable, or
86
+ // is silently skipped" covers more than just coverage-ledger.js's own
87
+ // per-detector accounting; a scan whose ANY known-good signal degraded
88
+ // is not "complete" under strict's own definition.
89
+ const status = scanHealth?.status;
90
+ if (!scanHealth || status !== 'complete') {
91
+ const a = scanHealth?.analyzers;
92
+ const parts = [];
93
+ if (a?.failed > 0) parts.push(`${a.failed} analyzer(s) failed`);
94
+ if (a?.timedOut > 0) parts.push(`${a.timedOut} analyzer(s) timed out`);
95
+ if (a?.skippedByPolicy > 0) parts.push(`${a.skippedByPolicy} analyzer(s) silently skipped by policy`);
96
+ const analyzerSummary = parts.length ? ` (${parts.join(', ')})` : '';
97
+ return {
98
+ ok: false,
99
+ mode: 'strict',
100
+ reason: `strict mode requires a fully complete scan; scanHealth.status is '${status ?? 'unknown'}'${analyzerSummary}`,
101
+ conditions,
102
+ };
103
+ }
104
+
105
+ // M2 §2.5: strict cares about overall scan completeness, which now
106
+ // explicitly includes PROVENANCE completeness, not just detector/analyzer
107
+ // completeness. A finding whose findingProvenance status is outside
108
+ // ['complete','uncommitted'] — including a finding with NO
109
+ // findingProvenance at all, e.g. --no-provenance was used — means strict
110
+ // cannot vouch for this scan's provenance the same way it already refuses
111
+ // to vouch for a scan with a failed analyzer.
112
+ //
113
+ // KNOWN INTERACTION: scan.secrets/scan.logicVulns are unconditionally
114
+ // stamped not_available today (M0+M1 deliberately deferred real origin
115
+ // resolution for those two channels — see the M2/M3/M4 design spec's
116
+ // §2.6). Any real secret or logic finding therefore fails strict mode
117
+ // until that resolution work lands. This is the literal, intended
118
+ // consequence of "never false certainty" applied to strict's own
119
+ // definition, not an oversight — a strict-mode operator with secrets
120
+ // findings should expect this until M3+ closes that gap.
121
+ //
122
+ // This list is INCOMPLETE without scan.supplyChain, and the omission
123
+ // matters more than the secrets/logic one above because it hits nearly
124
+ // every real project. engine.js stamps every supplyChain entry
125
+ // not_available too (see the loop over `supplyChain` right after the
126
+ // `annotateGitProvenance` calls), and that bucket covers three distinct
127
+ // populations, not one:
128
+ //
129
+ // - transitive `vulnerable_dep` findings: a genuine, if currently
130
+ // unresolved, DEFERRAL — same shape as secrets/logicVulns above. The
131
+ // vulnerable version was never declared in this repo's own manifests,
132
+ // so there is no local commit to walk yet, but one could exist to
133
+ // resolve in a later phase.
134
+ // - `unpinned_dep` / `no_lockfile` findings: a CATEGORY ERROR, not a
135
+ // deferral. These describe an ABSENT state (a version range with no
136
+ // pin, a manifest with no lockfile) — there is no "commit that
137
+ // introduced a missing lockfile" for any future resolver to find,
138
+ // because the finding is about the absence of an event, not an event
139
+ // itself. No amount of future engineering work makes these resolvable.
140
+ //
141
+ // Direct `vulnerable_dep` findings DO go through real origin resolution
142
+ // (`resolveDirectSCAOrigin`, gated on `isDirect`) and are not part of this
143
+ // limitation.
144
+ //
145
+ // Net effect: because `unpinned_dep`/`no_lockfile` findings are a category
146
+ // error rather than a deferral, `--assurance strict` will fail on nearly
147
+ // any real project that has a `package.json` (or equivalent manifest)
148
+ // today — an unpinned or unlocked dependency is common, and this check has
149
+ // no way to ever resolve one. This is a known, disclosed limitation of the
150
+ // current implementation, not a bug, and it is not something this check
151
+ // should route around: exempting these finding types from the strict-mode
152
+ // gate was considered and deliberately deferred to a future milestone
153
+ // rather than done here, so strict mode keeps refusing to vouch for
154
+ // provenance it cannot actually speak to.
155
+ const badProvenance = (Array.isArray(findings) ? findings : []).filter((f) => !(0,_posture_provenance_schema_js__WEBPACK_IMPORTED_MODULE_0__/* .isProvenanceHealthy */ .lL)(f?.findingProvenance));
156
+ if (badProvenance.length > 0) {
157
+ return {
158
+ ok: false,
159
+ mode: 'strict',
160
+ reason: `strict mode requires complete finding provenance; ${badProvenance.length} finding(s) have status outside [complete, uncommitted]`,
161
+ conditions,
162
+ };
163
+ }
164
+
165
+ return { ok: true, mode: 'strict', reason: null, conditions };
166
+ }
167
+
168
+ const _internals = { _isValidMode };
169
+
170
+
171
+ /***/ })
172
+
173
+ };