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

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 (195) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +779 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +2 -2
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/{271.index.js → 2271.index.js} +2 -2
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +379 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +5 -5
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +5 -5
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +2 -2
  35. package/dist/5051.index.js +841 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +3 -3
  42. package/dist/{449.index.js → 5830.index.js} +2 -2
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +3 -3
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +560 -189
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/dist/frontend/index.html +21 -0
  72. package/dist/frontend/src/app.js +176 -0
  73. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  74. package/dist/frontend/src/components/filter-rail.js +119 -0
  75. package/dist/frontend/src/components/query-bar.js +126 -0
  76. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  77. package/dist/frontend/src/export-entry.js +36 -0
  78. package/dist/frontend/src/lib/api-client.js +92 -0
  79. package/dist/frontend/src/lib/contrast.js +34 -0
  80. package/dist/frontend/src/lib/dom.js +24 -0
  81. package/dist/frontend/src/lib/escape-html.js +16 -0
  82. package/dist/frontend/src/lib/flow-path.js +40 -0
  83. package/dist/frontend/src/lib/focus-controls.js +149 -0
  84. package/dist/frontend/src/lib/protection-visual.js +46 -0
  85. package/dist/frontend/src/lib/query-language.js +240 -0
  86. package/dist/frontend/src/lib/row-filters.js +43 -0
  87. package/dist/frontend/src/lib/state.js +84 -0
  88. package/dist/frontend/src/main.js +83 -0
  89. package/dist/frontend/src/shell.js +184 -0
  90. package/dist/frontend/src/views/architecture-view.js +798 -0
  91. package/dist/frontend/src/views/inventory-view.js +292 -0
  92. package/dist/frontend/src/views/privacy-view.js +172 -0
  93. package/dist/frontend/src/views/trace-view.js +206 -0
  94. package/dist/frontend/styles/architecture-view.css +93 -0
  95. package/dist/frontend/styles/filter-rail.css +34 -0
  96. package/dist/frontend/styles/inspector.css +69 -0
  97. package/dist/frontend/styles/inventory-view.css +74 -0
  98. package/dist/frontend/styles/privacy-view.css +86 -0
  99. package/dist/frontend/styles/query-bar.css +107 -0
  100. package/dist/frontend/styles/shell.css +155 -0
  101. package/dist/frontend/styles/tokens.css +128 -0
  102. package/dist/frontend/styles/trace-view.css +95 -0
  103. package/package.json +13 -6
  104. package/src/dataflow/CLAUDE.md +1 -1
  105. package/src/dataflow/catalog.js +42 -0
  106. package/src/dataflow/orm-write-catalog.js +175 -0
  107. package/src/engine.js +92 -1
  108. package/src/ir/CLAUDE.md +1 -0
  109. package/src/ir/chrome-probe.mjs +150 -0
  110. package/src/ir/parser-js.js +94 -7
  111. package/src/lineage/CLAUDE.md +1203 -0
  112. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  113. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  114. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  115. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  116. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  117. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  118. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  119. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  120. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  121. package/src/lineage/classification.js +56 -0
  122. package/src/lineage/coverage.js +658 -0
  123. package/src/lineage/cross-repo-link.js +107 -0
  124. package/src/lineage/dataflow-graph.schema.json +184 -0
  125. package/src/lineage/decision-story.js +206 -0
  126. package/src/lineage/drift-policy.js +279 -0
  127. package/src/lineage/driver.js +135 -0
  128. package/src/lineage/engine.js +992 -0
  129. package/src/lineage/export-briefing.js +628 -0
  130. package/src/lineage/export-csv.js +62 -0
  131. package/src/lineage/export-json.js +238 -0
  132. package/src/lineage/export-privacy.js +258 -0
  133. package/src/lineage/federation-loader.js +111 -0
  134. package/src/lineage/field-identity.js +78 -0
  135. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  136. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  137. package/src/lineage/flow-grade.js +221 -0
  138. package/src/lineage/governance-edit.js +169 -0
  139. package/src/lineage/graph-builder.js +1114 -0
  140. package/src/lineage/graph-diff.js +431 -0
  141. package/src/lineage/graph-snapshot.js +180 -0
  142. package/src/lineage/handling-analyzer.js +168 -0
  143. package/src/lineage/ids.js +349 -0
  144. package/src/lineage/impact-assessment.js +76 -0
  145. package/src/lineage/impact-engine.js +268 -0
  146. package/src/lineage/index.js +281 -0
  147. package/src/lineage/language-coverage-tiers.js +58 -0
  148. package/src/lineage/obligation-mapping.js +126 -0
  149. package/src/lineage/obligation-predicates.js +235 -0
  150. package/src/lineage/observation-adapters.js +282 -0
  151. package/src/lineage/observation-correlation.js +622 -0
  152. package/src/lineage/observation-store.js +497 -0
  153. package/src/lineage/path-query.js +410 -0
  154. package/src/lineage/path-store.js +400 -0
  155. package/src/lineage/protection.js +53 -0
  156. package/src/lineage/recipient-profile.js +192 -0
  157. package/src/lineage/recipient-registry.js +394 -0
  158. package/src/lineage/redact-graph.js +224 -0
  159. package/src/lineage/remediation.js +417 -0
  160. package/src/lineage/resolve-destination.js +91 -0
  161. package/src/lineage/runtime-observation.js +464 -0
  162. package/src/lineage/scenario-diff.js +84 -0
  163. package/src/lineage/scenario-engine.js +251 -0
  164. package/src/lineage/scenario.js +101 -0
  165. package/src/lineage/schema.js +167 -0
  166. package/src/lineage/sink-registry.js +427 -0
  167. package/src/lineage/source-registry.js +357 -0
  168. package/src/lineage/source-seeding.js +212 -0
  169. package/src/lineage/summaries.js +590 -0
  170. package/src/lineage/transform-catalog.js +397 -0
  171. package/src/lineage/transit-protection.js +150 -0
  172. package/src/lineage/validate.js +285 -0
  173. package/src/mcp/CLAUDE.md +7 -1
  174. package/src/mcp/dataflow-tools.js +160 -0
  175. package/src/mcp/server.js +1 -1
  176. package/src/mcp/tools.js +2 -1
  177. package/src/pipeline/scan-health.js +19 -1
  178. package/src/posture/CLAUDE.md +19 -0
  179. package/src/posture/artifact-registry.js +52 -0
  180. package/src/posture/auditor-walkthrough.js +76 -0
  181. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  182. package/src/posture/obligation-evidence-pack.js +202 -0
  183. package/src/posture/remediation-ledger.js +337 -0
  184. package/src/server/CLAUDE.md +47 -0
  185. package/src/server/graph-loader.js +141 -0
  186. package/src/server/http-server.js +325 -0
  187. package/src/server/routes.js +129 -0
  188. package/src/server/security.js +111 -0
  189. package/src/server/static-assets.js +144 -0
  190. package/src/shared/frontend-root.js +52 -0
  191. package/dist/11.index.js +0 -353
  192. package/dist/259.index.js +0 -975
  193. package/dist/317.index.js +0 -300
  194. package/dist/609.index.js +0 -741
  195. package/dist/838.index.js +0 -152
@@ -0,0 +1,779 @@
1
+ export const id = 1122;
2
+ export const ids = [1122,3180];
3
+ export const modules = {
4
+
5
+ /***/ 1122:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+
9
+ // EXPORTS
10
+ __webpack_require__.d(__webpack_exports__, {
11
+ exportPdf: () => (/* binding */ exportPdf),
12
+ exportPng: () => (/* binding */ exportPng),
13
+ exportSvg: () => (/* binding */ exportSvg)
14
+ });
15
+
16
+ // UNUSED EXPORTS: _validTimeoutMs
17
+
18
+ // EXTERNAL MODULE: external "node:fs"
19
+ var external_node_fs_ = __webpack_require__(3024);
20
+ // EXTERNAL MODULE: external "node:os"
21
+ var external_node_os_ = __webpack_require__(8161);
22
+ // EXTERNAL MODULE: external "node:path"
23
+ var external_node_path_ = __webpack_require__(6760);
24
+ // EXTERNAL MODULE: external "node:child_process"
25
+ var external_node_child_process_ = __webpack_require__(1421);
26
+ // EXTERNAL MODULE: external "node:url"
27
+ var external_node_url_ = __webpack_require__(3136);
28
+ // EXTERNAL MODULE: ./scripts/generate-html-report.mjs + 1 modules
29
+ var generate_html_report = __webpack_require__(3180);
30
+ ;// CONCATENATED MODULE: ./src/ir/chrome-probe.mjs
31
+ // chrome-probe.mjs — Milestone 4, sub-project PNG/SVG/PDF export.
32
+ //
33
+ // Chrome/Chromium binary discovery, mirroring parser-py-cst.js's own
34
+ // already-proven probePythonAvailable() pattern exactly — this
35
+ // codebase's established convention for "optional local tool, detect
36
+ // and degrade gracefully" (see that file's own header comment for the
37
+ // full rationale this file inherits without repeating).
38
+
39
+
40
+
41
+
42
+ let _capability = null;
43
+
44
+ // A malformed env var (e.g. AGENTIC_SECURITY_CHROME_PROBE_TIMEOUT_MS=oops)
45
+ // must fall back to the default, never reach spawnSync as NaN — found by
46
+ // the final whole-branch review: `Number('oops')` is NaN, and NaN as a
47
+ // spawnSync `timeout` option throws ERR_OUT_OF_RANGE synchronously.
48
+ // _tryBinary's own try/catch swallows that throw and returns null for
49
+ // EVERY candidate, so probeChromeAvailable would misreport
50
+ // `no-chrome-found` on a machine with a perfectly working Chrome install.
51
+ //
52
+ // Blank/whitespace-only is treated as unset (falls back), not as `0` —
53
+ // found by this fix's own scoped re-review: `Number('')` is `0`, and a
54
+ // naive `n >= 0` guard let an empty-but-exported env var (a blanked
55
+ // .env line, an unset-but-exported CI var) through as a real `timeout:
56
+ // 0`, which spawnSync treats as NO timeout at all — an unbounded hang
57
+ // risk inside this exact capability probe, the failure class
58
+ // parser-py-cst.js's own probePythonAvailable() is already careful
59
+ // about. An explicit `"0"` is still honored (matches this file's
60
+ // pre-fix behavior for that literal value).
61
+ //
62
+ // Must be a safe INTEGER, not merely finite — found by a second scoped
63
+ // re-review, reproduced live: `Number.isFinite(1.5)` is true, so a
64
+ // fractional env var (e.g. "1.5") passed this guard unchanged and then
65
+ // hit spawnSync's own `timeout` option, which throws ERR_OUT_OF_RANGE
66
+ // for any non-integer — the exact class of uncaught throw this whole
67
+ // function exists to prevent, just for a different malformed input
68
+ // than the original NaN case. `-0` is rejected too (Object.is check):
69
+ // it is a safe integer and `-0 >= 0`, so without the explicit check it
70
+ // would silently mean "no timeout", one character away from the "0"
71
+ // this function deliberately allows.
72
+ // Exported test-only — the outcome-based integration tests (a real
73
+ // probe/export succeeding or not) can't distinguish `timeout: 0` from
74
+ // `timeout: <default>` when the underlying command finishes quickly
75
+ // either way, so a direct table of this function's own input/output
76
+ // pairs is the only thing that actually pins the "0"-vs-default
77
+ // boundary — found by a third scoped re-review: the blank-env-var
78
+ // integration test below passed even with the round-2 blank-handling
79
+ // bug fully reintroduced, for exactly this reason.
80
+ function _validTimeoutMs(raw, fallback) {
81
+ const s = String(raw ?? '').trim();
82
+ if (!s) return fallback;
83
+ const n = Number(s);
84
+ if (!Number.isSafeInteger(n) || n < 0 || Object.is(n, -0)) return fallback;
85
+ return n;
86
+ }
87
+ const PROBE_TIMEOUT_MS = _validTimeoutMs(process.env.AGENTIC_SECURITY_CHROME_PROBE_TIMEOUT_MS, 5000);
88
+
89
+ // Real, common install locations, per platform. Deliberately excludes
90
+ // AGENTIC_SECURITY_CHROME_PATH — that override is handled separately in
91
+ // probeChromeAvailable() as an authoritative, non-fallback choice (see
92
+ // the comment there for why).
93
+ //
94
+ // KNOWN-GOOD ABSOLUTE PATHS ARE TRIED FIRST, PATH-resolvable bare names
95
+ // LAST — found by the final whole-branch review, reproduced live: with
96
+ // bare names tried first, a `chrome` shell script anywhere earlier on
97
+ // PATH than the real browser is silently preferred and then executed
98
+ // with this process's own render arguments. The `--version` output
99
+ // check (`_tryBinary` below) is a functional smoke test, not a security
100
+ // gate — it cannot distinguish a real Chrome/Chromium binary from any
101
+ // script that echoes a matching string. Preferring the well-known
102
+ // install locations narrows, though does not eliminate, this exposure
103
+ // (a compromised PATH entry named exactly `chrome`/`google-chrome`/etc.
104
+ // is still tried if no absolute-path candidate exists on this machine —
105
+ // this feature already assumes local machine trust, same as every other
106
+ // "optional local tool" this codebase shells out to). The `linux` list
107
+ // below was empty in this fix's first pass — found by the scoped
108
+ // re-review to make the whole reordering a no-op on the platform CI
109
+ // images actually run on, since every Linux candidate was a bare PATH
110
+ // name. Package-manager and snap install locations are now included.
111
+ function _candidatePaths() {
112
+ const onPath = ['google-chrome-stable', 'google-chrome', 'chromium-browser', 'chromium', 'chrome'];
113
+ const platformPaths = {
114
+ darwin: [
115
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
116
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
117
+ `${process.env.HOME || ''}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
118
+ ],
119
+ win32: [
120
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
121
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
122
+ `${process.env.LOCALAPPDATA || ''}\\Google\\Chrome\\Application\\chrome.exe`,
123
+ ],
124
+ linux: [
125
+ '/usr/bin/google-chrome-stable',
126
+ '/usr/bin/google-chrome',
127
+ '/usr/bin/chromium-browser',
128
+ '/usr/bin/chromium',
129
+ '/opt/google/chrome/chrome',
130
+ '/snap/bin/chromium',
131
+ ],
132
+ };
133
+ const platformSpecific = platformPaths[process.platform] || [];
134
+ return [...platformSpecific, ...onPath];
135
+ }
136
+
137
+ // Tries one candidate binary. Returns {ok:true, chrome:bin} on a real,
138
+ // working Chrome/Chromium, or null on any failure (missing binary, spawn
139
+ // error, non-zero exit, unrecognized --version output) — never throws.
140
+ function _tryBinary(bin) {
141
+ if (!bin) return null;
142
+ // An absolute-path candidate that doesn't exist can't spawn — skip the
143
+ // spawnSync call entirely rather than let it throw ENOENT.
144
+ if (bin.includes('/') || bin.includes('\\')) {
145
+ if (!external_node_fs_.existsSync(bin)) return null;
146
+ }
147
+ let r;
148
+ try {
149
+ r = external_node_child_process_.spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: PROBE_TIMEOUT_MS });
150
+ } catch { return null; }
151
+ if (r.error || r.status !== 0) return null;
152
+ const out = (r.stdout || r.stderr || '');
153
+ if (!/Chrom(e|ium)/i.test(out)) return null;
154
+ return { ok: true, chrome: bin };
155
+ }
156
+
157
+ function resetChromeProbe() { _capability = null; }
158
+
159
+ function probeChromeAvailable() {
160
+ if (_capability) return _capability;
161
+
162
+ // An explicit override is authoritative, not just "try this one first
163
+ // then fall back": an operator (or CI environment) who deliberately
164
+ // pointed AGENTIC_SECURITY_CHROME_PATH at a binary wants a failure
165
+ // reported when that binary doesn't work — not a silent switch to
166
+ // whatever else this process happens to auto-detect on the machine.
167
+ const fromEnv = process.env.AGENTIC_SECURITY_CHROME_PATH;
168
+ if (fromEnv) {
169
+ const hit = _tryBinary(fromEnv);
170
+ _capability = hit || { ok: false, reason: 'chrome-path-invalid' };
171
+ return _capability;
172
+ }
173
+
174
+ for (const bin of _candidatePaths()) {
175
+ const hit = _tryBinary(bin);
176
+ if (hit) { _capability = hit; return _capability; }
177
+ }
178
+ _capability = { ok: false, reason: 'no-chrome-found' };
179
+ return _capability;
180
+ }
181
+
182
+ ;// CONCATENATED MODULE: ./scripts/export-image.mjs
183
+ // export-image.mjs — Milestone 4, sub-project PNG/SVG/PDF export.
184
+ //
185
+ // Builds on the already-shipped self-contained HTML report
186
+ // (generate-html-report.mjs) and a real, locally discovered Chrome
187
+ // binary's own native headless flags — empirically confirmed this
188
+ // sub-project's own scoping investigation to produce deterministic PNG
189
+ // (byte-identical across repeated runs), valid PDF, and (via DOM
190
+ // extraction) valid SVG, with zero new code needed to render anything
191
+ // and zero new npm dependency.
192
+
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201
+ // A malformed env var (e.g. AGENTIC_SECURITY_CHROME_RENDER_TIMEOUT_MS=oops)
202
+ // must fall back to the default, never reach spawnSync as NaN — NaN as a
203
+ // spawnSync `timeout` option throws ERR_OUT_OF_RANGE synchronously, which
204
+ // none of the three export functions below catch (their try/finally only
205
+ // guards cleanup, not this construction-time computation).
206
+ //
207
+ // Blank/whitespace-only is treated as unset (falls back), not as `0` —
208
+ // found by this fix's own scoped re-review: `Number('')` is `0`, and a
209
+ // naive `n >= 0` guard let an empty-but-exported env var through as a
210
+ // real `timeout: 0`, which spawnSync treats as NO timeout at all — an
211
+ // unbounded-hang risk on a wedged Chrome. An explicit `"0"` is still
212
+ // honored (matches this file's pre-fix behavior for that literal
213
+ // value).
214
+ //
215
+ // Must be a safe INTEGER, not merely finite — found by a second scoped
216
+ // re-review, reproduced live: `Number.isFinite(1.5)` is true, so a
217
+ // fractional env var (e.g. "1.5") passed this guard unchanged and then
218
+ // hit spawnSync's own `timeout` option, which throws ERR_OUT_OF_RANGE
219
+ // for any non-integer. `-0` is rejected too (Object.is check): it is a
220
+ // safe integer and `-0 >= 0`, so it would otherwise silently mean "no
221
+ // timeout", one character away from the "0" this function deliberately
222
+ // allows. Kept identical to chrome-probe.mjs's own copy of this
223
+ // function rather than factored into a shared module — two small
224
+ // functions, not worth a new shared file for.
225
+ // Exported test-only — see chrome-probe.mjs's own copy of this
226
+ // function for why a direct input/output table, not an integration
227
+ // test, is what actually pins the "0"-vs-default boundary.
228
+ function export_image_validTimeoutMs(raw, fallback) {
229
+ const s = String(raw ?? '').trim();
230
+ if (!s) return fallback;
231
+ const n = Number(s);
232
+ if (!Number.isSafeInteger(n) || n < 0 || Object.is(n, -0)) return fallback;
233
+ return n;
234
+ }
235
+ // 15000 (the original default) left no headroom: the v0.147.0 release run
236
+ // took 7983ms for this same render on a good draw of the CI runner, and the
237
+ // v0.147.1 run timed out at ~15000ms on a slower draw — a 2x runner-speed
238
+ // swing entirely within normal GitHub Actions variance, not a regression.
239
+ // 30000 gives ~4x headroom over the observed good-case time.
240
+ const RENDER_TIMEOUT_MS = export_image_validTimeoutMs(process.env.AGENTIC_SECURITY_CHROME_RENDER_TIMEOUT_MS, 30000);
241
+
242
+ function _writeTempHtml(graph, opts) {
243
+ const html = (0,generate_html_report.generateHtmlReport)(graph, opts);
244
+ const dir = external_node_fs_.mkdtempSync(external_node_path_.join(external_node_os_.tmpdir(), 'agsec-export-'));
245
+ const file = external_node_path_.join(dir, 'report.html');
246
+ external_node_fs_.writeFileSync(file, html);
247
+ return { dir, file };
248
+ }
249
+
250
+ function _hashUrl(file, opts) {
251
+ // #view=<name> — the shell's own already-shipped hash-state mechanism
252
+ // (frontend/src/lib/state.js's parseStateFromHash, read at mount time
253
+ // by shell.js) — confirmed empirically this sub-project's own scoping
254
+ // investigation to select the initial active view with zero new
255
+ // frontend code. No `#` fragment when no view is requested (the shell
256
+ // defaults to 'architecture' on its own).
257
+ //
258
+ // pathToFileURL, never hand-built string concatenation: the temp
259
+ // directory comes from os.tmpdir(), which on some machines/CI images
260
+ // contains a literal `#` (found live by the final whole-branch
261
+ // review) — `file://${file}` truncates at that character, and Chrome
262
+ // silently renders its own internal error page for the broken URL
263
+ // instead of failing. pathToFileURL percent-encodes it correctly.
264
+ // Also the only correct way to build a file: URL on Windows, whose
265
+ // paths are never valid appended directly after `file://`.
266
+ const hash = opts.view ? `#view=${encodeURIComponent(opts.view)}` : '';
267
+ return (0,external_node_url_.pathToFileURL)(file).href + hash;
268
+ }
269
+
270
+ function _cleanup(dir) {
271
+ try { external_node_fs_.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
272
+ }
273
+
274
+ // Confirmed this sub-project's own scoping investigation: exactly ONE
275
+ // real `<svg>` element exists in the whole rendered page
276
+ // (architecture-view.js's own root, class="arch-view") — a direct
277
+ // source grep confirms svgEl()/createElementNS is used nowhere else.
278
+ // Only present when the Architecture View is the active view, though
279
+ // (see _verifyRealRender below for the view-agnostic check used for
280
+ // PNG/PDF, which can capture any view).
281
+ function _extractArchSvg(dom) {
282
+ const start = dom.indexOf('<svg class="arch-view"');
283
+ if (start === -1) return { ok: false, reason: 'no <svg class="arch-view"> element found in the rendered page' };
284
+ const end = dom.indexOf('</svg>', start);
285
+ if (end === -1) return { ok: false, reason: 'found <svg class="arch-view"> but no matching </svg> close tag' };
286
+ return { ok: true, markup: dom.slice(start, end + '</svg>'.length) };
287
+ }
288
+
289
+ // role="tablist" — shell.js's own view-switcher tab bar (frontend/src/shell.js),
290
+ // present in EVERY view (unlike the architecture-only <svg class="arch-view">
291
+ // _extractArchSvg looks for), and part of the shell chrome, not
292
+ // view-specific content. Its presence in a --dump-dom capture is this
293
+ // module's positive signal that Chrome actually rendered the real
294
+ // report and not its own internal error page (which exits 0 and still
295
+ // writes a screenshot/PDF file — found live by the final whole-branch
296
+ // review: a `#` in the temp path broke the URL, and the resulting
297
+ // correctly-dimensioned-but-wrong PNG was Chrome's own error page).
298
+ function _hasRealShellChrome(dom) {
299
+ return dom.includes('role="tablist"');
300
+ }
301
+
302
+ function _dumpDom(chrome, url, opts) {
303
+ const r = external_node_child_process_.spawnSync(chrome.chrome, [
304
+ '--headless=new', '--disable-gpu',
305
+ `--window-size=${opts.width || 1680},${opts.height || 945}`,
306
+ '--virtual-time-budget=5000',
307
+ '--dump-dom',
308
+ url,
309
+ ], { timeout: RENDER_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
310
+ if (r.error || r.status !== 0) {
311
+ return { ok: false, reason: `chrome dump-dom failed: ${r.error?.message || r.stderr || 'unknown error'}` };
312
+ }
313
+ return { ok: true, dom: r.stdout };
314
+ }
315
+
316
+ // Verifies the page actually rendered the real report (not Chrome's own
317
+ // error page for a broken URL) before PNG/PDF capture is trusted. Costs
318
+ // one extra Chrome invocation per PNG/PDF export — deliberate: a
319
+ // screenshot/PDF file gives no positive signal of its own content, and
320
+ // Chrome exits 0 with a written output file for its own error page too.
321
+ function _verifyRealRender(chrome, url, opts) {
322
+ const dumped = _dumpDom(chrome, url, opts);
323
+ if (!dumped.ok) return dumped;
324
+ if (!_hasRealShellChrome(dumped.dom)) {
325
+ return { ok: false, reason: 'rendered page has no real report shell (role="tablist") — likely a broken URL or a Chrome error page' };
326
+ }
327
+ return { ok: true };
328
+ }
329
+
330
+ async function exportPng(graph, opts = {}) {
331
+ const chrome = probeChromeAvailable();
332
+ if (!chrome.ok) return { ok: false, reason: `chrome not available: ${chrome.reason}` };
333
+ const { dir, file } = _writeTempHtml(graph, opts);
334
+ const width = opts.width || 1680;
335
+ const height = opts.height || 945;
336
+ const outPng = external_node_path_.join(dir, 'out.png');
337
+ try {
338
+ const url = _hashUrl(file, opts);
339
+ const verified = _verifyRealRender(chrome, url, opts);
340
+ if (!verified.ok) return verified;
341
+ const r = external_node_child_process_.spawnSync(chrome.chrome, [
342
+ '--headless=new', '--disable-gpu',
343
+ `--window-size=${width},${height}`,
344
+ '--force-device-scale-factor=1',
345
+ '--virtual-time-budget=5000',
346
+ `--screenshot=${outPng}`,
347
+ url,
348
+ ], { timeout: RENDER_TIMEOUT_MS, encoding: 'utf8' });
349
+ if (r.error || r.status !== 0 || !external_node_fs_.existsSync(outPng)) {
350
+ return { ok: false, reason: `chrome screenshot failed: ${r.error?.message || r.stderr || 'unknown error'}` };
351
+ }
352
+ return { ok: true, data: external_node_fs_.readFileSync(outPng) };
353
+ } finally {
354
+ _cleanup(dir);
355
+ }
356
+ }
357
+
358
+ async function exportPdf(graph, opts = {}) {
359
+ const chrome = probeChromeAvailable();
360
+ if (!chrome.ok) return { ok: false, reason: `chrome not available: ${chrome.reason}` };
361
+ const { dir, file } = _writeTempHtml(graph, opts);
362
+ const outPdf = external_node_path_.join(dir, 'out.pdf');
363
+ try {
364
+ const url = _hashUrl(file, opts);
365
+ const verified = _verifyRealRender(chrome, url, opts);
366
+ if (!verified.ok) return verified;
367
+ const r = external_node_child_process_.spawnSync(chrome.chrome, [
368
+ '--headless=new', '--disable-gpu',
369
+ '--virtual-time-budget=5000',
370
+ `--print-to-pdf=${outPdf}`,
371
+ url,
372
+ ], { timeout: RENDER_TIMEOUT_MS, encoding: 'utf8' });
373
+ if (r.error || r.status !== 0 || !external_node_fs_.existsSync(outPdf)) {
374
+ return { ok: false, reason: `chrome print-to-pdf failed: ${r.error?.message || r.stderr || 'unknown error'}` };
375
+ }
376
+ return { ok: true, data: external_node_fs_.readFileSync(outPdf) };
377
+ } finally {
378
+ _cleanup(dir);
379
+ }
380
+ }
381
+
382
+ async function exportSvg(graph, opts = {}) {
383
+ const chrome = probeChromeAvailable();
384
+ if (!chrome.ok) return { ok: false, reason: `chrome not available: ${chrome.reason}` };
385
+ const { dir, file } = _writeTempHtml(graph, opts);
386
+ try {
387
+ const url = _hashUrl(file, opts);
388
+ const dumped = _dumpDom(chrome, url, opts);
389
+ if (!dumped.ok) return dumped;
390
+ const extracted = _extractArchSvg(dumped.dom);
391
+ if (!extracted.ok) return extracted;
392
+ // Real, standalone SVG documents need an explicit xmlns — the
393
+ // in-page element inherits it implicitly from its HTML parent
394
+ // document, which a standalone .svg file does not have.
395
+ const standalone = extracted.markup.replace('<svg class="arch-view"', '<svg xmlns="http://www.w3.org/2000/svg" class="arch-view"');
396
+ return { ok: true, data: Buffer.from(standalone, 'utf8') };
397
+ } finally {
398
+ _cleanup(dir);
399
+ }
400
+ }
401
+
402
+
403
+ /***/ }),
404
+
405
+ /***/ 3180:
406
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
407
+
408
+
409
+ // EXPORTS
410
+ __webpack_require__.d(__webpack_exports__, {
411
+ generateHtmlReport: () => (/* binding */ generateHtmlReport)
412
+ });
413
+
414
+ // EXTERNAL MODULE: external "node:fs"
415
+ var external_node_fs_ = __webpack_require__(3024);
416
+ // EXTERNAL MODULE: external "node:path"
417
+ var external_node_path_ = __webpack_require__(6760);
418
+ // EXTERNAL MODULE: external "node:url"
419
+ var external_node_url_ = __webpack_require__(3136);
420
+ ;// CONCATENATED MODULE: ./scripts/bundle-frontend.mjs
421
+ // bundle-frontend.mjs — Milestone 4, sub-project Self-contained HTML
422
+ // report. A minimal, hand-rolled ES-module bundler, deliberately NOT a
423
+ // general one — see this sub-project's own scoping doc for why a real
424
+ // bundler dependency (esbuild etc.) was considered and NOT chosen:
425
+ // frontend/src/'s real module graph (confirmed by a full-tree grep before
426
+ // this file was written) uses ONLY named imports/exports, no default
427
+ // exports, no `export *`, no namespace or side-effect-only imports. This
428
+ // bundler supports exactly that subset and throws a clear error on
429
+ // anything else, rather than silently mishandling an unsupported form.
430
+ //
431
+ // Algorithm: parse each file's own import specifiers + exported top-level
432
+ // names via regex (safe here because the supported subset's syntax is
433
+ // simple and unambiguous — a full parser is not needed for this bounded,
434
+ // fully-enumerated 21-file input); topologically order every reachable
435
+ // module; wrap EVERY NON-ENTRY module in its own IIFE (this is what
436
+ // prevents two dependency files' same-named PRIVATE top-level bindings
437
+ // from colliding when concatenated — a real risk with naive flat
438
+ // concatenation, proven by this file's own test suite); each module's
439
+ // IIFE destructures its own imports from its dependencies' already-
440
+ // evaluated namespace objects (`const { a, b } = __NS_shared;`) and
441
+ // returns its own exports as a plain object.
442
+ //
443
+ // The ENTRY module is deliberately NOT wrapped in an IIFE: its body is
444
+ // emitted directly at the top level of the output (after its own
445
+ // destructured imports), so its top-level declarations — functions the
446
+ // caller needs to invoke directly, e.g. `bootstrap()` in a `<script>`
447
+ // tag, or (as this file's own tests require) a bare exported name called
448
+ // straight off the bundled output — become real top-level bindings
449
+ // rather than being trapped inside a closure whose return value is
450
+ // discarded. Wrapping the entry in an IIFE too (as an earlier draft of
451
+ // this algorithm did) type-checks and looks symmetrical, but it is a
452
+ // correctness bug: it makes every exported name of the entry module
453
+ // unreachable from outside the bundle, which is never what a bundle's
454
+ // caller wants. Only the entry is unwrapped — every other module is
455
+ // still fully IIFE-isolated, so the collision guarantee above is
456
+ // unaffected (there is exactly one unwrapped scope per bundle, by
457
+ // construction, since the entry is the unique DFS root and cannot be
458
+ // re-entered — a cycle back to it is rejected below as a circular
459
+ // import).
460
+
461
+
462
+
463
+
464
+ const IMPORT_RE = /import\s*\{([\s\S]*?)\}\s*from\s*['"](\.[^'"]+)['"];?/g;
465
+ const UNSUPPORTED_IMPORT_RE = /^import\s+(?!\{)/m;
466
+ const EXPORT_STAR_RE = /^export\s*\*/m;
467
+ const EXPORT_DEFAULT_RE = /^export\s+default\b/m;
468
+ // Three more real, unsupported forms — added after the final whole-branch
469
+ // review found each builds with NO error (`EXPORT_NAMED_RE` below requires
470
+ // `function`/`class`/`const`/`let`/`var` immediately after `export `, so
471
+ // `export async function` matches none of these guards OR the named-export
472
+ // extractor, and is silently dropped as neither a caught error nor a real
473
+ // export) and only fails later as a page-load SyntaxError, blanking the
474
+ // whole page with no visible error — `export async function` already
475
+ // exists in the real tree today (frontend/src/lib/api-client.js), outside
476
+ // today's bundled entry point's own reachable graph only by luck.
477
+ const EXPORT_ASYNC_RE = /^export\s+async\s+function/m;
478
+ const EXPORT_LIST_RE = /^export\s*\{/m;
479
+ const IMPORT_RENAME_RE = /\bas\b/;
480
+ const EXPORT_NAMED_RE = /^export\s+(?:function\*?|class|const|let|var)\s+([A-Za-z0-9_$]+)/gm;
481
+
482
+ function _nsName(absPath, seen) {
483
+ // A stable, collision-free JS identifier per module, derived from its
484
+ // path. seen is a Map<absPath, name> shared across the whole bundle run.
485
+ if (seen.has(absPath)) return seen.get(absPath);
486
+ const base = absPath.replace(/[^A-Za-z0-9_$]/g, '_');
487
+ const name = `__NS_${base}`;
488
+ seen.set(absPath, name);
489
+ return name;
490
+ }
491
+
492
+ function _parseModule(absPath) {
493
+ const src = external_node_fs_.readFileSync(absPath, 'utf8');
494
+ if (UNSUPPORTED_IMPORT_RE.test(src) || EXPORT_STAR_RE.test(src) || EXPORT_DEFAULT_RE.test(src) || EXPORT_ASYNC_RE.test(src) || EXPORT_LIST_RE.test(src)) {
495
+ throw new Error(`bundleFrontendModules: unsupported import/export form in ${absPath} — only named imports/exports are supported (no default/namespace/side-effect imports, no export */export default/export {}-list/export async function)`);
496
+ }
497
+ const imports = [];
498
+ let m;
499
+ IMPORT_RE.lastIndex = 0;
500
+ while ((m = IMPORT_RE.exec(src))) {
501
+ if (IMPORT_RENAME_RE.test(m[1])) {
502
+ throw new Error(`bundleFrontendModules: unsupported \`import { x as y }\` rename in ${absPath} — only bare named imports are supported`);
503
+ }
504
+ const names = m[1].split(',').map((s) => s.trim()).filter(Boolean);
505
+ const resolved = external_node_path_.resolve(external_node_path_.dirname(absPath), m[2]);
506
+ imports.push({ names, resolved });
507
+ }
508
+ const exportNames = [];
509
+ EXPORT_NAMED_RE.lastIndex = 0;
510
+ while ((m = EXPORT_NAMED_RE.exec(src))) exportNames.push(m[1]);
511
+ // Strip import lines entirely; strip the leading `export ` keyword from
512
+ // each exported declaration (the declaration itself — function/const/
513
+ // class — is kept verbatim, so the body is otherwise untouched).
514
+ const body = src
515
+ .replace(IMPORT_RE, '')
516
+ .replace(/^export\s+(?=(?:function\*?|class|const|let|var)\s)/gm, '');
517
+ return { absPath, body, imports, exportNames };
518
+ }
519
+
520
+ function bundleFrontendModules(entryAbsPath) {
521
+ const nsNames = new Map();
522
+ const parsed = new Map(); // absPath -> parsed module
523
+ const order = []; // topological, dependency-first
524
+ const visiting = new Set();
525
+
526
+ function visit(absPath) {
527
+ // `visiting` (currently-on-the-DFS-stack) must be checked BEFORE
528
+ // `parsed` (fully-resolved, safe to skip) — found by this task's own
529
+ // review: the original ordering set `parsed` before recursing into a
530
+ // module's own imports, so a cycle back to an ancestor hit the
531
+ // `parsed.has()` check first and returned silently, making the
532
+ // `visiting`-based throw below unreachable for ANY cycle. Reproduced
533
+ // live: a real entry<->b.js cycle built successfully at bundle time
534
+ // and only failed later, as a confusing `ReferenceError` (TDZ) when
535
+ // the broken output was executed, instead of this function's own
536
+ // clear build-time error.
537
+ if (visiting.has(absPath)) {
538
+ throw new Error(`bundleFrontendModules: circular import detected at ${absPath} — this bundler does not support cycles`);
539
+ }
540
+ if (parsed.has(absPath)) return;
541
+ visiting.add(absPath);
542
+ const mod = _parseModule(absPath);
543
+ parsed.set(absPath, mod);
544
+ for (const imp of mod.imports) visit(imp.resolved);
545
+ visiting.delete(absPath);
546
+ order.push(absPath);
547
+ }
548
+ visit(entryAbsPath);
549
+
550
+ const chunks = [];
551
+ for (const absPath of order) {
552
+ const mod = parsed.get(absPath);
553
+ const destructures = mod.imports
554
+ .map((imp) => ` const { ${imp.names.join(', ')} } = ${_nsName(imp.resolved, nsNames)};`)
555
+ .join('\n');
556
+
557
+ if (absPath === entryAbsPath) {
558
+ // Entry: emit unwrapped at top level so its declarations become
559
+ // real top-level bindings the caller can invoke — see the header
560
+ // comment for why this must not be an IIFE like every other module.
561
+ chunks.push(`${destructures}\n${mod.body}`);
562
+ continue;
563
+ }
564
+
565
+ const ns = _nsName(absPath, nsNames);
566
+ const returnObj = mod.exportNames.length ? ` return { ${mod.exportNames.join(', ')} };` : ' return {};';
567
+ chunks.push(`const ${ns} = (function() {\n${destructures}\n${mod.body}\n${returnObj}\n})();`);
568
+ }
569
+ const bundled = chunks.join('\n\n');
570
+
571
+ // Real gap found by the final whole-branch review, live-reproduced: the
572
+ // UNSUPPORTED_IMPORT_RE/EXPORT_STAR_RE/EXPORT_DEFAULT_RE guards above
573
+ // only cover THREE specific unsupported forms — real ES syntax this
574
+ // bundler doesn't handle (`export async function`, `export { g };`
575
+ // re-export lists, `import { h as hh }` renames) builds with NO error
576
+ // and only fails later, as a page-load-time SyntaxError that silently
577
+ // blanks the whole page (export-entry.js's own showError() never runs,
578
+ // since the entire inline <script> fails to parse before any of its
579
+ // code executes) — directly contradicting this file's own documented
580
+ // "throws a clear error on anything else" promise. `export async
581
+ // function` already exists in the real tree today
582
+ // (frontend/src/lib/api-client.js) — outside today's bundled entry
583
+ // point's own reachable graph only by luck. Rather than enumerating
584
+ // every unsupported form by name (the exact approach that missed these
585
+ // three), validate the FINAL bundle is syntactically valid JS before
586
+ // returning it — this converts any parse failure, including forms not
587
+ // yet imagined, into a clear build-time error. `new Function` compiles
588
+ // without executing (measured ~1ms on the real ~131KB bundle — cheap).
589
+ try {
590
+ // eslint-disable-next-line no-new-func
591
+ new Function(bundled);
592
+ } catch (e) {
593
+ throw new Error(`bundleFrontendModules: the assembled bundle is not valid JavaScript (${e.message}) — one of the ${order.length} modules likely uses an ES form this bundler doesn't support (only named imports/exports, no async/generator export shorthand ambiguity, no \`export { x };\` lists, no \`import { x as y }\` renames)`);
594
+ }
595
+ return bundled;
596
+ }
597
+
598
+ // EXTERNAL MODULE: ./src/lineage/export-json.js
599
+ var export_json = __webpack_require__(859);
600
+ // EXTERNAL MODULE: ./src/shared/frontend-root.js
601
+ var frontend_root = __webpack_require__(1185);
602
+ ;// CONCATENATED MODULE: ./scripts/generate-html-report.mjs
603
+ // generate-html-report.mjs — Milestone 4, sub-project Self-contained
604
+ // HTML report. Assembles ONE offline-safe HTML document: inline CSS,
605
+ // one bundled inline <script> (Task 1's bundler over Task 2's
606
+ // export-entry.js), and the graph embedded via exportGraphJSON
607
+ // (redacted by default) as a global the bundled script reads.
608
+
609
+
610
+
611
+
612
+
613
+
614
+
615
+
616
+ const HERE = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
617
+ // See src/shared/frontend-root.js: a hardcoded `../../frontend` resolved
618
+ // correctly for this file's unbundled dev location (scanner/scripts/) but
619
+ // broke for the published package, whose build now copies frontend/ into
620
+ // scanner/dist/frontend/ — a different relative depth from this same
621
+ // module once ncc bundles it into a dist/ chunk.
622
+ const FRONTEND_ROOT = (0,frontend_root/* resolveFrontendRoot */.D)(HERE);
623
+ const STYLES_DIR = external_node_path_.join(FRONTEND_ROOT, 'styles');
624
+ const ENTRY_PATH = external_node_path_.join(FRONTEND_ROOT, 'src', 'export-entry.js');
625
+
626
+ // NOT alphabetical — confirmed this session by reading the real
627
+ // frontend/index.html directly: its own <link> order is
628
+ // tokens.css, shell.css, architecture-view.css, inspector.css,
629
+ // privacy-view.css, trace-view.css, filter-rail.css, inventory-view.css,
630
+ // query-bar.css. `tokens.css` first is load-bearing (CSS custom
631
+ // properties the other 8 files consume via `var(--...)` — an alphabetical
632
+ // sort would put it 8th, after 7 files that reference undefined custom
633
+ // properties on first paint). Matching real cascade order avoids a real
634
+ // specificity/undefined-custom-property regression alphabetical sorting
635
+ // would introduce silently. Re-confirmed against the real index.html
636
+ // directly at implementation time (unchanged from the task-3 brief's own
637
+ // citation).
638
+ const CSS_LOAD_ORDER = [
639
+ 'tokens.css', 'shell.css', 'architecture-view.css', 'inspector.css',
640
+ 'privacy-view.css', 'trace-view.css', 'filter-rail.css',
641
+ 'inventory-view.css', 'query-bar.css',
642
+ ];
643
+
644
+ function _inlineCss() {
645
+ const onDisk = new Set(external_node_fs_.readdirSync(STYLES_DIR).filter((f) => f.endsWith('.css')));
646
+ const missing = CSS_LOAD_ORDER.filter((f) => !onDisk.has(f));
647
+ if (missing.length) throw new Error(`generate-html-report: CSS_LOAD_ORDER names files not found in ${STYLES_DIR}: ${missing.join(', ')} — frontend/index.html's own <link> list may have changed; re-sync CSS_LOAD_ORDER with it`);
648
+ const extra = [...onDisk].filter((f) => !CSS_LOAD_ORDER.includes(f));
649
+ if (extra.length) throw new Error(`generate-html-report: real CSS files not in CSS_LOAD_ORDER: ${extra.join(', ')} — a new stylesheet was added to frontend/styles/ without updating this list's load order`);
650
+ return CSS_LOAD_ORDER.map((f) => external_node_fs_.readFileSync(external_node_path_.join(STYLES_DIR, f), 'utf8')).join('\n');
651
+ }
652
+
653
+ function generateHtmlReport(graph, opts = {}) {
654
+ const exported = (0,export_json.exportGraphJSON)(graph, opts);
655
+ const css = _inlineCss();
656
+ const bundledJs = bundleFrontendModules(ENTRY_PATH);
657
+ // Escape `<` so scanned-source-derived content (a node/flow/data-element
658
+ // label — none of which _redactGraph covers, since labels aren't a
659
+ // secret-shaped surface, but CAN carry arbitrary scanned identifier
660
+ // text) can never contain a literal `</script>` that breaks out of this
661
+ // inline data script and injects a second one. Same mitigation, same
662
+ // reasoning, as the existing SAST/SCA HTML report's own precedent
663
+ // (scanner/src/report/index.js's toHTML) — found missing here and
664
+ // fixed by this sub-project's own final task review, which reproduced
665
+ // the injection live before this fix landed.
666
+ const envelopeJson = JSON.stringify(exported).replace(/</g, '\\u003c');
667
+ return `<!DOCTYPE html>
668
+ <html lang="en" data-theme="dark">
669
+ <head>
670
+ <meta charset="UTF-8" />
671
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
672
+ <title>Data Flow Explorer — self-contained report</title>
673
+ <style>
674
+ ${css}
675
+ </style>
676
+ </head>
677
+ <body>
678
+ <div id="app-root"></div>
679
+ <script>
680
+ // The full exportGraphJSON envelope (digest/scope/coverage/limitations/
681
+ // confidential + the redacted graph body) is embedded whole here, under
682
+ // its own name, so this report carries the same tamper-evident/disclosure
683
+ // metadata the JSON exporter produces (PRD §17.5) — never dropped just
684
+ // because this is an HTML report rather than a .json file.
685
+ //
686
+ // __AGENTIC_SECURITY_EXPORTED_GRAPH__ is then bound to the envelope's own
687
+ // \`.graph\` body, NOT the envelope itself — this is a deliberate,
688
+ // necessary departure from an earlier draft of this generator (which
689
+ // assigned the whole envelope directly to that name). frontend/src/
690
+ // export-entry.js (Task 2, already shipped and covered by its own golden
691
+ // DOM-equivalence test, test/bundle-frontend-golden.test.js) reads this
692
+ // EXACT global and passes it straight through to bootstrap(rootEl, graph)
693
+ // -> mountShell(rootEl, graph)/computeFilterFacets(graph), both of which
694
+ // expect a real DataFlowGraph v1 document (top-level nodes/edges/flows/...),
695
+ // never the exportGraphJSON envelope shape (which nests the graph one
696
+ // level down, under .graph, alongside digest/scope/confidential). Handing
697
+ // bootstrap() the envelope instead of the graph would silently break real
698
+ // rendering — exactly what this sub-project's own real-Chrome acceptance
699
+ // proof (task-3 brief, Step 5) exists to catch. Confirmed against
700
+ // export-entry.js's and Task 2's golden test's real source before this
701
+ // generator was written, not assumed.
702
+ window.__AGENTIC_SECURITY_EXPORT_ENVELOPE__ = ${envelopeJson};
703
+ window.__AGENTIC_SECURITY_EXPORTED_GRAPH__ = window.__AGENTIC_SECURITY_EXPORT_ENVELOPE__.graph;
704
+ </script>
705
+ <script>
706
+ ${bundledJs}
707
+ </script>
708
+ </body>
709
+ </html>`;
710
+ }
711
+
712
+
713
+ /***/ }),
714
+
715
+ /***/ 1185:
716
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
717
+
718
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
719
+ /* harmony export */ D: () => (/* binding */ resolveFrontendRoot)
720
+ /* harmony export */ });
721
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
722
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
723
+ // frontend-root.js — locates the Data Flow Explorer's `frontend/` assets
724
+ // (index.html/src/styles) regardless of whether this code is running:
725
+ // - unbundled, straight out of scanner/src/ or scanner/scripts/ (dev/test —
726
+ // frontend/ is a monorepo sibling of scanner/, 2-3 levels up), or
727
+ // - bundled by `npm run build` (ncc splits a dynamic import() into its own
728
+ // chunk file physically written into scanner/dist/, so import.meta.url
729
+ // inside that chunk reflects dist/'s own location, not the original
730
+ // source file's — a fixed relative-levels-up path that was correct for
731
+ // one depth breaks silently at the other), or
732
+ // - installed from the published npm package, where the build copies
733
+ // frontend/'s servable files into scanner/dist/frontend/ (a sibling of
734
+ // the chunk file itself, i.e. 0 levels up) — see scripts/copy-frontend.mjs.
735
+ //
736
+ // Rather than hardcode one of those depths (the bug this file fixes: every
737
+ // consumer used to hardcode the dev-only depth), search upward from the
738
+ // caller's own directory and take the first candidate that actually has a
739
+ // frontend/index.html on disk. Never guessed silently past that — a caller
740
+ // with no match anywhere gets a clear, actionable error instead of a
741
+ // downstream ENOENT/404 with no indication why.
742
+
743
+
744
+
745
+
746
+ const MAX_LEVELS_UP = 4;
747
+
748
+ /**
749
+ * @param {string} startDir - `path.dirname(fileURLToPath(import.meta.url))`
750
+ * of the CALLING module (not this file) — each caller's own bundled/
751
+ * unbundled location determines which candidate depth resolves.
752
+ * @returns {string} absolute path to a real `frontend/` directory containing
753
+ * `index.html`.
754
+ * @throws if no candidate directory up to MAX_LEVELS_UP contains one.
755
+ */
756
+ function resolveFrontendRoot(startDir) {
757
+ const tried = [];
758
+ for (let up = 0; up <= MAX_LEVELS_UP; up++) {
759
+ const candidate = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(startDir, ...Array(up).fill('..'), 'frontend');
760
+ tried.push(candidate);
761
+ // Every caller invokes resolveFrontendRoot() once, at module-load time,
762
+ // to compute a top-level FRONTEND_ROOT const (see static-assets.js /
763
+ // generate-html-report.mjs) — never per-request inside the explore
764
+ // server's request handler. At most MAX_LEVELS_UP+1 (5) sync stat calls
765
+ // at process startup is not a request-path DoS surface.
766
+ if (node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(candidate, 'index.html'))) return candidate; // agentic-security-ignore: dos-sync-io
767
+ }
768
+ throw new Error(
769
+ `resolveFrontendRoot: no frontend/index.html found searching up from ${startDir}. ` +
770
+ `Tried: ${tried.join(', ')}. If you're running from a source checkout, run \`npm run build\` ` +
771
+ `first (it copies frontend/ into scanner/dist/frontend/); if you're running the published ` +
772
+ `package, this indicates a packaging defect — report it.`
773
+ );
774
+ }
775
+
776
+
777
+ /***/ })
778
+
779
+ };