privalint 0.1.1 → 0.3.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/README.de.md +117 -0
- package/README.md +55 -1
- package/dist/assessment.d.ts +6 -0
- package/dist/assessment.js +79 -0
- package/dist/assessment.js.map +1 -0
- package/dist/capture.d.ts +5 -0
- package/dist/capture.js +155 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/consent.d.ts +3 -0
- package/dist/consent.js +13 -6
- package/dist/consent.js.map +1 -1
- package/dist/diagnostics.d.ts +27 -0
- package/dist/diagnostics.js +187 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/i18n.d.ts +5 -0
- package/dist/i18n.js +3 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/legal.d.ts +10 -0
- package/dist/legal.js +99 -0
- package/dist/legal.js.map +1 -0
- package/dist/network.d.ts +67 -0
- package/dist/network.js +235 -0
- package/dist/network.js.map +1 -0
- package/dist/observations.d.ts +3 -0
- package/dist/observations.js +4 -0
- package/dist/observations.js.map +1 -0
- package/dist/reporters/capture.d.ts +5 -0
- package/dist/reporters/capture.js +35 -0
- package/dist/reporters/capture.js.map +1 -0
- package/dist/reporters/markdown.js +21 -12
- package/dist/reporters/markdown.js.map +1 -1
- package/dist/reporters/messages.d.ts +21 -0
- package/dist/reporters/messages.js +68 -0
- package/dist/reporters/messages.js.map +1 -0
- package/dist/reporters/sarif.js +31 -1
- package/dist/reporters/sarif.js.map +1 -1
- package/dist/reporters/shared.d.ts +1 -3
- package/dist/reporters/shared.js +0 -17
- package/dist/reporters/shared.js.map +1 -1
- package/dist/reporters/text.js +23 -15
- package/dist/reporters/text.js.map +1 -1
- package/dist/rules/google-fonts.js +28 -10
- package/dist/rules/google-fonts.js.map +1 -1
- package/dist/rules/impressum-link.js +21 -8
- package/dist/rules/impressum-link.js.map +1 -1
- package/dist/rules/links.d.ts +2 -0
- package/dist/rules/links.js +2 -0
- package/dist/rules/links.js.map +1 -1
- package/dist/rules/pre-consent-trackers.js +20 -7
- package/dist/rules/pre-consent-trackers.js.map +1 -1
- package/dist/rules/privacy-policy-link.js +12 -5
- package/dist/rules/privacy-policy-link.js.map +1 -1
- package/dist/rules/reject-option.js +28 -8
- package/dist/rules/reject-option.js.map +1 -1
- package/dist/rules/third-party-cdns.js +10 -3
- package/dist/rules/third-party-cdns.js.map +1 -1
- package/dist/rules/third-party-embeds.js +10 -3
- package/dist/rules/third-party-embeds.js.map +1 -1
- package/dist/rules/third-party-hits.js +2 -1
- package/dist/rules/third-party-hits.js.map +1 -1
- package/dist/rules/tracker-hits.d.ts +4 -2
- package/dist/rules/tracker-hits.js +16 -4
- package/dist/rules/tracker-hits.js.map +1 -1
- package/dist/rules/trackers-after-reject.js +24 -12
- package/dist/rules/trackers-after-reject.js.map +1 -1
- package/dist/scan-options.d.ts +39 -0
- package/dist/scan-options.js +2 -0
- package/dist/scan-options.js.map +1 -0
- package/dist/scanner.d.ts +4 -32
- package/dist/scanner.js +87 -104
- package/dist/scanner.js.map +1 -1
- package/dist/types.d.ts +70 -0
- package/package.json +11 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracker-hits.js","sourceRoot":"","sources":["../../src/rules/tracker-hits.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"tracker-hits.js","sourceRoot":"","sources":["../../src/rules/tracker-hits.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GAGjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAa,GAAG,EAAa,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,MAAM,YAAY,GAAG,CAAC,CAAC;AAQvB,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAC7B,QAAoC,EACpC,OAAkC,EAClC,IAAU;IAEV,MAAM,YAAY,GAAG,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuD,CAAC;IAC5E,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAE,QAAgB,EAAE,EAAE;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAU,EAAE,CAAC;QACjF,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjD,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,CAAC,OAAO,EAAE,GAAG,YAAY,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,OAAO,GACX,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC;YAChC,gBAAgB,CAAC,WAAW,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QACnE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,CAAC,OAAO,EAAE,UAAU,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QACxD,OAAO;QACP,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC;KAC/C,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,eAAe,GAAkC;IACrD,SAAS,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE;IAC7C,WAAW,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,SAAS,EAAE;IACjD,gBAAgB,EAAE,EAAE,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,gBAAgB,EAAE;IAChE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,cAAc,EAAE;IAC5C,sBAAsB,EAAE,EAAE,EAAE,EAAE,sBAAsB,EAAE,EAAE,EAAE,2BAA2B,EAAE;IACvF,aAAa,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE;CACxD,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAyB,EAAE,IAAU,EAAE,EAAE,CACrE,GAAG,CAAC,IAAI,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { say } from '../i18n.js';
|
|
2
|
+
import { categoryLabel, findTrackerHits } from './tracker-hits.js';
|
|
2
3
|
const ID = 'trackers-after-reject';
|
|
3
4
|
export const trackersAfterReject = {
|
|
4
5
|
id: ID,
|
|
@@ -7,21 +8,32 @@ export const trackersAfterReject = {
|
|
|
7
8
|
'page and records what still loads. Anything on the tracker list that loads or sets new ' +
|
|
8
9
|
'cookies after that ignores the visitor’s explicit refusal.',
|
|
9
10
|
references: ['§ 25 Abs. 1 TDDDG', 'Art. 6(1)(a) GDPR', 'Art. 7(3) GDPR'],
|
|
10
|
-
check({ page }) {
|
|
11
|
+
check({ page, lang }) {
|
|
11
12
|
const after = page.consent?.afterReject;
|
|
12
13
|
if (after === undefined) {
|
|
13
14
|
return [];
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
const button = page.consent?.rejectLabel ?? say(lang, { en: 'reject', de: 'Ablehnen' });
|
|
17
|
+
return findTrackerHits(after.requests, after.cookies, lang).map(({ tracker, evidence }) => {
|
|
18
|
+
const { vendor } = tracker;
|
|
19
|
+
const category = categoryLabel(tracker.category, lang);
|
|
20
|
+
return {
|
|
21
|
+
ruleId: ID,
|
|
22
|
+
// Same reasoning as pre-consent-trackers: the container alone may be configured correctly.
|
|
23
|
+
severity: tracker.category === 'tag-manager' ? 'warning' : 'error',
|
|
24
|
+
message: say(lang, {
|
|
25
|
+
en: `${vendor} (${category}) still runs after clicking "${button}".`,
|
|
26
|
+
de: `${vendor} (${category}) läuft weiter, obwohl „${button}“ geklickt wurde.`,
|
|
27
|
+
}),
|
|
28
|
+
pageUrl: page.finalUrl,
|
|
29
|
+
subject: vendor,
|
|
30
|
+
evidence,
|
|
31
|
+
fix: say(lang, {
|
|
32
|
+
en: `Check that ${vendor} is assigned to a consent category in your consent manager and that the category is blocked when the visitor rejects.`,
|
|
33
|
+
de: `Prüfen, ob ${vendor} im Consent-Tool einer Kategorie zugeordnet ist und diese Kategorie beim Ablehnen blockiert wird.`,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
25
37
|
},
|
|
26
38
|
};
|
|
27
39
|
//# sourceMappingURL=trackers-after-reject.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trackers-after-reject.js","sourceRoot":"","sources":["../../src/rules/trackers-after-reject.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trackers-after-reject.js","sourceRoot":"","sources":["../../src/rules/trackers-after-reject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnE,MAAM,EAAE,GAAG,uBAAuB,CAAC;AAEnC,MAAM,CAAC,MAAM,mBAAmB,GAAS;IACvC,EAAE,EAAE,EAAE;IACN,KAAK,EAAE,8CAA8C;IACrD,WAAW,EACT,0FAA0F;QAC1F,yFAAyF;QACzF,4DAA4D;IAC9D,UAAU,EAAE,CAAC,mBAAmB,EAAE,mBAAmB,EAAE,gBAAgB,CAAC;IACxE,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,WAAW,IAAI,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QACxF,OAAO,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,GAAG,CAC7D,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAW,EAAE;YACjC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;YAC3B,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACvD,OAAO;gBACL,MAAM,EAAE,EAAE;gBACV,2FAA2F;gBAC3F,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO;gBAClE,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE;oBACjB,EAAE,EAAE,GAAG,MAAM,KAAK,QAAQ,gCAAgC,MAAM,IAAI;oBACpE,EAAE,EAAE,GAAG,MAAM,KAAK,QAAQ,2BAA2B,MAAM,mBAAmB;iBAC/E,CAAC;gBACF,OAAO,EAAE,IAAI,CAAC,QAAQ;gBACtB,OAAO,EAAE,MAAM;gBACf,QAAQ;gBACR,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE;oBACb,EAAE,EAAE,cAAc,MAAM,uHAAuH;oBAC/I,EAAE,EAAE,cAAc,MAAM,mGAAmG;iBAC5H,CAAC;aACH,CAAC;QACJ,CAAC,CACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Lang } from './i18n.js';
|
|
2
|
+
import type { Region } from './legal.js';
|
|
3
|
+
import type { NetworkAdapter, NetworkLimits } from './network.js';
|
|
4
|
+
import type { PageSnapshot, Rule } from './types.js';
|
|
5
|
+
export interface ScanOptions {
|
|
6
|
+
maxPages?: number | undefined;
|
|
7
|
+
settleMs?: number | undefined;
|
|
8
|
+
/** Navigation/network-idle timeout, distinct from transport and capture deadlines. */
|
|
9
|
+
timeoutMs?: number | undefined;
|
|
10
|
+
locale?: string | undefined;
|
|
11
|
+
lang?: Lang | undefined;
|
|
12
|
+
/** Tool classification profile; does not determine which law applies. */
|
|
13
|
+
region?: Region | undefined;
|
|
14
|
+
rules?: readonly Rule[];
|
|
15
|
+
/** Legacy test helper: third-party requests receive synthetic empty responses. */
|
|
16
|
+
offline?: boolean;
|
|
17
|
+
consentCheck?: boolean | undefined;
|
|
18
|
+
headed?: boolean | undefined;
|
|
19
|
+
channel?: string | undefined;
|
|
20
|
+
/** Trusted hook receiving RAW observations. Do not log/expose without redaction. */
|
|
21
|
+
onPage?: (snapshot: PageSnapshot) => void;
|
|
22
|
+
/** Caller-owned transport; scan owns browser, fresh per-page contexts and request signals. */
|
|
23
|
+
networkAdapter?: NetworkAdapter;
|
|
24
|
+
/** Per-page budgets across initial loading and rejection. Requires networkAdapter. */
|
|
25
|
+
networkLimits?: NetworkLimits;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
/** Entire capture including queue wait and consent; default 60s with an adapter. */
|
|
28
|
+
captureTimeoutMs?: number;
|
|
29
|
+
/** Stored issue details per page; counters are never truncated. Default 100. */
|
|
30
|
+
maxDiagnostics?: number;
|
|
31
|
+
/** Legacy default: throw on start-page navigation failure. Adapter default: report it. */
|
|
32
|
+
failureMode?: 'throw' | 'report';
|
|
33
|
+
}
|
|
34
|
+
export declare const DEFAULTS: {
|
|
35
|
+
maxPages: number;
|
|
36
|
+
settleMs: number;
|
|
37
|
+
timeoutMs: number;
|
|
38
|
+
locale: string;
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan-options.js","sourceRoot":"","sources":["../src/scan-options.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/scanner.d.ts
CHANGED
|
@@ -1,33 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/** Extra time to wait after the network is idle, to catch delayed tags. */
|
|
6
|
-
settleMs?: number | undefined;
|
|
7
|
-
/** Navigation timeout per page. */
|
|
8
|
-
timeoutMs?: number | undefined;
|
|
9
|
-
locale?: string | undefined;
|
|
10
|
-
rules?: readonly Rule[];
|
|
11
|
-
/**
|
|
12
|
-
* Answer every third-party request with an empty response instead of fetching it.
|
|
13
|
-
* Requests are still recorded. Used by the test suite to stay off the network.
|
|
14
|
-
*/
|
|
15
|
-
offline?: boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Click "reject" on the start page's consent banner and record what still loads.
|
|
18
|
-
* On by default.
|
|
19
|
-
*/
|
|
20
|
-
consentCheck?: boolean | undefined;
|
|
21
|
-
/**
|
|
22
|
-
* Open a visible browser window instead of running headless. Some sites only show their
|
|
23
|
-
* consent banner to regular browsers. Needs a display, so it doesn't work on CI runners.
|
|
24
|
-
*/
|
|
25
|
-
headed?: boolean | undefined;
|
|
26
|
-
/**
|
|
27
|
-
* Use an installed browser instead of Playwright's bundled Chromium,
|
|
28
|
-
* e.g. `chrome` or `msedge`.
|
|
29
|
-
*/
|
|
30
|
-
channel?: string | undefined;
|
|
31
|
-
onPage?: (snapshot: PageSnapshot) => void;
|
|
32
|
-
}
|
|
1
|
+
import { type ScanOptions } from './scan-options.js';
|
|
2
|
+
import type { ScanResult } from './types.js';
|
|
3
|
+
export type { ScanOptions } from './scan-options.js';
|
|
4
|
+
export declare function throwIfAborted(signal?: AbortSignal): void;
|
|
33
5
|
export declare function scan(target: string, options?: ScanOptions): Promise<ScanResult>;
|
package/dist/scanner.js
CHANGED
|
@@ -1,39 +1,67 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
|
-
import {
|
|
2
|
+
import { assessPage } from './assessment.js';
|
|
3
|
+
import { capturePage } from './capture.js';
|
|
4
|
+
import { diagnosticUrl } from './diagnostics.js';
|
|
5
|
+
import { applyRegion } from './legal.js';
|
|
6
|
+
import { networkLimits } from './network.js';
|
|
3
7
|
import { defaultRules } from './rules/index.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
8
|
+
import { DEFAULTS } from './scan-options.js';
|
|
9
|
+
import { crawlTarget, withoutHash } from './url.js';
|
|
10
|
+
function integer(value, name, minimum) {
|
|
11
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
12
|
+
throw new RangeError(`${name} must be a safe integer of at least ${minimum}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function throwIfAborted(signal) {
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
throw new DOMException('Scan aborted', 'AbortError');
|
|
18
|
+
}
|
|
11
19
|
export async function scan(target, options = {}) {
|
|
20
|
+
throwIfAborted(options.signal);
|
|
12
21
|
const startedAt = Date.now();
|
|
13
22
|
const rules = options.rules ?? defaultRules;
|
|
14
|
-
const maxPages =
|
|
23
|
+
const maxPages = options.maxPages ?? DEFAULTS.maxPages;
|
|
24
|
+
integer(maxPages, 'maxPages', 1);
|
|
25
|
+
integer(options.settleMs ?? DEFAULTS.settleMs, 'settleMs', 0);
|
|
26
|
+
integer(options.timeoutMs ?? DEFAULTS.timeoutMs, 'timeoutMs', 1);
|
|
27
|
+
integer(options.maxDiagnostics ?? 100, 'maxDiagnostics', 0);
|
|
28
|
+
if (options.captureTimeoutMs !== undefined)
|
|
29
|
+
integer(options.captureTimeoutMs, 'captureTimeoutMs', 1);
|
|
30
|
+
if (options.networkLimits && !options.networkAdapter) {
|
|
31
|
+
throw new TypeError('networkLimits requires networkAdapter');
|
|
32
|
+
}
|
|
33
|
+
if (options.networkAdapter && options.offline) {
|
|
34
|
+
throw new TypeError('offline cannot be combined with networkAdapter');
|
|
35
|
+
}
|
|
36
|
+
if (options.networkAdapter)
|
|
37
|
+
networkLimits(options.networkLimits);
|
|
38
|
+
const lang = options.lang ?? 'en';
|
|
39
|
+
const region = options.region ?? 'de';
|
|
15
40
|
let siteUrl = new URL(target);
|
|
41
|
+
if (!/^https?:$/.test(siteUrl.protocol))
|
|
42
|
+
throw new TypeError('Expected an HTTP(S) target');
|
|
16
43
|
const browser = await chromium.launch({
|
|
17
44
|
headless: options.headed !== true,
|
|
18
45
|
...(options.channel === undefined ? {} : { channel: options.channel }),
|
|
19
46
|
});
|
|
20
47
|
try {
|
|
48
|
+
throwIfAborted(options.signal);
|
|
21
49
|
const snapshots = [];
|
|
22
50
|
const queue = [siteUrl.href];
|
|
23
51
|
const seen = new Set(queue);
|
|
24
52
|
const captured = new Set();
|
|
25
53
|
let url = queue.shift();
|
|
26
54
|
while (url !== undefined && snapshots.length < maxPages) {
|
|
55
|
+
throwIfAborted(options.signal);
|
|
27
56
|
const isStart = snapshots.length === 0;
|
|
28
57
|
const snapshot = await capturePage(browser, url, siteUrl, options, isStart);
|
|
29
58
|
if (isStart) {
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
const failureMode = options.failureMode ?? (options.networkAdapter ? 'report' : 'throw');
|
|
60
|
+
if (snapshot.error !== undefined && failureMode === 'throw') {
|
|
61
|
+
throw new Error(`Could not load ${diagnosticUrl(url)}: ${snapshot.error}`);
|
|
32
62
|
}
|
|
33
|
-
// Follow redirects like http -> https or apex -> www for the rest of the crawl.
|
|
34
63
|
siteUrl = new URL(snapshot.finalUrl);
|
|
35
64
|
}
|
|
36
|
-
// Several links can redirect to the same page; only lint it once.
|
|
37
65
|
const finalUrl = withoutHash(snapshot.finalUrl);
|
|
38
66
|
if (!captured.has(finalUrl)) {
|
|
39
67
|
captured.add(finalUrl);
|
|
@@ -50,118 +78,73 @@ export async function scan(target, options = {}) {
|
|
|
50
78
|
}
|
|
51
79
|
url = queue.shift();
|
|
52
80
|
}
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
isFirstParty(page.finalUrl, siteUrl))
|
|
58
|
-
.flatMap((page) => rules.flatMap((rule) => rule.check({ siteUrl, page })));
|
|
59
|
-
return {
|
|
81
|
+
throwIfAborted(options.signal);
|
|
82
|
+
const assessments = snapshots.map((page, index) => assessPage(page, rules, siteUrl, lang, index === 0, options.consentCheck !== false));
|
|
83
|
+
const partial = assessments.some((assessment) => assessment.evaluations.some((evaluation) => evaluation.partial || evaluation.reason === 'consent-disabled')) || snapshots.some((page) => !page.capture?.complete);
|
|
84
|
+
const result = {
|
|
60
85
|
siteUrl: siteUrl.href,
|
|
86
|
+
lang,
|
|
87
|
+
region,
|
|
61
88
|
scannedAt: new Date(startedAt).toISOString(),
|
|
62
89
|
durationMs: Date.now() - startedAt,
|
|
63
|
-
pages: snapshots.map((page) => ({
|
|
90
|
+
pages: snapshots.map((page, index) => ({
|
|
64
91
|
url: page.url,
|
|
65
92
|
finalUrl: page.finalUrl,
|
|
66
93
|
status: page.status,
|
|
67
|
-
requestCount: page.requests.length,
|
|
94
|
+
requestCount: page.requestCount ?? page.requests.length,
|
|
68
95
|
cookieCount: page.cookies.length,
|
|
69
96
|
...(page.error === undefined ? {} : { error: page.error }),
|
|
97
|
+
...(page.capture === undefined ? {} : { capture: page.capture }),
|
|
98
|
+
ruleEvaluations: assessments[index]?.evaluations ?? [],
|
|
70
99
|
})),
|
|
71
100
|
...consentSummary(snapshots[0]),
|
|
72
|
-
findings,
|
|
101
|
+
findings: applyRegion(assessments.flatMap((assessment) => assessment.findings), region),
|
|
73
102
|
rules: rules.map(({ check: _check, ...meta }) => meta),
|
|
103
|
+
coverage: partial ? 'partial' : 'complete',
|
|
74
104
|
};
|
|
105
|
+
return options.networkAdapter ? redactReport(result) : result;
|
|
75
106
|
}
|
|
76
107
|
finally {
|
|
77
108
|
await browser.close();
|
|
78
109
|
}
|
|
79
110
|
}
|
|
80
111
|
function consentSummary(start) {
|
|
81
|
-
if (start?.consent === undefined)
|
|
112
|
+
if (start?.consent === undefined)
|
|
82
113
|
return {};
|
|
83
|
-
}
|
|
84
114
|
const { afterReject: _afterReject, ...summary } = start.consent;
|
|
85
115
|
return { consent: summary };
|
|
86
116
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.slice(0, 200),
|
|
120
|
-
})));
|
|
121
|
-
const resourceHints = await page.$$eval('link[rel][href]', (elements) => elements.map((link) => ({
|
|
122
|
-
rel: (link.getAttribute('rel') ?? '').toLowerCase(),
|
|
123
|
-
href: link.href,
|
|
124
|
-
})));
|
|
125
|
-
const cookies = (await context.cookies()).map(({ name, domain, path, expires }) => ({
|
|
126
|
-
name,
|
|
127
|
-
domain,
|
|
128
|
-
path,
|
|
129
|
-
expires,
|
|
130
|
-
}));
|
|
131
|
-
const snapshot = {
|
|
132
|
-
url,
|
|
133
|
-
finalUrl: page.url(),
|
|
134
|
-
status: response?.status() ?? null,
|
|
135
|
-
// Copy: the listener keeps writing to `requests` during the consent check.
|
|
136
|
-
requests: [...requests],
|
|
137
|
-
cookies,
|
|
138
|
-
links,
|
|
139
|
-
resourceHints,
|
|
140
|
-
};
|
|
141
|
-
if (checkConsent && options.consentCheck !== false) {
|
|
142
|
-
snapshot.consent = await observeConsent(page, context, requests, {
|
|
143
|
-
settleMs: options.settleMs ?? DEFAULTS.settleMs,
|
|
144
|
-
timeoutMs: options.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
return snapshot;
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
// Downloads, timeouts and crashed pages should not end the whole crawl.
|
|
151
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
-
return {
|
|
153
|
-
url,
|
|
154
|
-
finalUrl: url,
|
|
155
|
-
status: null,
|
|
156
|
-
requests,
|
|
157
|
-
cookies: [],
|
|
158
|
-
links: [],
|
|
159
|
-
resourceHints: [],
|
|
160
|
-
error: message.split('\n')[0] ?? message,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
finally {
|
|
164
|
-
await context.close();
|
|
165
|
-
}
|
|
117
|
+
/** Adapter reports omit URL credentials, paths, queries and fragments by default. */
|
|
118
|
+
function redactReport(result) {
|
|
119
|
+
const redactText = (text) => text.replace(/https?:\/\/[^\s<>"']+/gi, diagnosticUrl);
|
|
120
|
+
return {
|
|
121
|
+
...result,
|
|
122
|
+
siteUrl: diagnosticUrl(result.siteUrl),
|
|
123
|
+
...(result.consent === undefined
|
|
124
|
+
? {}
|
|
125
|
+
: {
|
|
126
|
+
consent: {
|
|
127
|
+
...result.consent,
|
|
128
|
+
...(result.consent.acceptLabel === undefined
|
|
129
|
+
? {}
|
|
130
|
+
: { acceptLabel: redactText(result.consent.acceptLabel) }),
|
|
131
|
+
...(result.consent.rejectLabel === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: { rejectLabel: redactText(result.consent.rejectLabel) }),
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
pages: result.pages.map((page) => ({
|
|
137
|
+
...page,
|
|
138
|
+
url: diagnosticUrl(page.url),
|
|
139
|
+
finalUrl: diagnosticUrl(page.finalUrl),
|
|
140
|
+
})),
|
|
141
|
+
findings: result.findings.map((finding) => ({
|
|
142
|
+
...finding,
|
|
143
|
+
pageUrl: diagnosticUrl(finding.pageUrl),
|
|
144
|
+
message: redactText(finding.message),
|
|
145
|
+
evidence: finding.evidence.map(redactText),
|
|
146
|
+
...(finding.fix === undefined ? {} : { fix: redactText(finding.fix) }),
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
166
149
|
}
|
|
167
150
|
//# sourceMappingURL=scanner.js.map
|
package/dist/scanner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,59 @@
|
|
|
1
|
+
import type { Lang } from './i18n.js';
|
|
2
|
+
import type { Region } from './legal.js';
|
|
1
3
|
export type Severity = 'error' | 'warning' | 'info';
|
|
2
4
|
export interface RecordedRequest {
|
|
3
5
|
url: string;
|
|
4
6
|
resourceType: string;
|
|
7
|
+
/** Absent on legacy snapshots. Only received responses count as network evidence. */
|
|
8
|
+
outcome?: 'pending' | 'response' | 'failed';
|
|
9
|
+
status?: number;
|
|
10
|
+
}
|
|
11
|
+
export type LoadIssueKind = 'security-blocked' | 'unsupported-method' | 'unsupported-protocol' | 'dns' | 'tls' | 'connection' | 'timeout' | 'resource-limit' | 'http-error' | 'browser-failure' | 'aborted';
|
|
12
|
+
export type CapturePhase = 'before-consent' | 'after-reject';
|
|
13
|
+
export interface LoadDiagnostic {
|
|
14
|
+
kind: LoadIssueKind;
|
|
15
|
+
phase: CapturePhase;
|
|
16
|
+
/** Origin only: user info, path, query and fragment are never retained. */
|
|
17
|
+
url: string;
|
|
18
|
+
status?: number;
|
|
19
|
+
limit?: 'requests' | 'response-bytes' | 'total-bytes' | 'redirects';
|
|
20
|
+
}
|
|
21
|
+
export interface LoadDiagnostics {
|
|
22
|
+
/** HTTP(S) browser requests, including documents and every redirect hop, across both phases. */
|
|
23
|
+
requests: number;
|
|
24
|
+
/** Requests counted but omitted from bounded in-memory rule evidence. */
|
|
25
|
+
omittedRequests: number;
|
|
26
|
+
/** Received HTTP responses, including redirects and HTTP errors; may overlap failed. */
|
|
27
|
+
responses: number;
|
|
28
|
+
/** Browser requestfinished events (including HTTP errors). */
|
|
29
|
+
finished: number;
|
|
30
|
+
/** Failed or unfinished requests at capture end; disjoint from finished. */
|
|
31
|
+
failed: number;
|
|
32
|
+
/** Subset of failed: explicitly denied by the adapter's security policy. */
|
|
33
|
+
blocked: number;
|
|
34
|
+
/** Subset of responses: status >= 400. */
|
|
35
|
+
httpErrors: number;
|
|
36
|
+
/** Adapter response-body bytes consumed, including the chunk exceeding a limit. */
|
|
37
|
+
bytes: number;
|
|
38
|
+
/** All issues, even when their details exceed the storage cap. */
|
|
39
|
+
issueCount: number;
|
|
40
|
+
dropped: number;
|
|
41
|
+
issues: LoadDiagnostic[];
|
|
42
|
+
}
|
|
43
|
+
export type ObservationKind = 'document' | 'network' | 'consent' | 'after-reject';
|
|
44
|
+
export interface RuleEvaluation {
|
|
45
|
+
ruleId: string;
|
|
46
|
+
status: 'executed' | 'skipped' | 'not-evaluable';
|
|
47
|
+
reason?: 'disabled' | 'consent-disabled' | 'not-start-page' | 'missing-observation';
|
|
48
|
+
/** Executed against available positive evidence, without ruling out additional findings. */
|
|
49
|
+
partial: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface CaptureObservation {
|
|
52
|
+
complete: boolean;
|
|
53
|
+
/** Loading coverage before the consent interaction, for pre-consent rule evaluation. */
|
|
54
|
+
beforeConsentComplete?: boolean;
|
|
55
|
+
document: boolean;
|
|
56
|
+
diagnostics: LoadDiagnostics;
|
|
5
57
|
}
|
|
6
58
|
export interface RecordedCookie {
|
|
7
59
|
name: string;
|
|
@@ -21,6 +73,7 @@ export interface ResourceHint {
|
|
|
21
73
|
}
|
|
22
74
|
/** What happened when privalint looked for a consent banner and clicked "reject". */
|
|
23
75
|
export interface ConsentObservation {
|
|
76
|
+
state?: 'not-detected' | 'reject-unavailable' | 'interaction-failed' | 'reject-tested';
|
|
24
77
|
bannerFound: boolean;
|
|
25
78
|
/** Name of the consent manager, if it was recognised. */
|
|
26
79
|
cmp?: string;
|
|
@@ -35,6 +88,8 @@ export interface ConsentObservation {
|
|
|
35
88
|
requests: RecordedRequest[];
|
|
36
89
|
cookies: RecordedCookie[];
|
|
37
90
|
};
|
|
91
|
+
/** True only if the reject observation window completed without load issues. */
|
|
92
|
+
afterRejectComplete?: boolean;
|
|
38
93
|
error?: string;
|
|
39
94
|
}
|
|
40
95
|
/**
|
|
@@ -46,6 +101,8 @@ export interface PageSnapshot {
|
|
|
46
101
|
finalUrl: string;
|
|
47
102
|
status: number | null;
|
|
48
103
|
requests: RecordedRequest[];
|
|
104
|
+
/** Initial-phase request count, including records beyond the retained evidence cap. */
|
|
105
|
+
requestCount?: number;
|
|
49
106
|
cookies: RecordedCookie[];
|
|
50
107
|
links: PageLink[];
|
|
51
108
|
resourceHints: ResourceHint[];
|
|
@@ -53,6 +110,7 @@ export interface PageSnapshot {
|
|
|
53
110
|
consent?: ConsentObservation;
|
|
54
111
|
/** Set when the page could not be loaded, e.g. a download or a timeout. */
|
|
55
112
|
error?: string;
|
|
113
|
+
capture?: CaptureObservation;
|
|
56
114
|
}
|
|
57
115
|
export interface Finding {
|
|
58
116
|
ruleId: string;
|
|
@@ -63,10 +121,14 @@ export interface Finding {
|
|
|
63
121
|
fix?: string;
|
|
64
122
|
/** What the finding is about, e.g. a vendor. Keeps fingerprints stable across runs. */
|
|
65
123
|
subject?: string;
|
|
124
|
+
/** Legal basis in the country the scan was run for. */
|
|
125
|
+
references?: string[];
|
|
66
126
|
}
|
|
67
127
|
export interface RuleContext {
|
|
68
128
|
siteUrl: URL;
|
|
69
129
|
page: PageSnapshot;
|
|
130
|
+
/** Language for messages and fixes. */
|
|
131
|
+
lang: Lang;
|
|
70
132
|
}
|
|
71
133
|
export interface Rule {
|
|
72
134
|
id: string;
|
|
@@ -74,6 +136,8 @@ export interface Rule {
|
|
|
74
136
|
description: string;
|
|
75
137
|
/** Laws, rulings or guidelines the rule is based on. */
|
|
76
138
|
references: string[];
|
|
139
|
+
/** Observations needed by custom rules. Missing means a complete document is required. */
|
|
140
|
+
requires?: readonly ObservationKind[];
|
|
77
141
|
check(context: RuleContext): Finding[];
|
|
78
142
|
}
|
|
79
143
|
export interface PageSummary {
|
|
@@ -83,10 +147,14 @@ export interface PageSummary {
|
|
|
83
147
|
requestCount: number;
|
|
84
148
|
cookieCount: number;
|
|
85
149
|
error?: string;
|
|
150
|
+
capture?: CaptureObservation;
|
|
151
|
+
ruleEvaluations?: RuleEvaluation[];
|
|
86
152
|
}
|
|
87
153
|
export type ConsentSummary = Omit<ConsentObservation, 'afterReject'>;
|
|
88
154
|
export interface ScanResult {
|
|
89
155
|
siteUrl: string;
|
|
156
|
+
lang: Lang;
|
|
157
|
+
region: Region;
|
|
90
158
|
scannedAt: string;
|
|
91
159
|
durationMs: number;
|
|
92
160
|
pages: PageSummary[];
|
|
@@ -94,4 +162,6 @@ export interface ScanResult {
|
|
|
94
162
|
consent?: ConsentSummary;
|
|
95
163
|
findings: Finding[];
|
|
96
164
|
rules: Array<Omit<Rule, 'check'>>;
|
|
165
|
+
/** Observation coverage, not a legal compliance judgement. */
|
|
166
|
+
coverage?: 'complete' | 'partial';
|
|
97
167
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privalint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Lint your website for GDPR problems: trackers before consent and after reject, Google Fonts, embeds, missing Impressum. Real browser, CLI and GitHub Action.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"README.de.md"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
21
22
|
"node": ">=20.12"
|
|
@@ -41,7 +42,11 @@
|
|
|
41
42
|
"impressum",
|
|
42
43
|
"lint",
|
|
43
44
|
"playwright",
|
|
44
|
-
"github-action"
|
|
45
|
+
"github-action",
|
|
46
|
+
"tdddg",
|
|
47
|
+
"dsg",
|
|
48
|
+
"oesterreich",
|
|
49
|
+
"schweiz"
|
|
45
50
|
],
|
|
46
51
|
"license": "MIT",
|
|
47
52
|
"dependencies": {
|
|
@@ -55,8 +60,8 @@
|
|
|
55
60
|
},
|
|
56
61
|
"repository": {
|
|
57
62
|
"type": "git",
|
|
58
|
-
"url": "git+https://github.com/
|
|
63
|
+
"url": "git+https://github.com/JExcellence/privalint.git"
|
|
59
64
|
},
|
|
60
|
-
"homepage": "https://github.com/
|
|
61
|
-
"bugs": "https://github.com/
|
|
65
|
+
"homepage": "https://github.com/JExcellence/privalint#readme",
|
|
66
|
+
"bugs": "https://github.com/JExcellence/privalint/issues"
|
|
62
67
|
}
|