@clear-capabilities/agentic-security-scanner 0.142.0 → 0.144.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/bin/agentic-security.js +530 -54
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +108 -17
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +1 -1
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/384.index.js +1 -1
  10. package/dist/435.index.js +165 -52
  11. package/dist/526.index.js +108 -17
  12. package/dist/552.index.js +97 -0
  13. package/dist/637.index.js +1 -1
  14. package/dist/730.index.js +311 -0
  15. package/dist/736.index.js +301 -0
  16. package/dist/824.index.js +7 -0
  17. package/dist/905.index.js +88 -22
  18. package/dist/920.index.js +491 -0
  19. package/dist/970.index.js +109 -0
  20. package/dist/agentic-security.mjs +13 -13
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/dist/calibration-seed.json +2 -0
  23. package/package.json +19 -11
  24. package/src/dataflow/index.js +18 -0
  25. package/src/dataflow/privacy-catalog.js +290 -0
  26. package/src/dataflow/privacy-deep-walker.js +515 -0
  27. package/src/dataflow/privacy-governance.js +126 -0
  28. package/src/dataflow/privacy-inventory.js +154 -0
  29. package/src/dataflow/privacy-sink-policy.js +125 -0
  30. package/src/dataflow/privacy-taint.js +115 -54
  31. package/src/dataflow/privacy-taxonomy.js +233 -0
  32. package/src/discovery/disprove.js +7 -3
  33. package/src/discovery/hunter.js +9 -5
  34. package/src/discovery/index.js +2 -2
  35. package/src/discovery/llm-invoke.js +69 -13
  36. package/src/egress/audit.js +147 -0
  37. package/src/egress/policy.js +313 -0
  38. package/src/egress/redact.js +180 -0
  39. package/src/engine.js +575 -288
  40. package/src/fix/apply-fix-service.js +403 -0
  41. package/src/fix/approver-registry.js +157 -0
  42. package/src/llm-validator/index.js +86 -9
  43. package/src/llm-validator/model-status.js +66 -0
  44. package/src/mcp/tools.js +157 -50
  45. package/src/pipeline/analyzer-supervisor.js +93 -0
  46. package/src/pipeline/analyzer-worker.js +26 -0
  47. package/src/pipeline/annotator-runner.js +33 -0
  48. package/src/pipeline/assurance-mode.js +91 -0
  49. package/src/pipeline/cascade-worker-pool.js +172 -0
  50. package/src/pipeline/cascade-worker.js +43 -0
  51. package/src/pipeline/coverage-ledger.js +0 -0
  52. package/src/pipeline/detector-runner.js +51 -0
  53. package/src/pipeline/enrichment-completion.js +58 -0
  54. package/src/pipeline/evidence-provenance.js +91 -0
  55. package/src/pipeline/finding-schema.js +101 -0
  56. package/src/pipeline/legacy-compat.js +101 -0
  57. package/src/pipeline/producer-collector.js +48 -0
  58. package/src/pipeline/producer-registry.js +112 -0
  59. package/src/pipeline/scan-health.js +144 -0
  60. package/src/posture/CLAUDE.md +2 -0
  61. package/src/posture/accuracy-scorecard.js +96 -1
  62. package/src/posture/adversary-agent.js +15 -3
  63. package/src/posture/artifact-registry.js +217 -0
  64. package/src/posture/auditor-walkthrough.js +70 -8
  65. package/src/posture/calibration-feedback.js +201 -0
  66. package/src/posture/calibration-seed.json +2 -0
  67. package/src/posture/calibration.js +25 -0
  68. package/src/posture/compliance-evidence-signing.js +131 -0
  69. package/src/posture/compliance-policy.js +314 -17
  70. package/src/posture/custom-rules.js +36 -0
  71. package/src/posture/deterministic.js +8 -1
  72. package/src/posture/encryption-provider.js +205 -0
  73. package/src/posture/evidence-grade-wording.js +71 -0
  74. package/src/posture/fix-history.js +113 -19
  75. package/src/posture/fix-honesty-gate.js +47 -6
  76. package/src/posture/fix-verify.js +56 -7
  77. package/src/posture/fleet.js +0 -0
  78. package/src/posture/flow-narration.js +7 -2
  79. package/src/posture/legal-hold.js +140 -0
  80. package/src/posture/llm-redteam.js +10 -1
  81. package/src/posture/material-change.js +90 -0
  82. package/src/posture/policy-bundle.js +274 -0
  83. package/src/posture/privacy-framework.js +33 -6
  84. package/src/posture/production-feedback.js +179 -0
  85. package/src/posture/retention-policy.js +132 -0
  86. package/src/posture/risk-dollars.js +216 -26
  87. package/src/posture/scan-checkpoint.js +176 -31
  88. package/src/posture/state-dir.js +36 -1
  89. package/src/posture/state-lifecycle-report.js +77 -0
  90. package/src/posture/suppressions.js +59 -3
  91. package/src/privacy/ir-adapter.js +380 -0
  92. package/src/report/index.js +83 -18
  93. package/src/report/oscal.js +635 -0
  94. package/src/sast/cpp.js +3 -14
  95. package/src/sca/llm-function-extract.js +6 -0
@@ -0,0 +1,223 @@
1
+ export const id = 1;
2
+ export const ids = [1];
3
+ export const modules = {
4
+
5
+ /***/ 3001:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ OUTCOMES: () => (/* binding */ OUTCOMES),
10
+ /* harmony export */ buildCalibrationReport: () => (/* binding */ buildCalibrationReport),
11
+ /* harmony export */ recordCalibrationFeedback: () => (/* binding */ recordCalibrationFeedback),
12
+ /* harmony export */ renderCalibrationReportSummary: () => (/* binding */ renderCalibrationReportSummary)
13
+ /* harmony export */ });
14
+ /* unused harmony exports CALIBRATION_FEEDBACK_FILE, loadCalibrationFeedback, _internals */
15
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
16
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7598);
17
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
18
+ // FR-806 (assurance-hardening PRD): "Validate model calibration against
19
+ // accepted and realized incidents where customers opt in | Calibration
20
+ // reports are aggregated and privacy-preserving."
21
+ //
22
+ // SCOPE, stated explicitly because the PRD's own wording is terse: this
23
+ // codebase has no SaaS control plane and takes no runtime cloud calls
24
+ // (root CLAUDE.md's own "No runtime cloud calls" rule, and E10's own goal
25
+ // of "consistent policy... without requiring a SaaS control plane") — so
26
+ // "aggregated" here means aggregated WITHIN one installation, across every
27
+ // feedback event an operator has recorded over time, never aggregated
28
+ // ACROSS installations on some central server. An operator who wants a
29
+ // cross-organization rollup can feed this module's own report output into
30
+ // their own aggregation, the same way `fleet.js` composes many single-repo
31
+ // scans without a hosted backend.
32
+ //
33
+ // TWO OUTCOMES, matching the PRD's own two named cases:
34
+ // 'accepted-risk' — an operator/customer reviewed a finding, accepted
35
+ // the risk, and (later, of their own accord) reports
36
+ // that no incident occurred. A well-calibrated model
37
+ // should have predicted LOW confidence/risk for
38
+ // these.
39
+ // 'realized-incident' — an operator reports that a finding's
40
+ // vulnerability WAS actually exploited or otherwise
41
+ // caused a real incident. A well-calibrated model
42
+ // should have predicted HIGH confidence/risk for
43
+ // these — a realized incident on a LOW-predicted
44
+ // finding is exactly the miscalibration this
45
+ // requirement exists to surface.
46
+ //
47
+ // PRIVACY-PRESERVING AT THE SOURCE, not just at the report layer: a
48
+ // feedback record snapshots only the model's OWN prediction signals
49
+ // (confidence, severity, riskDollars.ev) plus the operator's outcome and
50
+ // optional free-text note — never file path, line, vuln title, or code
51
+ // snippet. This mirrors this codebase's existing privacy modules' own
52
+ // discipline (dataflow/privacy-*.js) of never persisting more than a
53
+ // report needs to answer its one question.
54
+ //
55
+ // OPT-IN, genuinely: nothing here is ever auto-populated by a scan. A
56
+ // record exists only when an operator explicitly calls
57
+ // `recordCalibrationFeedback` (via the CLI's `calibration-feedback record`
58
+ // command) — the file simply does not exist for every project that never
59
+ // opts in, and every read degrades to "no data" rather than throwing.
60
+
61
+
62
+
63
+
64
+
65
+ const CALIBRATION_FEEDBACK_FILE = 'calibration-feedback.jsonl';
66
+ const OUTCOMES = Object.freeze(['accepted-risk', 'realized-incident']);
67
+
68
+ // Below this many samples, a rate is an artifact of the sample, not a
69
+ // property of the model — same precedent as fix-metrics.js's RELIABLE_N.
70
+ const RELIABLE_N = 10;
71
+
72
+ function _findFinding(scanRoot, findingId) {
73
+ try {
74
+ const raw = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync((0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, 'last-scan.json'), 'utf8'));
75
+ const findings = Array.isArray(raw.findings) ? raw.findings : [];
76
+ return findings.find(f => f && (f.id === findingId || f.stableId === findingId)) || null;
77
+ } catch { return null; }
78
+ }
79
+
80
+ // FR-806 privacy fix: the caller-supplied findingId is routinely a finding's
81
+ // plain `.id` (e.g. "client-side:DANGEROUS_INNERHTML:src/billing/secret.js:142"),
82
+ // which embeds the exact file path and line this module's own docstring
83
+ // promises never to persist. Never write the caller's raw string to disk:
84
+ // prefer the matched finding's own privacy-safe `.stableId` (a hash, by
85
+ // construction elsewhere in this codebase), and otherwise hash the input
86
+ // ourselves so a stale/removed finding's id can never leak path/line either.
87
+ function _privacySafeFindingId(findingId, finding) {
88
+ if (finding && typeof finding.stableId === 'string' && finding.stableId) return finding.stableId;
89
+ return node_crypto__WEBPACK_IMPORTED_MODULE_1__.createHash('sha256').update(findingId).digest('hex').slice(0, 16);
90
+ }
91
+
92
+ /**
93
+ * Record one opt-in calibration-feedback event. Snapshots ONLY the
94
+ * model's own prediction signals for the named finding (if it is still
95
+ * present in the last scan — a finding fixed/removed since is still
96
+ * recordable, just without a fresh snapshot) plus the outcome and an
97
+ * optional note. Never throws; returns {ok, record} or {ok:false, reason}.
98
+ */
99
+ function recordCalibrationFeedback(scanRoot, { findingId, outcome, note } = {}) {
100
+ if (!findingId || typeof findingId !== 'string') return { ok: false, reason: '--finding-id is required' };
101
+ if (!OUTCOMES.includes(outcome)) return { ok: false, reason: `--outcome must be one of: ${OUTCOMES.join(', ')}` };
102
+ const finding = _findFinding(scanRoot, findingId);
103
+ const record = {
104
+ at: new Date().toISOString(),
105
+ findingId: _privacySafeFindingId(findingId, finding),
106
+ outcome,
107
+ predictedConfidence: finding && typeof finding.confidence === 'number' ? finding.confidence : null,
108
+ predictedConfidenceTier: finding?.confidenceTier || null,
109
+ predictedSeverity: finding?.severity || null,
110
+ predictedRiskEv: finding?.riskDollars && typeof finding.riskDollars.ev === 'number' ? finding.riskDollars.ev : null,
111
+ note: note ? String(note).slice(0, 280) : null,
112
+ };
113
+ // Append-only, same primitives as fix-metrics.js/triage-memory.js's own
114
+ // JSONL writers — a single fs.appendFileSync, never a read-modify-write
115
+ // of the whole file (which would also reintroduce a TOCTOU between an
116
+ // existence check and the write, the exact anti-pattern this codebase's
117
+ // own conventions forbid).
118
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
119
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return { ok: false, reason: 'no safe state directory' };
120
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return { ok: false, reason: 'state writes are disabled (--no-state)' };
121
+ try {
122
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
123
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.appendFileSync((0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, CALIBRATION_FEEDBACK_FILE), JSON.stringify(record) + '\n', 'utf8');
124
+ } catch (e) { return { ok: false, reason: e.message }; }
125
+ return { ok: true, record };
126
+ }
127
+
128
+ /**
129
+ * Every well-formed feedback event ever recorded. A line that fails to
130
+ * parse or lacks a valid outcome is skipped, not fatal.
131
+ */
132
+ function loadCalibrationFeedback(scanRoot) {
133
+ let fp;
134
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, CALIBRATION_FEEDBACK_FILE); } catch { return []; }
135
+ let raw;
136
+ try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return []; }
137
+ const out = [];
138
+ for (const line of raw.split('\n')) {
139
+ if (!line.trim()) continue;
140
+ try {
141
+ const rec = JSON.parse(line);
142
+ if (rec && OUTCOMES.includes(rec.outcome)) out.push(rec);
143
+ } catch { /* torn or hand-edited line — drop it, keep the rest */ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ function _avg(nums) {
149
+ const v = nums.filter(n => typeof n === 'number' && Number.isFinite(n));
150
+ return v.length ? v.reduce((a, b) => a + b, 0) / v.length : null;
151
+ }
152
+
153
+ function _summarizeOutcome(records) {
154
+ const withConfidence = records.map(r => r.predictedConfidence).filter(c => typeof c === 'number');
155
+ return {
156
+ n: records.length,
157
+ reliable: records.length >= RELIABLE_N,
158
+ avgPredictedConfidence: _avg(withConfidence),
159
+ withoutPrediction: records.length - withConfidence.length,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Aggregate ALL recorded feedback (this installation only — see the
165
+ * module header for why cross-installation aggregation is out of scope)
166
+ * into a privacy-preserving report: rates and averages only, never a
167
+ * per-finding breakdown, never file/line/vuln text (none of that was ever
168
+ * stored in the first place).
169
+ */
170
+ function buildCalibrationReport(scanRoot) {
171
+ const records = loadCalibrationFeedback(scanRoot);
172
+ const acceptedRisk = records.filter(r => r.outcome === 'accepted-risk');
173
+ const realizedIncident = records.filter(r => r.outcome === 'realized-incident');
174
+ return {
175
+ schema: 'agentic-security/calibration-report@1',
176
+ generatedAt: new Date().toISOString(),
177
+ totalEvents: records.length,
178
+ acceptedRisk: _summarizeOutcome(acceptedRisk),
179
+ realizedIncident: _summarizeOutcome(realizedIncident),
180
+ // The calibration question itself: accepted-risk events SHOULD skew
181
+ // toward low predicted confidence; realized-incident events SHOULD
182
+ // skew toward high. This flag is a coarse, disclosed-uncertainty
183
+ // signal, not a verdict — it only ever fires when BOTH buckets have
184
+ // enough samples to say anything at all (RELIABLE_N each).
185
+ possibleMiscalibration: (() => {
186
+ const a = _summarizeOutcome(acceptedRisk);
187
+ const r = _summarizeOutcome(realizedIncident);
188
+ if (!a.reliable || !r.reliable || a.avgPredictedConfidence == null || r.avgPredictedConfidence == null) return null;
189
+ return r.avgPredictedConfidence <= a.avgPredictedConfidence;
190
+ })(),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * One block of human-readable summary, or null when nothing has ever been
196
+ * recorded — genuinely opt-in, so "nothing recorded" is the expected
197
+ * default state for almost every project, not an error.
198
+ */
199
+ function renderCalibrationReportSummary(report) {
200
+ if (!report || report.totalEvents === 0) return null;
201
+ const lines = [
202
+ 'Calibration feedback (this installation, opt-in):',
203
+ ` accepted-risk: n=${report.acceptedRisk.n}${report.acceptedRisk.reliable ? '' : ' (below reliable sample size)'}` +
204
+ (report.acceptedRisk.avgPredictedConfidence != null ? ` avg predicted confidence=${report.acceptedRisk.avgPredictedConfidence.toFixed(2)}` : ''),
205
+ ` realized-incident: n=${report.realizedIncident.n}${report.realizedIncident.reliable ? '' : ' (below reliable sample size)'}` +
206
+ (report.realizedIncident.avgPredictedConfidence != null ? ` avg predicted confidence=${report.realizedIncident.avgPredictedConfidence.toFixed(2)}` : ''),
207
+ ];
208
+ if (report.possibleMiscalibration === true) {
209
+ lines.push(' ⚠ realized incidents were NOT predicted with higher confidence than accepted risks — possible miscalibration.');
210
+ } else if (report.possibleMiscalibration === false) {
211
+ lines.push(' ✓ realized incidents were predicted with higher confidence than accepted risks, as expected.');
212
+ } else {
213
+ lines.push(' (not enough samples in both buckets yet to assess calibration — this is not a pass or fail.)');
214
+ }
215
+ return lines.join('\n');
216
+ }
217
+
218
+ const _internals = { RELIABLE_N, _findFinding };
219
+
220
+
221
+ /***/ })
222
+
223
+ };
package/dist/113.index.js CHANGED
@@ -8,9 +8,10 @@ export const modules = {
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
9
  /* harmony export */ I3: () => (/* binding */ recordFixAttempt),
10
10
  /* harmony export */ fixDurationReport: () => (/* binding */ fixDurationReport),
11
- /* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary)
11
+ /* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary),
12
+ /* harmony export */ sU: () => (/* binding */ loadFixAttempts)
12
13
  /* harmony export */ });
13
- /* unused harmony exports FIX_STAGES, loadFixAttempts, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
14
+ /* unused harmony exports FIX_STAGES, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
14
15
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
15
16
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
16
17
  /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
@@ -491,8 +492,8 @@ var external_node_child_process_ = __webpack_require__(1421);
491
492
  var external_node_fs_ = __webpack_require__(3024);
492
493
  // EXTERNAL MODULE: external "node:path"
493
494
  var external_node_path_ = __webpack_require__(6760);
494
- // EXTERNAL MODULE: ./src/engine.js + 200 modules
495
- var engine = __webpack_require__(3474);
495
+ // EXTERNAL MODULE: ./src/engine.js + 211 modules
496
+ var engine = __webpack_require__(7087);
496
497
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
497
498
  // Deterministic honesty gates on fix / finding output (#7).
498
499
  //
@@ -637,23 +638,64 @@ function computeFixTier(signals) {
637
638
  }
638
639
 
639
640
  /**
640
- * Compose the three gates for a single fix's output.
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.
641
654
  *
642
- * ok = residual-honesty ok AND evidence-citation ok, further constrained by the
643
- * tier/residual consistency invariant:
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:
644
680
  * - a FULL tier must NOT carry a residual (a full fix has nothing left);
645
681
  * - a non-FULL tier MUST document a residual (say what's still open).
646
682
  *
647
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.
648
689
  * @returns {{ ok: boolean, tier: string, violations: string[] }}
649
690
  */
650
- function gateFixOutput({ residual, verdict, evidence, signals } = {}) {
691
+ function gateFixOutput({ residual, verdict, evidence, signals } = {}, { pocLeg = null } = {}) {
651
692
  const tier = computeFixTier(signals);
652
693
  const residualCheck = checkResidualHonesty(residual);
653
694
  const evidenceCheck = requireCitedEvidence(verdict, evidence);
695
+ const mechanicalCheck = checkMechanicalTierEvidence(tier, pocLeg);
654
696
 
655
- const violations = [...residualCheck.violations, ...evidenceCheck.violations];
656
- let ok = residualCheck.ok && evidenceCheck.ok;
697
+ const violations = [...residualCheck.violations, ...evidenceCheck.violations, ...mechanicalCheck.violations];
698
+ let ok = residualCheck.ok && evidenceCheck.ok && mechanicalCheck.ok;
657
699
 
658
700
  const residualEmpty = typeof residual !== 'string' || residual.trim() === '';
659
701
  if (tier === 'FULL' && !residualEmpty) {
@@ -1011,12 +1053,6 @@ async function verifyFix({
1011
1053
  // and on the result so a caller cannot mistake it for a verified patch.
1012
1054
  const _testedPrePatch = !tests.skipped && _candidateDiffersFromDisk(scanRoot, files);
1013
1055
  const testsOk = tests.skipped ? true : tests.passed === true;
1014
- let honesty = null;
1015
- if (fixMeta && typeof fixMeta === 'object') {
1016
- try { honesty = gateFixOutput(fixMeta); } catch { honesty = null; }
1017
- }
1018
- _lap('honesty');
1019
-
1020
1056
  // R5 — the PoC leg. Re-run the finding's proof-of-concept against the
1021
1057
  // CANDIDATE patch inside R1's sandbox. A patch that still lets the PoC
1022
1058
  // demonstrate the predicted effect has not fixed anything, however green the
@@ -1028,6 +1064,11 @@ async function verifyFix({
1028
1064
  // to run, or a sandbox that could not start, is recorded as `inconclusive`
1029
1065
  // and left out of the verdict entirely. Treating "could not prove it" as
1030
1066
  // "fixed" is exactly the false confidence this leg exists to prevent.
1067
+ //
1068
+ // Computed BEFORE the honesty gate (FR-308): a still-exploitable PoC is
1069
+ // MECHANICAL evidence, not a self-report, and gateFixOutput cross-checks a
1070
+ // self-reported FULL tier against it below — the ordering matters, not
1071
+ // just the value.
1031
1072
  let pocLeg = { status: 'not-requested', reason: null, tier: null };
1032
1073
  if (poc?.code) {
1033
1074
  try {
@@ -1046,7 +1087,54 @@ async function verifyFix({
1046
1087
  _lap('poc');
1047
1088
  const pocOk = pocLeg.status !== 'still-exploitable';
1048
1089
 
1090
+ // FR-308: "a mitigation or workaround cannot be represented as a full
1091
+ // fix" — gateFixOutput's tier/residual check is a pure self-consistency
1092
+ // check (fixMeta.signals is agent-self-reported; see fix-honesty-gate.js's
1093
+ // header for why nothing there is server-computable). pocLeg IS
1094
+ // server-computable — a real execution result, not a claim — so it is
1095
+ // passed through as the one MECHANICAL cross-check available: a
1096
+ // self-reported FULL tier is refuted, not just internally inconsistent,
1097
+ // when the PoC still demonstrates the vulnerability against the patch.
1098
+ // D-0024: `fixMeta` is a shared envelope — FR-307/FR-1002's `approval`
1099
+ // key lives alongside FR-308's completeness self-report (`residual`/
1100
+ // `verdict`/`evidence`/`signals`). Gating on mere object-truthiness meant
1101
+ // a caller supplying ONLY `approval` (a real, common shape once the CLI's
1102
+ // --approved-by flag and the MCP schema fix made that reachable) got
1103
+ // silently gated on an UNRELATED FR-308 self-consistency check it never
1104
+ // engaged with — computeFixTier(undefined) defaults to MITIGATION, which
1105
+ // then demands a `residual` nobody was ever asked to supply, blocking an
1106
+ // otherwise-genuine, approved fix for a reason that has nothing to do
1107
+ // with completeness honesty. Scope the gate to fixMeta shapes that
1108
+ // actually make a completeness-adjacent claim.
1109
+ const hasHonestyClaim = fixMeta && typeof fixMeta === 'object' &&
1110
+ (fixMeta.residual !== undefined || fixMeta.verdict !== undefined ||
1111
+ fixMeta.evidence !== undefined || fixMeta.signals !== undefined);
1112
+ let honesty = null;
1113
+ if (hasHonestyClaim) {
1114
+ try { honesty = gateFixOutput(fixMeta, { pocLeg }); } catch { honesty = null; }
1115
+ }
1116
+ _lap('honesty');
1117
+
1049
1118
  const ok = rescan.ok && (lint.ok || lint.skipped) && testsOk && pocOk && (honesty ? honesty.ok : true);
1119
+
1120
+ // FR-305 (assurance-hardening PRD): `ok` alone conflates "every leg
1121
+ // genuinely ran and passed" with "passed, but a required leg was skipped
1122
+ // or unavailable" — `lint.ok`/`testsOk` are both true in the skipped case
1123
+ // by design (this codebase does not fail-closed just because a repo has
1124
+ // no linter or no detected test runner), so a caller checking only `ok`
1125
+ // cannot tell the difference. `lint.skipped` is only ever true when a
1126
+ // linter WAS configured but its binary could not be run (missing config
1127
+ // entirely returns `runner: 'none'` with no `skipped` field at all — that
1128
+ // is a genuine N/A, nothing was required, not a degradation). `verifiedFull`
1129
+ // is the honest label: true only when nothing that WAS required was
1130
+ // skipped. A caller must never present `ok: true, verifiedFull: false` as
1131
+ // "fully verified" — `degradedLegs` names exactly what was skipped so a
1132
+ // report can say so plainly instead of a bare pass.
1133
+ const degradedLegs = [];
1134
+ if (lint.skipped) degradedLegs.push(`lint: ${lint.runner} not installed`);
1135
+ if (tests.skipped) degradedLegs.push(`tests: skipped (${tests.reason})`);
1136
+ const verifiedFull = ok && degradedLegs.length === 0;
1137
+
1050
1138
  const durations = { ...stages, totalMs: Date.now() - t0 };
1051
1139
  const summary = [
1052
1140
  `re-scan: ${rescan.ok ? 'PASS' : 'FAIL — ' + rescan.reason}`,
@@ -1068,6 +1156,8 @@ async function verifyFix({
1068
1156
  : pocLeg.status === 'still-exploitable' ? `poc: FAIL — the proof-of-concept still demonstrates the vulnerability against the patch`
1069
1157
  : pocLeg.status === 'no-longer-proven' ? 'poc: PASS (ran against the patch and no longer demonstrates the vulnerability)'
1070
1158
  : `poc: inconclusive — not counted either way (${pocLeg.reason || 'no detail reported'})`,
1159
+ // FR-305: never let a degraded pass read the same as a full one.
1160
+ ok && !verifiedFull ? `NOTE: PASSED, but NOT fully verified — ${degradedLegs.join('; ')}` : null,
1071
1161
  ].filter(Boolean).join('\n');
1072
1162
  // Persist the attempt so the distribution can be reported from real runs.
1073
1163
  // `testsRan` is the load-bearing field: it is what keeps "verified with no
@@ -1080,6 +1170,7 @@ async function verifyFix({
1080
1170
  at: new Date().toISOString(),
1081
1171
  stableId: originalFindingStableId || null,
1082
1172
  ok,
1173
+ verifiedFull,
1083
1174
  testsRan: !tests.skipped,
1084
1175
  testsPassed: tests.skipped ? null : tests.passed === true,
1085
1176
  testedPrePatch: _testedPrePatch,
@@ -1092,7 +1183,7 @@ async function verifyFix({
1092
1183
  });
1093
1184
  }
1094
1185
 
1095
- return { ok, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary };
1186
+ return { ok, verifiedFull, degradedLegs, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary };
1096
1187
  }
1097
1188
 
1098
1189
 
@@ -0,0 +1,163 @@
1
+ export const id = 144;
2
+ export const ids = [144];
3
+ export const modules = {
4
+
5
+ /***/ 5144:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ addLegalHold: () => (/* binding */ addLegalHold),
10
+ /* harmony export */ isUnderHold: () => (/* binding */ isUnderHold),
11
+ /* harmony export */ listLegalHolds: () => (/* binding */ listLegalHolds),
12
+ /* harmony export */ loadLegalHolds: () => (/* binding */ loadLegalHolds),
13
+ /* harmony export */ removeLegalHold: () => (/* binding */ removeLegalHold)
14
+ /* harmony export */ });
15
+ /* unused harmony export LEGAL_HOLD_FILE */
16
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
17
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1174);
18
+ /* harmony import */ var _artifact_registry_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(471);
19
+ // FR-707 (assurance-hardening PRD): "Support legal hold and policy-
20
+ // authorized retention exceptions | Legal hold is identity-bound, reasoned,
21
+ // time-bounded where applicable, and auditable."
22
+ //
23
+ // A third instance of the recurring {owner, reason, expires_at} exception
24
+ // shape this codebase already uses twice — `posture/suppressions.js`'s
25
+ // pro-tier exception (scoped to a FINDING) and `posture/compliance-policy.js`'s
26
+ // structured `not-applicable` (scoped to a COMPLIANCE CONTROL). Per D-0025,
27
+ // these are deliberately distinct mechanisms serving different subjects, not
28
+ // one shared module — this file is the third subject: a STATE ARTIFACT.
29
+ //
30
+ // Field naming matches the existing two schemas' snake_case convention
31
+ // (`owner`, `reason`, `expires_at`) rather than inventing a fourth style.
32
+ //
33
+ // identity-bound -> `owner` (required, who placed the hold and is
34
+ // accountable for lifting it)
35
+ // reasoned -> `reason` (required — "we might need this later" is
36
+ // not a reason; same discipline suppressions.js
37
+ // already enforces for its own exceptions)
38
+ // time-bounded
39
+ // where applicable -> `expires_at` is OPTIONAL: a null/absent value is
40
+ // an INDEFINITE hold, which the acceptance
41
+ // criterion's own "where applicable" phrase
42
+ // explicitly allows (a genuine legal matter may have
43
+ // no known end date) — an ISO date value behaves
44
+ // exactly like FR-506/FR-1004's own expiring
45
+ // exceptions: once past, the hold is no longer
46
+ // active and the artifact is exposed to its normal
47
+ // retention TTL again.
48
+ // auditable -> persisted as a single JSON array under
49
+ // `.agentic-security/legal-holds.json` (itself
50
+ // registered as operator-config — an operator/legal
51
+ // team's own input, never scanner-written from scan
52
+ // results), readable via `listLegalHolds`.
53
+ //
54
+ // Consulted from TWO places, not one: `retention-policy.js#findExpiredArtifacts`
55
+ // (defense in depth for any caller reaching it directly) AND `cmdReset`
56
+ // itself for its PLAIN (non-`--expired`) path, which deletes every
57
+ // registered 'generated' artifact unconditionally and would otherwise blow
58
+ // through a hold that only gated TTL expiry.
59
+
60
+
61
+
62
+
63
+
64
+ const LEGAL_HOLD_FILE = 'legal-holds.json';
65
+
66
+ function _loadRaw(scanRoot) {
67
+ let fp;
68
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, LEGAL_HOLD_FILE); } catch { return []; }
69
+ let raw;
70
+ try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return []; }
71
+ try {
72
+ const doc = JSON.parse(raw);
73
+ return Array.isArray(doc) ? doc : [];
74
+ } catch { return []; }
75
+ }
76
+
77
+ /**
78
+ * All legal holds ever recorded for this project, malformed entries
79
+ * dropped rather than throwing. Never filters by expiry — see
80
+ * `isUnderHold`/`listLegalHolds` for that.
81
+ */
82
+ function loadLegalHolds(scanRoot) {
83
+ return _loadRaw(scanRoot).filter(h => h && typeof h === 'object' && typeof h.artifact === 'string' && typeof h.owner === 'string' && typeof h.reason === 'string');
84
+ }
85
+
86
+ /**
87
+ * Is `artifactName` currently protected by an active (non-expired) hold?
88
+ * Returns the matching hold record, or null. Multiple holds on the same
89
+ * artifact are permitted (e.g. two independent legal matters); the first
90
+ * still-active one found is returned.
91
+ */
92
+ function isUnderHold(artifactName, holds, now = Date.now()) {
93
+ for (const h of holds) {
94
+ if (h.artifact !== artifactName) continue;
95
+ if (!h.expires_at) return h; // indefinite hold — always active
96
+ const t = Date.parse(h.expires_at);
97
+ if (!Number.isFinite(t) || t >= now) return h;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Active (non-expired) holds only, unless `includeExpired`. For
104
+ * `legal-hold list` / auditing.
105
+ */
106
+ function listLegalHolds(scanRoot, { includeExpired = false, now = Date.now() } = {}) {
107
+ const holds = loadLegalHolds(scanRoot);
108
+ if (includeExpired) return holds;
109
+ return holds.filter(h => {
110
+ if (!h.expires_at) return true;
111
+ const t = Date.parse(h.expires_at);
112
+ return !Number.isFinite(t) || t >= now;
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Add a legal hold. Validates identity-bound + reasoned up front (both
118
+ * required, non-empty) and that `expires_at`, if given, is a parseable
119
+ * date in the future — an already-expired hold would be a hold that
120
+ * protects nothing, which is never a legitimate request. `artifact` must
121
+ * name a real registered artifact (artifact-registry.js) — a hold on an
122
+ * unrecognised name can never protect anything and almost always means a
123
+ * typo. Returns `{ok:true, hold}` or `{ok:false, reason}`; never throws.
124
+ */
125
+ function addLegalHold(scanRoot, { artifact, owner, reason, expires_at } = {}) {
126
+ if (!artifact || typeof artifact !== 'string') return { ok: false, reason: '--artifact is required' };
127
+ if (!(0,_artifact_registry_js__WEBPACK_IMPORTED_MODULE_2__/* .isRegisteredArtifact */ .Jl)(artifact)) return { ok: false, reason: `"${artifact}" is not a registered state artifact` };
128
+ if (!owner || typeof owner !== 'string') return { ok: false, reason: '--owner is required (identity-bound)' };
129
+ if (!reason || typeof reason !== 'string') return { ok: false, reason: '--reason is required (reasoned)' };
130
+ if (expires_at) {
131
+ const t = Date.parse(expires_at);
132
+ if (!Number.isFinite(t)) return { ok: false, reason: 'expires_at must be a parseable date' };
133
+ if (t < Date.now()) return { ok: false, reason: 'expires_at is in the past — a hold that already expired protects nothing' };
134
+ }
135
+ const hold = { artifact, owner, reason, expires_at: expires_at || null, created_at: new Date().toISOString() };
136
+ const holds = _loadRaw(scanRoot);
137
+ holds.push(hold);
138
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, LEGAL_HOLD_FILE);
139
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .safeWriteState */ .Ep)(fp, JSON.stringify(holds, null, 2) + '\n')) {
140
+ return { ok: false, reason: 'state writes are disabled (--no-state) or this is not a safe state directory' };
141
+ }
142
+ return { ok: true, hold };
143
+ }
144
+
145
+ /**
146
+ * Remove every hold on `artifact` (lifting a hold, not letting it expire).
147
+ * Returns the number removed. A no-op (0) if none existed — never an error.
148
+ */
149
+ function removeLegalHold(scanRoot, artifact) {
150
+ const holds = _loadRaw(scanRoot);
151
+ const remaining = holds.filter(h => !(h && h.artifact === artifact));
152
+ const removedCount = holds.length - remaining.length;
153
+ if (removedCount > 0) {
154
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, LEGAL_HOLD_FILE);
155
+ (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .safeWriteState */ .Ep)(fp, JSON.stringify(remaining, null, 2) + '\n');
156
+ }
157
+ return removedCount;
158
+ }
159
+
160
+
161
+ /***/ })
162
+
163
+ };
package/dist/178.index.js CHANGED
@@ -13,7 +13,7 @@ export const modules = {
13
13
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
14
14
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
15
15
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
16
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3474);
16
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(7087);
17
17
  // Time-travel + counterfactual scanning (v0.68).
18
18
  //
19
19
  // Two new modes that exploit the pure-input shape of runFullScan:
package/dist/238.index.js CHANGED
@@ -8,9 +8,10 @@ export const modules = {
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
9
  /* harmony export */ I3: () => (/* binding */ recordFixAttempt),
10
10
  /* harmony export */ fixDurationReport: () => (/* binding */ fixDurationReport),
11
- /* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary)
11
+ /* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary),
12
+ /* harmony export */ sU: () => (/* binding */ loadFixAttempts)
12
13
  /* harmony export */ });
13
- /* unused harmony exports FIX_STAGES, loadFixAttempts, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
14
+ /* unused harmony exports FIX_STAGES, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
14
15
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
15
16
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
16
17
  /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);