@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.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.
- package/CHANGELOG.md +139 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +702 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +2 -2
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/{271.index.js → 2271.index.js} +2 -2
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +307 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +5 -5
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +5 -5
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +2 -2
- package/dist/5051.index.js +770 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +3 -3
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +560 -189
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/package.json +12 -5
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +139 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -0,0 +1,702 @@
|
|
|
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
|
+
const RENDER_TIMEOUT_MS = export_image_validTimeoutMs(process.env.AGENTIC_SECURITY_CHROME_RENDER_TIMEOUT_MS, 15000);
|
|
236
|
+
|
|
237
|
+
function _writeTempHtml(graph, opts) {
|
|
238
|
+
const html = (0,generate_html_report.generateHtmlReport)(graph, opts);
|
|
239
|
+
const dir = external_node_fs_.mkdtempSync(external_node_path_.join(external_node_os_.tmpdir(), 'agsec-export-'));
|
|
240
|
+
const file = external_node_path_.join(dir, 'report.html');
|
|
241
|
+
external_node_fs_.writeFileSync(file, html);
|
|
242
|
+
return { dir, file };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function _hashUrl(file, opts) {
|
|
246
|
+
// #view=<name> — the shell's own already-shipped hash-state mechanism
|
|
247
|
+
// (frontend/src/lib/state.js's parseStateFromHash, read at mount time
|
|
248
|
+
// by shell.js) — confirmed empirically this sub-project's own scoping
|
|
249
|
+
// investigation to select the initial active view with zero new
|
|
250
|
+
// frontend code. No `#` fragment when no view is requested (the shell
|
|
251
|
+
// defaults to 'architecture' on its own).
|
|
252
|
+
//
|
|
253
|
+
// pathToFileURL, never hand-built string concatenation: the temp
|
|
254
|
+
// directory comes from os.tmpdir(), which on some machines/CI images
|
|
255
|
+
// contains a literal `#` (found live by the final whole-branch
|
|
256
|
+
// review) — `file://${file}` truncates at that character, and Chrome
|
|
257
|
+
// silently renders its own internal error page for the broken URL
|
|
258
|
+
// instead of failing. pathToFileURL percent-encodes it correctly.
|
|
259
|
+
// Also the only correct way to build a file: URL on Windows, whose
|
|
260
|
+
// paths are never valid appended directly after `file://`.
|
|
261
|
+
const hash = opts.view ? `#view=${encodeURIComponent(opts.view)}` : '';
|
|
262
|
+
return (0,external_node_url_.pathToFileURL)(file).href + hash;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _cleanup(dir) {
|
|
266
|
+
try { external_node_fs_.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Confirmed this sub-project's own scoping investigation: exactly ONE
|
|
270
|
+
// real `<svg>` element exists in the whole rendered page
|
|
271
|
+
// (architecture-view.js's own root, class="arch-view") — a direct
|
|
272
|
+
// source grep confirms svgEl()/createElementNS is used nowhere else.
|
|
273
|
+
// Only present when the Architecture View is the active view, though
|
|
274
|
+
// (see _verifyRealRender below for the view-agnostic check used for
|
|
275
|
+
// PNG/PDF, which can capture any view).
|
|
276
|
+
function _extractArchSvg(dom) {
|
|
277
|
+
const start = dom.indexOf('<svg class="arch-view"');
|
|
278
|
+
if (start === -1) return { ok: false, reason: 'no <svg class="arch-view"> element found in the rendered page' };
|
|
279
|
+
const end = dom.indexOf('</svg>', start);
|
|
280
|
+
if (end === -1) return { ok: false, reason: 'found <svg class="arch-view"> but no matching </svg> close tag' };
|
|
281
|
+
return { ok: true, markup: dom.slice(start, end + '</svg>'.length) };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// role="tablist" — shell.js's own view-switcher tab bar (frontend/src/shell.js),
|
|
285
|
+
// present in EVERY view (unlike the architecture-only <svg class="arch-view">
|
|
286
|
+
// _extractArchSvg looks for), and part of the shell chrome, not
|
|
287
|
+
// view-specific content. Its presence in a --dump-dom capture is this
|
|
288
|
+
// module's positive signal that Chrome actually rendered the real
|
|
289
|
+
// report and not its own internal error page (which exits 0 and still
|
|
290
|
+
// writes a screenshot/PDF file — found live by the final whole-branch
|
|
291
|
+
// review: a `#` in the temp path broke the URL, and the resulting
|
|
292
|
+
// correctly-dimensioned-but-wrong PNG was Chrome's own error page).
|
|
293
|
+
function _hasRealShellChrome(dom) {
|
|
294
|
+
return dom.includes('role="tablist"');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function _dumpDom(chrome, url, opts) {
|
|
298
|
+
const r = external_node_child_process_.spawnSync(chrome.chrome, [
|
|
299
|
+
'--headless=new', '--disable-gpu',
|
|
300
|
+
`--window-size=${opts.width || 1680},${opts.height || 945}`,
|
|
301
|
+
'--virtual-time-budget=5000',
|
|
302
|
+
'--dump-dom',
|
|
303
|
+
url,
|
|
304
|
+
], { timeout: RENDER_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
|
|
305
|
+
if (r.error || r.status !== 0) {
|
|
306
|
+
return { ok: false, reason: `chrome dump-dom failed: ${r.error?.message || r.stderr || 'unknown error'}` };
|
|
307
|
+
}
|
|
308
|
+
return { ok: true, dom: r.stdout };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Verifies the page actually rendered the real report (not Chrome's own
|
|
312
|
+
// error page for a broken URL) before PNG/PDF capture is trusted. Costs
|
|
313
|
+
// one extra Chrome invocation per PNG/PDF export — deliberate: a
|
|
314
|
+
// screenshot/PDF file gives no positive signal of its own content, and
|
|
315
|
+
// Chrome exits 0 with a written output file for its own error page too.
|
|
316
|
+
function _verifyRealRender(chrome, url, opts) {
|
|
317
|
+
const dumped = _dumpDom(chrome, url, opts);
|
|
318
|
+
if (!dumped.ok) return dumped;
|
|
319
|
+
if (!_hasRealShellChrome(dumped.dom)) {
|
|
320
|
+
return { ok: false, reason: 'rendered page has no real report shell (role="tablist") — likely a broken URL or a Chrome error page' };
|
|
321
|
+
}
|
|
322
|
+
return { ok: true };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function exportPng(graph, opts = {}) {
|
|
326
|
+
const chrome = probeChromeAvailable();
|
|
327
|
+
if (!chrome.ok) return { ok: false, reason: `chrome not available: ${chrome.reason}` };
|
|
328
|
+
const { dir, file } = _writeTempHtml(graph, opts);
|
|
329
|
+
const width = opts.width || 1680;
|
|
330
|
+
const height = opts.height || 945;
|
|
331
|
+
const outPng = external_node_path_.join(dir, 'out.png');
|
|
332
|
+
try {
|
|
333
|
+
const url = _hashUrl(file, opts);
|
|
334
|
+
const verified = _verifyRealRender(chrome, url, opts);
|
|
335
|
+
if (!verified.ok) return verified;
|
|
336
|
+
const r = external_node_child_process_.spawnSync(chrome.chrome, [
|
|
337
|
+
'--headless=new', '--disable-gpu',
|
|
338
|
+
`--window-size=${width},${height}`,
|
|
339
|
+
'--force-device-scale-factor=1',
|
|
340
|
+
'--virtual-time-budget=5000',
|
|
341
|
+
`--screenshot=${outPng}`,
|
|
342
|
+
url,
|
|
343
|
+
], { timeout: RENDER_TIMEOUT_MS, encoding: 'utf8' });
|
|
344
|
+
if (r.error || r.status !== 0 || !external_node_fs_.existsSync(outPng)) {
|
|
345
|
+
return { ok: false, reason: `chrome screenshot failed: ${r.error?.message || r.stderr || 'unknown error'}` };
|
|
346
|
+
}
|
|
347
|
+
return { ok: true, data: external_node_fs_.readFileSync(outPng) };
|
|
348
|
+
} finally {
|
|
349
|
+
_cleanup(dir);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function exportPdf(graph, opts = {}) {
|
|
354
|
+
const chrome = probeChromeAvailable();
|
|
355
|
+
if (!chrome.ok) return { ok: false, reason: `chrome not available: ${chrome.reason}` };
|
|
356
|
+
const { dir, file } = _writeTempHtml(graph, opts);
|
|
357
|
+
const outPdf = external_node_path_.join(dir, 'out.pdf');
|
|
358
|
+
try {
|
|
359
|
+
const url = _hashUrl(file, opts);
|
|
360
|
+
const verified = _verifyRealRender(chrome, url, opts);
|
|
361
|
+
if (!verified.ok) return verified;
|
|
362
|
+
const r = external_node_child_process_.spawnSync(chrome.chrome, [
|
|
363
|
+
'--headless=new', '--disable-gpu',
|
|
364
|
+
'--virtual-time-budget=5000',
|
|
365
|
+
`--print-to-pdf=${outPdf}`,
|
|
366
|
+
url,
|
|
367
|
+
], { timeout: RENDER_TIMEOUT_MS, encoding: 'utf8' });
|
|
368
|
+
if (r.error || r.status !== 0 || !external_node_fs_.existsSync(outPdf)) {
|
|
369
|
+
return { ok: false, reason: `chrome print-to-pdf failed: ${r.error?.message || r.stderr || 'unknown error'}` };
|
|
370
|
+
}
|
|
371
|
+
return { ok: true, data: external_node_fs_.readFileSync(outPdf) };
|
|
372
|
+
} finally {
|
|
373
|
+
_cleanup(dir);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function exportSvg(graph, opts = {}) {
|
|
378
|
+
const chrome = probeChromeAvailable();
|
|
379
|
+
if (!chrome.ok) return { ok: false, reason: `chrome not available: ${chrome.reason}` };
|
|
380
|
+
const { dir, file } = _writeTempHtml(graph, opts);
|
|
381
|
+
try {
|
|
382
|
+
const url = _hashUrl(file, opts);
|
|
383
|
+
const dumped = _dumpDom(chrome, url, opts);
|
|
384
|
+
if (!dumped.ok) return dumped;
|
|
385
|
+
const extracted = _extractArchSvg(dumped.dom);
|
|
386
|
+
if (!extracted.ok) return extracted;
|
|
387
|
+
// Real, standalone SVG documents need an explicit xmlns — the
|
|
388
|
+
// in-page element inherits it implicitly from its HTML parent
|
|
389
|
+
// document, which a standalone .svg file does not have.
|
|
390
|
+
const standalone = extracted.markup.replace('<svg class="arch-view"', '<svg xmlns="http://www.w3.org/2000/svg" class="arch-view"');
|
|
391
|
+
return { ok: true, data: Buffer.from(standalone, 'utf8') };
|
|
392
|
+
} finally {
|
|
393
|
+
_cleanup(dir);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
/***/ }),
|
|
399
|
+
|
|
400
|
+
/***/ 3180:
|
|
401
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
// EXPORTS
|
|
405
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
406
|
+
generateHtmlReport: () => (/* binding */ generateHtmlReport)
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// EXTERNAL MODULE: external "node:fs"
|
|
410
|
+
var external_node_fs_ = __webpack_require__(3024);
|
|
411
|
+
// EXTERNAL MODULE: external "node:path"
|
|
412
|
+
var external_node_path_ = __webpack_require__(6760);
|
|
413
|
+
// EXTERNAL MODULE: external "node:url"
|
|
414
|
+
var external_node_url_ = __webpack_require__(3136);
|
|
415
|
+
;// CONCATENATED MODULE: ./scripts/bundle-frontend.mjs
|
|
416
|
+
// bundle-frontend.mjs — Milestone 4, sub-project Self-contained HTML
|
|
417
|
+
// report. A minimal, hand-rolled ES-module bundler, deliberately NOT a
|
|
418
|
+
// general one — see this sub-project's own scoping doc for why a real
|
|
419
|
+
// bundler dependency (esbuild etc.) was considered and NOT chosen:
|
|
420
|
+
// frontend/src/'s real module graph (confirmed by a full-tree grep before
|
|
421
|
+
// this file was written) uses ONLY named imports/exports, no default
|
|
422
|
+
// exports, no `export *`, no namespace or side-effect-only imports. This
|
|
423
|
+
// bundler supports exactly that subset and throws a clear error on
|
|
424
|
+
// anything else, rather than silently mishandling an unsupported form.
|
|
425
|
+
//
|
|
426
|
+
// Algorithm: parse each file's own import specifiers + exported top-level
|
|
427
|
+
// names via regex (safe here because the supported subset's syntax is
|
|
428
|
+
// simple and unambiguous — a full parser is not needed for this bounded,
|
|
429
|
+
// fully-enumerated 21-file input); topologically order every reachable
|
|
430
|
+
// module; wrap EVERY NON-ENTRY module in its own IIFE (this is what
|
|
431
|
+
// prevents two dependency files' same-named PRIVATE top-level bindings
|
|
432
|
+
// from colliding when concatenated — a real risk with naive flat
|
|
433
|
+
// concatenation, proven by this file's own test suite); each module's
|
|
434
|
+
// IIFE destructures its own imports from its dependencies' already-
|
|
435
|
+
// evaluated namespace objects (`const { a, b } = __NS_shared;`) and
|
|
436
|
+
// returns its own exports as a plain object.
|
|
437
|
+
//
|
|
438
|
+
// The ENTRY module is deliberately NOT wrapped in an IIFE: its body is
|
|
439
|
+
// emitted directly at the top level of the output (after its own
|
|
440
|
+
// destructured imports), so its top-level declarations — functions the
|
|
441
|
+
// caller needs to invoke directly, e.g. `bootstrap()` in a `<script>`
|
|
442
|
+
// tag, or (as this file's own tests require) a bare exported name called
|
|
443
|
+
// straight off the bundled output — become real top-level bindings
|
|
444
|
+
// rather than being trapped inside a closure whose return value is
|
|
445
|
+
// discarded. Wrapping the entry in an IIFE too (as an earlier draft of
|
|
446
|
+
// this algorithm did) type-checks and looks symmetrical, but it is a
|
|
447
|
+
// correctness bug: it makes every exported name of the entry module
|
|
448
|
+
// unreachable from outside the bundle, which is never what a bundle's
|
|
449
|
+
// caller wants. Only the entry is unwrapped — every other module is
|
|
450
|
+
// still fully IIFE-isolated, so the collision guarantee above is
|
|
451
|
+
// unaffected (there is exactly one unwrapped scope per bundle, by
|
|
452
|
+
// construction, since the entry is the unique DFS root and cannot be
|
|
453
|
+
// re-entered — a cycle back to it is rejected below as a circular
|
|
454
|
+
// import).
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
const IMPORT_RE = /import\s*\{([\s\S]*?)\}\s*from\s*['"](\.[^'"]+)['"];?/g;
|
|
460
|
+
const UNSUPPORTED_IMPORT_RE = /^import\s+(?!\{)/m;
|
|
461
|
+
const EXPORT_STAR_RE = /^export\s*\*/m;
|
|
462
|
+
const EXPORT_DEFAULT_RE = /^export\s+default\b/m;
|
|
463
|
+
// Three more real, unsupported forms — added after the final whole-branch
|
|
464
|
+
// review found each builds with NO error (`EXPORT_NAMED_RE` below requires
|
|
465
|
+
// `function`/`class`/`const`/`let`/`var` immediately after `export `, so
|
|
466
|
+
// `export async function` matches none of these guards OR the named-export
|
|
467
|
+
// extractor, and is silently dropped as neither a caught error nor a real
|
|
468
|
+
// export) and only fails later as a page-load SyntaxError, blanking the
|
|
469
|
+
// whole page with no visible error — `export async function` already
|
|
470
|
+
// exists in the real tree today (frontend/src/lib/api-client.js), outside
|
|
471
|
+
// today's bundled entry point's own reachable graph only by luck.
|
|
472
|
+
const EXPORT_ASYNC_RE = /^export\s+async\s+function/m;
|
|
473
|
+
const EXPORT_LIST_RE = /^export\s*\{/m;
|
|
474
|
+
const IMPORT_RENAME_RE = /\bas\b/;
|
|
475
|
+
const EXPORT_NAMED_RE = /^export\s+(?:function\*?|class|const|let|var)\s+([A-Za-z0-9_$]+)/gm;
|
|
476
|
+
|
|
477
|
+
function _nsName(absPath, seen) {
|
|
478
|
+
// A stable, collision-free JS identifier per module, derived from its
|
|
479
|
+
// path. seen is a Map<absPath, name> shared across the whole bundle run.
|
|
480
|
+
if (seen.has(absPath)) return seen.get(absPath);
|
|
481
|
+
const base = absPath.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
482
|
+
const name = `__NS_${base}`;
|
|
483
|
+
seen.set(absPath, name);
|
|
484
|
+
return name;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function _parseModule(absPath) {
|
|
488
|
+
const src = external_node_fs_.readFileSync(absPath, 'utf8');
|
|
489
|
+
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)) {
|
|
490
|
+
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)`);
|
|
491
|
+
}
|
|
492
|
+
const imports = [];
|
|
493
|
+
let m;
|
|
494
|
+
IMPORT_RE.lastIndex = 0;
|
|
495
|
+
while ((m = IMPORT_RE.exec(src))) {
|
|
496
|
+
if (IMPORT_RENAME_RE.test(m[1])) {
|
|
497
|
+
throw new Error(`bundleFrontendModules: unsupported \`import { x as y }\` rename in ${absPath} — only bare named imports are supported`);
|
|
498
|
+
}
|
|
499
|
+
const names = m[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
500
|
+
const resolved = external_node_path_.resolve(external_node_path_.dirname(absPath), m[2]);
|
|
501
|
+
imports.push({ names, resolved });
|
|
502
|
+
}
|
|
503
|
+
const exportNames = [];
|
|
504
|
+
EXPORT_NAMED_RE.lastIndex = 0;
|
|
505
|
+
while ((m = EXPORT_NAMED_RE.exec(src))) exportNames.push(m[1]);
|
|
506
|
+
// Strip import lines entirely; strip the leading `export ` keyword from
|
|
507
|
+
// each exported declaration (the declaration itself — function/const/
|
|
508
|
+
// class — is kept verbatim, so the body is otherwise untouched).
|
|
509
|
+
const body = src
|
|
510
|
+
.replace(IMPORT_RE, '')
|
|
511
|
+
.replace(/^export\s+(?=(?:function\*?|class|const|let|var)\s)/gm, '');
|
|
512
|
+
return { absPath, body, imports, exportNames };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function bundleFrontendModules(entryAbsPath) {
|
|
516
|
+
const nsNames = new Map();
|
|
517
|
+
const parsed = new Map(); // absPath -> parsed module
|
|
518
|
+
const order = []; // topological, dependency-first
|
|
519
|
+
const visiting = new Set();
|
|
520
|
+
|
|
521
|
+
function visit(absPath) {
|
|
522
|
+
// `visiting` (currently-on-the-DFS-stack) must be checked BEFORE
|
|
523
|
+
// `parsed` (fully-resolved, safe to skip) — found by this task's own
|
|
524
|
+
// review: the original ordering set `parsed` before recursing into a
|
|
525
|
+
// module's own imports, so a cycle back to an ancestor hit the
|
|
526
|
+
// `parsed.has()` check first and returned silently, making the
|
|
527
|
+
// `visiting`-based throw below unreachable for ANY cycle. Reproduced
|
|
528
|
+
// live: a real entry<->b.js cycle built successfully at bundle time
|
|
529
|
+
// and only failed later, as a confusing `ReferenceError` (TDZ) when
|
|
530
|
+
// the broken output was executed, instead of this function's own
|
|
531
|
+
// clear build-time error.
|
|
532
|
+
if (visiting.has(absPath)) {
|
|
533
|
+
throw new Error(`bundleFrontendModules: circular import detected at ${absPath} — this bundler does not support cycles`);
|
|
534
|
+
}
|
|
535
|
+
if (parsed.has(absPath)) return;
|
|
536
|
+
visiting.add(absPath);
|
|
537
|
+
const mod = _parseModule(absPath);
|
|
538
|
+
parsed.set(absPath, mod);
|
|
539
|
+
for (const imp of mod.imports) visit(imp.resolved);
|
|
540
|
+
visiting.delete(absPath);
|
|
541
|
+
order.push(absPath);
|
|
542
|
+
}
|
|
543
|
+
visit(entryAbsPath);
|
|
544
|
+
|
|
545
|
+
const chunks = [];
|
|
546
|
+
for (const absPath of order) {
|
|
547
|
+
const mod = parsed.get(absPath);
|
|
548
|
+
const destructures = mod.imports
|
|
549
|
+
.map((imp) => ` const { ${imp.names.join(', ')} } = ${_nsName(imp.resolved, nsNames)};`)
|
|
550
|
+
.join('\n');
|
|
551
|
+
|
|
552
|
+
if (absPath === entryAbsPath) {
|
|
553
|
+
// Entry: emit unwrapped at top level so its declarations become
|
|
554
|
+
// real top-level bindings the caller can invoke — see the header
|
|
555
|
+
// comment for why this must not be an IIFE like every other module.
|
|
556
|
+
chunks.push(`${destructures}\n${mod.body}`);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const ns = _nsName(absPath, nsNames);
|
|
561
|
+
const returnObj = mod.exportNames.length ? ` return { ${mod.exportNames.join(', ')} };` : ' return {};';
|
|
562
|
+
chunks.push(`const ${ns} = (function() {\n${destructures}\n${mod.body}\n${returnObj}\n})();`);
|
|
563
|
+
}
|
|
564
|
+
const bundled = chunks.join('\n\n');
|
|
565
|
+
|
|
566
|
+
// Real gap found by the final whole-branch review, live-reproduced: the
|
|
567
|
+
// UNSUPPORTED_IMPORT_RE/EXPORT_STAR_RE/EXPORT_DEFAULT_RE guards above
|
|
568
|
+
// only cover THREE specific unsupported forms — real ES syntax this
|
|
569
|
+
// bundler doesn't handle (`export async function`, `export { g };`
|
|
570
|
+
// re-export lists, `import { h as hh }` renames) builds with NO error
|
|
571
|
+
// and only fails later, as a page-load-time SyntaxError that silently
|
|
572
|
+
// blanks the whole page (export-entry.js's own showError() never runs,
|
|
573
|
+
// since the entire inline <script> fails to parse before any of its
|
|
574
|
+
// code executes) — directly contradicting this file's own documented
|
|
575
|
+
// "throws a clear error on anything else" promise. `export async
|
|
576
|
+
// function` already exists in the real tree today
|
|
577
|
+
// (frontend/src/lib/api-client.js) — outside today's bundled entry
|
|
578
|
+
// point's own reachable graph only by luck. Rather than enumerating
|
|
579
|
+
// every unsupported form by name (the exact approach that missed these
|
|
580
|
+
// three), validate the FINAL bundle is syntactically valid JS before
|
|
581
|
+
// returning it — this converts any parse failure, including forms not
|
|
582
|
+
// yet imagined, into a clear build-time error. `new Function` compiles
|
|
583
|
+
// without executing (measured ~1ms on the real ~131KB bundle — cheap).
|
|
584
|
+
try {
|
|
585
|
+
// eslint-disable-next-line no-new-func
|
|
586
|
+
new Function(bundled);
|
|
587
|
+
} catch (e) {
|
|
588
|
+
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)`);
|
|
589
|
+
}
|
|
590
|
+
return bundled;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// EXTERNAL MODULE: ./src/lineage/export-json.js
|
|
594
|
+
var export_json = __webpack_require__(859);
|
|
595
|
+
;// CONCATENATED MODULE: ./scripts/generate-html-report.mjs
|
|
596
|
+
// generate-html-report.mjs — Milestone 4, sub-project Self-contained
|
|
597
|
+
// HTML report. Assembles ONE offline-safe HTML document: inline CSS,
|
|
598
|
+
// one bundled inline <script> (Task 1's bundler over Task 2's
|
|
599
|
+
// export-entry.js), and the graph embedded via exportGraphJSON
|
|
600
|
+
// (redacted by default) as a global the bundled script reads.
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
const HERE = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
|
|
609
|
+
const FRONTEND_ROOT = external_node_path_.resolve(HERE, '../../frontend');
|
|
610
|
+
const STYLES_DIR = external_node_path_.join(FRONTEND_ROOT, 'styles');
|
|
611
|
+
const ENTRY_PATH = external_node_path_.join(FRONTEND_ROOT, 'src', 'export-entry.js');
|
|
612
|
+
|
|
613
|
+
// NOT alphabetical — confirmed this session by reading the real
|
|
614
|
+
// frontend/index.html directly: its own <link> order is
|
|
615
|
+
// tokens.css, shell.css, architecture-view.css, inspector.css,
|
|
616
|
+
// privacy-view.css, trace-view.css, filter-rail.css, inventory-view.css,
|
|
617
|
+
// query-bar.css. `tokens.css` first is load-bearing (CSS custom
|
|
618
|
+
// properties the other 8 files consume via `var(--...)` — an alphabetical
|
|
619
|
+
// sort would put it 8th, after 7 files that reference undefined custom
|
|
620
|
+
// properties on first paint). Matching real cascade order avoids a real
|
|
621
|
+
// specificity/undefined-custom-property regression alphabetical sorting
|
|
622
|
+
// would introduce silently. Re-confirmed against the real index.html
|
|
623
|
+
// directly at implementation time (unchanged from the task-3 brief's own
|
|
624
|
+
// citation).
|
|
625
|
+
const CSS_LOAD_ORDER = [
|
|
626
|
+
'tokens.css', 'shell.css', 'architecture-view.css', 'inspector.css',
|
|
627
|
+
'privacy-view.css', 'trace-view.css', 'filter-rail.css',
|
|
628
|
+
'inventory-view.css', 'query-bar.css',
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
function _inlineCss() {
|
|
632
|
+
const onDisk = new Set(external_node_fs_.readdirSync(STYLES_DIR).filter((f) => f.endsWith('.css')));
|
|
633
|
+
const missing = CSS_LOAD_ORDER.filter((f) => !onDisk.has(f));
|
|
634
|
+
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`);
|
|
635
|
+
const extra = [...onDisk].filter((f) => !CSS_LOAD_ORDER.includes(f));
|
|
636
|
+
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`);
|
|
637
|
+
return CSS_LOAD_ORDER.map((f) => external_node_fs_.readFileSync(external_node_path_.join(STYLES_DIR, f), 'utf8')).join('\n');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function generateHtmlReport(graph, opts = {}) {
|
|
641
|
+
const exported = (0,export_json.exportGraphJSON)(graph, opts);
|
|
642
|
+
const css = _inlineCss();
|
|
643
|
+
const bundledJs = bundleFrontendModules(ENTRY_PATH);
|
|
644
|
+
// Escape `<` so scanned-source-derived content (a node/flow/data-element
|
|
645
|
+
// label — none of which _redactGraph covers, since labels aren't a
|
|
646
|
+
// secret-shaped surface, but CAN carry arbitrary scanned identifier
|
|
647
|
+
// text) can never contain a literal `</script>` that breaks out of this
|
|
648
|
+
// inline data script and injects a second one. Same mitigation, same
|
|
649
|
+
// reasoning, as the existing SAST/SCA HTML report's own precedent
|
|
650
|
+
// (scanner/src/report/index.js's toHTML) — found missing here and
|
|
651
|
+
// fixed by this sub-project's own final task review, which reproduced
|
|
652
|
+
// the injection live before this fix landed.
|
|
653
|
+
const envelopeJson = JSON.stringify(exported).replace(/</g, '\\u003c');
|
|
654
|
+
return `<!DOCTYPE html>
|
|
655
|
+
<html lang="en" data-theme="dark">
|
|
656
|
+
<head>
|
|
657
|
+
<meta charset="UTF-8" />
|
|
658
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
659
|
+
<title>Data Flow Explorer — self-contained report</title>
|
|
660
|
+
<style>
|
|
661
|
+
${css}
|
|
662
|
+
</style>
|
|
663
|
+
</head>
|
|
664
|
+
<body>
|
|
665
|
+
<div id="app-root"></div>
|
|
666
|
+
<script>
|
|
667
|
+
// The full exportGraphJSON envelope (digest/scope/coverage/limitations/
|
|
668
|
+
// confidential + the redacted graph body) is embedded whole here, under
|
|
669
|
+
// its own name, so this report carries the same tamper-evident/disclosure
|
|
670
|
+
// metadata the JSON exporter produces (PRD §17.5) — never dropped just
|
|
671
|
+
// because this is an HTML report rather than a .json file.
|
|
672
|
+
//
|
|
673
|
+
// __AGENTIC_SECURITY_EXPORTED_GRAPH__ is then bound to the envelope's own
|
|
674
|
+
// \`.graph\` body, NOT the envelope itself — this is a deliberate,
|
|
675
|
+
// necessary departure from an earlier draft of this generator (which
|
|
676
|
+
// assigned the whole envelope directly to that name). frontend/src/
|
|
677
|
+
// export-entry.js (Task 2, already shipped and covered by its own golden
|
|
678
|
+
// DOM-equivalence test, test/bundle-frontend-golden.test.js) reads this
|
|
679
|
+
// EXACT global and passes it straight through to bootstrap(rootEl, graph)
|
|
680
|
+
// -> mountShell(rootEl, graph)/computeFilterFacets(graph), both of which
|
|
681
|
+
// expect a real DataFlowGraph v1 document (top-level nodes/edges/flows/...),
|
|
682
|
+
// never the exportGraphJSON envelope shape (which nests the graph one
|
|
683
|
+
// level down, under .graph, alongside digest/scope/confidential). Handing
|
|
684
|
+
// bootstrap() the envelope instead of the graph would silently break real
|
|
685
|
+
// rendering — exactly what this sub-project's own real-Chrome acceptance
|
|
686
|
+
// proof (task-3 brief, Step 5) exists to catch. Confirmed against
|
|
687
|
+
// export-entry.js's and Task 2's golden test's real source before this
|
|
688
|
+
// generator was written, not assumed.
|
|
689
|
+
window.__AGENTIC_SECURITY_EXPORT_ENVELOPE__ = ${envelopeJson};
|
|
690
|
+
window.__AGENTIC_SECURITY_EXPORTED_GRAPH__ = window.__AGENTIC_SECURITY_EXPORT_ENVELOPE__.graph;
|
|
691
|
+
</script>
|
|
692
|
+
<script>
|
|
693
|
+
${bundledJs}
|
|
694
|
+
</script>
|
|
695
|
+
</body>
|
|
696
|
+
</html>`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
/***/ })
|
|
701
|
+
|
|
702
|
+
};
|