@tracelane/report 0.1.0-alpha.2 → 0.1.0-alpha.20
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/NOTICE +8 -0
- package/README.md +33 -5
- package/dist/_security/detectors/insecure-cookies.d.ts +4 -0
- package/dist/_security/detectors/insecure-cookies.d.ts.map +1 -0
- package/dist/_security/detectors/insecure-cookies.js +32 -0
- package/dist/_security/detectors/insecure-cookies.js.map +1 -0
- package/dist/_security/detectors/missing-headers.d.ts +4 -0
- package/dist/_security/detectors/missing-headers.d.ts.map +1 -0
- package/dist/_security/detectors/missing-headers.js +36 -0
- package/dist/_security/detectors/missing-headers.js.map +1 -0
- package/dist/_security/detectors/mixed-content.d.ts +5 -0
- package/dist/_security/detectors/mixed-content.d.ts.map +1 -0
- package/dist/_security/detectors/mixed-content.js +74 -0
- package/dist/_security/detectors/mixed-content.js.map +1 -0
- package/dist/_security/detectors/reverse-tabnabbing.d.ts +10 -0
- package/dist/_security/detectors/reverse-tabnabbing.d.ts.map +1 -0
- package/dist/_security/detectors/reverse-tabnabbing.js +44 -0
- package/dist/_security/detectors/reverse-tabnabbing.js.map +1 -0
- package/dist/_security/index.d.ts +29 -0
- package/dist/_security/index.d.ts.map +1 -0
- package/dist/_security/index.js +36 -0
- package/dist/_security/index.js.map +1 -0
- package/dist/_security/response-meta.d.ts +28 -0
- package/dist/_security/response-meta.d.ts.map +1 -0
- package/dist/_security/response-meta.js +50 -0
- package/dist/_security/response-meta.js.map +1 -0
- package/dist/_security/serialized-dom.d.ts +20 -0
- package/dist/_security/serialized-dom.d.ts.map +1 -0
- package/dist/_security/serialized-dom.js +32 -0
- package/dist/_security/serialized-dom.js.map +1 -0
- package/dist/_security/suppress.d.ts +7 -0
- package/dist/_security/suppress.d.ts.map +1 -0
- package/dist/_security/suppress.js +8 -0
- package/dist/_security/suppress.js.map +1 -0
- package/dist/assets.d.ts +20 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +60 -6
- package/dist/assets.js.map +1 -1
- package/dist/build-report.d.ts +19 -0
- package/dist/build-report.d.ts.map +1 -1
- package/dist/build-report.js +20 -2
- package/dist/build-report.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +11 -1
- package/dist/markdown.js.map +1 -1
- package/dist/metadata.d.ts +21 -2
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +98 -27
- package/dist/metadata.js.map +1 -1
- package/dist/panels.d.ts +40 -5
- package/dist/panels.d.ts.map +1 -1
- package/dist/panels.js +175 -23
- package/dist/panels.js.map +1 -1
- package/dist/report-writer.d.ts +43 -0
- package/dist/report-writer.d.ts.map +1 -0
- package/dist/report-writer.js +65 -0
- package/dist/report-writer.js.map +1 -0
- package/dist/template.d.ts +19 -0
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +987 -72
- package/dist/template.js.map +1 -1
- package/package.json +28 -7
package/NOTICE
CHANGED
|
@@ -10,6 +10,14 @@ This product includes software developed by:
|
|
|
10
10
|
rrweb-player 1.0.0-alpha.4 (MIT License) — UMD + CSS inlined into report HTML.
|
|
11
11
|
- fflate (https://github.com/101arrowz/fflate) 0.8.x (MIT License) —
|
|
12
12
|
browser gunzip build inlined into report HTML for offline decompression.
|
|
13
|
+
- Google Inc. + Universal Thirst — Fraunces (SIL Open Font License 1.1) —
|
|
14
|
+
variable serif font, latin charset (normal + italic styles), embedded
|
|
15
|
+
into report HTML as base64 woff2 inside @font-face rules. Used for the
|
|
16
|
+
hero headline + section heads. Source: @fontsource-variable/fraunces.
|
|
17
|
+
- Philipp Nurullin + JetBrains — JetBrains Mono (SIL Open Font License 1.1)
|
|
18
|
+
— variable monospace font, latin charset (normal style only), embedded
|
|
19
|
+
the same way. Used for all data rows + metadata. Source:
|
|
20
|
+
@fontsource-variable/jetbrains-mono.
|
|
13
21
|
|
|
14
22
|
This product is licensed under the Apache License, Version 2.0.
|
|
15
23
|
See the LICENSE file for details.
|
package/README.md
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/Cubenest/rrweb-stack/main/assets/brand/sub-tracelane.svg" height="40" alt="tracelane">
|
|
2
|
+
|
|
1
3
|
# @tracelane/report
|
|
2
4
|
|
|
3
|
-
The
|
|
5
|
+
> The HTML report builder behind tracelane — turns a captured rrweb event stream into a single, self-contained `.html` file that replays offline. No SaaS, no dashboard, no signup.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@tracelane/report)
|
|
8
|
+
[](https://www.npmjs.com/package/@tracelane/report)
|
|
9
|
+
[](https://github.com/Cubenest/rrweb-stack/blob/main/LICENSE)
|
|
10
|
+
[](https://github.com/Cubenest/rrweb-stack/actions/workflows/ci.yml)
|
|
11
|
+
[](https://scorecard.dev/viewer/?uri=github.com/Cubenest/rrweb-stack)
|
|
12
|
+
[](https://www.npmjs.com/package/@tracelane/report)
|
|
13
|
+
[](https://www.npmjs.com/package/@tracelane/report)
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
The self-contained, offline HTML report builder for [`tracelane`](https://github.com/Cubenest/rrweb-stack). Given a captured rrweb event stream plus test metadata, it produces a **single `.html` file** that:
|
|
4
17
|
|
|
5
18
|
- opens in any browser, fully offline (no network fetch at view time);
|
|
6
19
|
- embeds the [`rrweb-player`](https://www.npmjs.com/package/rrweb-player) UMD + CSS inline;
|
|
7
20
|
- embeds the events as a gzipped, base64-encoded blob that is decompressed in-page with an inlined [`fflate`](https://github.com/101arrowz/fflate) gunzip;
|
|
8
21
|
- renders console + network panels, a metadata header, and a "Copy as Markdown for AI paste" button.
|
|
9
22
|
|
|
10
|
-
Not generally intended for direct consumption — depend on a product package (`@tracelane/wdio`) instead.
|
|
23
|
+
**Not generally intended for direct consumption** — depend on a product package (`@tracelane/wdio`) instead. See the [`@tracelane/wdio` README](https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-wdio) for the integration guide.
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install @tracelane/report
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **ESM-only.** The package ships `"type": "module"` and a single `import` export — there is no CommonJS entry, so `require('@tracelane/report')` will not work. Use `import { buildReport } from '@tracelane/report'` (or a dynamic `import()` from CJS).
|
|
31
|
+
- **Node >= 22** is required (`engines.node`).
|
|
11
32
|
|
|
12
33
|
## Usage
|
|
13
34
|
|
|
@@ -30,12 +51,19 @@ const html = buildReport(events, {
|
|
|
30
51
|
|
|
31
52
|
## Design
|
|
32
53
|
|
|
33
|
-
See [P1 PRD §F](https://github.com/Cubenest/rrweb-stack/blob/main/prds/compass_artifact_wf-d53d32da-17e9-41b5-bb70-21dd1bf648c6_text_markdown.md) and [ADR-0005](https://github.com/Cubenest/rrweb-stack/blob/main/prds/adrs/0005-p1-failed-only-self-contained-html.md).
|
|
34
|
-
|
|
35
54
|
- **Player:** `rrweb-player@1.0.0-alpha.4` (upstream). `@posthog/rrweb-player` was the natural lineage match for the `@cubenest/rrweb-core` substrate fork, but every published version pins an unpublished dependency (`@posthog/rrweb-packer@0.0.0`, 404) and is therefore uninstallable. Upstream `rrweb-player` descends from the same rrweb 2.x line (`2.0.0-alpha.x`) that the substrate's `@posthog/rrweb@0.0.34` was forked from (`2.0.0-alpha.17`), so the recorded event shape replays correctly.
|
|
36
55
|
- **Decompression:** the build side uses `@cubenest/rrweb-core`'s `compress()` (fflate gzip); the view side inlines fflate's browser `gunzipSync` for a small (~8 KB) offline decompressor.
|
|
37
56
|
- **Asset inlining:** the player UMD/CSS and the fflate gunzip source are read from `node_modules` at build time (`fs.readFileSync`) — never hand-pasted into source.
|
|
38
57
|
|
|
39
58
|
## License
|
|
40
59
|
|
|
41
|
-
Apache 2.0. The inlined rrweb player and fflate remain MIT-licensed; see NOTICE.
|
|
60
|
+
Apache 2.0. The inlined rrweb player and fflate remain MIT-licensed; see [NOTICE](https://github.com/Cubenest/rrweb-stack/blob/main/packages/tracelane-report/NOTICE).
|
|
61
|
+
|
|
62
|
+
## Related packages
|
|
63
|
+
|
|
64
|
+
- [`@tracelane/cli`](https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-cli) — wires the recorder into your test runners (WebdriverIO and Playwright; Cypress on the roadmap).
|
|
65
|
+
- [`@tracelane/wdio`](https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-wdio) — the WebdriverIO integration that captures sessions and calls this builder on failure.
|
|
66
|
+
- [`@tracelane/playwright`](https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-playwright) — the Playwright integration.
|
|
67
|
+
- [`@tracelane/core`](https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-core) — the capture engine (rrweb event stream + `compress()`).
|
|
68
|
+
|
|
69
|
+
See the [CHANGELOG](https://github.com/Cubenest/rrweb-stack/blob/main/packages/tracelane-report/CHANGELOG.md) for release history.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insecure-cookies.d.ts","sourceRoot":"","sources":["../../../src/_security/detectors/insecure-cookies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAY,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAQxD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,eAAe,EAAE,CAuBvF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const FLAGS = [
|
|
2
|
+
{ key: 'secure', label: 'Secure', severity: 'medium' },
|
|
3
|
+
{ key: 'httpOnly', label: 'HttpOnly', severity: 'low' },
|
|
4
|
+
{ key: 'sameSite', label: 'SameSite', severity: 'low' },
|
|
5
|
+
];
|
|
6
|
+
export function detectInsecureCookies(metas) {
|
|
7
|
+
const out = [];
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
for (const m of metas) {
|
|
10
|
+
for (const c of m.setCookies) {
|
|
11
|
+
for (const f of FLAGS) {
|
|
12
|
+
if (c[f.key])
|
|
13
|
+
continue;
|
|
14
|
+
const id = `insecure-cookie:${c.name}:${f.label}`;
|
|
15
|
+
if (seen.has(id))
|
|
16
|
+
continue;
|
|
17
|
+
seen.add(id);
|
|
18
|
+
out.push({
|
|
19
|
+
id,
|
|
20
|
+
signal: 'insecure-cookie',
|
|
21
|
+
severity: f.severity,
|
|
22
|
+
title: `Cookie '${c.name}' missing ${f.label}`,
|
|
23
|
+
detail: `Set-Cookie '${c.name}' did not set the ${f.label} attribute.`,
|
|
24
|
+
evidence: `${c.name}:${f.label}`,
|
|
25
|
+
advisory: true,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=insecure-cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insecure-cookies.js","sourceRoot":"","sources":["../../../src/_security/detectors/insecure-cookies.ts"],"names":[],"mappings":"AAGA,MAAM,KAAK,GAAqF;IAC9F,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACtD,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE;IACvD,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE;CACxD,CAAC;AAEF,MAAM,UAAU,qBAAqB,CAAC,KAA8B;IAClE,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;YAC7B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;oBAAE,SAAS;gBACvB,MAAM,EAAE,GAAG,mBAAmB,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBAClD,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,SAAS;gBAC3B,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACb,GAAG,CAAC,IAAI,CAAC;oBACP,EAAE;oBACF,MAAM,EAAE,iBAAiB;oBACzB,QAAQ,EAAE,CAAC,CAAC,QAAQ;oBACpB,KAAK,EAAE,WAAW,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,KAAK,EAAE;oBAC9C,MAAM,EAAE,eAAe,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,KAAK,aAAa;oBACtE,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE;oBAChC,QAAQ,EAAE,IAAI;iBACf,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-headers.d.ts","sourceRoot":"","sources":["../../../src/_security/detectors/missing-headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAY,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAcxD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,eAAe,EAAE,CAoBtF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const HEADERS = [
|
|
2
|
+
{ name: 'content-security-policy', severity: 'high', label: 'Content-Security-Policy' },
|
|
3
|
+
{
|
|
4
|
+
name: 'strict-transport-security',
|
|
5
|
+
severity: 'medium',
|
|
6
|
+
label: 'Strict-Transport-Security (HSTS)',
|
|
7
|
+
},
|
|
8
|
+
{ name: 'x-frame-options', severity: 'medium', label: 'X-Frame-Options' },
|
|
9
|
+
{ name: 'x-content-type-options', severity: 'medium', label: 'X-Content-Type-Options' },
|
|
10
|
+
{ name: 'referrer-policy', severity: 'low', label: 'Referrer-Policy' },
|
|
11
|
+
];
|
|
12
|
+
export function detectMissingHeaders(metas) {
|
|
13
|
+
const main = metas.find((m) => m.isMainDocument);
|
|
14
|
+
if (!main)
|
|
15
|
+
return [];
|
|
16
|
+
// HTTPS gate: header/HSTS checks are moot + noisy on non-HTTPS (localhost/non-prod).
|
|
17
|
+
if (!main.url.startsWith('https://'))
|
|
18
|
+
return [];
|
|
19
|
+
const present = new Set(main.presentSecurityHeaders.map((h) => h.toLowerCase()));
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const h of HEADERS) {
|
|
22
|
+
if (present.has(h.name))
|
|
23
|
+
continue;
|
|
24
|
+
out.push({
|
|
25
|
+
id: `missing-security-header:${h.name}`,
|
|
26
|
+
signal: 'missing-security-header',
|
|
27
|
+
severity: h.severity,
|
|
28
|
+
title: `Missing ${h.label} header`,
|
|
29
|
+
detail: `The main document response did not set the ${h.label} header.`,
|
|
30
|
+
evidence: h.name,
|
|
31
|
+
advisory: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=missing-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-headers.js","sourceRoot":"","sources":["../../../src/_security/detectors/missing-headers.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,GAA0D;IACrE,EAAE,IAAI,EAAE,yBAAyB,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,yBAAyB,EAAE;IACvF;QACE,IAAI,EAAE,2BAA2B;QACjC,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,kCAAkC;KAC1C;IACD,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACzE,EAAE,IAAI,EAAE,wBAAwB,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,wBAAwB,EAAE;IACvF,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE;CACvE,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,KAA8B;IACjE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;IACjD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,qFAAqF;IACrF,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACjF,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,SAAS;QAClC,GAAG,CAAC,IAAI,CAAC;YACP,EAAE,EAAE,2BAA2B,CAAC,CAAC,IAAI,EAAE;YACvC,MAAM,EAAE,yBAAyB;YACjC,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,KAAK,EAAE,WAAW,CAAC,CAAC,KAAK,SAAS;YAClC,MAAM,EAAE,8CAA8C,CAAC,CAAC,KAAK,UAAU;YACvE,QAAQ,EAAE,CAAC,CAAC,IAAI;YAChB,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { eventWithTime } from '@cubenest/rrweb-core';
|
|
2
|
+
import type { SecurityFinding } from '../index.js';
|
|
3
|
+
import type { ResponseMeta } from '../response-meta.js';
|
|
4
|
+
export declare function detectMixedContent(events: readonly eventWithTime[], metas: readonly ResponseMeta[]): SecurityFinding[];
|
|
5
|
+
//# sourceMappingURL=mixed-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mixed-content.d.ts","sourceRoot":"","sources":["../../../src/_security/detectors/mixed-content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA0CxD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,KAAK,EAAE,SAAS,YAAY,EAAE,GAC7B,eAAe,EAAE,CA8BnB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { collectRoots, walk } from '../serialized-dom.js';
|
|
2
|
+
/**
|
|
3
|
+
* Walks rrweb serialized DOM snapshots for subresource-loading attributes that
|
|
4
|
+
* point at `http://` on an HTTPS page — the mixed-content risk. Re-sourced from
|
|
5
|
+
* the DOM (like reverse-tabnabbing) because the capture layer only emits a
|
|
6
|
+
* `[tracelane.sec]` meta for the MAIN DOCUMENT, so subresource URLs never reach
|
|
7
|
+
* the meta stream. The HTTPS gate comes from the main-document meta.
|
|
8
|
+
*
|
|
9
|
+
* Scope (kept tight to limit false positives for the MVP):
|
|
10
|
+
* - the `src` attribute on ANY element (img, script, iframe, video, audio,
|
|
11
|
+
* source, …),
|
|
12
|
+
* - the `href` attribute ONLY on a `<link>` whose `rel` actually fetches a
|
|
13
|
+
* subresource (stylesheet/preload/icon/…). A `<link rel="canonical">` or
|
|
14
|
+
* `<a href>` is a navigation/metadata hint, not a subresource, so it is NOT
|
|
15
|
+
* flagged.
|
|
16
|
+
* - `srcset` is not parsed.
|
|
17
|
+
* Dedupes by url; advisory, never an audit result.
|
|
18
|
+
*/
|
|
19
|
+
// `<link rel>` values that actually fetch a resource over the wire (so an
|
|
20
|
+
// `http://` href is genuine mixed content). Excludes metadata/hint rels like
|
|
21
|
+
// canonical, alternate, author, license, and the connection-only hints
|
|
22
|
+
// dns-prefetch/preconnect (which establish a connection but load no content).
|
|
23
|
+
const SUBRESOURCE_LINK_RELS = new Set([
|
|
24
|
+
'stylesheet',
|
|
25
|
+
'preload',
|
|
26
|
+
'modulepreload',
|
|
27
|
+
'prefetch',
|
|
28
|
+
'prerender',
|
|
29
|
+
'icon',
|
|
30
|
+
'manifest',
|
|
31
|
+
]);
|
|
32
|
+
function linkLoadsSubresource(rel) {
|
|
33
|
+
if (typeof rel !== 'string')
|
|
34
|
+
return false;
|
|
35
|
+
return rel
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.split(/\s+/)
|
|
38
|
+
.some((token) => SUBRESOURCE_LINK_RELS.has(token));
|
|
39
|
+
}
|
|
40
|
+
export function detectMixedContent(events, metas) {
|
|
41
|
+
const main = metas.find((mt) => mt.isMainDocument);
|
|
42
|
+
// Mixed content is only meaningful on an HTTPS page.
|
|
43
|
+
if (!main || !main.url.startsWith('https://'))
|
|
44
|
+
return [];
|
|
45
|
+
const out = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const root of collectRoots(events)) {
|
|
48
|
+
for (const n of walk(root)) {
|
|
49
|
+
if (n.type !== 2)
|
|
50
|
+
continue;
|
|
51
|
+
const attrs = n.attributes ?? {};
|
|
52
|
+
const tag = n.tagName?.toLowerCase();
|
|
53
|
+
const urls = [attrs.src];
|
|
54
|
+
if (tag === 'link' && linkLoadsSubresource(attrs.rel))
|
|
55
|
+
urls.push(attrs.href);
|
|
56
|
+
for (const u of urls) {
|
|
57
|
+
if (typeof u !== 'string' || !u.startsWith('http://') || seen.has(u))
|
|
58
|
+
continue;
|
|
59
|
+
seen.add(u);
|
|
60
|
+
out.push({
|
|
61
|
+
id: `mixed-content:${u}`,
|
|
62
|
+
signal: 'mixed-content',
|
|
63
|
+
severity: 'high',
|
|
64
|
+
title: 'Mixed content (HTTP resource on an HTTPS page)',
|
|
65
|
+
detail: `An HTTP resource (${u}) was loaded by the HTTPS page ${main.url}.`,
|
|
66
|
+
evidence: u,
|
|
67
|
+
advisory: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=mixed-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mixed-content.js","sourceRoot":"","sources":["../../../src/_security/detectors/mixed-content.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;;;;;;;;;;;;;GAgBG;AAEH,0EAA0E;AAC1E,6EAA6E;AAC7E,uEAAuE;AACvE,8EAA8E;AAC9E,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,YAAY;IACZ,SAAS;IACT,eAAe;IACf,UAAU;IACV,WAAW;IACX,MAAM;IACN,UAAU;CACX,CAAC,CAAC;AAEH,SAAS,oBAAoB,CAAC,GAAY;IACxC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,GAAG;SACP,WAAW,EAAE;SACb,KAAK,CAAC,KAAK,CAAC;SACZ,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AACvD,CAAC;AACD,MAAM,UAAU,kBAAkB,CAChC,MAAgC,EAChC,KAA8B;IAE9B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC;IACnD,qDAAqD;IACrD,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,EAAE,CAAC;IAEzD,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;gBAAE,SAAS;YAC3B,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC;YACjC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC;YACrC,MAAM,IAAI,GAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACpC,IAAI,GAAG,KAAK,MAAM,IAAI,oBAAoB,CAAC,KAAK,CAAC,GAAG,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC7E,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;oBAAE,SAAS;gBAC/E,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACZ,GAAG,CAAC,IAAI,CAAC;oBACP,EAAE,EAAE,iBAAiB,CAAC,EAAE;oBACxB,MAAM,EAAE,eAAe;oBACvB,QAAQ,EAAE,MAAM;oBAChB,KAAK,EAAE,gDAAgD;oBACvD,MAAM,EAAE,qBAAqB,CAAC,kCAAkC,IAAI,CAAC,GAAG,GAAG;oBAC3E,QAAQ,EAAE,CAAC;oBACX,QAAQ,EAAE,IAAI;iBACf,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { eventWithTime } from '@cubenest/rrweb-core';
|
|
2
|
+
import type { SecurityFinding } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Walks rrweb serialized DOM snapshots (FullSnapshot trees + IncrementalSnapshot
|
|
5
|
+
* `adds`) for `<a target="_blank">` links lacking a `rel` of `noopener`/
|
|
6
|
+
* `noreferrer` — the classic reverse-tabnabbing risk. Pure over plain serialized
|
|
7
|
+
* node objects; no DOM API. Dedupes by href; advisory, never an audit result.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectReverseTabnabbing(events: readonly eventWithTime[]): SecurityFinding[];
|
|
10
|
+
//# sourceMappingURL=reverse-tabnabbing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverse-tabnabbing.d.ts","sourceRoot":"","sources":["../../../src/_security/detectors/reverse-tabnabbing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AASnD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,eAAe,EAAE,CA0B3F"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { collectRoots, walk } from '../serialized-dom.js';
|
|
2
|
+
function relIsSafe(rel) {
|
|
3
|
+
if (typeof rel !== 'string')
|
|
4
|
+
return false;
|
|
5
|
+
const tokens = rel.toLowerCase().split(/\s+/);
|
|
6
|
+
return tokens.includes('noopener') || tokens.includes('noreferrer');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Walks rrweb serialized DOM snapshots (FullSnapshot trees + IncrementalSnapshot
|
|
10
|
+
* `adds`) for `<a target="_blank">` links lacking a `rel` of `noopener`/
|
|
11
|
+
* `noreferrer` — the classic reverse-tabnabbing risk. Pure over plain serialized
|
|
12
|
+
* node objects; no DOM API. Dedupes by href; advisory, never an audit result.
|
|
13
|
+
*/
|
|
14
|
+
export function detectReverseTabnabbing(events) {
|
|
15
|
+
const roots = collectRoots(events);
|
|
16
|
+
const out = [];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const root of roots) {
|
|
19
|
+
for (const n of walk(root)) {
|
|
20
|
+
if (n.type !== 2 || n.tagName?.toLowerCase() !== 'a')
|
|
21
|
+
continue;
|
|
22
|
+
const attrs = n.attributes ?? {};
|
|
23
|
+
const target = typeof attrs.target === 'string' ? attrs.target.toLowerCase() : '';
|
|
24
|
+
if (target !== '_blank' || relIsSafe(attrs.rel))
|
|
25
|
+
continue;
|
|
26
|
+
const href = typeof attrs.href === 'string' ? attrs.href : '(no href)';
|
|
27
|
+
const id = `reverse-tabnabbing:${href}`;
|
|
28
|
+
if (seen.has(id))
|
|
29
|
+
continue;
|
|
30
|
+
seen.add(id);
|
|
31
|
+
out.push({
|
|
32
|
+
id,
|
|
33
|
+
signal: 'reverse-tabnabbing',
|
|
34
|
+
severity: 'medium',
|
|
35
|
+
title: 'Reverse tabnabbing risk (target="_blank" without rel="noopener")',
|
|
36
|
+
detail: `An <a target="_blank"> link (${href}) is missing rel="noopener"/"noreferrer".`,
|
|
37
|
+
evidence: href,
|
|
38
|
+
advisory: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=reverse-tabnabbing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverse-tabnabbing.js","sourceRoot":"","sources":["../../../src/_security/detectors/reverse-tabnabbing.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAE1D,SAAS,SAAS,CAAC,GAAY;IAC7B,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;AACtE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAgC;IACtE,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,GAAG;gBAAE,SAAS;YAC/D,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClF,IAAI,MAAM,KAAK,QAAQ,IAAI,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC1D,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC;YACvE,MAAM,EAAE,GAAG,sBAAsB,IAAI,EAAE,CAAC;YACxC,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC3B,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACb,GAAG,CAAC,IAAI,CAAC;gBACP,EAAE;gBACF,MAAM,EAAE,oBAAoB;gBAC5B,QAAQ,EAAE,QAAQ;gBAClB,KAAK,EAAE,kEAAkE;gBACzE,MAAM,EAAE,gCAAgC,IAAI,2CAA2C;gBACvF,QAAQ,EAAE,IAAI;gBACd,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { eventWithTime } from '@cubenest/rrweb-core';
|
|
2
|
+
import { type Suppression } from './suppress.js';
|
|
3
|
+
export type { Suppression };
|
|
4
|
+
/** console.error prefix the capture layer uses for privacy-safe response metadata. */
|
|
5
|
+
export declare const SEC_CONSOLE_PREFIX = "[tracelane.sec]";
|
|
6
|
+
/** rrweb Custom event tag the capture layer uses for privacy-safe response metadata. */
|
|
7
|
+
export declare const SEC_EVENT_TAG = "tracelane.sec";
|
|
8
|
+
export type SecuritySignal = 'missing-security-header' | 'mixed-content' | 'insecure-cookie' | 'reverse-tabnabbing';
|
|
9
|
+
export type Severity = 'low' | 'medium' | 'high';
|
|
10
|
+
export interface SecurityFinding {
|
|
11
|
+
/** stable id, e.g. `${signal}:${evidence}` */
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly signal: SecuritySignal;
|
|
14
|
+
readonly severity: Severity;
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly detail: string;
|
|
17
|
+
readonly evidence: string;
|
|
18
|
+
/** framing invariant — always true; these are advisory, not audit results */
|
|
19
|
+
readonly advisory: true;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Derive advisory security-hygiene findings from a captured event stream.
|
|
23
|
+
* Pure + total: never throws (a failing detector contributes nothing). Findings
|
|
24
|
+
* are advisory only — NOT a security audit/scan/guarantee.
|
|
25
|
+
*/
|
|
26
|
+
export declare function analyze(events: readonly eventWithTime[], opts?: {
|
|
27
|
+
suppress?: readonly Suppression[];
|
|
28
|
+
}): SecurityFinding[];
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/_security/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAO1D,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,eAAe,CAAC;AAEpE,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,oBAAoB,CAAC;AAEpD,wFAAwF;AACxF,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C,MAAM,MAAM,cAAc,GACtB,yBAAyB,GACzB,eAAe,GACf,iBAAiB,GACjB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;CACzB;AAYD;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;CAAO,GAC/C,eAAe,EAAE,CAanB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { detectInsecureCookies } from './detectors/insecure-cookies.js';
|
|
2
|
+
import { detectMissingHeaders } from './detectors/missing-headers.js';
|
|
3
|
+
import { detectMixedContent } from './detectors/mixed-content.js';
|
|
4
|
+
import { detectReverseTabnabbing } from './detectors/reverse-tabnabbing.js';
|
|
5
|
+
import { scrapeResponseMeta } from './response-meta.js';
|
|
6
|
+
import { applySuppressions } from './suppress.js';
|
|
7
|
+
/** console.error prefix the capture layer uses for privacy-safe response metadata. */
|
|
8
|
+
export const SEC_CONSOLE_PREFIX = '[tracelane.sec]';
|
|
9
|
+
/** rrweb Custom event tag the capture layer uses for privacy-safe response metadata. */
|
|
10
|
+
export const SEC_EVENT_TAG = 'tracelane.sec';
|
|
11
|
+
const SEVERITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
12
|
+
function safe(fn, fallback) {
|
|
13
|
+
try {
|
|
14
|
+
return fn();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derive advisory security-hygiene findings from a captured event stream.
|
|
22
|
+
* Pure + total: never throws (a failing detector contributes nothing). Findings
|
|
23
|
+
* are advisory only — NOT a security audit/scan/guarantee.
|
|
24
|
+
*/
|
|
25
|
+
export function analyze(events, opts = {}) {
|
|
26
|
+
const metas = safe(() => scrapeResponseMeta(events), []);
|
|
27
|
+
const findings = [
|
|
28
|
+
...safe(() => detectMissingHeaders(metas), []),
|
|
29
|
+
...safe(() => detectMixedContent(events, metas), []),
|
|
30
|
+
...safe(() => detectInsecureCookies(metas), []),
|
|
31
|
+
...safe(() => detectReverseTabnabbing(events), []),
|
|
32
|
+
];
|
|
33
|
+
const kept = applySuppressions(findings, opts.suppress ?? []);
|
|
34
|
+
return [...kept].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || a.signal.localeCompare(b.signal));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/_security/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAE5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAoB,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAIpE,sFAAsF;AACtF,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAEpD,wFAAwF;AACxF,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAsB7C,MAAM,aAAa,GAA6B,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AAE/E,SAAS,IAAI,CAAI,EAAW,EAAE,QAAW;IACvC,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CACrB,MAAgC,EAChC,OAA8C,EAAE;IAEhD,MAAM,KAAK,GAAG,IAAI,CAAiB,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAsB;QAClC,GAAG,IAAI,CAAoB,GAAG,EAAE,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QACjE,GAAG,IAAI,CAAoB,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;QACvE,GAAG,IAAI,CAAoB,GAAG,EAAE,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QAClE,GAAG,IAAI,CAAoB,GAAG,EAAE,CAAC,uBAAuB,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;KACtE,CAAC;IACF,MAAM,IAAI,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CACnB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAC5F,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { eventWithTime } from '@cubenest/rrweb-core';
|
|
2
|
+
/**
|
|
3
|
+
* Privacy-safe response metadata recovered from a `tracelane.sec` Custom event.
|
|
4
|
+
* Carries only header NAMES (never values), per-cookie flag presence
|
|
5
|
+
* booleans, and the cookie name — never header values or cookie values.
|
|
6
|
+
*/
|
|
7
|
+
export interface ResponseMeta {
|
|
8
|
+
url: string;
|
|
9
|
+
status: number;
|
|
10
|
+
isMainDocument: boolean;
|
|
11
|
+
/** lowercased header NAMES that were present (never values) */
|
|
12
|
+
presentSecurityHeaders: string[];
|
|
13
|
+
/** per-cookie flag presence (never names/values beyond the cookie name) */
|
|
14
|
+
setCookies: {
|
|
15
|
+
name: string;
|
|
16
|
+
secure: boolean;
|
|
17
|
+
httpOnly: boolean;
|
|
18
|
+
sameSite: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read privacy-safe response metadata out of the rrweb Custom events the
|
|
23
|
+
* capture layer injects (tag `tracelane.sec`, payload = the meta object).
|
|
24
|
+
* Replaces the old console-scrape channel, which raced navigation. Malformed
|
|
25
|
+
* payloads are skipped.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scrapeResponseMeta(events: readonly eventWithTime[]): ResponseMeta[];
|
|
28
|
+
//# sourceMappingURL=response-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-meta.d.ts","sourceRoot":"","sources":["../../src/_security/response-meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,2EAA2E;IAC3E,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CACvF;AAkCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,YAAY,EAAE,CASnF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EventType } from '@cubenest/rrweb-core';
|
|
2
|
+
import { SEC_EVENT_TAG } from './index.js';
|
|
3
|
+
function isCookieFlags(c) {
|
|
4
|
+
if (!c || typeof c !== 'object')
|
|
5
|
+
return false;
|
|
6
|
+
const k = c;
|
|
7
|
+
return (typeof k.name === 'string' &&
|
|
8
|
+
typeof k.secure === 'boolean' &&
|
|
9
|
+
typeof k.httpOnly === 'boolean' &&
|
|
10
|
+
typeof k.sameSite === 'boolean');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Full structural validation of a `tracelane.sec` Custom-event payload, including
|
|
14
|
+
* array ELEMENT types. A page can't forge a Node-side Custom event, but a
|
|
15
|
+
* shape-malformed object (e.g. `presentSecurityHeaders: [123]` or
|
|
16
|
+
* `setCookies: [{}]`) must still be rejected to honor the "malformed payloads
|
|
17
|
+
* are skipped" contract and protect downstream consumers from runtime errors.
|
|
18
|
+
*/
|
|
19
|
+
function isResponseMeta(parsed) {
|
|
20
|
+
if (!parsed || typeof parsed !== 'object')
|
|
21
|
+
return false;
|
|
22
|
+
const m = parsed;
|
|
23
|
+
return (typeof m.url === 'string' &&
|
|
24
|
+
typeof m.status === 'number' &&
|
|
25
|
+
typeof m.isMainDocument === 'boolean' &&
|
|
26
|
+
Array.isArray(m.presentSecurityHeaders) &&
|
|
27
|
+
m.presentSecurityHeaders.every((h) => typeof h === 'string') &&
|
|
28
|
+
Array.isArray(m.setCookies) &&
|
|
29
|
+
m.setCookies.every(isCookieFlags));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read privacy-safe response metadata out of the rrweb Custom events the
|
|
33
|
+
* capture layer injects (tag `tracelane.sec`, payload = the meta object).
|
|
34
|
+
* Replaces the old console-scrape channel, which raced navigation. Malformed
|
|
35
|
+
* payloads are skipped.
|
|
36
|
+
*/
|
|
37
|
+
export function scrapeResponseMeta(events) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const e of events) {
|
|
40
|
+
if (e.type !== EventType.Custom)
|
|
41
|
+
continue;
|
|
42
|
+
const data = e.data;
|
|
43
|
+
if (data.tag !== SEC_EVENT_TAG)
|
|
44
|
+
continue;
|
|
45
|
+
if (isResponseMeta(data.payload))
|
|
46
|
+
out.push(data.payload);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=response-meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-meta.js","sourceRoot":"","sources":["../../src/_security/response-meta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAiB3C,SAAS,aAAa,CAAC,CAAU;IAC/B,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,CAAC,GAAG,CAA4B,CAAC;IACvC,OAAO,CACL,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,MAAM,KAAK,SAAS;QAC7B,OAAO,CAAC,CAAC,QAAQ,KAAK,SAAS;QAC/B,OAAO,CAAC,CAAC,QAAQ,KAAK,SAAS,CAChC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,MAAe;IACrC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxD,MAAM,CAAC,GAAG,MAAiC,CAAC;IAC5C,OAAO,CACL,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ;QACzB,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAC5B,OAAO,CAAC,CAAC,cAAc,KAAK,SAAS;QACrC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;QACvC,CAAC,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;QAC5D,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;QAC3B,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,aAAa,CAAC,CAClC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAgC;IACjE,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,MAAM;YAAE,SAAS;QAC1C,MAAM,IAAI,GAAG,CAAC,CAAC,IAA4C,CAAC;QAC5D,IAAI,IAAI,CAAC,GAAG,KAAK,aAAa;YAAE,SAAS;QACzC,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { eventWithTime } from '@cubenest/rrweb-core';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal shape of an rrweb serialized DOM node. `type === 2` is an Element.
|
|
4
|
+
* Pure plain-object view — no DOM API, no rrweb internals beyond this shape.
|
|
5
|
+
*/
|
|
6
|
+
export interface SNode {
|
|
7
|
+
type?: number;
|
|
8
|
+
tagName?: string;
|
|
9
|
+
attributes?: Record<string, unknown>;
|
|
10
|
+
childNodes?: SNode[];
|
|
11
|
+
}
|
|
12
|
+
/** Depth-first walk: yields `node`, then recurses into `childNodes`. */
|
|
13
|
+
export declare function walk(node: SNode | undefined): Generator<SNode>;
|
|
14
|
+
/**
|
|
15
|
+
* Collect serialized-DOM roots from a captured event stream: the `data.node`
|
|
16
|
+
* tree of each FullSnapshot plus each `data.adds[].node` from an
|
|
17
|
+
* IncrementalSnapshot mutation. Shared by the DOM-walking detectors.
|
|
18
|
+
*/
|
|
19
|
+
export declare function collectRoots(events: readonly eventWithTime[]): SNode[];
|
|
20
|
+
//# sourceMappingURL=serialized-dom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialized-dom.d.ts","sourceRoot":"","sources":["../../src/_security/serialized-dom.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,KAAK;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,UAAU,CAAC,EAAE,KAAK,EAAE,CAAC;CACtB;AAED,wEAAwE;AACxE,wBAAiB,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,CAI/D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,KAAK,EAAE,CAatE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EventType } from '@cubenest/rrweb-core';
|
|
2
|
+
/** Depth-first walk: yields `node`, then recurses into `childNodes`. */
|
|
3
|
+
export function* walk(node) {
|
|
4
|
+
if (!node)
|
|
5
|
+
return;
|
|
6
|
+
yield node;
|
|
7
|
+
for (const child of node.childNodes ?? [])
|
|
8
|
+
yield* walk(child);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Collect serialized-DOM roots from a captured event stream: the `data.node`
|
|
12
|
+
* tree of each FullSnapshot plus each `data.adds[].node` from an
|
|
13
|
+
* IncrementalSnapshot mutation. Shared by the DOM-walking detectors.
|
|
14
|
+
*/
|
|
15
|
+
export function collectRoots(events) {
|
|
16
|
+
const roots = [];
|
|
17
|
+
for (const e of events) {
|
|
18
|
+
if (e.type === EventType.FullSnapshot) {
|
|
19
|
+
roots.push(e.data.node);
|
|
20
|
+
}
|
|
21
|
+
else if (e.type === EventType.IncrementalSnapshot) {
|
|
22
|
+
const adds = e.data.adds;
|
|
23
|
+
if (Array.isArray(adds)) {
|
|
24
|
+
for (const a of adds)
|
|
25
|
+
if (a.node)
|
|
26
|
+
roots.push(a.node);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return roots;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=serialized-dom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialized-dom.js","sourceRoot":"","sources":["../../src/_security/serialized-dom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAcjD,wEAAwE;AACxE,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,IAAuB;IAC3C,IAAI,CAAC,IAAI;QAAE,OAAO;IAClB,MAAM,IAAI,CAAC;IACX,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE;QAAE,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAgC;IAC3D,MAAM,KAAK,GAAY,EAAE,CAAC;IAC1B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,YAAY,EAAE,CAAC;YACtC,KAAK,CAAC,IAAI,CAAE,CAAC,CAAC,IAAwB,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;aAAM,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,mBAAmB,EAAE,CAAC;YACpD,MAAM,IAAI,GAAI,CAAC,CAAC,IAAsC,CAAC,IAAI,CAAC;YAC5D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxB,KAAK,MAAM,CAAC,IAAI,IAAI;oBAAE,IAAI,CAAC,CAAC,IAAI;wBAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SecurityFinding, SecuritySignal } from './index.js';
|
|
2
|
+
export interface Suppression {
|
|
3
|
+
signal?: SecuritySignal;
|
|
4
|
+
evidence?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function applySuppressions(findings: readonly SecurityFinding[], rules: readonly Suppression[]): SecurityFinding[];
|
|
7
|
+
//# sourceMappingURL=suppress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suppress.d.ts","sourceRoot":"","sources":["../../src/_security/suppress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,SAAS,eAAe,EAAE,EACpC,KAAK,EAAE,SAAS,WAAW,EAAE,GAC5B,eAAe,EAAE,CAWnB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function applySuppressions(findings, rules) {
|
|
2
|
+
if (rules.length === 0)
|
|
3
|
+
return [...findings];
|
|
4
|
+
return findings.filter((f) => !rules.some((r) => (r.signal !== undefined || r.evidence !== undefined) &&
|
|
5
|
+
(r.signal === undefined || r.signal === f.signal) &&
|
|
6
|
+
(r.evidence === undefined || r.evidence === f.evidence)));
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=suppress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suppress.js","sourceRoot":"","sources":["../../src/_security/suppress.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,iBAAiB,CAC/B,QAAoC,EACpC,KAA6B;IAE7B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC7C,OAAO,QAAQ,CAAC,MAAM,CACpB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,KAAK,CAAC,IAAI,CACT,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC;QACpD,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC;QACjD,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC,CAC1D,CACJ,CAAC;AACJ,CAAC"}
|
package/dist/assets.d.ts
CHANGED
|
@@ -16,4 +16,24 @@ export declare function loadPlayerCss(): string;
|
|
|
16
16
|
* pako for consistency with `@cubenest/rrweb-core`'s fflate-based `compress()`.
|
|
17
17
|
*/
|
|
18
18
|
export declare function loadFflateGunzipSource(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Fraunces Variable (weight axis 100-900, latin charset, normal style).
|
|
21
|
+
* SIL OFL-1.1 — credited in NOTICE. ~36 KB raw → ~49 KB base64.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadFrauncesNormal(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Fraunces Variable (weight axis 100-900, latin charset, italic style).
|
|
26
|
+
* Same package + license as the normal weight; ~45 KB raw → ~60 KB base64.
|
|
27
|
+
* Used for the headline's emphasized clause and the section heads.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadFrauncesItalic(): string;
|
|
30
|
+
/**
|
|
31
|
+
* JetBrains Mono Variable (weight axis 100-800, latin charset, normal style).
|
|
32
|
+
* SIL OFL-1.1 — credited in NOTICE. ~40 KB raw → ~54 KB base64.
|
|
33
|
+
*
|
|
34
|
+
* Italic intentionally NOT loaded — the data rows (console + network + meta
|
|
35
|
+
* strip + timestamps) never use italics, so the second 43 KB italic woff2
|
|
36
|
+
* would add weight to every report for no design benefit.
|
|
37
|
+
*/
|
|
38
|
+
export declare function loadJetBrainsMonoNormal(): string;
|
|
19
39
|
//# sourceMappingURL=assets.d.ts.map
|
package/dist/assets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAuDA;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AA8BD;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAKhD"}
|