@slashgear/gdpr-cookie-scanner 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +44 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +24 -0
  6. package/.github/workflows/ci.yml +38 -0
  7. package/.github/workflows/release.yml +57 -0
  8. package/.idea/gdpr-report.iml +8 -0
  9. package/.idea/modules.xml +8 -0
  10. package/.idea/vcs.xml +6 -0
  11. package/CHANGELOG.md +7 -0
  12. package/CLAUDE.md +75 -0
  13. package/CODE_OF_CONDUCT.md +41 -0
  14. package/CONTRIBUTING.md +79 -0
  15. package/LICENSE +21 -0
  16. package/README.md +127 -0
  17. package/SECURITY.md +15 -0
  18. package/dist/analyzers/compliance.d.ts +13 -0
  19. package/dist/analyzers/compliance.d.ts.map +1 -0
  20. package/dist/analyzers/compliance.js +171 -0
  21. package/dist/analyzers/compliance.js.map +1 -0
  22. package/dist/analyzers/wording.d.ts +13 -0
  23. package/dist/analyzers/wording.d.ts.map +1 -0
  24. package/dist/analyzers/wording.js +91 -0
  25. package/dist/analyzers/wording.js.map +1 -0
  26. package/dist/classifiers/cookie-classifier.d.ts +8 -0
  27. package/dist/classifiers/cookie-classifier.d.ts.map +1 -0
  28. package/dist/classifiers/cookie-classifier.js +108 -0
  29. package/dist/classifiers/cookie-classifier.js.map +1 -0
  30. package/dist/classifiers/network-classifier.d.ts +9 -0
  31. package/dist/classifiers/network-classifier.d.ts.map +1 -0
  32. package/dist/classifiers/network-classifier.js +51 -0
  33. package/dist/classifiers/network-classifier.js.map +1 -0
  34. package/dist/classifiers/tracker-list.d.ts +16 -0
  35. package/dist/classifiers/tracker-list.d.ts.map +1 -0
  36. package/dist/classifiers/tracker-list.js +86 -0
  37. package/dist/classifiers/tracker-list.js.map +1 -0
  38. package/dist/cli.d.ts +3 -0
  39. package/dist/cli.d.ts.map +1 -0
  40. package/dist/cli.js +110 -0
  41. package/dist/cli.js.map +1 -0
  42. package/dist/report/generator.d.ts +19 -0
  43. package/dist/report/generator.d.ts.map +1 -0
  44. package/dist/report/generator.js +552 -0
  45. package/dist/report/generator.js.map +1 -0
  46. package/dist/scanner/browser.d.ts +11 -0
  47. package/dist/scanner/browser.d.ts.map +1 -0
  48. package/dist/scanner/browser.js +38 -0
  49. package/dist/scanner/browser.js.map +1 -0
  50. package/dist/scanner/consent-modal.d.ts +5 -0
  51. package/dist/scanner/consent-modal.d.ts.map +1 -0
  52. package/dist/scanner/consent-modal.js +244 -0
  53. package/dist/scanner/consent-modal.js.map +1 -0
  54. package/dist/scanner/cookies.d.ts +11 -0
  55. package/dist/scanner/cookies.d.ts.map +1 -0
  56. package/dist/scanner/cookies.js +30 -0
  57. package/dist/scanner/cookies.js.map +1 -0
  58. package/dist/scanner/index.d.ts +9 -0
  59. package/dist/scanner/index.d.ts.map +1 -0
  60. package/dist/scanner/index.js +146 -0
  61. package/dist/scanner/index.js.map +1 -0
  62. package/dist/scanner/network.d.ts +8 -0
  63. package/dist/scanner/network.d.ts.map +1 -0
  64. package/dist/scanner/network.js +41 -0
  65. package/dist/scanner/network.js.map +1 -0
  66. package/dist/types.d.ts +105 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +2 -0
  69. package/dist/types.js.map +1 -0
  70. package/package.json +52 -0
  71. package/renovate.json +17 -0
  72. package/src/analyzers/compliance.ts +203 -0
  73. package/src/analyzers/wording.ts +112 -0
  74. package/src/classifiers/cookie-classifier.ts +125 -0
  75. package/src/classifiers/network-classifier.ts +65 -0
  76. package/src/classifiers/tracker-list.ts +105 -0
  77. package/src/cli.ts +134 -0
  78. package/src/report/generator.ts +703 -0
  79. package/src/scanner/browser.ts +52 -0
  80. package/src/scanner/consent-modal.ts +276 -0
  81. package/src/scanner/cookies.ts +43 -0
  82. package/src/scanner/index.ts +163 -0
  83. package/src/scanner/network.ts +51 -0
  84. package/src/types.ts +134 -0
  85. package/tsconfig.json +18 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/scanner/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgD,MAAM,YAAY,CAAC;AASpF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAoB;IACtD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;QACpC,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE;YACJ,cAAc;YACd,0BAA0B;YAC1B,yBAAyB;YACzB,+CAA+C;SAChD;KACF,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;QACvC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QACtC,SAAS,EACP,OAAO,CAAC,SAAS;YACjB;gBACE,iDAAiD;gBACjD,wCAAwC;gBACxC,gCAAgC;aACjC,CAAC,IAAI,CAAC,GAAG,CAAC;QACb,gDAAgD;QAChD,YAAY,EAAE,SAAS;KACxB,CAAC,CAAC;IAEH,2DAA2D;IAC3D,MAAM,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IAE/E,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IAErC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAuB;IACtD,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;IAC7B,MAAM,OAAO,CAAC,gBAAgB,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAuB;IACxD,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;AAClD,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Page } from "playwright";
2
+ import type { ConsentModal } from "../types.js";
3
+ import type { ScanOptions } from "../types.js";
4
+ export declare function detectConsentModal(page: Page, options: ScanOptions): Promise<ConsentModal>;
5
+ //# sourceMappingURL=consent-modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent-modal.d.ts","sourceRoot":"","sources":["../../src/scanner/consent-modal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAqD,MAAM,aAAa,CAAC;AAEnG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAwD/C,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA8EhG"}
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Ordered list of CSS selectors to try for detecting a consent modal/banner.
3
+ * Covers major CMP platforms (Axeptio, Cookiebot, OneTrust, Didomi, Tarteaucitron, etc.)
4
+ */
5
+ const MODAL_SELECTORS = [
6
+ // Well-known CMPs
7
+ "#axeptio_overlay",
8
+ "#axeptio-root",
9
+ "#CybotCookiebotDialog",
10
+ "#onetrust-consent-sdk",
11
+ "#onetrust-banner-sdk",
12
+ ".didomi-popup-container",
13
+ ".didomi-consent-popup",
14
+ "#didomi-host",
15
+ "#tarteaucitronRoot",
16
+ "#tarteaucitron",
17
+ "#usercentrics-root",
18
+ "#sp-cc",
19
+ "#gdpr-consent-tool-wrapper",
20
+ ".cc-banner",
21
+ ".cc-window",
22
+ "#cookieConsent",
23
+ "#cookie-consent",
24
+ "#cookie-banner",
25
+ "#cookie-notice",
26
+ "#cookie-law-info-bar",
27
+ // Generic heuristics
28
+ "[class*='cookie'][class*='banner']",
29
+ "[class*='cookie'][class*='modal']",
30
+ "[class*='cookie'][class*='popup']",
31
+ "[class*='consent'][class*='banner']",
32
+ "[class*='consent'][class*='modal']",
33
+ "[id*='cookie'][id*='banner']",
34
+ "[id*='cookie'][id*='modal']",
35
+ "[id*='consent']",
36
+ "[aria-label*='cookie' i]",
37
+ "[aria-label*='consent' i]",
38
+ "[aria-label*='cookies' i]",
39
+ "[role='dialog'][aria-label*='cookie' i]",
40
+ "[role='alertdialog']",
41
+ ];
42
+ const ACCEPT_PATTERNS = [
43
+ /\b(accept|accepter|acceptez|tout accepter|accept all|j'accepte|i accept|agree|ok\b|d'accord|continuer|continue|valider|confirmer)\b/i,
44
+ ];
45
+ const REJECT_PATTERNS = [
46
+ /\b(refus|refuse|refuser|reject|deny|decline|tout refuser|reject all|non merci|no thanks|continuer sans accepter|skip)\b/i,
47
+ ];
48
+ const PREFERENCES_PATTERNS = [
49
+ /\b(param[eè]tres|pr[eé]f[eé]rences|personnaliser|customise|customize|manage|g[eé]rer|options|choose|choisir|configure)\b/i,
50
+ ];
51
+ export async function detectConsentModal(page, options) {
52
+ // Try each selector until we find a visible modal
53
+ let foundSelector = null;
54
+ for (const selector of MODAL_SELECTORS) {
55
+ try {
56
+ const element = await page.$(selector);
57
+ if (!element)
58
+ continue;
59
+ const isVisible = await element.isVisible();
60
+ if (isVisible) {
61
+ foundSelector = selector;
62
+ break;
63
+ }
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ }
69
+ // Fallback: look for any large fixed/sticky element with cookie-related text
70
+ if (!foundSelector) {
71
+ foundSelector = await page.evaluate(() => {
72
+ const candidates = document.querySelectorAll("div, section, aside, dialog");
73
+ const keywords = /cookie|consent|consentement|rgpd|gdpr|privacy|vie priv/i;
74
+ for (const el of candidates) {
75
+ const style = window.getComputedStyle(el);
76
+ const isFixed = style.position === "fixed" || style.position === "sticky";
77
+ const text = el.textContent ?? "";
78
+ const hasCookieText = keywords.test(text);
79
+ const isLargeEnough = el.getBoundingClientRect().width > 200;
80
+ if (isFixed && hasCookieText && isLargeEnough) {
81
+ // Generate a unique selector
82
+ if (el.id)
83
+ return `#${el.id}`;
84
+ const classes = Array.from(el.classList).slice(0, 2).join(".");
85
+ if (classes)
86
+ return `${el.tagName.toLowerCase()}.${classes}`;
87
+ }
88
+ }
89
+ return null;
90
+ });
91
+ }
92
+ if (!foundSelector) {
93
+ return {
94
+ detected: false,
95
+ selector: null,
96
+ text: "",
97
+ buttons: [],
98
+ checkboxes: [],
99
+ hasGranularControls: false,
100
+ layerCount: 0,
101
+ screenshotPath: null,
102
+ };
103
+ }
104
+ // Extract modal text
105
+ const modalText = await page.$eval(foundSelector, (el) => el.textContent ?? "").catch(() => "");
106
+ // Find all buttons and interactive elements within the modal
107
+ const buttons = await extractButtons(page, foundSelector);
108
+ // Find checkboxes / toggles
109
+ const checkboxes = await extractCheckboxes(page, foundSelector);
110
+ // Detect if there are nested layers (e.g., "more options" behind a click)
111
+ const hasGranularControls = checkboxes.length > 0 || buttons.some((b) => b.type === "preferences");
112
+ return {
113
+ detected: true,
114
+ selector: foundSelector,
115
+ text: modalText.trim().replace(/\s+/g, " "),
116
+ buttons,
117
+ checkboxes,
118
+ hasGranularControls,
119
+ layerCount: hasGranularControls ? 2 : 1,
120
+ screenshotPath: null,
121
+ };
122
+ }
123
+ async function extractButtons(page, modalSelector) {
124
+ const buttonEls = await page.$$(`${modalSelector} button, ${modalSelector} [role="button"], ${modalSelector} a[href="#"]`);
125
+ const buttons = [];
126
+ for (const el of buttonEls) {
127
+ try {
128
+ const text = ((await el.textContent()) ?? "").trim();
129
+ if (!text)
130
+ continue;
131
+ const isVisible = await el.isVisible();
132
+ const box = await el.boundingBox();
133
+ const computedStyle = await el.evaluate((node) => {
134
+ const style = window.getComputedStyle(node);
135
+ return {
136
+ fontSize: parseFloat(style.fontSize),
137
+ backgroundColor: style.backgroundColor,
138
+ color: style.color,
139
+ };
140
+ });
141
+ const type = classifyButtonType(text);
142
+ // Build a unique selector for this button
143
+ const selector = await el.evaluate((node) => {
144
+ const el = node;
145
+ if (el.id)
146
+ return `#${el.id}`;
147
+ const classes = Array.from(el.classList).slice(0, 3).join(".");
148
+ const tag = el.tagName.toLowerCase();
149
+ // Try to build a text-based selector as fallback
150
+ const escapedText = el.textContent?.trim().substring(0, 30) ?? "";
151
+ return classes ? `${tag}.${classes}` : `${tag}:has-text("${escapedText}")`;
152
+ });
153
+ const contrastRatio = computeContrastRatio(computedStyle.color, computedStyle.backgroundColor);
154
+ buttons.push({
155
+ type,
156
+ text,
157
+ selector,
158
+ isVisible,
159
+ boundingBox: box,
160
+ fontSize: computedStyle.fontSize || null,
161
+ backgroundColor: computedStyle.backgroundColor,
162
+ textColor: computedStyle.color,
163
+ contrastRatio,
164
+ clickDepth: 1,
165
+ });
166
+ }
167
+ catch {
168
+ continue;
169
+ }
170
+ }
171
+ return buttons;
172
+ }
173
+ async function extractCheckboxes(page, modalSelector) {
174
+ return page
175
+ .evaluate((selector) => {
176
+ const modal = document.querySelector(selector);
177
+ if (!modal)
178
+ return [];
179
+ const checkboxes = [];
180
+ const inputs = modal.querySelectorAll('input[type="checkbox"], input[type="radio"], [role="switch"], [role="checkbox"]');
181
+ for (const input of inputs) {
182
+ const el = input;
183
+ // Find associated label
184
+ let label = "";
185
+ if (el.id) {
186
+ const labelEl = document.querySelector(`label[for="${el.id}"]`);
187
+ label = labelEl?.textContent?.trim() ?? "";
188
+ }
189
+ if (!label) {
190
+ const parent = el.closest("label") ?? el.parentElement;
191
+ label = parent?.textContent?.trim() ?? "";
192
+ }
193
+ checkboxes.push({
194
+ name: el.name || el.id || "",
195
+ label: label.substring(0, 100),
196
+ isCheckedByDefault: el.checked || el.getAttribute("aria-checked") === "true",
197
+ category: "unknown", // will be classified later
198
+ selector: el.id ? `#${el.id}` : "",
199
+ });
200
+ }
201
+ return checkboxes;
202
+ }, modalSelector)
203
+ .catch(() => []);
204
+ }
205
+ function classifyButtonType(text) {
206
+ if (ACCEPT_PATTERNS.some((p) => p.test(text)))
207
+ return "accept";
208
+ if (REJECT_PATTERNS.some((p) => p.test(text)))
209
+ return "reject";
210
+ if (PREFERENCES_PATTERNS.some((p) => p.test(text)))
211
+ return "preferences";
212
+ if (/\b(ferm|close|×|✕)\b/i.test(text))
213
+ return "close";
214
+ return "unknown";
215
+ }
216
+ /**
217
+ * Basic contrast ratio computation from RGB strings.
218
+ * Returns null if colors cannot be parsed.
219
+ */
220
+ function computeContrastRatio(fg, bg) {
221
+ const fgRgb = parseRgb(fg);
222
+ const bgRgb = parseRgb(bg);
223
+ if (!fgRgb || !bgRgb)
224
+ return null;
225
+ const fgL = relativeLuminance(fgRgb);
226
+ const bgL = relativeLuminance(bgRgb);
227
+ const lighter = Math.max(fgL, bgL);
228
+ const darker = Math.min(fgL, bgL);
229
+ return parseFloat(((lighter + 0.05) / (darker + 0.05)).toFixed(2));
230
+ }
231
+ function parseRgb(color) {
232
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
233
+ if (!match)
234
+ return null;
235
+ return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
236
+ }
237
+ function relativeLuminance([r, g, b]) {
238
+ const toLinear = (c) => {
239
+ const s = c / 255;
240
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
241
+ };
242
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
243
+ }
244
+ //# sourceMappingURL=consent-modal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent-modal.js","sourceRoot":"","sources":["../../src/scanner/consent-modal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { BrowserContext } from "playwright";
2
+ import type { ScannedCookie } from "../types.js";
3
+ type CapturePhase = ScannedCookie["capturedAt"];
4
+ export declare function captureCookies(context: BrowserContext, phase: CapturePhase): Promise<ScannedCookie[]>;
5
+ export declare function diffCookies(before: ScannedCookie[], after: ScannedCookie[]): {
6
+ added: ScannedCookie[];
7
+ removed: ScannedCookie[];
8
+ persisted: ScannedCookie[];
9
+ };
10
+ export {};
11
+ //# sourceMappingURL=cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/scanner/cookies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,KAAK,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;AAEhD,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,aAAa,EAAE,CAAC,CAmB1B;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,aAAa,EAAE,EACvB,KAAK,EAAE,aAAa,EAAE,GACrB;IAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IAAC,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,SAAS,EAAE,aAAa,EAAE,CAAA;CAAE,CASlF"}
@@ -0,0 +1,30 @@
1
+ import { classifyCookie } from "../classifiers/cookie-classifier.js";
2
+ export async function captureCookies(context, phase) {
3
+ const rawCookies = await context.cookies();
4
+ return rawCookies.map((c) => {
5
+ const classification = classifyCookie(c.name, c.domain, c.value);
6
+ return {
7
+ name: c.name,
8
+ domain: c.domain,
9
+ path: c.path,
10
+ value: c.value.substring(0, 100), // truncate long values
11
+ expires: c.expires === -1 ? null : c.expires,
12
+ httpOnly: c.httpOnly,
13
+ secure: c.secure,
14
+ sameSite: c.sameSite ?? null,
15
+ category: classification.category,
16
+ requiresConsent: classification.requiresConsent,
17
+ capturedAt: phase,
18
+ };
19
+ });
20
+ }
21
+ export function diffCookies(before, after) {
22
+ const beforeKeys = new Set(before.map((c) => `${c.domain}|${c.name}`));
23
+ const afterKeys = new Set(after.map((c) => `${c.domain}|${c.name}`));
24
+ return {
25
+ added: after.filter((c) => !beforeKeys.has(`${c.domain}|${c.name}`)),
26
+ removed: before.filter((c) => !afterKeys.has(`${c.domain}|${c.name}`)),
27
+ persisted: after.filter((c) => beforeKeys.has(`${c.domain}|${c.name}`)),
28
+ };
29
+ }
30
+ //# sourceMappingURL=cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/scanner/cookies.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAIrE,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,OAAuB,EACvB,KAAmB;IAEnB,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IAE3C,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC1B,MAAM,cAAc,GAAG,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QACjE,OAAO;YACL,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,uBAAuB;YACzD,OAAO,EAAE,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;YAC5C,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI;YAC5B,QAAQ,EAAE,cAAc,CAAC,QAAQ;YACjC,eAAe,EAAE,cAAc,CAAC,eAAe;YAC/C,UAAU,EAAE,KAAK;SAClB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,MAAuB,EACvB,KAAsB;IAEtB,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAErE,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACpE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACtE,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;KACxE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ScanOptions, ScanResult } from "../types.js";
2
+ type PhaseCallback = (message: string) => void;
3
+ export declare class Scanner {
4
+ private readonly options;
5
+ constructor(options: ScanOptions);
6
+ run(onPhase?: PhaseCallback): Promise<ScanResult>;
7
+ }
8
+ export {};
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAO3D,KAAK,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAE/C,qBAAa,OAAO;IACN,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,WAAW;IAE3C,GAAG,CAAC,OAAO,GAAE,aAAwB,GAAG,OAAO,CAAC,UAAU,CAAC;CAoJlE"}
@@ -0,0 +1,146 @@
1
+ import { mkdir } from "fs/promises";
2
+ import { join } from "path";
3
+ import { createBrowser, clearState, closeBrowser } from "./browser.js";
4
+ import { captureCookies } from "./cookies.js";
5
+ import { createNetworkInterceptor } from "./network.js";
6
+ import { detectConsentModal } from "./consent-modal.js";
7
+ import { analyzeCompliance } from "../analyzers/compliance.js";
8
+ export class Scanner {
9
+ options;
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+ async run(onPhase = () => { }) {
14
+ const startTime = Date.now();
15
+ const screenshotPaths = [];
16
+ const errors = [];
17
+ if (this.options.screenshots) {
18
+ await mkdir(this.options.outputDir, { recursive: true });
19
+ }
20
+ // ────────────────────────────────────────────────────────────
21
+ // Phase 1 — Load page, capture state BEFORE any interaction
22
+ // ────────────────────────────────────────────────────────────
23
+ onPhase("Phase 1/4 — Loading page (no interaction)...");
24
+ const session1 = await createBrowser(this.options);
25
+ const interceptor1 = createNetworkInterceptor(session1.page, "before-interaction");
26
+ try {
27
+ await session1.page.goto(this.options.url, {
28
+ waitUntil: "networkidle",
29
+ timeout: this.options.timeout,
30
+ });
31
+ }
32
+ catch (err) {
33
+ errors.push(`Navigation timeout or error: ${String(err)}`);
34
+ }
35
+ // Give a moment for late-loading scripts
36
+ await session1.page.waitForTimeout(2000);
37
+ const cookiesBeforeInteraction = await captureCookies(session1.context, "before-interaction");
38
+ const networkBeforeInteraction = interceptor1.getRequests();
39
+ interceptor1.stop();
40
+ // ────────────────────────────────────────────────────────────
41
+ // Phase 2 — Detect and analyze the consent modal
42
+ // ────────────────────────────────────────────────────────────
43
+ onPhase("Phase 2/4 — Analyzing consent modal...");
44
+ const modal = await detectConsentModal(session1.page, this.options);
45
+ if (this.options.screenshots && modal.detected) {
46
+ const screenshotPath = join(this.options.outputDir, "modal-initial.png");
47
+ await session1.page.screenshot({ path: screenshotPath, fullPage: false });
48
+ screenshotPaths.push(screenshotPath);
49
+ modal.screenshotPath = screenshotPath;
50
+ }
51
+ // ────────────────────────────────────────────────────────────
52
+ // Phase 3 — Click REJECT, capture state after
53
+ // ────────────────────────────────────────────────────────────
54
+ onPhase("Phase 3/4 — Testing reject button...");
55
+ const interceptor3 = createNetworkInterceptor(session1.page, "after-reject");
56
+ let cookiesAfterReject = cookiesBeforeInteraction;
57
+ let networkAfterReject = [];
58
+ const rejectButton = modal.buttons.find((b) => b.type === "reject");
59
+ if (rejectButton) {
60
+ try {
61
+ await session1.page.click(rejectButton.selector, { timeout: 5000 });
62
+ await session1.page.waitForTimeout(2000);
63
+ cookiesAfterReject = await captureCookies(session1.context, "after-reject");
64
+ networkAfterReject = interceptor3.getRequests();
65
+ }
66
+ catch (err) {
67
+ errors.push(`Could not click reject button: ${String(err)}`);
68
+ }
69
+ }
70
+ else {
71
+ errors.push("No reject button found — could not test rejection flow");
72
+ }
73
+ interceptor3.stop();
74
+ if (this.options.screenshots) {
75
+ const screenshotPath = join(this.options.outputDir, "after-reject.png");
76
+ await session1.page.screenshot({ path: screenshotPath, fullPage: false });
77
+ screenshotPaths.push(screenshotPath);
78
+ }
79
+ await closeBrowser(session1);
80
+ // ────────────────────────────────────────────────────────────
81
+ // Phase 4 — Fresh session, click ACCEPT, capture state after
82
+ // ────────────────────────────────────────────────────────────
83
+ onPhase("Phase 4/4 — Testing accept button...");
84
+ const session2 = await createBrowser(this.options);
85
+ await clearState(session2.context);
86
+ const interceptor4 = createNetworkInterceptor(session2.page, "after-accept");
87
+ let cookiesAfterAccept = cookiesBeforeInteraction;
88
+ let networkAfterAccept = [];
89
+ try {
90
+ await session2.page.goto(this.options.url, {
91
+ waitUntil: "networkidle",
92
+ timeout: this.options.timeout,
93
+ });
94
+ await session2.page.waitForTimeout(2000);
95
+ const modal2 = await detectConsentModal(session2.page, this.options);
96
+ const acceptButton = modal2.buttons.find((b) => b.type === "accept");
97
+ if (acceptButton) {
98
+ await session2.page.click(acceptButton.selector, { timeout: 5000 });
99
+ await session2.page.waitForTimeout(3000);
100
+ cookiesAfterAccept = await captureCookies(session2.context, "after-accept");
101
+ networkAfterAccept = interceptor4.getRequests();
102
+ }
103
+ else {
104
+ errors.push("No accept button found — could not test acceptance flow");
105
+ }
106
+ }
107
+ catch (err) {
108
+ errors.push(`Accept phase error: ${String(err)}`);
109
+ }
110
+ interceptor4.stop();
111
+ if (this.options.screenshots) {
112
+ const screenshotPath = join(this.options.outputDir, "after-accept.png");
113
+ await session2.page.screenshot({ path: screenshotPath, fullPage: false });
114
+ screenshotPaths.push(screenshotPath);
115
+ }
116
+ await closeBrowser(session2);
117
+ // ────────────────────────────────────────────────────────────
118
+ // Analyze compliance
119
+ // ────────────────────────────────────────────────────────────
120
+ const compliance = analyzeCompliance({
121
+ modal,
122
+ cookiesBeforeInteraction,
123
+ cookiesAfterAccept,
124
+ cookiesAfterReject,
125
+ networkBeforeInteraction,
126
+ networkAfterAccept,
127
+ networkAfterReject,
128
+ });
129
+ return {
130
+ url: this.options.url,
131
+ scanDate: new Date().toISOString(),
132
+ duration: Date.now() - startTime,
133
+ modal,
134
+ cookiesBeforeInteraction,
135
+ cookiesAfterAccept,
136
+ cookiesAfterReject,
137
+ networkBeforeInteraction,
138
+ networkAfterAccept,
139
+ networkAfterReject,
140
+ compliance,
141
+ screenshotPaths,
142
+ errors,
143
+ };
144
+ }
145
+ }
146
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Page } from "playwright";
2
+ import type { NetworkRequest } from "../types.js";
3
+ export type NetworkPhase = "before-interaction" | "after-accept" | "after-reject";
4
+ export declare function createNetworkInterceptor(page: Page, phase: NetworkPhase): {
5
+ stop: () => void;
6
+ getRequests: () => NetworkRequest[];
7
+ };
8
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +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;;;EA4CvE"}
@@ -0,0 +1,41 @@
1
+ import { classifyNetworkRequest } from "../classifiers/network-classifier.js";
2
+ export function createNetworkInterceptor(page, phase) {
3
+ const captured = [];
4
+ const responseMap = new Map();
5
+ const onResponse = (response) => {
6
+ responseMap.set(response.request().url(), {
7
+ status: response.status(),
8
+ contentType: response.headers()["content-type"] ?? null,
9
+ });
10
+ };
11
+ const onRequest = (request) => {
12
+ const url = request.url();
13
+ // Skip data URIs and internal chrome requests
14
+ if (url.startsWith("data:") || url.startsWith("chrome-extension:"))
15
+ return;
16
+ const classification = classifyNetworkRequest(url, request.resourceType());
17
+ const meta = responseMap.get(url) ?? null;
18
+ captured.push({
19
+ url,
20
+ method: request.method(),
21
+ resourceType: request.resourceType(),
22
+ initiator: null,
23
+ isThirdParty: classification.isThirdParty,
24
+ trackerCategory: classification.trackerCategory,
25
+ trackerName: classification.trackerName,
26
+ capturedAt: phase,
27
+ responseStatus: meta?.status ?? null,
28
+ contentType: meta?.contentType ?? null,
29
+ });
30
+ };
31
+ page.on("response", onResponse);
32
+ page.on("request", onRequest);
33
+ return {
34
+ stop: () => {
35
+ page.off("response", onResponse);
36
+ page.off("request", onRequest);
37
+ },
38
+ getRequests: () => [...captured],
39
+ };
40
+ }
41
+ //# sourceMappingURL=network.js.map
@@ -0,0 +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"}