@clear-capabilities/agentic-security-scanner 0.175.0 → 0.176.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 (101) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/agentic-security.js +199 -28
  3. package/dist/2066.index.js +850 -0
  4. package/dist/2199.index.js +1 -1
  5. package/dist/2584.index.js +3332 -0
  6. package/dist/2923.index.js +13 -0
  7. package/dist/2946.index.js +81 -0
  8. package/dist/3196.index.js +27 -0
  9. package/dist/3727.index.js +292 -0
  10. package/dist/4168.index.js +509 -0
  11. package/dist/4281.index.js +993 -0
  12. package/dist/4978bec8fdf5c733dc85.json +283 -0
  13. package/dist/5099.index.js +96 -0
  14. package/dist/5616.index.js +1471 -0
  15. package/dist/5620.index.js +2370 -0
  16. package/dist/5817.index.js +714 -0
  17. package/dist/6333.index.js +98 -0
  18. package/dist/6655.index.js +3221 -0
  19. package/dist/7838.index.js +446 -974
  20. package/dist/818.index.js +4 -3
  21. package/dist/83.index.js +159 -0
  22. package/dist/8551.index.js +82 -0
  23. package/dist/8633.index.js +93 -0
  24. package/dist/8714.index.js +76 -76
  25. package/dist/8825.index.js +84 -0
  26. package/dist/932.index.js +186 -0
  27. package/dist/9560.index.js +358 -19
  28. package/dist/9800.index.js +6 -1
  29. package/dist/agentic-security.mjs +10 -10
  30. package/dist/agentic-security.mjs.sha256 +1 -1
  31. package/dist/df1e8683e7eda7b2b79c.js +29 -0
  32. package/package.json +5 -4
  33. package/src/artifact/CLAUDE.md +4 -0
  34. package/src/artifact/artifact-evidence.js +63 -0
  35. package/src/artifact/cosign.js +61 -0
  36. package/src/artifact/engine-source.js +72 -0
  37. package/src/artifact/oci-layout.js +47 -0
  38. package/src/artifact/registry.js +213 -0
  39. package/src/capabilities/tool-registry.js +2 -0
  40. package/src/lineage/CLAUDE.md +4 -0
  41. package/src/lineage/identity-token.js +11 -0
  42. package/src/lineage/observation-adapters.js +13 -0
  43. package/src/lineage/runtime-edge-states.js +139 -0
  44. package/src/lineage/runtime-explorer-view.js +82 -0
  45. package/src/lineage/runtime-observation.js +11 -0
  46. package/src/mcp/CLAUDE.md +3 -1
  47. package/src/mcp/proof-tools.js +132 -0
  48. package/src/mcp/server.js +1 -1
  49. package/src/mcp/tools.js +3 -1
  50. package/src/posture/CLAUDE.md +23 -0
  51. package/src/posture/artifact-registry.js +4 -0
  52. package/src/posture/dnr/attack-facts.js +31 -0
  53. package/src/posture/dnr/cli.js +138 -0
  54. package/src/posture/dnr/detection-ir.js +123 -0
  55. package/src/posture/dnr/exposure-window.js +134 -0
  56. package/src/posture/dnr/hunt.js +84 -0
  57. package/src/posture/dnr/log-reader.js +189 -0
  58. package/src/posture/dnr/matchers.js +90 -0
  59. package/src/posture/dnr/outputs.js +262 -0
  60. package/src/posture/dnr/payload-classes.js +120 -0
  61. package/src/posture/dnr/redact.js +25 -0
  62. package/src/posture/dnr/store.js +57 -0
  63. package/src/posture/dnr/validate.js +64 -0
  64. package/src/posture/dnr/view.js +51 -0
  65. package/src/posture/evaluation/lab/arm-ollama.js +88 -0
  66. package/src/posture/evaluation/lab/arms.js +142 -0
  67. package/src/posture/evaluation/lab/cli.js +135 -0
  68. package/src/posture/evaluation/lab/dataset.js +139 -0
  69. package/src/posture/evaluation/lab/materialize.js +44 -0
  70. package/src/posture/evaluation/lab/neutralize.js +144 -0
  71. package/src/posture/evaluation/lab/report.js +176 -0
  72. package/src/posture/evaluation/lab/run.js +225 -0
  73. package/src/posture/evaluation/lab/score.js +135 -0
  74. package/src/posture/evaluation/lab/scoreboard.js +48 -0
  75. package/src/posture/evaluation/lab/stats.js +41 -0
  76. package/src/posture/evaluation/lab/store.js +53 -0
  77. package/src/posture/fleet/cli.js +248 -63
  78. package/src/posture/fleet/discover-edges.js +135 -0
  79. package/src/posture/fleet/enterprise-graph.js +148 -0
  80. package/src/posture/fleet/fleet-graph.js +9 -4
  81. package/src/posture/fleet/fleet-manifest.js +50 -5
  82. package/src/posture/fleet/job-runner.js +153 -0
  83. package/src/posture/fleet/job-store.js +136 -0
  84. package/src/posture/fleet/org-health.js +56 -0
  85. package/src/posture/fleet/scan-cache.js +112 -0
  86. package/src/posture/fleet/scan-child.js +29 -0
  87. package/src/posture/proof-fabric/cli.js +34 -6
  88. package/src/posture/proof-fabric/container.js +57 -0
  89. package/src/posture/proof-fabric/fix-proof.js +2 -1
  90. package/src/posture/proof-fabric/generate-python.js +129 -0
  91. package/src/posture/proof-fabric/generate.js +213 -0
  92. package/src/posture/proof-fabric/reproducer.js +3 -0
  93. package/src/posture/proof-fabric/runner.js +20 -11
  94. package/src/posture/proof-fabric/siblings.js +77 -0
  95. package/src/posture/routing/policy-router.js +103 -0
  96. package/src/posture/rule-compiler/cli.js +64 -6
  97. package/src/posture/rule-compiler/corpus.js +10 -2
  98. package/src/posture/rule-compiler/mutation.js +72 -1
  99. package/src/posture/rule-compiler/promotion.js +21 -5
  100. package/src/posture/rule-compiler/proposal.js +132 -0
  101. package/src/posture/rule-compiler/store.js +16 -0
@@ -0,0 +1,2370 @@
1
+ export const id = 5620;
2
+ export const ids = [5620,3736];
3
+ export const modules = {
4
+
5
+ /***/ 53948:
6
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
7
+
8
+ module.exports = __webpack_require__.p + "df1e8683e7eda7b2b79c.js";
9
+
10
+ /***/ }),
11
+
12
+ /***/ 80313:
13
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
14
+
15
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
16
+ /* harmony export */ JQ: () => (/* binding */ rollupFleet),
17
+ /* harmony export */ LL: () => (/* binding */ renderFleetSummary),
18
+ /* harmony export */ gI: () => (/* binding */ runFleet)
19
+ /* harmony export */ });
20
+ /* unused harmony exports loadFleetState, ownersFor, renderFleetHtml, _internals */
21
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
22
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
23
+ /* harmony import */ var _policy_bundle_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(23736);
24
+ // PRD Epic 5 — fleet orchestration.
25
+ //
26
+ // Many repositories, one invocation, one rolled-up result set. The existing
27
+ // `org-scan` could iterate repos; what it could not do is survive an
28
+ // interruption, bound its own concurrency, tell you what is NEW since the last
29
+ // run, or route a finding to the person who owns the file. Those four are the
30
+ // difference between a loop and something an organisation can put on a
31
+ // schedule.
32
+ //
33
+ // THE HARD LINE (PRD §2.2, §10): everything here runs from the CLI on the
34
+ // customer's infrastructure. There is no server, no account, no multi-tenancy,
35
+ // and the "UI" is a static artifact generated from local files. A fleet feature
36
+ // is exactly where a local-first tool drifts into a platform, so the constraint
37
+ // is stated here rather than left to judgement.
38
+ //
39
+ // FOUR PROPERTIES, EACH OF WHICH IS A FAILURE MODE IF ABSENT:
40
+ //
41
+ // 1. ISOLATION — one repo that explodes must not take the run with it. A
42
+ // fleet scan that dies on repo 3 of 50 is worse than useless: it looks
43
+ // like a completed scan of 3 repos.
44
+ // 2. RESUMABILITY — the checkpoint records COMPLETED repos, and is written
45
+ // after each one. A resumed run skips them rather than re-scanning, which
46
+ // is what makes a 50-repo run survivable on a laptop or a flaky runner.
47
+ // 3. BOUNDED CONCURRENCY — an unbounded fan-out over 50 repos will exhaust
48
+ // memory or file handles on the machine it was supposed to help.
49
+ // 4. HONEST ROLL-UP — a repo that FAILED is counted as failed, never folded
50
+ // into "0 findings". Silent zeros are how a fleet report says everything
51
+ // is fine because nothing ran.
52
+ //
53
+ // DELTA IS COMPUTED FROM STABLE IDS, not counts. "3 more criticals than last
54
+ // week" tells you nothing about which; a set difference over stableId tells you
55
+ // exactly what appeared, and is what a scheduled run should notify on.
56
+
57
+
58
+
59
+
60
+
61
+ const SCHEMA = 'agentic-security/fleet-state@1';
62
+
63
+ /** Read a checkpoint, or an empty one. Never throws. */
64
+ function loadFleetState(stateFile) {
65
+ try {
66
+ const j = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(stateFile, 'utf8'));
67
+ if (j && j.schema === SCHEMA) return j;
68
+ } catch { /* absent or unreadable -> start clean */ }
69
+ return { schema: SCHEMA, completed: {}, startedAt: null };
70
+ }
71
+
72
+ function _saveFleetState(stateFile, state) {
73
+ try {
74
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_1__.dirname(stateFile), { recursive: true });
75
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(stateFile, JSON.stringify(state, null, 2));
76
+ return true;
77
+ } catch { return false; }
78
+ }
79
+
80
+ /**
81
+ * Owners for a path, from a CODEOWNERS file.
82
+ *
83
+ * Last matching rule wins, which is the documented CODEOWNERS precedence — a
84
+ * first-match implementation silently routes to the most general owner, which
85
+ * is the one least able to act on the finding.
86
+ */
87
+ function ownersFor(codeownersText, filePath) {
88
+ if (typeof codeownersText !== 'string' || !filePath) return [];
89
+ let owners = [];
90
+ for (const raw of codeownersText.split('\n')) {
91
+ const line = raw.replace(/#.*$/, '').trim();
92
+ if (!line) continue;
93
+ const parts = line.split(/\s+/);
94
+ const pattern = parts.shift();
95
+ const these = parts.filter(p => p.startsWith('@') || p.includes('@'));
96
+ if (!these.length) continue;
97
+ if (_matchesGlob(pattern, filePath)) owners = these;
98
+ }
99
+ return owners;
100
+ }
101
+
102
+ function _matchesGlob(pattern, file) {
103
+ const f = file.replace(/^\.?\//, '');
104
+ let p = pattern.replace(/^\.?\//, '');
105
+ if (p === '*') return true;
106
+ if (p.endsWith('/')) return f.startsWith(p);
107
+ const PLACEHOLDER = '\uE000'; // escape SEQUENCE in source -- 6 ASCII chars, not a raw embedded byte
108
+ const rx = new RegExp('^' + p
109
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
110
+ .replace(/\*\*/g, PLACEHOLDER)
111
+ .replace(/\*/g, '[^/]*')
112
+ .replace(new RegExp(PLACEHOLDER, 'g'), '.*') + '(/.*)?$');
113
+ return rx.test(f);
114
+ }
115
+
116
+ function _readCodeowners(repoRoot) {
117
+ for (const rel of ['CODEOWNERS', '.github/CODEOWNERS', 'docs/CODEOWNERS']) {
118
+ try { return node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(repoRoot, rel), 'utf8'); } catch { /* next */ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
124
+
125
+ function _summarise(scan) {
126
+ const all = [
127
+ ...(scan.findings || []), ...(scan.secrets || []),
128
+ ...(scan.logicVulns || []), ...(scan.supplyChain || []),
129
+ ];
130
+ const bySeverity = Object.fromEntries(SEVERITIES.map(s => [s, 0]));
131
+ const ids = [];
132
+ let proven = 0;
133
+ for (const f of all) {
134
+ const sev = String(f.severity || 'info').toLowerCase();
135
+ if (sev in bySeverity) bySeverity[sev]++;
136
+ if (f.stableId) ids.push(f.stableId);
137
+ if (f.proofTier === 'execution-proven') proven++;
138
+ }
139
+ return { total: all.length, bySeverity, proven, ids, findings: all };
140
+ }
141
+
142
+ /**
143
+ * M4 §4.4 — provenance-aware debt for ONE repo, computed here (inside the
144
+ * worker, from the untrimmed findings array) rather than inside `rollupFleet`,
145
+ * because FR-1005 forbids raw finding data from surviving onto the per-repo
146
+ * result entry (`test('FR-1005: ... the entry must not carry the raw findings
147
+ * array at all')` in fleet.test.js) — a finding can carry a raw source
148
+ * snippet, so only this derived summary (a finding id, a commit date, a day
149
+ * count; never a file path or snippet) is threaded onto `entry.provenanceDebt`.
150
+ *
151
+ * A finding contributes only when its `findingProvenance.status` is
152
+ * `'complete'` — the honest end of the confidence spectrum (see
153
+ * `provenance/schema.js`'s `PROVENANCE_STATUS`). `partial`/`uncommitted`/etc.
154
+ * are not proof of origin and must never win "oldest".
155
+ */
156
+ function _provenanceDebtForRepo(findings, now = Date.now()) {
157
+ let oldest = null;
158
+ let completeCount = 0;
159
+ for (const f of (findings || [])) {
160
+ if (f.findingProvenance?.status !== 'complete') continue;
161
+ const authorDate = f.findingProvenance?.findingOrigin?.authorDate;
162
+ if (!authorDate) continue;
163
+ completeCount++;
164
+ const ageDays = Math.max(0, Math.floor((now - Date.parse(authorDate)) / 86400000));
165
+ if (!oldest || ageDays > oldest.ageDays) {
166
+ // FR-PROV-019: "Reports never show an age without its basis and
167
+ // confidence." The basis is structurally implied here (only
168
+ // status:'complete' findings reach this line, so the age is always
169
+ // git-derived, and the rendered text says "proven-origin") — but the
170
+ // confidence half has to be carried explicitly, same as
171
+ // mttr.js's renderSlaSummary. Captured at selection time so the
172
+ // renderers below cannot show the age without it.
173
+ oldest = {
174
+ findingId: f.id || f.stableId || null,
175
+ authorDate,
176
+ ageDays,
177
+ confidence: f.findingProvenance?.confidence?.level || null,
178
+ };
179
+ }
180
+ }
181
+ return { oldest, completeCount };
182
+ }
183
+
184
+ /**
185
+ * Scan many repositories with bounded concurrency, resumably.
186
+ *
187
+ * `runScan` is injected so this module has no engine import cycle and so the
188
+ * orchestration can be tested without running real scans.
189
+ */
190
+ async function runFleet({
191
+ repos = [], concurrency = 4, stateFile, runScan,
192
+ resume = true, previous = null, onProgress = () => {},
193
+ // X-704: repositories whose every durable portfolio work unit is verified and current
194
+ // (portfolio/work-units.js `fullyVerifiedRepositories`). They are skipped like a resumed completed repo,
195
+ // and, as with a resumed repo, the skip is reported in `skipped` rather than counted as scanned.
196
+ portfolioVerified = [],
197
+ } = {}) {
198
+ if (typeof runScan !== 'function') {
199
+ return { ok: false, reason: 'no runScan supplied' };
200
+ }
201
+ const state = (resume && stateFile) ? loadFleetState(stateFile) : { schema: SCHEMA, completed: {}, startedAt: null };
202
+ const results = [];
203
+ const skipped = [];
204
+
205
+ const queue = repos.filter((r) => {
206
+ if ((resume && state.completed[r]) || portfolioVerified.includes(r)) { skipped.push(r); return false; }
207
+ return true;
208
+ });
209
+
210
+ const limit = Math.max(1, Math.min(concurrency, 32));
211
+ let cursor = 0;
212
+
213
+ async function worker() {
214
+ while (cursor < queue.length) {
215
+ const repo = queue[cursor++];
216
+ let entry;
217
+ try {
218
+ const { scan } = await runScan(repo);
219
+ const s = _summarise(scan);
220
+ const codeowners = _readCodeowners(repo);
221
+ // Route each finding to the owner of its file. Done here rather than at
222
+ // report time because the CODEOWNERS file lives in the repo, and by
223
+ // report time the fleet has moved on.
224
+ const owners = {};
225
+ if (codeowners) {
226
+ for (const f of s.findings) {
227
+ if (!f.file) continue;
228
+ const o = ownersFor(codeowners, f.file);
229
+ for (const own of o) (owners[own] ||= []).push(f.stableId || `${f.file}:${f.line}`);
230
+ }
231
+ }
232
+ // FR-1006: "assurance-health views" and "policy drift" — a repo can
233
+ // be RISK-clean (zero/low findings) while still having a governance
234
+ // gap (a partial scan, a rejected/expired policy bundle nobody
235
+ // noticed). scanHealth comes straight from the engine's own
236
+ // computeScanHealth (FR-206) — nothing new is inferred here, this is
237
+ // the first time the fleet rollup SURFACES a signal that already
238
+ // existed per-scan. computePolicyDrift is null (not an empty object)
239
+ // when the repo has no policy bundles configured at all — matching
240
+ // this session's "no baseline configured -> no-op" convention.
241
+ let policyDrift = null;
242
+ try { policyDrift = (0,_policy_bundle_js__WEBPACK_IMPORTED_MODULE_2__/* .computePolicyDrift */ .rE)(repo); } catch { /* best-effort — never fail a repo's scan over drift reporting */ }
243
+ // M4 §4.4: provenance-aware debt, computed HERE (from `s.findings`,
244
+ // the untrimmed array) rather than in `rollupFleet` — by the time
245
+ // `rollupFleet` sees `results`, the raw findings are gone (FR-1005).
246
+ // Only the derived summary (id/date/age, never a file or snippet)
247
+ // survives onto the entry. `mttr` threads through the SAME aggregate
248
+ // `mttr.js`'s `computeMTTR` already produces (`scan.mttr`, stamped by
249
+ // the CLI's persistence step — see posture/mttr.js) rather than
250
+ // inventing a second notion of remediation time; there is no
251
+ // `scan.remediatedFindings` field anywhere in this codebase.
252
+ const provenanceDebt = _provenanceDebtForRepo(s.findings);
253
+ entry = {
254
+ repo, ok: true, total: s.total, bySeverity: s.bySeverity,
255
+ proven: s.proven, ids: s.ids, owners, scannedAt: new Date().toISOString(),
256
+ scanHealth: scan.scanHealth || null, policyDrift,
257
+ provenanceDebt, mttr: scan.mttr || null,
258
+ };
259
+ } catch (e) {
260
+ // A failed repo is FAILED, never zero findings.
261
+ entry = { repo, ok: false, error: String(e?.message || e), total: null, bySeverity: null, ids: [], scannedAt: new Date().toISOString(), scanHealth: null, policyDrift: null, provenanceDebt: null, mttr: null };
262
+ }
263
+ results.push(entry);
264
+ if (stateFile) {
265
+ // FR-1006 (assurance-hardening PRD): "scan freshness" — this field
266
+ // existed as an always-null stub before (never read anywhere, never
267
+ // populated), which is the same as not existing for anyone trying to
268
+ // answer "when was this repo last scanned". Stamped from the SAME
269
+ // entry.scannedAt the results array already carries, not a second,
270
+ // possibly-diverging timestamp.
271
+ state.completed[repo] = { at: entry.scannedAt, ok: entry.ok, total: entry.total };
272
+ _saveFleetState(stateFile, state);
273
+ }
274
+ onProgress(entry);
275
+ }
276
+ }
277
+
278
+ await Promise.all(Array.from({ length: limit }, () => worker()));
279
+ return { ok: true, results, skipped, rollup: rollupFleet(results, previous) };
280
+ }
281
+
282
+ /**
283
+ * Roll many per-repo results into one view, with a delta when a previous run
284
+ * is supplied.
285
+ */
286
+ function rollupFleet(results, previous = null) {
287
+ const scanned = results.filter(r => r.ok);
288
+ const failed = results.filter(r => !r.ok);
289
+ const bySeverity = Object.fromEntries(SEVERITIES.map(s => [s, 0]));
290
+ let total = 0, proven = 0;
291
+ for (const r of scanned) {
292
+ total += r.total || 0;
293
+ proven += r.proven || 0;
294
+ for (const s of SEVERITIES) bySeverity[s] += r.bySeverity?.[s] || 0;
295
+ }
296
+
297
+ // Delta by stable id, not by count.
298
+ let newFindings = null;
299
+ if (previous && Array.isArray(previous.results)) {
300
+ const before = new Set(previous.results.flatMap(r => r.ids || []));
301
+ newFindings = {};
302
+ for (const r of scanned) {
303
+ const fresh = (r.ids || []).filter(id => !before.has(id));
304
+ if (fresh.length) newFindings[r.repo] = fresh;
305
+ }
306
+ }
307
+
308
+ // FR-1006: governance/coverage gaps, counted and surfaced SEPARATELY from
309
+ // the risk rollup above (total/bySeverity/proven) — a repo can contribute
310
+ // zero to those and still need attention here. `null` scanHealth (a
311
+ // failed repo, or a runScan that predates FR-206) is not counted as
312
+ // partial — absence of a signal is not evidence of a problem.
313
+ const partialScanHealth = scanned.filter(r => r.scanHealth && r.scanHealth.status !== 'complete');
314
+ const withPolicyDrift = scanned.filter(r => r.policyDrift && (r.policyDrift.overrides.length || r.policyDrift.rejected.length));
315
+
316
+ // M4 §4.4: provenance-aware debt + MTTR, surfaced as its own section
317
+ // alongside the FR-1006 governance section above — same additive pattern,
318
+ // same reason: a repo can be risk-clean and still carry old, PROVEN debt,
319
+ // and that must not be folded into (or hidden by) the risk rollup above.
320
+ //
321
+ // `oldestProvenDebt` only ever comes from a repo's own `provenanceDebt`,
322
+ // which `runFleet`'s worker computed from `findingProvenance.status ===
323
+ // 'complete'` findings only — never a wall-clock guess dressed as proof.
324
+ // A repo with no complete-status findings is disclosed BY NAME in
325
+ // `reposWithNoProvenDebt`, never silently absent from a "no debt" story a
326
+ // reader could misread as "this repo has no old findings" when the truth
327
+ // is "we couldn't PROVE any origin here". A `results` entry that predates
328
+ // this field (no `provenanceDebt` key at all — e.g. an older checkpoint,
329
+ // or a hand-built test fixture) is treated the same as "none found",
330
+ // consistent with every other optional signal in this rollup (scanHealth,
331
+ // policyDrift).
332
+ let oldestProvenDebt = null;
333
+ const reposWithNoProvenDebt = [];
334
+ const perRepoOldestProvenDebt = [];
335
+ for (const r of scanned) {
336
+ const oldest = r.provenanceDebt?.oldest || null;
337
+ if (!oldest) { reposWithNoProvenDebt.push(r.repo); continue; }
338
+ const withRepo = { repo: r.repo, ...oldest };
339
+ perRepoOldestProvenDebt.push(withRepo);
340
+ if (!oldestProvenDebt || oldest.ageDays > oldestProvenDebt.ageDays) oldestProvenDebt = withRepo;
341
+ }
342
+
343
+ // Fleet-wide MTTR reuses each repo's own `scan.mttr` (mttr.js's
344
+ // `computeMTTR` output — `{count, meanDays, medianDays, perSeverity}`),
345
+ // combined as a count-weighted mean, rather than recomputing wall-clock
346
+ // remediation time here from a second source of truth. `byAgeBasis` is
347
+ // deliberately always `{}`: `computeMTTR` does not track ageBasis per
348
+ // remediated finding (only per-severity), and there is no
349
+ // `scan.remediatedFindings` array anywhere in this codebase to derive one
350
+ // from — see `runFleet`'s worker comment. Reporting a fabricated breakdown
351
+ // would be worse than reporting none.
352
+ //
353
+ // FINAL-REVIEW FIX (M4, item 4): `mttr` used to default to `{n:0, ...}`
354
+ // whenever no scanned repo's `.mttr` counted a remediation — which is
355
+ // exactly what happens on EVERY real production fleet run, since
356
+ // `scripts/fleet.mjs` drives `runFleet` with `runScan` straight from
357
+ // `src/runScan.js`, and that function never sets `.mttr` at all (the only
358
+ // real setter is the CLI's OWN single-repo persistence step in
359
+ // `bin/agentic-security.js`, which fleet mode does not go through — see
360
+ // that file's comment for the full investigation). `{n:0,...}` and "no
361
+ // repo ever attempted to compute this" are materially different facts:
362
+ // the first is a real, honest zero; the second is a missing capability.
363
+ // Collapsing them made every real fleet run silently render a fabricated-
364
+ // looking "no remediated findings recorded yet" line that reads as a
365
+ // measurement rather than an absence.
366
+ //
367
+ // `anyMttrSupplied` distinguishes them: it is true only when at least one
368
+ // SCANNED repo's entry carried a non-null `.mttr` object at all — which
369
+ // only happens when something upstream (the CLI's own persistence path,
370
+ // or a caller-supplied `runScan` that wires the same pipeline itself)
371
+ // actually ran `computeMTTR`. `mttr` stays `null` — not a zeroed object —
372
+ // when nothing did, so a consumer can tell "tracked, zero fixes so far"
373
+ // from "not tracked at all" without inspecting `n`.
374
+ let mttrCount = 0, mttrWeightedDays = 0, anyMttrSupplied = false;
375
+ for (const r of scanned) {
376
+ const m = r.mttr;
377
+ if (m == null) continue;
378
+ anyMttrSupplied = true;
379
+ if (typeof m.count === 'number' && m.count > 0 && typeof m.meanDays === 'number') {
380
+ mttrCount += m.count;
381
+ mttrWeightedDays += m.meanDays * m.count;
382
+ }
383
+ }
384
+ const mttr = anyMttrSupplied
385
+ ? { n: mttrCount, meanDays: mttrCount ? mttrWeightedDays / mttrCount : null, byAgeBasis: {} }
386
+ : null;
387
+
388
+ return {
389
+ repos: results.length,
390
+ scanned: scanned.length,
391
+ // Surfaced at the top level, not buried: a fleet report whose failures are
392
+ // a footnote reads as a clean fleet.
393
+ failed: failed.length,
394
+ failures: failed.map(f => ({ repo: f.repo, error: f.error })),
395
+ total, proven, bySeverity,
396
+ reposWithPartialScanHealth: partialScanHealth.length,
397
+ reposWithPolicyDrift: withPolicyDrift.length,
398
+ ...(newFindings ? { newFindings } : {}),
399
+ provenance: { oldestProvenDebt, reposWithNoProvenDebt, perRepoOldestProvenDebt, mttr },
400
+ };
401
+ }
402
+
403
+ /** One-line summary. Leads with failures when there are any. */
404
+ function renderFleetSummary(rollup) {
405
+ if (!rollup) return null;
406
+ const head = rollup.failed
407
+ ? `${rollup.failed} of ${rollup.repos} repo(s) FAILED to scan — their findings are unknown, not zero; `
408
+ : '';
409
+ const sev = SEVERITIES.filter(s => rollup.bySeverity[s]).map(s => `${rollup.bySeverity[s]} ${s}`).join(', ');
410
+ const nf = rollup.newFindings
411
+ ? ` ${Object.values(rollup.newFindings).reduce((a, b) => a + b.length, 0)} new since last run.`
412
+ : '';
413
+ // FR-1006: governance/coverage gaps reported as their own clause, never
414
+ // folded into the risk sentence — a fleet with zero findings but 12 repos
415
+ // on a partial scan is not "clean."
416
+ const gov = [];
417
+ if (rollup.reposWithPartialScanHealth) gov.push(`${rollup.reposWithPartialScanHealth} repo(s) with a partial (not complete) scan`);
418
+ if (rollup.reposWithPolicyDrift) gov.push(`${rollup.reposWithPolicyDrift} repo(s) with policy drift or a rejected policy bundle`);
419
+ const govClause = gov.length ? ` GOVERNANCE: ${gov.join('; ')}.` : '';
420
+ // M4 §4.4: proven-origin debt as its own clause, matching the governance
421
+ // clause's honesty rule — a null `oldestProvenDebt` gets an explicit "none"
422
+ // line rather than silently vanishing from the sentence, which would read
423
+ // as "no debt" instead of "nothing PROVEN yet."
424
+ const prov = rollup.provenance;
425
+ let provClause = '';
426
+ if (prov) {
427
+ // FR-PROV-019: the age never ships without its basis ("proven-origin",
428
+ // structurally guaranteed by the status:'complete' filter upstream) AND
429
+ // its confidence. A finding whose provenance carried no confidence level
430
+ // says so rather than implying certainty by omission.
431
+ const bits = prov.oldestProvenDebt
432
+ ? [`oldest proven-origin finding: ${prov.oldestProvenDebt.repo} (${prov.oldestProvenDebt.ageDays}d, `
433
+ + `${prov.oldestProvenDebt.confidence ? `${String(prov.oldestProvenDebt.confidence).toUpperCase()} confidence` : 'confidence unknown'})`]
434
+ : ['no proven-origin findings across the fleet'];
435
+ if (prov.reposWithNoProvenDebt.length) bits.push(`${prov.reposWithNoProvenDebt.length} repo(s) with no complete-status provenance`);
436
+ provClause = ` PROVENANCE: ${bits.join('; ')}.`;
437
+ }
438
+ // X-707: portfolio progress is its own clause, present only when the `portfolio-assurance` feature attached it
439
+ // (portfolio/progress.js `attachPortfolioProgress`), so flag-off output is byte-identical. It states verified units and
440
+ // repositories separately from the scan counts above: a finished controller is not a passing portfolio.
441
+ const pp = rollup.portfolioProgress;
442
+ const portfolioClause = pp ? ` PORTFOLIO: ${pp.units.verified}/${pp.units.total} unit(s) verified, ${pp.coverage.fullyVerified}/${pp.coverage.total} repo(s) fully verified, ${pp.review.pending} pending human review; ${pp.completion.statement}` : '';
443
+ return `${head}${rollup.scanned} repo(s) scanned, ${rollup.total} finding(s)`
444
+ + `${sev ? ` (${sev})` : ''}, ${rollup.proven} execution-proven.${nf}${govClause}${provClause}${portfolioClause}`;
445
+ }
446
+
447
+ /**
448
+ * A self-contained rollup page. No network, no scripts, no fonts — it must
449
+ * render from local artifacts on an air-gapped machine (PRD AC3).
450
+ */
451
+ function renderFleetHtml(rollup, results) {
452
+ const esc = (s) => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
453
+ // FR-1006: "scan freshness" — WHEN each repo was last scanned, not just
454
+ // whether it was. A failed repo still carries a real scannedAt (the
455
+ // attempt happened; it just didn't succeed) so a reader can tell "failed
456
+ // just now" from "failed, and nobody has retried in weeks" apart.
457
+ const rows = results.map(r => r.ok
458
+ ? `<tr><td>${esc(r.repo)}</td><td>${r.total}</td>`
459
+ + SEVERITIES.map(s => `<td>${r.bySeverity?.[s] ?? 0}</td>`).join('')
460
+ + `<td>${r.proven ?? 0}</td><td>${esc(r.scannedAt || 'unknown')}</td></tr>`
461
+ : `<tr class="fail"><td>${esc(r.repo)}</td><td colspan="7">SCAN FAILED — ${esc(r.error)}</td><td>${esc(r.scannedAt || 'unknown')}</td></tr>`).join('\n');
462
+ // FR-1006: a SEPARATE table, not a column bolted onto the risk table above
463
+ // — "distinguishes risk findings from scan and governance coverage gaps"
464
+ // is most honestly satisfied by keeping the two visually and structurally
465
+ // apart, not by merging them into one row a reader has to parse. Only
466
+ // repos with something to report appear here; a fleet with nothing to flag
467
+ // gets a one-line "nothing to report" rather than an empty table.
468
+ const govRows = results
469
+ .filter(r => (r.scanHealth && r.scanHealth.status !== 'complete') || (r.policyDrift && (r.policyDrift.overrides.length || r.policyDrift.rejected.length)))
470
+ .map(r => {
471
+ const healthCell = r.scanHealth && r.scanHealth.status !== 'complete'
472
+ ? esc(`${r.scanHealth.status}: ${(r.scanHealth.conditions || []).join('; ') || 'see scanHealth'}`)
473
+ : '—';
474
+ const driftParts = [];
475
+ if (r.policyDrift?.overrides?.length) {
476
+ driftParts.push(...r.policyDrift.overrides.map(o => `${esc(o.key)} overridden by ${esc(o.overriddenBy)}`));
477
+ }
478
+ if (r.policyDrift?.rejected?.length) {
479
+ driftParts.push(...r.policyDrift.rejected.map(x => `${esc(x.scope)} policy REJECTED: ${esc(x.reason)}`));
480
+ }
481
+ const driftCell = driftParts.length ? driftParts.join('; ') : '—';
482
+ return `<tr><td>${esc(r.repo)}</td><td>${healthCell}</td><td>${driftCell}</td></tr>`;
483
+ }).join('\n');
484
+ const govSection = govRows
485
+ ? `<h2>Governance / coverage gaps</h2>
486
+ <table border="1" cellpadding="4">
487
+ <tr><th>repo</th><th>scan health</th><th>policy drift</th></tr>
488
+ ${govRows}
489
+ </table>`
490
+ : `<h2>Governance / coverage gaps</h2>
491
+ <p>Nothing to report — every scanned repo has a complete scan and no policy drift.</p>`;
492
+ // M4 §4.4: a SEPARATE section, same precedent as the governance section
493
+ // above — proven-origin debt is neither a risk-severity fact nor a
494
+ // governance-coverage fact, so it gets its own visual and structural space
495
+ // rather than being bolted onto either existing table. Lists the oldest
496
+ // finding PER REPO (not just the single fleet-wide oldest), so a reader can
497
+ // see which repos are carrying old proven debt, not only the single worst
498
+ // one.
499
+ const prov = rollup.provenance;
500
+ let provSection = '';
501
+ if (prov) {
502
+ // FR-PROV-019: the confidence column is not decoration — an age column
503
+ // with no confidence beside it is exactly the "age without its basis and
504
+ // confidence" the PRD forbids. The basis is the table's own framing
505
+ // (every row is proven-origin by construction); the confidence has to be
506
+ // shown per row, and an absent level reads "unknown" rather than blank,
507
+ // so a missing value can never be mistaken for a confident one.
508
+ const provRows = (prov.perRepoOldestProvenDebt || [])
509
+ .map(p => `<tr><td>${esc(p.repo)}</td><td>${esc(p.findingId ?? 'unknown')}</td><td>${esc(p.authorDate)}</td><td>${p.ageDays}</td><td>${esc(p.confidence ? String(p.confidence).toUpperCase() : 'unknown')}</td></tr>`)
510
+ .join('\n');
511
+ const provTable = provRows
512
+ ? `<table border="1" cellpadding="4">
513
+ <tr><th>repo</th><th>finding</th><th>author date</th><th>age (days)</th><th>origin confidence</th></tr>
514
+ ${provRows}
515
+ </table>`
516
+ : `<p>No complete-status (proven-origin) findings across the fleet.</p>`;
517
+ const noProvenNote = prov.reposWithNoProvenDebt.length
518
+ ? `<p>${prov.reposWithNoProvenDebt.length} repo(s) with no complete-status provenance: ${prov.reposWithNoProvenDebt.map(esc).join(', ')}</p>`
519
+ : '';
520
+ const mttrByBasisRows = Object.entries(prov.mttr?.byAgeBasis || {})
521
+ .map(([basis, n]) => `<tr><td>${esc(basis)}</td><td>${n}</td></tr>`).join('\n');
522
+ const mttrByBasisTable = mttrByBasisRows
523
+ ? `<table border="1" cellpadding="4">
524
+ <tr><th>age basis</th><th>count</th></tr>
525
+ ${mttrByBasisRows}
526
+ </table>`
527
+ : '';
528
+ // Three distinct states, not two (final-review fix, item 4): `prov.mttr
529
+ // === null` means no scanned repo ever supplied a real mttr.js aggregate
530
+ // at all — the honest disclosure is "not available", never "no
531
+ // remediated findings recorded yet", which reads as a real zero
532
+ // measurement rather than an absent capability. A non-null `mttr` with
533
+ // `n === 0` is the legitimate zero: something DID track remediation and
534
+ // genuinely found none yet.
535
+ const mttrLine = !prov.mttr
536
+ ? `<p>Fleet MTTR: not available — fleet mode does not yet track finding remediation history across runs.</p>`
537
+ : prov.mttr.n > 0
538
+ ? `<p>Fleet MTTR: ${prov.mttr.n} remediated finding(s)${prov.mttr.meanDays != null ? `, mean ${Math.round(prov.mttr.meanDays)}d to remediate` : ''}.</p>${mttrByBasisTable}`
539
+ : `<p>Fleet MTTR: no remediated findings recorded yet.</p>`;
540
+ provSection = `<h2>Provenance-Proven Debt</h2>
541
+ ${provTable}
542
+ ${noProvenNote}
543
+ ${mttrLine}`;
544
+ }
545
+ return `<!-- generated offline; no external resources -->
546
+ <h1>Fleet scan</h1>
547
+ <p>${esc(renderFleetSummary(rollup) || '')}</p>
548
+ <h2>Risk findings</h2>
549
+ <table border="1" cellpadding="4">
550
+ <tr><th>repo</th><th>total</th>${SEVERITIES.map(s => `<th>${s}</th>`).join('')}<th>proven</th><th>scanned at</th></tr>
551
+ ${rows}
552
+ </table>
553
+ ${govSection}
554
+ ${provSection}`;
555
+ }
556
+
557
+ const _internals = { SCHEMA, SEVERITIES, _matchesGlob, _summarise, _provenanceDebtForRepo };
558
+
559
+
560
+ /***/ }),
561
+
562
+ /***/ 25620:
563
+ /***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
564
+
565
+ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
566
+ __webpack_require__.r(__webpack_exports__);
567
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
568
+ /* harmony export */ cmdFleet: () => (/* binding */ cmdFleet),
569
+ /* harmony export */ parseFleetQuestion: () => (/* binding */ parseFleetQuestion)
570
+ /* harmony export */ });
571
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
572
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
573
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(77598);
574
+ /* harmony import */ var _fleet_manifest_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(62790);
575
+ /* harmony import */ var _fleet_graph_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(94355);
576
+ /* harmony import */ var _fleet_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(80313);
577
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(31174);
578
+ /* harmony import */ var _job_store_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(20121);
579
+ /* harmony import */ var _job_runner_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(89072);
580
+ /* harmony import */ var _org_health_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(75435);
581
+ /* harmony import */ var _discover_edges_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(89632);
582
+ /* harmony import */ var _enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(81630);
583
+ var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_job_runner_js__WEBPACK_IMPORTED_MODULE_8__]);
584
+ _job_runner_js__WEBPACK_IMPORTED_MODULE_8__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
585
+ // `agentic-security fleet init|scan|status|resume|query|explore`.
586
+ //
587
+ // The scanning half is the job fabric: a manifest becomes one idempotent job per repository in a local SQLite store, workers lease
588
+ // them, every scan runs in a supervised child under a hard timeout, and `resume` continues a run after anything stops it. The
589
+ // per-repository entry and the roll-up are the existing `posture/fleet.js` ones (counts, owners, scan health, delta), so a fleet
590
+ // report reads as it always has, with the organization's health and the enterprise graph added on top.
591
+ //
592
+ // Exit codes: 0 the organization is GREEN, 1 it is not (a required repository has no result, or a result is partial), 4 usage.
593
+
594
+
595
+
596
+
597
+
598
+
599
+
600
+
601
+
602
+
603
+
604
+
605
+
606
+ const GRAPH_FILE = 'fleet-graph.json';
607
+ const ENTERPRISE_FILE = node_path__WEBPACK_IMPORTED_MODULE_1__.join('fleet', 'enterprise-graph.json');
608
+ const DB_FILE = node_path__WEBPACK_IMPORTED_MODULE_1__.join('fleet', 'fleet.db');
609
+ const ROLLUP_FILE = 'fleet-rollup.json';
610
+ const sha = (s) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_2__.createHash)('sha256').update(s).digest('hex');
611
+
612
+ const usage = () => { console.error('usage: fleet init | edges [--undeclared] | scan <manifest.yaml> [--workers N] [--retries N] [--timeout-seconds N | --timeout-ms N] [--no-cache] | status [run-id] | resume <run-id|manifest.yaml> [--retry-failed] | query (--depends-on <id> | --blast-radius <component> [--version v] | --internet-to <data-class> [--involving-vulnerable auth|<repo>] | "<question>") | explore [--out file.html]'); return 4; };
613
+ const isManifestArg = (s) => /\.ya?ml$/i.test(String(s ?? ''));
614
+ const manifestArg = (args) => { const m = args._[2]; return m && !String(m).startsWith('--') ? m : 'fleet.yaml'; };
615
+ const dbPath = (root) => (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, DB_FILE);
616
+
617
+ async function cmdFleet(args, root) {
618
+ const sub = args._[1];
619
+ if (sub === 'init') return cmdInit(args, root);
620
+ if (sub === 'scan') return cmdScan(args, root);
621
+ if (sub === 'status') return cmdStatus(args, root);
622
+ if (sub === 'resume') return cmdResume(args, root);
623
+ if (sub === 'query') return cmdQuery(args, root);
624
+ if (sub === 'explore') return cmdExplore(args, root);
625
+ if (sub === 'edges') return cmdEdges(args, root);
626
+ return usage();
627
+ }
628
+
629
+ /** `fleet edges [--undeclared] [--json]`: every edge of the last enterprise graph with the evidence it rests on; --undeclared keeps the ones found only in code. */
630
+ async function cmdEdges(args, root) {
631
+ const f = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ENTERPRISE_FILE);
632
+ if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(f)) { console.error('No enterprise graph yet. Run `agentic-security fleet scan <manifest.yaml>` first.'); return 4; }
633
+ const g = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(f, 'utf8'));
634
+ const edges = g.edges.filter((e) => e.type !== 'exposes' && (!args.flags.undeclared || e.discovered));
635
+ if (args.flags.json) { console.log(JSON.stringify({ discovery: g.discovery ?? null, edges }, null, 2)); return 0; }
636
+ if (!edges.length) { console.log(args.flags.undeclared ? 'No edge was found in code that the manifest does not declare.' : 'The graph has no edges.'); }
637
+ for (const e of edges) {
638
+ const where = e.evidence.filter((x) => x.source === 'discovered-code-literal').map((x) => `${x.file}:${x.line}`);
639
+ console.log(`${e.from} -[${e.type}]-> ${e.to} ${e.discovered ? 'FOUND ONLY IN CODE' : where.length ? 'declared, and seen in code' : 'declared'}${where.length ? ` (${where.join(', ')})` : ''}`);
640
+ }
641
+ if (g.discovery?.limitations) console.log(`\nLimits: ${g.discovery.limitations.join(' ')}`);
642
+ else if (g.discovery?.note) console.log(`\nEdge discovery: ${g.discovery.note}.`);
643
+ return 0;
644
+ }
645
+
646
+ async function cmdInit(args, root) {
647
+ const target = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, manifestArg(args) === 'fleet.yaml' ? 'fleet.yaml' : manifestArg(args));
648
+ if (node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(target) && !args.flags.force) { console.error(`${target} already exists. Pass --force to overwrite.`); return 4; }
649
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(target, (0,_fleet_manifest_js__WEBPACK_IMPORTED_MODULE_3__/* .starterManifestYaml */ .yr)(args.flags.organization || 'example'));
650
+ console.log(`Wrote ${target}. Edit it to list your repositories, then run: agentic-security fleet scan ${node_path__WEBPACK_IMPORTED_MODULE_1__.basename(target)}`);
651
+ return 0;
652
+ }
653
+
654
+ function overrides(manifest, args) {
655
+ const m = { ...manifest };
656
+ const num = (v, lo, hi) => { const n = Number(v); return Number.isInteger(n) && n >= lo && n <= hi ? n : null; };
657
+ if (args.flags.workers !== undefined) { const n = num(args.flags.workers, 1, 32); if (n == null) return { error: '--workers must be an integer from 1 to 32' }; m.workers = n; }
658
+ if (args.flags.retries !== undefined) { const n = num(args.flags.retries, 0, 5); if (n == null) return { error: '--retries must be an integer from 0 to 5' }; m.retries = n; }
659
+ if (args.flags['timeout-ms'] !== undefined) { const n = num(args.flags['timeout-ms'], 50, 86400000); if (n == null) return { error: '--timeout-ms must be an integer from 50 to 86400000' }; m.perRepoTimeoutMs = n; }
660
+ if (args.flags['timeout-seconds'] !== undefined) { const n = num(args.flags['timeout-seconds'], 1, 86400); if (n == null) return { error: '--timeout-seconds must be an integer from 1 to 86400' }; m.perRepoTimeoutMs = n * 1000; }
661
+ return { manifest: m };
662
+ }
663
+
664
+ function entriesOf(artifacts, manifest) {
665
+ return manifest.repositories.filter((r) => artifacts.has(r.id)).map((r) => ({ ...artifacts.get(r.id).entry, repo: r.id }));
666
+ }
667
+
668
+ async function finishRun({ args, root, store, runId, manifest, manifestFile, interrupted }) {
669
+ const { artifacts, problems } = (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .loadArtifacts */ .kZ)({ store, runId, root });
670
+ const jobs = store.listJobs(runId);
671
+ const health = (0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .computeOrgHealth */ .WB)({ manifest, jobs });
672
+ for (const p of problems) { const r = health.repositories.find((x) => x.id === p.repoId); if (r) { r.state = 'failed'; r.error = p.problem; } }
673
+ if (problems.length) { health.counts.complete -= problems.filter((p) => health.repositories.find((x) => x.id === p.repoId)).length; health.counts.failed += problems.length; if (health.assurance !== 'red' && manifest.repositories.some((r) => r.required && problems.some((p) => p.repoId === r.id))) health.assurance = 'red'; health.reasons.push(`${problems.length} result(s) could not be verified: ${problems.map((p) => `${p.repoId} (${p.problem})`).join(', ')}`); }
674
+
675
+ let previous = null;
676
+ const rollupFile = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ROLLUP_FILE);
677
+ try { previous = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(rollupFile, 'utf8')); } catch { /* first run */ }
678
+ const rollup = (0,_fleet_js__WEBPACK_IMPORTED_MODULE_5__/* .rollupFleet */ .JQ)(entriesOf(artifacts, manifest).concat(health.repositories.filter((r) => !artifacts.has(r.id) && r.state !== 'complete' && r.state !== 'partial').map((r) => ({ repo: r.id, ok: false, error: r.error || r.state, total: null, bySeverity: null, ids: [], scannedAt: new Date().toISOString(), scanHealth: null, policyDrift: null, provenanceDebt: null, mttr: null }))), previous);
679
+ (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.safeWriteState)(rollupFile, JSON.stringify(rollup, null, 2));
680
+
681
+ const dep = (0,_fleet_graph_js__WEBPACK_IMPORTED_MODULE_4__/* .buildFleetDependencyGraph */ .u_)(manifest.repositories);
682
+ (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.safeWriteState)((0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, GRAPH_FILE), JSON.stringify({ organization: manifest.organization, builtAt: new Date().toISOString(), ...dep }, null, 2));
683
+ // edges the code implies; skipped with --no-discover. A failure here never fails the run: the graph is then declared-only and says so.
684
+ let discovered = null; let discoveryNote = args.flags['no-discover'] ? 'skipped (--no-discover)' : null;
685
+ if (!args.flags['no-discover']) { try { discovered = (0,_discover_edges_js__WEBPACK_IMPORTED_MODULE_10__/* .discoverEdges */ .U)({ manifest }); } catch (e) { discoveryNote = `failed (${String(e.message).slice(0, 80)}); the graph holds declared edges only`; } }
686
+ const split = discovered ? (0,_discover_edges_js__WEBPACK_IMPORTED_MODULE_10__/* .splitDeclared */ .Z)(discovered, manifest) : null;
687
+ const graph = (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .finalizeGraph */ .Th)((0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .buildEnterpriseGraph */ .Ws)({ manifest, dependencyGraph: dep, discovered }));
688
+ const findingsByRepo = Object.fromEntries([...artifacts].map(([id, a]) => [id, a.findings ?? []]));
689
+ (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.safeWriteState)((0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ENTERPRISE_FILE), JSON.stringify({ ...graph, runId, manifestFile, findingsByRepo, discovery: discovered ? { scanned: discovered.scanned, limitations: discovered.limitations, undeclared: split.undeclared.length, confirmed: split.confirmed.length } : { note: discoveryNote }, builtAt: new Date().toISOString() }));
690
+
691
+ if (args.flags.json) console.log(JSON.stringify({ runId, health, rollup }, null, 2));
692
+ else {
693
+ console.log((0,_fleet_js__WEBPACK_IMPORTED_MODULE_5__/* .renderFleetSummary */ .LL)(rollup));
694
+ console.log(`\nRun: ${runId}\n${(0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .renderOrgHealth */ .Vp)(health)}`);
695
+ if (split) console.log(`\nEdges found in code: ${split.undeclared.length} not in the manifest, ${split.confirmed.length} matching a declared edge. ${split.undeclared.length ? 'List them with: agentic-security fleet edges --undeclared' : ''}`.trimEnd());
696
+ else if (discoveryNote) console.log(`\nEdge discovery ${discoveryNote}.`);
697
+ if (interrupted) console.log(`\nInterrupted. Continue with: agentic-security fleet resume ${runId}`);
698
+ else if (health.counts.failed || health.counts.timedOut || health.counts.notRun) console.log(`\nRetry the failed and timed-out repositories with: agentic-security fleet resume ${runId} --retry-failed`);
699
+ }
700
+ return health.assurance === 'green' && !interrupted ? 0 : 1;
701
+ }
702
+
703
+ async function runAll({ args, root, store, runId, manifest, manifestFile }) {
704
+ const ac = new AbortController(); const onSig = () => ac.abort(); process.once('SIGINT', onSig); process.once('SIGTERM', onSig);
705
+ try {
706
+ await (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .runJobs */ .Ag)({ manifest, store, runId, root, scan: _job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .childScan */ .IT, signal: ac.signal, useCache: !args.flags['no-cache'], onEvent: (e) => {
707
+ if (args.flags.json) return;
708
+ if (e.type === 'done') console.error(`[fleet] done: ${e.repo}${e.cached ? ' (cached)' : ''}${e.status === 'partial' ? ' (partial)' : ''}`);
709
+ else if (e.type === 'failed') console.error(`[fleet] ${e.timedOut ? 'TIMED OUT' : 'FAILED'}: ${e.repo} (${e.error})${e.next === 'queued' ? ', will retry' : ''}`);
710
+ else if (e.type === 'fenced') console.error(`[fleet] ${e.repo}: result discarded, another worker holds the job`);
711
+ } });
712
+ return await finishRun({ args, root, store, runId, manifest, manifestFile, interrupted: ac.signal.aborted });
713
+ } finally { process.off('SIGINT', onSig); process.off('SIGTERM', onSig); }
714
+ }
715
+
716
+ async function cmdScan(args, root) {
717
+ const manifestFile = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, manifestArg(args));
718
+ const loaded = (0,_fleet_manifest_js__WEBPACK_IMPORTED_MODULE_3__/* .loadFleetManifest */ .Cb)(manifestFile);
719
+ if (!loaded.ok) { console.error(`Invalid fleet manifest: ${loaded.errors.join('; ')}`); return 4; }
720
+ const o = overrides(loaded.manifest, args);
721
+ if (o.error) { console.error(o.error); return 4; }
722
+ const { manifest } = o;
723
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.ensureStateDir)(root)) { console.error('No safe state directory for this project.'); return 4; }
724
+ const store = await (0,_job_store_js__WEBPACK_IMPORTED_MODULE_7__/* .createSqliteJobStore */ .J6)({ file: dbPath(root) });
725
+ try {
726
+ const manifestHash = sha(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(manifestFile));
727
+ console.error(`[fleet] planning ${manifest.repositories.length} repository(ies) for ${manifest.organization} (${manifest.workers} worker(s), ${manifest.retries} retr${manifest.retries === 1 ? 'y' : 'ies'}, ${Math.round(manifest.perRepoTimeoutMs / 1000)}s per repository)...`);
728
+ const plan = (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .planRun */ .nY)({ manifest, manifestHash, store, meta: { manifestFile } });
729
+ return await runAll({ args, root, store, runId: plan.runId, manifest, manifestFile });
730
+ } finally { store.close(); }
731
+ }
732
+
733
+ async function cmdResume(args, root) {
734
+ const target = String(args._[2] ?? '');
735
+ if (!target) return usage();
736
+ const dbFile = dbPath(root);
737
+ if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(dbFile)) { console.error('No fleet run exists in this project. Start one with: agentic-security fleet scan <manifest.yaml>'); return 4; }
738
+ const store = await (0,_job_store_js__WEBPACK_IMPORTED_MODULE_7__/* .createSqliteJobStore */ .J6)({ file: dbFile });
739
+ try {
740
+ let run;
741
+ if (isManifestArg(target)) {
742
+ const hash = sha(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, target)));
743
+ run = store.listRuns().find((r) => r.manifestHash === hash && store.listJobs(r.runId).some((j) => ['queued', 'leased'].includes(j.status)));
744
+ if (!run) { console.error('Nothing to resume for that manifest: no run of it has unfinished work. Use `fleet resume <run-id> --retry-failed` to retry failed repositories.'); return 4; }
745
+ } else run = store.getRun(target);
746
+ if (!run) { console.error(`No run ${target}. Runs: ${store.listRuns().slice(0, 5).map((r) => r.runId).join(', ') || 'none'}`); return 4; }
747
+ const full = store.getRun(run.runId);
748
+ const manifestFile = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, args.flags.manifest ?? full.meta?.manifestFile ?? (isManifestArg(target) ? target : 'fleet.yaml'));
749
+ const loaded = (0,_fleet_manifest_js__WEBPACK_IMPORTED_MODULE_3__/* .loadFleetManifest */ .Cb)(manifestFile);
750
+ if (!loaded.ok) { console.error(`Invalid fleet manifest: ${loaded.errors.join('; ')}`); return 4; }
751
+ const hash = sha(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(manifestFile));
752
+ if (hash !== full.manifestHash) { console.error(`The manifest ${node_path__WEBPACK_IMPORTED_MODULE_1__.basename(manifestFile)} has changed since run ${full.runId} was planned. A resumed run must scan what it planned; start a new scan instead.`); return 4; }
753
+ const o = overrides(loaded.manifest, args);
754
+ if (o.error) { console.error(o.error); return 4; }
755
+ const freed = (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .reclaimDeadWorkers */ .zD)(store, full.runId) + store.reclaimExpired(full.runId, { now: Date.now() });
756
+ const requeued = args.flags['retry-failed'] ? store.requeueTerminal(full.runId, ['failed', 'timed_out']) : 0;
757
+ // a limit given on the command line applies to the work still to do, including the work just re-queued
758
+ if (args.flags['timeout-ms'] !== undefined || args.flags['timeout-seconds'] !== undefined) store.retime(full.runId, o.manifest.perRepoTimeoutMs);
759
+ const open = store.listJobs(full.runId).filter((j) => ['queued', 'leased'].includes(j.status)).length;
760
+ console.error(`[fleet] resuming ${full.runId}: ${open} unfinished, ${freed} lease(s) recovered${requeued ? `, ${requeued} failed repository(ies) re-queued` : ''}; finished results are reused.`);
761
+ return await runAll({ args, root, store, runId: full.runId, manifest: o.manifest, manifestFile });
762
+ } finally { store.close(); }
763
+ }
764
+
765
+ async function cmdStatus(args, root) {
766
+ const dbFile = dbPath(root);
767
+ if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(dbFile)) {
768
+ const f = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ROLLUP_FILE);
769
+ if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(f)) { console.error('No fleet scan has been run yet. Run `agentic-security fleet scan <manifest.yaml>` first.'); return 4; }
770
+ const rollup = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(f, 'utf8'));
771
+ console.log(args.flags.json ? JSON.stringify(rollup, null, 2) : (0,_fleet_js__WEBPACK_IMPORTED_MODULE_5__/* .renderFleetSummary */ .LL)(rollup));
772
+ return 0;
773
+ }
774
+ const store = await (0,_job_store_js__WEBPACK_IMPORTED_MODULE_7__/* .createSqliteJobStore */ .J6)({ file: dbFile });
775
+ try {
776
+ const arg = args._[2];
777
+ const run = arg && !isManifestArg(arg) ? store.getRun(String(arg)) : store.listRuns()[0] && store.getRun(store.listRuns()[0].runId);
778
+ if (!run) { console.error(arg && !isManifestArg(arg) ? `No run ${arg}.` : 'No fleet run exists yet.'); return 4; }
779
+ const meta = run.meta ?? {};
780
+ const manifest = (0,_fleet_manifest_js__WEBPACK_IMPORTED_MODULE_3__/* .loadFleetManifest */ .Cb)(node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, meta.manifestFile ?? 'fleet.yaml'));
781
+ const jobs = store.listJobs(run.runId);
782
+ const m = manifest.ok ? manifest.manifest : { repositories: jobs.map((j) => ({ id: j.repoId, required: j.required })) };
783
+ const health = (0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .computeOrgHealth */ .WB)({ manifest: m, jobs });
784
+ if (args.flags.json) { console.log(JSON.stringify({ runId: run.runId, health, jobs: jobs.map((j) => ({ repo: j.repoId, status: j.status, attempts: j.attempts, worker: j.worker, leaseExpiresAt: j.leaseExpiresAt, error: j.error })) }, null, 2)); return 0; }
785
+ console.log(`Run: ${run.runId} (${run.organization})\n${(0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .renderOrgHealth */ .Vp)(health)}`);
786
+ const live = jobs.filter((j) => j.status === 'leased');
787
+ if (live.length) console.log(`\nIn progress: ${live.map((j) => `${j.repoId} (attempt ${j.attempts}, lease until ${new Date(j.leaseExpiresAt).toISOString()})`).join(', ')}`);
788
+ const f = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ROLLUP_FILE);
789
+ if (node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(f)) console.log(`\n${(0,_fleet_js__WEBPACK_IMPORTED_MODULE_5__/* .renderFleetSummary */ .LL)(JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(f, 'utf8')))}`);
790
+ return 0;
791
+ } finally { store.close(); }
792
+ }
793
+
794
+ // "Show every Internet-facing path to PCI data involving vulnerable authentication code" is recognised as a fixed shape and mapped
795
+ // to the structured query below. Anything else is refused with the flags to use; this is not a natural-language engine.
796
+ function parseFleetQuestion(text) {
797
+ const m = /^\s*show\s+every\s+internet-facing\s+path\s+to\s+([a-z][a-z0-9-]*)\s+data(?:\s+involving\s+vulnerable\s+([a-z][a-z0-9-]*)\s+(?:code|components?|librar(?:y|ies)))?\s*\.?\s*$/i.exec(String(text ?? ''));
798
+ if (!m) return null;
799
+ const word = m[2]?.toLowerCase() ?? null;
800
+ return { toDataClass: m[1].toLowerCase(), involving: word ? { family: word === 'authentication' || word === 'authorization' ? 'auth' : word } : null };
801
+ }
802
+
803
+ function readEnterprise(root) {
804
+ const f = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ENTERPRISE_FILE);
805
+ if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(f)) return null;
806
+ try { return JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(f, 'utf8')); } catch { return null; }
807
+ }
808
+
809
+ async function cmdQuery(args, root) {
810
+ const flags = args.flags;
811
+ const question = args._[2] && !isManifestArg(args._[2]) ? String(args._[2]) : null;
812
+ const parsed = question ? parseFleetQuestion(question) : null;
813
+ if (question && !parsed) { console.error(`Not a recognised question. This command answers one shape: "Show every Internet-facing path to <data class> data [involving vulnerable <authentication|family> code]". For anything else use --depends-on, --blast-radius, or --internet-to with --involving-vulnerable.`); return 4; }
814
+
815
+ if (flags['depends-on']) {
816
+ const f = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, GRAPH_FILE);
817
+ if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(f)) { console.error('No fleet dependency graph yet. Run `agentic-security fleet scan <manifest.yaml>` first.'); return 4; }
818
+ const graph = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(f, 'utf8'));
819
+ const target = String(flags['depends-on']);
820
+ if (!graph.nodes.some((n) => n.id === target)) { console.error(`${target} is not a repository in the last fleet scan.`); return 4; }
821
+ const radius = (0,_fleet_graph_js__WEBPACK_IMPORTED_MODULE_4__/* .blastRadius */ .X2)(graph, target);
822
+ if (flags.json) { console.log(JSON.stringify({ target, blastRadius: radius }, null, 2)); return 0; }
823
+ if (!radius.length) { console.log(`No fleet repository depends on ${target} (by declared package name).`); return 0; }
824
+ console.log(`Repositories depending on ${target} (directly or transitively):`);
825
+ for (const r of radius) console.log(` ${r.id} (depth ${r.depth}, via ${r.path.slice(1).join(' -> ')})`);
826
+ return 0;
827
+ }
828
+
829
+ const ent = readEnterprise(root);
830
+ if (!ent) { console.error('No enterprise graph yet. Run `agentic-security fleet scan <manifest.yaml>` first.'); return 4; }
831
+ const bad = (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .validateEnterpriseGraph */ .cl)(ent);
832
+ if (bad.length) { console.error(`The stored enterprise graph is inconsistent (${bad[0]}). Re-run the scan.`); return 1; }
833
+ const findings = new Map(Object.entries(ent.findingsByRepo ?? {}));
834
+
835
+ if (flags['blast-radius']) {
836
+ const r = (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .componentBlastRadius */ ._o)({ graph: ent, component: String(flags['blast-radius']), version: flags.version ? String(flags.version) : null });
837
+ if (!r) { console.error(`${flags['blast-radius']} is not in the last fleet scan.`); return 4; }
838
+ console.log(flags.json ? JSON.stringify(r, null, 2) : (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .renderBlastRadius */ .lB)(r));
839
+ return 0;
840
+ }
841
+
842
+ const toClass = parsed?.toDataClass ?? flags['internet-to'];
843
+ if (toClass) {
844
+ const sel = parsed?.involving ?? (flags['involving-vulnerable'] ? (/^[a-z][a-z0-9-]*$/.test(String(flags['involving-vulnerable'])) && ent.nodes.some((n) => n.id === flags['involving-vulnerable']) ? { repo: String(flags['involving-vulnerable']) } : { family: String(flags['involving-vulnerable']) }) : null);
845
+ const involving = sel ? (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .vulnerableSet */ .r8)({ graph: ent, findingsByRepo: findings, selector: sel }) : null;
846
+ const res = (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .attackPaths */ .Nn)({ graph: ent, toDataClass: String(toClass).toLowerCase(), involving });
847
+ if (flags.json) { console.log(JSON.stringify({ ...res, involving: involving ? { direct: [...involving.direct], all: [...involving.all], why: Object.fromEntries(involving.why) } : null }, null, 2)); return 0; }
848
+ if (!res.targets.length) { console.log(`No node in the fleet manifest is marked as holding ${toClass} data, so there is nothing to reach.`); return 0; }
849
+ if (!res.paths.length) console.log(`No Internet-facing path to ${toClass} data${involving ? ' involving that vulnerable code' : ''} was found in the declared graph.${res.truncated ? ' (search bound reached)' : ''}`);
850
+ for (const p of res.paths) {
851
+ console.log(p.nodes.map((n) => (n === _enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .INTERNET */ .aw ? 'Internet' : n)).join(' -> '));
852
+ if (p.discoveredEdges?.length) console.log(` rests on edge(s) found only in code, not declared: ${p.discoveredEdges.join(', ')}`);
853
+ if (p.involving.length) console.log(` involves vulnerable code: ${p.involving.map((n) => `${n} (${involving.why.get(n)})`).join(', ')}`);
854
+ }
855
+ if (res.paths.length) console.log(`\n${res.paths.length} path(s). Reach edges are as declared in the fleet manifest, plus any marked above as found only in code.${res.truncated ? ' The search bound was reached; more may exist.' : ''}`);
856
+ return 0;
857
+ }
858
+ return usage();
859
+ }
860
+
861
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
862
+ async function cmdExplore(args, root) {
863
+ const ent = readEnterprise(root);
864
+ if (!ent) { console.error('No enterprise graph yet. Run `agentic-security fleet scan <manifest.yaml>` first.'); return 4; }
865
+ const rows = ent.edges.map((e) => `<tr><td>${esc(e.from)}</td><td>${esc(e.type)}${e.reach ? '' : ' (dependency)'}</td><td>${esc(e.to)}</td><td>${e.evidence.map((x) => esc(`${x.source}: ${x.repo ?? ''} ${x.field ?? x.declared ?? ''} ${x.value ?? x.spec ?? ''}`.trim())).join('<br>')}</td></tr>`).join('\n');
866
+ const nodes = ent.nodes.map((n) => `<tr><td>${esc(n.id)}</td><td>${esc(n.type)}</td><td>${n.internetFacing ? 'yes' : ''}</td><td>${esc(n.environment ?? '')}</td><td>${esc(n.dataClasses.join(', '))}</td><td>${esc(n.trustZone ?? '')}</td></tr>`).join('\n');
867
+ const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Fleet graph</title><style>body{font:14px/1.5 system-ui,sans-serif;max-width:70rem;margin:2rem auto;padding:0 1rem}table{border-collapse:collapse;margin:.5rem 0 2rem;width:100%}td,th{border:1px solid #8884;padding:.25rem .6rem;text-align:left;vertical-align:top}</style></head><body><h1>${esc(ent.organization)} fleet graph</h1><p>Run ${esc(ent.runId)}. Reach edges are as declared in the fleet manifest; dependency edges come from package manifests. Every edge lists its evidence.</p><h2>Nodes</h2><table><tr><th>id</th><th>type</th><th>internet-facing</th><th>environment</th><th>data</th><th>trust zone</th></tr>${nodes}</table><h2>Edges</h2><table><tr><th>from</th><th>type</th><th>to</th><th>evidence</th></tr>${rows}</table></body></html>\n`;
868
+ const out = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, String(args.flags.out ?? 'fleet-graph.html'));
869
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(out, html);
870
+ console.log(`Wrote ${out} (${ent.nodes.length} nodes, ${ent.edges.length} edges). A static page, no server.`);
871
+ return 0;
872
+ }
873
+
874
+ __webpack_async_result__();
875
+ } catch(e) { __webpack_async_result__(e); } });
876
+
877
+ /***/ }),
878
+
879
+ /***/ 89632:
880
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
881
+
882
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
883
+ /* harmony export */ U: () => (/* binding */ discoverEdges),
884
+ /* harmony export */ Z: () => (/* binding */ splitDeclared)
885
+ /* harmony export */ });
886
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
887
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
888
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
889
+ // Edges the code implies but the manifest does not declare (PRD 4: the enterprise graph should not be only what someone wrote down).
890
+ //
891
+ // What this does: reads the text of each repository's source and configuration files and looks for a few exact, checkable
892
+ // patterns that name ANOTHER node of the manifest:
893
+ // * a URL literal whose host IS a node's id or alias, or whose first label is (payments-api matches payments-api.svc.cluster.local);
894
+ // * a queue/topic resource's name or alias as a string literal on a line that publishes to it, or on one that consumes from it;
895
+ // * a database resource's name or alias on a line that opens a connection.
896
+ // Each hit becomes an edge of type `calls`, `produces`, `consumes` or `connects-to` with the file and line that produced it, tagged
897
+ // `source: discovered-code-literal`, and is reported separately from declared edges: a discovered edge never replaces or hides a
898
+ // declared one, and a query result says which of its edges were only discovered.
899
+ //
900
+ // What it cannot do, and says so: it matches whole tokens, not substrings (so `orders` never matches `reorders`); it sees literals, not
901
+ // values built at run time, environment-supplied URLs or service-mesh routing; it cannot tell the direction of a database use; it
902
+ // skips comment lines but does not parse code, so a literal in a string that is never executed still counts. A discovered edge is a
903
+ // lead with evidence, not a fact. It is bounded in files, bytes and time.
904
+
905
+
906
+
907
+
908
+ const DISCOVERY_LIMITS = Object.freeze({ maxFilesPerRepo: 2000, maxFileBytes: 512 * 1024, maxTotalBytes: 64 * 1024 * 1024, maxEvidencePerEdge: 3 });
909
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor', '.venv', 'venv', '__pycache__', 'target', _state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .STATE_DIR_NAME */ .Ky, 'coverage', '.next', 'dist-newstyle', '.stack-work']);
910
+ const TEXT_EXT = /\.(?:js|mjs|cjs|jsx|ts|tsx|py|java|kt|go|rb|php|cs|rs|scala|ya?ml|json|toml|properties|env|tf|conf|ini|hs|nix)$/i;
911
+ const COMMENT = /^\s*(?:\/\/|#|\*|\/\*|--|;)/;
912
+ const URL_RE = /\b[a-z][a-z0-9+.-]{1,12}:\/\/[^\s'"`<>)\]}]{1,300}/gi;
913
+ const PUBLISH = /\b(?:send_?message|publish|produce|send_?to_?queue|enqueue|put_?record|xadd|lpush|rpush|sendmessage|sendtoqueue|emit_?to)\b/i;
914
+ const CONSUME = /\b(?:consume|subscribe|receive_?message|poll|on_?message|get_?messages?|basic_?consume|xread|brpop|blpop|receivemessage)\b/i;
915
+ const CONNECT = /(?:\b(?:connect|create_?connection|createpool|new\s+pool|mongoclient|drivermanager|sqlalchemy|create_engine|database_url|dbname)\b|jdbc:|postgres(?:ql)?:\/\/|mysql:\/\/|mongodb(?:\+srv)?:\/\/|redis:\/\/)/i;
916
+
917
+ /** The names a node answers to: its id plus declared aliases, lower-case. */
918
+ function namesOf(node) { return [node.id, ...(node.aliases ?? [])].map((x) => String(x).toLowerCase()); }
919
+
920
+ function* sourceFiles(root, limits) {
921
+ let count = 0; let bytes = 0;
922
+ const stack = ['']; const rootReal = node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(root);
923
+ while (stack.length) {
924
+ const rel = stack.pop();
925
+ let ents; try { ents = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(root, rel), { withFileTypes: true }); } catch { continue; }
926
+ for (const e of ents.sort((a, b) => (a.name < b.name ? -1 : 1))) {
927
+ if (e.isSymbolicLink()) continue;
928
+ const r = rel ? `${rel}/${e.name}` : e.name;
929
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(r); continue; }
930
+ if (!e.isFile() || !TEXT_EXT.test(e.name)) continue;
931
+ let st; try { st = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(root, r)); } catch { continue; }
932
+ if (st.size > limits.maxFileBytes) continue;
933
+ if (++count > limits.maxFilesPerRepo || (bytes += st.size) > limits.maxTotalBytes) return;
934
+ let real; try { real = node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(root, r)); } catch { continue; }
935
+ if (!real.startsWith(rootReal + node_path__WEBPACK_IMPORTED_MODULE_1__.sep)) continue;
936
+ yield { rel: r, abs: real };
937
+ }
938
+ }
939
+ }
940
+
941
+ const stringLiterals = (line) => [...line.matchAll(/(['"`])((?:\\.|(?!\1).){1,200})\1/g)].map((m) => m[2]);
942
+ const tokenIn = (haystackLiteral, name) => new RegExp(`(?:^|[^a-z0-9_-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:$|[^a-z0-9_-])`, 'i').test(haystackLiteral);
943
+
944
+ /**
945
+ * @param {{manifest: object, limits?: object}} o
946
+ * @returns {{edges: Array<{type: string, from: string, to: string, evidence: object[]}>, scanned: Record<string, {files: number, truncated: boolean}>, limitations: string[]}}
947
+ */
948
+ function discoverEdges({ manifest, limits = DISCOVERY_LIMITS }) {
949
+ const L = { ...DISCOVERY_LIMITS, ...limits };
950
+ const repoNodes = manifest.repositories.map((r) => ({ id: r.id, aliases: r.aliases ?? [], kind: 'repo' }));
951
+ const resNodes = (manifest.resources ?? []).map((r) => ({ id: r.id, aliases: r.aliases ?? [], kind: r.kind }));
952
+ const found = new Map(); const scanned = {};
953
+ const add = (type, from, to, ev) => {
954
+ const key = `${type}\u0000${from}\u0000${to}`;
955
+ const e = found.get(key) ?? { type, from, to, evidence: [] };
956
+ if (e.evidence.length < L.maxEvidencePerEdge) e.evidence.push({ source: 'discovered-code-literal', repo: from, declared: false, ...ev });
957
+ found.set(key, e);
958
+ };
959
+ for (const repo of manifest.repositories) {
960
+ let files = 0; let truncated = false;
961
+ const targets = [...repoNodes.filter((n) => n.id !== repo.id), ...resNodes];
962
+ const it = sourceFiles(repo.path, L);
963
+ for (;;) {
964
+ const nx = it.next();
965
+ if (nx.done) break;
966
+ files++;
967
+ let text; try { text = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(nx.value.abs, 'utf8'); } catch { continue; }
968
+ if (text.includes('\0')) continue;
969
+ const lines = text.split('\n');
970
+ for (let i = 0; i < lines.length; i++) {
971
+ const line = lines[i];
972
+ if (line.length > 2000 || COMMENT.test(line)) continue;
973
+ // 1. URL literals naming a repository or gateway
974
+ for (const m of line.matchAll(URL_RE)) {
975
+ let host; try { host = new URL(m[0].replace(/[.,;]+$/, '')).hostname.toLowerCase(); } catch { continue; }
976
+ const first = host.split('.')[0];
977
+ for (const n of targets) {
978
+ if (n.kind !== 'repo' && n.kind !== 'gateway' && n.kind !== 'external-provider') continue;
979
+ if (namesOf(n).some((nm) => nm === host || nm === first)) add(n.kind === 'external-provider' ? 'uses-provider' : 'calls', repo.id, n.id, { file: nx.value.rel, line: i + 1, host });
980
+ }
981
+ for (const n of targets) if (n.kind === 'database' && namesOf(n).some((nm) => nm === host || nm === host.split('.')[0]) && CONNECT.test(m[0])) add('connects-to', repo.id, n.id, { file: nx.value.rel, line: i + 1, host });
982
+ }
983
+ // 2. queue and database names as literals on a publishing / consuming / connecting line
984
+ const lits = stringLiterals(line);
985
+ if (!lits.length) continue;
986
+ for (const n of targets) {
987
+ if (n.kind === 'queue') {
988
+ if (!lits.some((l) => namesOf(n).some((nm) => tokenIn(l, nm)))) continue;
989
+ if (PUBLISH.test(line)) add('produces', repo.id, n.id, { file: nx.value.rel, line: i + 1, name: n.id });
990
+ else if (CONSUME.test(line)) add('consumes', n.id, repo.id, { file: nx.value.rel, line: i + 1, name: n.id });
991
+ } else if (n.kind === 'database' && CONNECT.test(line) && lits.some((l) => namesOf(n).some((nm) => tokenIn(l, nm)))) add('connects-to', repo.id, n.id, { file: nx.value.rel, line: i + 1, name: n.id });
992
+ }
993
+ }
994
+ }
995
+ if (files >= L.maxFilesPerRepo) truncated = true;
996
+ scanned[repo.id] = { files, truncated };
997
+ }
998
+ return {
999
+ edges: [...found.values()].sort((a, b) => (a.type + a.from + a.to < b.type + b.from + b.to ? -1 : 1)),
1000
+ scanned,
1001
+ limitations: [
1002
+ 'Only string and URL literals are seen: a URL built at run time, read from the environment or routed by a mesh is not found.',
1003
+ 'Names match as whole tokens; an alias must be declared in the manifest for a node whose code name differs from its id.',
1004
+ 'The direction of a database use is not known, so those edges are `connects-to`.',
1005
+ 'A discovered edge is a lead with file and line evidence, not a confirmed call.',
1006
+ ],
1007
+ };
1008
+ }
1009
+
1010
+ /** Discovered edges whose (type, from, to) the manifest already declares, and those it does not. */
1011
+ function splitDeclared(discovered, manifest) {
1012
+ const declared = new Set();
1013
+ for (const r of manifest.repositories) {
1014
+ for (const t of r.calls) declared.add(`calls\u0000${r.id}\u0000${t}`);
1015
+ for (const t of r.produces) declared.add(`produces\u0000${r.id}\u0000${t}`);
1016
+ for (const t of r.consumes) declared.add(`consumes\u0000${t}\u0000${r.id}`);
1017
+ for (const t of r.providers) declared.add(`uses-provider\u0000${r.id}\u0000${t}`);
1018
+ for (const t of [...r.reads, ...r.writes]) declared.add(`connects-to\u0000${r.id}\u0000${t}`);
1019
+ }
1020
+ const undeclared = []; const confirmed = [];
1021
+ for (const e of discovered.edges) (declared.has(`${e.type}\u0000${e.from}\u0000${e.to}`) ? confirmed : undeclared).push(e);
1022
+ return { undeclared, confirmed };
1023
+ }
1024
+
1025
+
1026
+ /***/ }),
1027
+
1028
+ /***/ 81630:
1029
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1030
+
1031
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1032
+ /* harmony export */ Nn: () => (/* binding */ attackPaths),
1033
+ /* harmony export */ Th: () => (/* binding */ finalizeGraph),
1034
+ /* harmony export */ Ws: () => (/* binding */ buildEnterpriseGraph),
1035
+ /* harmony export */ _o: () => (/* binding */ componentBlastRadius),
1036
+ /* harmony export */ aw: () => (/* binding */ INTERNET),
1037
+ /* harmony export */ cl: () => (/* binding */ validateEnterpriseGraph),
1038
+ /* harmony export */ lB: () => (/* binding */ renderBlastRadius),
1039
+ /* harmony export */ r8: () => (/* binding */ vulnerableSet)
1040
+ /* harmony export */ });
1041
+ /* unused harmony export MAX_DEPTH */
1042
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
1043
+ // The enterprise graph (PRD 4, requirements 6 to 8): the repositories, APIs, queues, databases, external providers and trust zones
1044
+ // of an organization, with every edge carrying the evidence it came from.
1045
+ //
1046
+ // Two kinds of edge, never confused:
1047
+ // * `depends-on` edges come from each repository's own package manifest (the declared name and the spec it pins). A flaw in the
1048
+ // dependency can affect the dependent. They are NOT movement an attacker makes.
1049
+ // * REACH edges are what an attacker can move along: a call from one service to another, a producer to a queue, a queue to its
1050
+ // consumer, a service to a database or an external provider, and the Internet to anything declared Internet-facing. Those come
1051
+ // from the fleet manifest, which is operator-declared. They are labelled `source: fleet-manifest` and are exactly as good as the
1052
+ // operator's declaration; nothing here pretends to have discovered them. (Code-derived call and queue edges are what the Data
1053
+ // Flow Explorer's per-repository graph provides; joining that in is not built.)
1054
+ //
1055
+ // A graph edge without evidence is a bug, and `validateEnterpriseGraph` says so: "every graph edge traceable to source evidence".
1056
+
1057
+
1058
+ const ENTERPRISE_GRAPH_SCHEMA = 'agentic-security/enterprise-graph';
1059
+ const INTERNET = 'internet';
1060
+ const MAX_PATHS = 200;
1061
+ const MAX_DEPTH = 8;
1062
+ const AUTH_CWES = new Set(['CWE-287', 'CWE-306', 'CWE-285', 'CWE-862', 'CWE-863', 'CWE-384', 'CWE-613', 'CWE-639', 'CWE-290', 'CWE-294', 'CWE-308', 'CWE-1390']);
1063
+
1064
+ const edgeId = (type, from, to) => `edge:${type}:${from}:${to}`;
1065
+ const hash = (v) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash)('sha256').update(JSON.stringify(v)).digest('hex');
1066
+
1067
+ function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null }) {
1068
+ const nodes = new Map();
1069
+ const edges = new Map();
1070
+ const addNode = (n) => nodes.set(n.id, n);
1071
+ const addEdge = (type, from, to, evidence, reach) => {
1072
+ const id = edgeId(type, from, to);
1073
+ const e = edges.get(id) ?? { id, type, from, to, reach, evidence: [] };
1074
+ e.evidence.push(evidence);
1075
+ edges.set(id, e);
1076
+ };
1077
+ addNode({ id: INTERNET, type: 'internet', kind: 'internet', internetFacing: false, environment: null, dataClasses: [], trustZone: 'internet', required: false });
1078
+ for (const r of manifest.repositories) addNode({ id: r.id, type: 'repository', kind: r.kind, internetFacing: r.internetFacing, environment: r.environment, dataClasses: r.dataClasses, trustZone: r.trustZone, required: r.required });
1079
+ for (const r of manifest.resources ?? []) addNode({ id: r.id, type: r.kind, kind: r.kind, internetFacing: r.internetFacing, environment: r.environment, dataClasses: r.dataClasses, trustZone: r.trustZone, required: false });
1080
+
1081
+ const ev = (repo, field, value) => ({ source: 'fleet-manifest', repo, field, value });
1082
+ for (const n of [...nodes.values()]) if (n.internetFacing) addEdge('exposes', INTERNET, n.id, ev(n.id, 'internetFacing', true), true);
1083
+ for (const r of manifest.repositories) {
1084
+ for (const t of r.calls) addEdge('calls', r.id, t, ev(r.id, 'calls', t), true);
1085
+ for (const t of r.produces) addEdge('produces', r.id, t, ev(r.id, 'produces', t), true);
1086
+ for (const t of r.consumes) addEdge('consumes', t, r.id, ev(r.id, 'consumes', t), true);
1087
+ for (const t of r.reads) addEdge('reads', r.id, t, ev(r.id, 'reads', t), true);
1088
+ for (const t of r.writes) addEdge('writes', r.id, t, ev(r.id, 'writes', t), true);
1089
+ for (const t of r.providers) addEdge('uses-provider', r.id, t, ev(r.id, 'providers', t), true);
1090
+ }
1091
+ for (const d of dependencyGraph?.edges ?? []) addEdge('depends-on', d.from, d.to, d.evidence?.[0] ?? { source: 'package-manifest', repo: d.from, file: null, declared: d.via, spec: null }, false);
1092
+ // Edges found in the code. They share an id with a declared edge of the same type and endpoints, so the evidence list shows both; an
1093
+ // edge with ONLY discovered evidence is marked `discovered: true` so a query can say it rests on a code literal, not on a declaration.
1094
+ for (const d of discovered?.edges ?? []) for (const evidence of d.evidence) addEdge(d.type, d.from, d.to, evidence, true);
1095
+ for (const e of edges.values()) e.discovered = e.evidence.every((x) => x.source === 'discovered-code-literal');
1096
+ return { schema: ENTERPRISE_GRAPH_SCHEMA, version: 1, organization: manifest.organization, nodes: [...nodes.values()], edges: [...edges.values()], digest: null };
1097
+ }
1098
+
1099
+ function finalizeGraph(graph) { return { ...graph, digest: hash({ n: graph.nodes, e: graph.edges }) }; }
1100
+
1101
+ /** Every edge has evidence, every endpoint exists. Returns a list of problems. */
1102
+ function validateEnterpriseGraph(g) {
1103
+ const errs = [];
1104
+ const ids = new Set((g?.nodes ?? []).map((n) => n.id));
1105
+ for (const e of g?.edges ?? []) {
1106
+ if (!Array.isArray(e.evidence) || !e.evidence.length || !e.evidence.every((x) => x && typeof x.source === 'string')) errs.push(`edge ${e.id} has no source evidence`);
1107
+ if (!ids.has(e.from) || !ids.has(e.to)) errs.push(`edge ${e.id} points at a node that is not in the graph`);
1108
+ }
1109
+ return errs;
1110
+ }
1111
+
1112
+ const nodeOf = (g, id) => g.nodes.find((n) => n.id === id);
1113
+
1114
+ /** Repositories that match a "vulnerable code" selector, and everything that depends on them. */
1115
+ function vulnerableSet({ graph, findingsByRepo = new Map(), selector }) {
1116
+ const direct = new Set(); const why = new Map();
1117
+ if (selector.repo) { direct.add(selector.repo); why.set(selector.repo, 'named'); }
1118
+ for (const [repo, findings] of findingsByRepo) {
1119
+ const hit = (findings ?? []).find((f) => (selector.family === 'auth' ? (AUTH_CWES.has(String(f.cwe)) || /auth/i.test(String(f.family ?? ''))) : selector.family ? new RegExp(selector.family, 'i').test(String(f.family ?? f.vuln ?? '')) : false) || (selector.cwe && selector.cwe.includes(String(f.cwe))));
1120
+ if (hit) { direct.add(repo); why.set(repo, `finding ${hit.id ?? hit.stableId ?? ''} ${hit.cwe ?? ''}`.trim()); }
1121
+ }
1122
+ const incoming = new Map();
1123
+ for (const e of graph.edges) if (e.type === 'depends-on') { if (!incoming.has(e.to)) incoming.set(e.to, []); incoming.get(e.to).push(e.from); }
1124
+ const all = new Set(direct); const stack = [...direct];
1125
+ while (stack.length) { const c = stack.pop(); for (const f of incoming.get(c) ?? []) if (!all.has(f)) { all.add(f); why.set(f, `depends on ${c}`); stack.push(f); } }
1126
+ return { direct, all, why };
1127
+ }
1128
+
1129
+ /**
1130
+ * Attack paths: simple paths from the Internet to any node holding `toDataClass`, along reach edges only. A path is "involving"
1131
+ * when it passes through a repository in `involving`. Bounded in depth and count; `truncated` says when a bound was hit.
1132
+ */
1133
+ function attackPaths({ graph, toDataClass, from = INTERNET, involving = null, maxDepth = MAX_DEPTH, maxPaths = MAX_PATHS }) {
1134
+ const out = new Map();
1135
+ for (const e of graph.edges) if (e.reach) { if (!out.has(e.from)) out.set(e.from, []); out.get(e.from).push(e); }
1136
+ const results = []; let truncated = false;
1137
+ const targets = new Set(graph.nodes.filter((n) => n.dataClasses.includes(toDataClass)).map((n) => n.id));
1138
+ const walk = (id, nodePath, edgePath) => {
1139
+ if (results.length >= maxPaths) { truncated = true; return; }
1140
+ if (targets.has(id) && nodePath.length > 1) {
1141
+ const inv = involving ? nodePath.filter((n) => involving.all.has(n)) : [];
1142
+ if (!involving || inv.length) results.push({ nodes: [...nodePath], edges: edgePath.map((e) => e.id), involving: inv, discoveredEdges: edgePath.filter((e) => e.discovered).map((e) => e.id), evidence: edgePath.flatMap((e) => e.evidence.map((x) => ({ edge: e.id, ...x }))) });
1143
+ }
1144
+ if (nodePath.length > maxDepth) { truncated = true; return; }
1145
+ for (const e of out.get(id) ?? []) if (!nodePath.includes(e.to)) walk(e.to, [...nodePath, e.to], [...edgePath, e]);
1146
+ };
1147
+ if (nodeOf(graph, from)) walk(from, [from], []);
1148
+ return { dataClass: toDataClass, targets: [...targets], paths: results, truncated, bounds: { maxDepth, maxPaths } };
1149
+ }
1150
+
1151
+ /** The shared-component report: who depends on `component` (transitively), and how many of them are exposed, in production, or hold regulated data. */
1152
+ function componentBlastRadius({ graph, component, version = null }) {
1153
+ if (!nodeOf(graph, component)) return null;
1154
+ const incoming = new Map();
1155
+ for (const e of graph.edges) if (e.type === 'depends-on') { if (!incoming.has(e.to)) incoming.set(e.to, []); incoming.get(e.to).push(e); }
1156
+ const found = new Map(); const queue = [{ id: component, path: [component], vm: 'not-asked' }];
1157
+ while (queue.length) {
1158
+ const { id, path, vm: parentVm } = queue.shift();
1159
+ for (const e of incoming.get(id) ?? []) {
1160
+ if (found.has(e.from)) continue;
1161
+ const spec = e.evidence.map((x) => x.spec).find((x) => x != null) ?? null;
1162
+ // only the dependents that pin the component itself can be judged against a version; one reached THROUGH another repository
1163
+ // inherits that repository's status, and is not reached at all when the repository in between was ruled out
1164
+ const direct = id === component;
1165
+ const vm = direct ? (version == null ? 'not-asked' : spec == null ? 'unknown' : String(spec).includes(version) ? 'matched' : 'mismatch') : parentVm;
1166
+ found.set(e.from, { id: e.from, depth: path.length, path: [...path, e.from], spec, versionMatch: vm, inherited: !direct, evidence: e.evidence });
1167
+ if (vm !== 'mismatch') queue.push({ id: e.from, path: [...path, e.from], vm });
1168
+ }
1169
+ }
1170
+ const all = [...found.values()].sort((a, b) => a.depth - b.depth || a.id.localeCompare(b.id));
1171
+ const counted = all.filter((d) => d.versionMatch !== 'mismatch');
1172
+ const attrs = (d) => nodeOf(graph, d.id);
1173
+ const classes = {};
1174
+ for (const d of counted) for (const c of attrs(d).dataClasses) classes[c] = (classes[c] ?? 0) + 1;
1175
+ return {
1176
+ component, version, dependents: all,
1177
+ counts: { repositories: counted.length, internetFacing: counted.filter((d) => attrs(d).internetFacing).length, production: counted.filter((d) => attrs(d).environment === 'production').length, byDataClass: classes },
1178
+ excludedByVersion: all.filter((d) => d.versionMatch === 'mismatch').map((d) => d.id),
1179
+ unknownVersion: counted.filter((d) => d.versionMatch === 'unknown').map((d) => d.id),
1180
+ };
1181
+ }
1182
+
1183
+ function renderBlastRadius(r) {
1184
+ const n = (k, one, many) => `${k} ${k === 1 ? one : many}`;
1185
+ const L = [`${r.component}${r.version ? `@${r.version}` : ''}`, '', 'Used by:', n(r.counts.repositories, 'repository', 'repositories'), n(r.counts.internetFacing, 'Internet-facing service', 'Internet-facing services'), n(r.counts.production, 'production app', 'production apps')];
1186
+ for (const [c, k] of Object.entries(r.counts.byDataClass).sort()) L.push(n(k, `${c.toUpperCase()}-scoped service`, `${c.toUpperCase()}-scoped services`));
1187
+ if (r.excludedByVersion.length) L.push('', `Excluded, their pinned version does not match: ${r.excludedByVersion.join(', ')}`);
1188
+ if (r.unknownVersion.length) L.push('', `Counted, but their pinned version could not be read: ${r.unknownVersion.join(', ')}`);
1189
+ return L.join('\n');
1190
+ }
1191
+
1192
+
1193
+ /***/ }),
1194
+
1195
+ /***/ 94355:
1196
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1197
+
1198
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1199
+ /* harmony export */ X2: () => (/* binding */ blastRadius),
1200
+ /* harmony export */ u_: () => (/* binding */ buildFleetDependencyGraph)
1201
+ /* harmony export */ });
1202
+ /* unused harmony export readRepoManifest */
1203
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1204
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
1205
+ // The cross-repository dependency graph for the fleet feature (PRD 4).
1206
+ //
1207
+ // Reads each repo's own manifest to learn (a) the package name it publishes under, if any, and (b) the package
1208
+ // names it depends on. An edge A -> B means "A declares a dependency that resolves to B's own published name" —
1209
+ // i.e. B is upstream of A, and a defect in B can affect A. This is a real, lightweight cross-repo dependency graph
1210
+ // built purely from manifests already on disk; it does not need deployment config (Kubernetes/compose/Terraform)
1211
+ // the way the existing deployment-boundary graph does, so a fleet of plain application repos works out of the box.
1212
+
1213
+
1214
+
1215
+ const MANIFESTS = Object.freeze([
1216
+ { file: 'package.json', ecosystem: 'npm' },
1217
+ { file: 'go.mod', ecosystem: 'go' },
1218
+ { file: 'Cargo.toml', ecosystem: 'cargo' },
1219
+ { file: 'requirements.txt', ecosystem: 'pypi' },
1220
+ { file: 'pyproject.toml', ecosystem: 'pypi' },
1221
+ ]);
1222
+
1223
+ function readBounded(p, maxBytes = 1024 * 1024) {
1224
+ try {
1225
+ const st = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(p);
1226
+ if (!st.isFile() || st.size > maxBytes) return null;
1227
+ return node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p, 'utf8');
1228
+ } catch { return null; }
1229
+ }
1230
+
1231
+ function parsePackageJson(text) {
1232
+ let j;
1233
+ try { j = JSON.parse(text); } catch { return { name: null, deps: new Set() }; }
1234
+ const specs = new Map();
1235
+ for (const block of [j.peerDependencies, j.devDependencies, j.dependencies]) for (const [k, v] of Object.entries(block || {})) if (typeof v === 'string') specs.set(k, v.slice(0, 200));
1236
+ const deps = new Set([...Object.keys(j.dependencies || {}), ...Object.keys(j.devDependencies || {}), ...Object.keys(j.peerDependencies || {})]);
1237
+ return { name: typeof j.name === 'string' ? j.name : null, deps, specs, version: typeof j.version === 'string' ? j.version : null };
1238
+ }
1239
+
1240
+ function parseGoMod(text) {
1241
+ const m = /^module\s+(\S+)/m.exec(text);
1242
+ const deps = new Set();
1243
+ const block = /require\s*\(([\s\S]*?)\)/.exec(text);
1244
+ const lines = block ? block[1].split('\n') : text.split('\n').filter((l) => /^require\s/.test(l.trim()));
1245
+ for (const raw of lines) {
1246
+ const l = raw.replace(/^require\s+/, '').trim();
1247
+ const dm = /^(\S+)\s+v?\S+/.exec(l);
1248
+ if (dm) deps.add(dm[1]);
1249
+ }
1250
+ return { name: m ? m[1] : null, deps };
1251
+ }
1252
+
1253
+ function parseCargoToml(text) {
1254
+ const nm = /\[package\][\s\S]*?\bname\s*=\s*"([^"]+)"/.exec(text);
1255
+ const deps = new Set();
1256
+ const section = /\[dependencies\]([\s\S]*?)(\n\[|$)/.exec(text);
1257
+ if (section) {
1258
+ for (const line of section[1].split('\n')) {
1259
+ const dm = /^\s*([A-Za-z0-9_-]+)\s*=/.exec(line);
1260
+ if (dm) deps.add(dm[1]);
1261
+ }
1262
+ }
1263
+ return { name: nm ? nm[1] : null, deps };
1264
+ }
1265
+
1266
+ function parseRequirementsTxt(text) {
1267
+ const deps = new Set();
1268
+ for (const raw of text.split('\n')) {
1269
+ const l = raw.trim();
1270
+ if (!l || l.startsWith('#') || l.startsWith('-')) continue;
1271
+ const dm = /^([A-Za-z0-9._-]+)/.exec(l);
1272
+ if (dm) deps.add(dm[1].toLowerCase());
1273
+ }
1274
+ return { name: null, deps };
1275
+ }
1276
+
1277
+ function parsePyprojectToml(text) {
1278
+ const nm = /\[project\][\s\S]*?\bname\s*=\s*"([^"]+)"/.exec(text);
1279
+ const deps = new Set();
1280
+ const section = /dependencies\s*=\s*\[([\s\S]*?)\]/.exec(text);
1281
+ if (section) {
1282
+ for (const m of section[1].matchAll(/"([A-Za-z0-9._-]+)/g)) deps.add(m[1].toLowerCase());
1283
+ }
1284
+ return { name: nm ? nm[1] : null, deps };
1285
+ }
1286
+
1287
+ const PARSERS = { 'package.json': parsePackageJson, 'go.mod': parseGoMod, 'Cargo.toml': parseCargoToml, 'requirements.txt': parseRequirementsTxt, 'pyproject.toml': parsePyprojectToml };
1288
+
1289
+ /** One repo's declared identity and dependency names, merged across every manifest file it has. */
1290
+ function readRepoManifest(repoPath) {
1291
+ let name = null;
1292
+ const deps = new Set();
1293
+ const ecosystems = new Set();
1294
+ const specs = new Map();
1295
+ const files = new Map();
1296
+ for (const { file, ecosystem } of MANIFESTS) {
1297
+ const text = readBounded(node_path__WEBPACK_IMPORTED_MODULE_1__.join(repoPath, file));
1298
+ if (text == null) continue;
1299
+ const parsed = PARSERS[file](text);
1300
+ if (parsed.name && !name) name = parsed.name;
1301
+ for (const d of parsed.deps) { deps.add(d); if (!files.has(d)) files.set(d, file); }
1302
+ for (const [d, spec] of parsed.specs ?? []) specs.set(d, spec);
1303
+ ecosystems.add(ecosystem);
1304
+ }
1305
+ return { name, deps, ecosystems: [...ecosystems], specs, files };
1306
+ }
1307
+
1308
+ /**
1309
+ * Build the fleet dependency graph. `repos`: [{id, path}]. An edge {from, to} means `from` depends on `to` (so a
1310
+ * defect in `to` can affect `from`). Matching is by declared package NAME only — two repos that happen to share a
1311
+ * name under different ecosystems are not disambiguated, a disclosed limit, not a guess.
1312
+ */
1313
+ function buildFleetDependencyGraph(repos) {
1314
+ const manifests = new Map();
1315
+ for (const r of repos) manifests.set(r.id, { ...r, manifest: readRepoManifest(r.path) });
1316
+
1317
+ const byName = new Map();
1318
+ for (const [id, r] of manifests) if (r.manifest.name) {
1319
+ if (!byName.has(r.manifest.name)) byName.set(r.manifest.name, []);
1320
+ byName.get(r.manifest.name).push(id);
1321
+ }
1322
+
1323
+ const edges = [];
1324
+ for (const [id, r] of manifests) {
1325
+ for (const dep of r.manifest.deps) {
1326
+ const targets = byName.get(dep);
1327
+ if (!targets) continue;
1328
+ for (const to of targets) if (to !== id) edges.push({ from: id, to, via: dep, evidence: [{ source: 'package-manifest', repo: id, file: r.manifest.files.get(dep) ?? null, declared: dep, spec: r.manifest.specs.get(dep) ?? null }] });
1329
+ }
1330
+ }
1331
+ const nodes = [...manifests.values()].map((r) => ({ id: r.id, path: r.path, declaredName: r.manifest.name, ecosystems: r.manifest.ecosystems }));
1332
+ return { nodes, edges };
1333
+ }
1334
+
1335
+ /** Transitive downstream closure: which fleet repos depend on `repoId`, directly or transitively, with the shortest path to each. */
1336
+ function blastRadius(graph, repoId) {
1337
+ const incoming = new Map();
1338
+ for (const e of graph.edges) { if (!incoming.has(e.to)) incoming.set(e.to, []); incoming.get(e.to).push(e.from); }
1339
+ const best = new Map();
1340
+ const queue = [{ id: repoId, path: [repoId] }];
1341
+ while (queue.length) {
1342
+ const { id, path: p } = queue.shift();
1343
+ for (const from of incoming.get(id) || []) {
1344
+ if (best.has(from)) continue;
1345
+ const newPath = [...p, from];
1346
+ best.set(from, { depth: newPath.length - 1, path: newPath });
1347
+ queue.push({ id: from, path: newPath });
1348
+ }
1349
+ }
1350
+ return [...best.entries()].map(([id, v]) => ({ id, ...v })).sort((a, b) => a.depth - b.depth || (a.id < b.id ? -1 : 1));
1351
+ }
1352
+
1353
+
1354
+ /***/ }),
1355
+
1356
+ /***/ 62790:
1357
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1358
+
1359
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1360
+ /* harmony export */ Cb: () => (/* binding */ loadFleetManifest),
1361
+ /* harmony export */ yr: () => (/* binding */ starterManifestYaml)
1362
+ /* harmony export */ });
1363
+ /* unused harmony exports ENVIRONMENTS, parseDuration, validateFleetManifest */
1364
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1365
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
1366
+ /* harmony import */ var _util_yaml_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(82340);
1367
+ // The fleet manifest (PRD 4 "Fleet Manifest"). `fleet.yaml`:
1368
+ //
1369
+ // organization: example
1370
+ // repositories:
1371
+ // - id: frontend
1372
+ // path: ../frontend
1373
+ // workers: 4
1374
+ // perRepoTimeout: 20m
1375
+ //
1376
+ // Parsing reuses util/yaml.js (the one YAML entry point in this codebase). Validation is closed-world: an unknown
1377
+ // top-level key, a duplicate repository id, or a path that does not resolve to a real directory all refuse the
1378
+ // whole manifest rather than silently skipping one repo.
1379
+
1380
+
1381
+
1382
+
1383
+ const FLEET_MANIFEST_SCHEMA_KEYS = Object.freeze(['organization', 'repositories', 'workers', 'perRepoTimeout', 'retries', 'resources']);
1384
+ const REPO_KEYS = Object.freeze(['id', 'path', 'required', 'kind', 'internetFacing', 'environment', 'dataClasses', 'trustZone', 'calls', 'produces', 'consumes', 'reads', 'writes', 'providers', 'aliases']);
1385
+ const RESOURCE_KEYS = Object.freeze(['id', 'kind', 'dataClasses', 'trustZone', 'internetFacing', 'environment', 'aliases']);
1386
+ const REPO_KINDS = Object.freeze(['service', 'library', 'frontend', 'worker', 'gateway']);
1387
+ const RESOURCE_KINDS = Object.freeze(['queue', 'database', 'external-provider', 'gateway']);
1388
+ const ENVIRONMENTS = Object.freeze(['production', 'staging', 'development']);
1389
+ const NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
1390
+ const DURATION_RE = /^(\d+)(s|m|h)$/;
1391
+
1392
+ function parseDuration(s, fallbackMs) {
1393
+ if (typeof s === 'number' && s > 0) return s * 1000;
1394
+ const m = DURATION_RE.exec(String(s ?? ''));
1395
+ if (!m) return fallbackMs;
1396
+ const n = Number(m[1]);
1397
+ return m[2] === 's' ? n * 1000 : m[2] === 'm' ? n * 60000 : n * 3600000;
1398
+ }
1399
+
1400
+ /** Validate a parsed manifest object against the manifest's own base directory (for path resolution). */
1401
+ function validateFleetManifest(doc, { baseDir = '.' } = {}) {
1402
+ const errors = [];
1403
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return { ok: false, errors: ['manifest must be a YAML mapping'] };
1404
+ for (const k of Object.keys(doc)) if (!FLEET_MANIFEST_SCHEMA_KEYS.includes(k)) errors.push(`unknown top-level key: ${k}`);
1405
+ if (typeof doc.organization !== 'string' || !doc.organization) errors.push('organization is required and must be a string');
1406
+ if (!Array.isArray(doc.repositories) || doc.repositories.length === 0) errors.push('repositories must be a non-empty array');
1407
+ const seen = new Set();
1408
+ const repos = [];
1409
+ const idList = (v, where) => { if (v === undefined) return []; if (!Array.isArray(v) || !v.every((x) => typeof x === 'string' && NAME_RE.test(x))) { errors.push(`${where} must be a list of ids`); return []; } return v; };
1410
+ const aliasList = (v, where) => { if (v === undefined) return []; if (!Array.isArray(v) || v.length > 16 || !v.every((x) => typeof x === 'string' && /^[a-z0-9][a-z0-9._-]{0,99}$/.test(x))) { errors.push(`${where} must be a list of up to 16 lower-case names or hostnames`); return []; } return v; };
1411
+ const classes = (v, where) => { if (v === undefined) return []; if (!Array.isArray(v) || !v.every((x) => typeof x === 'string' && /^[a-z][a-z0-9-]{0,30}$/.test(x))) { errors.push(`${where} must be a list of data class names such as pci or pii`); return []; } return v; };
1412
+ for (const [i, r] of (Array.isArray(doc.repositories) ? doc.repositories : []).entries()) {
1413
+ if (!r || typeof r !== 'object') { errors.push(`repositories[${i}] must be a mapping`); continue; }
1414
+ for (const k of Object.keys(r)) if (!REPO_KEYS.includes(k)) errors.push(`repositories[${i}]: unknown key ${k}`);
1415
+ if (typeof r.id !== 'string' || !NAME_RE.test(r.id)) { errors.push(`repositories[${i}].id must be a lowercase kebab-case string`); continue; }
1416
+ if (seen.has(r.id)) { errors.push(`duplicate repository id: ${r.id}`); continue; }
1417
+ seen.add(r.id);
1418
+ if (typeof r.path !== 'string' || !r.path) { errors.push(`repositories[${i}].path is required`); continue; }
1419
+ const abs = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(baseDir, r.path);
1420
+ let isDir = false;
1421
+ try { isDir = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(abs).isDirectory(); } catch { /* reported below */ }
1422
+ if (!isDir) errors.push(`repositories[${i}].path does not resolve to a directory: ${r.path}`);
1423
+ if (r.kind !== undefined && !REPO_KINDS.includes(r.kind)) errors.push(`repositories[${i}].kind must be one of ${REPO_KINDS.join(', ')}`);
1424
+ if (r.environment !== undefined && !ENVIRONMENTS.includes(r.environment)) errors.push(`repositories[${i}].environment must be one of ${ENVIRONMENTS.join(', ')}`);
1425
+ for (const k of ['required', 'internetFacing']) if (r[k] !== undefined && typeof r[k] !== 'boolean') errors.push(`repositories[${i}].${k} must be true or false`);
1426
+ if (r.trustZone !== undefined && !(typeof r.trustZone === 'string' && NAME_RE.test(r.trustZone))) errors.push(`repositories[${i}].trustZone must be a name`);
1427
+ repos.push({
1428
+ id: r.id, path: abs, required: r.required !== false, kind: r.kind ?? null, internetFacing: r.internetFacing === true, environment: r.environment ?? null,
1429
+ dataClasses: classes(r.dataClasses, `repositories[${i}].dataClasses`), trustZone: r.trustZone ?? null,
1430
+ aliases: aliasList(r.aliases, `repositories[${i}].aliases`), calls: idList(r.calls, `repositories[${i}].calls`), produces: idList(r.produces, `repositories[${i}].produces`), consumes: idList(r.consumes, `repositories[${i}].consumes`),
1431
+ reads: idList(r.reads, `repositories[${i}].reads`), writes: idList(r.writes, `repositories[${i}].writes`), providers: idList(r.providers, `repositories[${i}].providers`),
1432
+ });
1433
+ }
1434
+ const resources = [];
1435
+ const resIds = new Set();
1436
+ if (doc.resources !== undefined && !Array.isArray(doc.resources)) errors.push('resources must be a list');
1437
+ for (const [i, r] of (Array.isArray(doc.resources) ? doc.resources : []).entries()) {
1438
+ if (!r || typeof r !== 'object') { errors.push(`resources[${i}] must be a mapping`); continue; }
1439
+ for (const k of Object.keys(r)) if (!RESOURCE_KEYS.includes(k)) errors.push(`resources[${i}]: unknown key ${k}`);
1440
+ if (typeof r.id !== 'string' || !NAME_RE.test(r.id)) { errors.push(`resources[${i}].id must be a lowercase kebab-case string`); continue; }
1441
+ if (seen.has(r.id) || resIds.has(r.id)) { errors.push(`resource id ${r.id} collides with another repository or resource`); continue; }
1442
+ resIds.add(r.id);
1443
+ if (!RESOURCE_KINDS.includes(r.kind)) errors.push(`resources[${i}].kind must be one of ${RESOURCE_KINDS.join(', ')}`);
1444
+ if (r.environment !== undefined && !ENVIRONMENTS.includes(r.environment)) errors.push(`resources[${i}].environment must be one of ${ENVIRONMENTS.join(', ')}`);
1445
+ if (r.internetFacing !== undefined && typeof r.internetFacing !== 'boolean') errors.push(`resources[${i}].internetFacing must be true or false`);
1446
+ resources.push({ id: r.id, kind: r.kind, aliases: aliasList(r.aliases, `resources[${i}].aliases`), dataClasses: classes(r.dataClasses, `resources[${i}].dataClasses`), trustZone: r.trustZone ?? null, internetFacing: r.internetFacing === true, environment: r.environment ?? null });
1447
+ }
1448
+ const known = new Set([...seen, ...resIds]);
1449
+ for (const r of repos) {
1450
+ for (const [field, kinds] of [['calls', ['repo', 'gateway']], ['produces', ['queue']], ['consumes', ['queue']], ['reads', ['database']], ['writes', ['database']], ['providers', ['external-provider']]]) {
1451
+ for (const target of r[field]) {
1452
+ if (!known.has(target)) { errors.push(`repository ${r.id}: ${field} names ${target}, which is neither a repository nor a resource`); continue; }
1453
+ const res = resources.find((x) => x.id === target);
1454
+ const ok = res ? kinds.includes(res.kind) : kinds.includes('repo');
1455
+ if (!ok) errors.push(`repository ${r.id}: ${field} cannot point at ${target}${res ? ` (a ${res.kind})` : ' (a repository)'}`);
1456
+ }
1457
+ }
1458
+ }
1459
+ if (doc.workers != null && !(Number.isInteger(doc.workers) && doc.workers >= 1 && doc.workers <= 32)) errors.push('workers must be an integer in 1..32');
1460
+ if (doc.retries != null && !(Number.isInteger(doc.retries) && doc.retries >= 0 && doc.retries <= 5)) errors.push('retries must be an integer in 0..5');
1461
+ if (doc.perRepoTimeout != null && parseDuration(doc.perRepoTimeout, null) == null) errors.push(`perRepoTimeout is not a valid duration: ${doc.perRepoTimeout}`);
1462
+ if (errors.length) return { ok: false, errors };
1463
+ return {
1464
+ ok: true, errors: [],
1465
+ manifest: {
1466
+ organization: doc.organization, repositories: repos, resources,
1467
+ workers: doc.workers ?? 4, retries: doc.retries ?? 1, perRepoTimeoutMs: parseDuration(doc.perRepoTimeout, 20 * 60000),
1468
+ },
1469
+ };
1470
+ }
1471
+
1472
+ function loadFleetManifest(filePath) {
1473
+ let text;
1474
+ try { text = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(filePath, 'utf8'); } catch (e) { return { ok: false, errors: [`cannot read ${filePath}: ${e.message}`] }; }
1475
+ let doc;
1476
+ try { doc = (0,_util_yaml_js__WEBPACK_IMPORTED_MODULE_2__/* .load */ .Hh)(text); } catch (e) { return { ok: false, errors: [`invalid YAML: ${e.message}`] }; }
1477
+ return validateFleetManifest(doc, { baseDir: node_path__WEBPACK_IMPORTED_MODULE_1__.dirname(node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(filePath)) });
1478
+ }
1479
+
1480
+ function starterManifestYaml(organization = 'example') {
1481
+ return `organization: ${organization}
1482
+ repositories:
1483
+ - id: example-repo
1484
+ path: .
1485
+ workers: 4
1486
+ perRepoTimeout: 20m
1487
+ `;
1488
+ }
1489
+
1490
+
1491
+ /***/ }),
1492
+
1493
+ /***/ 89072:
1494
+ /***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
1495
+
1496
+ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
1497
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1498
+ /* harmony export */ Ag: () => (/* binding */ runJobs),
1499
+ /* harmony export */ IT: () => (/* binding */ childScan),
1500
+ /* harmony export */ kZ: () => (/* binding */ loadArtifacts),
1501
+ /* harmony export */ nY: () => (/* binding */ planRun),
1502
+ /* harmony export */ zD: () => (/* binding */ reclaimDeadWorkers)
1503
+ /* harmony export */ });
1504
+ /* unused harmony export DEFAULTS */
1505
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1506
+ /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
1507
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
1508
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(77598);
1509
+ /* harmony import */ var node_url__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(73136);
1510
+ /* harmony import */ var _sandbox_supervise_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(74676);
1511
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(31174);
1512
+ /* harmony import */ var _scan_cache_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(75846);
1513
+ /* harmony import */ var _scan_child_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(3398);
1514
+ var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_scan_child_js__WEBPACK_IMPORTED_MODULE_8__]);
1515
+ _scan_child_js__WEBPACK_IMPORTED_MODULE_8__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
1516
+ // The fleet scan controller (PRD 4, requirements 2 to 5). It turns a manifest into jobs in the job store and runs them with bounded
1517
+ // workers, each scan in its own child process under a hard timeout, so one hung repository costs one worker for one timeout and
1518
+ // nothing else. Every state change is in the store, which is what makes `fleet resume` an ordinary continuation.
1519
+ //
1520
+ // plan fingerprint each repository, derive its cache key, enqueue one idempotent job per repository
1521
+ // run claim -> (cache hit | scan in a supervised child, heartbeating) -> complete | fail-with-retry
1522
+ // recover an expired lease, or a lease held by a dead process on this host, goes back in the queue
1523
+ //
1524
+ // A worker that loses its lease (it stopped heartbeating long enough for another worker to take the job) is fenced out: its late
1525
+ // result is refused by the store and its scan is aborted. Results are written per attempt (`<repo>.<epoch>.json`) and the job row
1526
+ // points at the winning one, so a retry can never produce two sets of findings for one repository.
1527
+
1528
+
1529
+
1530
+
1531
+
1532
+
1533
+
1534
+
1535
+
1536
+
1537
+ const SCAN_CHILD = (0,node_url__WEBPACK_IMPORTED_MODULE_4__.fileURLToPath)(new URL(/* asset import */ __webpack_require__(53948), __webpack_require__.b));
1538
+ const sha = (s) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_3__.createHash)('sha256').update(s).digest('hex');
1539
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1540
+ const DEFAULTS = Object.freeze({ leaseMs: 30_000, heartbeatMs: 10_000, retryDelayMs: 2_000, pollMs: 100 });
1541
+
1542
+ /** The default scan: one supervised child process per repository. Throws `{code: 'TIMEOUT'|'ERROR', message}`. */
1543
+ async function childScan(repoPath, { timeoutMs, signal }) {
1544
+ const r = await (0,_sandbox_supervise_js__WEBPACK_IMPORTED_MODULE_5__/* .superviseSpawn */ .u)(process.execPath, [SCAN_CHILD, repoPath], { cwd: repoPath, env: { ...process.env, AGENTIC_SECURITY_NO_STATE: '1' }, timeoutMs, signal, maxOutputBytes: 96 * 1024 * 1024 });
1545
+ const fail = (code, message) => Object.assign(new Error(message), { code });
1546
+ if (r.timedOut) throw fail('TIMEOUT', `no result within ${Math.round(timeoutMs / 1000)}s`);
1547
+ if (r.cancelled) throw fail('CANCELLED', 'cancelled');
1548
+ if (r.spawnError) throw fail('ERROR', `could not start the scan: ${r.spawnError}`);
1549
+ if (r.outputCapped) throw fail('ERROR', 'the scan produced more output than allowed');
1550
+ if (r.exitCode !== 0) throw fail('ERROR', `scan exited ${r.exitCode}: ${String(r.stderr).trim().split('\n').pop()?.slice(0, 200) ?? ''}`);
1551
+ const a = r.stdout.indexOf(_scan_child_js__WEBPACK_IMPORTED_MODULE_8__/* .BEGIN */ .Q); const b = r.stdout.indexOf(_scan_child_js__WEBPACK_IMPORTED_MODULE_8__/* .END */ .K);
1552
+ if (a < 0 || b < a) throw fail('ERROR', 'the scan produced no result block');
1553
+ try { return JSON.parse(r.stdout.slice(a + _scan_child_js__WEBPACK_IMPORTED_MODULE_8__/* .BEGIN */ .Q.length, b)); } catch (e) { throw fail('ERROR', `unreadable scan result: ${e.message}`); }
1554
+ }
1555
+
1556
+ const hostId = () => node_os__WEBPACK_IMPORTED_MODULE_1__.hostname();
1557
+ const pidAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
1558
+
1559
+ /** Free jobs held by workers on THIS host whose process no longer exists. Other hosts are handled by lease expiry alone. */
1560
+ function reclaimDeadWorkers(store, runId, { host = hostId(), alive = pidAlive, self = null } = {}) {
1561
+ let freed = 0;
1562
+ const seen = new Set();
1563
+ for (const j of store.listJobs(runId)) {
1564
+ if (j.status !== 'leased' || !j.worker || seen.has(j.worker) || j.worker === self) continue;
1565
+ const m = /^(.*):(\d+):\d+$/.exec(j.worker);
1566
+ if (!m || m[1] !== host) continue;
1567
+ seen.add(j.worker);
1568
+ if (!alive(Number(m[2]))) freed += store.reclaimWorker(runId, j.worker);
1569
+ }
1570
+ return freed;
1571
+ }
1572
+
1573
+ function planRun({ manifest, manifestHash, store, runId = null, config = {}, now = Date.now(), identity = (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .scannerIdentity */ .fs)(), fingerprint = _scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .repoFingerprint */ .NJ, retries = manifest.retries, meta = {} }) {
1574
+ const scanConfig = { provenance: false, ...config };
1575
+ const prints = manifest.repositories.map((r) => ({ r, fp: fingerprint(r.path) }));
1576
+ const configHash = sha(JSON.stringify(scanConfig));
1577
+ const id = runId ?? `frun-${sha(JSON.stringify({ manifestHash, configHash, identity, t: now, prints: prints.map((p) => [p.r.id, p.fp.content]) })).slice(0, 16)}`;
1578
+ store.begin({ runId: id, organization: manifest.organization, manifestHash, configHash, scannerIdentity: identity, createdAt: now, meta: { workers: manifest.workers, perRepoTimeoutMs: manifest.perRepoTimeoutMs, retries, ...meta } });
1579
+ const created = store.enqueue(id, prints.map(({ r, fp }) => ({ jobId: `${id}:${r.id}`, repoId: r.id, commit: fp.commit, cacheKey: (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .cacheKey */ .k$)({ fingerprint: fp, config: scanConfig, identity }), timeoutMs: manifest.perRepoTimeoutMs, maxAttempts: retries + 1, required: r.required })), { now });
1580
+ return { runId: id, created, identity };
1581
+ }
1582
+
1583
+ /**
1584
+ * Run (or continue) a planned run until no job is queued or leased by a live worker. Returns when the work is done, the signal
1585
+ * aborts, or `maxWaitMs` passes with only other workers' leases outstanding.
1586
+ */
1587
+ async function runJobs({ manifest, store, runId, root, scan = childScan, workers = manifest.workers, leaseMs = DEFAULTS.leaseMs, heartbeatMs = DEFAULTS.heartbeatMs, retryDelayMs = DEFAULTS.retryDelayMs, pollMs = DEFAULTS.pollMs, maxWaitMs = null, signal, now = () => Date.now(), onEvent = () => {}, cacheDir = null, artifactDir = null, useCache = true }) {
1588
+ const repoById = new Map(manifest.repositories.map((r) => [r.id, r]));
1589
+ const cacheD = cacheDir ?? (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, 'fleet', 'cache');
1590
+ const artD = artifactDir ?? (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, 'fleet', 'artifacts', runId);
1591
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(artD, { recursive: true, mode: 0o700 });
1592
+ const deadline = maxWaitMs ?? (manifest.perRepoTimeoutMs * 3 + leaseMs);
1593
+ const writeArtifact = (job, payload) => {
1594
+ const text = JSON.stringify({ schema: 'agentic-security/fleet-scan-artifact', version: 1, runId, repoId: job.repoId, epoch: job.epoch, commit: job.commit, ...payload });
1595
+ const file = node_path__WEBPACK_IMPORTED_MODULE_2__.join(artD, `${job.repoId}.${job.epoch}.json`);
1596
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(file, text, { mode: 0o600 });
1597
+ return { file, sha256: sha(text) };
1598
+ };
1599
+
1600
+ async function worker(slot) {
1601
+ const id = `${hostId()}:${process.pid}:${slot}`;
1602
+ let idleSince = null;
1603
+ for (;;) {
1604
+ if (signal?.aborted) return;
1605
+ store.reclaimExpired(runId, { now: now() });
1606
+ const job = store.claim(runId, { worker: id, leaseMs, now: now() });
1607
+ if (!job) {
1608
+ const open = store.listJobs(runId).filter((j) => j.status === 'queued' || j.status === 'leased');
1609
+ if (!open.length) return;
1610
+ idleSince ??= now();
1611
+ if (now() - idleSince > deadline) return;
1612
+ await sleep(pollMs);
1613
+ continue;
1614
+ }
1615
+ idleSince = null;
1616
+ const repo = repoById.get(job.repoId);
1617
+ onEvent({ type: 'start', repo: job.repoId, attempt: job.attempts });
1618
+ const cached = useCache ? (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .readCache */ .xe)(cacheD, job.cacheKey) : null;
1619
+ if (cached) {
1620
+ const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, cached: true });
1621
+ const status = cached.entry?.scanHealth && cached.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
1622
+ if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: cached.entry?.total ?? null, cached: true, artifact: node_path__WEBPACK_IMPORTED_MODULE_2__.basename(a.file) }, now: now() })) onEvent({ type: 'done', repo: job.repoId, cached: true, status });
1623
+ continue;
1624
+ }
1625
+ const ac = new AbortController();
1626
+ const onAbort = () => ac.abort();
1627
+ signal?.addEventListener('abort', onAbort, { once: true });
1628
+ let lost = false;
1629
+ const hb = setInterval(() => { if (!store.heartbeat(job.jobId, job.epoch, { now: now(), leaseMs })) { lost = true; ac.abort(); } }, heartbeatMs);
1630
+ try {
1631
+ const out = await scan(repo.path, { timeoutMs: job.timeoutMs, signal: ac.signal });
1632
+ clearInterval(hb);
1633
+ if (lost) { onEvent({ type: 'fenced', repo: job.repoId }); continue; }
1634
+ if (!out?.entry) throw Object.assign(new Error('the scan returned no entry'), { code: 'ERROR' });
1635
+ if (out.entry.ok === false) throw Object.assign(new Error(out.entry.error || 'the scan failed'), { code: 'ERROR' });
1636
+ const a = writeArtifact(job, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
1637
+ const status = out.entry.scanHealth && out.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
1638
+ if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: out.entry.total ?? null, cached: false, artifact: node_path__WEBPACK_IMPORTED_MODULE_2__.basename(a.file) }, now: now() })) {
1639
+ if (useCache && status === 'succeeded') (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .writeCache */ .aR)(cacheD, job.cacheKey, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
1640
+ onEvent({ type: 'done', repo: job.repoId, cached: false, status });
1641
+ } else onEvent({ type: 'fenced', repo: job.repoId });
1642
+ } catch (e) {
1643
+ clearInterval(hb);
1644
+ if (lost || e?.code === 'CANCELLED') { if (e?.code === 'CANCELLED' && !lost) store.fail(job.jobId, job.epoch, { error: 'cancelled', now: now(), retryDelayMs: 0 }); continue; }
1645
+ const r = store.fail(job.jobId, job.epoch, { error: e?.message ?? String(e), timedOut: e?.code === 'TIMEOUT', now: now(), retryDelayMs });
1646
+ onEvent({ type: 'failed', repo: job.repoId, error: e?.message ?? String(e), timedOut: e?.code === 'TIMEOUT', next: r?.state ?? null });
1647
+ } finally { clearInterval(hb); signal?.removeEventListener('abort', onAbort); }
1648
+ }
1649
+ }
1650
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(workers, 32)) }, (_, i) => worker(i)));
1651
+ return { aborted: !!signal?.aborted };
1652
+ }
1653
+
1654
+ /** Load the artifacts the jobs point at, verifying each against the hash the store recorded. A missing or altered artifact is reported, not used. */
1655
+ function loadArtifacts({ store, runId, root, artifactDir = null }) {
1656
+ const dir = artifactDir ?? (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, 'fleet', 'artifacts', runId);
1657
+ const out = new Map(); const problems = [];
1658
+ for (const j of store.listJobs(runId)) {
1659
+ if (!['succeeded', 'partial'].includes(j.status)) continue;
1660
+ const file = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, j.summary?.artifact ?? `${j.repoId}.${j.epoch}.json`);
1661
+ try {
1662
+ const text = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8');
1663
+ if (sha(text) !== j.artifactSha256) { problems.push({ repoId: j.repoId, problem: 'artifact does not match the recorded hash' }); continue; }
1664
+ out.set(j.repoId, JSON.parse(text));
1665
+ } catch { problems.push({ repoId: j.repoId, problem: 'artifact is missing or unreadable' }); }
1666
+ }
1667
+ return { artifacts: out, problems };
1668
+ }
1669
+
1670
+ __webpack_async_result__();
1671
+ } catch(e) { __webpack_async_result__(e); } });
1672
+
1673
+ /***/ }),
1674
+
1675
+ /***/ 20121:
1676
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1677
+
1678
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1679
+ /* harmony export */ Hd: () => (/* binding */ isTerminal),
1680
+ /* harmony export */ J6: () => (/* binding */ createSqliteJobStore)
1681
+ /* harmony export */ });
1682
+ /* unused harmony exports JOB_STATUSES, createMemoryJobStore */
1683
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1684
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
1685
+ // The fleet job store (PRD 4, requirements 2 and 3): jobs with a lease, a heartbeat, a retry count, a timeout and a result hash,
1686
+ // behind one small interface so the storage can change without the scheduler noticing.
1687
+ //
1688
+ // createSqliteJobStore({ file }) local deployment, one file, no server (node:sqlite)
1689
+ // createMemoryJobStore() the reference implementation; the conformance tests run the SAME assertions against both,
1690
+ // which is what shows the interface does not secretly depend on SQLite. A PostgreSQL or a
1691
+ // queue-backed store would be a third implementation of the same methods.
1692
+ //
1693
+ // Semantics that matter (and are tested on both stores):
1694
+ // * IDEMPOTENT: a job's id is derived from its run, repository, commit and configuration, and `enqueue` of an existing id does
1695
+ // nothing. Enqueueing a run twice does not make two jobs.
1696
+ // * LEASED: `claim` hands one queued job to one worker, increments a fencing `epoch`, and starts a lease. A worker that stops
1697
+ // heartbeating loses the job when the lease expires; `reclaimExpired` puts it back in the queue.
1698
+ // * FENCED: `heartbeat`, `complete` and `fail` must present the epoch they were given. A worker that lost its lease (and whose
1699
+ // job another worker now holds) is refused, so a slow zombie can neither extend a lease it no longer has nor overwrite the
1700
+ // result of the worker that replaced it. That is what keeps a retry from duplicating findings.
1701
+ // * BOUNDED RETRIES: a failure puts the job back in the queue until `maxAttempts`, then it is `failed` (or `timed_out`),
1702
+ // terminal, and visible in the organization's health. A job is never silently dropped.
1703
+
1704
+
1705
+
1706
+ const JOB_STATUSES = Object.freeze(['queued', 'leased', 'succeeded', 'partial', 'failed', 'timed_out', 'cancelled']);
1707
+ const TERMINAL = new Set(['succeeded', 'partial', 'failed', 'timed_out', 'cancelled']);
1708
+ const isTerminal = (s) => TERMINAL.has(s);
1709
+
1710
+ const jobOf = (r) => (r ? {
1711
+ jobId: r.job_id, runId: r.run_id, repoId: r.repo_id, commit: r.commit_sha ?? null, cacheKey: r.cache_key ?? null, status: r.status, worker: r.worker ?? null,
1712
+ epoch: r.epoch, leaseExpiresAt: r.lease_expires_at ?? null, heartbeatAt: r.heartbeat_at ?? null, attempts: r.attempts, maxAttempts: r.max_attempts,
1713
+ timeoutMs: r.timeout_ms, nextAttemptAt: r.next_attempt_at ?? 0, artifactSha256: r.artifact_sha256 ?? null, summary: r.summary ? JSON.parse(r.summary) : null,
1714
+ error: r.error ?? null, createdAt: r.created_at, startedAt: r.started_at ?? null, finishedAt: r.finished_at ?? null, required: !!r.required,
1715
+ } : null);
1716
+
1717
+ // ------------------------------------------------------------------------------------------------------- memory
1718
+ function createMemoryJobStore() {
1719
+ const runs = new Map(); const jobs = new Map();
1720
+ const all = (runId) => [...jobs.values()].filter((j) => j.runId === runId);
1721
+ const store = {
1722
+ kind: 'memory',
1723
+ begin(run) { if (!runs.has(run.runId)) runs.set(run.runId, { ...run }); return runs.get(run.runId); },
1724
+ getRun: (runId) => runs.get(runId) ?? null,
1725
+ listRuns: () => [...runs.values()].sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))),
1726
+ enqueue(runId, list, { now = Date.now() } = {}) {
1727
+ let created = 0;
1728
+ for (const j of list) { if (jobs.has(j.jobId)) continue; jobs.set(j.jobId, { runId, repoId: j.repoId, jobId: j.jobId, commit: j.commit ?? null, cacheKey: j.cacheKey ?? null, status: 'queued', worker: null, epoch: 0, leaseExpiresAt: null, heartbeatAt: null, attempts: 0, maxAttempts: j.maxAttempts ?? 2, timeoutMs: j.timeoutMs, nextAttemptAt: 0, artifactSha256: null, summary: null, error: null, createdAt: now, startedAt: null, finishedAt: null, required: j.required !== false }); created++; }
1729
+ return created;
1730
+ },
1731
+ claim(runId, { worker, leaseMs, now = Date.now() }) {
1732
+ const j = all(runId).filter((x) => x.status === 'queued' && x.nextAttemptAt <= now).sort((a, b) => a.createdAt - b.createdAt || a.repoId.localeCompare(b.repoId))[0];
1733
+ if (!j) return null;
1734
+ Object.assign(j, { status: 'leased', worker, epoch: j.epoch + 1, attempts: j.attempts + 1, leaseExpiresAt: now + leaseMs, heartbeatAt: now, startedAt: now, error: null });
1735
+ return { ...j };
1736
+ },
1737
+ heartbeat(jobId, epoch, { now = Date.now(), leaseMs }) { const j = jobs.get(jobId); if (!j || j.status !== 'leased' || j.epoch !== epoch) return false; j.heartbeatAt = now; j.leaseExpiresAt = now + leaseMs; return true; },
1738
+ complete(jobId, epoch, { status = 'succeeded', artifactSha256 = null, summary = null, now = Date.now() }) {
1739
+ const j = jobs.get(jobId); if (!j || j.status !== 'leased' || j.epoch !== epoch) return false;
1740
+ Object.assign(j, { status, artifactSha256, summary, finishedAt: now, leaseExpiresAt: null, error: null }); return true;
1741
+ },
1742
+ fail(jobId, epoch, { error, timedOut = false, now = Date.now(), retryDelayMs = 0 }) {
1743
+ const j = jobs.get(jobId); if (!j || j.status !== 'leased' || j.epoch !== epoch) return null;
1744
+ j.error = String(error ?? '').slice(0, 500); j.leaseExpiresAt = null; j.worker = null;
1745
+ if (j.attempts < j.maxAttempts) { j.status = 'queued'; j.nextAttemptAt = now + retryDelayMs; return { state: 'queued', attempts: j.attempts }; }
1746
+ j.status = timedOut ? 'timed_out' : 'failed'; j.finishedAt = now; return { state: j.status, attempts: j.attempts };
1747
+ },
1748
+ reclaimExpired(runId, { now = Date.now() }) { let n = 0; for (const j of all(runId)) if (j.status === 'leased' && j.leaseExpiresAt != null && j.leaseExpiresAt <= now) { j.status = 'queued'; j.worker = null; j.leaseExpiresAt = null; j.nextAttemptAt = 0; j.error = 'lease expired'; n++; } return n; },
1749
+ reclaimWorker(runId, worker) { let n = 0; for (const j of all(runId)) if (j.status === 'leased' && j.worker === worker) { j.status = 'queued'; j.worker = null; j.leaseExpiresAt = null; j.nextAttemptAt = 0; j.error = 'worker is gone'; n++; } return n; },
1750
+ cancel(runId, { now = Date.now() }) { let n = 0; for (const j of all(runId)) if (!TERMINAL.has(j.status)) { j.status = 'cancelled'; j.finishedAt = now; j.leaseExpiresAt = null; n++; } return n; },
1751
+ retime(runId, timeoutMs) { let n = 0; for (const j of all(runId)) if (j.status === 'queued') { j.timeoutMs = timeoutMs; n++; } return n; },
1752
+ requeueTerminal(runId, statuses) { let n = 0; for (const j of all(runId)) if (statuses.includes(j.status)) { Object.assign(j, { status: 'queued', worker: null, attempts: 0, nextAttemptAt: 0, leaseExpiresAt: null, finishedAt: null, error: null }); n++; } return n; },
1753
+ listJobs: (runId) => all(runId).map((j) => ({ ...j })).sort((a, b) => a.repoId.localeCompare(b.repoId)),
1754
+ getJob: (jobId) => (jobs.get(jobId) ? { ...jobs.get(jobId) } : null),
1755
+ close() {},
1756
+ };
1757
+ return store;
1758
+ }
1759
+
1760
+ // ------------------------------------------------------------------------------------------------------- sqlite
1761
+ const SCHEMA = `
1762
+ CREATE TABLE IF NOT EXISTS fleet_runs (run_id TEXT PRIMARY KEY, organization TEXT NOT NULL, manifest_hash TEXT NOT NULL, config_hash TEXT NOT NULL, scanner_identity TEXT, created_at INTEGER NOT NULL, meta TEXT);
1763
+ CREATE TABLE IF NOT EXISTS fleet_jobs (
1764
+ job_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES fleet_runs(run_id), repo_id TEXT NOT NULL, commit_sha TEXT, cache_key TEXT,
1765
+ status TEXT NOT NULL, worker TEXT, epoch INTEGER NOT NULL DEFAULT 0, lease_expires_at INTEGER, heartbeat_at INTEGER,
1766
+ attempts INTEGER NOT NULL DEFAULT 0, max_attempts INTEGER NOT NULL, timeout_ms INTEGER NOT NULL, next_attempt_at INTEGER NOT NULL DEFAULT 0,
1767
+ artifact_sha256 TEXT, summary TEXT, error TEXT, created_at INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER, required INTEGER NOT NULL DEFAULT 1
1768
+ );
1769
+ CREATE INDEX IF NOT EXISTS fleet_jobs_run_status ON fleet_jobs (run_id, status);
1770
+ `;
1771
+
1772
+ async function createSqliteJobStore({ file }) {
1773
+ const { DatabaseSync } = await Promise.resolve(/* import() */).then(__webpack_require__.t.bind(__webpack_require__, 80099, 19));
1774
+ if (file !== ':memory:') node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_1__.dirname(file), { recursive: true, mode: 0o700 });
1775
+ const db = new DatabaseSync(file);
1776
+ db.exec('PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;');
1777
+ db.exec(SCHEMA);
1778
+ const tx = (fn) => { db.exec('BEGIN IMMEDIATE'); try { const r = fn(); db.exec('COMMIT'); return r; } catch (e) { try { db.exec('ROLLBACK'); } catch { /* already rolled back */ } throw e; } };
1779
+ const q = (sql, ...p) => db.prepare(sql).all(...p);
1780
+ const one = (sql, ...p) => db.prepare(sql).get(...p);
1781
+ const run = (sql, ...p) => db.prepare(sql).run(...p);
1782
+ return {
1783
+ kind: 'sqlite', file,
1784
+ begin(r) { run('INSERT OR IGNORE INTO fleet_runs (run_id, organization, manifest_hash, config_hash, scanner_identity, created_at, meta) VALUES (?,?,?,?,?,?,?)', r.runId, r.organization, r.manifestHash, r.configHash, r.scannerIdentity ?? null, r.createdAt, JSON.stringify(r.meta ?? {})); return this.getRun(r.runId); },
1785
+ getRun(runId) { const r = one('SELECT * FROM fleet_runs WHERE run_id = ?', runId); return r ? { runId: r.run_id, organization: r.organization, manifestHash: r.manifest_hash, configHash: r.config_hash, scannerIdentity: r.scanner_identity, createdAt: r.created_at, meta: JSON.parse(r.meta ?? '{}') } : null; },
1786
+ listRuns() { return q('SELECT * FROM fleet_runs ORDER BY created_at DESC').map((r) => ({ runId: r.run_id, organization: r.organization, manifestHash: r.manifest_hash, createdAt: r.created_at })); },
1787
+ enqueue(runId, list, { now = Date.now() } = {}) {
1788
+ return tx(() => { let created = 0; for (const j of list) { const r = run('INSERT OR IGNORE INTO fleet_jobs (job_id, run_id, repo_id, commit_sha, cache_key, status, max_attempts, timeout_ms, created_at, required) VALUES (?,?,?,?,?,?,?,?,?,?)', j.jobId, runId, j.repoId, j.commit ?? null, j.cacheKey ?? null, 'queued', j.maxAttempts ?? 2, j.timeoutMs, now, j.required === false ? 0 : 1); created += Number(r.changes); } return created; });
1789
+ },
1790
+ claim(runId, { worker, leaseMs, now = Date.now() }) {
1791
+ return tx(() => {
1792
+ const j = one("SELECT job_id FROM fleet_jobs WHERE run_id = ? AND status = 'queued' AND next_attempt_at <= ? ORDER BY created_at, repo_id LIMIT 1", runId, now);
1793
+ if (!j) return null;
1794
+ run("UPDATE fleet_jobs SET status = 'leased', worker = ?, epoch = epoch + 1, attempts = attempts + 1, lease_expires_at = ?, heartbeat_at = ?, started_at = ?, error = NULL WHERE job_id = ?", worker, now + leaseMs, now, now, j.job_id);
1795
+ return jobOf(one('SELECT * FROM fleet_jobs WHERE job_id = ?', j.job_id));
1796
+ });
1797
+ },
1798
+ heartbeat(jobId, epoch, { now = Date.now(), leaseMs }) { return Number(run("UPDATE fleet_jobs SET heartbeat_at = ?, lease_expires_at = ? WHERE job_id = ? AND status = 'leased' AND epoch = ?", now, now + leaseMs, jobId, epoch).changes) === 1; },
1799
+ complete(jobId, epoch, { status = 'succeeded', artifactSha256 = null, summary = null, now = Date.now() }) { return Number(run("UPDATE fleet_jobs SET status = ?, artifact_sha256 = ?, summary = ?, finished_at = ?, lease_expires_at = NULL, error = NULL WHERE job_id = ? AND status = 'leased' AND epoch = ?", status, artifactSha256, summary ? JSON.stringify(summary) : null, now, jobId, epoch).changes) === 1; },
1800
+ fail(jobId, epoch, { error, timedOut = false, now = Date.now(), retryDelayMs = 0 }) {
1801
+ return tx(() => {
1802
+ const j = one("SELECT attempts, max_attempts FROM fleet_jobs WHERE job_id = ? AND status = 'leased' AND epoch = ?", jobId, epoch);
1803
+ if (!j) return null;
1804
+ const msg = String(error ?? '').slice(0, 500);
1805
+ if (j.attempts < j.max_attempts) { run("UPDATE fleet_jobs SET status = 'queued', worker = NULL, lease_expires_at = NULL, next_attempt_at = ?, error = ? WHERE job_id = ?", now + retryDelayMs, msg, jobId); return { state: 'queued', attempts: j.attempts }; }
1806
+ const st = timedOut ? 'timed_out' : 'failed';
1807
+ run('UPDATE fleet_jobs SET status = ?, worker = NULL, lease_expires_at = NULL, finished_at = ?, error = ? WHERE job_id = ?', st, now, msg, jobId);
1808
+ return { state: st, attempts: j.attempts };
1809
+ });
1810
+ },
1811
+ reclaimExpired(runId, { now = Date.now() }) { return Number(run("UPDATE fleet_jobs SET status = 'queued', worker = NULL, lease_expires_at = NULL, next_attempt_at = 0, error = 'lease expired' WHERE run_id = ? AND status = 'leased' AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?", runId, now).changes); },
1812
+ reclaimWorker(runId, worker) { return Number(run("UPDATE fleet_jobs SET status = 'queued', worker = NULL, lease_expires_at = NULL, next_attempt_at = 0, error = 'worker is gone' WHERE run_id = ? AND status = 'leased' AND worker = ?", runId, worker).changes); },
1813
+ cancel(runId, { now = Date.now() }) { return Number(run("UPDATE fleet_jobs SET status = 'cancelled', finished_at = ?, lease_expires_at = NULL WHERE run_id = ? AND status IN ('queued','leased')", now, runId).changes); },
1814
+ retime(runId, timeoutMs) { return Number(run("UPDATE fleet_jobs SET timeout_ms = ? WHERE run_id = ? AND status = 'queued'", timeoutMs, runId).changes); },
1815
+ requeueTerminal(runId, statuses) { return Number(run(`UPDATE fleet_jobs SET status = 'queued', worker = NULL, attempts = 0, next_attempt_at = 0, lease_expires_at = NULL, finished_at = NULL, error = NULL WHERE run_id = ? AND status IN (${statuses.map(() => '?').join(',')})`, runId, ...statuses).changes); },
1816
+ listJobs: (runId) => q('SELECT * FROM fleet_jobs WHERE run_id = ? ORDER BY repo_id', runId).map(jobOf),
1817
+ getJob: (jobId) => jobOf(one('SELECT * FROM fleet_jobs WHERE job_id = ?', jobId)),
1818
+ close() { try { db.close(); } catch { /* already closed */ } },
1819
+ };
1820
+ }
1821
+
1822
+
1823
+ /***/ }),
1824
+
1825
+ /***/ 75435:
1826
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1827
+
1828
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1829
+ /* harmony export */ Vp: () => (/* binding */ renderOrgHealth),
1830
+ /* harmony export */ WB: () => (/* binding */ computeOrgHealth)
1831
+ /* harmony export */ });
1832
+ /* unused harmony export ASSURANCE */
1833
+ /* harmony import */ var _job_store_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20121);
1834
+ // Organization-level scan health (PRD 4, requirement 9). Pure.
1835
+ //
1836
+ // Repositories requested: 1,250
1837
+ // Complete: 1,228 Partial: 11 Failed: 6 Timed out: 5 Not run: ...
1838
+ //
1839
+ // The one rule: an organization is not GREEN while a required repository has no usable result. A repository that was requested but
1840
+ // has no job, was cancelled, is still queued or leased, failed or timed out is counted under its own heading and can never disappear
1841
+ // from the total. `partial` (the scan finished but the engine itself reported a degraded scan) keeps the organization out of GREEN
1842
+ // as well, because a partial scan is not evidence of a clean repository.
1843
+ //
1844
+ // green every required repository has a complete result and no optional repository has a problem
1845
+ // degraded every required repository has a result, but some result is partial, or an optional repository has a problem
1846
+ // red at least one required repository has no result
1847
+
1848
+
1849
+ const ASSURANCE = Object.freeze(['green', 'degraded', 'red']);
1850
+
1851
+ function computeOrgHealth({ manifest, jobs }) {
1852
+ const byRepo = new Map(jobs.map((j) => [j.repoId, j]));
1853
+ const counts = { requested: manifest.repositories.length, complete: 0, partial: 0, failed: 0, timedOut: 0, cancelled: 0, notRun: 0 };
1854
+ const repos = [];
1855
+ for (const r of manifest.repositories) {
1856
+ const j = byRepo.get(r.id);
1857
+ let state;
1858
+ if (!j) state = 'not-run';
1859
+ else if (j.status === 'succeeded') state = 'complete';
1860
+ else if (j.status === 'partial') state = 'partial';
1861
+ else if (j.status === 'failed') state = 'failed';
1862
+ else if (j.status === 'timed_out') state = 'timed-out';
1863
+ else if (j.status === 'cancelled') state = 'cancelled';
1864
+ else state = 'not-run'; // queued or leased: no result yet
1865
+ counts[{ complete: 'complete', partial: 'partial', failed: 'failed', 'timed-out': 'timedOut', cancelled: 'cancelled', 'not-run': 'notRun' }[state]]++;
1866
+ repos.push({ id: r.id, required: r.required, state, attempts: j?.attempts ?? 0, error: j?.error ?? null, cached: !!j?.summary?.cached, findings: j?.summary?.total ?? null, jobStatus: j?.status ?? null, finished: j ? (0,_job_store_js__WEBPACK_IMPORTED_MODULE_0__/* .isTerminal */ .Hd)(j.status) : false });
1867
+ }
1868
+ const noResult = (x) => !['complete', 'partial'].includes(x.state);
1869
+ const requiredMissing = repos.filter((x) => x.required && noResult(x));
1870
+ const requiredPartial = repos.filter((x) => x.required && x.state === 'partial');
1871
+ const optionalProblems = repos.filter((x) => !x.required && x.state !== 'complete');
1872
+ const assurance = requiredMissing.length ? 'red' : (requiredPartial.length || optionalProblems.length) ? 'degraded' : 'green';
1873
+ const reasons = [
1874
+ ...(requiredMissing.length ? [`${requiredMissing.length} required ${requiredMissing.length === 1 ? 'repository has' : 'repositories have'} no result: ${requiredMissing.slice(0, 8).map((x) => `${x.id} (${x.state})`).join(', ')}${requiredMissing.length > 8 ? ', ...' : ''}`] : []),
1875
+ ...(requiredPartial.length ? [`${requiredPartial.length} required ${requiredPartial.length === 1 ? 'repository was' : 'repositories were'} only partially scanned: ${requiredPartial.map((x) => x.id).join(', ')}`] : []),
1876
+ ...(optionalProblems.length ? [`${optionalProblems.length} optional ${optionalProblems.length === 1 ? 'repository has' : 'repositories have'} a problem: ${optionalProblems.map((x) => `${x.id} (${x.state})`).join(', ')}`] : []),
1877
+ ];
1878
+ return { counts, assurance, reasons, repositories: repos };
1879
+ }
1880
+
1881
+ const n = (x) => x.toLocaleString('en-US');
1882
+ function renderOrgHealth(h) {
1883
+ const c = h.counts;
1884
+ const L = [`Repositories requested: ${n(c.requested)}`, `Complete: ${n(c.complete)}`, `Partial: ${n(c.partial)}`, `Failed: ${n(c.failed)}`, `Timed out: ${n(c.timedOut)}`];
1885
+ if (c.cancelled) L.push(`Cancelled: ${n(c.cancelled)}`);
1886
+ if (c.notRun) L.push(`Not run: ${n(c.notRun)}`);
1887
+ L.push('', `Assurance: ${h.assurance.toUpperCase()}`, ...h.reasons.map((r) => ` ${r}`));
1888
+ return L.join('\n');
1889
+ }
1890
+
1891
+
1892
+ /***/ }),
1893
+
1894
+ /***/ 75846:
1895
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1896
+
1897
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1898
+ /* harmony export */ NJ: () => (/* binding */ repoFingerprint),
1899
+ /* harmony export */ aR: () => (/* binding */ writeCache),
1900
+ /* harmony export */ fs: () => (/* binding */ scannerIdentity),
1901
+ /* harmony export */ k$: () => (/* binding */ cacheKey),
1902
+ /* harmony export */ xe: () => (/* binding */ readCache)
1903
+ /* harmony export */ });
1904
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1905
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
1906
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(77598);
1907
+ /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(31421);
1908
+ /* harmony import */ var node_url__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(73136);
1909
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(31174);
1910
+ // The content-addressed scan cache (PRD 4, requirement 5). A repository's scan result is reused only when EVERYTHING that could
1911
+ // change it is unchanged: the commit (and any uncommitted change), the lockfiles, the scan configuration and the scanner itself.
1912
+ //
1913
+ // key = sha256( repository fingerprint, lockfile digests, configuration, scanner identity )
1914
+ //
1915
+ // Anything that cannot be determined makes the key `null`, and a null key means "do not cache": an unknown input must never be
1916
+ // treated as an unchanged one. The scanner identity is the package version plus the SHA-256 recorded for the shipped bundle, so a
1917
+ // change to scanner logic changes the key. A source checkout with no built bundle has no such identity and therefore never reuses
1918
+ // a cached result.
1919
+ //
1920
+ // A cached record carries its own digest; one that was edited, truncated or does not match its key is a miss, never a hit.
1921
+
1922
+
1923
+
1924
+
1925
+
1926
+
1927
+
1928
+ const LOCKFILES = Object.freeze(['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'go.sum', 'Cargo.lock', 'Pipfile.lock', 'poetry.lock', 'requirements.txt', 'Gemfile.lock', 'composer.lock', 'flake.lock', 'cabal.project.freeze', 'stack.yaml.lock']);
1929
+ const SKIP = new Set(['.git', 'node_modules', _state_dir_js__WEBPACK_IMPORTED_MODULE_5__/* .STATE_DIR_NAME */ .Ky]);
1930
+ const sha = (s) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_2__.createHash)('sha256').update(s).digest('hex');
1931
+ const MAX_FILES = 5000; const MAX_FILE_BYTES = 2 * 1024 * 1024; const MAX_TOTAL_BYTES = 128 * 1024 * 1024;
1932
+
1933
+ function git(repo, args, timeout = 10000) {
1934
+ const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_3__.spawnSync)('git', ['-C', repo, ...args], { encoding: 'utf8', timeout, killSignal: 'SIGKILL', maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
1935
+ return r.error || r.status !== 0 ? null : r.stdout;
1936
+ }
1937
+
1938
+ function treeDigest(root) {
1939
+ const h = (0,node_crypto__WEBPACK_IMPORTED_MODULE_2__.createHash)('sha256'); let files = 0; let total = 0; let complete = true;
1940
+ const walk = (d, rel) => {
1941
+ let ents = [];
1942
+ try { ents = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); } catch { complete = false; return; }
1943
+ for (const e of ents) {
1944
+ if (!complete) return;
1945
+ if (SKIP.has(e.name)) continue;
1946
+ const p = node_path__WEBPACK_IMPORTED_MODULE_1__.join(d, e.name); const r = rel ? `${rel}/${e.name}` : e.name;
1947
+ if (e.isSymbolicLink()) { h.update(`L ${r}\0`); continue; }
1948
+ if (e.isDirectory()) { walk(p, r); continue; }
1949
+ if (!e.isFile()) continue;
1950
+ if (++files > MAX_FILES) { complete = false; return; }
1951
+ let st; try { st = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(p); } catch { complete = false; return; }
1952
+ total += st.size;
1953
+ if (total > MAX_TOTAL_BYTES) { complete = false; return; }
1954
+ if (st.size > MAX_FILE_BYTES) { h.update(`B ${r} ${st.size}\0`); continue; }
1955
+ try { h.update(`F ${r}\0${sha(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p))}\0`); } catch { complete = false; return; }
1956
+ }
1957
+ };
1958
+ walk(root, '');
1959
+ return complete ? h.digest('hex') : null;
1960
+ }
1961
+
1962
+ /** What identifies this repository's content right now, or null parts where it cannot be determined. */
1963
+ function repoFingerprint(repoPath) {
1964
+ const head = git(repoPath, ['rev-parse', 'HEAD']);
1965
+ const commit = head ? head.trim() : null;
1966
+ let content = null; let dirty = null;
1967
+ if (commit) {
1968
+ const status = git(repoPath, ['status', '--porcelain=v1', '-uall']);
1969
+ dirty = status == null ? null : status.trim().length > 0;
1970
+ if (dirty === false) content = `git:${commit}`;
1971
+ else if (dirty === true) { const t = treeDigest(repoPath); content = t ? `git+worktree:${commit}:${t}` : null; }
1972
+ } else { const t = treeDigest(repoPath); content = t ? `tree:${t}` : null; }
1973
+ const lock = {};
1974
+ for (const f of LOCKFILES) { try { const st = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(repoPath, f)); if (st.isFile() && st.size <= 32 * 1024 * 1024) lock[f] = sha(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(repoPath, f))); } catch { /* absent */ } }
1975
+ return { commit, dirty, content, lockfiles: lock };
1976
+ }
1977
+
1978
+ /** Package version + the bundle's recorded hash. Null when the scanner cannot be identified (a source checkout), which disables the cache. */
1979
+ function scannerIdentity() {
1980
+ try {
1981
+ const here = node_path__WEBPACK_IMPORTED_MODULE_1__.dirname((0,node_url__WEBPACK_IMPORTED_MODULE_4__.fileURLToPath)(import.meta.url));
1982
+ for (const base of [node_path__WEBPACK_IMPORTED_MODULE_1__.join(here, '..', '..', '..'), node_path__WEBPACK_IMPORTED_MODULE_1__.join(here, '..', '..', '..', '..')]) {
1983
+ const pkg = node_path__WEBPACK_IMPORTED_MODULE_1__.join(base, 'package.json');
1984
+ const sidecar = node_path__WEBPACK_IMPORTED_MODULE_1__.join(base, 'dist', 'agentic-security.mjs.sha256');
1985
+ if (node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(pkg) && node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sidecar)) {
1986
+ const version = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(pkg, 'utf8')).version;
1987
+ const digest = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sidecar, 'utf8').trim().split(/\s+/)[0];
1988
+ if (version && /^[0-9a-f]{64}$/i.test(digest)) return `${version}+${digest}`;
1989
+ }
1990
+ }
1991
+ } catch { /* unknown */ }
1992
+ return null;
1993
+ }
1994
+
1995
+ function cacheKey({ fingerprint, config, identity }) {
1996
+ if (!fingerprint?.content || !identity) return null;
1997
+ return sha(JSON.stringify({ c: fingerprint.content, l: fingerprint.lockfiles, cfg: config, id: identity }));
1998
+ }
1999
+
2000
+ function readCache(dir, key) {
2001
+ if (!key || !/^[0-9a-f]{64}$/.test(key)) return null;
2002
+ try {
2003
+ const doc = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, `${key}.json`), 'utf8'));
2004
+ if (doc.key !== key) return null;
2005
+ const { digest, ...rest } = doc;
2006
+ return digest === sha(JSON.stringify(rest)) ? doc : null;
2007
+ } catch { return null; }
2008
+ }
2009
+
2010
+ function writeCache(dir, key, payload) {
2011
+ if (!key || !/^[0-9a-f]{64}$/.test(key)) return false;
2012
+ try {
2013
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true, mode: 0o700 });
2014
+ const body = { schema: 'agentic-security/fleet-scan-cache', version: 1, key, ...payload };
2015
+ const doc = { ...body, digest: sha(JSON.stringify(body)) };
2016
+ const tmp = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, `.${key}.${process.pid}.tmp`);
2017
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(tmp, JSON.stringify(doc), { mode: 0o600 });
2018
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.renameSync(tmp, node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, `${key}.json`));
2019
+ return true;
2020
+ } catch { return false; }
2021
+ }
2022
+
2023
+
2024
+ /***/ }),
2025
+
2026
+ /***/ 3398:
2027
+ /***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
2028
+
2029
+ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
2030
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2031
+ /* harmony export */ K: () => (/* binding */ END),
2032
+ /* harmony export */ Q: () => (/* binding */ BEGIN)
2033
+ /* harmony export */ });
2034
+ /* harmony import */ var _fleet_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(80313);
2035
+ /* harmony import */ var _runScan_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(45950);
2036
+ var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_runScan_js__WEBPACK_IMPORTED_MODULE_1__]);
2037
+ _runScan_js__WEBPACK_IMPORTED_MODULE_1__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
2038
+ // Child-process entry for ONE repository's scan inside a fleet run (PRD 4, requirement 4: one hung repository must not stop the
2039
+ // fleet). `node scan-child.js <repoPath>`. It exists as its own process so the controller can kill a scan that does not finish,
2040
+ // together with everything the scan started; an in-process scan cannot be interrupted.
2041
+ //
2042
+ // It runs the scan through the existing `runFleet` for a single repository, so the per-repository entry (finding counts, owners
2043
+ // from CODEOWNERS, scan health, policy drift, provenance debt) is exactly what a fleet entry has always been, and prints that entry
2044
+ // plus a slim copy of the findings between markers. Scan state writes are disabled (`AGENTIC_SECURITY_NO_STATE=1`, set by the
2045
+ // controller): scanning a repository for the fleet must not change that repository.
2046
+
2047
+
2048
+
2049
+ const BEGIN = '@@FLEET-SCAN-BEGIN@@';
2050
+ const END = '@@FLEET-SCAN-END@@';
2051
+ const MAX_FINDINGS = 5000;
2052
+
2053
+ const slim = (f) => ({ id: f.id ?? null, stableId: f.stableId ?? null, severity: f.severity ?? null, cwe: f.cwe ?? null, family: f.family ?? null, vuln: f.vuln ?? null, file: f.file ?? null, line: Number.isInteger(f.line) ? f.line : null, parser: f.parser ?? null });
2054
+
2055
+ async function main(repoPath) {
2056
+ let captured = null;
2057
+ const res = await (0,_fleet_js__WEBPACK_IMPORTED_MODULE_0__/* .runFleet */ .gI)({ repos: [repoPath], concurrency: 1, resume: false, runScan: async (p) => { const r = await (0,_runScan_js__WEBPACK_IMPORTED_MODULE_1__.runScan)(p, { provenance: false }); captured = r.scan; return r; } });
2058
+ const entry = res.results?.[0];
2059
+ if (!entry) throw new Error('the scan produced no entry');
2060
+ const findings = (captured?.findings ?? []).slice(0, MAX_FINDINGS).map(slim);
2061
+ process.stdout.write(`${BEGIN}${JSON.stringify({ entry, findings, findingsTruncated: (captured?.findings?.length ?? 0) > MAX_FINDINGS })}${END}`);
2062
+ }
2063
+
2064
+ if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
2065
+ main(process.argv[2]).then(() => process.exit(0), (e) => { process.stderr.write(String(e?.stack || e).slice(0, 4000)); process.exit(1); });
2066
+ }
2067
+
2068
+ __webpack_async_result__();
2069
+ } catch(e) { __webpack_async_result__(e); } });
2070
+
2071
+ /***/ }),
2072
+
2073
+ /***/ 23736:
2074
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
2075
+
2076
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2077
+ /* harmony export */ buildPolicyBundle: () => (/* binding */ buildPolicyBundle),
2078
+ /* harmony export */ ensurePolicyKeyPair: () => (/* binding */ ensurePolicyKeyPair),
2079
+ /* harmony export */ loadPolicyBundles: () => (/* binding */ loadPolicyBundles),
2080
+ /* harmony export */ loadPolicyPublicKey: () => (/* binding */ loadPolicyPublicKey),
2081
+ /* harmony export */ rE: () => (/* binding */ computePolicyDrift),
2082
+ /* harmony export */ resolveEffectivePolicy: () => (/* binding */ resolveEffectivePolicy),
2083
+ /* harmony export */ signPolicyBundle: () => (/* binding */ signPolicyBundle)
2084
+ /* harmony export */ });
2085
+ /* unused harmony exports POLICY_BUNDLE_SCHEMA, SCOPES, canonicalPolicyBytes, verifyPolicyBundle */
2086
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
2087
+ /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
2088
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
2089
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(77598);
2090
+ /* harmony import */ var _evidence_bundle_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(98317);
2091
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(31174);
2092
+ // Signed, portable policy bundles (assurance-hardening PRD FR-1001).
2093
+ //
2094
+ // "Support signed portable policy bundles with organization, repository, and
2095
+ // environment inheritance" | "Tampered or expired policy is rejected;
2096
+ // effective policy is explainable."
2097
+ //
2098
+ // WHY REUSE evidence-bundle.js's Ed25519 SCHEME, NOT integrity.js's HMAC
2099
+ // -------------------------------------------------------------------------
2100
+ // A policy bundle is authored by one party (typically a central security
2101
+ // team, "the organization") and distributed to many repositories that never
2102
+ // had the signing key. integrity.js's per-install symmetric HMAC is
2103
+ // tamper-evidence for the SAME install that wrote it — a different install
2104
+ // verifying a bundle it didn't sign is exactly the case that scheme cannot
2105
+ // serve. evidence-bundle.js already solved this for findings: Ed25519,
2106
+ // verify with only the public key. This module reuses that primitive
2107
+ // directly (canonicalisation shape, allowlist-signed-fields discipline,
2108
+ // exclusive-create key generation) rather than inventing a third signing
2109
+ // mechanism, per this session's own established "survey before building a
2110
+ // parallel primitive" practice (see D-0023/D-0025 for the same discipline
2111
+ // applied to identity and exception mechanisms respectively).
2112
+ //
2113
+ // EA-03 LESSON, APPLIED HERE FROM THE START
2114
+ // -------------------------------------------------------------------------
2115
+ // evidence-bundle.js's own history (see its EA-03 comment) is the reason
2116
+ // `canonicalPolicyBytes` signs an explicit ALLOWLIST and `verifyPolicyBundle`
2117
+ // REJECTS any top-level key outside it: a bundle with a field stapled on
2118
+ // after signing must fail verification, not silently pass with the addition
2119
+ // unverified. Getting this right on the first pass — rather than discovering
2120
+ // the gap the way evidence-bundle.js did — is the entire point of writing it
2121
+ // down here.
2122
+ //
2123
+ // SCOPE — WHAT THIS MODULE DOES NOT DO
2124
+ // -------------------------------------------------------------------------
2125
+ // It does not invent a policy SCHEMA — `policy` is an opaque object; whatever
2126
+ // keys a bundle carries (egress rules, severity floors, approved providers,
2127
+ // anything else this repo already treats as policy) are merged the same way.
2128
+ // It does not implement deep/recursive merging — inheritance is shallow,
2129
+ // top-level-key override (organization -> repository -> environment, most
2130
+ // specific wins per key), matching how every other flat policy config file
2131
+ // in this repo (egress-policy.yml, compliance-severity-policy.json, …) is
2132
+ // already shaped. A future consumer needing nested-key inheritance is new,
2133
+ // separate scope, not assumed here.
2134
+
2135
+
2136
+
2137
+
2138
+
2139
+
2140
+
2141
+
2142
+ const POLICY_BUNDLE_SCHEMA = 'agentic-security/policy-bundle@1';
2143
+
2144
+ const SCOPES = ['organization', 'repository', 'environment'];
2145
+ // Inheritance order — later entries override earlier ones, per top-level key.
2146
+ const INHERITANCE_ORDER = ['organization', 'repository', 'environment'];
2147
+
2148
+ function keyDir() {
2149
+ const xdg = process.env.XDG_CONFIG_HOME || node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.config');
2150
+ return node_path__WEBPACK_IMPORTED_MODULE_2__.join(xdg, 'agentic-security', 'policy-bundles');
2151
+ }
2152
+
2153
+ /** A SEPARATE Ed25519 keypair from evidence-bundle.js's finding-attestation
2154
+ * key — signing "this policy is authentic" and "this finding really came
2155
+ * from a scan run by me" are different trust domains and must not share a
2156
+ * key. Reuses evidence-bundle.js's hardened, race-safe generation logic
2157
+ * directly (its exclusive-create collision handling is the kind of code
2158
+ * worth NOT re-implementing) rather than duplicating it — the separation
2159
+ * that matters is the DIRECTORY (`policy-bundles/`, isolated from
2160
+ * evidence-bundle.js's own key dir), not the leaf filenames, which are
2161
+ * `attest-key.pem`/`attest-key.pub.pem` regardless of caller (a
2162
+ * evidence-bundle.js internal, not overridable here) — a readability wart,
2163
+ * not a security one, since the directory is what prevents key reuse. */
2164
+ function ensurePolicyKeyPair(dir = keyDir()) {
2165
+ return (0,_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_4__.ensureKeyPair)(dir);
2166
+ }
2167
+
2168
+ /**
2169
+ * Deterministic JSON, keys sorted at every level — identical algorithm to
2170
+ * evidence-bundle.js's canonicalJson (duplicated rather than imported: it is
2171
+ * a pure, three-line function, and importing it would couple this module's
2172
+ * signature format to evidence-bundle.js's internals for no real benefit).
2173
+ */
2174
+ function canonicalJson(value) {
2175
+ if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null);
2176
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
2177
+ const keys = Object.keys(value).sort();
2178
+ return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
2179
+ }
2180
+
2181
+ // The complete set of fields a legitimately-built bundle carries, BEFORE
2182
+ // signing. This is the signed allowlist AND (in verifyPolicyBundle) the
2183
+ // complete set of keys a signed bundle is permitted to have alongside
2184
+ // `signature` — see the EA-03 note above for why both matter.
2185
+ const BUNDLE_FIELDS = ['schema', 'scope', 'policy', 'issuedAt', 'expiresAt'];
2186
+ const BUNDLE_TOP_LEVEL_KEYS = new Set([...BUNDLE_FIELDS, 'signature']);
2187
+
2188
+ function canonicalPolicyBytes(bundle) {
2189
+ const signed = {};
2190
+ for (const k of BUNDLE_FIELDS) signed[k] = bundle[k] ?? null;
2191
+ return Buffer.from(canonicalJson(signed), 'utf8');
2192
+ }
2193
+
2194
+ /**
2195
+ * Build an unsigned bundle. `policy` is caller-supplied and opaque — this
2196
+ * module does not validate its shape, only its provenance and freshness.
2197
+ */
2198
+ function buildPolicyBundle(scope, policy, { issuedAt, expiresAt } = {}) {
2199
+ if (!SCOPES.includes(scope)) return null;
2200
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) return null;
2201
+ return {
2202
+ schema: POLICY_BUNDLE_SCHEMA,
2203
+ scope,
2204
+ policy,
2205
+ issuedAt: issuedAt ?? new Date().toISOString(),
2206
+ expiresAt: expiresAt ?? null,
2207
+ };
2208
+ }
2209
+
2210
+ /** Sign a bundle. Returns a new object; the input is not mutated. */
2211
+ function signPolicyBundle(bundle, privateKeyPem) {
2212
+ const sig = node_crypto__WEBPACK_IMPORTED_MODULE_3__.sign(null, canonicalPolicyBytes(bundle), privateKeyPem);
2213
+ return {
2214
+ ...bundle,
2215
+ signature: { algorithm: 'ed25519', canonicalisation: POLICY_BUNDLE_SCHEMA, value: sig.toString('base64') },
2216
+ };
2217
+ }
2218
+
2219
+ /**
2220
+ * Verify a bundle: signature authenticity AND freshness. Never throws — a
2221
+ * malformed or tampered bundle from an untrusted source (a repo checking out
2222
+ * whatever an org distributed) is an expected input, not exceptional.
2223
+ *
2224
+ * @returns {{ok: boolean, reason: string|null}}
2225
+ */
2226
+ function verifyPolicyBundle(bundle, publicKeyPem, { now = Date.now() } = {}) {
2227
+ if (!bundle || typeof bundle !== 'object') return { ok: false, reason: 'bundle is not an object' };
2228
+ if (bundle.schema !== POLICY_BUNDLE_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
2229
+ if (!SCOPES.includes(bundle.scope)) return { ok: false, reason: `unrecognised scope: ${bundle.scope}` };
2230
+ const unknownKeys = Object.keys(bundle).filter(k => !BUNDLE_TOP_LEVEL_KEYS.has(k));
2231
+ if (unknownKeys.length) {
2232
+ return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
2233
+ }
2234
+ const sig = bundle.signature;
2235
+ if (!sig?.value) return { ok: false, reason: 'bundle is unsigned' };
2236
+ if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
2237
+ if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
2238
+ let sigOk = false;
2239
+ try {
2240
+ sigOk = node_crypto__WEBPACK_IMPORTED_MODULE_3__.verify(null, canonicalPolicyBytes(bundle), publicKeyPem, Buffer.from(sig.value, 'base64'));
2241
+ } catch (e) {
2242
+ return { ok: false, reason: `verification error: ${e.message}` };
2243
+ }
2244
+ if (!sigOk) return { ok: false, reason: 'signature does not match the bundle contents — it was modified after signing' };
2245
+ if (bundle.expiresAt) {
2246
+ const exp = Date.parse(bundle.expiresAt);
2247
+ if (!Number.isFinite(exp)) return { ok: false, reason: `expiresAt is not a valid date: ${bundle.expiresAt}` };
2248
+ if (exp < now) return { ok: false, reason: `policy expired on ${bundle.expiresAt}` };
2249
+ }
2250
+ return { ok: true, reason: null };
2251
+ }
2252
+
2253
+ /**
2254
+ * Merge verified bundles into one effective policy, in inheritance order
2255
+ * (organization -> repository -> environment; most specific wins per key).
2256
+ * A tampered or expired bundle is EXCLUDED from the merge (rejected, not
2257
+ * silently dropped — its scope and reason are reported so a rejection is
2258
+ * visible, not just absent). `provenance[key]` names which scope's bundle
2259
+ * last set that key — the "effective policy is explainable" half of the
2260
+ * acceptance criterion.
2261
+ *
2262
+ * @param {Array<{scope: string, bundle: object}>} entries
2263
+ * @param {string} publicKeyPem
2264
+ * @returns {{effective: object, provenance: Record<string,string>,
2265
+ * accepted: string[], rejected: Array<{scope:string, reason:string}>}}
2266
+ */
2267
+ function resolveEffectivePolicy(entries, publicKeyPem, { now = Date.now() } = {}) {
2268
+ const byScope = new Map();
2269
+ for (const e of entries || []) {
2270
+ if (e && SCOPES.includes(e.scope)) byScope.set(e.scope, e.bundle);
2271
+ }
2272
+ const effective = {};
2273
+ const provenance = {};
2274
+ const accepted = [];
2275
+ const rejected = [];
2276
+ for (const scope of INHERITANCE_ORDER) {
2277
+ const bundle = byScope.get(scope);
2278
+ if (!bundle) continue;
2279
+ const v = verifyPolicyBundle(bundle, publicKeyPem, { now });
2280
+ if (!v.ok) { rejected.push({ scope, reason: v.reason }); continue; }
2281
+ accepted.push(scope);
2282
+ for (const [k, val] of Object.entries(bundle.policy)) {
2283
+ effective[k] = val;
2284
+ provenance[k] = scope;
2285
+ }
2286
+ }
2287
+ return { effective, provenance, accepted, rejected };
2288
+ }
2289
+
2290
+ /**
2291
+ * Load whichever policy bundle files exist under
2292
+ * `.agentic-security/policy-bundles/{organization,repository,environment}.json`.
2293
+ * Degrades gracefully — a missing directory or missing/malformed individual
2294
+ * file is simply absent from the result, never thrown. Read-first-in-try/
2295
+ * catch throughout (no existsSync-then-readFileSync — D-0012/D-0022).
2296
+ */
2297
+ function loadPolicyBundles(scanRoot) {
2298
+ if (!scanRoot) return [];
2299
+ const entries = [];
2300
+ for (const scope of SCOPES) {
2301
+ let fp;
2302
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_5__.statePath)(scanRoot, 'policy-bundles', `${scope}.json`); } catch { continue; }
2303
+ let raw;
2304
+ try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { continue; }
2305
+ try {
2306
+ const bundle = JSON.parse(raw);
2307
+ if (bundle && typeof bundle === 'object') entries.push({ scope, bundle });
2308
+ } catch { /* malformed — skip, do not throw */ }
2309
+ }
2310
+ return entries;
2311
+ }
2312
+
2313
+ /**
2314
+ * Read the operator-supplied public key an org distributes alongside its
2315
+ * bundles, from `.agentic-security/policy-bundle-public-key.pem`. Returns
2316
+ * null (not a throw) if absent or unreadable — resolution then rejects
2317
+ * every bundle for lack of a key, same as any other "no public key
2318
+ * supplied" case in verifyPolicyBundle.
2319
+ */
2320
+ function loadPolicyPublicKey(scanRoot) {
2321
+ if (!scanRoot) return null;
2322
+ let fp;
2323
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_5__.statePath)(scanRoot, 'policy-bundle-public-key.pem'); } catch { return null; }
2324
+ try { return node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return null; }
2325
+ }
2326
+
2327
+ /**
2328
+ * FR-1006 ("policy drift"): does this repository's LOCALLY resolved effective
2329
+ * policy diverge from what the organization-scope bundle alone specifies —
2330
+ * and is anything the repo is relying on actually being rejected (tampered
2331
+ * or expired) rather than enforced? Both are GOVERNANCE gaps, distinct from
2332
+ * risk findings, which is the exact distinction FR-1006's acceptance
2333
+ * criterion asks fleet output to make.
2334
+ *
2335
+ * No bundles configured at all is a no-op (`null`), matching this session's
2336
+ * established convention: drift can only be reported relative to a
2337
+ * baseline, and a repo with no organization bundle has no baseline to drift
2338
+ * from.
2339
+ *
2340
+ * @returns {{accepted: string[], rejected: Array<{scope,reason}>,
2341
+ * overrides: Array<{key, organizationValue, effectiveValue, overriddenBy}>}
2342
+ * | null}
2343
+ */
2344
+ function computePolicyDrift(scanRoot) {
2345
+ const entries = loadPolicyBundles(scanRoot);
2346
+ if (!entries.length) return null;
2347
+ const publicKeyPem = loadPolicyPublicKey(scanRoot);
2348
+ const { effective, provenance, accepted, rejected } = resolveEffectivePolicy(entries, publicKeyPem);
2349
+
2350
+ const overrides = [];
2351
+ const orgEntry = entries.find(e => e.scope === 'organization');
2352
+ if (orgEntry && accepted.includes('organization')) {
2353
+ for (const [key, organizationValue] of Object.entries(orgEntry.bundle.policy || {})) {
2354
+ const overriddenBy = provenance[key];
2355
+ if (overriddenBy && overriddenBy !== 'organization' && !_deepEqual(effective[key], organizationValue)) {
2356
+ overrides.push({ key, organizationValue, effectiveValue: effective[key], overriddenBy });
2357
+ }
2358
+ }
2359
+ }
2360
+ return { accepted, rejected, overrides };
2361
+ }
2362
+
2363
+ function _deepEqual(a, b) {
2364
+ return JSON.stringify(a) === JSON.stringify(b);
2365
+ }
2366
+
2367
+
2368
+ /***/ })
2369
+
2370
+ };