@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/bin/agentic-security.js +73 -10
  3. package/dist/1122.index.js +16 -16
  4. package/dist/1208.index.js +23 -0
  5. package/dist/1301.index.js +3 -3
  6. package/dist/1379.index.js +3 -3
  7. package/dist/1444.index.js +3 -3
  8. package/dist/1660.index.js +1 -1
  9. package/dist/1700.index.js +5 -5
  10. package/dist/1905.index.js +12 -12
  11. package/dist/1920.index.js +10 -10
  12. package/dist/2238.index.js +4 -4
  13. package/dist/2271.index.js +3 -3
  14. package/dist/2432.index.js +8 -8
  15. package/dist/2659.index.js +1 -1
  16. package/dist/2826.index.js +2 -2
  17. package/dist/2830.index.js +3 -3
  18. package/dist/2923.index.js +1 -1
  19. package/dist/3001.index.js +4 -4
  20. package/dist/3117.index.js +4 -4
  21. package/dist/3180.index.js +9 -9
  22. package/dist/3276.index.js +1 -1
  23. package/dist/3415.index.js +1 -1
  24. package/dist/3499.index.js +1 -1
  25. package/dist/3518.index.js +4 -4
  26. package/dist/3736.index.js +7 -7
  27. package/dist/3839.index.js +4 -4
  28. package/dist/4113.index.js +441 -418
  29. package/dist/4265.index.js +4 -4
  30. package/dist/4384.index.js +8 -3
  31. package/dist/4399.index.js +29 -3
  32. package/dist/4547.index.js +2 -2
  33. package/dist/4863.index.js +6 -6
  34. package/dist/4970.index.js +2 -2
  35. package/dist/5051.index.js +15 -15
  36. package/dist/5144.index.js +4 -4
  37. package/dist/5333.index.js +8 -8
  38. package/dist/5343.index.js +2 -2
  39. package/dist/5350.index.js +6 -6
  40. package/dist/5561.index.js +1 -1
  41. package/dist/5637.index.js +10 -5
  42. package/dist/5724.index.js +21642 -0
  43. package/dist/5756.index.js +99 -489
  44. package/dist/5830.index.js +3 -3
  45. package/dist/6257.index.js +21 -8
  46. package/dist/6626.index.js +5 -5
  47. package/dist/6662.index.js +1 -1
  48. package/dist/6675.index.js +4 -4
  49. package/dist/6730.index.js +10 -10
  50. package/dist/6829.index.js +4 -4
  51. package/dist/6944.index.js +2 -2
  52. package/dist/6994.index.js +5 -5
  53. package/dist/7039.index.js +83 -19
  54. package/dist/7178.index.js +11 -6
  55. package/dist/7227.index.js +3 -3
  56. package/dist/7552.index.js +2 -2
  57. package/dist/7709.index.js +2 -2
  58. package/dist/7838.index.js +1037 -0
  59. package/dist/8218.index.js +4 -4
  60. package/dist/8476.index.js +4 -4
  61. package/dist/8513.index.js +8 -8
  62. package/dist/8520.index.js +1 -1
  63. package/dist/8752.index.js +4 -4
  64. package/dist/9207.index.js +2 -2
  65. package/dist/9220.index.js +5 -5
  66. package/dist/9390.index.js +6 -6
  67. package/dist/9503.index.js +2 -2
  68. package/dist/9560.index.js +3221 -0
  69. package/dist/957.index.js +4 -4
  70. package/dist/9801.index.js +1 -1
  71. package/dist/9824.index.js +5 -5
  72. package/dist/agentic-security.mjs +23 -23
  73. package/dist/agentic-security.mjs.sha256 +1 -1
  74. package/package.json +3 -2
  75. package/src/egress/redact.js +1 -1
  76. package/src/engine.js +13 -2
  77. package/src/llm-validator/agent-loop.js +85 -5
  78. package/src/llm-validator/fix-proposal.js +16 -3
  79. package/src/llm-validator/model-capabilities.js +26 -1
  80. package/src/llm-validator/model-probe.js +46 -8
  81. package/src/llm-validator/ollama-provider.js +26 -1
  82. package/src/llm-validator/oom-feedback.js +69 -0
  83. package/src/llm-validator/providers.js +50 -0
  84. package/src/llm-validator/redact.js +177 -11
  85. package/src/report/index.js +14 -3
  86. package/src/sast/java-ast-folding.js +18 -2
@@ -1,8 +1,235 @@
1
1
  export const id = 4113;
2
- export const ids = [4113,2238,3526];
2
+ export const ids = [4113,2238,7838];
3
3
  export const modules = {
4
4
 
5
- /***/ 2238:
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:
6
233
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
234
 
8
235
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
@@ -12,9 +239,9 @@ export const modules = {
12
239
  /* harmony export */ sU: () => (/* binding */ loadFixAttempts)
13
240
  /* harmony export */ });
14
241
  /* 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);
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);
18
245
  // Time-to-validated-fix (R5, the reporting half).
19
246
  //
20
247
  // `verifyFix` already RUNS the stages and `test-runner.js` already times the
@@ -292,17 +519,20 @@ function renderFixAxes(sum) {
292
519
 
293
520
  /***/ }),
294
521
 
295
- /***/ 4113:
296
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
522
+ /***/ 64113:
523
+ /***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
297
524
 
525
+ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
298
526
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
299
527
  /* harmony export */ verifyFixWithTests: () => (/* binding */ verifyFixWithTests)
300
528
  /* harmony export */ });
301
529
  /* unused harmony export runProjectTests */
302
- /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
303
- /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
304
- /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
305
- /* harmony import */ var _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3526);
530
+ /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(31421);
531
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
532
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
533
+ /* harmony import */ var _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(27838);
534
+ var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_fix_verify_js__WEBPACK_IMPORTED_MODULE_3__]);
535
+ _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
306
536
  // Closed-loop fix verification (v0.68).
307
537
  //
308
538
  // Existing `fix-verify.js` does scan + lint. This module adds the third
@@ -470,412 +700,42 @@ function _summarize(legs, verdict) {
470
700
  return `${verdict} (${bits.join(' · ')})`;
471
701
  }
472
702
 
703
+ __webpack_async_result__();
704
+ } catch(e) { __webpack_async_result__(e); } });
473
705
 
474
706
  /***/ }),
475
707
 
476
- /***/ 3526:
477
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
708
+ /***/ 27838:
709
+ /***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
478
710
 
479
- // ESM COMPAT FLAG
711
+ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
480
712
  __webpack_require__.r(__webpack_exports__);
481
-
482
- // EXPORTS
483
- __webpack_require__.d(__webpack_exports__, {
484
- runProjectLinter: () => (/* binding */ runProjectLinter),
485
- verifyFix: () => (/* binding */ verifyFix),
486
- verifyPatch: () => (/* binding */ verifyPatch)
487
- });
488
-
489
- // EXTERNAL MODULE: external "node:child_process"
490
- var external_node_child_process_ = __webpack_require__(1421);
491
- // EXTERNAL MODULE: external "node:fs"
492
- var external_node_fs_ = __webpack_require__(3024);
493
- // EXTERNAL MODULE: external "node:path"
494
- var external_node_path_ = __webpack_require__(6760);
495
- // EXTERNAL MODULE: ./src/engine.js + 244 modules
496
- var engine = __webpack_require__(9753);
497
- ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
498
- // Deterministic honesty gates on fix / finding output (#7).
499
- //
500
- // The project's verification discipline (scanner/CLAUDE.md) exists because
501
- // several releases shipped broken or false because work was reported as done
502
- // without confirming the artifact changed. Two of those failure modes are
503
- // *textual* — they live in the prose an agent emits alongside a fix — and can
504
- // be caught deterministically, with no LLM and no network:
713
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
714
+ /* harmony export */ runProjectLinter: () => (/* binding */ runProjectLinter),
715
+ /* harmony export */ verifyFix: () => (/* binding */ verifyFix),
716
+ /* harmony export */ verifyPatch: () => (/* binding */ verifyPatch)
717
+ /* harmony export */ });
718
+ /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(31421);
719
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
720
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
721
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(67198);
722
+ /* harmony import */ var _fix_honesty_gate_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27785);
723
+ /* harmony import */ var _test_runner_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(35136);
724
+ /* harmony import */ var _fix_metrics_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(32238);
725
+ var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_engine_js__WEBPACK_IMPORTED_MODULE_3__]);
726
+ _engine_js__WEBPACK_IMPORTED_MODULE_3__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
727
+ // Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
505
728
  //
506
- // 1. Hand-wave residual-risk prose. "The input is adequately handled",
507
- // "future work", "tbd", "later" — vague assurances that claim safety
508
- // without naming a concrete remaining vector. A residual you can't name
509
- // is a residual you're guessing about; reject the guess.
729
+ // Given a candidate patch (the new file content + the finding stableId being
730
+ // fixed), verify it:
510
731
  //
511
- // 2. An unbacked "this is a false positive / provably safe" verdict. Marking
512
- // a finding safe is a coverage *reduction* it must cite a `file:line`
513
- // that shows why, exactly like the rules-override gate refuses to silently
514
- // shrink coverage.
515
- //
516
- // Plus a conservative fix-tier classifier so a partial remediation can never be
517
- // labelled FULL: any workaround-only signal (rate-limit, docs, log-without-
518
- // reject) is WORKAROUND; anything short of (sink signature changed + all callers
519
- // routed + a discriminating test) is at most MITIGATION; only the full set with
520
- // no partial-sanitization caveat earns FULL.
521
- //
522
- // Pure functions, no side effects, no throwing — safe to call from a command,
523
- // a hook, or the MCP verify_fix path.
524
-
525
- // Vague-assurance phrases that a real residual must never hide behind. Matched
526
- // case-insensitively with word boundaries so "later" doesn't trip on
527
- // "collateral" and "tbd" doesn't trip on a longer token.
528
- const BANNED_RESIDUAL_PHRASES = Object.freeze([
529
- 'adequately handled',
530
- 'adequately handles',
531
- 'properly validated',
532
- 'properly handled',
533
- 'handled properly',
534
- 'handled safely',
535
- 'future work',
536
- 'more work needed',
537
- 'to be done',
538
- 'tbd',
539
- 'later',
540
- ]);
541
-
542
- // A citation shaped like `file:line` — one or more non-space, non-colon chars,
543
- // a colon, then digits. Unanchored: it need only appear somewhere in the item.
544
- const CITATION_RE = /[^\s:]+:\d+/;
545
-
546
- // Verdicts that assert the finding is not real and therefore demand a citation.
547
- // Compared after normalizing separators (`_`/space → `-`) and lowercasing, so
548
- // FALSE_POSITIVE, false-positive, and "provably safe" all land here.
549
- const FP_VERDICTS = Object.freeze(new Set(['false-positive', 'provably-safe', 'safe']));
550
-
551
- function _escapeRe(s) {
552
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
553
- }
554
-
555
- /**
556
- * Reject vague-assurance / hand-wave residual-risk prose.
557
- *
558
- * An empty or whitespace-only residual is ok — there is no residual to lie
559
- * about. A non-empty residual is rejected when it contains any banned phrase;
560
- * each match yields one violation naming the offending phrase.
561
- *
562
- * @param {string} residualText
563
- * @returns {{ ok: boolean, violations: string[] }}
564
- */
565
- function checkResidualHonesty(residualText) {
566
- const text = typeof residualText === 'string' ? residualText : '';
567
- if (text.trim() === '') return { ok: true, violations: [] };
568
-
569
- const violations = [];
570
- for (const phrase of BANNED_RESIDUAL_PHRASES) {
571
- const re = new RegExp(`\\b${_escapeRe(phrase)}\\b`, 'i');
572
- if (re.test(text)) {
573
- violations.push(`vague-assurance phrase: "${phrase}"`);
574
- }
575
- }
576
- return { ok: violations.length === 0, violations };
577
- }
578
-
579
- function _isCitation(item) {
580
- if (typeof item === 'string') return CITATION_RE.test(item);
581
- if (item && typeof item === 'object' && typeof item.location === 'string') {
582
- return CITATION_RE.test(item.location);
583
- }
584
- return false;
585
- }
586
-
587
- function _normalizeVerdict(verdict) {
588
- return String(verdict).trim().toLowerCase().replace(/[_\s]+/g, '-');
589
- }
590
-
591
- /**
592
- * Require a file:line citation behind a "this is not real" verdict.
593
- *
594
- * For a false-positive / provably-safe / safe verdict (case-insensitive; also
595
- * accepts FALSE_POSITIVE), at least one evidence item must be a `file:line`
596
- * citation — either a string matching /[^\s:]+:\d+/ or an object
597
- * `{ location: "file:line" }`. Any other verdict passes unconditionally.
598
- *
599
- * @param {string} verdict
600
- * @param {Array|string|object} evidence
601
- * @returns {{ ok: boolean, violations: string[] }}
602
- */
603
- function requireCitedEvidence(verdict, evidence) {
604
- if (typeof verdict !== 'string' || !FP_VERDICTS.has(_normalizeVerdict(verdict))) {
605
- return { ok: true, violations: [] };
606
- }
607
- const items = Array.isArray(evidence)
608
- ? evidence
609
- : evidence == null
610
- ? []
611
- : [evidence];
612
- if (items.some(_isCitation)) return { ok: true, violations: [] };
613
- return {
614
- ok: false,
615
- violations: ['false-positive/safe verdict requires a file:line citation'],
616
- };
617
- }
618
-
619
- /**
620
- * Classify a fix into FULL | MITIGATION | WORKAROUND, conservative-first.
621
- *
622
- * @param {object} signals
623
- * @param {boolean} signals.sinkSignatureChanged
624
- * @param {boolean} signals.allCallersRouted
625
- * @param {boolean} signals.testDiscriminates - a test that fails pre-fix, passes post-fix
626
- * @param {boolean} [signals.rateLimitOnly]
627
- * @param {boolean} [signals.docsOnly]
628
- * @param {boolean} [signals.logOnlyNoReject]
629
- * @param {boolean} [signals.partialSanitization]
630
- * @returns {'FULL'|'MITIGATION'|'WORKAROUND'}
631
- */
632
- function computeFixTier(signals) {
633
- const s = signals && typeof signals === 'object' ? signals : {};
634
- if (s.rateLimitOnly || s.docsOnly || s.logOnlyNoReject) return 'WORKAROUND';
635
- const complete = s.sinkSignatureChanged && s.allCallersRouted && s.testDiscriminates;
636
- if (s.partialSanitization || !complete) return 'MITIGATION';
637
- return 'FULL';
638
- }
639
-
640
- /**
641
- * FR-308: cross-check a fix-completeness TIER against MECHANICAL evidence,
642
- * when any is available. `signals` (computeFixTier's input) is agent-
643
- * self-reported — this module cannot compute sinkSignatureChanged /
644
- * allCallersRouted / testDiscriminates itself (see the header above: "the
645
- * gate can only run against claims the AGENT self-reports... nothing here
646
- * is server-computable"). `pocLeg` is different: fix-verify.js's PoC leg is
647
- * a REAL execution result (posture/CLAUDE.md's execution-proof tiers), not
648
- * a claim. When it is available and shows the proof-of-concept STILL
649
- * demonstrates the vulnerability against the patch, a self-reported FULL
650
- * tier is not merely internally inconsistent — it is REFUTED by fact. This
651
- * is the literal "a mitigation or workaround cannot be represented as a
652
- * full fix" acceptance criterion, now backed by mechanical evidence where
653
- * it exists rather than by self-report consistency alone.
654
- *
655
- * A `pocLeg` of `not-requested` or `inconclusive` carries no mechanical
656
- * signal either way and is a no-op here — this check can only ever ADD a
657
- * violation on real contrary evidence, never manufacture one from absence.
658
- *
659
- * @param {string} tier
660
- * @param {{status: string, reason?: string}|null} pocLeg
661
- * @returns {{ ok: boolean, violations: string[] }}
662
- */
663
- function checkMechanicalTierEvidence(tier, pocLeg) {
664
- if (!pocLeg || typeof pocLeg !== 'object') return { ok: true, violations: [] };
665
- if (tier === 'FULL' && pocLeg.status === 'still-exploitable') {
666
- return {
667
- ok: false,
668
- violations: [`tier 'FULL' is refuted by mechanical evidence: the proof-of-concept still demonstrates the vulnerability against the patch${pocLeg.reason ? ` (${pocLeg.reason})` : ''}`],
669
- };
670
- }
671
- return { ok: true, violations: [] };
672
- }
673
-
674
- /**
675
- * Compose the four gates for a single fix's output.
676
- *
677
- * ok = residual-honesty ok AND evidence-citation ok AND mechanical-tier-
678
- * evidence ok, further constrained by the tier/residual consistency
679
- * invariant:
680
- * - a FULL tier must NOT carry a residual (a full fix has nothing left);
681
- * - a non-FULL tier MUST document a residual (say what's still open).
682
- *
683
- * @param {{ residual?: string, verdict?: string, evidence?: any, signals?: object }} input
684
- * @param {{ pocLeg?: object|null }} [mechanical] - FR-308: optional real
685
- * execution evidence (fix-verify.js's pocLeg) to cross-check the
686
- * self-reported tier against. Omitted entirely by any caller that has no
687
- * PoC leg to offer — this parameter never REQUIRES mechanical evidence,
688
- * it only USES it when present.
689
- * @returns {{ ok: boolean, tier: string, violations: string[] }}
690
- */
691
- function gateFixOutput({ residual, verdict, evidence, signals } = {}, { pocLeg = null } = {}) {
692
- const tier = computeFixTier(signals);
693
- const residualCheck = checkResidualHonesty(residual);
694
- const evidenceCheck = requireCitedEvidence(verdict, evidence);
695
- const mechanicalCheck = checkMechanicalTierEvidence(tier, pocLeg);
696
-
697
- const violations = [...residualCheck.violations, ...evidenceCheck.violations, ...mechanicalCheck.violations];
698
- let ok = residualCheck.ok && evidenceCheck.ok && mechanicalCheck.ok;
699
-
700
- const residualEmpty = typeof residual !== 'string' || residual.trim() === '';
701
- if (tier === 'FULL' && !residualEmpty) {
702
- violations.push('FULL tier cannot carry a residual');
703
- ok = false;
704
- }
705
- if (tier !== 'FULL' && residualEmpty) {
706
- violations.push('non-FULL tier must document a residual');
707
- ok = false;
708
- }
709
-
710
- return { ok, tier, violations };
711
- }
712
-
713
- const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERDICTS });
714
-
715
- ;// CONCATENATED MODULE: ./src/posture/test-runner.js
716
- // R5 (partial, roadmap) — the project's own test suite as a verification
717
- // stage for `verifyFix()` (see `fix-verify.js`). Closes the gap where a
718
- // "verified" fix only proved a finding's stableId stopped firing — a patch
719
- // that deletes the feature entirely would satisfy that just as well as a
720
- // real fix. Running the project's own tests is the cheapest available check
721
- // that the application still works.
722
- //
723
- // Execution-safety note: this spawns the TARGET PROJECT's own test command
724
- // in the target project's own directory. That is deliberately NOT routed
725
- // through the R1 confinement sandbox (`../sandbox/`). That sandbox exists to
726
- // contain untrusted proof-of-concept exploit code the scanner itself
727
- // synthesizes — code nobody has vetted, being run for the first time. A
728
- // project's pre-existing test suite is the opposite case: it is the
729
- // project's own trusted source, already sitting on disk, and running it is
730
- // exactly what a human developer does by hand before trusting a fix.
731
- // Wrapping "npm test" / "pytest" / "go test" in the PoC sandbox's
732
- // syscall/network/filesystem restrictions would break the large majority of
733
- // real test suites (they bind local ports, spawn child processes, write temp
734
- // fixtures, etc.) for no corresponding security benefit.
735
-
736
-
737
-
738
-
739
-
740
- const DEFAULT_TIMEOUT_MS = 300_000;
741
- const NPM_PLACEHOLDER = /Error: no test specified/i;
742
-
743
- function _exists(scanRoot, rel) {
744
- try { return external_node_fs_.existsSync(external_node_path_.join(scanRoot, rel)); } catch { return false; }
745
- }
746
-
747
- function _isDir(scanRoot, rel) {
748
- try { return external_node_fs_.statSync(external_node_path_.join(scanRoot, rel)).isDirectory(); } catch { return false; }
749
- }
750
-
751
- function _binaryAvailable(cmd) {
752
- try {
753
- const r = (0,external_node_child_process_.spawnSync)(cmd, ['--version'], { timeout: 5_000, stdio: 'ignore' });
754
- return !(r.error && r.error.code === 'ENOENT');
755
- } catch {
756
- return false;
757
- }
758
- }
759
-
760
- // Detect the project's test command. Read-only — never spawns the actual
761
- // test run, only (optionally) a cheap `--version` probe to confirm a tool
762
- // like `pytest` is actually installed before committing to it. Returns
763
- // `null` when nothing detectable is found — most scanned repos will hit
764
- // this path, and that must not be treated as a failure by callers.
765
- function detectTestCommand(scanRoot) {
766
- if (!scanRoot) return null;
767
-
768
- // JS/TS — package.json with a real (non-placeholder) `scripts.test`.
769
- let pkg = null;
770
- try { pkg = JSON.parse(external_node_fs_.readFileSync(external_node_path_.join(scanRoot, 'package.json'), 'utf8')); } catch { pkg = null; }
771
- const testScript = pkg && pkg.scripts && pkg.scripts.test;
772
- if (testScript && !NPM_PLACEHOLDER.test(String(testScript))) {
773
- if (_exists(scanRoot, 'pnpm-lock.yaml')) return { cmd: 'pnpm', args: ['test'], kind: 'pnpm' };
774
- if (_exists(scanRoot, 'yarn.lock')) return { cmd: 'yarn', args: ['test'], kind: 'yarn' };
775
- if (_exists(scanRoot, 'bun.lockb') || _exists(scanRoot, 'bun.lock')) return { cmd: 'bun', args: ['test'], kind: 'bun' };
776
- return { cmd: 'npm', args: ['test', '--silent'], kind: 'npm' };
777
- }
778
-
779
- // Python — pytest.ini / pyproject.toml / tox.ini / a tests/ dir, and the
780
- // `pytest` binary actually available. If pytest isn't installed we do NOT
781
- // report a python test command — falling through lets a later language
782
- // marker (e.g. go.mod in a polyglot repo) still be detected.
783
- const pyMarker = _exists(scanRoot, 'pytest.ini') || _exists(scanRoot, 'pyproject.toml') ||
784
- _exists(scanRoot, 'tox.ini') || _isDir(scanRoot, 'tests');
785
- if (pyMarker && _binaryAvailable('pytest')) {
786
- return { cmd: 'pytest', args: ['-q'], kind: 'pytest' };
787
- }
788
-
789
- // Go
790
- if (_exists(scanRoot, 'go.mod')) {
791
- return { cmd: 'go', args: ['test', './...'], kind: 'go' };
792
- }
793
-
794
- return null;
795
- }
796
-
797
- // Run the detected test command with a walltime budget. Always returns a
798
- // result object — never throws. Distinguishes four outcomes:
799
- // - no detectable/runnable command -> status: 'skipped' (does NOT fail)
800
- // - ran and exited 0 -> status: 'passed'
801
- // - ran and exited non-zero -> status: 'failed'
802
- // - ran past the timeout budget -> status: 'failed', timedOut: true
803
- function runProjectTests(scanRoot, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
804
- const startedAt = Date.now();
805
- const command = detectTestCommand(scanRoot);
806
- if (!command) {
807
- return {
808
- status: 'skipped', passed: null, skipped: true,
809
- reason: 'no-test-command-detected', exitCode: null, timedOut: false,
810
- durationMs: Date.now() - startedAt,
811
- };
812
- }
813
-
814
- let r;
815
- try {
816
- r = (0,external_node_child_process_.spawnSync)(command.cmd, command.args, {
817
- cwd: scanRoot,
818
- encoding: 'utf8',
819
- timeout: timeoutMs,
820
- env: { ...process.env, CI: '1' },
821
- });
822
- } catch (e) {
823
- // The spawn call itself threw (rare — e.g. cwd vanished). Treat as
824
- // "could not run", not "ran and failed".
825
- return {
826
- status: 'skipped', passed: null, skipped: true,
827
- reason: `spawn-error: ${e.message}`, exitCode: null, timedOut: false,
828
- durationMs: Date.now() - startedAt,
829
- };
830
- }
831
- const durationMs = Date.now() - startedAt;
832
-
833
- if (r.error && r.error.code === 'ENOENT') {
834
- // The detected tool isn't actually installed on this machine. Not a
835
- // test failure — the suite never ran.
836
- return {
837
- status: 'skipped', passed: null, skipped: true,
838
- reason: `${command.kind}-not-installed`, exitCode: null, timedOut: false, durationMs,
839
- };
840
- }
841
-
842
- if (r.status === null) {
843
- // spawnSync sets status:null both on timeout-kill and on being killed by
844
- // another signal; either way the run did not complete, which is a
845
- // verification failure, never a skip — we asked for a result and the
846
- // process was terminated before producing one.
847
- return {
848
- status: 'failed', passed: false, skipped: false,
849
- reason: 'timed-out', exitCode: null, timedOut: true, durationMs,
850
- };
851
- }
852
-
853
- return {
854
- status: r.status === 0 ? 'passed' : 'failed',
855
- passed: r.status === 0,
856
- skipped: false,
857
- reason: r.status === 0 ? null : 'test-failures',
858
- exitCode: r.status,
859
- timedOut: false,
860
- durationMs,
861
- };
862
- }
863
-
864
- // EXTERNAL MODULE: ./src/posture/fix-metrics.js
865
- var fix_metrics = __webpack_require__(2238);
866
- ;// CONCATENATED MODULE: ./src/posture/fix-verify.js
867
- // Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
868
- //
869
- // Given a candidate patch (the new file content + the finding stableId being
870
- // fixed), verify it:
871
- //
872
- // 1. The original finding's stableId no longer fires on the patched file.
873
- // 2. No new findings at severity ≥ medium were introduced by the patch.
874
- // 3. The project's existing linter (when present) passes on the patched file.
875
- // 4. The project's own test suite (when detectable) still passes. This is
876
- // the R5 gap-closer: a patch that silently deletes the feature would
877
- // satisfy (1) and (2) just as well as a real fix — only running the
878
- // tests catches that. See `test-runner.js` for detection + execution.
732
+ // 1. The original finding's stableId no longer fires on the patched file.
733
+ // 2. No new findings at severity medium were introduced by the patch.
734
+ // 3. The project's existing linter (when present) passes on the patched file.
735
+ // 4. The project's own test suite (when detectable) still passes. This is
736
+ // the R5 gap-closer: a patch that silently deletes the feature would
737
+ // satisfy (1) and (2) just as well as a real fix only running the
738
+ // tests catches that. See `test-runner.js` for detection + execution.
879
739
  //
880
740
  // If any of those fail, the caller is expected to NOT apply the patch and
881
741
  // instead surface a "fix plan" — a numbered list of steps the engineer can
@@ -912,7 +772,7 @@ async function verifyPatch({
912
772
  // would mass-mark the rest of the project as remediated, then
913
773
  // `reintroduced` on the next real scan. The patched content is also not
914
774
  // committed, so there is no history to resolve provenance against anyway.
915
- scan = await (0,engine/* runFullScan */.wW)({ fileContents, depFileContents, scanRoot, provenance: false }, () => {});
775
+ scan = await (0,_engine_js__WEBPACK_IMPORTED_MODULE_3__/* .runFullScan */ .wW)({ fileContents, depFileContents, scanRoot, provenance: false }, () => {});
916
776
  } catch (e) {
917
777
  return { ok: false, reason: 'rescan-failed', error: e.message };
918
778
  }
@@ -943,7 +803,7 @@ function runProjectLinter(scanRoot, filePaths) {
943
803
  if (!scanRoot || !Array.isArray(filePaths) || filePaths.length === 0) {
944
804
  return { ok: true, runner: 'none' };
945
805
  }
946
- const has = (p) => { try { return external_node_fs_.existsSync(external_node_path_.join(scanRoot, p)); } catch { return false; } };
806
+ const has = (p) => { try { return node_fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, p)); } catch { return false; } };
947
807
  // Pick the linter by config file present in the repo root.
948
808
  const jsFiles = filePaths.filter(f => /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(f));
949
809
  const pyFiles = filePaths.filter(f => /\.py$/i.test(f));
@@ -971,7 +831,7 @@ function runProjectLinter(scanRoot, filePaths) {
971
831
  function runLinter(cwd, cmd, args) {
972
832
  let r;
973
833
  try {
974
- r = (0,external_node_child_process_.spawnSync)(cmd, args, { cwd, encoding: 'utf8', timeout: 60_000 });
834
+ r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)(cmd, args, { cwd, encoding: 'utf8', timeout: 60_000 });
975
835
  } catch (e) {
976
836
  return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing', error: e.message };
977
837
  }
@@ -1023,8 +883,8 @@ function _candidateDiffersFromDisk(scanRoot, files) {
1023
883
  for (const [rel, content] of Object.entries(files)) {
1024
884
  if (typeof content !== 'string') continue;
1025
885
  try {
1026
- const abs = external_node_path_.resolve(scanRoot, rel);
1027
- if (external_node_fs_.readFileSync(abs, 'utf8') !== content) return true;
886
+ const abs = node_path__WEBPACK_IMPORTED_MODULE_2__.resolve(scanRoot, rel);
887
+ if (node_fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(abs, 'utf8') !== content) return true;
1028
888
  } catch {
1029
889
  return true; // candidate file absent on disk -> definitely not applied
1030
890
  }
@@ -1054,7 +914,7 @@ async function verifyFix({
1054
914
  _lap('rescan');
1055
915
  const lint = runProjectLinter(scanRoot, Object.keys(files || {}));
1056
916
  _lap('lint');
1057
- const tests = runProjectTests(scanRoot, testTimeoutMs != null ? { timeoutMs: testTimeoutMs } : {});
917
+ const tests = (0,_test_runner_js__WEBPACK_IMPORTED_MODULE_5__/* .runProjectTests */ .I)(scanRoot, testTimeoutMs != null ? { timeoutMs: testTimeoutMs } : {});
1058
918
  _lap('tests');
1059
919
  // True when a candidate patch was supplied but has not been written, so the
1060
920
  // suite necessarily ran against the pre-patch tree. Surfaced in the summary
@@ -1119,7 +979,7 @@ async function verifyFix({
1119
979
  fixMeta.evidence !== undefined || fixMeta.signals !== undefined);
1120
980
  let honesty = null;
1121
981
  if (hasHonestyClaim) {
1122
- try { honesty = gateFixOutput(fixMeta, { pocLeg }); } catch { honesty = null; }
982
+ try { honesty = (0,_fix_honesty_gate_js__WEBPACK_IMPORTED_MODULE_4__/* .gateFixOutput */ ._u)(fixMeta, { pocLeg }); } catch { honesty = null; }
1123
983
  }
1124
984
  _lap('honesty');
1125
985
 
@@ -1174,7 +1034,7 @@ async function verifyFix({
1174
1034
  // suite ran against the pre-patch tree, so its timing is real while its
1175
1035
  // verdict is about a different tree.
1176
1036
  if (recordMetrics && scanRoot) {
1177
- (0,fix_metrics/* recordFixAttempt */.I3)(scanRoot, {
1037
+ (0,_fix_metrics_js__WEBPACK_IMPORTED_MODULE_6__/* .recordFixAttempt */ .I3)(scanRoot, {
1178
1038
  at: new Date().toISOString(),
1179
1039
  stableId: originalFindingStableId || null,
1180
1040
  ok,
@@ -1194,6 +1054,169 @@ async function verifyFix({
1194
1054
  return { ok, verifiedFull, degradedLegs, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary };
1195
1055
  }
1196
1056
 
1057
+ __webpack_async_result__();
1058
+ } catch(e) { __webpack_async_result__(e); } });
1059
+
1060
+ /***/ }),
1061
+
1062
+ /***/ 35136:
1063
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1064
+
1065
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1066
+ /* harmony export */ I: () => (/* binding */ runProjectTests)
1067
+ /* harmony export */ });
1068
+ /* unused harmony export detectTestCommand */
1069
+ /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(31421);
1070
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
1071
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
1072
+ // R5 (partial, roadmap) — the project's own test suite as a verification
1073
+ // stage for `verifyFix()` (see `fix-verify.js`). Closes the gap where a
1074
+ // "verified" fix only proved a finding's stableId stopped firing — a patch
1075
+ // that deletes the feature entirely would satisfy that just as well as a
1076
+ // real fix. Running the project's own tests is the cheapest available check
1077
+ // that the application still works.
1078
+ //
1079
+ // Execution-safety note: this spawns the TARGET PROJECT's own test command
1080
+ // in the target project's own directory. That is deliberately NOT routed
1081
+ // through the R1 confinement sandbox (`../sandbox/`). That sandbox exists to
1082
+ // contain untrusted proof-of-concept exploit code the scanner itself
1083
+ // synthesizes — code nobody has vetted, being run for the first time. A
1084
+ // project's pre-existing test suite is the opposite case: it is the
1085
+ // project's own trusted source, already sitting on disk, and running it is
1086
+ // exactly what a human developer does by hand before trusting a fix.
1087
+ // Wrapping "npm test" / "pytest" / "go test" in the PoC sandbox's
1088
+ // syscall/network/filesystem restrictions would break the large majority of
1089
+ // real test suites (they bind local ports, spawn child processes, write temp
1090
+ // fixtures, etc.) for no corresponding security benefit.
1091
+
1092
+
1093
+
1094
+
1095
+
1096
+ const DEFAULT_TIMEOUT_MS = 300_000;
1097
+ const NPM_PLACEHOLDER = /Error: no test specified/i;
1098
+
1099
+ function _exists(scanRoot, rel) {
1100
+ try { return node_fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel)); } catch { return false; }
1101
+ }
1102
+
1103
+ function _isDir(scanRoot, rel) {
1104
+ try { return node_fs__WEBPACK_IMPORTED_MODULE_1__.statSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel)).isDirectory(); } catch { return false; }
1105
+ }
1106
+
1107
+ function _binaryAvailable(cmd) {
1108
+ try {
1109
+ const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)(cmd, ['--version'], { timeout: 5_000, stdio: 'ignore' });
1110
+ return !(r.error && r.error.code === 'ENOENT');
1111
+ } catch {
1112
+ return false;
1113
+ }
1114
+ }
1115
+
1116
+ // Detect the project's test command. Read-only — never spawns the actual
1117
+ // test run, only (optionally) a cheap `--version` probe to confirm a tool
1118
+ // like `pytest` is actually installed before committing to it. Returns
1119
+ // `null` when nothing detectable is found — most scanned repos will hit
1120
+ // this path, and that must not be treated as a failure by callers.
1121
+ function detectTestCommand(scanRoot) {
1122
+ if (!scanRoot) return null;
1123
+
1124
+ // JS/TS — package.json with a real (non-placeholder) `scripts.test`.
1125
+ let pkg = null;
1126
+ 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; }
1127
+ const testScript = pkg && pkg.scripts && pkg.scripts.test;
1128
+ if (testScript && !NPM_PLACEHOLDER.test(String(testScript))) {
1129
+ if (_exists(scanRoot, 'pnpm-lock.yaml')) return { cmd: 'pnpm', args: ['test'], kind: 'pnpm' };
1130
+ if (_exists(scanRoot, 'yarn.lock')) return { cmd: 'yarn', args: ['test'], kind: 'yarn' };
1131
+ if (_exists(scanRoot, 'bun.lockb') || _exists(scanRoot, 'bun.lock')) return { cmd: 'bun', args: ['test'], kind: 'bun' };
1132
+ return { cmd: 'npm', args: ['test', '--silent'], kind: 'npm' };
1133
+ }
1134
+
1135
+ // Python — pytest.ini / pyproject.toml / tox.ini / a tests/ dir, and the
1136
+ // `pytest` binary actually available. If pytest isn't installed we do NOT
1137
+ // report a python test command — falling through lets a later language
1138
+ // marker (e.g. go.mod in a polyglot repo) still be detected.
1139
+ const pyMarker = _exists(scanRoot, 'pytest.ini') || _exists(scanRoot, 'pyproject.toml') ||
1140
+ _exists(scanRoot, 'tox.ini') || _isDir(scanRoot, 'tests');
1141
+ if (pyMarker && _binaryAvailable('pytest')) {
1142
+ return { cmd: 'pytest', args: ['-q'], kind: 'pytest' };
1143
+ }
1144
+
1145
+ // Go
1146
+ if (_exists(scanRoot, 'go.mod')) {
1147
+ return { cmd: 'go', args: ['test', './...'], kind: 'go' };
1148
+ }
1149
+
1150
+ return null;
1151
+ }
1152
+
1153
+ // Run the detected test command with a walltime budget. Always returns a
1154
+ // result object — never throws. Distinguishes four outcomes:
1155
+ // - no detectable/runnable command -> status: 'skipped' (does NOT fail)
1156
+ // - ran and exited 0 -> status: 'passed'
1157
+ // - ran and exited non-zero -> status: 'failed'
1158
+ // - ran past the timeout budget -> status: 'failed', timedOut: true
1159
+ function runProjectTests(scanRoot, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
1160
+ const startedAt = Date.now();
1161
+ const command = detectTestCommand(scanRoot);
1162
+ if (!command) {
1163
+ return {
1164
+ status: 'skipped', passed: null, skipped: true,
1165
+ reason: 'no-test-command-detected', exitCode: null, timedOut: false,
1166
+ durationMs: Date.now() - startedAt,
1167
+ };
1168
+ }
1169
+
1170
+ let r;
1171
+ try {
1172
+ r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)(command.cmd, command.args, {
1173
+ cwd: scanRoot,
1174
+ encoding: 'utf8',
1175
+ timeout: timeoutMs,
1176
+ env: { ...process.env, CI: '1' },
1177
+ });
1178
+ } catch (e) {
1179
+ // The spawn call itself threw (rare — e.g. cwd vanished). Treat as
1180
+ // "could not run", not "ran and failed".
1181
+ return {
1182
+ status: 'skipped', passed: null, skipped: true,
1183
+ reason: `spawn-error: ${e.message}`, exitCode: null, timedOut: false,
1184
+ durationMs: Date.now() - startedAt,
1185
+ };
1186
+ }
1187
+ const durationMs = Date.now() - startedAt;
1188
+
1189
+ if (r.error && r.error.code === 'ENOENT') {
1190
+ // The detected tool isn't actually installed on this machine. Not a
1191
+ // test failure — the suite never ran.
1192
+ return {
1193
+ status: 'skipped', passed: null, skipped: true,
1194
+ reason: `${command.kind}-not-installed`, exitCode: null, timedOut: false, durationMs,
1195
+ };
1196
+ }
1197
+
1198
+ if (r.status === null) {
1199
+ // spawnSync sets status:null both on timeout-kill and on being killed by
1200
+ // another signal; either way the run did not complete, which is a
1201
+ // verification failure, never a skip — we asked for a result and the
1202
+ // process was terminated before producing one.
1203
+ return {
1204
+ status: 'failed', passed: false, skipped: false,
1205
+ reason: 'timed-out', exitCode: null, timedOut: true, durationMs,
1206
+ };
1207
+ }
1208
+
1209
+ return {
1210
+ status: r.status === 0 ? 'passed' : 'failed',
1211
+ passed: r.status === 0,
1212
+ skipped: false,
1213
+ reason: r.status === 0 ? null : 'test-failures',
1214
+ exitCode: r.status,
1215
+ timedOut: false,
1216
+ durationMs,
1217
+ };
1218
+ }
1219
+
1197
1220
 
1198
1221
  /***/ })
1199
1222