@clear-capabilities/agentic-security-scanner 0.147.0 → 0.148.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 (45) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/bin/agentic-security.js +47 -15
  3. package/dist/1122.index.js +79 -2
  4. package/dist/3180.index.js +73 -1
  5. package/dist/5051.index.js +77 -6
  6. package/dist/agentic-security.mjs +2 -2
  7. package/dist/agentic-security.mjs.sha256 +1 -1
  8. package/dist/compliance-frameworks/nist-800-171-r3.json +1269 -0
  9. package/dist/frontend/index.html +21 -0
  10. package/dist/frontend/src/app.js +176 -0
  11. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  12. package/dist/frontend/src/components/filter-rail.js +119 -0
  13. package/dist/frontend/src/components/query-bar.js +126 -0
  14. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  15. package/dist/frontend/src/export-entry.js +36 -0
  16. package/dist/frontend/src/lib/api-client.js +92 -0
  17. package/dist/frontend/src/lib/contrast.js +34 -0
  18. package/dist/frontend/src/lib/dom.js +24 -0
  19. package/dist/frontend/src/lib/escape-html.js +16 -0
  20. package/dist/frontend/src/lib/flow-path.js +40 -0
  21. package/dist/frontend/src/lib/focus-controls.js +149 -0
  22. package/dist/frontend/src/lib/protection-visual.js +46 -0
  23. package/dist/frontend/src/lib/query-language.js +240 -0
  24. package/dist/frontend/src/lib/row-filters.js +43 -0
  25. package/dist/frontend/src/lib/state.js +84 -0
  26. package/dist/frontend/src/main.js +83 -0
  27. package/dist/frontend/src/shell.js +184 -0
  28. package/dist/frontend/src/views/architecture-view.js +798 -0
  29. package/dist/frontend/src/views/inventory-view.js +292 -0
  30. package/dist/frontend/src/views/privacy-view.js +172 -0
  31. package/dist/frontend/src/views/trace-view.js +206 -0
  32. package/dist/frontend/styles/architecture-view.css +93 -0
  33. package/dist/frontend/styles/filter-rail.css +34 -0
  34. package/dist/frontend/styles/inspector.css +69 -0
  35. package/dist/frontend/styles/inventory-view.css +74 -0
  36. package/dist/frontend/styles/privacy-view.css +86 -0
  37. package/dist/frontend/styles/query-bar.css +107 -0
  38. package/dist/frontend/styles/shell.css +155 -0
  39. package/dist/frontend/styles/tokens.css +128 -0
  40. package/dist/frontend/styles/trace-view.css +95 -0
  41. package/package.json +2 -2
  42. package/src/posture/auditor-walkthrough.js +89 -6
  43. package/src/posture/compliance-frameworks/nist-800-171-r3.json +1269 -0
  44. package/src/server/static-assets.js +11 -6
  45. package/src/shared/frontend-root.js +52 -0
package/CHANGELOG.md CHANGED
@@ -11,6 +11,210 @@
11
11
 
12
12
 
13
13
 
14
+ ## 0.148.0 - NIST SP 800-171 Rev. 3 (CUI / CMMC basis) as the 10th bundled framework
15
+
16
+ Adds NIST SP 800-171 Rev. 3 to `/compliance --report <framework>` (aliases
17
+ `800-171` and `cui`), `--walkthrough`, `--gap` and `--format oscal`, plus a
18
+ standalone deep-attestation scanner at `scripts/nist-800-171/scan.py`.
19
+
20
+ **All 97 requirements are carried, including the 43 this engine cannot assess.**
21
+ Shipping only the code-observable subset would have been a smaller, better-looking
22
+ artifact and a dishonest one, omission reads as coverage. Whole families
23
+ (Awareness and Training, Personnel Security, Physical Protection) and most of
24
+ Incident Response, Maintenance and Media Protection report as requiring manual
25
+ evidence. Ratings: 16 `yes`, 38 `partial`, 43 `no`.
26
+
27
+ **The `code_testable` rating is ours, not NIST's.** Unlike the AI 600-1 workbook,
28
+ the 800-171 export rates no control for testability. That judgment lives in
29
+ `scripts/nist-800-171/code-testability.json` with a per-requirement rationale, and
30
+ is joined into the generated catalog by `build-catalog.py`. A control with no
31
+ rating is a hard build failure, never a default, defaulting would either invent
32
+ coverage or silently suppress a requirement. The generator is stdlib-only, so
33
+ unlike the openpyxl-based AI 600-1 gate its drift check can never be unrunnable
34
+ for a missing dependency.
35
+
36
+ **Fixes a real pre-existing bug in the shared evaluator.** `evaluateFramework`,
37
+ behind `--report`/`--walkthrough`/`--gap`/`--format oscal` for *all ten* frameworks
38
+ had no vacuous-satisfaction guard: a scan that read zero files produced empty
39
+ finding buckets, and an empty bucket rendered as `✓ no open findings`. Measured on
40
+ 800-171, **32 of 97 controls read `present` off a scan that examined nothing**. The
41
+ guard existed only inside `privacy-framework.js`, whose own comment already named
42
+ `evaluateFramework` as where the hazard originates. The fix reads three count
43
+ fields (a real scan persists `scanned.files` and `_scanMeta.filesScanned` but *not*
44
+ top-level `filesScanned`, which exists only on the in-memory object) and treats
45
+ **absent as unknown, not zero**, degrading requires positive evidence that nothing
46
+ was examined, so a genuinely clean project is unaffected. 2419/2419 posture tests
47
+ pass with no test file modified.
48
+
49
+ **One scanning engine, not two.** `scripts/nist-compliance/scan.py` now reads its
50
+ framework identity from the catalog it is given rather than from hardcoded strings,
51
+ and the 800-171 scanner is a thin wrapper supplying defaults. This keeps exactly one
52
+ copy of the ReDoS-hardened matcher, two copies would mean a fix to one silently
53
+ missing the other. The AI 600-1 scanner's md/csv/json output was pinned before and
54
+ after and is byte-identical.
55
+
56
+ **Fixes OSCAL documents that could not be attested.** Every compliance OSCAL export
57
+ read its timestamp from `scan._scanMeta.startedAt`, a key scans do not have, they
58
+ carry `startedAt` at the top level. So `_when()` fell through to `new Date()` at emit
59
+ time, which that function's own comment says must never happen, because "two emits of
60
+ one scan then differ and the artifact cannot be attested." Two emits of the same scan
61
+ genuinely differed. Fixed at all three call sites (both in `bin/agentic-security.js`,
62
+ one in `commands/compliance.md`); `last-modified` is now the scan's own clock, and with
63
+ `AGENTIC_SECURITY_DETERMINISTIC=1` the document is byte-identical across runs. This
64
+ affected every framework, not just the new one.
65
+
66
+ Also: the `nist-catalog-freshness` release gate now iterates every generated catalog
67
+ instead of naming one script path, `docs/compliance/nist-800-171-r3-coverage.md` is
68
+ pointer-based rather than restating counts that would drift, and that page carries a
69
+ recorded walkthrough (`docs/brand/nist-800-171-demo.tape`, regenerable with `vhs`)
70
+ showing assess → remediate → re-assess against `examples/demo-app`.
71
+
72
+ **Fixes `module:scan-history`, which could never resolve.** The evaluator's artifact table
73
+ listed only `scan-history/` (a directory) while an ordinary scan writes `scan-history.json`
74
+ (a file), both spellings are real in this codebase (`findings-memory.js` uses the
75
+ directory; `security-trend.js` and `router.js` use the file), but only one was listed. Every
76
+ control mapped to it reported the artifact missing even when the history existed, across
77
+ five bundled frameworks. Table entries may now be an array of acceptable paths, and any one
78
+ of them evidences the control.
79
+
80
+ Measured effect, because a change that moves compliance verdicts should be quantified rather
81
+ than asserted: **no control anywhere became `satisfied`**, `present` held at 43 across all
82
+ five frameworks. Eight controls moved `manual` → `partial` (2 eu-ai-act, 1 hipaa, 4
83
+ nist-800-171-r3, 1 nist-ai-600-1, 1 nist-privacy-1-1). Those are artifact-existence controls,
84
+ which the honesty model already caps at `partial`, so finding the artifact can only move them
85
+ out of "not assessed", never to satisfied. In OSCAL terms the reports get *stricter*, not
86
+ more flattering: for 800-171 against the demo app, findings went 42 → 46 with satisfied
87
+ unchanged at 28, i.e. four controls moved from unassessed into assessed-and-not-satisfied.
88
+
89
+ No CMMC assessment and no SPRS score is produced or implied.
90
+
91
+ ## 0.147.5 — Fix: `bench:provenance:check`'s cold-memory sample size was unreliable on GitHub Actions
92
+
93
+ `v0.147.4`'s tag was pushed but its hosted release-gate run failed before
94
+ reaching `npm publish` — the `dataflow-watch` fix held (the full test suite
95
+ passed cleanly), but `bench/provenance/runner.mjs`'s `bench:provenance:check`
96
+ failed: "only 8/20 iterations produced a usable sample (need >= 10)" for
97
+ `cold.memOverheadRatio`. This is the SAME failure `v0.147.3`'s release run
98
+ also hit (7/20 that time) — two consecutive, reproducible failures on
99
+ GitHub's runners specifically, while this bench passes cleanly (~20/20)
100
+ every time locally, so this is a real platform gap, not one-off noise.
101
+
102
+ Root cause: `cold.memOverheadRatio` is a per-iteration ratio against the
103
+ without-provenance arm's own peak-RSS delta, and `measure()` skips the
104
+ ratio entirely for an iteration where that delta is `0`
105
+ (`if (without.rssDeltaBytes > 0) { ... }`) — the without-provenance scan is
106
+ only ~40-60ms, short enough that the 10ms-interval peak-RSS poller can
107
+ genuinely observe no net growth for it, especially under a shared runner's
108
+ different GC pacing. `BASELINE.json` itself already records this as
109
+ expected (`memOverheadRatio.n: 19`, one dropped iteration out of 20 even on
110
+ the machine that captured it) — GitHub Actions just drops far more of them
111
+ (a ~35-40% survival rate against RELIABLE_N=10 at N=20 leaves no margin).
112
+
113
+ Raises `N` from 20 to 60 in `bench/provenance/runner.mjs` — the same
114
+ already-established remedy this exact file used once before for a related
115
+ small-sample fragility (doubling 10→20), extended further given the new,
116
+ worse-than-expected survival rate. `RELIABLE_N` (10) and the regression
117
+ thresholds are unchanged; `BASELINE.json`'s reference values were not
118
+ touched (the measured overhead is well within budget — this was purely a
119
+ sample-count reliability failure, not a regression). Verified locally:
120
+ passes with real margin (cold memory p95 11.42x vs a 24.38x limit), at a
121
+ real but bounded cost — this one bench step now takes ~2:23 instead of
122
+ ~40-60s, a release-time-only cost. `v0.147.4` was never published to any
123
+ registry (confirmed via `npm view`). Carries the identical fixes from
124
+ 0.147.1-0.147.4 forward.
125
+
126
+ ## 0.147.4 — Fix: close a real fs.watch startup race in `dataflow watch` (test-only; no product behavior change)
127
+
128
+ `v0.147.3`'s tag was pushed but its hosted release-gate run failed before
129
+ reaching `npm publish` — again `cli/dataflow-watch-2`, but this time raising
130
+ the wait timeout (0.147.3's own fix) did NOT help: it timed out at the
131
+ *full* 90000ms budget with zero rescan output, which the real rescan
132
+ pipeline (~1-2s once triggered) rules out as "merely slow." Root cause: `bin/
133
+ agentic-security.js`'s `cmdDataflowWatch` prints its "watching ... Ctrl-C to
134
+ stop" banner — the exact string these tests poll for as the "watcher is
135
+ live" signal — *before* calling `watchProject()`, which is where
136
+ `fs.watch()` actually arms its (possibly recursive, whole-tree-walking on
137
+ Linux) inotify descriptors. A test file-write landing in that gap produces
138
+ an event inotify never delivers at all — a permanent miss, not a delay —
139
+ which a longer timeout cannot fix by construction.
140
+
141
+ Fixed with a 1000ms settle delay in the test, inserted between seeing the
142
+ startup banner and writing the trigger file, in both `dataflow-watch-1` and
143
+ `dataflow-watch-2` (`test/cli/dataflow-watch.test.js`). No product code
144
+ changed — `cmdDataflowWatch`'s banner-before-watchProject ordering is real,
145
+ pre-existing behavior, unrelated to anything in 0.147.1-0.147.3, and left
146
+ alone here rather than reordered under release time pressure without full
147
+ confidence in every caller depending on today's ordering. `v0.147.3` was
148
+ never published to any registry (confirmed via `npm view`). Carries the
149
+ identical fixes from 0.147.1-0.147.3 forward.
150
+
151
+ ## 0.147.3 — CI fix: raise dataflow-watch's live-subprocess wait timeout (no functional change from 0.147.2)
152
+
153
+ Same story as 0.147.2, different test: `v0.147.2`'s hosted release-gate run
154
+ also failed before reaching `npm publish` — `test/cli/dataflow-watch.test.js`'s
155
+ `cli/dataflow-watch-2` polls a real subprocess's live stderr for a
156
+ `DRIFT POLICY VIOLATION` line after triggering a file-watch-driven rescan,
157
+ with a 30000ms budget. That step completes in ~1.4s on a normal machine (a
158
+ ~20x margin) but still timed out on this run — this repo's watch mode uses
159
+ Node's native `fs.watch(..., { recursive: true })`
160
+ (`src/posture/watch-mode.js`), documented as slower/less reliable under
161
+ Linux CI runners' inotify handling than on a dev machine. `v0.147.2` was
162
+ never published to any registry (confirmed via `npm view`).
163
+
164
+ Raises the wait budget from 30000ms to a shared `WAIT_TIMEOUT_MS = 90000`
165
+ across both real-subprocess tests in that file (the ones that depend on the
166
+ file-watch path — an unrelated fast-fail test in the same file was left
167
+ untouched). No product code changed; this carries the identical fixes from
168
+ 0.147.1 and 0.147.2 forward.
169
+
170
+ ## 0.147.2 — CI fix: raise the headless-Chrome render timeout (no functional change from 0.147.1)
171
+
172
+ `v0.147.1`'s tag was pushed but its hosted release-gate run failed before
173
+ ever reaching `npm publish` — `scripts/export-image.mjs`'s `exportPng` test
174
+ hit its own hardcoded 15000ms Chrome render timeout on a slower draw of the
175
+ GitHub Actions runner (the same render took 7983ms on the immediately
176
+ preceding v0.147.0 release run — a ~2x runner-speed swing well within
177
+ normal CI variance, not a regression). `v0.147.1` was never published to
178
+ any registry; this release carries the identical `explore`/`dataflow
179
+ export --format html` fix described below under 0.147.1, plus this one
180
+ timeout change: `RENDER_TIMEOUT_MS` default raised from 15000ms to 30000ms
181
+ in `scripts/export-image.mjs`, giving roughly 4x headroom over the observed
182
+ good-case render time. No other code changed.
183
+
184
+ ## 0.147.1 — Fix: `explore` and `dataflow export --format html` were broken for every real npm/npx install
185
+
186
+ Both commands located the Data Flow Explorer frontend (`frontend/`, a
187
+ repo-root sibling of `scanner/` in this monorepo) via a fixed relative path
188
+ computed from each module's own `import.meta.url`
189
+ (`path.resolve(HERE, '../../../frontend')` in `static-assets.js`,
190
+ `'../../frontend'` in `generate-html-report.mjs`). That math was correct
191
+ only for the unbundled dev checkout. Once `npm run build` (ncc) split those
192
+ modules into their own `dist/*.index.js` chunks, `import.meta.url` inside
193
+ them reflected `dist/`'s own, shallower location, and the same fixed
194
+ relative math resolved *outside the installed package entirely* — and
195
+ `frontend/` was never part of the published npm tarball to begin with. The
196
+ practical effect: `agentic-security explore .` 404'd on `GET /` for every
197
+ real `npx`/`npm install` user (`{"error":"not found"}`, never an
198
+ index.html), and `dataflow export . --format html` failed outright
199
+ (`ENOENT: no such file or directory, scandir '.../frontend/styles'`). Both
200
+ only ever worked when run from a full monorepo clone.
201
+
202
+ Fixed by shipping the frontend's servable files inside the published
203
+ package: `scanner/scripts/copy-frontend.mjs` (new) copies `frontend/`'s
204
+ allowlisted files (`index.html`, `src/**/*.js`, top-level `styles/*.css` —
205
+ the exact same allowlist `static-assets.js` already enforced for serving)
206
+ into `scanner/dist/frontend/` as part of `npm run build`; a new shared
207
+ resolver, `scanner/src/shared/frontend-root.js`, replaces both hardcoded
208
+ relative paths with a search-upward strategy that finds the frontend
209
+ correctly regardless of bundling depth. Verified by packing a real tarball
210
+ (`npm pack`) and installing it into an isolated directory with no monorepo
211
+ present: `explore`'s `GET /` now returns 200 with the real `index.html`
212
+ (previously 404), static JS/CSS assets and the token-authenticated
213
+ `/api/v1/graph` endpoint both 200, and `dataflow export --format html`
214
+ exits 0 and writes a real self-contained report (previously exited 2).
215
+ `dpia`/`ropa` export formats were already unaffected (a separate,
216
+ already-self-contained code path) and are unchanged.
217
+
14
218
  ## 0.147.0 — Documentation overhaul: a cohesive assurance-platform story, verified against the shipped code
15
219
 
16
220
  The 0.144.0 Assurance Hardening release shipped a real 3-state ship verdict,
@@ -2447,7 +2447,7 @@ async function cmdCompliance(args) {
2447
2447
  if (fmt === 'oscal') {
2448
2448
  const { toOSCALCompliance, complianceRowsFromEvaluation } = await import('../src/report/oscal.js');
2449
2449
  writeStdout(JSON.stringify(
2450
- toOSCALCompliance(fw, complianceRowsFromEvaluation(evaluation), { startedAt: scan._scanMeta?.startedAt }),
2450
+ toOSCALCompliance(fw, complianceRowsFromEvaluation(evaluation), { startedAt: scan.startedAt || scan._scanMeta?.startedAt }),
2451
2451
  null, 2) + '\n');
2452
2452
  return 0;
2453
2453
  }
@@ -2478,7 +2478,7 @@ async function cmdCompliance(args) {
2478
2478
  const { toOSCALCompliance, complianceRowsFromPrivacy } = await import('../src/report/oscal.js');
2479
2479
  const fwMeta = loadFramework(scanRoot, PRIVACY_FRAMEWORK_ID) || { id: PRIVACY_FRAMEWORK_ID, name: r.frameworkName };
2480
2480
  writeStdout(JSON.stringify(
2481
- toOSCALCompliance(fwMeta, complianceRowsFromPrivacy(r), { startedAt: scan._scanMeta?.startedAt }),
2481
+ toOSCALCompliance(fwMeta, complianceRowsFromPrivacy(r), { startedAt: scan.startedAt || scan._scanMeta?.startedAt }),
2482
2482
  null, 2) + '\n');
2483
2483
  return args.flags['fail-on'] === 'gap' && r.summary.gap > 0 ? 1 : 0;
2484
2484
  }
@@ -6302,7 +6302,27 @@ async function cmdDataflowWatch(args) {
6302
6302
  // change that makes `scan --watch` exit before it ever watches anything.
6303
6303
  }
6304
6304
 
6305
+ // A version below the declared `engines.node` floor is still let through by
6306
+ // npm (EBADENGINE is a warning, not an install failure), so on an old Node
6307
+ // this file used to either crash deep inside a dependency with a confusing
6308
+ // stack, or — on the specific versions handled by the guard below — do
6309
+ // nothing at all. Fail fast, once, with a message that names the actual
6310
+ // requirement instead.
6311
+ function checkNodeVersionOrExit() {
6312
+ const required = __require('../package.json').engines?.node;
6313
+ const requiredMajor = Number(/(\d+)/.exec(required || '')?.[1]);
6314
+ const actualMajor = Number(process.versions.node.split('.')[0]);
6315
+ if (Number.isFinite(requiredMajor) && actualMajor < requiredMajor) {
6316
+ console.error(
6317
+ `agentic-security: requires Node.js ${required} — you're running Node ${process.versions.node}.\n` +
6318
+ `Upgrade Node (e.g. 'nvm install ${requiredMajor}' or https://nodejs.org/) and try again.`
6319
+ );
6320
+ process.exit(1);
6321
+ }
6322
+ }
6323
+
6305
6324
  async function main() {
6325
+ checkNodeVersionOrExit();
6306
6326
  const args = parseArgs(process.argv.slice(2));
6307
6327
  const cmd = args._[0];
6308
6328
  try {
@@ -6663,18 +6683,30 @@ async function main() {
6663
6683
  // @clear-capabilities/agentic-security-scanner` install path), Node
6664
6684
  // resolves `import.meta.url` to the symlink's realpath while
6665
6685
  // `process.argv[1]` stays the symlink path as invoked, so the two never
6666
- // match, the guard is always false, and the CLI silently exits with no
6667
- // output. `import.meta.main` is resolved correctly through a symlink
6668
- // verified live through an actual symlink, not just read about see the
6669
- // Task 17 fix report. It was added in Node v24.2.0 (backported to
6670
- // v22.18.0) and is currently Stability 1.0 (early development) per Node's
6671
- // own docs NOT stable, and NOT available on v20.11 as an earlier
6672
- // version of this comment incorrectly claimed. Concretely: it is
6673
- // `undefined` on Node 24.0.0/24.1.x, which satisfy this repo's declared
6674
- // `engines.node: ">=24.0.0"` floor, so `import.meta.main` alone would
6675
- // reproduce this exact bug (main() silently never runs) on a plain
6676
- // non-symlinked invocation under those two point releases. The `??`
6677
- // fallback below covers that gap without bumping the engines floor.
6678
- if (import.meta.main ?? (import.meta.url === `file://${process.argv[1]}`)) {
6686
+ // match under a naive comparison. `import.meta.main` is resolved correctly
6687
+ // through a symlink verified live through an actual symlink, not just
6688
+ // read about see the Task 17 fix report. It was added in Node v24.2.0
6689
+ // (backported to v22.18.0) and is currently Stability 1.0 (early
6690
+ // development) per Node's own docs NOT stable, and NOT available on
6691
+ // v20.11 as an earlier version of this comment incorrectly claimed.
6692
+ // Concretely: it is `undefined` on Node 24.0.0/24.1.x, which satisfy this
6693
+ // repo's declared `engines.node: ">=24.0.0"` floor, so `import.meta.main`
6694
+ // alone would reproduce this exact bug (main() silently never runs) on
6695
+ // those two point releases when invoked through the symlinked bin (npx/
6696
+ // global install) the common case, not the exception. The fallback
6697
+ // below resolves `process.argv[1]`'s realpath before comparing, so it
6698
+ // gives the right answer through a symlink too — verified live against a
6699
+ // real symlink, same as the `import.meta.main` claim above — instead of
6700
+ // silently exiting with no output on every Node version where
6701
+ // `import.meta.main` is undefined (also: <22.18.0, and 23.x).
6702
+ function isDirectCliInvocation() {
6703
+ if (import.meta.main !== undefined) return import.meta.main;
6704
+ try {
6705
+ return import.meta.url === `file://${fs.realpathSync(process.argv[1])}`;
6706
+ } catch {
6707
+ return false;
6708
+ }
6709
+ }
6710
+ if (isDirectCliInvocation()) {
6679
6711
  main();
6680
6712
  }
@@ -232,7 +232,12 @@ function export_image_validTimeoutMs(raw, fallback) {
232
232
  if (!Number.isSafeInteger(n) || n < 0 || Object.is(n, -0)) return fallback;
233
233
  return n;
234
234
  }
235
- const RENDER_TIMEOUT_MS = export_image_validTimeoutMs(process.env.AGENTIC_SECURITY_CHROME_RENDER_TIMEOUT_MS, 15000);
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);
236
241
 
237
242
  function _writeTempHtml(graph, opts) {
238
243
  const html = (0,generate_html_report.generateHtmlReport)(graph, opts);
@@ -592,6 +597,8 @@ function bundleFrontendModules(entryAbsPath) {
592
597
 
593
598
  // EXTERNAL MODULE: ./src/lineage/export-json.js
594
599
  var export_json = __webpack_require__(859);
600
+ // EXTERNAL MODULE: ./src/shared/frontend-root.js
601
+ var frontend_root = __webpack_require__(1185);
595
602
  ;// CONCATENATED MODULE: ./scripts/generate-html-report.mjs
596
603
  // generate-html-report.mjs — Milestone 4, sub-project Self-contained
597
604
  // HTML report. Assembles ONE offline-safe HTML document: inline CSS,
@@ -605,8 +612,14 @@ var export_json = __webpack_require__(859);
605
612
 
606
613
 
607
614
 
615
+
608
616
  const HERE = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
609
- const FRONTEND_ROOT = external_node_path_.resolve(HERE, '../../frontend');
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);
610
623
  const STYLES_DIR = external_node_path_.join(FRONTEND_ROOT, 'styles');
611
624
  const ENTRY_PATH = external_node_path_.join(FRONTEND_ROOT, 'src', 'export-entry.js');
612
625
 
@@ -697,6 +710,70 @@ ${bundledJs}
697
710
  }
698
711
 
699
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
+
700
777
  /***/ })
701
778
 
702
779
  };
@@ -197,6 +197,8 @@ function bundleFrontendModules(entryAbsPath) {
197
197
 
198
198
  // EXTERNAL MODULE: ./src/lineage/export-json.js
199
199
  var export_json = __webpack_require__(859);
200
+ // EXTERNAL MODULE: ./src/shared/frontend-root.js
201
+ var frontend_root = __webpack_require__(1185);
200
202
  ;// CONCATENATED MODULE: ./scripts/generate-html-report.mjs
201
203
  // generate-html-report.mjs — Milestone 4, sub-project Self-contained
202
204
  // HTML report. Assembles ONE offline-safe HTML document: inline CSS,
@@ -210,8 +212,14 @@ var export_json = __webpack_require__(859);
210
212
 
211
213
 
212
214
 
215
+
213
216
  const HERE = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
214
- const FRONTEND_ROOT = external_node_path_.resolve(HERE, '../../frontend');
217
+ // See src/shared/frontend-root.js: a hardcoded `../../frontend` resolved
218
+ // correctly for this file's unbundled dev location (scanner/scripts/) but
219
+ // broke for the published package, whose build now copies frontend/ into
220
+ // scanner/dist/frontend/ — a different relative depth from this same
221
+ // module once ncc bundles it into a dist/ chunk.
222
+ const FRONTEND_ROOT = (0,frontend_root/* resolveFrontendRoot */.D)(HERE);
215
223
  const STYLES_DIR = external_node_path_.join(FRONTEND_ROOT, 'styles');
216
224
  const ENTRY_PATH = external_node_path_.join(FRONTEND_ROOT, 'src', 'export-entry.js');
217
225
 
@@ -302,6 +310,70 @@ ${bundledJs}
302
310
  }
303
311
 
304
312
 
313
+ /***/ }),
314
+
315
+ /***/ 1185:
316
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
317
+
318
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
319
+ /* harmony export */ D: () => (/* binding */ resolveFrontendRoot)
320
+ /* harmony export */ });
321
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
322
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
323
+ // frontend-root.js — locates the Data Flow Explorer's `frontend/` assets
324
+ // (index.html/src/styles) regardless of whether this code is running:
325
+ // - unbundled, straight out of scanner/src/ or scanner/scripts/ (dev/test —
326
+ // frontend/ is a monorepo sibling of scanner/, 2-3 levels up), or
327
+ // - bundled by `npm run build` (ncc splits a dynamic import() into its own
328
+ // chunk file physically written into scanner/dist/, so import.meta.url
329
+ // inside that chunk reflects dist/'s own location, not the original
330
+ // source file's — a fixed relative-levels-up path that was correct for
331
+ // one depth breaks silently at the other), or
332
+ // - installed from the published npm package, where the build copies
333
+ // frontend/'s servable files into scanner/dist/frontend/ (a sibling of
334
+ // the chunk file itself, i.e. 0 levels up) — see scripts/copy-frontend.mjs.
335
+ //
336
+ // Rather than hardcode one of those depths (the bug this file fixes: every
337
+ // consumer used to hardcode the dev-only depth), search upward from the
338
+ // caller's own directory and take the first candidate that actually has a
339
+ // frontend/index.html on disk. Never guessed silently past that — a caller
340
+ // with no match anywhere gets a clear, actionable error instead of a
341
+ // downstream ENOENT/404 with no indication why.
342
+
343
+
344
+
345
+
346
+ const MAX_LEVELS_UP = 4;
347
+
348
+ /**
349
+ * @param {string} startDir - `path.dirname(fileURLToPath(import.meta.url))`
350
+ * of the CALLING module (not this file) — each caller's own bundled/
351
+ * unbundled location determines which candidate depth resolves.
352
+ * @returns {string} absolute path to a real `frontend/` directory containing
353
+ * `index.html`.
354
+ * @throws if no candidate directory up to MAX_LEVELS_UP contains one.
355
+ */
356
+ function resolveFrontendRoot(startDir) {
357
+ const tried = [];
358
+ for (let up = 0; up <= MAX_LEVELS_UP; up++) {
359
+ const candidate = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(startDir, ...Array(up).fill('..'), 'frontend');
360
+ tried.push(candidate);
361
+ // Every caller invokes resolveFrontendRoot() once, at module-load time,
362
+ // to compute a top-level FRONTEND_ROOT const (see static-assets.js /
363
+ // generate-html-report.mjs) — never per-request inside the explore
364
+ // server's request handler. At most MAX_LEVELS_UP+1 (5) sync stat calls
365
+ // at process startup is not a request-path DoS surface.
366
+ 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
367
+ }
368
+ throw new Error(
369
+ `resolveFrontendRoot: no frontend/index.html found searching up from ${startDir}. ` +
370
+ `Tried: ${tried.join(', ')}. If you're running from a source checkout, run \`npm run build\` ` +
371
+ `first (it copies frontend/ into scanner/dist/frontend/); if you're running the published ` +
372
+ `package, this indicates a packaging defect — report it.`
373
+ );
374
+ }
375
+
376
+
305
377
  /***/ })
306
378
 
307
379
  };
@@ -25,6 +25,8 @@ var security = __webpack_require__(6944);
25
25
  var routes = __webpack_require__(4268);
26
26
  // EXTERNAL MODULE: external "node:url"
27
27
  var external_node_url_ = __webpack_require__(3136);
28
+ // EXTERNAL MODULE: ./src/shared/frontend-root.js
29
+ var frontend_root = __webpack_require__(1185);
28
30
  ;// CONCATENATED MODULE: ./src/server/static-assets.js
29
31
  // static-assets.js — Milestone 3, sub-project Wire.
30
32
  //
@@ -43,13 +45,18 @@ var external_node_url_ = __webpack_require__(3136);
43
45
 
44
46
 
45
47
 
46
- // Located the SAME way scanner/src/mcp/server.js locates files relative to
47
- // its own module (path.dirname(fileURLToPath(import.meta.url))) — the real,
48
- // existing precedent for this pattern in this codebase, not a new one.
49
- // scanner/src/server/ -> ../../../frontend, computed (not guessed) and
50
- // confirmed to resolve to the real frontend/ directory.
48
+
49
+ // Located relative to this module's own directory (path.dirname(
50
+ // fileURLToPath(import.meta.url)), the same pattern scanner/src/mcp/server.js
51
+ // uses), but via resolveFrontendRoot's search-upward strategy rather than a
52
+ // single hardcoded relative depth a fixed `../../../frontend` resolved
53
+ // correctly for this file's unbundled dev location but broke (silently, at
54
+ // runtime, for every real npx/npm user) once `npm run build` split this
55
+ // module into its own dist/ chunk, whose import.meta.url reflects dist/'s
56
+ // own shallower location. See src/shared/frontend-root.js for the full story
57
+ // and scripts/copy-frontend.mjs for the build-time copy this now finds.
51
58
  const _here = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
52
- const FRONTEND_ROOT = external_node_path_.resolve(_here, '..', '..', '..', 'frontend');
59
+ const FRONTEND_ROOT = (0,frontend_root/* resolveFrontendRoot */.D)(_here);
53
60
 
54
61
  const CONTENT_TYPE_MAP = Object.freeze({
55
62
  '.html': 'text/html; charset=utf-8',
@@ -765,6 +772,70 @@ function isValidHost(hostHeader, expectedPort) {
765
772
  const CSP_HEADER_VALUE = "default-src 'none'; frame-ancestors 'none'";
766
773
 
767
774
 
775
+ /***/ }),
776
+
777
+ /***/ 1185:
778
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
779
+
780
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
781
+ /* harmony export */ D: () => (/* binding */ resolveFrontendRoot)
782
+ /* harmony export */ });
783
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
784
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
785
+ // frontend-root.js — locates the Data Flow Explorer's `frontend/` assets
786
+ // (index.html/src/styles) regardless of whether this code is running:
787
+ // - unbundled, straight out of scanner/src/ or scanner/scripts/ (dev/test —
788
+ // frontend/ is a monorepo sibling of scanner/, 2-3 levels up), or
789
+ // - bundled by `npm run build` (ncc splits a dynamic import() into its own
790
+ // chunk file physically written into scanner/dist/, so import.meta.url
791
+ // inside that chunk reflects dist/'s own location, not the original
792
+ // source file's — a fixed relative-levels-up path that was correct for
793
+ // one depth breaks silently at the other), or
794
+ // - installed from the published npm package, where the build copies
795
+ // frontend/'s servable files into scanner/dist/frontend/ (a sibling of
796
+ // the chunk file itself, i.e. 0 levels up) — see scripts/copy-frontend.mjs.
797
+ //
798
+ // Rather than hardcode one of those depths (the bug this file fixes: every
799
+ // consumer used to hardcode the dev-only depth), search upward from the
800
+ // caller's own directory and take the first candidate that actually has a
801
+ // frontend/index.html on disk. Never guessed silently past that — a caller
802
+ // with no match anywhere gets a clear, actionable error instead of a
803
+ // downstream ENOENT/404 with no indication why.
804
+
805
+
806
+
807
+
808
+ const MAX_LEVELS_UP = 4;
809
+
810
+ /**
811
+ * @param {string} startDir - `path.dirname(fileURLToPath(import.meta.url))`
812
+ * of the CALLING module (not this file) — each caller's own bundled/
813
+ * unbundled location determines which candidate depth resolves.
814
+ * @returns {string} absolute path to a real `frontend/` directory containing
815
+ * `index.html`.
816
+ * @throws if no candidate directory up to MAX_LEVELS_UP contains one.
817
+ */
818
+ function resolveFrontendRoot(startDir) {
819
+ const tried = [];
820
+ for (let up = 0; up <= MAX_LEVELS_UP; up++) {
821
+ const candidate = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(startDir, ...Array(up).fill('..'), 'frontend');
822
+ tried.push(candidate);
823
+ // Every caller invokes resolveFrontendRoot() once, at module-load time,
824
+ // to compute a top-level FRONTEND_ROOT const (see static-assets.js /
825
+ // generate-html-report.mjs) — never per-request inside the explore
826
+ // server's request handler. At most MAX_LEVELS_UP+1 (5) sync stat calls
827
+ // at process startup is not a request-path DoS surface.
828
+ 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
829
+ }
830
+ throw new Error(
831
+ `resolveFrontendRoot: no frontend/index.html found searching up from ${startDir}. ` +
832
+ `Tried: ${tried.join(', ')}. If you're running from a source checkout, run \`npm run build\` ` +
833
+ `first (it copies frontend/ into scanner/dist/frontend/); if you're running the published ` +
834
+ `package, this indicates a packaging defect — report it.`
835
+ );
836
+ }
837
+
838
+
768
839
  /***/ })
769
840
 
770
841
  };