@slashgear/gdpr-cookie-scanner 2.0.4 → 3.1.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/.github/workflows/update-trackers.yml +95 -0
- package/CHANGELOG.md +102 -0
- package/CLAUDE.md +1 -1
- package/README.md +78 -5
- package/dist/analyzers/compliance.d.ts.map +1 -1
- package/dist/analyzers/compliance.js +52 -9
- package/dist/analyzers/compliance.js.map +1 -1
- package/dist/classifiers/cookie-classifier.d.ts.map +1 -1
- package/dist/classifiers/cookie-classifier.js +2 -1
- package/dist/classifiers/cookie-classifier.js.map +1 -1
- package/dist/classifiers/network-classifier.d.ts +1 -0
- package/dist/classifiers/network-classifier.d.ts.map +1 -1
- package/dist/classifiers/network-classifier.js +10 -1
- package/dist/classifiers/network-classifier.js.map +1 -1
- package/dist/classifiers/tracker-list.d.ts +1 -0
- package/dist/classifiers/tracker-list.d.ts.map +1 -1
- package/dist/classifiers/tracker-list.js +7 -1
- package/dist/classifiers/tracker-list.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/report/generator.d.ts.map +1 -1
- package/dist/report/generator.js +82 -41
- package/dist/report/generator.js.map +1 -1
- package/dist/scanner/index.js +4 -4
- package/dist/scanner/index.js.map +1 -1
- package/dist/scanner/network.d.ts.map +1 -1
- package/dist/scanner/network.js +1 -0
- package/dist/scanner/network.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/scripts/update-trackers.ts +273 -0
- package/src/analyzers/compliance.ts +54 -11
- package/src/classifiers/cookie-classifier.ts +2 -1
- package/src/classifiers/network-classifier.ts +11 -1
- package/src/classifiers/tracker-list.ts +9 -1
- package/src/cli.ts +1 -5
- package/src/index.ts +87 -0
- package/src/report/generator.ts +83 -44
- package/src/scanner/index.ts +4 -4
- package/src/scanner/network.ts +1 -0
- package/src/types.ts +2 -1
- package/tests/analyzers/compliance.test.ts +489 -0
- package/tests/analyzers/wording.test.ts +160 -0
- package/tests/classifiers/cookie-classifier.test.ts +270 -0
- package/tests/classifiers/network-classifier.test.ts +140 -0
- package/tests/e2e/scanner.test.ts +4 -7
- package/tests/unit/compliance.test.ts +99 -13
- package/tests/unit/network-classifier.test.ts +27 -0
package/dist/scanner/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export class Scanner {
|
|
|
14
14
|
const startTime = Date.now();
|
|
15
15
|
const screenshotPaths = [];
|
|
16
16
|
const errors = [];
|
|
17
|
-
if (this.options.screenshots) {
|
|
17
|
+
if (this.options.screenshots && this.options.outputDir) {
|
|
18
18
|
await mkdir(this.options.outputDir, { recursive: true });
|
|
19
19
|
}
|
|
20
20
|
// ────────────────────────────────────────────────────────────
|
|
@@ -44,7 +44,7 @@ export class Scanner {
|
|
|
44
44
|
// ────────────────────────────────────────────────────────────
|
|
45
45
|
onPhase("Phase 2/4 — Analyzing consent modal...");
|
|
46
46
|
const modal = await detectConsentModal(session1.page, this.options);
|
|
47
|
-
if (this.options.screenshots && modal.detected) {
|
|
47
|
+
if (this.options.screenshots && this.options.outputDir && modal.detected) {
|
|
48
48
|
const screenshotPath = join(this.options.outputDir, "modal-initial.png");
|
|
49
49
|
await session1.page.screenshot({ path: screenshotPath, fullPage: false });
|
|
50
50
|
screenshotPaths.push(screenshotPath);
|
|
@@ -73,7 +73,7 @@ export class Scanner {
|
|
|
73
73
|
errors.push("No reject button found — could not test rejection flow");
|
|
74
74
|
}
|
|
75
75
|
interceptor3.stop();
|
|
76
|
-
if (this.options.screenshots) {
|
|
76
|
+
if (this.options.screenshots && this.options.outputDir) {
|
|
77
77
|
const screenshotPath = join(this.options.outputDir, "after-reject.png");
|
|
78
78
|
await session1.page.screenshot({ path: screenshotPath, fullPage: false });
|
|
79
79
|
screenshotPaths.push(screenshotPath);
|
|
@@ -116,7 +116,7 @@ export class Scanner {
|
|
|
116
116
|
errors.push(`Accept phase error: ${String(err)}`);
|
|
117
117
|
}
|
|
118
118
|
interceptor4.stop();
|
|
119
|
-
if (this.options.screenshots) {
|
|
119
|
+
if (this.options.screenshots && this.options.outputDir) {
|
|
120
120
|
const screenshotPath = join(this.options.outputDir, "after-accept.png");
|
|
121
121
|
await session2.page.screenshot({ path: screenshotPath, fullPage: false });
|
|
122
122
|
screenshotPaths.push(screenshotPath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAI/D,MAAM,OAAO,OAAO;IACW;IAA7B,YAA6B,OAAoB;QAApB,YAAO,GAAP,OAAO,CAAa;IAAG,CAAC;IAErD,KAAK,CAAC,GAAG,CAAC,UAAyB,GAAG,EAAE,GAAE,CAAC;QACzC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,eAAe,GAAa,EAAE,CAAC;QACrC,MAAM,MAAM,GAAa,EAAE,CAAC;QAE5B,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/scanner/network.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAqB,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,MAAM,YAAY,GAAG,oBAAoB,GAAG,cAAc,GAAG,cAAc,CAAC;AAElF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY;;;
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/scanner/network.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAqB,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,MAAM,YAAY,GAAG,oBAAoB,GAAG,cAAc,GAAG,cAAc,CAAC;AAElF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY;;;EA6CvE"}
|
package/dist/scanner/network.js
CHANGED
|
@@ -23,6 +23,7 @@ export function createNetworkInterceptor(page, phase) {
|
|
|
23
23
|
isThirdParty: classification.isThirdParty,
|
|
24
24
|
trackerCategory: classification.trackerCategory,
|
|
25
25
|
trackerName: classification.trackerName,
|
|
26
|
+
requiresConsent: classification.requiresConsent,
|
|
26
27
|
capturedAt: phase,
|
|
27
28
|
responseStatus: meta?.status ?? null,
|
|
28
29
|
contentType: meta?.contentType ?? null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/scanner/network.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAI9E,MAAM,UAAU,wBAAwB,CAAC,IAAU,EAAE,KAAmB;IACtE,MAAM,QAAQ,GAAqB,EAAE,CAAC;IACtC,MAAM,WAAW,GAAG,IAAI,GAAG,EAA0D,CAAC;IAEtF,MAAM,UAAU,GAAG,CAAC,QAAkB,EAAE,EAAE;QACxC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE;YACzB,WAAW,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,IAAI,IAAI;SACxD,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,CAAC,OAAgB,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;QAE1B,8CAA8C;QAC9C,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,mBAAmB,CAAC;YAAE,OAAO;QAE3E,MAAM,cAAc,GAAG,sBAAsB,CAAC,GAAG,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;QAE1C,QAAQ,CAAC,IAAI,CAAC;YACZ,GAAG;YACH,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE;YACxB,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE;YACpC,SAAS,EAAE,IAAI;YACf,YAAY,EAAE,cAAc,CAAC,YAAY;YACzC,eAAe,EAAE,cAAc,CAAC,eAAe;YAC/C,WAAW,EAAE,cAAc,CAAC,WAAW;YACvC,UAAU,EAAE,KAAK;YACjB,cAAc,EAAE,IAAI,EAAE,MAAM,IAAI,IAAI;YACpC,WAAW,EAAE,IAAI,EAAE,WAAW,IAAI,IAAI;SACvC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAE9B,OAAO;QACL,IAAI,EAAE,GAAG,EAAE;YACT,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACjC,CAAC;QACD,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC;KACjC,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/scanner/network.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAI9E,MAAM,UAAU,wBAAwB,CAAC,IAAU,EAAE,KAAmB;IACtE,MAAM,QAAQ,GAAqB,EAAE,CAAC;IACtC,MAAM,WAAW,GAAG,IAAI,GAAG,EAA0D,CAAC;IAEtF,MAAM,UAAU,GAAG,CAAC,QAAkB,EAAE,EAAE;QACxC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE;YACzB,WAAW,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,IAAI,IAAI;SACxD,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,CAAC,OAAgB,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;QAE1B,8CAA8C;QAC9C,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,mBAAmB,CAAC;YAAE,OAAO;QAE3E,MAAM,cAAc,GAAG,sBAAsB,CAAC,GAAG,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;QAE1C,QAAQ,CAAC,IAAI,CAAC;YACZ,GAAG;YACH,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE;YACxB,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE;YACpC,SAAS,EAAE,IAAI;YACf,YAAY,EAAE,cAAc,CAAC,YAAY;YACzC,eAAe,EAAE,cAAc,CAAC,eAAe;YAC/C,WAAW,EAAE,cAAc,CAAC,WAAW;YACvC,eAAe,EAAE,cAAc,CAAC,eAAe;YAC/C,UAAU,EAAE,KAAK;YACjB,cAAc,EAAE,IAAI,EAAE,MAAM,IAAI,IAAI;YACpC,WAAW,EAAE,IAAI,EAAE,WAAW,IAAI,IAAI;SACvC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAE9B,OAAO;QACL,IAAI,EAAE,GAAG,EAAE;YACT,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACjC,CAAC;QACD,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC;KACjC,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface NetworkRequest {
|
|
|
21
21
|
isThirdParty: boolean;
|
|
22
22
|
trackerCategory: TrackerCategory | null;
|
|
23
23
|
trackerName: string | null;
|
|
24
|
+
requiresConsent: boolean;
|
|
24
25
|
capturedAt: "before-interaction" | "after-accept" | "after-reject";
|
|
25
26
|
responseStatus: number | null;
|
|
26
27
|
contentType: string | null;
|
|
@@ -82,7 +83,7 @@ export interface ComplianceScore {
|
|
|
82
83
|
export type ReportFormat = "md" | "html" | "json" | "pdf";
|
|
83
84
|
export interface ScanOptions {
|
|
84
85
|
url: string;
|
|
85
|
-
outputDir
|
|
86
|
+
outputDir?: string;
|
|
86
87
|
timeout: number;
|
|
87
88
|
screenshots: boolean;
|
|
88
89
|
locale: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slashgear/gdpr-cookie-scanner",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "CLI tool to scan websites for GDPR cookie consent compliance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"compliance",
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
30
37
|
"publishConfig": {
|
|
31
38
|
"access": "public",
|
|
32
39
|
"registry": "https://registry.npmjs.org"
|
|
@@ -59,6 +66,8 @@
|
|
|
59
66
|
"typecheck": "tsc --noEmit",
|
|
60
67
|
"test": "vitest run",
|
|
61
68
|
"test:watch": "vitest",
|
|
62
|
-
"changeset": "changeset"
|
|
69
|
+
"changeset": "changeset",
|
|
70
|
+
"update-trackers": "node --experimental-strip-types scripts/update-trackers.ts",
|
|
71
|
+
"update-trackers:dry": "node --experimental-strip-types scripts/update-trackers.ts --dry-run"
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/update-trackers.ts
|
|
3
|
+
*
|
|
4
|
+
* Maintenance script: fetches Disconnect.me and DuckDuckGo Tracker Radar,
|
|
5
|
+
* maps their categories to TrackerCategory, and merges new entries into
|
|
6
|
+
* src/classifiers/tracker-list.ts — preserving all manually curated entries.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node --experimental-strip-types scripts/update-trackers.ts
|
|
10
|
+
* node --experimental-strip-types scripts/update-trackers.ts --dry-run
|
|
11
|
+
*
|
|
12
|
+
* Or via npm:
|
|
13
|
+
* pnpm update-trackers
|
|
14
|
+
* pnpm update-trackers -- --dry-run
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, resolve } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
// ── Types (inlined so the script runs without tsc) ────────────────────────────
|
|
22
|
+
|
|
23
|
+
type TrackerCategory =
|
|
24
|
+
| "analytics"
|
|
25
|
+
| "advertising"
|
|
26
|
+
| "social"
|
|
27
|
+
| "fingerprinting"
|
|
28
|
+
| "pixel"
|
|
29
|
+
| "cdn"
|
|
30
|
+
| "unknown";
|
|
31
|
+
|
|
32
|
+
interface TrackerEntry {
|
|
33
|
+
name: string;
|
|
34
|
+
category: TrackerCategory;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Disconnect.me ─────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
type DisconnectDomainMap = Record<string, string[]>;
|
|
40
|
+
type DisconnectService = Record<string, DisconnectDomainMap | string>;
|
|
41
|
+
type DisconnectData = { categories: Record<string, DisconnectService[]> };
|
|
42
|
+
|
|
43
|
+
const DISCONNECT_CATEGORY_MAP: Record<string, TrackerCategory> = {
|
|
44
|
+
Analytics: "analytics",
|
|
45
|
+
Advertising: "advertising",
|
|
46
|
+
Social: "social",
|
|
47
|
+
Content: "cdn",
|
|
48
|
+
Fingerprinting: "fingerprinting",
|
|
49
|
+
Email: "advertising",
|
|
50
|
+
Disconnect: "advertising",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
async function fetchDisconnect(): Promise<Map<string, TrackerEntry>> {
|
|
54
|
+
const url =
|
|
55
|
+
"https://raw.githubusercontent.com/disconnectme/disconnect-tracking-protection/master/services.json";
|
|
56
|
+
const res = await fetch(url);
|
|
57
|
+
if (!res.ok) throw new Error(`Disconnect.me fetch failed: HTTP ${res.status}`);
|
|
58
|
+
const data = (await res.json()) as DisconnectData;
|
|
59
|
+
const result = new Map<string, TrackerEntry>();
|
|
60
|
+
|
|
61
|
+
for (const [catName, services] of Object.entries(data.categories)) {
|
|
62
|
+
const category = DISCONNECT_CATEGORY_MAP[catName];
|
|
63
|
+
if (!category) continue;
|
|
64
|
+
for (const serviceObj of services) {
|
|
65
|
+
for (const [serviceName, value] of Object.entries(serviceObj)) {
|
|
66
|
+
if (typeof value !== "object" || value === null) continue;
|
|
67
|
+
const domainMap = value as DisconnectDomainMap;
|
|
68
|
+
for (const [mainDomain, subDomains] of Object.entries(domainMap)) {
|
|
69
|
+
result.set(mainDomain, { name: serviceName, category });
|
|
70
|
+
for (const sub of subDomains) {
|
|
71
|
+
// Subdomain inherits parent name/category unless already set
|
|
72
|
+
if (!result.has(sub)) {
|
|
73
|
+
result.set(sub, { name: serviceName, category });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── DuckDuckGo Tracker Radar ──────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
interface DDGTracker {
|
|
87
|
+
domain: string;
|
|
88
|
+
owner?: { name: string; displayName: string };
|
|
89
|
+
prevalence: number;
|
|
90
|
+
categories?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type DDGData = { trackers: Record<string, DDGTracker> };
|
|
94
|
+
|
|
95
|
+
const DDG_CATEGORY_MAP: Record<string, TrackerCategory> = {
|
|
96
|
+
Analytics: "analytics",
|
|
97
|
+
Advertising: "advertising",
|
|
98
|
+
"Social Network": "social",
|
|
99
|
+
Fingerprinting: "fingerprinting",
|
|
100
|
+
CDN: "cdn",
|
|
101
|
+
"Action Pixels": "pixel",
|
|
102
|
+
"Embedded Content": "cdn",
|
|
103
|
+
"Session Replay": "analytics",
|
|
104
|
+
"Email Tracking": "advertising",
|
|
105
|
+
"Ad Motivated Tracking": "advertising",
|
|
106
|
+
"Audience Measurement": "analytics",
|
|
107
|
+
"Third-Party Analytics Marketing": "advertising",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Minimum prevalence (fraction of crawled sites) to include in the DB. */
|
|
111
|
+
const MIN_PREVALENCE = 0.001; // ≥ 0.1 %
|
|
112
|
+
|
|
113
|
+
async function fetchDDG(): Promise<Map<string, TrackerEntry>> {
|
|
114
|
+
const url =
|
|
115
|
+
"https://raw.githubusercontent.com/duckduckgo/tracker-radar/main/build-data/generated/tds.json";
|
|
116
|
+
const res = await fetch(url);
|
|
117
|
+
if (!res.ok) throw new Error(`DDG Tracker Radar fetch failed: HTTP ${res.status}`);
|
|
118
|
+
const data = (await res.json()) as DDGData;
|
|
119
|
+
const result = new Map<string, TrackerEntry>();
|
|
120
|
+
|
|
121
|
+
for (const tracker of Object.values(data.trackers)) {
|
|
122
|
+
if ((tracker.prevalence ?? 0) < MIN_PREVALENCE) continue;
|
|
123
|
+
|
|
124
|
+
let category: TrackerCategory | undefined;
|
|
125
|
+
for (const cat of tracker.categories ?? []) {
|
|
126
|
+
const mapped = DDG_CATEGORY_MAP[cat];
|
|
127
|
+
if (mapped) {
|
|
128
|
+
category = mapped;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!category) continue;
|
|
133
|
+
|
|
134
|
+
const name = tracker.owner?.displayName ?? tracker.domain;
|
|
135
|
+
result.set(tracker.domain, { name, category });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Tracker-list.ts patcher ───────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const MARKER_START = "// ── AUTO-GENERATED: DO NOT EDIT BELOW THIS LINE";
|
|
144
|
+
const MARKER_END = "// ── END AUTO-GENERATED";
|
|
145
|
+
|
|
146
|
+
const CAT_ORDER: TrackerCategory[] = [
|
|
147
|
+
"analytics",
|
|
148
|
+
"advertising",
|
|
149
|
+
"social",
|
|
150
|
+
"fingerprinting",
|
|
151
|
+
"pixel",
|
|
152
|
+
"cdn",
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/** Extract domain keys that already exist in the manual section of the file. */
|
|
156
|
+
function extractExistingKeys(manualSection: string): Set<string> {
|
|
157
|
+
const keys = new Set<string>();
|
|
158
|
+
// Matches tracker entry lines: "domain.com": { name:
|
|
159
|
+
const re = /^\s+"([^"]+)":\s*\{\s*name:/gm;
|
|
160
|
+
let m: RegExpExecArray | null;
|
|
161
|
+
while ((m = re.exec(manualSection)) !== null) {
|
|
162
|
+
keys.add(m[1]);
|
|
163
|
+
}
|
|
164
|
+
return keys;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderAutoSection(entries: Map<string, TrackerEntry>, date: string): string {
|
|
168
|
+
const lines: string[] = [
|
|
169
|
+
` ${MARKER_START} ──────────────────────────`,
|
|
170
|
+
` // Last updated: ${date}`,
|
|
171
|
+
` // Sources: Disconnect.me · DuckDuckGo Tracker Radar (prevalence ≥ ${MIN_PREVALENCE * 100}%)`,
|
|
172
|
+
` // New entries: ${entries.size} domains`,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
if (entries.size === 0) {
|
|
176
|
+
lines.push(` ${MARKER_END}`);
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push("");
|
|
181
|
+
|
|
182
|
+
const byCategory = new Map<TrackerCategory, Array<[string, TrackerEntry]>>();
|
|
183
|
+
for (const [domain, entry] of entries) {
|
|
184
|
+
const bucket = byCategory.get(entry.category) ?? [];
|
|
185
|
+
bucket.push([domain, entry]);
|
|
186
|
+
byCategory.set(entry.category, bucket);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sorted = [...byCategory.entries()].sort(
|
|
190
|
+
([a], [b]) => CAT_ORDER.indexOf(a) - CAT_ORDER.indexOf(b),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
for (const [cat, domainEntries] of sorted) {
|
|
194
|
+
domainEntries.sort(([a], [b]) => a.localeCompare(b));
|
|
195
|
+
const pad = "─".repeat(Math.max(0, 45 - cat.length));
|
|
196
|
+
lines.push(` // ── ${cat} (auto) ${pad}`);
|
|
197
|
+
for (const [domain, { name, category }] of domainEntries) {
|
|
198
|
+
const escaped = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
199
|
+
lines.push(` "${domain}": { name: "${escaped}", category: "${category}" },`);
|
|
200
|
+
}
|
|
201
|
+
lines.push("");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
lines.push(` ${MARKER_END}`);
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function patchFile(content: string, newEntries: Map<string, TrackerEntry>, date: string): string {
|
|
209
|
+
const autoSection = renderAutoSection(newEntries, date);
|
|
210
|
+
const startMarker = ` ${MARKER_START}`;
|
|
211
|
+
const endMarker = ` ${MARKER_END}`;
|
|
212
|
+
|
|
213
|
+
const startIdx = content.indexOf(startMarker);
|
|
214
|
+
const endIdx = content.indexOf(endMarker);
|
|
215
|
+
|
|
216
|
+
if (startIdx >= 0 && endIdx >= 0) {
|
|
217
|
+
const before = content.slice(0, startIdx);
|
|
218
|
+
const after = content.slice(endIdx + endMarker.length);
|
|
219
|
+
return before + autoSection + after;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// First run: insert before the closing `};` of TRACKER_DB
|
|
223
|
+
const closingIdx = content.lastIndexOf("\n};");
|
|
224
|
+
return content.slice(0, closingIdx) + "\n\n" + autoSection + "\n" + content.slice(closingIdx + 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
async function main(): Promise<void> {
|
|
230
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
231
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
232
|
+
const trackerListPath = resolve(__dirname, "../src/classifiers/tracker-list.ts");
|
|
233
|
+
|
|
234
|
+
console.log("Fetching Disconnect.me…");
|
|
235
|
+
const disconnectEntries = await fetchDisconnect();
|
|
236
|
+
console.log(` → ${disconnectEntries.size} domains`);
|
|
237
|
+
|
|
238
|
+
console.log(`Fetching DuckDuckGo Tracker Radar (prevalence ≥ ${MIN_PREVALENCE * 100}%)…`);
|
|
239
|
+
const ddgEntries = await fetchDDG();
|
|
240
|
+
console.log(` → ${ddgEntries.size} domains`);
|
|
241
|
+
|
|
242
|
+
const content = readFileSync(trackerListPath, "utf-8");
|
|
243
|
+
const startIdx = content.indexOf(` ${MARKER_START}`);
|
|
244
|
+
const manualSection = startIdx >= 0 ? content.slice(0, startIdx) : content;
|
|
245
|
+
const existingKeys = extractExistingKeys(manualSection);
|
|
246
|
+
console.log(` → ${existingKeys.size} existing entries kept as-is`);
|
|
247
|
+
|
|
248
|
+
// Merge: Disconnect first, DDG overrides (more precise display names)
|
|
249
|
+
const merged = new Map<string, TrackerEntry>();
|
|
250
|
+
for (const [domain, entry] of disconnectEntries) {
|
|
251
|
+
if (!existingKeys.has(domain)) merged.set(domain, entry);
|
|
252
|
+
}
|
|
253
|
+
for (const [domain, entry] of ddgEntries) {
|
|
254
|
+
if (!existingKeys.has(domain)) merged.set(domain, entry);
|
|
255
|
+
}
|
|
256
|
+
console.log(` → ${merged.size} new entries to write`);
|
|
257
|
+
|
|
258
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
259
|
+
const newContent = patchFile(content, merged, date);
|
|
260
|
+
|
|
261
|
+
if (dryRun) {
|
|
262
|
+
console.log("\n[dry-run] Result preview (last 60 lines):\n");
|
|
263
|
+
console.log(newContent.split("\n").slice(-60).join("\n"));
|
|
264
|
+
} else {
|
|
265
|
+
writeFileSync(trackerListPath, newContent, "utf-8");
|
|
266
|
+
console.log(`\nWrote ${trackerListPath}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
main().catch((err: unknown) => {
|
|
271
|
+
console.error(err);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
});
|
|
@@ -21,10 +21,21 @@ interface ComplianceInput {
|
|
|
21
21
|
export function analyzeCompliance(input: ComplianceInput): ComplianceScore {
|
|
22
22
|
const issues: DarkPatternIssue[] = [];
|
|
23
23
|
|
|
24
|
+
// Determine whether a consent mechanism is actually required
|
|
25
|
+
const hasNonEssentialCookies = [
|
|
26
|
+
...input.cookiesBeforeInteraction,
|
|
27
|
+
...input.cookiesAfterAccept,
|
|
28
|
+
].some((c) => c.requiresConsent);
|
|
29
|
+
const hasNonEssentialTrackers = [
|
|
30
|
+
...input.networkBeforeInteraction,
|
|
31
|
+
...input.networkAfterAccept,
|
|
32
|
+
].some((r) => r.requiresConsent);
|
|
33
|
+
const consentRequired = hasNonEssentialCookies || hasNonEssentialTrackers;
|
|
34
|
+
|
|
24
35
|
// ── A. Consent validity (0-25) ────────────────────────────────
|
|
25
36
|
let consentValidity = 25;
|
|
26
37
|
|
|
27
|
-
if (!input.modal.detected) {
|
|
38
|
+
if (!input.modal.detected && consentRequired) {
|
|
28
39
|
issues.push({
|
|
29
40
|
type: "no-reject-button",
|
|
30
41
|
severity: "critical",
|
|
@@ -32,7 +43,7 @@ export function analyzeCompliance(input: ComplianceInput): ComplianceScore {
|
|
|
32
43
|
evidence: "A consent mechanism is required before depositing non-essential cookies",
|
|
33
44
|
});
|
|
34
45
|
consentValidity = 0;
|
|
35
|
-
} else {
|
|
46
|
+
} else if (input.modal.detected) {
|
|
36
47
|
// Wording analysis
|
|
37
48
|
const wordingResult = analyzeButtonWording(input.modal.buttons);
|
|
38
49
|
const textResult = analyzeModalText(input.modal.text);
|
|
@@ -59,9 +70,9 @@ export function analyzeCompliance(input: ComplianceInput): ComplianceScore {
|
|
|
59
70
|
// ── B. Easy refusal (0-25) ────────────────────────────────────
|
|
60
71
|
let easyRefusal = 25;
|
|
61
72
|
|
|
62
|
-
if (!input.modal.detected) {
|
|
73
|
+
if (!input.modal.detected && consentRequired) {
|
|
63
74
|
easyRefusal = 0;
|
|
64
|
-
} else {
|
|
75
|
+
} else if (input.modal.detected) {
|
|
65
76
|
const acceptButton = input.modal.buttons.find((b) => b.type === "accept");
|
|
66
77
|
const rejectButton = input.modal.buttons.find((b) => b.type === "reject");
|
|
67
78
|
|
|
@@ -110,14 +121,48 @@ export function analyzeCompliance(input: ComplianceInput): ComplianceScore {
|
|
|
110
121
|
easyRefusal -= 5;
|
|
111
122
|
}
|
|
112
123
|
}
|
|
124
|
+
|
|
125
|
+
// Contrast ratio: reject button must meet minimum legibility
|
|
126
|
+
if (rejectButton && rejectButton.contrastRatio !== null) {
|
|
127
|
+
const ratio = rejectButton.contrastRatio;
|
|
128
|
+
if (ratio < 3.0) {
|
|
129
|
+
issues.push({
|
|
130
|
+
type: "asymmetric-prominence",
|
|
131
|
+
severity: "critical",
|
|
132
|
+
description: "Reject button has critically low contrast ratio",
|
|
133
|
+
evidence: `Contrast ratio ${ratio}:1 — WCAG AA requires 4.5:1 for normal text (${rejectButton.backgroundColor ?? "?"} / ${rejectButton.textColor ?? "?"})`,
|
|
134
|
+
});
|
|
135
|
+
easyRefusal -= 10;
|
|
136
|
+
} else if (ratio < 4.5) {
|
|
137
|
+
issues.push({
|
|
138
|
+
type: "asymmetric-prominence",
|
|
139
|
+
severity: "warning",
|
|
140
|
+
description: "Reject button contrast ratio is below WCAG AA threshold",
|
|
141
|
+
evidence: `Contrast ratio ${ratio}:1 — WCAG AA requires 4.5:1 for normal text (${rejectButton.backgroundColor ?? "?"} / ${rejectButton.textColor ?? "?"})`,
|
|
142
|
+
});
|
|
143
|
+
easyRefusal -= 5;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Relative contrast asymmetry: accept visually pops, reject is muted
|
|
147
|
+
const acceptContrast = acceptButton?.contrastRatio ?? null;
|
|
148
|
+
if (acceptContrast !== null && acceptContrast >= rejectButton.contrastRatio * 1.5) {
|
|
149
|
+
issues.push({
|
|
150
|
+
type: "asymmetric-prominence",
|
|
151
|
+
severity: "warning",
|
|
152
|
+
description: "Accept button has significantly higher contrast than reject button",
|
|
153
|
+
evidence: `Accept: ${acceptContrast}:1, Reject: ${rejectButton.contrastRatio}:1`,
|
|
154
|
+
});
|
|
155
|
+
easyRefusal -= 3;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
// ── C. Transparency (0-25) ────────────────────────────────────
|
|
116
161
|
let transparency = 25;
|
|
117
162
|
|
|
118
|
-
if (!input.modal.detected) {
|
|
163
|
+
if (!input.modal.detected && consentRequired) {
|
|
119
164
|
transparency = 0;
|
|
120
|
-
} else {
|
|
165
|
+
} else if (input.modal.detected) {
|
|
121
166
|
if (!input.modal.hasGranularControls) {
|
|
122
167
|
transparency -= 10;
|
|
123
168
|
}
|
|
@@ -139,8 +184,8 @@ export function analyzeCompliance(input: ComplianceInput): ComplianceScore {
|
|
|
139
184
|
}
|
|
140
185
|
}
|
|
141
186
|
|
|
142
|
-
// No privacy policy link anywhere on the page
|
|
143
|
-
if (!input.privacyPolicyUrl) {
|
|
187
|
+
// No privacy policy link anywhere on the page (only relevant when consent is required)
|
|
188
|
+
if (!input.privacyPolicyUrl && consentRequired) {
|
|
144
189
|
issues.push({
|
|
145
190
|
type: "missing-info",
|
|
146
191
|
severity: "warning",
|
|
@@ -182,9 +227,7 @@ export function analyzeCompliance(input: ComplianceInput): ComplianceScore {
|
|
|
182
227
|
}
|
|
183
228
|
|
|
184
229
|
// Network trackers firing before interaction
|
|
185
|
-
const preInteractionTrackers = input.networkBeforeInteraction.filter(
|
|
186
|
-
(r) => r.trackerCategory !== null && r.trackerCategory !== "cdn",
|
|
187
|
-
);
|
|
230
|
+
const preInteractionTrackers = input.networkBeforeInteraction.filter((r) => r.requiresConsent);
|
|
188
231
|
|
|
189
232
|
if (preInteractionTrackers.length > 0) {
|
|
190
233
|
issues.push({
|
|
@@ -94,7 +94,8 @@ const COOKIE_PATTERNS: Array<{
|
|
|
94
94
|
|
|
95
95
|
// ── Social ─────────────────────────────────────────────────────
|
|
96
96
|
{ pattern: /^(fbsr_|fbm_)/, category: "social", requiresConsent: true }, // Facebook login
|
|
97
|
-
{ pattern: /^(yt-|
|
|
97
|
+
{ pattern: /^(yt-|YSC|GPS)$/i, category: "social", requiresConsent: true }, // YouTube
|
|
98
|
+
{ pattern: /^VISITOR_INFO/i, category: "social", requiresConsent: true }, // YouTube VISITOR_INFO1_LIVE etc.
|
|
98
99
|
|
|
99
100
|
// ── Personalization ────────────────────────────────────────────
|
|
100
101
|
{
|
|
@@ -5,6 +5,7 @@ interface NetworkClassification {
|
|
|
5
5
|
isThirdParty: boolean;
|
|
6
6
|
trackerCategory: TrackerCategory | null;
|
|
7
7
|
trackerName: string | null;
|
|
8
|
+
requiresConsent: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function classifyNetworkRequest(url: string, resourceType: string): NetworkClassification {
|
|
@@ -13,7 +14,12 @@ export function classifyNetworkRequest(url: string, resourceType: string): Netwo
|
|
|
13
14
|
try {
|
|
14
15
|
hostname = new URL(url).hostname.replace(/^www\./, "");
|
|
15
16
|
} catch {
|
|
16
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
isThirdParty: false,
|
|
19
|
+
trackerCategory: null,
|
|
20
|
+
trackerName: null,
|
|
21
|
+
requiresConsent: false,
|
|
22
|
+
};
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
// Check tracker database (exact match or suffix match)
|
|
@@ -23,6 +29,7 @@ export function classifyNetworkRequest(url: string, resourceType: string): Netwo
|
|
|
23
29
|
isThirdParty: true,
|
|
24
30
|
trackerCategory: entry.category,
|
|
25
31
|
trackerName: entry.name,
|
|
32
|
+
requiresConsent: entry.consentRequired !== false && entry.category !== "cdn",
|
|
26
33
|
};
|
|
27
34
|
}
|
|
28
35
|
}
|
|
@@ -33,6 +40,7 @@ export function classifyNetworkRequest(url: string, resourceType: string): Netwo
|
|
|
33
40
|
isThirdParty: true,
|
|
34
41
|
trackerCategory: "pixel",
|
|
35
42
|
trackerName: "Tracking Pixel",
|
|
43
|
+
requiresConsent: true,
|
|
36
44
|
};
|
|
37
45
|
}
|
|
38
46
|
|
|
@@ -42,6 +50,7 @@ export function classifyNetworkRequest(url: string, resourceType: string): Netwo
|
|
|
42
50
|
isThirdParty: true,
|
|
43
51
|
trackerCategory: "pixel",
|
|
44
52
|
trackerName: "Tracking Pixel (image)",
|
|
53
|
+
requiresConsent: true,
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
|
|
@@ -49,6 +58,7 @@ export function classifyNetworkRequest(url: string, resourceType: string): Netwo
|
|
|
49
58
|
isThirdParty: false,
|
|
50
59
|
trackerCategory: null,
|
|
51
60
|
trackerName: null,
|
|
61
|
+
requiresConsent: false,
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -3,6 +3,7 @@ import type { TrackerCategory } from "../types.js";
|
|
|
3
3
|
interface TrackerEntry {
|
|
4
4
|
name: string;
|
|
5
5
|
category: TrackerCategory;
|
|
6
|
+
consentRequired?: boolean; // absent = true by default
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -25,7 +26,7 @@ export const TRACKER_DB: Record<string, TrackerEntry> = {
|
|
|
25
26
|
// ── Meta / Facebook ───────────────────────────────────────────
|
|
26
27
|
"connect.facebook.net": { name: "Facebook SDK", category: "social" },
|
|
27
28
|
"graph.facebook.com": { name: "Facebook Graph API", category: "social" },
|
|
28
|
-
"facebook.com
|
|
29
|
+
"pixel.facebook.com": { name: "Meta Pixel", category: "advertising" },
|
|
29
30
|
"fbcdn.net": { name: "Facebook CDN", category: "social" },
|
|
30
31
|
|
|
31
32
|
// ── Microsoft ─────────────────────────────────────────────────
|
|
@@ -89,6 +90,13 @@ export const TRACKER_DB: Record<string, TrackerEntry> = {
|
|
|
89
90
|
"optimizely.com": { name: "Optimizely", category: "analytics" },
|
|
90
91
|
"vwo.com": { name: "VWO", category: "analytics" },
|
|
91
92
|
"app.convert.com": { name: "Convert", category: "analytics" },
|
|
93
|
+
|
|
94
|
+
// ── Consent-exempt analytics (CNIL ePrivacy exemption) ───────
|
|
95
|
+
"plausible.io": {
|
|
96
|
+
name: "Plausible Analytics",
|
|
97
|
+
category: "analytics",
|
|
98
|
+
consentRequired: false,
|
|
99
|
+
},
|
|
92
100
|
};
|
|
93
101
|
|
|
94
102
|
/**
|
package/src/cli.ts
CHANGED
|
@@ -23,11 +23,7 @@ program
|
|
|
23
23
|
.option("--no-screenshots", "Disable screenshot capture")
|
|
24
24
|
.option("-l, --locale <locale>", "Browser locale for language detection", "fr-FR")
|
|
25
25
|
.option("-v, --verbose", "Show detailed output", false)
|
|
26
|
-
.option(
|
|
27
|
-
"-f, --format <formats>",
|
|
28
|
-
"Output formats: md, html, json, pdf (comma-separated)",
|
|
29
|
-
"md,pdf",
|
|
30
|
-
)
|
|
26
|
+
.option("-f, --format <formats>", "Output formats: md, html, json, pdf (comma-separated)", "html")
|
|
31
27
|
.action(async (url: string, opts) => {
|
|
32
28
|
console.log();
|
|
33
29
|
console.log(styleText(["bold", "blue"], " GDPR Cookie Scanner"));
|