@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
|
@@ -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
|
+
};
|