@slashgear/gdpr-cookie-scanner 3.3.0 → 3.5.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/.github/workflows/update-cookie-db.yml +94 -0
- package/CHANGELOG.md +130 -0
- package/CLAUDE.md +18 -0
- package/NEXT_STEPS.md +0 -25
- package/README.md +25 -24
- package/dist/classifiers/cookie-lookup.d.ts +8 -0
- package/dist/classifiers/cookie-lookup.d.ts.map +1 -0
- package/dist/classifiers/cookie-lookup.js +50 -0
- package/dist/classifiers/cookie-lookup.js.map +1 -0
- package/dist/cli.js +6 -5
- package/dist/cli.js.map +1 -1
- package/dist/data/open-cookie-database.json +25614 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/report/generator.d.ts +1 -0
- package/dist/report/generator.d.ts.map +1 -1
- package/dist/report/generator.js +100 -4
- package/dist/report/generator.js.map +1 -1
- package/dist/report/html.d.ts.map +1 -1
- package/dist/report/html.js +16 -2
- package/dist/report/html.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 +4 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -3
- package/scripts/copy-data.mjs +4 -0
- package/scripts/update-cookie-db.mjs +15 -0
- package/src/classifiers/cookie-lookup.ts +72 -0
- package/src/cli.ts +13 -5
- package/src/data/open-cookie-database.json +25614 -0
- package/src/index.ts +3 -1
- package/src/report/generator.ts +119 -6
- package/src/report/html.ts +16 -2
- package/src/scanner/consent-modal.ts +169 -32
- package/src/scanner/index.ts +11 -2
- package/src/types.ts +4 -1
- 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
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: Update Cookie DB
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# 1st of every month at 04:00 UTC
|
|
6
|
+
- cron: "0 4 1 * *"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
update:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
14
|
+
pull-requests: write
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- uses: pnpm/action-setup@v4
|
|
20
|
+
with:
|
|
21
|
+
version: latest
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: 24
|
|
26
|
+
cache: pnpm
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: pnpm install --frozen-lockfile
|
|
30
|
+
|
|
31
|
+
- name: Fetch Open Cookie Database
|
|
32
|
+
run: pnpm update:ocd
|
|
33
|
+
|
|
34
|
+
- name: Detect changes
|
|
35
|
+
id: diff
|
|
36
|
+
run: |
|
|
37
|
+
git diff --quiet src/data/open-cookie-database.json \
|
|
38
|
+
&& echo "changed=false" >> "$GITHUB_OUTPUT" \
|
|
39
|
+
|| echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
40
|
+
|
|
41
|
+
- name: Type-check
|
|
42
|
+
if: steps.diff.outputs.changed == 'true'
|
|
43
|
+
run: pnpm typecheck
|
|
44
|
+
|
|
45
|
+
- name: Format
|
|
46
|
+
if: steps.diff.outputs.changed == 'true'
|
|
47
|
+
run: pnpm format
|
|
48
|
+
|
|
49
|
+
- name: Create PR
|
|
50
|
+
if: steps.diff.outputs.changed == 'true'
|
|
51
|
+
env:
|
|
52
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
53
|
+
run: |
|
|
54
|
+
BRANCH="chore/cookie-db-$(date +%Y%m)"
|
|
55
|
+
DATE="$(date +%Y-%m-%d)"
|
|
56
|
+
SLUG="cookie-db-update-${DATE}"
|
|
57
|
+
|
|
58
|
+
git config user.name "github-actions[bot]"
|
|
59
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
60
|
+
git checkout -b "$BRANCH"
|
|
61
|
+
|
|
62
|
+
# Write changeset
|
|
63
|
+
cat > ".changeset/${SLUG}.md" << EOF
|
|
64
|
+
---
|
|
65
|
+
"@slashgear/gdpr-cookie-scanner": patch
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
chore: monthly Open Cookie Database update
|
|
69
|
+
EOF
|
|
70
|
+
|
|
71
|
+
git add src/data/open-cookie-database.json ".changeset/${SLUG}.md"
|
|
72
|
+
git commit -m "chore: update Open Cookie Database"
|
|
73
|
+
git push origin "$BRANCH"
|
|
74
|
+
|
|
75
|
+
gh pr create \
|
|
76
|
+
--title "chore: monthly Open Cookie Database update ($(date +%B %Y))" \
|
|
77
|
+
--body "$(cat <<'BODY'
|
|
78
|
+
## Summary
|
|
79
|
+
|
|
80
|
+
Automated monthly update of `src/data/open-cookie-database.json`.
|
|
81
|
+
|
|
82
|
+
- **Source**: [Open Cookie Database](https://github.com/jkwakman/Open-Cookie-Database) (Apache 2.0)
|
|
83
|
+
- Cookie descriptions are used to pre-fill the Description column in reports.
|
|
84
|
+
|
|
85
|
+
## Test plan
|
|
86
|
+
|
|
87
|
+
- [ ] `pnpm typecheck` passes (validated by CI)
|
|
88
|
+
- [ ] `pnpm build` produces `dist/data/open-cookie-database.json`
|
|
89
|
+
- [ ] Spot-check a few cookie descriptions in the updated file
|
|
90
|
+
|
|
91
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
92
|
+
BODY
|
|
93
|
+
)" \
|
|
94
|
+
--base main
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,135 @@
|
|
|
1
1
|
# @slashgear/gdpr-cookie-scanner
|
|
2
2
|
|
|
3
|
+
## 3.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f41d0a0: feat: pre-fill cookie descriptions from Open Cookie Database and add CSV output format
|
|
8
|
+
|
|
9
|
+
Cookie reports now automatically include descriptions, platform names, retention
|
|
10
|
+
periods, and privacy-policy links for ~2 000+ recognised cookies, sourced from the
|
|
11
|
+
Open Cookie Database (Apache 2.0, vendored at `src/data/open-cookie-database.json`).
|
|
12
|
+
|
|
13
|
+
Previously the Description column in the Markdown cookie inventory was left as a
|
|
14
|
+
`<!-- fill in -->` placeholder. It is now pre-populated whenever the OCD contains a
|
|
15
|
+
matching entry (exact name or wildcard prefix), with the placeholder kept only for
|
|
16
|
+
unrecognised cookies.
|
|
17
|
+
|
|
18
|
+
The same enrichment is applied to the HTML report (new Description column in every
|
|
19
|
+
cookie table, with the platform and privacy-policy link surfaced in a tooltip) and to
|
|
20
|
+
the new `csv` output format (`gdpr-cookies-*.csv`), which includes all cookie
|
|
21
|
+
metadata plus the OCD fields in a machine-readable file suitable for DPA submissions
|
|
22
|
+
or spreadsheet review.
|
|
23
|
+
|
|
24
|
+
A `pnpm update:ocd` script and a monthly GitHub Actions workflow
|
|
25
|
+
(`.github/workflows/update-cookie-db.yml`) keep the vendored database up to date.
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- b88dad5: fix: truncate long cookie names with ellipsis in HTML cookie tables
|
|
30
|
+
|
|
31
|
+
Cookie names that contain URLs or other unusually long strings (e.g. Optimizely
|
|
32
|
+
session keys) were breaking the table layout in the HTML report. The Name column
|
|
33
|
+
now clamps to 220 px with `text-overflow: ellipsis`; hovering the cell reveals
|
|
34
|
+
the full name via the `title` attribute.
|
|
35
|
+
|
|
36
|
+
## 3.4.0
|
|
37
|
+
|
|
38
|
+
### Minor Changes
|
|
39
|
+
|
|
40
|
+
- 39794dc: Crop consent modal screenshot to the element; make after-reject/accept screenshots opt-in.
|
|
41
|
+
|
|
42
|
+
Two behaviour changes:
|
|
43
|
+
|
|
44
|
+
**Modal screenshot is now always captured (cropped)**
|
|
45
|
+
The consent modal screenshot (`modal-initial.png`) is taken whenever a modal
|
|
46
|
+
is detected and `outputDir` is set, regardless of the `--screenshots` flag.
|
|
47
|
+
The screenshot is clipped to the bounding box of the modal element instead of
|
|
48
|
+
capturing the full viewport, giving a tighter, more readable image. If the
|
|
49
|
+
bounding box cannot be determined (rare), it falls back to the viewport
|
|
50
|
+
screenshot.
|
|
51
|
+
|
|
52
|
+
**`--no-screenshots` replaced by `--screenshots`**
|
|
53
|
+
Previously all three screenshots were enabled by default and `--no-screenshots`
|
|
54
|
+
opted out. Now only the modal screenshot is taken by default; passing
|
|
55
|
+
`--screenshots` additionally captures `after-reject.png` and `after-accept.png`
|
|
56
|
+
(full viewport, as before). The `screenshots` field in `ScanOptions` / the
|
|
57
|
+
programmatic API retains the same type (`boolean`) with updated semantics:
|
|
58
|
+
`false` (default) = modal only; `true` = modal + after-reject + after-accept.
|
|
59
|
+
|
|
60
|
+
- 6a71a18: Add multi-language consent button detection (de, es, it, nl, pl, pt).
|
|
61
|
+
|
|
62
|
+
Previously, button classification only covered French and English, causing
|
|
63
|
+
false "no reject button" findings on sites served in other EU locales.
|
|
64
|
+
|
|
65
|
+
The fix has two parts:
|
|
66
|
+
|
|
67
|
+
1. **Locale-aware pattern map** — `ACCEPT_PATTERNS` / `REJECT_PATTERNS` /
|
|
68
|
+
`PREFERENCES_PATTERNS` are replaced by a `PATTERNS_BY_LOCALE` map keyed by
|
|
69
|
+
BCP 47 primary subtag, covering `en`, `fr`, `de`, `es`, `it`, `nl`, `pl`,
|
|
70
|
+
`pt`. Polish patterns use a negative lookbehind instead of `\b` because
|
|
71
|
+
several Polish words end in non-ASCII characters (ć, ę, ó) that fall
|
|
72
|
+
outside JS `\w`.
|
|
73
|
+
|
|
74
|
+
2. **`<html lang>` detection** — `detectConsentModal` now reads the page's
|
|
75
|
+
declared language from `document.documentElement.lang` and normalises it to
|
|
76
|
+
a primary subtag (e.g. `"de-DE"` → `"de"`). When the language is
|
|
77
|
+
recognised, only that locale's patterns plus English (universal fallback)
|
|
78
|
+
are tested. When the language is missing or unsupported, all available
|
|
79
|
+
patterns are tried — preserving the previous behaviour for unknown pages.
|
|
80
|
+
|
|
81
|
+
The public export `classifyButtonText(text, lang)` is added for testing and
|
|
82
|
+
programmatic use; 56 new unit tests cover every supported locale.
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- ceed240: Add unit tests for `computeContrastRatio`, `parseRgb`, and `relativeLuminance`.
|
|
87
|
+
|
|
88
|
+
These three pure functions in `consent-modal.ts` were previously only exercised
|
|
89
|
+
indirectly through the E2E suite. The new test file
|
|
90
|
+
(`tests/scanner/contrast-ratio.test.ts`) covers the happy path, edge cases
|
|
91
|
+
(identical colours, fully transparent rgba, non-integer ratios), and documents
|
|
92
|
+
the known limitations — named colours (`white`, `black`) and hex values (`#fff`)
|
|
93
|
+
return `null` until the parser is extended.
|
|
94
|
+
|
|
95
|
+
The functions are now exported so they can be imported by the test suite without
|
|
96
|
+
moving them to a separate module.
|
|
97
|
+
|
|
98
|
+
- df24a36: Fix consent modal detection for CMPs that start hidden (e.g. Axeptio).
|
|
99
|
+
|
|
100
|
+
`MODAL_SELECTORS` was a single flat list where every candidate was required to
|
|
101
|
+
pass `isVisible()`. CMPs such as Axeptio inject their overlay as `display:none`
|
|
102
|
+
during initialisation and reveal it via JS animation a few hundred milliseconds
|
|
103
|
+
later. The visibility check caused the scanner to skip `#axeptio_overlay` and
|
|
104
|
+
fall through to the first matching generic heuristic (e.g. `[id*='consent']`),
|
|
105
|
+
which could be a completely unrelated element.
|
|
106
|
+
|
|
107
|
+
The list is now split into two:
|
|
108
|
+
|
|
109
|
+
- **`CMP_SELECTORS`** — precise, platform-specific identifiers. DOM presence
|
|
110
|
+
alone is treated as a reliable signal. Once the element is found the scanner
|
|
111
|
+
waits up to 3 s for it to become visible (so button extraction sees an
|
|
112
|
+
interactive state) but proceeds regardless, preventing a slow CMP from
|
|
113
|
+
silently falling back to a wrong heuristic.
|
|
114
|
+
- **`HEURISTIC_SELECTORS`** — broad patterns that could match unrelated
|
|
115
|
+
elements. Visibility is still required to avoid false positives.
|
|
116
|
+
|
|
117
|
+
- f9efe0b: Normalise button text whitespace before classification.
|
|
118
|
+
|
|
119
|
+
`classifyButtonType` previously received raw `textContent` that had only been
|
|
120
|
+
`.trim()`-ed. CMP HTML templates frequently embed ` ` (U+00A0), newlines,
|
|
121
|
+
or tabs inside button labels, causing pattern matching to silently fail.
|
|
122
|
+
|
|
123
|
+
A `normalizeText` helper now collapses any whitespace sequence (including
|
|
124
|
+
U+00A0 and all Unicode spaces covered by JS `\s`) into a single ASCII space
|
|
125
|
+
before the regex is tested. The normalisation is applied in two places:
|
|
126
|
+
|
|
127
|
+
- `classifyButtonText` (public export) — defensive normalisation of any caller-
|
|
128
|
+
provided string.
|
|
129
|
+
- `extractButtons` — the raw `el.textContent()` result is normalised before
|
|
130
|
+
being stored in `ConsentButton.text` and before classification, so the
|
|
131
|
+
report also shows the cleaned label.
|
|
132
|
+
|
|
3
133
|
## 3.3.0
|
|
4
134
|
|
|
5
135
|
### Minor Changes
|
package/CLAUDE.md
CHANGED
|
@@ -85,3 +85,21 @@ Grade thresholds: A ≥ 90, B ≥ 75, C ≥ 55, D ≥ 35, F < 35. Exit code 1 on
|
|
|
85
85
|
### Module system
|
|
86
86
|
|
|
87
87
|
The project uses `"type": "module"` with `"moduleResolution": "NodeNext"`. All local imports **must** include the `.js` extension even for `.ts` source files.
|
|
88
|
+
|
|
89
|
+
## Cookie database (Open Cookie Database)
|
|
90
|
+
|
|
91
|
+
The file `src/data/open-cookie-database.json` is vendored from
|
|
92
|
+
https://github.com/jkwakman/Open-Cookie-Database (Apache 2.0).
|
|
93
|
+
|
|
94
|
+
To update manually:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pnpm update:ocd # fetches latest JSON, overwrites src/data/open-cookie-database.json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A GitHub Actions workflow (`.github/workflows/update-cookie-db.yml`) runs this
|
|
101
|
+
automatically on the 1st of each month and opens a PR if the file changed.
|
|
102
|
+
|
|
103
|
+
The lookup module (`src/classifiers/cookie-lookup.ts`) builds an exact-match index
|
|
104
|
+
and a wildcard-prefix index at startup. `src/data/` is copied to `dist/data/` by
|
|
105
|
+
`scripts/copy-data.mjs` as part of the `pnpm build` step.
|
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`, `csv` (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,14 +85,14 @@ 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
|
|
93
93
|
|
|
94
94
|
# Generate all formats at once
|
|
95
|
-
gdpr-scan scan https://example.com -f md,html,json,pdf
|
|
95
|
+
gdpr-scan scan https://example.com -f md,html,json,pdf,csv
|
|
96
96
|
|
|
97
97
|
# Scan with a mobile viewport (390×844, iPhone UA)
|
|
98
98
|
gdpr-scan scan https://example.com --viewport mobile
|
|
@@ -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'
|
|
@@ -262,7 +262,7 @@ const result = await scan("https://example.com", { locale: "fr-FR" });
|
|
|
262
262
|
const generator = new ReportGenerator({
|
|
263
263
|
url: result.url,
|
|
264
264
|
outputDir: "./reports",
|
|
265
|
-
formats: ["html", "json"], // 'md' | 'html' | 'json' | 'pdf'
|
|
265
|
+
formats: ["html", "json"], // 'md' | 'html' | 'json' | 'pdf' | 'csv'
|
|
266
266
|
locale: "fr-FR",
|
|
267
267
|
timeout: 30_000,
|
|
268
268
|
screenshots: false,
|
|
@@ -300,14 +300,15 @@ A real Chromium browser loads the page, interacts with the consent modal (reject
|
|
|
300
300
|
|
|
301
301
|
## Generated reports
|
|
302
302
|
|
|
303
|
-
Each scan produces up to
|
|
303
|
+
Each scan produces up to 5 file types in `<output-dir>/<hostname>/`:
|
|
304
304
|
|
|
305
|
-
| Format | Files | Description
|
|
306
|
-
| ------ | -------------------------------------------------------------- |
|
|
307
|
-
| `md` | `gdpr-report-*.md`, `gdpr-checklist-*.md`, `gdpr-cookies-*.md` | Main compliance report, per-rule checklist with legal references, and deduplicated cookie inventory
|
|
308
|
-
| `html` | `gdpr-report-*.html` | Self-contained styled report — grade badge, score cards, dark-pattern issues, cookie and tracker tables. Opens in any browser, no dependencies
|
|
309
|
-
| `json` | `gdpr-report-*.json` | Full raw scan result for programmatic processing or CI integration
|
|
310
|
-
| `pdf` | `gdpr-report-*.pdf` | PDF built from the Markdown reports via Playwright
|
|
305
|
+
| Format | Files | Description |
|
|
306
|
+
| ------ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
307
|
+
| `md` | `gdpr-report-*.md`, `gdpr-checklist-*.md`, `gdpr-cookies-*.md` | Main compliance report, per-rule checklist with legal references, and deduplicated cookie inventory |
|
|
308
|
+
| `html` | `gdpr-report-*.html` | Self-contained styled report — grade badge, score cards, dark-pattern issues, cookie and tracker tables. Opens in any browser, no dependencies |
|
|
309
|
+
| `json` | `gdpr-report-*.json` | Full raw scan result for programmatic processing or CI integration |
|
|
310
|
+
| `pdf` | `gdpr-report-*.pdf` | PDF built from the Markdown reports via Playwright |
|
|
311
|
+
| `csv` | `gdpr-cookies-*.csv` | Deduplicated cookie inventory with OCD descriptions, platform, retention period and privacy link — ready for spreadsheet review or DPA submissions |
|
|
311
312
|
|
|
312
313
|
All formats contain:
|
|
313
314
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-lookup.d.ts","sourceRoot":"","sources":["../../src/classifiers/cookie-lookup.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AA6CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAiB1D"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
// Build indexes once at module load time
|
|
5
|
+
const exactIndex = new Map();
|
|
6
|
+
const wildcardEntries = [];
|
|
7
|
+
function buildIndexes() {
|
|
8
|
+
const dbPath = join(dirname(fileURLToPath(import.meta.url)), "../data/open-cookie-database.json");
|
|
9
|
+
const raw = JSON.parse(readFileSync(dbPath, "utf-8"));
|
|
10
|
+
for (const entries of Object.values(raw)) {
|
|
11
|
+
for (const e of entries) {
|
|
12
|
+
if (!e.cookie)
|
|
13
|
+
continue;
|
|
14
|
+
const ocdEntry = {
|
|
15
|
+
description: e.description,
|
|
16
|
+
platform: e.dataController,
|
|
17
|
+
retentionPeriod: e.retentionPeriod,
|
|
18
|
+
privacyLink: e.privacyLink,
|
|
19
|
+
};
|
|
20
|
+
if (e.wildcardMatch === "1") {
|
|
21
|
+
wildcardEntries.push({ prefix: e.cookie.toLowerCase(), entry: ocdEntry });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const key = e.cookie.toLowerCase();
|
|
25
|
+
if (!exactIndex.has(key)) {
|
|
26
|
+
exactIndex.set(key, ocdEntry);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
buildIndexes();
|
|
33
|
+
export function lookupCookie(name) {
|
|
34
|
+
const lower = name.toLowerCase();
|
|
35
|
+
// 1. Exact match
|
|
36
|
+
const exact = exactIndex.get(lower);
|
|
37
|
+
if (exact)
|
|
38
|
+
return exact;
|
|
39
|
+
// 2. Wildcard prefix match (longest prefix wins)
|
|
40
|
+
let best = null;
|
|
41
|
+
let bestLen = 0;
|
|
42
|
+
for (const { prefix, entry } of wildcardEntries) {
|
|
43
|
+
if (lower.startsWith(prefix) && prefix.length > bestLen) {
|
|
44
|
+
best = entry;
|
|
45
|
+
bestLen = prefix.length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return best;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=cookie-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-lookup.js","sourceRoot":"","sources":["../../src/classifiers/cookie-lookup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAqBzC,yCAAyC;AACzC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAoB,CAAC;AAC/C,MAAM,eAAe,GAA+C,EAAE,CAAC;AAEvE,SAAS,YAAY;IACnB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAClG,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAkC,CAAC;IAEvF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,MAAM;gBAAE,SAAS;YACxB,MAAM,QAAQ,GAAa;gBACzB,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,QAAQ,EAAE,CAAC,CAAC,cAAc;gBAC1B,eAAe,EAAE,CAAC,CAAC,eAAe;gBAClC,WAAW,EAAE,CAAC,CAAC,WAAW;aAC3B,CAAC;YACF,IAAI,CAAC,CAAC,aAAa,KAAK,GAAG,EAAE,CAAC;gBAC5B,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC5E,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;gBACnC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBACzB,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,YAAY,EAAE,CAAC;AAEf,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAEjC,iBAAiB;IACjB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IAExB,iDAAiD;IACjD,IAAI,IAAI,GAAoB,IAAI,CAAC;IACjC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,eAAe,EAAE,CAAC;QAChD,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YACxD,IAAI,GAAG,KAAK,CAAC;YACb,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -16,10 +16,10 @@ 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
|
-
.option("-f, --format <formats>", "Output formats: md, html, json, pdf (comma-separated)", "html")
|
|
22
|
+
.option("-f, --format <formats>", "Output formats: md, html, json, pdf, csv (comma-separated)", "html")
|
|
23
23
|
.option("--viewport <preset>", "Viewport preset: desktop (1280×900), tablet (768×1024), mobile (390×844)", "desktop")
|
|
24
24
|
.option("--fail-on <threshold>", "Exit with code 1 if grade is below this letter (A/B/C/D/F) or score is below this number", "F")
|
|
25
25
|
.option("--json-summary", "Emit a JSON summary line to stdout after the scan (machine-readable)", false)
|
|
@@ -41,20 +41,20 @@ program
|
|
|
41
41
|
console.log(styleText("gray", ` Output : ${outputDir}`));
|
|
42
42
|
console.log(styleText("gray", ` Viewport : ${viewport}`));
|
|
43
43
|
console.log();
|
|
44
|
-
const validFormats = new Set(["md", "html", "json", "pdf"]);
|
|
44
|
+
const validFormats = new Set(["md", "html", "json", "pdf", "csv"]);
|
|
45
45
|
const formats = opts.format
|
|
46
46
|
.split(",")
|
|
47
47
|
.map((f) => f.trim().toLowerCase())
|
|
48
48
|
.filter((f) => validFormats.has(f));
|
|
49
49
|
if (formats.length === 0) {
|
|
50
|
-
console.error(styleText("red", " Invalid --format value. Valid options: md, html, json, pdf"));
|
|
50
|
+
console.error(styleText("red", " Invalid --format value. Valid options: md, html, json, pdf, csv"));
|
|
51
51
|
process.exit(2);
|
|
52
52
|
}
|
|
53
53
|
const options = {
|
|
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,
|
|
@@ -90,6 +90,7 @@ program
|
|
|
90
90
|
html: "HTML",
|
|
91
91
|
json: "JSON",
|
|
92
92
|
pdf: "PDF",
|
|
93
|
+
csv: "CSV",
|
|
93
94
|
};
|
|
94
95
|
for (const [fmt, path] of Object.entries(paths)) {
|
|
95
96
|
console.log(styleText("green", ` ${(labels[fmt] ?? fmt).padEnd(8)} ${path}`));
|
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":";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,CACL,eAAe,EACf,8HAA8H,CAC/H;KACA,MAAM,CAAC,uBAAuB,EAAE,uCAAuC,EAAE,OAAO,CAAC;KACjF,MAAM,CAAC,eAAe,EAAE,sBAAsB,EAAE,KAAK,CAAC;KACtD,MAAM,CACL,wBAAwB,EACxB,4DAA4D,EAC5D,MAAM,CACP;KACA,MAAM,CACL,qBAAqB,EACrB,0EAA0E,EAC1E,SAAS,CACV;KACA,MAAM,CACL,uBAAuB,EACvB,0FAA0F,EAC1F,GAAG,CACJ;KACA,MAAM,CACL,gBAAgB,EAChB,sEAAsE,EACtE,KAAK,CACN;KACA,MAAM,CACL,UAAU,EACV,kFAAkF,EAClF,KAAK,CACN;KACA,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,IAAI,EAAE,EAAE;IAClC,OAAO,CAAC,GAAG,EAAE,CAAC;IACd,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,uBAAuB,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,yCAAyC,CAAC,CAAC,CAAC;IAC1E,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC;IAEvD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAiB,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAI,IAAI,CAAC,QAAmB,CAAC,WAAW,EAAE,CAAC;IACzD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAA0B,CAAC,EAAE,CAAC;QACpD,OAAO,CAAC,KAAK,CACX,SAAS,CAAC,KAAK,EAAE,oEAAoE,CAAC,CACvF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,EAAE,CAAC,CAAC,CAAC;IACtD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,gBAAgB,SAAS,EAAE,CAAC,CAAC,CAAC;IAC5D,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,gBAAgB,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,EAAE,CAAC;IAEd,MAAM,YAAY,GAAG,IAAI,GAAG,CAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IACjF,MAAM,OAAO,GAAI,IAAI,CAAC,MAAiB;SACpC,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAClC,MAAM,CAAC,CAAC,CAAC,EAAqB,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAiB,CAAC,CAAC,CAAC;IAEzE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CACX,SAAS,CAAC,KAAK,EAAE,mEAAmE,CAAC,CACtF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAgB;QAC3B,GAAG,EAAE,aAAa;QAClB,SAAS;QACT,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QACnC,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;QACtC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,OAAO;QACP,QAAQ,EAAE,QAA0B;QACpC,MAAM,EAAE,IAAI,CAAC,MAAiB;KAC/B,CAAC;IAEF,MAAM,OAAO,GAAG,aAAa,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,CAAC;IAE9D,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QAErC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,sCAAsC,EAAE,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACzC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,EAAE,CAAC;QAEd,MAAM,SAAS,GAAG,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAE/C,OAAO,CAAC,GAAG,CACT,SAAS,CACP,MAAM,EACN,uBAAuB,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,CACzF,CACF,CAAC;QACF,OAAO,CAAC,GAAG,EAAE,CAAC;QAEd,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,qBAAqB,CAAC,CAAC,CAAC;YAC5F,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBACzD,MAAM,IAAI,GACR,KAAK,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;gBACnF,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YAClD,CAAC;YACD,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxC,OAAO,CAAC,GAAG,CACT,SAAS,CACP,MAAM,EACN,eAAe,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,oBAAoB,CACvE,CACF,CAAC;YACJ,CAAC;YACD,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,CAAC;QAED,MAAM,MAAM,GAA2B;YACrC,EAAE,EAAE,UAAU;YACd,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,MAAM;YACZ,GAAG,EAAE,KAAK;YACV,GAAG,EAAE,KAAK;SACX,CAAC;QACF,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,CAAC;QAEd,MAAM,SAAS,GAAG,IAAI,CAAC,MAAgB,CAAC;QACxC,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAC9F,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,GAAG,CACT,SAAS,CACP,KAAK,EACL,6BAA6B,MAAM,CAAC,UAAU,CAAC,KAAK,eAAe,MAAM,CAAC,UAAU,CAAC,KAAK,wBAAwB,SAAS,CAAC,WAAW,EAAE,EAAE,CAC5I,CACF,CAAC;YACF,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,CAAC;QAED,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,IAAI,CAAC,SAAS,CAAC;gBACb,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK;gBAC9B,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK;gBAC9B,MAAM,EAAE,CAAC,MAAM;gBACf,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;gBAClC,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,SAAS;gBACtC,MAAM,EAAE;oBACN,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM;oBACtC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM;oBAClF,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;wBAC1C,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;wBACpB,WAAW,EAAE,CAAC,CAAC,WAAW;qBAC3B,CAAC,CAAC;iBACJ;gBACD,WAAW,EAAE,KAAK;aACnB,CAAC,GAAG,IAAI,CACV,CAAC;QACJ,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;QACvC,OAAO,CAAC,KAAK,CACX,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CACnF,CAAC;QACF,IAAI,IAAI,CAAC,OAAO,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACtD,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,eAAe,CAAC;KACxB,WAAW,CAAC,4CAA4C,CAAC;KACzD,MAAM,CAAC,KAAK,IAAI,EAAE;IACjB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAC;IACrE,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9C,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC3B,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,gCAAgC,CAAC,CAAC,CAAC;IACjE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,OAAO,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,cAAc,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,oBAAoB,CAAC,CAAC;AAChF,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AAE5B,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9D,OAAO,WAAW,GAAG,EAAE,CAAC;IAC1B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,WAAW,GAA2B,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AAE7E,SAAS,iBAAiB,CAAC,KAAa,EAAE,KAAa,EAAE,SAAiB;IACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,OAAO,KAAK,GAAG,QAAQ,CAAC;IAC1B,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;IACtC,IAAI,CAAC,CAAC,KAAK,IAAI,WAAW,CAAC,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CACX,SAAS,CACP,KAAK,EACL,8BAA8B,SAAS,iDAAiD,CACzF,CACF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,MAAM,OAAO,GACX,KAAK,IAAI,EAAE;QACT,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC,CAAC,KAAK,IAAI,EAAE;YACX,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YACpC,CAAC,CAAC,KAAK,IAAI,EAAE;gBACX,CAAC,CAAC,SAAS,CAAC,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC1C,CAAC,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,OAAO,MAAM,CAAC;AAC1B,CAAC"}
|