@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +555 -0
  2. package/bin/agentic-security.js +770 -49
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +117 -18
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +24 -6
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/271.index.js +165 -0
  10. package/dist/384.index.js +1 -1
  11. package/dist/435.index.js +187 -52
  12. package/dist/444.index.js +11 -2
  13. package/dist/449.index.js +76 -12
  14. package/dist/526.index.js +117 -18
  15. package/dist/552.index.js +97 -0
  16. package/dist/637.index.js +27 -5
  17. package/dist/730.index.js +311 -0
  18. package/dist/736.index.js +301 -0
  19. package/dist/824.index.js +7 -0
  20. package/dist/905.index.js +88 -22
  21. package/dist/920.index.js +491 -0
  22. package/dist/970.index.js +173 -0
  23. package/dist/agentic-security.mjs +13 -13
  24. package/dist/agentic-security.mjs.sha256 +1 -1
  25. package/dist/calibration-seed.json +2 -0
  26. package/package.json +23 -9
  27. package/src/compare.js +6 -1
  28. package/src/dataflow/CLAUDE.md +1 -1
  29. package/src/dataflow/index.js +18 -0
  30. package/src/dataflow/privacy-catalog.js +290 -0
  31. package/src/dataflow/privacy-deep-walker.js +515 -0
  32. package/src/dataflow/privacy-governance.js +126 -0
  33. package/src/dataflow/privacy-inventory.js +154 -0
  34. package/src/dataflow/privacy-sink-policy.js +125 -0
  35. package/src/dataflow/privacy-taint.js +115 -54
  36. package/src/dataflow/privacy-taxonomy.js +233 -0
  37. package/src/discovery/disprove.js +7 -3
  38. package/src/discovery/hunter.js +9 -5
  39. package/src/discovery/index.js +2 -2
  40. package/src/discovery/llm-invoke.js +69 -13
  41. package/src/egress/audit.js +147 -0
  42. package/src/egress/policy.js +313 -0
  43. package/src/egress/redact.js +180 -0
  44. package/src/engine.js +1048 -302
  45. package/src/fix/apply-fix-service.js +404 -0
  46. package/src/fix/approver-registry.js +157 -0
  47. package/src/history-scan.js +22 -5
  48. package/src/ir/CLAUDE.md +1 -1
  49. package/src/llm-validator/index.js +86 -9
  50. package/src/llm-validator/model-status.js +66 -0
  51. package/src/lsp/server.js +49 -2
  52. package/src/mcp/tools.js +177 -50
  53. package/src/pipeline/analyzer-supervisor.js +93 -0
  54. package/src/pipeline/analyzer-worker.js +26 -0
  55. package/src/pipeline/annotator-runner.js +33 -0
  56. package/src/pipeline/assurance-mode.js +154 -0
  57. package/src/pipeline/cascade-worker-pool.js +172 -0
  58. package/src/pipeline/cascade-worker.js +43 -0
  59. package/src/pipeline/coverage-ledger.js +0 -0
  60. package/src/pipeline/detector-runner.js +51 -0
  61. package/src/pipeline/enrichment-completion.js +58 -0
  62. package/src/pipeline/evidence-provenance.js +91 -0
  63. package/src/pipeline/finding-schema.js +108 -0
  64. package/src/pipeline/legacy-compat.js +101 -0
  65. package/src/pipeline/producer-collector.js +48 -0
  66. package/src/pipeline/producer-registry.js +112 -0
  67. package/src/pipeline/scan-health.js +144 -0
  68. package/src/posture/CLAUDE.md +123 -0
  69. package/src/posture/accuracy-scorecard.js +156 -1
  70. package/src/posture/adversary-agent.js +15 -3
  71. package/src/posture/artifact-registry.js +241 -0
  72. package/src/posture/auditor-walkthrough.js +186 -21
  73. package/src/posture/calibration-feedback.js +201 -0
  74. package/src/posture/calibration-seed.json +2 -0
  75. package/src/posture/calibration.js +25 -0
  76. package/src/posture/compliance-evidence-signing.js +131 -0
  77. package/src/posture/compliance-policy.js +324 -17
  78. package/src/posture/cross-repo-memory.js +7 -2
  79. package/src/posture/custom-rules.js +36 -0
  80. package/src/posture/deterministic.js +8 -1
  81. package/src/posture/encryption-provider.js +205 -0
  82. package/src/posture/evidence-grade-wording.js +71 -0
  83. package/src/posture/fix-history.js +137 -20
  84. package/src/posture/fix-honesty-gate.js +47 -6
  85. package/src/posture/fix-verify.js +65 -8
  86. package/src/posture/fleet.js +0 -0
  87. package/src/posture/flow-narration.js +7 -2
  88. package/src/posture/git-history.js +13 -5
  89. package/src/posture/legal-hold.js +140 -0
  90. package/src/posture/llm-redteam.js +10 -1
  91. package/src/posture/material-change.js +111 -2
  92. package/src/posture/mttr.js +75 -12
  93. package/src/posture/policy-bundle.js +274 -0
  94. package/src/posture/pre-incident-archaeology.js +39 -7
  95. package/src/posture/privacy-framework.js +47 -6
  96. package/src/posture/production-feedback.js +179 -0
  97. package/src/posture/provenance/ai-authorship.js +68 -0
  98. package/src/posture/provenance/branch-entry.js +80 -0
  99. package/src/posture/provenance/cache.js +143 -0
  100. package/src/posture/provenance/confidence.js +36 -0
  101. package/src/posture/provenance/coordinator.js +786 -0
  102. package/src/posture/provenance/dag-walk.js +249 -0
  103. package/src/posture/provenance/evidence-attribution.js +59 -0
  104. package/src/posture/provenance/git-evidence.js +310 -0
  105. package/src/posture/provenance/lifecycle.js +208 -0
  106. package/src/posture/provenance/missing-control-resolver.js +137 -0
  107. package/src/posture/provenance/origin-resolver.js +342 -0
  108. package/src/posture/provenance/predicate-replay.js +133 -0
  109. package/src/posture/provenance/providers/config.js +39 -0
  110. package/src/posture/provenance/providers/github.js +62 -0
  111. package/src/posture/provenance/providers/gitlab.js +58 -0
  112. package/src/posture/provenance/repo-lineage.js +74 -0
  113. package/src/posture/provenance/sca-origin.js +139 -0
  114. package/src/posture/provenance/schema.js +255 -0
  115. package/src/posture/provenance/transitive-sca.js +147 -0
  116. package/src/posture/provenance/validate.js +30 -0
  117. package/src/posture/provenance-evidence-bundle.js +144 -0
  118. package/src/posture/retention-policy.js +132 -0
  119. package/src/posture/risk-dollars.js +216 -26
  120. package/src/posture/sbom-diff.js +15 -2
  121. package/src/posture/scan-checkpoint.js +176 -31
  122. package/src/posture/secret-history.js +10 -2
  123. package/src/posture/state-dir.js +64 -5
  124. package/src/posture/state-lifecycle-report.js +77 -0
  125. package/src/posture/suppressions.js +59 -3
  126. package/src/posture/vuln-archaeology.js +8 -2
  127. package/src/pr-delta.js +25 -4
  128. package/src/privacy/ir-adapter.js +380 -0
  129. package/src/report/index.js +248 -5
  130. package/src/report/oscal.js +7 -2
  131. package/src/runScan.js +34 -5
  132. package/src/sast/cpp.js +3 -14
  133. package/src/sast/rate-limit.js +33 -3
  134. package/src/sca/llm-function-extract.js +6 -0
  135. package/src/util/git-hardening.js +128 -0
@@ -0,0 +1,311 @@
1
+ export const id = 730;
2
+ export const ids = [730,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
+ /***/ 6730:
164
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
165
+
166
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
167
+ /* harmony export */ findExpiredArtifacts: () => (/* binding */ findExpiredArtifacts)
168
+ /* harmony export */ });
169
+ /* unused harmony exports RETENTION_DEFAULTS, loadRetentionPolicy, effectiveTtlDays */
170
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
171
+ /* harmony import */ var _util_yaml_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2340);
172
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
173
+ /* harmony import */ var _artifact_registry_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(471);
174
+ /* harmony import */ var _legal_hold_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5144);
175
+ // Retention policy: default and maximum TTL by artifact class (assurance-
176
+ // hardening PRD FR-702).
177
+ //
178
+ // "Enforce default and maximum TTL by artifact class | Expired caches,
179
+ // scans, evidence, tickets, and backups are purged or archived according
180
+ // to policy."
181
+ //
182
+ // TWO NUMBERS PER CLASS, ON PURPOSE
183
+ // --------------------------------------------------------------------------
184
+ // `defaultDays` is what applies when nobody configures anything — a
185
+ // reasonable, disclosed starting point, not a regulatory citation (an
186
+ // operator with a real compliance regime should configure their own via
187
+ // the policy file below). `maxDays` is a CEILING an operator's own
188
+ // configuration cannot exceed: the literal "default AND maximum" wording
189
+ // asks for both a floor-free default and a hard cap, not just a knob. An
190
+ // operator who wants indefinite retention for `evidence` for their own
191
+ // audit reasons should say so in their own retention program — this
192
+ // module will not silently allow a TTL past its built-in ceiling for a
193
+ // class, because "TTL" than can be configured to "never" is not a TTL.
194
+ //
195
+ // PURGE, NOT ARCHIVE
196
+ // --------------------------------------------------------------------------
197
+ // The acceptance criterion allows either. This implements PURGE (delete)
198
+ // only — the simpler, safer, and more directly verifiable of the two.
199
+ // "Archive" implies a defined export format and destination, which is
200
+ // FR-706's own separate scope (manifest-based export); an operator who
201
+ // wants to archive before purging already has the tool for it once FR-706
202
+ // exists, or can back up `.agentic-security/` by their own means before
203
+ // running the enforcement command.
204
+ //
205
+ // WHICH ARTIFACTS THIS APPLIES TO
206
+ // --------------------------------------------------------------------------
207
+ // Only 'generated' artifacts carrying a `retentionClass` in
208
+ // artifact-registry.js — deliberately a SUBSET of all generated artifacts
209
+ // (see that module's own header for which ones were left classless and
210
+ // why). An 'operator-config' artifact is NEVER touched by this module,
211
+ // regardless of age, matching FR-703's own "reset preserves operator-
212
+ // authored configuration" precedent.
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+ const POLICY_FILE = 'retention-policy.yml';
221
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
222
+
223
+ // Engineering defaults, not a regulatory claim — see the header above.
224
+ const RETENTION_DEFAULTS = {
225
+ cache: { defaultDays: 7, maxDays: 30 },
226
+ scan: { defaultDays: 90, maxDays: 365 },
227
+ evidence: { defaultDays: 365, maxDays: 1095 },
228
+ ticket: { defaultDays: 180, maxDays: 730 },
229
+ backup: { defaultDays: 30, maxDays: 180 },
230
+ };
231
+
232
+ /**
233
+ * Load an operator's `.agentic-security/retention-policy.yml`. Never
234
+ * throws — a missing or malformed file degrades to "no overrides," the
235
+ * same no-op-until-configured convention this repo uses for every other
236
+ * policy surface. Shape:
237
+ * cache: { defaultDays: 3 }
238
+ * evidence: { defaultDays: 730 }
239
+ * A class not mentioned, or a file not present at all, uses
240
+ * RETENTION_DEFAULTS unmodified.
241
+ */
242
+ function loadRetentionPolicy(scanRoot) {
243
+ if (!scanRoot) return null;
244
+ let fp;
245
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, POLICY_FILE); } catch { return null; }
246
+ let raw;
247
+ try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return null; }
248
+ try {
249
+ const doc = _util_yaml_js__WEBPACK_IMPORTED_MODULE_1__/* .load */ .Hh(raw);
250
+ if (!doc || typeof doc !== 'object') return null;
251
+ return doc;
252
+ } catch { return null; }
253
+ }
254
+
255
+ /**
256
+ * The TTL (in days) actually in effect for a class, after applying any
257
+ * operator override and clamping it to the class's own maxDays — an
258
+ * override CANNOT raise retention past the ceiling, only lower it (or
259
+ * leave it at the default). An unrecognised class (should not happen —
260
+ * only artifact-registry.js's own 5 named classes are ever passed in)
261
+ * degrades to null, meaning "not subject to a TTL."
262
+ */
263
+ function effectiveTtlDays(retentionClass, policy) {
264
+ const bounds = RETENTION_DEFAULTS[retentionClass];
265
+ if (!bounds) return null;
266
+ const override = policy?.[retentionClass]?.defaultDays;
267
+ if (typeof override === 'number' && Number.isFinite(override) && override >= 0) {
268
+ return Math.min(override, bounds.maxDays);
269
+ }
270
+ return bounds.defaultDays;
271
+ }
272
+
273
+ /**
274
+ * Which registered, retention-classed artifacts under `scanRoot`'s state
275
+ * dir are currently past their effective TTL. Never throws — a missing
276
+ * state dir or an artifact that does not exist on disk is simply absent
277
+ * from the result, not an error.
278
+ *
279
+ * @returns {Array<{name: string, isDir: boolean, retentionClass: string,
280
+ * ageDays: number, ttlDays: number}>}
281
+ */
282
+ function findExpiredArtifacts(scanRoot, { now = Date.now() } = {}) {
283
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
284
+ let dirExists = true;
285
+ try { node_fs__WEBPACK_IMPORTED_MODULE_0__.accessSync(dir); } catch { dirExists = false; }
286
+ if (!dirExists) return [];
287
+ const policy = loadRetentionPolicy(scanRoot);
288
+ // FR-707: an artifact under an active legal hold is NEVER reported as
289
+ // expired, regardless of TTL — checked here (not just in cmdReset) so
290
+ // any future caller of this function inherits the same guarantee.
291
+ const holds = (0,_legal_hold_js__WEBPACK_IMPORTED_MODULE_4__.loadLegalHolds)(scanRoot);
292
+ const expired = [];
293
+ for (const artifact of (0,_artifact_registry_js__WEBPACK_IMPORTED_MODULE_3__/* .listArtifactsWithRetentionClass */ .U_)()) {
294
+ const ttlDays = effectiveTtlDays(artifact.retentionClass, policy);
295
+ if (ttlDays === null) continue;
296
+ if ((0,_legal_hold_js__WEBPACK_IMPORTED_MODULE_4__.isUnderHold)(artifact.name, holds, now)) continue;
297
+ const p = `${dir}/${artifact.name}`;
298
+ let st;
299
+ try { st = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(p); } catch { continue; } // not present — nothing to expire
300
+ const ageDays = (now - st.mtimeMs) / MS_PER_DAY;
301
+ if (ageDays > ttlDays) {
302
+ expired.push({ name: artifact.name, isDir: st.isDirectory(), retentionClass: artifact.retentionClass, ageDays, ttlDays });
303
+ }
304
+ }
305
+ return expired;
306
+ }
307
+
308
+
309
+ /***/ })
310
+
311
+ };
@@ -0,0 +1,301 @@
1
+ export const id = 736;
2
+ export const ids = [736];
3
+ export const modules = {
4
+
5
+ /***/ 3736:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ buildPolicyBundle: () => (/* binding */ buildPolicyBundle),
10
+ /* harmony export */ ensurePolicyKeyPair: () => (/* binding */ ensurePolicyKeyPair),
11
+ /* harmony export */ loadPolicyBundles: () => (/* binding */ loadPolicyBundles),
12
+ /* harmony export */ loadPolicyPublicKey: () => (/* binding */ loadPolicyPublicKey),
13
+ /* harmony export */ resolveEffectivePolicy: () => (/* binding */ resolveEffectivePolicy),
14
+ /* harmony export */ signPolicyBundle: () => (/* binding */ signPolicyBundle)
15
+ /* harmony export */ });
16
+ /* unused harmony exports POLICY_BUNDLE_SCHEMA, SCOPES, canonicalPolicyBytes, verifyPolicyBundle, computePolicyDrift */
17
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
18
+ /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8161);
19
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
20
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(7598);
21
+ /* harmony import */ var _evidence_bundle_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(8317);
22
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(1174);
23
+ // Signed, portable policy bundles (assurance-hardening PRD FR-1001).
24
+ //
25
+ // "Support signed portable policy bundles with organization, repository, and
26
+ // environment inheritance" | "Tampered or expired policy is rejected;
27
+ // effective policy is explainable."
28
+ //
29
+ // WHY REUSE evidence-bundle.js's Ed25519 SCHEME, NOT integrity.js's HMAC
30
+ // -------------------------------------------------------------------------
31
+ // A policy bundle is authored by one party (typically a central security
32
+ // team, "the organization") and distributed to many repositories that never
33
+ // had the signing key. integrity.js's per-install symmetric HMAC is
34
+ // tamper-evidence for the SAME install that wrote it — a different install
35
+ // verifying a bundle it didn't sign is exactly the case that scheme cannot
36
+ // serve. evidence-bundle.js already solved this for findings: Ed25519,
37
+ // verify with only the public key. This module reuses that primitive
38
+ // directly (canonicalisation shape, allowlist-signed-fields discipline,
39
+ // exclusive-create key generation) rather than inventing a third signing
40
+ // mechanism, per this session's own established "survey before building a
41
+ // parallel primitive" practice (see D-0023/D-0025 for the same discipline
42
+ // applied to identity and exception mechanisms respectively).
43
+ //
44
+ // EA-03 LESSON, APPLIED HERE FROM THE START
45
+ // -------------------------------------------------------------------------
46
+ // evidence-bundle.js's own history (see its EA-03 comment) is the reason
47
+ // `canonicalPolicyBytes` signs an explicit ALLOWLIST and `verifyPolicyBundle`
48
+ // REJECTS any top-level key outside it: a bundle with a field stapled on
49
+ // after signing must fail verification, not silently pass with the addition
50
+ // unverified. Getting this right on the first pass — rather than discovering
51
+ // the gap the way evidence-bundle.js did — is the entire point of writing it
52
+ // down here.
53
+ //
54
+ // SCOPE — WHAT THIS MODULE DOES NOT DO
55
+ // -------------------------------------------------------------------------
56
+ // It does not invent a policy SCHEMA — `policy` is an opaque object; whatever
57
+ // keys a bundle carries (egress rules, severity floors, approved providers,
58
+ // anything else this repo already treats as policy) are merged the same way.
59
+ // It does not implement deep/recursive merging — inheritance is shallow,
60
+ // top-level-key override (organization -> repository -> environment, most
61
+ // specific wins per key), matching how every other flat policy config file
62
+ // in this repo (egress-policy.yml, compliance-severity-policy.json, …) is
63
+ // already shaped. A future consumer needing nested-key inheritance is new,
64
+ // separate scope, not assumed here.
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+ const POLICY_BUNDLE_SCHEMA = 'agentic-security/policy-bundle@1';
74
+
75
+ const SCOPES = ['organization', 'repository', 'environment'];
76
+ // Inheritance order — later entries override earlier ones, per top-level key.
77
+ const INHERITANCE_ORDER = ['organization', 'repository', 'environment'];
78
+
79
+ function keyDir() {
80
+ const xdg = process.env.XDG_CONFIG_HOME || node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.config');
81
+ return node_path__WEBPACK_IMPORTED_MODULE_2__.join(xdg, 'agentic-security', 'policy-bundles');
82
+ }
83
+
84
+ /** A SEPARATE Ed25519 keypair from evidence-bundle.js's finding-attestation
85
+ * key — signing "this policy is authentic" and "this finding really came
86
+ * from a scan run by me" are different trust domains and must not share a
87
+ * key. Reuses evidence-bundle.js's hardened, race-safe generation logic
88
+ * directly (its exclusive-create collision handling is the kind of code
89
+ * worth NOT re-implementing) rather than duplicating it — the separation
90
+ * that matters is the DIRECTORY (`policy-bundles/`, isolated from
91
+ * evidence-bundle.js's own key dir), not the leaf filenames, which are
92
+ * `attest-key.pem`/`attest-key.pub.pem` regardless of caller (a
93
+ * evidence-bundle.js internal, not overridable here) — a readability wart,
94
+ * not a security one, since the directory is what prevents key reuse. */
95
+ function ensurePolicyKeyPair(dir = keyDir()) {
96
+ return (0,_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_4__.ensureKeyPair)(dir);
97
+ }
98
+
99
+ /**
100
+ * Deterministic JSON, keys sorted at every level — identical algorithm to
101
+ * evidence-bundle.js's canonicalJson (duplicated rather than imported: it is
102
+ * a pure, three-line function, and importing it would couple this module's
103
+ * signature format to evidence-bundle.js's internals for no real benefit).
104
+ */
105
+ function canonicalJson(value) {
106
+ if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null);
107
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
108
+ const keys = Object.keys(value).sort();
109
+ return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
110
+ }
111
+
112
+ // The complete set of fields a legitimately-built bundle carries, BEFORE
113
+ // signing. This is the signed allowlist AND (in verifyPolicyBundle) the
114
+ // complete set of keys a signed bundle is permitted to have alongside
115
+ // `signature` — see the EA-03 note above for why both matter.
116
+ const BUNDLE_FIELDS = ['schema', 'scope', 'policy', 'issuedAt', 'expiresAt'];
117
+ const BUNDLE_TOP_LEVEL_KEYS = new Set([...BUNDLE_FIELDS, 'signature']);
118
+
119
+ function canonicalPolicyBytes(bundle) {
120
+ const signed = {};
121
+ for (const k of BUNDLE_FIELDS) signed[k] = bundle[k] ?? null;
122
+ return Buffer.from(canonicalJson(signed), 'utf8');
123
+ }
124
+
125
+ /**
126
+ * Build an unsigned bundle. `policy` is caller-supplied and opaque — this
127
+ * module does not validate its shape, only its provenance and freshness.
128
+ */
129
+ function buildPolicyBundle(scope, policy, { issuedAt, expiresAt } = {}) {
130
+ if (!SCOPES.includes(scope)) return null;
131
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) return null;
132
+ return {
133
+ schema: POLICY_BUNDLE_SCHEMA,
134
+ scope,
135
+ policy,
136
+ issuedAt: issuedAt ?? new Date().toISOString(),
137
+ expiresAt: expiresAt ?? null,
138
+ };
139
+ }
140
+
141
+ /** Sign a bundle. Returns a new object; the input is not mutated. */
142
+ function signPolicyBundle(bundle, privateKeyPem) {
143
+ const sig = node_crypto__WEBPACK_IMPORTED_MODULE_3__.sign(null, canonicalPolicyBytes(bundle), privateKeyPem);
144
+ return {
145
+ ...bundle,
146
+ signature: { algorithm: 'ed25519', canonicalisation: POLICY_BUNDLE_SCHEMA, value: sig.toString('base64') },
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Verify a bundle: signature authenticity AND freshness. Never throws — a
152
+ * malformed or tampered bundle from an untrusted source (a repo checking out
153
+ * whatever an org distributed) is an expected input, not exceptional.
154
+ *
155
+ * @returns {{ok: boolean, reason: string|null}}
156
+ */
157
+ function verifyPolicyBundle(bundle, publicKeyPem, { now = Date.now() } = {}) {
158
+ if (!bundle || typeof bundle !== 'object') return { ok: false, reason: 'bundle is not an object' };
159
+ if (bundle.schema !== POLICY_BUNDLE_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
160
+ if (!SCOPES.includes(bundle.scope)) return { ok: false, reason: `unrecognised scope: ${bundle.scope}` };
161
+ const unknownKeys = Object.keys(bundle).filter(k => !BUNDLE_TOP_LEVEL_KEYS.has(k));
162
+ if (unknownKeys.length) {
163
+ return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
164
+ }
165
+ const sig = bundle.signature;
166
+ if (!sig?.value) return { ok: false, reason: 'bundle is unsigned' };
167
+ if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
168
+ if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
169
+ let sigOk = false;
170
+ try {
171
+ sigOk = node_crypto__WEBPACK_IMPORTED_MODULE_3__.verify(null, canonicalPolicyBytes(bundle), publicKeyPem, Buffer.from(sig.value, 'base64'));
172
+ } catch (e) {
173
+ return { ok: false, reason: `verification error: ${e.message}` };
174
+ }
175
+ if (!sigOk) return { ok: false, reason: 'signature does not match the bundle contents — it was modified after signing' };
176
+ if (bundle.expiresAt) {
177
+ const exp = Date.parse(bundle.expiresAt);
178
+ if (!Number.isFinite(exp)) return { ok: false, reason: `expiresAt is not a valid date: ${bundle.expiresAt}` };
179
+ if (exp < now) return { ok: false, reason: `policy expired on ${bundle.expiresAt}` };
180
+ }
181
+ return { ok: true, reason: null };
182
+ }
183
+
184
+ /**
185
+ * Merge verified bundles into one effective policy, in inheritance order
186
+ * (organization -> repository -> environment; most specific wins per key).
187
+ * A tampered or expired bundle is EXCLUDED from the merge (rejected, not
188
+ * silently dropped — its scope and reason are reported so a rejection is
189
+ * visible, not just absent). `provenance[key]` names which scope's bundle
190
+ * last set that key — the "effective policy is explainable" half of the
191
+ * acceptance criterion.
192
+ *
193
+ * @param {Array<{scope: string, bundle: object}>} entries
194
+ * @param {string} publicKeyPem
195
+ * @returns {{effective: object, provenance: Record<string,string>,
196
+ * accepted: string[], rejected: Array<{scope:string, reason:string}>}}
197
+ */
198
+ function resolveEffectivePolicy(entries, publicKeyPem, { now = Date.now() } = {}) {
199
+ const byScope = new Map();
200
+ for (const e of entries || []) {
201
+ if (e && SCOPES.includes(e.scope)) byScope.set(e.scope, e.bundle);
202
+ }
203
+ const effective = {};
204
+ const provenance = {};
205
+ const accepted = [];
206
+ const rejected = [];
207
+ for (const scope of INHERITANCE_ORDER) {
208
+ const bundle = byScope.get(scope);
209
+ if (!bundle) continue;
210
+ const v = verifyPolicyBundle(bundle, publicKeyPem, { now });
211
+ if (!v.ok) { rejected.push({ scope, reason: v.reason }); continue; }
212
+ accepted.push(scope);
213
+ for (const [k, val] of Object.entries(bundle.policy)) {
214
+ effective[k] = val;
215
+ provenance[k] = scope;
216
+ }
217
+ }
218
+ return { effective, provenance, accepted, rejected };
219
+ }
220
+
221
+ /**
222
+ * Load whichever policy bundle files exist under
223
+ * `.agentic-security/policy-bundles/{organization,repository,environment}.json`.
224
+ * Degrades gracefully — a missing directory or missing/malformed individual
225
+ * file is simply absent from the result, never thrown. Read-first-in-try/
226
+ * catch throughout (no existsSync-then-readFileSync — D-0012/D-0022).
227
+ */
228
+ function loadPolicyBundles(scanRoot) {
229
+ if (!scanRoot) return [];
230
+ const entries = [];
231
+ for (const scope of SCOPES) {
232
+ let fp;
233
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_5__/* .statePath */ .BQ)(scanRoot, 'policy-bundles', `${scope}.json`); } catch { continue; }
234
+ let raw;
235
+ try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { continue; }
236
+ try {
237
+ const bundle = JSON.parse(raw);
238
+ if (bundle && typeof bundle === 'object') entries.push({ scope, bundle });
239
+ } catch { /* malformed — skip, do not throw */ }
240
+ }
241
+ return entries;
242
+ }
243
+
244
+ /**
245
+ * Read the operator-supplied public key an org distributes alongside its
246
+ * bundles, from `.agentic-security/policy-bundle-public-key.pem`. Returns
247
+ * null (not a throw) if absent or unreadable — resolution then rejects
248
+ * every bundle for lack of a key, same as any other "no public key
249
+ * supplied" case in verifyPolicyBundle.
250
+ */
251
+ function loadPolicyPublicKey(scanRoot) {
252
+ if (!scanRoot) return null;
253
+ let fp;
254
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_5__/* .statePath */ .BQ)(scanRoot, 'policy-bundle-public-key.pem'); } catch { return null; }
255
+ try { return node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return null; }
256
+ }
257
+
258
+ /**
259
+ * FR-1006 ("policy drift"): does this repository's LOCALLY resolved effective
260
+ * policy diverge from what the organization-scope bundle alone specifies —
261
+ * and is anything the repo is relying on actually being rejected (tampered
262
+ * or expired) rather than enforced? Both are GOVERNANCE gaps, distinct from
263
+ * risk findings, which is the exact distinction FR-1006's acceptance
264
+ * criterion asks fleet output to make.
265
+ *
266
+ * No bundles configured at all is a no-op (`null`), matching this session's
267
+ * established convention: drift can only be reported relative to a
268
+ * baseline, and a repo with no organization bundle has no baseline to drift
269
+ * from.
270
+ *
271
+ * @returns {{accepted: string[], rejected: Array<{scope,reason}>,
272
+ * overrides: Array<{key, organizationValue, effectiveValue, overriddenBy}>}
273
+ * | null}
274
+ */
275
+ function computePolicyDrift(scanRoot) {
276
+ const entries = loadPolicyBundles(scanRoot);
277
+ if (!entries.length) return null;
278
+ const publicKeyPem = loadPolicyPublicKey(scanRoot);
279
+ const { effective, provenance, accepted, rejected } = resolveEffectivePolicy(entries, publicKeyPem);
280
+
281
+ const overrides = [];
282
+ const orgEntry = entries.find(e => e.scope === 'organization');
283
+ if (orgEntry && accepted.includes('organization')) {
284
+ for (const [key, organizationValue] of Object.entries(orgEntry.bundle.policy || {})) {
285
+ const overriddenBy = provenance[key];
286
+ if (overriddenBy && overriddenBy !== 'organization' && !_deepEqual(effective[key], organizationValue)) {
287
+ overrides.push({ key, organizationValue, effectiveValue: effective[key], overriddenBy });
288
+ }
289
+ }
290
+ }
291
+ return { accepted, rejected, overrides };
292
+ }
293
+
294
+ function _deepEqual(a, b) {
295
+ return JSON.stringify(a) === JSON.stringify(b);
296
+ }
297
+
298
+
299
+ /***/ })
300
+
301
+ };
package/dist/824.index.js CHANGED
@@ -12,6 +12,7 @@ export const modules = {
12
12
  /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
13
13
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
14
14
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
15
+ /* harmony import */ var _egress_policy_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5712);
15
16
  // LLM-assisted vulnerable function extraction for SCA findings.
16
17
  //
17
18
  // For CVEs without OSV ecosystem_specific data or GHSA fix commits,
@@ -24,6 +25,7 @@ export const modules = {
24
25
 
25
26
 
26
27
 
28
+
27
29
  const CACHE_DIR = process.env.XDG_CONFIG_HOME
28
30
  ? node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.XDG_CONFIG_HOME, 'agentic-security', 'llm-sca-cache')
29
31
  : node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.HOME || '/tmp', '.config', 'agentic-security', 'llm-sca-cache');
@@ -77,6 +79,11 @@ async function extractVulnFunctionsViaLLM(supplyChain, opts = {}) {
77
79
  if (!isLlmScaEnabled()) return [];
78
80
  const config = _endpointConfig();
79
81
  if (!config) return [];
82
+ // FR-601: one decision for the whole batch (same endpoint for every CVE in
83
+ // this run) — evaluated before any of the per-candidate prompts below are
84
+ // built.
85
+ const egressDecision = (0,_egress_policy_js__WEBPACK_IMPORTED_MODULE_3__/* .evaluateEgress */ .nn)({ scanRoot: opts.scanRoot, purpose: 'sca-llm-function-extract', endpoint: config.endpoint });
86
+ if (!egressDecision.allowed) return [];
80
87
 
81
88
  const enriched = [];
82
89
  const candidates = (supplyChain || []).filter(sc =>