@slashgear/gdpr-cookie-scanner 3.3.0 → 3.4.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/ci.yml +20 -1
- package/CHANGELOG.md +97 -0
- package/NEXT_STEPS.md +0 -25
- package/README.md +15 -15
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scanner/consent-modal.d.ts +14 -1
- package/dist/scanner/consent-modal.d.ts.map +1 -1
- package/dist/scanner/consent-modal.js +148 -32
- package/dist/scanner/consent-modal.js.map +1 -1
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +11 -2
- package/dist/scanner/index.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +5 -2
- package/src/index.ts +3 -1
- package/src/scanner/consent-modal.ts +169 -32
- package/src/scanner/index.ts +11 -2
- package/src/types.ts +3 -0
- package/tests/scanner/button-classification.test.ts +241 -0
- package/tests/scanner/contrast-ratio.test.ts +172 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -43,4 +43,23 @@ jobs:
|
|
|
43
43
|
run: pnpm exec playwright install chromium --with-deps
|
|
44
44
|
|
|
45
45
|
- name: Test
|
|
46
|
-
|
|
46
|
+
id: test
|
|
47
|
+
run: |
|
|
48
|
+
start=$(date +%s)
|
|
49
|
+
pnpm test
|
|
50
|
+
echo "duration=$(( $(date +%s) - start ))" >> "$GITHUB_OUTPUT"
|
|
51
|
+
|
|
52
|
+
- name: Record test suite duration
|
|
53
|
+
if: always()
|
|
54
|
+
run: |
|
|
55
|
+
duration="${{ steps.test.outputs.duration }}"
|
|
56
|
+
echo "## Test suite duration" >> "$GITHUB_STEP_SUMMARY"
|
|
57
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
58
|
+
echo "| Suite | Duration |" >> "$GITHUB_STEP_SUMMARY"
|
|
59
|
+
echo "| ----- | -------- |" >> "$GITHUB_STEP_SUMMARY"
|
|
60
|
+
echo "| unit + e2e | ${duration}s |" >> "$GITHUB_STEP_SUMMARY"
|
|
61
|
+
if [ "${duration:-0}" -gt 300 ]; then
|
|
62
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
63
|
+
echo "> [!WARNING]" >> "$GITHUB_STEP_SUMMARY"
|
|
64
|
+
echo "> Suite exceeded 300 s — possible performance regression." >> "$GITHUB_STEP_SUMMARY"
|
|
65
|
+
fi
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# @slashgear/gdpr-cookie-scanner
|
|
2
2
|
|
|
3
|
+
## 3.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 39794dc: Crop consent modal screenshot to the element; make after-reject/accept screenshots opt-in.
|
|
8
|
+
|
|
9
|
+
Two behaviour changes:
|
|
10
|
+
|
|
11
|
+
**Modal screenshot is now always captured (cropped)**
|
|
12
|
+
The consent modal screenshot (`modal-initial.png`) is taken whenever a modal
|
|
13
|
+
is detected and `outputDir` is set, regardless of the `--screenshots` flag.
|
|
14
|
+
The screenshot is clipped to the bounding box of the modal element instead of
|
|
15
|
+
capturing the full viewport, giving a tighter, more readable image. If the
|
|
16
|
+
bounding box cannot be determined (rare), it falls back to the viewport
|
|
17
|
+
screenshot.
|
|
18
|
+
|
|
19
|
+
**`--no-screenshots` replaced by `--screenshots`**
|
|
20
|
+
Previously all three screenshots were enabled by default and `--no-screenshots`
|
|
21
|
+
opted out. Now only the modal screenshot is taken by default; passing
|
|
22
|
+
`--screenshots` additionally captures `after-reject.png` and `after-accept.png`
|
|
23
|
+
(full viewport, as before). The `screenshots` field in `ScanOptions` / the
|
|
24
|
+
programmatic API retains the same type (`boolean`) with updated semantics:
|
|
25
|
+
`false` (default) = modal only; `true` = modal + after-reject + after-accept.
|
|
26
|
+
|
|
27
|
+
- 6a71a18: Add multi-language consent button detection (de, es, it, nl, pl, pt).
|
|
28
|
+
|
|
29
|
+
Previously, button classification only covered French and English, causing
|
|
30
|
+
false "no reject button" findings on sites served in other EU locales.
|
|
31
|
+
|
|
32
|
+
The fix has two parts:
|
|
33
|
+
|
|
34
|
+
1. **Locale-aware pattern map** — `ACCEPT_PATTERNS` / `REJECT_PATTERNS` /
|
|
35
|
+
`PREFERENCES_PATTERNS` are replaced by a `PATTERNS_BY_LOCALE` map keyed by
|
|
36
|
+
BCP 47 primary subtag, covering `en`, `fr`, `de`, `es`, `it`, `nl`, `pl`,
|
|
37
|
+
`pt`. Polish patterns use a negative lookbehind instead of `\b` because
|
|
38
|
+
several Polish words end in non-ASCII characters (ć, ę, ó) that fall
|
|
39
|
+
outside JS `\w`.
|
|
40
|
+
|
|
41
|
+
2. **`<html lang>` detection** — `detectConsentModal` now reads the page's
|
|
42
|
+
declared language from `document.documentElement.lang` and normalises it to
|
|
43
|
+
a primary subtag (e.g. `"de-DE"` → `"de"`). When the language is
|
|
44
|
+
recognised, only that locale's patterns plus English (universal fallback)
|
|
45
|
+
are tested. When the language is missing or unsupported, all available
|
|
46
|
+
patterns are tried — preserving the previous behaviour for unknown pages.
|
|
47
|
+
|
|
48
|
+
The public export `classifyButtonText(text, lang)` is added for testing and
|
|
49
|
+
programmatic use; 56 new unit tests cover every supported locale.
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- ceed240: Add unit tests for `computeContrastRatio`, `parseRgb`, and `relativeLuminance`.
|
|
54
|
+
|
|
55
|
+
These three pure functions in `consent-modal.ts` were previously only exercised
|
|
56
|
+
indirectly through the E2E suite. The new test file
|
|
57
|
+
(`tests/scanner/contrast-ratio.test.ts`) covers the happy path, edge cases
|
|
58
|
+
(identical colours, fully transparent rgba, non-integer ratios), and documents
|
|
59
|
+
the known limitations — named colours (`white`, `black`) and hex values (`#fff`)
|
|
60
|
+
return `null` until the parser is extended.
|
|
61
|
+
|
|
62
|
+
The functions are now exported so they can be imported by the test suite without
|
|
63
|
+
moving them to a separate module.
|
|
64
|
+
|
|
65
|
+
- df24a36: Fix consent modal detection for CMPs that start hidden (e.g. Axeptio).
|
|
66
|
+
|
|
67
|
+
`MODAL_SELECTORS` was a single flat list where every candidate was required to
|
|
68
|
+
pass `isVisible()`. CMPs such as Axeptio inject their overlay as `display:none`
|
|
69
|
+
during initialisation and reveal it via JS animation a few hundred milliseconds
|
|
70
|
+
later. The visibility check caused the scanner to skip `#axeptio_overlay` and
|
|
71
|
+
fall through to the first matching generic heuristic (e.g. `[id*='consent']`),
|
|
72
|
+
which could be a completely unrelated element.
|
|
73
|
+
|
|
74
|
+
The list is now split into two:
|
|
75
|
+
|
|
76
|
+
- **`CMP_SELECTORS`** — precise, platform-specific identifiers. DOM presence
|
|
77
|
+
alone is treated as a reliable signal. Once the element is found the scanner
|
|
78
|
+
waits up to 3 s for it to become visible (so button extraction sees an
|
|
79
|
+
interactive state) but proceeds regardless, preventing a slow CMP from
|
|
80
|
+
silently falling back to a wrong heuristic.
|
|
81
|
+
- **`HEURISTIC_SELECTORS`** — broad patterns that could match unrelated
|
|
82
|
+
elements. Visibility is still required to avoid false positives.
|
|
83
|
+
|
|
84
|
+
- f9efe0b: Normalise button text whitespace before classification.
|
|
85
|
+
|
|
86
|
+
`classifyButtonType` previously received raw `textContent` that had only been
|
|
87
|
+
`.trim()`-ed. CMP HTML templates frequently embed ` ` (U+00A0), newlines,
|
|
88
|
+
or tabs inside button labels, causing pattern matching to silently fail.
|
|
89
|
+
|
|
90
|
+
A `normalizeText` helper now collapses any whitespace sequence (including
|
|
91
|
+
U+00A0 and all Unicode spaces covered by JS `\s`) into a single ASCII space
|
|
92
|
+
before the regex is tested. The normalisation is applied in two places:
|
|
93
|
+
|
|
94
|
+
- `classifyButtonText` (public export) — defensive normalisation of any caller-
|
|
95
|
+
provided string.
|
|
96
|
+
- `extractButtons` — the raw `el.textContent()` result is normalised before
|
|
97
|
+
being stored in `ConsentButton.text` and before classification, so the
|
|
98
|
+
report also shows the cleaned label.
|
|
99
|
+
|
|
3
100
|
## 3.3.0
|
|
4
101
|
|
|
5
102
|
### Minor Changes
|
package/NEXT_STEPS.md
CHANGED
|
@@ -14,25 +14,6 @@ Ideas and improvement areas for `gdpr-cookie-scanner`. Not a roadmap — pick wh
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## Language support
|
|
18
|
-
|
|
19
|
-
Currently only French and English patterns are covered. A lot of EU sites served in other locales will get false "no reject button" results.
|
|
20
|
-
|
|
21
|
-
Languages to add at minimum (ordered by EU population / GDPR enforcement activity):
|
|
22
|
-
|
|
23
|
-
| Locale | Reject examples | Accept examples |
|
|
24
|
-
| ------- | ------------------------ | -------------------------------- |
|
|
25
|
-
| `de-DE` | Ablehnen, Alle ablehnen | Alle akzeptieren, Zustimmen |
|
|
26
|
-
| `es-ES` | Rechazar, Rechazar todo | Aceptar, Aceptar todo |
|
|
27
|
-
| `it-IT` | Rifiuta, Rifiuta tutto | Accetta, Accetta tutto |
|
|
28
|
-
| `nl-NL` | Weigeren, Alles weigeren | Accepteren, Alles accepteren |
|
|
29
|
-
| `pl-PL` | Odrzuć, Odrzuć wszystkie | Zaakceptuj, Zaakceptuj wszystkie |
|
|
30
|
-
| `pt-PT` | Rejeitar, Rejeitar tudo | Aceitar, Aceitar tudo |
|
|
31
|
-
|
|
32
|
-
The patterns live in `src/scanner/consent-modal.ts`. A locale-aware pattern map keyed by BCP 47 tag would be cleaner than one giant regex.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
17
|
## Dark pattern detection gaps
|
|
37
18
|
|
|
38
19
|
Patterns that are explicitly listed in CNIL/EDPB guidelines but not yet detected:
|
|
@@ -81,12 +62,6 @@ Patterns that are explicitly listed in CNIL/EDPB guidelines but not yet detected
|
|
|
81
62
|
|
|
82
63
|
- **Report output tests** — no tests currently validate the content of generated Markdown, HTML, or JSON files. Add snapshot tests for at least the JSON output and spot-checks for key sections in HTML.
|
|
83
64
|
|
|
84
|
-
- **Contrast ratio unit tests** — `computeContrastRatio` is pure logic but only exercised through E2E tests. Deserves a dedicated unit test file covering edge cases (identical colours, transparent backgrounds, named/hex inputs once supported).
|
|
85
|
-
|
|
86
|
-
- **Locale-specific button pattern tests** — once multi-language patterns land, add a test case per locale to prevent regressions.
|
|
87
|
-
|
|
88
|
-
- **Performance baseline** — the E2E suite takes ~2 minutes. As features grow, tracking suite duration in CI would help catch regressions early.
|
|
89
|
-
|
|
90
65
|
---
|
|
91
66
|
|
|
92
67
|
## Infrastructure
|
package/README.md
CHANGED
|
@@ -63,18 +63,18 @@ gdpr-scan scan <url> [options]
|
|
|
63
63
|
|
|
64
64
|
### Options
|
|
65
65
|
|
|
66
|
-
| Option | Default | Description
|
|
67
|
-
| ------------------------ | ---------------- |
|
|
68
|
-
| `-o, --output <dir>` | `./gdpr-reports` | Output directory for the report
|
|
69
|
-
| `-t, --timeout <ms>` | `30000` | Navigation timeout
|
|
70
|
-
| `-f, --format <formats>` | `html` | Output formats: `md`, `html`, `json`, `pdf` (comma-separated)
|
|
71
|
-
| `--viewport <preset>` | `desktop` | Viewport preset: `desktop` (1280×900), `tablet` (768×1024), `mobile` (390×844)
|
|
72
|
-
| `--fail-on <threshold>` | `F` | Exit with code 1 if grade is below this letter (`A`/`B`/`C`/`D`/`F`) or score is below this number (`0–100`)
|
|
73
|
-
| `--json-summary` | — | Emit a machine-readable JSON line to stdout after the scan (parseable by `jq`)
|
|
74
|
-
| `--strict` | — | Treat unrecognised cookies and unknown third-party requests as requiring consent
|
|
75
|
-
| `--
|
|
76
|
-
| `-l, --locale <locale>` | `fr-FR` | Browser locale
|
|
77
|
-
| `-v, --verbose` | — | Show full stack trace on error
|
|
66
|
+
| Option | Default | Description |
|
|
67
|
+
| ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `-o, --output <dir>` | `./gdpr-reports` | Output directory for the report |
|
|
69
|
+
| `-t, --timeout <ms>` | `30000` | Navigation timeout |
|
|
70
|
+
| `-f, --format <formats>` | `html` | Output formats: `md`, `html`, `json`, `pdf` (comma-separated) |
|
|
71
|
+
| `--viewport <preset>` | `desktop` | Viewport preset: `desktop` (1280×900), `tablet` (768×1024), `mobile` (390×844) |
|
|
72
|
+
| `--fail-on <threshold>` | `F` | Exit with code 1 if grade is below this letter (`A`/`B`/`C`/`D`/`F`) or score is below this number (`0–100`) |
|
|
73
|
+
| `--json-summary` | — | Emit a machine-readable JSON line to stdout after the scan (parseable by `jq`) |
|
|
74
|
+
| `--strict` | — | Treat unrecognised cookies and unknown third-party requests as requiring consent |
|
|
75
|
+
| `--screenshots` | — | Also capture full-page screenshots after reject and accept interactions (the consent modal is always screenshotted when detected) |
|
|
76
|
+
| `-l, --locale <locale>` | `fr-FR` | Browser locale |
|
|
77
|
+
| `-v, --verbose` | — | Show full stack trace on error |
|
|
78
78
|
|
|
79
79
|
### Examples
|
|
80
80
|
|
|
@@ -85,8 +85,8 @@ gdpr-scan scan https://example.com
|
|
|
85
85
|
# With custom output directory
|
|
86
86
|
gdpr-scan scan https://example.com -o ./reports
|
|
87
87
|
|
|
88
|
-
# Scan in English
|
|
89
|
-
gdpr-scan scan https://example.com --locale en-US --
|
|
88
|
+
# Scan in English with full interaction screenshots (reject + accept)
|
|
89
|
+
gdpr-scan scan https://example.com --locale en-US --screenshots
|
|
90
90
|
|
|
91
91
|
# Generate a Markdown report instead
|
|
92
92
|
gdpr-scan scan https://example.com -f md
|
|
@@ -243,7 +243,7 @@ All fields of `ScanResult` — cookies, network requests, modal analysis, compli
|
|
|
243
243
|
const result = await scan("https://example.com", {
|
|
244
244
|
locale: "fr-FR", // browser locale, also controls report language
|
|
245
245
|
timeout: 60_000, // navigation timeout in ms (default: 30 000)
|
|
246
|
-
screenshots: true, // capture screenshots (
|
|
246
|
+
screenshots: true, // also capture after-reject and after-accept screenshots (modal is always screenshotted)
|
|
247
247
|
outputDir: "./reports", // where to save screenshots
|
|
248
248
|
verbose: false, // log scanner phases to stdout
|
|
249
249
|
viewport: "mobile", // 'desktop' (default) | 'tablet' | 'mobile'
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ program
|
|
|
16
16
|
.argument("<url>", "URL of the website to scan")
|
|
17
17
|
.option("-o, --output <dir>", "Output directory for the report", "./gdpr-reports")
|
|
18
18
|
.option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000")
|
|
19
|
-
.option("--
|
|
19
|
+
.option("--screenshots", "Capture full-page screenshots after reject and accept interactions (the consent modal is always screenshotted when detected)")
|
|
20
20
|
.option("-l, --locale <locale>", "Browser locale for language detection", "fr-FR")
|
|
21
21
|
.option("-v, --verbose", "Show detailed output", false)
|
|
22
22
|
.option("-f, --format <formats>", "Output formats: md, html, json, pdf (comma-separated)", "html")
|
|
@@ -54,7 +54,7 @@ program
|
|
|
54
54
|
url: normalizedUrl,
|
|
55
55
|
outputDir,
|
|
56
56
|
timeout: parseInt(opts.timeout, 10),
|
|
57
|
-
screenshots: opts.screenshots
|
|
57
|
+
screenshots: Boolean(opts.screenshots),
|
|
58
58
|
locale: opts.locale,
|
|
59
59
|
verbose: opts.verbose,
|
|
60
60
|
formats,
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,WAAW,CAAC;KACjB,WAAW,CAAC,mDAAmD,CAAC;KAChE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,sDAAsD,CAAC;KACnE,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAC;KAC/C,MAAM,CAAC,oBAAoB,EAAE,iCAAiC,EAAE,gBAAgB,CAAC;KACjF,MAAM,CAAC,oBAAoB,EAAE,oCAAoC,EAAE,OAAO,CAAC;KAC3E,MAAM,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,9 @@ import type { ScanResult, ViewportPreset } from "./types.js";
|
|
|
29
29
|
export interface ScanApiOptions {
|
|
30
30
|
/** Browser navigation timeout in ms. Default: 30 000. */
|
|
31
31
|
timeout?: number;
|
|
32
|
-
/**
|
|
32
|
+
/** Capture full-page screenshots after reject and accept interactions.
|
|
33
|
+
* The consent modal is always screenshotted (cropped to the element) when detected.
|
|
34
|
+
* Requires `outputDir`. Default: false. */
|
|
33
35
|
screenshots?: boolean;
|
|
34
36
|
/** Directory where screenshots (and optionally reports) are saved. */
|
|
35
37
|
outputDir?: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,YAAY,EACV,UAAU,EACV,WAAW,EACX,eAAe,EACf,YAAY,EACZ,aAAa,EACb,eAAe,EACf,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,YAAY,EACV,UAAU,EACV,WAAW,EACX,eAAe,EACf,YAAY,EACZ,aAAa,EACb,eAAe,EACf,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;gDAE4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,wGAAwG;IACxG,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiBzF"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAqBxD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAqBxD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA0B7C;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAW,EAAE,UAA0B,EAAE;IAClE,MAAM,aAAa,GACjB,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC;IAEnF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC;QAC1B,GAAG,EAAE,aAAa;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,MAAM;QAClC,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,KAAK;QACzC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,OAAO;QACjC,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,KAAK;QACjC,OAAO,EAAE,EAAE;QACX,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS;QACvC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,KAAK;KAChC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;AACvB,CAAC"}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import type { Page } from "playwright";
|
|
2
|
-
import type { ConsentModal } from "../types.js";
|
|
2
|
+
import type { ConsentModal, ConsentButtonType } from "../types.js";
|
|
3
3
|
import type { ScanOptions } from "../types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Find a privacy policy link within a given scope (modal selector) or the full page.
|
|
6
6
|
* Returns the absolute URL of the first matching link, or null.
|
|
7
7
|
*/
|
|
8
8
|
export declare function findPrivacyPolicyUrl(page: Page, scopeSelector?: string): Promise<string | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Classify a consent button label for a given page language.
|
|
11
|
+
* `lang` should be the BCP 47 primary subtag (e.g. "de", "fr") read from
|
|
12
|
+
* <html lang>, or null when the language is undetermined.
|
|
13
|
+
*/
|
|
14
|
+
export declare function classifyButtonText(text: string, lang: string | null): ConsentButtonType;
|
|
9
15
|
export declare function detectConsentModal(page: Page, options: ScanOptions): Promise<ConsentModal>;
|
|
16
|
+
/**
|
|
17
|
+
* Basic contrast ratio computation from RGB strings.
|
|
18
|
+
* Returns null if colors cannot be parsed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeContrastRatio(fg: string, bg: string): number | null;
|
|
21
|
+
export declare function parseRgb(color: string): [number, number, number] | null;
|
|
22
|
+
export declare function relativeLuminance([r, g, b]: [number, number, number]): number;
|
|
10
23
|
//# sourceMappingURL=consent-modal.d.ts.map
|
|
@@ -1 +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,
|
|
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,EAAkC,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEnG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA8E/C;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,EACV,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkCxB;AAkGD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,CAGvF;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA2GhG;AAwHD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU1E;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAIvE;AAED,wBAAgB,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAM7E"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Selectors for well-known CMP platforms.
|
|
3
|
+
* These are precise enough that DOM presence alone is a reliable signal —
|
|
4
|
+
* no visibility check required. Some CMPs (e.g. Axeptio) inject their
|
|
5
|
+
* container as display:none and reveal it after JS initialisation, so
|
|
6
|
+
* isVisible() would incorrectly skip them.
|
|
4
7
|
*/
|
|
5
|
-
const
|
|
6
|
-
// Well-known CMPs
|
|
8
|
+
const CMP_SELECTORS = [
|
|
7
9
|
"#axeptio_overlay",
|
|
8
10
|
"#axeptio-root",
|
|
9
11
|
"#CybotCookiebotDialog",
|
|
@@ -24,7 +26,13 @@ const MODAL_SELECTORS = [
|
|
|
24
26
|
"#cookie-banner",
|
|
25
27
|
"#cookie-notice",
|
|
26
28
|
"#cookie-law-info-bar",
|
|
27
|
-
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Generic heuristic selectors.
|
|
32
|
+
* These are broad enough to match unrelated elements, so a visibility
|
|
33
|
+
* check is required to avoid false positives.
|
|
34
|
+
*/
|
|
35
|
+
const HEURISTIC_SELECTORS = [
|
|
28
36
|
"[class*='cookie'][class*='banner']",
|
|
29
37
|
"[class*='cookie'][class*='modal']",
|
|
30
38
|
"[class*='cookie'][class*='popup']",
|
|
@@ -97,33 +105,137 @@ export async function findPrivacyPolicyUrl(page, scopeSelector) {
|
|
|
97
105
|
})
|
|
98
106
|
.catch(() => null);
|
|
99
107
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
]
|
|
108
|
+
/**
|
|
109
|
+
* Button label patterns keyed by BCP 47 primary-language subtag.
|
|
110
|
+
* English is always included as a fallback when the page language is known.
|
|
111
|
+
* When the language is unknown all locales are tested.
|
|
112
|
+
*/
|
|
113
|
+
const PATTERNS_BY_LOCALE = {
|
|
114
|
+
en: {
|
|
115
|
+
accept: /\b(accept|accept all|agree|ok|i accept|i agree|continue)\b/i,
|
|
116
|
+
reject: /\b(refuse|reject|reject all|deny|decline|no thanks|skip|opt[- ]out|continue without)\b/i,
|
|
117
|
+
preferences: /\b(manage|customize|customise|settings|options|choose|configure)\b/i,
|
|
118
|
+
},
|
|
119
|
+
fr: {
|
|
120
|
+
accept: /\b(accepter|acceptez|tout accepter|j'accepte|d'accord|continuer|valider|confirmer)\b/i,
|
|
121
|
+
reject: /\b(refus|refuser|tout refuser|rejeter|tout rejeter|non merci|continuer sans accepter)\b/i,
|
|
122
|
+
preferences: /\b(param[eè]tres|pr[eé]f[eé]rences|personnaliser|g[eé]rer|choisir)\b/i,
|
|
123
|
+
},
|
|
124
|
+
de: {
|
|
125
|
+
accept: /\b(akzeptieren|alle akzeptieren|zustimmen|einverstanden|annehmen|alle annehmen|ich stimme zu)\b/i,
|
|
126
|
+
reject: /\b(ablehnen|alle ablehnen|abweisen|nicht zustimmen|nein danke)\b/i,
|
|
127
|
+
preferences: /\b(einstellungen|anpassen|verwalten|konfigurieren|ausw[äa]hlen|mehr optionen)\b/i,
|
|
128
|
+
},
|
|
129
|
+
es: {
|
|
130
|
+
accept: /\b(aceptar|aceptar todo|acepto|estoy de acuerdo)\b/i,
|
|
131
|
+
reject: /\b(rechazar|rechazar todo|denegar|no gracias|continuar sin aceptar)\b/i,
|
|
132
|
+
preferences: /\b(ajustes|configurar|gestionar|opciones|personalizar|preferencias)\b/i,
|
|
133
|
+
},
|
|
134
|
+
it: {
|
|
135
|
+
accept: /\b(accetta|accetta tutto|accetto|acconsento|acconsento a tutto)\b/i,
|
|
136
|
+
reject: /\b(rifiuta|rifiuta tutto|nega|no grazie)\b/i,
|
|
137
|
+
preferences: /\b(impostazioni|personalizza|gestisci|opzioni|configura|preferenze)\b/i,
|
|
138
|
+
},
|
|
139
|
+
nl: {
|
|
140
|
+
accept: /\b(accepteren|alles accepteren|akkoord|ik ga akkoord)\b/i,
|
|
141
|
+
reject: /\b(weigeren|alles weigeren|afwijzen|nee bedankt)\b/i,
|
|
142
|
+
preferences: /\b(instellingen|aanpassen|beheren|instellen|voorkeuren)\b/i,
|
|
143
|
+
},
|
|
144
|
+
pl: {
|
|
145
|
+
// No \b anchors: Polish words often end in non-ASCII characters (ć, ę, ó)
|
|
146
|
+
// which are outside JS \w, so \b would not match at the word end.
|
|
147
|
+
// Negative lookbehind prevents "zgadzam się" from matching inside "nie zgadzam się".
|
|
148
|
+
accept: /zaakceptuj( wszystkie)?|(?<!nie\s)zgadzam si[eę]|akceptuj[eę]/i,
|
|
149
|
+
reject: /odrzuć( wszystkie)?|nie zgadzam si[eę]|odm[oó]w/i,
|
|
150
|
+
preferences: /ustawienia|dostosuj|zarz[aą]dzaj|opcje|skonfiguruj|preferencje/i,
|
|
151
|
+
},
|
|
152
|
+
pt: {
|
|
153
|
+
accept: /\b(aceitar|aceitar tudo|aceito|concordo)\b/i,
|
|
154
|
+
reject: /\b(rejeitar|rejeitar tudo|recusar|n[aã]o obrigado)\b/i,
|
|
155
|
+
preferences: /\b(configura[çc][oõ]es|personalizar|gerir|op[çc][oõ]es|defini[çc][oõ]es|prefer[eê]ncias)\b/i,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Build the applicable accept/reject/preferences pattern lists for a given
|
|
160
|
+
* primary-language tag detected from the page's <html lang> attribute.
|
|
161
|
+
*
|
|
162
|
+
* - Known language → locale patterns + English fallback
|
|
163
|
+
* - Unknown / missing → all available patterns (most robust)
|
|
164
|
+
*/
|
|
165
|
+
function resolveButtonPatterns(lang) {
|
|
166
|
+
if (!lang || !(lang in PATTERNS_BY_LOCALE)) {
|
|
167
|
+
const all = Object.values(PATTERNS_BY_LOCALE);
|
|
168
|
+
return {
|
|
169
|
+
accept: all.map((p) => p.accept),
|
|
170
|
+
reject: all.map((p) => p.reject),
|
|
171
|
+
preferences: all.map((p) => p.preferences),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const locale = PATTERNS_BY_LOCALE[lang];
|
|
175
|
+
const en = PATTERNS_BY_LOCALE.en;
|
|
176
|
+
const sets = lang === "en" ? [locale] : [locale, en];
|
|
177
|
+
return {
|
|
178
|
+
accept: sets.map((p) => p.accept),
|
|
179
|
+
reject: sets.map((p) => p.reject),
|
|
180
|
+
preferences: sets.map((p) => p.preferences),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Collapse any whitespace sequence (including / \u00A0, \n, \t, …)
|
|
185
|
+
* into a single ASCII space and strip leading/trailing whitespace.
|
|
186
|
+
* In JS, \s already covers \u00A0 and other Unicode spaces.
|
|
187
|
+
*/
|
|
188
|
+
function normalizeText(raw) {
|
|
189
|
+
return raw.replace(/\s+/g, " ").trim();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Classify a consent button label for a given page language.
|
|
193
|
+
* `lang` should be the BCP 47 primary subtag (e.g. "de", "fr") read from
|
|
194
|
+
* <html lang>, or null when the language is undetermined.
|
|
195
|
+
*/
|
|
196
|
+
export function classifyButtonText(text, lang) {
|
|
197
|
+
const { accept, reject, preferences } = resolveButtonPatterns(lang);
|
|
198
|
+
return classifyButtonType(normalizeText(text), accept, reject, preferences);
|
|
199
|
+
}
|
|
109
200
|
export async function detectConsentModal(page, options) {
|
|
110
|
-
// Try each selector until we find a visible modal
|
|
111
201
|
let foundSelector = null;
|
|
112
|
-
|
|
202
|
+
// Step 1 — specific CMP selectors: presence in DOM is sufficient.
|
|
203
|
+
// Some CMPs (e.g. Axeptio) insert their container as display:none and
|
|
204
|
+
// animate it in after JS initialisation. Requiring isVisible() would
|
|
205
|
+
// skip them and fall through to a wrong generic selector.
|
|
206
|
+
// Once the element is found we wait briefly (up to 3 s) for it to
|
|
207
|
+
// become visible so button extraction sees an interactive state.
|
|
208
|
+
for (const selector of CMP_SELECTORS) {
|
|
113
209
|
try {
|
|
114
210
|
const element = await page.$(selector);
|
|
115
211
|
if (!element)
|
|
116
212
|
continue;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
213
|
+
await page.waitForSelector(selector, { state: "visible", timeout: 3000 }).catch(() => { });
|
|
214
|
+
foundSelector = selector;
|
|
215
|
+
break;
|
|
122
216
|
}
|
|
123
217
|
catch {
|
|
124
218
|
continue;
|
|
125
219
|
}
|
|
126
220
|
}
|
|
221
|
+
// Step 2 — generic heuristics: require visibility to avoid false positives.
|
|
222
|
+
if (!foundSelector) {
|
|
223
|
+
for (const selector of HEURISTIC_SELECTORS) {
|
|
224
|
+
try {
|
|
225
|
+
const element = await page.$(selector);
|
|
226
|
+
if (!element)
|
|
227
|
+
continue;
|
|
228
|
+
const isVisible = await element.isVisible();
|
|
229
|
+
if (isVisible) {
|
|
230
|
+
foundSelector = selector;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
127
239
|
// Fallback: look for any large fixed/sticky element with cookie-related text
|
|
128
240
|
if (!foundSelector) {
|
|
129
241
|
foundSelector = await page.evaluate(() => {
|
|
@@ -160,10 +272,13 @@ export async function detectConsentModal(page, options) {
|
|
|
160
272
|
privacyPolicyUrl: null,
|
|
161
273
|
};
|
|
162
274
|
}
|
|
275
|
+
// Detect the page's declared language for locale-aware button classification
|
|
276
|
+
const rawLang = await page.$eval("html", (el) => el.lang ?? "").catch(() => "");
|
|
277
|
+
const pageLang = rawLang.split("-")[0].toLowerCase() || null;
|
|
163
278
|
// Extract modal text
|
|
164
279
|
const modalText = await page.$eval(foundSelector, (el) => el.textContent ?? "").catch(() => "");
|
|
165
280
|
// Find all buttons and interactive elements within the modal
|
|
166
|
-
const buttons = await extractButtons(page, foundSelector);
|
|
281
|
+
const buttons = await extractButtons(page, foundSelector, pageLang);
|
|
167
282
|
// Find checkboxes / toggles
|
|
168
283
|
const checkboxes = await extractCheckboxes(page, foundSelector);
|
|
169
284
|
// Detect if there are nested layers (e.g., "more options" behind a click)
|
|
@@ -182,12 +297,13 @@ export async function detectConsentModal(page, options) {
|
|
|
182
297
|
privacyPolicyUrl,
|
|
183
298
|
};
|
|
184
299
|
}
|
|
185
|
-
async function extractButtons(page, modalSelector) {
|
|
300
|
+
async function extractButtons(page, modalSelector, lang) {
|
|
301
|
+
const { accept, reject, preferences } = resolveButtonPatterns(lang);
|
|
186
302
|
const buttonEls = await page.$$(`${modalSelector} button, ${modalSelector} [role="button"], ${modalSelector} a[href="#"]`);
|
|
187
303
|
const buttons = [];
|
|
188
304
|
for (const el of buttonEls) {
|
|
189
305
|
try {
|
|
190
|
-
const text = ((await el.textContent()) ?? "")
|
|
306
|
+
const text = normalizeText((await el.textContent()) ?? "");
|
|
191
307
|
if (!text)
|
|
192
308
|
continue;
|
|
193
309
|
const isVisible = await el.isVisible();
|
|
@@ -200,7 +316,7 @@ async function extractButtons(page, modalSelector) {
|
|
|
200
316
|
color: style.color,
|
|
201
317
|
};
|
|
202
318
|
});
|
|
203
|
-
const type = classifyButtonType(text);
|
|
319
|
+
const type = classifyButtonType(text, accept, reject, preferences);
|
|
204
320
|
// Build a unique selector for this button
|
|
205
321
|
const selector = await el.evaluate((node) => {
|
|
206
322
|
const el = node;
|
|
@@ -264,12 +380,12 @@ async function extractCheckboxes(page, modalSelector) {
|
|
|
264
380
|
}, modalSelector)
|
|
265
381
|
.catch(() => []);
|
|
266
382
|
}
|
|
267
|
-
function classifyButtonType(text) {
|
|
268
|
-
if (
|
|
383
|
+
function classifyButtonType(text, accept, reject, preferences) {
|
|
384
|
+
if (accept.some((p) => p.test(text)))
|
|
269
385
|
return "accept";
|
|
270
|
-
if (
|
|
386
|
+
if (reject.some((p) => p.test(text)))
|
|
271
387
|
return "reject";
|
|
272
|
-
if (
|
|
388
|
+
if (preferences.some((p) => p.test(text)))
|
|
273
389
|
return "preferences";
|
|
274
390
|
if (/\b(ferm|close|×|✕)\b/i.test(text))
|
|
275
391
|
return "close";
|
|
@@ -279,7 +395,7 @@ function classifyButtonType(text) {
|
|
|
279
395
|
* Basic contrast ratio computation from RGB strings.
|
|
280
396
|
* Returns null if colors cannot be parsed.
|
|
281
397
|
*/
|
|
282
|
-
function computeContrastRatio(fg, bg) {
|
|
398
|
+
export function computeContrastRatio(fg, bg) {
|
|
283
399
|
const fgRgb = parseRgb(fg);
|
|
284
400
|
const bgRgb = parseRgb(bg);
|
|
285
401
|
if (!fgRgb || !bgRgb)
|
|
@@ -290,13 +406,13 @@ function computeContrastRatio(fg, bg) {
|
|
|
290
406
|
const darker = Math.min(fgL, bgL);
|
|
291
407
|
return parseFloat(((lighter + 0.05) / (darker + 0.05)).toFixed(2));
|
|
292
408
|
}
|
|
293
|
-
function parseRgb(color) {
|
|
409
|
+
export function parseRgb(color) {
|
|
294
410
|
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
295
411
|
if (!match)
|
|
296
412
|
return null;
|
|
297
413
|
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
298
414
|
}
|
|
299
|
-
function relativeLuminance([r, g, b]) {
|
|
415
|
+
export function relativeLuminance([r, g, b]) {
|
|
300
416
|
const toLinear = (c) => {
|
|
301
417
|
const s = c / 255;
|
|
302
418
|
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|