@clear-capabilities/agentic-security-scanner 0.150.1 → 0.150.2
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 +76 -0
- package/bin/agentic-security.js +73 -10
- package/dist/1122.index.js +16 -16
- package/dist/1208.index.js +23 -0
- package/dist/1301.index.js +3 -3
- package/dist/1379.index.js +3 -3
- package/dist/1444.index.js +3 -3
- package/dist/1660.index.js +1 -1
- package/dist/1700.index.js +5 -5
- package/dist/1905.index.js +12 -12
- package/dist/1920.index.js +10 -10
- package/dist/2238.index.js +4 -4
- package/dist/2271.index.js +3 -3
- package/dist/2432.index.js +8 -8
- package/dist/2659.index.js +1 -1
- package/dist/2826.index.js +2 -2
- package/dist/2830.index.js +3 -3
- package/dist/2923.index.js +1 -1
- package/dist/3001.index.js +4 -4
- package/dist/3117.index.js +4 -4
- package/dist/3180.index.js +9 -9
- package/dist/3276.index.js +1 -1
- package/dist/3415.index.js +1 -1
- package/dist/3499.index.js +1 -1
- package/dist/3518.index.js +4 -4
- package/dist/3736.index.js +7 -7
- package/dist/3839.index.js +4 -4
- package/dist/4113.index.js +441 -418
- package/dist/4265.index.js +4 -4
- package/dist/4384.index.js +8 -3
- package/dist/4399.index.js +29 -3
- package/dist/4547.index.js +2 -2
- package/dist/4863.index.js +6 -6
- package/dist/4970.index.js +2 -2
- package/dist/5051.index.js +15 -15
- package/dist/5144.index.js +4 -4
- package/dist/5333.index.js +8 -8
- package/dist/5343.index.js +2 -2
- package/dist/5350.index.js +6 -6
- package/dist/5561.index.js +1 -1
- package/dist/5637.index.js +10 -5
- package/dist/5724.index.js +21642 -0
- package/dist/5756.index.js +99 -489
- package/dist/5830.index.js +3 -3
- package/dist/6257.index.js +21 -8
- package/dist/6626.index.js +5 -5
- package/dist/6662.index.js +1 -1
- package/dist/6675.index.js +4 -4
- package/dist/6730.index.js +10 -10
- package/dist/6829.index.js +4 -4
- package/dist/6944.index.js +2 -2
- package/dist/6994.index.js +5 -5
- package/dist/7039.index.js +83 -19
- package/dist/7178.index.js +11 -6
- package/dist/7227.index.js +3 -3
- package/dist/7552.index.js +2 -2
- package/dist/7709.index.js +2 -2
- package/dist/7838.index.js +1037 -0
- package/dist/8218.index.js +4 -4
- package/dist/8476.index.js +4 -4
- package/dist/8513.index.js +8 -8
- package/dist/8520.index.js +1 -1
- package/dist/8752.index.js +4 -4
- package/dist/9207.index.js +2 -2
- package/dist/9220.index.js +5 -5
- package/dist/9390.index.js +6 -6
- package/dist/9503.index.js +2 -2
- package/dist/9560.index.js +3221 -0
- package/dist/957.index.js +4 -4
- package/dist/9801.index.js +1 -1
- package/dist/9824.index.js +5 -5
- package/dist/agentic-security.mjs +23 -23
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -2
- package/src/egress/redact.js +1 -1
- package/src/engine.js +13 -2
- package/src/llm-validator/agent-loop.js +85 -5
- package/src/llm-validator/fix-proposal.js +16 -3
- package/src/llm-validator/model-capabilities.js +26 -1
- package/src/llm-validator/model-probe.js +46 -8
- package/src/llm-validator/ollama-provider.js +26 -1
- package/src/llm-validator/oom-feedback.js +69 -0
- package/src/llm-validator/providers.js +50 -0
- package/src/llm-validator/redact.js +177 -11
- package/src/report/index.js +14 -3
- package/src/sast/java-ast-folding.js +18 -2
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
export const id = 7838;
|
|
2
|
+
export const ids = [7838,2238];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 27785:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ _u: () => (/* binding */ gateFixOutput)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony exports checkResidualHonesty, requireCitedEvidence, computeFixTier, checkMechanicalTierEvidence, _internals */
|
|
12
|
+
// Deterministic honesty gates on fix / finding output (#7).
|
|
13
|
+
//
|
|
14
|
+
// The project's verification discipline (scanner/CLAUDE.md) exists because
|
|
15
|
+
// several releases shipped broken or false because work was reported as done
|
|
16
|
+
// without confirming the artifact changed. Two of those failure modes are
|
|
17
|
+
// *textual* — they live in the prose an agent emits alongside a fix — and can
|
|
18
|
+
// be caught deterministically, with no LLM and no network:
|
|
19
|
+
//
|
|
20
|
+
// 1. Hand-wave residual-risk prose. "The input is adequately handled",
|
|
21
|
+
// "future work", "tbd", "later" — vague assurances that claim safety
|
|
22
|
+
// without naming a concrete remaining vector. A residual you can't name
|
|
23
|
+
// is a residual you're guessing about; reject the guess.
|
|
24
|
+
//
|
|
25
|
+
// 2. An unbacked "this is a false positive / provably safe" verdict. Marking
|
|
26
|
+
// a finding safe is a coverage *reduction* — it must cite a `file:line`
|
|
27
|
+
// that shows why, exactly like the rules-override gate refuses to silently
|
|
28
|
+
// shrink coverage.
|
|
29
|
+
//
|
|
30
|
+
// Plus a conservative fix-tier classifier so a partial remediation can never be
|
|
31
|
+
// labelled FULL: any workaround-only signal (rate-limit, docs, log-without-
|
|
32
|
+
// reject) is WORKAROUND; anything short of (sink signature changed + all callers
|
|
33
|
+
// routed + a discriminating test) is at most MITIGATION; only the full set with
|
|
34
|
+
// no partial-sanitization caveat earns FULL.
|
|
35
|
+
//
|
|
36
|
+
// Pure functions, no side effects, no throwing — safe to call from a command,
|
|
37
|
+
// a hook, or the MCP verify_fix path.
|
|
38
|
+
|
|
39
|
+
// Vague-assurance phrases that a real residual must never hide behind. Matched
|
|
40
|
+
// case-insensitively with word boundaries so "later" doesn't trip on
|
|
41
|
+
// "collateral" and "tbd" doesn't trip on a longer token.
|
|
42
|
+
const BANNED_RESIDUAL_PHRASES = Object.freeze([
|
|
43
|
+
'adequately handled',
|
|
44
|
+
'adequately handles',
|
|
45
|
+
'properly validated',
|
|
46
|
+
'properly handled',
|
|
47
|
+
'handled properly',
|
|
48
|
+
'handled safely',
|
|
49
|
+
'future work',
|
|
50
|
+
'more work needed',
|
|
51
|
+
'to be done',
|
|
52
|
+
'tbd',
|
|
53
|
+
'later',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// A citation shaped like `file:line` — one or more non-space, non-colon chars,
|
|
57
|
+
// a colon, then digits. Unanchored: it need only appear somewhere in the item.
|
|
58
|
+
const CITATION_RE = /[^\s:]+:\d+/;
|
|
59
|
+
|
|
60
|
+
// Verdicts that assert the finding is not real and therefore demand a citation.
|
|
61
|
+
// Compared after normalizing separators (`_`/space → `-`) and lowercasing, so
|
|
62
|
+
// FALSE_POSITIVE, false-positive, and "provably safe" all land here.
|
|
63
|
+
const FP_VERDICTS = Object.freeze(new Set(['false-positive', 'provably-safe', 'safe']));
|
|
64
|
+
|
|
65
|
+
function _escapeRe(s) {
|
|
66
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reject vague-assurance / hand-wave residual-risk prose.
|
|
71
|
+
*
|
|
72
|
+
* An empty or whitespace-only residual is ok — there is no residual to lie
|
|
73
|
+
* about. A non-empty residual is rejected when it contains any banned phrase;
|
|
74
|
+
* each match yields one violation naming the offending phrase.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} residualText
|
|
77
|
+
* @returns {{ ok: boolean, violations: string[] }}
|
|
78
|
+
*/
|
|
79
|
+
function checkResidualHonesty(residualText) {
|
|
80
|
+
const text = typeof residualText === 'string' ? residualText : '';
|
|
81
|
+
if (text.trim() === '') return { ok: true, violations: [] };
|
|
82
|
+
|
|
83
|
+
const violations = [];
|
|
84
|
+
for (const phrase of BANNED_RESIDUAL_PHRASES) {
|
|
85
|
+
const re = new RegExp(`\\b${_escapeRe(phrase)}\\b`, 'i');
|
|
86
|
+
if (re.test(text)) {
|
|
87
|
+
violations.push(`vague-assurance phrase: "${phrase}"`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { ok: violations.length === 0, violations };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _isCitation(item) {
|
|
94
|
+
if (typeof item === 'string') return CITATION_RE.test(item);
|
|
95
|
+
if (item && typeof item === 'object' && typeof item.location === 'string') {
|
|
96
|
+
return CITATION_RE.test(item.location);
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _normalizeVerdict(verdict) {
|
|
102
|
+
return String(verdict).trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Require a file:line citation behind a "this is not real" verdict.
|
|
107
|
+
*
|
|
108
|
+
* For a false-positive / provably-safe / safe verdict (case-insensitive; also
|
|
109
|
+
* accepts FALSE_POSITIVE), at least one evidence item must be a `file:line`
|
|
110
|
+
* citation — either a string matching /[^\s:]+:\d+/ or an object
|
|
111
|
+
* `{ location: "file:line" }`. Any other verdict passes unconditionally.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} verdict
|
|
114
|
+
* @param {Array|string|object} evidence
|
|
115
|
+
* @returns {{ ok: boolean, violations: string[] }}
|
|
116
|
+
*/
|
|
117
|
+
function requireCitedEvidence(verdict, evidence) {
|
|
118
|
+
if (typeof verdict !== 'string' || !FP_VERDICTS.has(_normalizeVerdict(verdict))) {
|
|
119
|
+
return { ok: true, violations: [] };
|
|
120
|
+
}
|
|
121
|
+
const items = Array.isArray(evidence)
|
|
122
|
+
? evidence
|
|
123
|
+
: evidence == null
|
|
124
|
+
? []
|
|
125
|
+
: [evidence];
|
|
126
|
+
if (items.some(_isCitation)) return { ok: true, violations: [] };
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
violations: ['false-positive/safe verdict requires a file:line citation'],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Classify a fix into FULL | MITIGATION | WORKAROUND, conservative-first.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} signals
|
|
137
|
+
* @param {boolean} signals.sinkSignatureChanged
|
|
138
|
+
* @param {boolean} signals.allCallersRouted
|
|
139
|
+
* @param {boolean} signals.testDiscriminates - a test that fails pre-fix, passes post-fix
|
|
140
|
+
* @param {boolean} [signals.rateLimitOnly]
|
|
141
|
+
* @param {boolean} [signals.docsOnly]
|
|
142
|
+
* @param {boolean} [signals.logOnlyNoReject]
|
|
143
|
+
* @param {boolean} [signals.partialSanitization]
|
|
144
|
+
* @returns {'FULL'|'MITIGATION'|'WORKAROUND'}
|
|
145
|
+
*/
|
|
146
|
+
function computeFixTier(signals) {
|
|
147
|
+
const s = signals && typeof signals === 'object' ? signals : {};
|
|
148
|
+
if (s.rateLimitOnly || s.docsOnly || s.logOnlyNoReject) return 'WORKAROUND';
|
|
149
|
+
const complete = s.sinkSignatureChanged && s.allCallersRouted && s.testDiscriminates;
|
|
150
|
+
if (s.partialSanitization || !complete) return 'MITIGATION';
|
|
151
|
+
return 'FULL';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* FR-308: cross-check a fix-completeness TIER against MECHANICAL evidence,
|
|
156
|
+
* when any is available. `signals` (computeFixTier's input) is agent-
|
|
157
|
+
* self-reported — this module cannot compute sinkSignatureChanged /
|
|
158
|
+
* allCallersRouted / testDiscriminates itself (see the header above: "the
|
|
159
|
+
* gate can only run against claims the AGENT self-reports... nothing here
|
|
160
|
+
* is server-computable"). `pocLeg` is different: fix-verify.js's PoC leg is
|
|
161
|
+
* a REAL execution result (posture/CLAUDE.md's execution-proof tiers), not
|
|
162
|
+
* a claim. When it is available and shows the proof-of-concept STILL
|
|
163
|
+
* demonstrates the vulnerability against the patch, a self-reported FULL
|
|
164
|
+
* tier is not merely internally inconsistent — it is REFUTED by fact. This
|
|
165
|
+
* is the literal "a mitigation or workaround cannot be represented as a
|
|
166
|
+
* full fix" acceptance criterion, now backed by mechanical evidence where
|
|
167
|
+
* it exists rather than by self-report consistency alone.
|
|
168
|
+
*
|
|
169
|
+
* A `pocLeg` of `not-requested` or `inconclusive` carries no mechanical
|
|
170
|
+
* signal either way and is a no-op here — this check can only ever ADD a
|
|
171
|
+
* violation on real contrary evidence, never manufacture one from absence.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} tier
|
|
174
|
+
* @param {{status: string, reason?: string}|null} pocLeg
|
|
175
|
+
* @returns {{ ok: boolean, violations: string[] }}
|
|
176
|
+
*/
|
|
177
|
+
function checkMechanicalTierEvidence(tier, pocLeg) {
|
|
178
|
+
if (!pocLeg || typeof pocLeg !== 'object') return { ok: true, violations: [] };
|
|
179
|
+
if (tier === 'FULL' && pocLeg.status === 'still-exploitable') {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
violations: [`tier 'FULL' is refuted by mechanical evidence: the proof-of-concept still demonstrates the vulnerability against the patch${pocLeg.reason ? ` (${pocLeg.reason})` : ''}`],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, violations: [] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compose the four gates for a single fix's output.
|
|
190
|
+
*
|
|
191
|
+
* ok = residual-honesty ok AND evidence-citation ok AND mechanical-tier-
|
|
192
|
+
* evidence ok, further constrained by the tier/residual consistency
|
|
193
|
+
* invariant:
|
|
194
|
+
* - a FULL tier must NOT carry a residual (a full fix has nothing left);
|
|
195
|
+
* - a non-FULL tier MUST document a residual (say what's still open).
|
|
196
|
+
*
|
|
197
|
+
* @param {{ residual?: string, verdict?: string, evidence?: any, signals?: object }} input
|
|
198
|
+
* @param {{ pocLeg?: object|null }} [mechanical] - FR-308: optional real
|
|
199
|
+
* execution evidence (fix-verify.js's pocLeg) to cross-check the
|
|
200
|
+
* self-reported tier against. Omitted entirely by any caller that has no
|
|
201
|
+
* PoC leg to offer — this parameter never REQUIRES mechanical evidence,
|
|
202
|
+
* it only USES it when present.
|
|
203
|
+
* @returns {{ ok: boolean, tier: string, violations: string[] }}
|
|
204
|
+
*/
|
|
205
|
+
function gateFixOutput({ residual, verdict, evidence, signals } = {}, { pocLeg = null } = {}) {
|
|
206
|
+
const tier = computeFixTier(signals);
|
|
207
|
+
const residualCheck = checkResidualHonesty(residual);
|
|
208
|
+
const evidenceCheck = requireCitedEvidence(verdict, evidence);
|
|
209
|
+
const mechanicalCheck = checkMechanicalTierEvidence(tier, pocLeg);
|
|
210
|
+
|
|
211
|
+
const violations = [...residualCheck.violations, ...evidenceCheck.violations, ...mechanicalCheck.violations];
|
|
212
|
+
let ok = residualCheck.ok && evidenceCheck.ok && mechanicalCheck.ok;
|
|
213
|
+
|
|
214
|
+
const residualEmpty = typeof residual !== 'string' || residual.trim() === '';
|
|
215
|
+
if (tier === 'FULL' && !residualEmpty) {
|
|
216
|
+
violations.push('FULL tier cannot carry a residual');
|
|
217
|
+
ok = false;
|
|
218
|
+
}
|
|
219
|
+
if (tier !== 'FULL' && residualEmpty) {
|
|
220
|
+
violations.push('non-FULL tier must document a residual');
|
|
221
|
+
ok = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { ok, tier, violations };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERDICTS });
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
/***/ }),
|
|
231
|
+
|
|
232
|
+
/***/ 32238:
|
|
233
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
234
|
+
|
|
235
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
236
|
+
/* harmony export */ I3: () => (/* binding */ recordFixAttempt),
|
|
237
|
+
/* harmony export */ fixDurationReport: () => (/* binding */ fixDurationReport),
|
|
238
|
+
/* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary),
|
|
239
|
+
/* harmony export */ sU: () => (/* binding */ loadFixAttempts)
|
|
240
|
+
/* harmony export */ });
|
|
241
|
+
/* unused harmony exports FIX_STAGES, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
|
|
242
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
243
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
|
|
244
|
+
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
|
|
245
|
+
// Time-to-validated-fix (R5, the reporting half).
|
|
246
|
+
//
|
|
247
|
+
// `verifyFix` already RUNS the stages and `test-runner.js` already times the
|
|
248
|
+
// slowest one. What did not exist was anything durable to read afterwards, so
|
|
249
|
+
// "how long does a fix actually take to validate" had no answer from real runs
|
|
250
|
+
// — only an estimate (`time-to-fix.js` guesses engineering hours from family
|
|
251
|
+
// and patch shape, before anything runs). This module is the opposite: it
|
|
252
|
+
// records what the pipeline observed and reports the distribution.
|
|
253
|
+
//
|
|
254
|
+
// THE HONESTY RULES, which are most of why this file is longer than a mean:
|
|
255
|
+
//
|
|
256
|
+
// 1. A failed attempt is NOT a data point about how long a fix takes. Fixes
|
|
257
|
+
// that fail verification fail fast (a re-scan that still sees the finding
|
|
258
|
+
// never reaches the test suite), so blending them into one average makes
|
|
259
|
+
// the pipeline look faster the worse it performs. Validated and failed
|
|
260
|
+
// attempts are summarised separately and never merged.
|
|
261
|
+
//
|
|
262
|
+
// 2. "Tests skipped" is not "tests passed". A project with no detectable
|
|
263
|
+
// suite can reach `ok:true` having run only the re-scan and the linter.
|
|
264
|
+
// That is a weaker claim than a fix whose suite executed, and it is also
|
|
265
|
+
// much faster, so counting the two together would quietly deflate the
|
|
266
|
+
// headline. They get their own bucket: `validated` means the suite ran and
|
|
267
|
+
// passed, `validatedWithoutTests` means there was no suite to run.
|
|
268
|
+
//
|
|
269
|
+
// 3. Every figure carries its `n`, and a percentile computed from too few
|
|
270
|
+
// samples is labelled unreliable rather than omitted or silently reported.
|
|
271
|
+
// Same precedent as the accuracy scorecard's `{n, d}` rates: a number
|
|
272
|
+
// without its denominator is not a measurement.
|
|
273
|
+
//
|
|
274
|
+
// Storage is append-only JSONL at `<scanRoot>/.agentic-security/fix-metrics.jsonl`,
|
|
275
|
+
// one record per verification attempt. Nothing here throws (posture
|
|
276
|
+
// convention) — an unwritable or corrupt log degrades to "no metrics", never
|
|
277
|
+
// to a failed verification.
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
const LOG_FILE = 'fix-metrics.jsonl';
|
|
284
|
+
|
|
285
|
+
// Below this many samples a percentile is an artifact of the sample, not a
|
|
286
|
+
// property of the pipeline. Reported anyway (hiding it invites re-deriving it
|
|
287
|
+
// wrong downstream) but flagged, so a caller cannot quote it as settled.
|
|
288
|
+
const RELIABLE_N = 10;
|
|
289
|
+
|
|
290
|
+
// The stages verifyFix runs, in execution order. Kept here so the recorder and
|
|
291
|
+
// the summariser cannot drift apart on stage naming.
|
|
292
|
+
const FIX_STAGES = Object.freeze(['rescan', 'lint', 'tests', 'honesty', 'poc']);
|
|
293
|
+
|
|
294
|
+
function _logPath(scanRoot) {
|
|
295
|
+
return (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, LOG_FILE);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Append one verification attempt. Best-effort and silent on failure: metrics
|
|
300
|
+
* must never be able to fail a fix that otherwise verified.
|
|
301
|
+
*
|
|
302
|
+
* @returns {boolean} whether the record was written (for tests, not callers).
|
|
303
|
+
*/
|
|
304
|
+
function recordFixAttempt(scanRoot, record) {
|
|
305
|
+
if (!scanRoot || !record || typeof record !== 'object') return false;
|
|
306
|
+
try {
|
|
307
|
+
const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
|
|
308
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return false;
|
|
309
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return false;
|
|
310
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
|
|
311
|
+
// One writeSync of one newline-terminated line: a concurrent reader sees
|
|
312
|
+
// whole records or nothing, and a torn tail is dropped on read.
|
|
313
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.appendFileSync(_logPath(scanRoot), JSON.stringify(record) + '\n', 'utf8');
|
|
314
|
+
return true;
|
|
315
|
+
} catch { return false; }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Read every well-formed attempt. A line that does not parse is skipped, not
|
|
320
|
+
* fatal — the last line of an interrupted write is the expected case.
|
|
321
|
+
*/
|
|
322
|
+
function loadFixAttempts(scanRoot) {
|
|
323
|
+
try {
|
|
324
|
+
const raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(_logPath(scanRoot), 'utf8');
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const line of raw.split('\n')) {
|
|
327
|
+
if (!line.trim()) continue;
|
|
328
|
+
try {
|
|
329
|
+
const rec = JSON.parse(line);
|
|
330
|
+
if (rec && typeof rec === 'object' && typeof rec.totalMs === 'number') out.push(rec);
|
|
331
|
+
} catch { /* torn or hand-edited line — drop it, keep the rest */ }
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
} catch { return []; }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Nearest-rank percentile over an ascending array. Nearest-rank rather than
|
|
338
|
+
// interpolated because these are observed durations, and an interpolated p50
|
|
339
|
+
// reports a duration that no run actually took.
|
|
340
|
+
function _pct(sorted, p) {
|
|
341
|
+
if (!sorted.length) return null;
|
|
342
|
+
const rank = Math.ceil((p / 100) * sorted.length);
|
|
343
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _dist(values) {
|
|
347
|
+
const v = values.filter(x => typeof x === 'number' && Number.isFinite(x) && x >= 0).sort((a, b) => a - b);
|
|
348
|
+
if (!v.length) return { n: 0, minMs: null, p50Ms: null, p90Ms: null, maxMs: null, meanMs: null, reliable: false };
|
|
349
|
+
const sum = v.reduce((a, b) => a + b, 0);
|
|
350
|
+
return {
|
|
351
|
+
n: v.length,
|
|
352
|
+
minMs: v[0],
|
|
353
|
+
p50Ms: _pct(v, 50),
|
|
354
|
+
p90Ms: _pct(v, 90),
|
|
355
|
+
maxMs: v[v.length - 1],
|
|
356
|
+
meanMs: Math.round(sum / v.length),
|
|
357
|
+
// Says whether the percentiles above may be quoted, not whether the count
|
|
358
|
+
// is real. n and min/max/mean are exact at any sample size.
|
|
359
|
+
reliable: v.length >= RELIABLE_N,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Which bucket an attempt belongs to. Deliberately total: every attempt lands
|
|
364
|
+
// in exactly one, so the bucket counts always sum to the attempt count and a
|
|
365
|
+
// mis-shaped record cannot silently vanish from the denominator.
|
|
366
|
+
function bucketOf(a) {
|
|
367
|
+
if (!a?.ok) return 'failed';
|
|
368
|
+
return a.testsRan ? 'validated' : 'validatedWithoutTests';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Summarise a set of attempts into the reported distribution.
|
|
373
|
+
*
|
|
374
|
+
* `validated` is the headline: attempts that verified AND whose test suite
|
|
375
|
+
* actually ran and passed. The other two buckets exist so that headline cannot
|
|
376
|
+
* be inflated by counting weaker or faster outcomes inside it.
|
|
377
|
+
*/
|
|
378
|
+
function summarizeFixDurations(attempts) {
|
|
379
|
+
const all = Array.isArray(attempts) ? attempts : [];
|
|
380
|
+
const buckets = { validated: [], validatedWithoutTests: [], failed: [] };
|
|
381
|
+
for (const a of all) buckets[bucketOf(a)].push(a);
|
|
382
|
+
|
|
383
|
+
const byStage = {};
|
|
384
|
+
for (const stage of FIX_STAGES) {
|
|
385
|
+
// Per-stage timings come from validated runs only. A stage's duration in a
|
|
386
|
+
// failed run is truncated by the failure (the pipeline stops), so mixing
|
|
387
|
+
// them in would understate every stage after the first failure point.
|
|
388
|
+
byStage[stage] = _dist(buckets.validated.map(a => a?.stages?.[stage]));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
attempts: all.length,
|
|
393
|
+
counts: {
|
|
394
|
+
validated: buckets.validated.length,
|
|
395
|
+
validatedWithoutTests: buckets.validatedWithoutTests.length,
|
|
396
|
+
failed: buckets.failed.length,
|
|
397
|
+
},
|
|
398
|
+
timeToValidatedFix: _dist(buckets.validated.map(a => a.totalMs)),
|
|
399
|
+
timeToValidatedFixWithoutTests: _dist(buckets.validatedWithoutTests.map(a => a.totalMs)),
|
|
400
|
+
timeToFailure: _dist(buckets.failed.map(a => a.totalMs)),
|
|
401
|
+
byStage,
|
|
402
|
+
reliableAtOrAbove: RELIABLE_N,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Read + summarise in one step. */
|
|
407
|
+
function fixDurationReport(scanRoot) {
|
|
408
|
+
return summarizeFixDurations(loadFixAttempts(scanRoot));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function _ms(v) {
|
|
412
|
+
if (v == null) return '—';
|
|
413
|
+
return v >= 1000 ? `${(v / 1000).toFixed(1)}s` : `${v}ms`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* One-paragraph human summary. Returns null when there is nothing measured —
|
|
418
|
+
* callers print nothing rather than printing an empty table.
|
|
419
|
+
*/
|
|
420
|
+
function renderFixDurationSummary(sum) {
|
|
421
|
+
if (!sum || !sum.attempts) return null;
|
|
422
|
+
const d = sum.timeToValidatedFix;
|
|
423
|
+
const parts = [];
|
|
424
|
+
if (d.n) {
|
|
425
|
+
parts.push(
|
|
426
|
+
`time-to-validated-fix: median ${_ms(d.p50Ms)}, p90 ${_ms(d.p90Ms)} `
|
|
427
|
+
+ `(n=${d.n}${d.reliable ? '' : `, below ${sum.reliableAtOrAbove} — percentiles not yet reliable`})`,
|
|
428
|
+
);
|
|
429
|
+
} else {
|
|
430
|
+
parts.push('time-to-validated-fix: no fix has both verified and had its test suite run yet');
|
|
431
|
+
}
|
|
432
|
+
if (sum.counts.validatedWithoutTests) {
|
|
433
|
+
parts.push(`${sum.counts.validatedWithoutTests} verified with no detectable test suite (excluded from the median above)`);
|
|
434
|
+
}
|
|
435
|
+
if (sum.counts.failed) {
|
|
436
|
+
parts.push(`${sum.counts.failed} failed verification, median ${_ms(sum.timeToFailure.p50Ms)} (counted separately)`);
|
|
437
|
+
}
|
|
438
|
+
return parts.join('; ') + '.';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const _internals = { _dist, _pct, RELIABLE_N };
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
// ── PRD F6.1 — score fixes on THREE AXES, not one ──────────────────────────
|
|
445
|
+
//
|
|
446
|
+
// The three axes the PRD names:
|
|
447
|
+
// (a) does the finding disappear — the rescan leg
|
|
448
|
+
// (b) does the project's own suite pass — the tests leg
|
|
449
|
+
// (c) does an independent verifier agree — the PoC re-check leg
|
|
450
|
+
//
|
|
451
|
+
// All three were already computed by verifyFixCore and then collapsed into one
|
|
452
|
+
// boolean, which is the problem: **(a) alone is satisfiable by deleting code.**
|
|
453
|
+
// A patch that removes the vulnerable function passes the rescan, has nothing
|
|
454
|
+
// left to fail, and — on a project with no detectable test suite — reaches
|
|
455
|
+
// ok:true having proven only that the detector went quiet.
|
|
456
|
+
//
|
|
457
|
+
// Reporting the axes separately makes that visible. `aOnly` is the number that
|
|
458
|
+
// matters most and the one nobody was publishing: attempts that satisfied ONLY
|
|
459
|
+
// the disappearance axis. A high aOnly with a high headline is the shape of a
|
|
460
|
+
// remediation feature that is deleting code and calling it a fix.
|
|
461
|
+
function summarizeFixAxes(attempts) {
|
|
462
|
+
const list = Array.isArray(attempts) ? attempts.filter(Boolean) : [];
|
|
463
|
+
const d = list.length;
|
|
464
|
+
|
|
465
|
+
const rate = (pred) => ({ n: list.filter(pred).length, d });
|
|
466
|
+
|
|
467
|
+
// Each axis is judged INDEPENDENTLY of the overall verdict, so a leg that
|
|
468
|
+
// passed inside a failed attempt still counts for its own axis. Reading them
|
|
469
|
+
// off `ok` would make the three axes three copies of the same number.
|
|
470
|
+
const findingDisappeared = rate((a) => a.rescanOk === true || (a.ok === true && a.rescanOk !== false));
|
|
471
|
+
const testsStillPass = rate((a) => a.testsRan === true && a.testsOk !== false);
|
|
472
|
+
const verifierAgrees = rate((a) => a.pocOk === true);
|
|
473
|
+
|
|
474
|
+
const satisfiesAll = rate((a) =>
|
|
475
|
+
(a.rescanOk === true || (a.ok === true && a.rescanOk !== false))
|
|
476
|
+
&& a.testsRan === true && a.testsOk !== false
|
|
477
|
+
&& a.pocOk === true);
|
|
478
|
+
|
|
479
|
+
// The honesty number: disappearance WITHOUT either corroborating axis.
|
|
480
|
+
const aOnly = rate((a) => {
|
|
481
|
+
const disappeared = a.rescanOk === true || (a.ok === true && a.rescanOk !== false);
|
|
482
|
+
const corroborated = (a.testsRan === true && a.testsOk !== false) || a.pocOk === true;
|
|
483
|
+
return disappeared && !corroborated;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
total: d,
|
|
488
|
+
findingDisappeared,
|
|
489
|
+
testsStillPass,
|
|
490
|
+
verifierAgrees,
|
|
491
|
+
satisfiesAll,
|
|
492
|
+
aOnly,
|
|
493
|
+
meaning:
|
|
494
|
+
'findingDisappeared = the detector went quiet; testsStillPass = the project suite ran AND passed; '
|
|
495
|
+
+ 'verifierAgrees = an independent PoC re-check confirmed the hole is shut. '
|
|
496
|
+
+ 'aOnly counts attempts that satisfied ONLY disappearance — the shape a code-deleting "fix" produces.',
|
|
497
|
+
caveat: d === 0
|
|
498
|
+
? 'no attempts recorded; every rate is 0/0 and means nothing'
|
|
499
|
+
: 'rates carry {n,d}; a small d is indicative, not settled',
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Markdown for a report. Denominators always attached. */
|
|
504
|
+
function renderFixAxes(sum) {
|
|
505
|
+
if (!sum || !sum.total) return '_No fix attempts recorded._\n';
|
|
506
|
+
const row = (label, r, note) => `| ${label} | ${r.n}/${r.d} | ${note} |`;
|
|
507
|
+
return [
|
|
508
|
+
'| Axis | Rate | Meaning |',
|
|
509
|
+
'|---|---|---|',
|
|
510
|
+
row('(a) finding disappeared', sum.findingDisappeared, 'the detector went quiet'),
|
|
511
|
+
row('(b) project tests pass', sum.testsStillPass, 'the suite RAN and passed'),
|
|
512
|
+
row('(c) verifier agrees', sum.verifierAgrees, 'an independent PoC re-check confirmed it'),
|
|
513
|
+
row('all three', sum.satisfiesAll, 'the only row that means "fixed"'),
|
|
514
|
+
row('(a) ALONE', sum.aOnly, 'satisfiable by deleting code — watch this number'),
|
|
515
|
+
'',
|
|
516
|
+
].join('\n');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
/***/ }),
|
|
521
|
+
|
|
522
|
+
/***/ 27838:
|
|
523
|
+
/***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
524
|
+
|
|
525
|
+
__webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
|
|
526
|
+
__webpack_require__.r(__webpack_exports__);
|
|
527
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
528
|
+
/* harmony export */ runProjectLinter: () => (/* binding */ runProjectLinter),
|
|
529
|
+
/* harmony export */ verifyFix: () => (/* binding */ verifyFix),
|
|
530
|
+
/* harmony export */ verifyPatch: () => (/* binding */ verifyPatch)
|
|
531
|
+
/* harmony export */ });
|
|
532
|
+
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(31421);
|
|
533
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
|
|
534
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
|
|
535
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(67198);
|
|
536
|
+
/* harmony import */ var _fix_honesty_gate_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27785);
|
|
537
|
+
/* harmony import */ var _test_runner_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(35136);
|
|
538
|
+
/* harmony import */ var _fix_metrics_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(32238);
|
|
539
|
+
var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_engine_js__WEBPACK_IMPORTED_MODULE_3__]);
|
|
540
|
+
_engine_js__WEBPACK_IMPORTED_MODULE_3__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
|
|
541
|
+
// Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
|
|
542
|
+
//
|
|
543
|
+
// Given a candidate patch (the new file content + the finding stableId being
|
|
544
|
+
// fixed), verify it:
|
|
545
|
+
//
|
|
546
|
+
// 1. The original finding's stableId no longer fires on the patched file.
|
|
547
|
+
// 2. No new findings at severity ≥ medium were introduced by the patch.
|
|
548
|
+
// 3. The project's existing linter (when present) passes on the patched file.
|
|
549
|
+
// 4. The project's own test suite (when detectable) still passes. This is
|
|
550
|
+
// the R5 gap-closer: a patch that silently deletes the feature would
|
|
551
|
+
// satisfy (1) and (2) just as well as a real fix — only running the
|
|
552
|
+
// tests catches that. See `test-runner.js` for detection + execution.
|
|
553
|
+
//
|
|
554
|
+
// If any of those fail, the caller is expected to NOT apply the patch and
|
|
555
|
+
// instead surface a "fix plan" — a numbered list of steps the engineer can
|
|
556
|
+
// follow — rather than dump a broken patch on the user.
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
567
|
+
|
|
568
|
+
// Run a focused re-scan over just the patched file(s) using the in-memory
|
|
569
|
+
// engine. No filesystem write needed — we hand the new content in via the
|
|
570
|
+
// fileContents map.
|
|
571
|
+
async function verifyPatch({
|
|
572
|
+
scanRoot,
|
|
573
|
+
originalFindingStableId,
|
|
574
|
+
files, // { [relPath]: newContent }
|
|
575
|
+
depFileContents = {},
|
|
576
|
+
} = {}) {
|
|
577
|
+
if (!files || typeof files !== 'object') return { ok: false, reason: 'no-files-provided' };
|
|
578
|
+
const fileContents = { ...files };
|
|
579
|
+
let scan;
|
|
580
|
+
try {
|
|
581
|
+
// `provenance:false` is REQUIRED here, not an optimisation. This scan is
|
|
582
|
+
// deliberately scoped to just the patched file(s), so its finding set is a
|
|
583
|
+
// tiny subset of the project's. updateLifecycle marks every open stableId
|
|
584
|
+
// NOT in the set it is handed as `remediated` — so a single fix
|
|
585
|
+
// verification (every /fix, apply_fix, and autopilot iteration runs one)
|
|
586
|
+
// would mass-mark the rest of the project as remediated, then
|
|
587
|
+
// `reintroduced` on the next real scan. The patched content is also not
|
|
588
|
+
// committed, so there is no history to resolve provenance against anyway.
|
|
589
|
+
scan = await (0,_engine_js__WEBPACK_IMPORTED_MODULE_3__/* .runFullScan */ .wW)({ fileContents, depFileContents, scanRoot, provenance: false }, () => {});
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return { ok: false, reason: 'rescan-failed', error: e.message };
|
|
592
|
+
}
|
|
593
|
+
const findings = (scan && scan.findings) || [];
|
|
594
|
+
const stillHasOriginal = !!originalFindingStableId &&
|
|
595
|
+
findings.some(f => f.stableId === originalFindingStableId);
|
|
596
|
+
if (stillHasOriginal) {
|
|
597
|
+
return { ok: false, reason: 'original-finding-still-present', stableId: originalFindingStableId };
|
|
598
|
+
}
|
|
599
|
+
const introducedHighOrAbove = findings.filter(f =>
|
|
600
|
+
(SEVERITY_RANK[f.severity] ?? 9) <= SEVERITY_RANK.medium);
|
|
601
|
+
// Don't count findings on lines outside the patched files — but our
|
|
602
|
+
// fileContents map IS the patched files, so every finding is in-scope.
|
|
603
|
+
return {
|
|
604
|
+
ok: introducedHighOrAbove.length === 0,
|
|
605
|
+
reason: introducedHighOrAbove.length === 0 ? 'verified' : 'introduced-new-findings',
|
|
606
|
+
introduced: introducedHighOrAbove.map(f => ({
|
|
607
|
+
vuln: f.vuln, file: f.file, line: f.line, severity: f.severity,
|
|
608
|
+
stableId: f.stableId,
|
|
609
|
+
})),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Detect which linter the project uses and run it on the patched files.
|
|
614
|
+
// Returns { ok, runner, output } or { ok: true, runner: 'none' } when no
|
|
615
|
+
// linter is configured (silent pass).
|
|
616
|
+
function runProjectLinter(scanRoot, filePaths) {
|
|
617
|
+
if (!scanRoot || !Array.isArray(filePaths) || filePaths.length === 0) {
|
|
618
|
+
return { ok: true, runner: 'none' };
|
|
619
|
+
}
|
|
620
|
+
const has = (p) => { try { return node_fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, p)); } catch { return false; } };
|
|
621
|
+
// Pick the linter by config file present in the repo root.
|
|
622
|
+
const jsFiles = filePaths.filter(f => /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(f));
|
|
623
|
+
const pyFiles = filePaths.filter(f => /\.py$/i.test(f));
|
|
624
|
+
const goFiles = filePaths.filter(f => /\.go$/i.test(f));
|
|
625
|
+
const javaFiles = filePaths.filter(f => /\.java$/i.test(f));
|
|
626
|
+
|
|
627
|
+
if (jsFiles.length && (has('.eslintrc') || has('.eslintrc.json') || has('.eslintrc.js') || has('eslint.config.js') || has('eslint.config.mjs'))) {
|
|
628
|
+
return runLinter(scanRoot, 'eslint', ['--no-error-on-unmatched-pattern', ...jsFiles]);
|
|
629
|
+
}
|
|
630
|
+
if (pyFiles.length && (has('pyproject.toml') || has('ruff.toml') || has('.ruff.toml'))) {
|
|
631
|
+
return runLinter(scanRoot, 'ruff', ['check', ...pyFiles]);
|
|
632
|
+
}
|
|
633
|
+
if (pyFiles.length && has('.flake8')) {
|
|
634
|
+
return runLinter(scanRoot, 'flake8', pyFiles);
|
|
635
|
+
}
|
|
636
|
+
if (goFiles.length && (has('.golangci.yml') || has('.golangci.yaml'))) {
|
|
637
|
+
return runLinter(scanRoot, 'golangci-lint', ['run', ...goFiles]);
|
|
638
|
+
}
|
|
639
|
+
if (javaFiles.length && has('checkstyle.xml')) {
|
|
640
|
+
return runLinter(scanRoot, 'checkstyle', ['-c', 'checkstyle.xml', ...javaFiles]);
|
|
641
|
+
}
|
|
642
|
+
return { ok: true, runner: 'none' };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function runLinter(cwd, cmd, args) {
|
|
646
|
+
let r;
|
|
647
|
+
try {
|
|
648
|
+
r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)(cmd, args, { cwd, encoding: 'utf8', timeout: 60_000 });
|
|
649
|
+
} catch (e) {
|
|
650
|
+
return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing', error: e.message };
|
|
651
|
+
}
|
|
652
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
653
|
+
return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing' };
|
|
654
|
+
}
|
|
655
|
+
if (r.status === null) {
|
|
656
|
+
return { ok: false, runner: cmd, reason: 'timed-out', output: (r.stderr || r.stdout || '').slice(-2000) };
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
ok: r.status === 0,
|
|
660
|
+
runner: cmd,
|
|
661
|
+
exitCode: r.status,
|
|
662
|
+
output: ((r.stderr || '') + (r.stdout || '')).slice(-2000),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Top-level verify: re-scan + lint. Returns the combined verdict + a
|
|
667
|
+
// human-readable summary string suitable for surfacing to the user.
|
|
668
|
+
// Addition #7 — deterministic honesty gates on fix output. When the caller
|
|
669
|
+
// supplies `fixMeta` ({ residual, verdict, evidence, signals }) — e.g. the
|
|
670
|
+
// security-fixer agent's residual-risk text + completeness signals — the fix's
|
|
671
|
+
// claims are checked mechanically (no hand-wave residual prose, a cited
|
|
672
|
+
// file:line for any FP/safe verdict, and a FULL/MITIGATION/WORKAROUND tier). A
|
|
673
|
+
// dishonest or over-claiming fix fails the gate. When `fixMeta` is absent
|
|
674
|
+
// (the deterministic MCP write path, which has no claims to check) the honesty
|
|
675
|
+
// gate is skipped and behavior is unchanged.
|
|
676
|
+
// R5 (partial) — the test-suite stage. Runs the target project's own tests,
|
|
677
|
+
// in the target project's own directory, against whatever is currently on
|
|
678
|
+
// disk there. See `test-runner.js`'s header comment for why that run is
|
|
679
|
+
// deliberately NOT routed through the R1 PoC-confinement sandbox: this is
|
|
680
|
+
// the project's own already-trusted suite, not untrusted synthesized code.
|
|
681
|
+
//
|
|
682
|
+
// Caveat that matters for callers: `verifyPatch` above re-scans the
|
|
683
|
+
// candidate patch purely in memory (no write to disk), but a test runner
|
|
684
|
+
// needs real files — there is no cheap way to hand a runner an in-memory
|
|
685
|
+
// overlay. So this leg reports on the CURRENT on-disk tree, not the
|
|
686
|
+
// candidate `files` map, when `verifyFix` is used as a pre-write preview
|
|
687
|
+
// (e.g. the `verify_fix` MCP tool). Callers that apply the patch first and
|
|
688
|
+
// then re-verify get the strongest signal; that ordering is not enforced
|
|
689
|
+
// here — it's the caller's responsibility, same as it already is for the
|
|
690
|
+
// closed-loop `fix-verify-loop.js` path.
|
|
691
|
+
// Does the caller's candidate patch differ from what is on disk right now?
|
|
692
|
+
// If so, any test run necessarily exercised the pre-patch tree. Compared by
|
|
693
|
+
// content so a patch that happens to match disk (already applied) is correctly
|
|
694
|
+
// treated as NOT pre-patch.
|
|
695
|
+
function _candidateDiffersFromDisk(scanRoot, files) {
|
|
696
|
+
if (!files || typeof files !== 'object') return false;
|
|
697
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
698
|
+
if (typeof content !== 'string') continue;
|
|
699
|
+
try {
|
|
700
|
+
const abs = node_path__WEBPACK_IMPORTED_MODULE_2__.resolve(scanRoot, rel);
|
|
701
|
+
if (node_fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(abs, 'utf8') !== content) return true;
|
|
702
|
+
} catch {
|
|
703
|
+
return true; // candidate file absent on disk -> definitely not applied
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function verifyFix({
|
|
710
|
+
scanRoot,
|
|
711
|
+
originalFindingStableId,
|
|
712
|
+
files,
|
|
713
|
+
depFileContents,
|
|
714
|
+
fixMeta,
|
|
715
|
+
testTimeoutMs,
|
|
716
|
+
recordMetrics = true,
|
|
717
|
+
poc,
|
|
718
|
+
} = {}) {
|
|
719
|
+
// R5 (reporting half) — time each stage as it runs. Measured here rather
|
|
720
|
+
// than inside each stage because only this function knows the boundaries of
|
|
721
|
+
// one verification ATTEMPT, which is the unit the distribution is over.
|
|
722
|
+
const stages = {};
|
|
723
|
+
const t0 = Date.now();
|
|
724
|
+
let mark = t0;
|
|
725
|
+
const _lap = (name) => { const now = Date.now(); stages[name] = now - mark; mark = now; };
|
|
726
|
+
|
|
727
|
+
const rescan = await verifyPatch({ scanRoot, originalFindingStableId, files, depFileContents });
|
|
728
|
+
_lap('rescan');
|
|
729
|
+
const lint = runProjectLinter(scanRoot, Object.keys(files || {}));
|
|
730
|
+
_lap('lint');
|
|
731
|
+
const tests = (0,_test_runner_js__WEBPACK_IMPORTED_MODULE_5__/* .runProjectTests */ .I)(scanRoot, testTimeoutMs != null ? { timeoutMs: testTimeoutMs } : {});
|
|
732
|
+
_lap('tests');
|
|
733
|
+
// True when a candidate patch was supplied but has not been written, so the
|
|
734
|
+
// suite necessarily ran against the pre-patch tree. Surfaced in the summary
|
|
735
|
+
// and on the result so a caller cannot mistake it for a verified patch.
|
|
736
|
+
const _testedPrePatch = !tests.skipped && _candidateDiffersFromDisk(scanRoot, files);
|
|
737
|
+
const testsOk = tests.skipped ? true : tests.passed === true;
|
|
738
|
+
// R5 — the PoC leg. Re-run the finding's proof-of-concept against the
|
|
739
|
+
// CANDIDATE patch inside R1's sandbox. A patch that still lets the PoC
|
|
740
|
+
// demonstrate the predicted effect has not fixed anything, however green the
|
|
741
|
+
// re-scan looks: the re-scan only proves the DETECTOR stopped firing, which
|
|
742
|
+
// a cosmetic edit can achieve. Execution is the stronger claim.
|
|
743
|
+
//
|
|
744
|
+
// Direction matters and is asymmetric on purpose. `execution-proven` after
|
|
745
|
+
// the patch is a hard FAIL. Anything else is NOT a pass — a PoC that failed
|
|
746
|
+
// to run, or a sandbox that could not start, is recorded as `inconclusive`
|
|
747
|
+
// and left out of the verdict entirely. Treating "could not prove it" as
|
|
748
|
+
// "fixed" is exactly the false confidence this leg exists to prevent.
|
|
749
|
+
//
|
|
750
|
+
// Computed BEFORE the honesty gate (FR-308): a still-exploitable PoC is
|
|
751
|
+
// MECHANICAL evidence, not a self-report, and gateFixOutput cross-checks a
|
|
752
|
+
// self-reported FULL tier against it below — the ordering matters, not
|
|
753
|
+
// just the value.
|
|
754
|
+
let pocLeg = { status: 'not-requested', reason: null, tier: null };
|
|
755
|
+
if (poc?.code) {
|
|
756
|
+
try {
|
|
757
|
+
const { proveFinding } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1291));
|
|
758
|
+
const proved = await proveFinding({ ...(poc.finding || {}), poc }, { files });
|
|
759
|
+
const tier = proved.proofTier;
|
|
760
|
+
pocLeg = tier === 'execution-proven'
|
|
761
|
+
? { status: 'still-exploitable', tier, reason: proved.proofEvidence?.observed || null }
|
|
762
|
+
: proved.proofEvidence?.ran
|
|
763
|
+
? { status: 'no-longer-proven', tier, reason: proved.proofEvidence?.reason || null }
|
|
764
|
+
: { status: 'inconclusive', tier, reason: proved.proofEvidence?.reason || null };
|
|
765
|
+
} catch (e) {
|
|
766
|
+
pocLeg = { status: 'inconclusive', tier: null, reason: `proof harness error: ${e.message}` };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
_lap('poc');
|
|
770
|
+
const pocOk = pocLeg.status !== 'still-exploitable';
|
|
771
|
+
|
|
772
|
+
// FR-308: "a mitigation or workaround cannot be represented as a full
|
|
773
|
+
// fix" — gateFixOutput's tier/residual check is a pure self-consistency
|
|
774
|
+
// check (fixMeta.signals is agent-self-reported; see fix-honesty-gate.js's
|
|
775
|
+
// header for why nothing there is server-computable). pocLeg IS
|
|
776
|
+
// server-computable — a real execution result, not a claim — so it is
|
|
777
|
+
// passed through as the one MECHANICAL cross-check available: a
|
|
778
|
+
// self-reported FULL tier is refuted, not just internally inconsistent,
|
|
779
|
+
// when the PoC still demonstrates the vulnerability against the patch.
|
|
780
|
+
// D-0024: `fixMeta` is a shared envelope — FR-307/FR-1002's `approval`
|
|
781
|
+
// key lives alongside FR-308's completeness self-report (`residual`/
|
|
782
|
+
// `verdict`/`evidence`/`signals`). Gating on mere object-truthiness meant
|
|
783
|
+
// a caller supplying ONLY `approval` (a real, common shape once the CLI's
|
|
784
|
+
// --approved-by flag and the MCP schema fix made that reachable) got
|
|
785
|
+
// silently gated on an UNRELATED FR-308 self-consistency check it never
|
|
786
|
+
// engaged with — computeFixTier(undefined) defaults to MITIGATION, which
|
|
787
|
+
// then demands a `residual` nobody was ever asked to supply, blocking an
|
|
788
|
+
// otherwise-genuine, approved fix for a reason that has nothing to do
|
|
789
|
+
// with completeness honesty. Scope the gate to fixMeta shapes that
|
|
790
|
+
// actually make a completeness-adjacent claim.
|
|
791
|
+
const hasHonestyClaim = fixMeta && typeof fixMeta === 'object' &&
|
|
792
|
+
(fixMeta.residual !== undefined || fixMeta.verdict !== undefined ||
|
|
793
|
+
fixMeta.evidence !== undefined || fixMeta.signals !== undefined);
|
|
794
|
+
let honesty = null;
|
|
795
|
+
if (hasHonestyClaim) {
|
|
796
|
+
try { honesty = (0,_fix_honesty_gate_js__WEBPACK_IMPORTED_MODULE_4__/* .gateFixOutput */ ._u)(fixMeta, { pocLeg }); } catch { honesty = null; }
|
|
797
|
+
}
|
|
798
|
+
_lap('honesty');
|
|
799
|
+
|
|
800
|
+
const ok = rescan.ok && (lint.ok || lint.skipped) && testsOk && pocOk && (honesty ? honesty.ok : true);
|
|
801
|
+
|
|
802
|
+
// FR-305 (assurance-hardening PRD): `ok` alone conflates "every leg
|
|
803
|
+
// genuinely ran and passed" with "passed, but a required leg was skipped
|
|
804
|
+
// or unavailable" — `lint.ok`/`testsOk` are both true in the skipped case
|
|
805
|
+
// by design (this codebase does not fail-closed just because a repo has
|
|
806
|
+
// no linter or no detected test runner), so a caller checking only `ok`
|
|
807
|
+
// cannot tell the difference. `lint.skipped` is only ever true when a
|
|
808
|
+
// linter WAS configured but its binary could not be run (missing config
|
|
809
|
+
// entirely returns `runner: 'none'` with no `skipped` field at all — that
|
|
810
|
+
// is a genuine N/A, nothing was required, not a degradation). `verifiedFull`
|
|
811
|
+
// is the honest label: true only when nothing that WAS required was
|
|
812
|
+
// skipped. A caller must never present `ok: true, verifiedFull: false` as
|
|
813
|
+
// "fully verified" — `degradedLegs` names exactly what was skipped so a
|
|
814
|
+
// report can say so plainly instead of a bare pass.
|
|
815
|
+
const degradedLegs = [];
|
|
816
|
+
if (lint.skipped) degradedLegs.push(`lint: ${lint.runner} not installed`);
|
|
817
|
+
if (tests.skipped) degradedLegs.push(`tests: skipped (${tests.reason})`);
|
|
818
|
+
const verifiedFull = ok && degradedLegs.length === 0;
|
|
819
|
+
|
|
820
|
+
const durations = { ...stages, totalMs: Date.now() - t0 };
|
|
821
|
+
const summary = [
|
|
822
|
+
`re-scan: ${rescan.ok ? 'PASS' : 'FAIL — ' + rescan.reason}`,
|
|
823
|
+
`linter: ${lint.runner === 'none' ? 'skipped (no linter config)'
|
|
824
|
+
: lint.skipped ? `${lint.runner} not installed`
|
|
825
|
+
: lint.ok ? `${lint.runner} PASS`
|
|
826
|
+
: `${lint.runner} FAIL (exit ${lint.exitCode})`}`,
|
|
827
|
+
// Say which tree the suite actually ran against. `files` is a candidate
|
|
828
|
+
// patch held in memory; the runner needs real files, so it sees whatever is
|
|
829
|
+
// on disk. Reporting a bare "PASS" here would let a caller believe the
|
|
830
|
+
// PATCH passed the tests when the suite may have run on unpatched code.
|
|
831
|
+
`tests: ${tests.skipped ? `skipped (${tests.reason})`
|
|
832
|
+
: tests.timedOut ? 'FAIL (timed out)'
|
|
833
|
+
: tests.passed ? `PASS${_testedPrePatch ? ' — on the CURRENT on-disk tree, NOT the candidate patch' : ''}`
|
|
834
|
+
: `FAIL (exit ${tests.exitCode})`}`,
|
|
835
|
+
honesty ? `honesty: ${honesty.ok ? `PASS (${honesty.tier})` : 'FAIL — ' + honesty.violations.join('; ')}` : null,
|
|
836
|
+
// Never render `inconclusive` as a pass — say plainly that nothing was proven.
|
|
837
|
+
pocLeg.status === 'not-requested' ? null
|
|
838
|
+
: pocLeg.status === 'still-exploitable' ? `poc: FAIL — the proof-of-concept still demonstrates the vulnerability against the patch`
|
|
839
|
+
: pocLeg.status === 'no-longer-proven' ? 'poc: PASS (ran against the patch and no longer demonstrates the vulnerability)'
|
|
840
|
+
: `poc: inconclusive — not counted either way (${pocLeg.reason || 'no detail reported'})`,
|
|
841
|
+
// FR-305: never let a degraded pass read the same as a full one.
|
|
842
|
+
ok && !verifiedFull ? `NOTE: PASSED, but NOT fully verified — ${degradedLegs.join('; ')}` : null,
|
|
843
|
+
].filter(Boolean).join('\n');
|
|
844
|
+
// Persist the attempt so the distribution can be reported from real runs.
|
|
845
|
+
// `testsRan` is the load-bearing field: it is what keeps "verified with no
|
|
846
|
+
// test suite to run" out of the headline time-to-validated-fix bucket.
|
|
847
|
+
// A patch that was never written to disk is recorded too, but flagged — its
|
|
848
|
+
// suite ran against the pre-patch tree, so its timing is real while its
|
|
849
|
+
// verdict is about a different tree.
|
|
850
|
+
if (recordMetrics && scanRoot) {
|
|
851
|
+
(0,_fix_metrics_js__WEBPACK_IMPORTED_MODULE_6__/* .recordFixAttempt */ .I3)(scanRoot, {
|
|
852
|
+
at: new Date().toISOString(),
|
|
853
|
+
stableId: originalFindingStableId || null,
|
|
854
|
+
ok,
|
|
855
|
+
verifiedFull,
|
|
856
|
+
testsRan: !tests.skipped,
|
|
857
|
+
testsPassed: tests.skipped ? null : tests.passed === true,
|
|
858
|
+
testedPrePatch: _testedPrePatch,
|
|
859
|
+
lintRan: !(lint.skipped || lint.runner === 'none'),
|
|
860
|
+
honestyGated: honesty != null,
|
|
861
|
+
pocStatus: pocLeg.status,
|
|
862
|
+
files: Object.keys(files || {}).length,
|
|
863
|
+
stages,
|
|
864
|
+
totalMs: durations.totalMs,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return { ok, verifiedFull, degradedLegs, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
__webpack_async_result__();
|
|
872
|
+
} catch(e) { __webpack_async_result__(e); } });
|
|
873
|
+
|
|
874
|
+
/***/ }),
|
|
875
|
+
|
|
876
|
+
/***/ 35136:
|
|
877
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
878
|
+
|
|
879
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
880
|
+
/* harmony export */ I: () => (/* binding */ runProjectTests)
|
|
881
|
+
/* harmony export */ });
|
|
882
|
+
/* unused harmony export detectTestCommand */
|
|
883
|
+
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(31421);
|
|
884
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
|
|
885
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
|
|
886
|
+
// R5 (partial, roadmap) — the project's own test suite as a verification
|
|
887
|
+
// stage for `verifyFix()` (see `fix-verify.js`). Closes the gap where a
|
|
888
|
+
// "verified" fix only proved a finding's stableId stopped firing — a patch
|
|
889
|
+
// that deletes the feature entirely would satisfy that just as well as a
|
|
890
|
+
// real fix. Running the project's own tests is the cheapest available check
|
|
891
|
+
// that the application still works.
|
|
892
|
+
//
|
|
893
|
+
// Execution-safety note: this spawns the TARGET PROJECT's own test command
|
|
894
|
+
// in the target project's own directory. That is deliberately NOT routed
|
|
895
|
+
// through the R1 confinement sandbox (`../sandbox/`). That sandbox exists to
|
|
896
|
+
// contain untrusted proof-of-concept exploit code the scanner itself
|
|
897
|
+
// synthesizes — code nobody has vetted, being run for the first time. A
|
|
898
|
+
// project's pre-existing test suite is the opposite case: it is the
|
|
899
|
+
// project's own trusted source, already sitting on disk, and running it is
|
|
900
|
+
// exactly what a human developer does by hand before trusting a fix.
|
|
901
|
+
// Wrapping "npm test" / "pytest" / "go test" in the PoC sandbox's
|
|
902
|
+
// syscall/network/filesystem restrictions would break the large majority of
|
|
903
|
+
// real test suites (they bind local ports, spawn child processes, write temp
|
|
904
|
+
// fixtures, etc.) for no corresponding security benefit.
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
911
|
+
const NPM_PLACEHOLDER = /Error: no test specified/i;
|
|
912
|
+
|
|
913
|
+
function _exists(scanRoot, rel) {
|
|
914
|
+
try { return node_fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel)); } catch { return false; }
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function _isDir(scanRoot, rel) {
|
|
918
|
+
try { return node_fs__WEBPACK_IMPORTED_MODULE_1__.statSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel)).isDirectory(); } catch { return false; }
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function _binaryAvailable(cmd) {
|
|
922
|
+
try {
|
|
923
|
+
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)(cmd, ['--version'], { timeout: 5_000, stdio: 'ignore' });
|
|
924
|
+
return !(r.error && r.error.code === 'ENOENT');
|
|
925
|
+
} catch {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Detect the project's test command. Read-only — never spawns the actual
|
|
931
|
+
// test run, only (optionally) a cheap `--version` probe to confirm a tool
|
|
932
|
+
// like `pytest` is actually installed before committing to it. Returns
|
|
933
|
+
// `null` when nothing detectable is found — most scanned repos will hit
|
|
934
|
+
// this path, and that must not be treated as a failure by callers.
|
|
935
|
+
function detectTestCommand(scanRoot) {
|
|
936
|
+
if (!scanRoot) return null;
|
|
937
|
+
|
|
938
|
+
// JS/TS — package.json with a real (non-placeholder) `scripts.test`.
|
|
939
|
+
let pkg = null;
|
|
940
|
+
try { pkg = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, 'package.json'), 'utf8')); } catch { pkg = null; }
|
|
941
|
+
const testScript = pkg && pkg.scripts && pkg.scripts.test;
|
|
942
|
+
if (testScript && !NPM_PLACEHOLDER.test(String(testScript))) {
|
|
943
|
+
if (_exists(scanRoot, 'pnpm-lock.yaml')) return { cmd: 'pnpm', args: ['test'], kind: 'pnpm' };
|
|
944
|
+
if (_exists(scanRoot, 'yarn.lock')) return { cmd: 'yarn', args: ['test'], kind: 'yarn' };
|
|
945
|
+
if (_exists(scanRoot, 'bun.lockb') || _exists(scanRoot, 'bun.lock')) return { cmd: 'bun', args: ['test'], kind: 'bun' };
|
|
946
|
+
return { cmd: 'npm', args: ['test', '--silent'], kind: 'npm' };
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Python — pytest.ini / pyproject.toml / tox.ini / a tests/ dir, and the
|
|
950
|
+
// `pytest` binary actually available. If pytest isn't installed we do NOT
|
|
951
|
+
// report a python test command — falling through lets a later language
|
|
952
|
+
// marker (e.g. go.mod in a polyglot repo) still be detected.
|
|
953
|
+
const pyMarker = _exists(scanRoot, 'pytest.ini') || _exists(scanRoot, 'pyproject.toml') ||
|
|
954
|
+
_exists(scanRoot, 'tox.ini') || _isDir(scanRoot, 'tests');
|
|
955
|
+
if (pyMarker && _binaryAvailable('pytest')) {
|
|
956
|
+
return { cmd: 'pytest', args: ['-q'], kind: 'pytest' };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Go
|
|
960
|
+
if (_exists(scanRoot, 'go.mod')) {
|
|
961
|
+
return { cmd: 'go', args: ['test', './...'], kind: 'go' };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Run the detected test command with a walltime budget. Always returns a
|
|
968
|
+
// result object — never throws. Distinguishes four outcomes:
|
|
969
|
+
// - no detectable/runnable command -> status: 'skipped' (does NOT fail)
|
|
970
|
+
// - ran and exited 0 -> status: 'passed'
|
|
971
|
+
// - ran and exited non-zero -> status: 'failed'
|
|
972
|
+
// - ran past the timeout budget -> status: 'failed', timedOut: true
|
|
973
|
+
function runProjectTests(scanRoot, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
974
|
+
const startedAt = Date.now();
|
|
975
|
+
const command = detectTestCommand(scanRoot);
|
|
976
|
+
if (!command) {
|
|
977
|
+
return {
|
|
978
|
+
status: 'skipped', passed: null, skipped: true,
|
|
979
|
+
reason: 'no-test-command-detected', exitCode: null, timedOut: false,
|
|
980
|
+
durationMs: Date.now() - startedAt,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let r;
|
|
985
|
+
try {
|
|
986
|
+
r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)(command.cmd, command.args, {
|
|
987
|
+
cwd: scanRoot,
|
|
988
|
+
encoding: 'utf8',
|
|
989
|
+
timeout: timeoutMs,
|
|
990
|
+
env: { ...process.env, CI: '1' },
|
|
991
|
+
});
|
|
992
|
+
} catch (e) {
|
|
993
|
+
// The spawn call itself threw (rare — e.g. cwd vanished). Treat as
|
|
994
|
+
// "could not run", not "ran and failed".
|
|
995
|
+
return {
|
|
996
|
+
status: 'skipped', passed: null, skipped: true,
|
|
997
|
+
reason: `spawn-error: ${e.message}`, exitCode: null, timedOut: false,
|
|
998
|
+
durationMs: Date.now() - startedAt,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
const durationMs = Date.now() - startedAt;
|
|
1002
|
+
|
|
1003
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
1004
|
+
// The detected tool isn't actually installed on this machine. Not a
|
|
1005
|
+
// test failure — the suite never ran.
|
|
1006
|
+
return {
|
|
1007
|
+
status: 'skipped', passed: null, skipped: true,
|
|
1008
|
+
reason: `${command.kind}-not-installed`, exitCode: null, timedOut: false, durationMs,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (r.status === null) {
|
|
1013
|
+
// spawnSync sets status:null both on timeout-kill and on being killed by
|
|
1014
|
+
// another signal; either way the run did not complete, which is a
|
|
1015
|
+
// verification failure, never a skip — we asked for a result and the
|
|
1016
|
+
// process was terminated before producing one.
|
|
1017
|
+
return {
|
|
1018
|
+
status: 'failed', passed: false, skipped: false,
|
|
1019
|
+
reason: 'timed-out', exitCode: null, timedOut: true, durationMs,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return {
|
|
1024
|
+
status: r.status === 0 ? 'passed' : 'failed',
|
|
1025
|
+
passed: r.status === 0,
|
|
1026
|
+
skipped: false,
|
|
1027
|
+
reason: r.status === 0 ? null : 'test-failures',
|
|
1028
|
+
exitCode: r.status,
|
|
1029
|
+
timedOut: false,
|
|
1030
|
+
durationMs,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
/***/ })
|
|
1036
|
+
|
|
1037
|
+
};
|