@slashgear/gdpr-cookie-scanner 1.0.0 → 1.2.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.
@@ -10,8 +10,6 @@ jobs:
10
10
  permissions:
11
11
  contents: write
12
12
  pull-requests: write
13
- packages: write
14
- id-token: write # required for npm trusted publishing (OIDC)
15
13
 
16
14
  steps:
17
15
  - uses: actions/checkout@v4
@@ -33,25 +31,12 @@ jobs:
33
31
  run: pnpm build
34
32
 
35
33
  - name: Create release PR or publish to npm
36
- id: changesets
37
34
  uses: changesets/action@v1
38
35
  with:
39
36
  publish: pnpm changeset publish
40
37
  title: "chore: release new version"
41
38
  commit: "chore: release new version"
39
+ createGithubReleases: false
42
40
  env:
43
41
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
- NPM_CONFIG_PROVENANCE: "true"
45
-
46
- - name: Configure registry for GitHub Packages
47
- if: steps.changesets.outputs.published == 'true'
48
- uses: actions/setup-node@v4
49
- with:
50
- node-version: 22
51
- registry-url: https://npm.pkg.github.com
52
-
53
- - name: Publish to GitHub Packages
54
- if: steps.changesets.outputs.published == 'true'
55
- run: npm publish
56
- env:
57
- NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,3 @@
1
+ # Files excluded from oxfmt formatting
2
+ .changeset/
3
+ CLAUDE.md
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @slashgear/gdpr-cookie-scanner
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 15deb9e: chore: simplify npm publishing — token-based auth, no GitHub Packages
8
+
9
+ The release pipeline now publishes exclusively to npmjs.org using a classic
10
+ NPM_TOKEN secret. GitHub Packages publishing and GitHub Releases creation have
11
+ been removed to reduce complexity. OIDC trusted publishing has been dropped in
12
+ favour of the more straightforward token approach.
13
+
14
+ ## 1.1.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 730e4d2: feat: generate a merged PDF report alongside the Markdown files
19
+
20
+ A single `gdpr-report-{hostname}-{date}.pdf` is now produced at the end of
21
+ each scan, combining the main compliance report, the checklist, and the cookie
22
+ inventory into one A4 document. The PDF is rendered via the Playwright browser
23
+ already installed as a dependency. The CLI prints the PDF path on completion.
24
+
25
+ ## 1.0.1
26
+
27
+ ### Patch Changes
28
+
29
+ - 56bfed3: fix: isolate navigation timeout in accept phase so cookies are captured even when networkidle times out
30
+
31
+ The accept phase (phase 4) previously wrapped navigation and cookie capture in a single try/catch, causing the entire phase to be skipped when `networkidle` timed out on heavy sites. The navigation timeout is now isolated so detection and capture proceed regardless.
32
+
33
+ feat: add cookie inventory report (`gdpr-cookies-*.md`)
34
+
35
+ A new per-scan file lists all unique cookies detected across all three phases (before consent, after acceptance, after rejection) with their category, phases of presence, expiry, consent requirement, and an empty "Description / Purpose" column for the DPO or technical owner to fill in.
36
+
37
+ feat: add hyperlinks to legal references in checklist
38
+
39
+ Every reference in the compliance checklist (GDPR Art. 7, EDPB Guidelines 03/2022, CNIL Recommendation 2022, etc.) now links to the official source document.
40
+
3
41
  ## 1.0.0
4
42
 
5
43
  ### Major Changes
package/CLAUDE.md CHANGED
@@ -21,15 +21,27 @@ node dist/cli.js list-trackers
21
21
 
22
22
  There are no tests currently.
23
23
 
24
+ ## Commit checklist
25
+
26
+ Before every commit, always run in order:
27
+
28
+ ```bash
29
+ pnpm lint # must show 0 errors (warnings tolerated if pre-existing)
30
+ pnpm format # auto-fix formatting — never commit with format issues
31
+ pnpm typecheck # must pass with 0 errors
32
+ ```
33
+
34
+ Then create a changeset (see below) and commit everything together.
35
+
36
+ Commits must follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, `docs:`, etc.).
37
+
24
38
  ## Release process
25
39
 
26
40
  This project uses [Changesets](https://github.com/changesets/changesets).
27
41
 
28
- - **Contributors**: run `pnpm changeset` before opening a PR to document the change (patch/minor/major + summary). Commit the generated `.changeset/*.md` file with your PR. Skip for docs/CI-only changes.
42
+ - **Contributors**: write a `.changeset/<slug>.md` file manually (the interactive CLI doesn't work in non-TTY environments) before every meaningful commit. Use `patch` for bug fixes, `minor` for new features, `major` for breaking changes. The summary should explain *what* changed and *why* — not just restate the diff. Skip for docs/CI-only changes.
29
43
  - **Maintainers**: merging changesets into `main` triggers the release workflow, which opens a "chore: release new version" PR (bumps version + updates `CHANGELOG.md`). Merging that PR publishes to GitHub Packages and creates the GitHub Release automatically.
30
44
 
31
- Commits must follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, `docs:`, etc.).
32
-
33
45
  ## Architecture
34
46
 
35
47
  This is a TypeScript CLI tool (`gdpr-scan`) that audits websites for GDPR/RGPD cookie consent compliance using Playwright. It produces a Markdown report in French.
@@ -56,7 +68,7 @@ Phase 3 and 4 require two separate browser sessions so cookie state is fully iso
56
68
  - **`src/classifiers/tracker-list.ts`** — Static database of tracker domains by category
57
69
  - **`src/analyzers/compliance.ts`** — Scores 4 dimensions (0–25 each) and surfaces `DarkPatternIssue` objects: consent validity, easy refusal, transparency, cookie behavior
58
70
  - **`src/analyzers/wording.ts`** — Text analysis of button labels and modal copy
59
- - **`src/report/generator.ts`** — Renders a Markdown report and checklist from `ScanResult`; runs `oxfmt` on generated files
71
+ - **`src/report/generator.ts`** — Renders 3 Markdown files from `ScanResult`: the main compliance report (`gdpr-report-*.md`), the checklist (`gdpr-checklist-*.md`), and the cookie inventory (`gdpr-cookies-*.md`); runs `oxfmt` on all generated files
60
72
  - **`src/types.ts`** — All shared TypeScript interfaces (`ScanResult`, `ScanOptions`, `ComplianceScore`, `ConsentModal`, etc.)
61
73
 
62
74
  ### Compliance scoring
package/dist/cli.js CHANGED
@@ -47,7 +47,7 @@ program
47
47
  spinner.succeed("Scan complete");
48
48
  console.log();
49
49
  const generator = new ReportGenerator(options);
50
- const reportPath = await generator.generate(result);
50
+ const { reportPath, pdfPath } = await generator.generate(result);
51
51
  console.log(chalk.bold(` Compliance score: ${formatScore(result.compliance.total)} ${result.compliance.grade}`));
52
52
  console.log();
53
53
  if (result.compliance.issues.length > 0) {
@@ -62,6 +62,7 @@ program
62
62
  console.log();
63
63
  }
64
64
  console.log(chalk.green(` Report saved: ${reportPath}`));
65
+ console.log(chalk.green(` PDF saved: ${pdfPath}`));
65
66
  console.log();
66
67
  process.exit(result.compliance.grade === "F" ? 1 : 0);
67
68
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,13 @@ import type { ScanOptions } from "../types.js";
3
3
  export declare class ReportGenerator {
4
4
  private readonly options;
5
5
  constructor(options: ScanOptions);
6
- generate(result: ScanResult): Promise<string>;
6
+ generate(result: ScanResult): Promise<{
7
+ reportPath: string;
8
+ pdfPath: string;
9
+ }>;
10
+ private buildHtmlBody;
11
+ private inlineImages;
12
+ private wrapHtml;
7
13
  private buildMarkdown;
8
14
  private buildScoreTable;
9
15
  private buildExecutiveSummary;
@@ -14,6 +20,7 @@ export declare class ReportGenerator {
14
20
  private buildCookiesAfterRejectSection;
15
21
  private buildNetworkSection;
16
22
  private buildRecommendations;
23
+ private buildCookiesInventory;
17
24
  private buildChecklist;
18
25
  }
19
26
  //# sourceMappingURL=generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/report/generator.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,UAAU,EAKX,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,WAAW;IAE3C,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBnD,OAAO,CAAC,aAAa;IA6ErB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,qBAAqB;IAkD7B,OAAO,CAAC,iBAAiB;IAwEzB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,kBAAkB;IAqC1B,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,8BAA8B;IAiBtC,OAAO,CAAC,mBAAmB;IAgD3B,OAAO,CAAC,oBAAoB;IA2D5B,OAAO,CAAC,cAAc;CAgPvB"}
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/report/generator.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,UAAU,EAKX,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,WAAW;IAE3C,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;YAmCtE,aAAa;YAkDb,YAAY;IAmC1B,OAAO,CAAC,QAAQ;IAgDhB,OAAO,CAAC,aAAa;IA6ErB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,qBAAqB;IAkD7B,OAAO,CAAC,iBAAiB;IAwEzB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,kBAAkB;IAqC1B,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,8BAA8B;IAiBtC,OAAO,CAAC,mBAAmB;IAgD3B,OAAO,CAAC,oBAAoB;IA2D5B,OAAO,CAAC,qBAAqB;IA6H7B,OAAO,CAAC,cAAc;CA6PvB"}
@@ -1,8 +1,10 @@
1
1
  import { execFile } from "child_process";
2
- import { writeFile, mkdir } from "fs/promises";
3
- import { basename, dirname, join } from "path";
2
+ import { writeFile, mkdir, readFile } from "fs/promises";
3
+ import { basename, dirname, extname, join } from "path";
4
4
  import { promisify } from "util";
5
5
  import { fileURLToPath } from "url";
6
+ import { Marked } from "marked";
7
+ import { generatePdf } from "./pdf.js";
6
8
  const execFileAsync = promisify(execFile);
7
9
  const oxfmtBin = join(dirname(fileURLToPath(import.meta.url)), "../../node_modules/.bin/oxfmt");
8
10
  export class ReportGenerator {
@@ -24,7 +26,138 @@ export class ReportGenerator {
24
26
  const checklist = this.buildChecklist(result);
25
27
  await writeFile(checklistPath, checklist, "utf-8");
26
28
  await execFileAsync(oxfmtBin, [checklistPath]).catch(() => { });
27
- return outputPath;
29
+ const cookiesFilename = `gdpr-cookies-${hostname}-${date}.md`;
30
+ const cookiesPath = join(this.options.outputDir, cookiesFilename);
31
+ const cookiesInventory = this.buildCookiesInventory(result);
32
+ await writeFile(cookiesPath, cookiesInventory, "utf-8");
33
+ await execFileAsync(oxfmtBin, [cookiesPath]).catch(() => { });
34
+ const combined = [markdown, checklist, cookiesInventory].join("\n\n---\n\n");
35
+ const rawBody = await this.buildHtmlBody(combined);
36
+ const body = await this.inlineImages(rawBody, this.options.outputDir);
37
+ const html = this.wrapHtml(body, hostname);
38
+ const pdfFilename = `gdpr-report-${hostname}-${date}.pdf`;
39
+ const pdfPath = join(this.options.outputDir, pdfFilename);
40
+ await generatePdf(html, pdfPath);
41
+ return { reportPath: outputPath, pdfPath };
42
+ }
43
+ async buildHtmlBody(markdown) {
44
+ const entries = [];
45
+ const idCounts = new Map();
46
+ const slugify = (text) => {
47
+ const base = text
48
+ .replace(/[^\p{L}\p{N}\s-]/gu, "")
49
+ .trim()
50
+ .toLowerCase()
51
+ .replace(/\s+/g, "-")
52
+ .replace(/-+/g, "-") || "section";
53
+ const count = idCounts.get(base) ?? 0;
54
+ idCounts.set(base, count + 1);
55
+ return count === 0 ? base : `${base}-${count}`;
56
+ };
57
+ const localMarked = new Marked();
58
+ localMarked.use({
59
+ renderer: {
60
+ heading({ text, depth }) {
61
+ const id = slugify(text);
62
+ if (depth <= 2)
63
+ entries.push({ level: depth, text, id });
64
+ return `<h${depth} id="${id}">${text}</h${depth}>\n`;
65
+ },
66
+ },
67
+ });
68
+ const body = await localMarked.parse(markdown);
69
+ if (entries.length === 0)
70
+ return body;
71
+ const tocItems = entries
72
+ .map(({ level, text, id }) => {
73
+ const cls = level === 1 ? "toc-h1" : "toc-h2";
74
+ return `<li class="${cls}"><a href="#${id}">${text}</a></li>`;
75
+ })
76
+ .join("\n");
77
+ const toc = `<nav class="toc">
78
+ <p class="toc-title">Table of Contents</p>
79
+ <ul>
80
+ ${tocItems}
81
+ </ul>
82
+ </nav>`;
83
+ return toc + "\n" + body;
84
+ }
85
+ async inlineImages(html, outputDir) {
86
+ const mimeTypes = {
87
+ ".png": "image/png",
88
+ ".jpg": "image/jpeg",
89
+ ".jpeg": "image/jpeg",
90
+ ".gif": "image/gif",
91
+ ".webp": "image/webp",
92
+ };
93
+ const imgRegex = /<img([^>]*)\ssrc="([^"#][^"]*)"([^>]*)>/gi;
94
+ const replacements = [];
95
+ for (const match of html.matchAll(imgRegex)) {
96
+ const [full, before, src, after] = match;
97
+ if (src.startsWith("data:") || src.startsWith("http://") || src.startsWith("https://"))
98
+ continue;
99
+ const mime = mimeTypes[extname(src).toLowerCase()];
100
+ if (!mime)
101
+ continue;
102
+ try {
103
+ const buf = await readFile(join(outputDir, src));
104
+ replacements.push({
105
+ original: full,
106
+ replacement: `<img${before} src="data:${mime};base64,${buf.toString("base64")}"${after}>`,
107
+ });
108
+ }
109
+ catch {
110
+ // file not found — leave the tag as-is
111
+ }
112
+ }
113
+ return replacements.reduce((acc, { original, replacement }) => acc.replace(original, replacement), html);
114
+ }
115
+ wrapHtml(body, hostname) {
116
+ return `<!DOCTYPE html>
117
+ <html lang="en">
118
+ <head>
119
+ <meta charset="UTF-8">
120
+ <title>GDPR Report — ${hostname}</title>
121
+ <style>
122
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
123
+ font-size: 11pt; line-height: 1.6; color: #1a1a1a; max-width: 900px;
124
+ margin: 0 auto; padding: 0 8px; }
125
+ h1 { font-size: 18pt; border-bottom: 2px solid #1a1a2e; padding-bottom: 6px;
126
+ color: #1a1a2e; margin-top: 2em; }
127
+ h2 { font-size: 14pt; color: #1a1a2e; margin-top: 1.5em; }
128
+ h3 { font-size: 12pt; margin-top: 1.2em; }
129
+ table { width: 100%; border-collapse: collapse; font-size: 9.5pt;
130
+ margin: 1em 0; page-break-inside: auto; }
131
+ th { background: #f0f0f4; padding: 6px 10px; text-align: left;
132
+ border-bottom: 2px solid #ccc; }
133
+ td { padding: 5px 10px; border-bottom: 1px solid #eee; vertical-align: top; }
134
+ tr { page-break-inside: avoid; }
135
+ code { font-family: "SFMono-Regular", Consolas, monospace; background: #f4f4f4;
136
+ padding: 1px 5px; border-radius: 3px; font-size: 9pt; }
137
+ pre { background: #f4f4f4; padding: 12px; border-radius: 4px;
138
+ overflow-x: auto; font-size: 9pt; }
139
+ blockquote { border-left: 3px solid #ccc; margin: 0.5em 0;
140
+ padding: 0.5em 1em; color: #555; }
141
+ hr { border: none; border-top: 1px solid #ddd; margin: 2em 0;
142
+ page-break-after: always; }
143
+ a { color: #0066cc; }
144
+ img { max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; }
145
+ nav.toc { background: #f4f5f8; border-left: 4px solid #1a1a2e; border-radius: 4px;
146
+ padding: 14px 20px; margin: 0 0 2.5em 0; page-break-inside: avoid; }
147
+ .toc-title { font-weight: 700; font-size: 11pt; margin: 0 0 10px 0; color: #1a1a2e; }
148
+ nav.toc ul { list-style: none; margin: 0; padding: 0; }
149
+ nav.toc li { margin: 3px 0; line-height: 1.4; }
150
+ .toc-h1 { font-weight: 600; margin-top: 6px; }
151
+ .toc-h2 { padding-left: 1.2em; font-size: 9.5pt; }
152
+ nav.toc a { color: #0055aa; text-decoration: none; }
153
+ @media print {
154
+ h1 { page-break-before: always; }
155
+ h1:first-child { page-break-before: avoid; }
156
+ }
157
+ </style>
158
+ </head>
159
+ <body>${body}</body>
160
+ </html>`;
28
161
  }
29
162
  buildMarkdown(r) {
30
163
  const hostname = new URL(r.url).hostname;
@@ -145,7 +278,7 @@ ${row("Cookie behavior", breakdown.cookieBehavior, 25)}
145
278
  const { modal } = r;
146
279
  const acceptBtn = modal.buttons.find((b) => b.type === "accept");
147
280
  const rejectBtn = modal.buttons.find((b) => b.type === "reject");
148
- const prefBtn = modal.buttons.find((b) => b.type === "preferences");
281
+ const _prefBtn = modal.buttons.find((b) => b.type === "preferences");
149
282
  const preTicked = modal.checkboxes.filter((c) => c.isCheckedByDefault);
150
283
  const lines = [
151
284
  `**CSS selector:** \`${modal.selector}\``,
@@ -348,6 +481,101 @@ ${rows.join("\n")}
348
481
  }
349
482
  return recs.join("\n\n");
350
483
  }
484
+ buildCookiesInventory(r) {
485
+ const hostname = new URL(r.url).hostname;
486
+ const scanDate = new Date(r.scanDate).toLocaleString("en-GB");
487
+ const cookieMap = new Map();
488
+ const phaseLabel = {
489
+ "before-interaction": "before consent",
490
+ "after-accept": "after acceptance",
491
+ "after-reject": "after rejection",
492
+ };
493
+ const allCookies = [
494
+ ...r.cookiesBeforeInteraction,
495
+ ...r.cookiesAfterAccept,
496
+ ...r.cookiesAfterReject,
497
+ ];
498
+ for (const c of allCookies) {
499
+ const key = `${c.name}||${c.domain}`;
500
+ if (!cookieMap.has(key)) {
501
+ cookieMap.set(key, {
502
+ name: c.name,
503
+ domain: c.domain,
504
+ category: c.category,
505
+ phases: new Set(),
506
+ expires: c.expires,
507
+ httpOnly: c.httpOnly,
508
+ secure: c.secure,
509
+ requiresConsent: c.requiresConsent,
510
+ });
511
+ }
512
+ cookieMap.get(key).phases.add(phaseLabel[c.capturedAt]);
513
+ }
514
+ const expires = (entry) => {
515
+ if (entry.expires === null)
516
+ return "Session";
517
+ const days = Math.round((entry.expires * 1000 - Date.now()) / 86400000);
518
+ if (days < 0)
519
+ return "Expired";
520
+ if (days === 0)
521
+ return "< 1 day";
522
+ if (days < 30)
523
+ return `${days} days`;
524
+ return `${Math.round(days / 30)} months`;
525
+ };
526
+ const categoryLabel = {
527
+ "strictly-necessary": "Strictly necessary",
528
+ analytics: "Analytics",
529
+ advertising: "Advertising",
530
+ social: "Social",
531
+ personalization: "Personalization",
532
+ unknown: "Unknown",
533
+ };
534
+ const entries = [...cookieMap.values()].sort((a, b) => {
535
+ // Sort: strictly-necessary first, then by category, then by name
536
+ const order = [
537
+ "strictly-necessary",
538
+ "analytics",
539
+ "advertising",
540
+ "social",
541
+ "personalization",
542
+ "unknown",
543
+ ];
544
+ const oa = order.indexOf(a.category);
545
+ const ob = order.indexOf(b.category);
546
+ if (oa !== ob)
547
+ return oa - ob;
548
+ return a.name.localeCompare(b.name);
549
+ });
550
+ const lines = [];
551
+ lines.push(`# Cookie Inventory — ${hostname}`);
552
+ lines.push(`
553
+ > **Scan date:** ${scanDate}
554
+ > **Scanned URL:** ${r.url}
555
+ > **Unique cookies detected:** ${entries.length}
556
+ `);
557
+ lines.push(`## Instructions`);
558
+ lines.push(`
559
+ This table lists all cookies detected during the scan, across all phases.
560
+ The **Description / Purpose** column is to be filled in by the DPO or technical owner.
561
+
562
+ - **Before consent** — cookie present from page load, before any interaction
563
+ - **After acceptance** — cookie set or persisting after clicking "Accept all"
564
+ - **After rejection** — cookie present after clicking "Reject all"
565
+ `);
566
+ lines.push(`## Cookie table\n`);
567
+ lines.push(`| Cookie | Domain | Category | Phases | Expiry | Consent required | Description / Purpose |`);
568
+ lines.push(`|--------|--------|----------|--------|--------|------------------|-----------------------|`);
569
+ for (const entry of entries) {
570
+ const phases = [...entry.phases].join(", ");
571
+ const consent = entry.requiresConsent ? "⚠️ Yes" : "✅ No";
572
+ const cat = categoryLabel[entry.category] ?? entry.category;
573
+ lines.push(`| \`${entry.name}\` | ${entry.domain} | ${cat} | ${phases} | ${expires(entry)} | ${consent} | <!-- fill in --> |`);
574
+ }
575
+ lines.push(`\n---`);
576
+ lines.push(`\n_Automatically generated by gdpr-cookie-scanner. Categories marked "Unknown" could not be identified automatically and should be verified manually._\n`);
577
+ return lines.join("\n") + "\n";
578
+ }
351
579
  buildChecklist(r) {
352
580
  const hostname = new URL(r.url).hostname;
353
581
  const scanDate = new Date(r.scanDate).toLocaleString("en-GB");
@@ -362,7 +590,7 @@ ${rows.join("\n")}
362
590
  rows.push({
363
591
  category: "Consent",
364
592
  rule: "Consent modal detected",
365
- reference: "RGPD Art. 7 · Dir. ePrivacy Art. 5(3)",
593
+ reference: "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [ePrivacy Dir. Art. 5(3)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32002L0058)",
366
594
  status: r.modal.detected ? ok : ko,
367
595
  detail: r.modal.detected
368
596
  ? `Detected (\`${r.modal.selector}\`)`
@@ -372,7 +600,7 @@ ${rows.join("\n")}
372
600
  rows.push({
373
601
  category: "Consent",
374
602
  rule: "No pre-ticked checkboxes",
375
- reference: "RGPD Recital 32",
603
+ reference: "[GDPR Recital 32](https://gdpr-info.eu/recitals/no-32/)",
376
604
  status: preTicked.length === 0 ? ok : ko,
377
605
  detail: preTicked.length === 0
378
606
  ? "No pre-ticked checkbox detected"
@@ -383,7 +611,7 @@ ${rows.join("\n")}
383
611
  rows.push({
384
612
  category: "Consent",
385
613
  rule: "Accept button label is unambiguous",
386
- reference: "RGPD Art. 4(11)",
614
+ reference: "[GDPR Art. 4(11)](https://gdpr-info.eu/art-4-gdpr/)",
387
615
  status: !r.modal.detected || !misleadingAccept
388
616
  ? ok
389
617
  : misleadingAccept.severity === "critical"
@@ -403,7 +631,7 @@ ${rows.join("\n")}
403
631
  rows.push({
404
632
  category: "Easy refusal",
405
633
  rule: "Reject button present at first layer",
406
- reference: "CNIL Recommendation 2022",
634
+ reference: "[CNIL Recommendation 2022](https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies/recommandation-sur-les-cookies-et-autres-traceurs)",
407
635
  status: !r.modal.detected ? ko : noReject ? ko : ok,
408
636
  detail: !r.modal.detected
409
637
  ? "Modal not detected"
@@ -415,7 +643,7 @@ ${rows.join("\n")}
415
643
  rows.push({
416
644
  category: "Easy refusal",
417
645
  rule: "Rejecting requires no more clicks than accepting",
418
- reference: "CNIL Recommendation 2022",
646
+ reference: "[CNIL Recommendation 2022](https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies/recommandation-sur-les-cookies-et-autres-traceurs)",
419
647
  status: !r.modal.detected ? ko : clickIssue ? ko : ok,
420
648
  detail: !r.modal.detected
421
649
  ? "Modal not detected"
@@ -429,7 +657,7 @@ ${rows.join("\n")}
429
657
  rows.push({
430
658
  category: "Easy refusal",
431
659
  rule: "Size symmetry between Accept and Reject",
432
- reference: "CEPD Guidelines 03/2022",
660
+ reference: "[EDPB Guidelines 03/2022](https://edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-032022-dark-patterns-social-media-platform_en)",
433
661
  status: !r.modal.detected ? ko : sizeIssue ? warn : ok,
434
662
  detail: !r.modal.detected
435
663
  ? "Modal not detected"
@@ -441,7 +669,7 @@ ${rows.join("\n")}
441
669
  rows.push({
442
670
  category: "Easy refusal",
443
671
  rule: "Font symmetry between Accept and Reject",
444
- reference: "CEPD Guidelines 03/2022",
672
+ reference: "[EDPB Guidelines 03/2022](https://edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-032022-dark-patterns-social-media-platform_en)",
445
673
  status: !r.modal.detected ? ko : nudgeIssue ? warn : ok,
446
674
  detail: !r.modal.detected
447
675
  ? "Modal not detected"
@@ -453,7 +681,7 @@ ${rows.join("\n")}
453
681
  rows.push({
454
682
  category: "Transparency",
455
683
  rule: "Granular controls available",
456
- reference: "CEPD Guidelines 05/2020",
684
+ reference: "[EDPB Guidelines 05/2020](https://edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-052020-consent-under-regulation-2016679_en)",
457
685
  status: !r.modal.detected ? ko : r.modal.hasGranularControls ? ok : warn,
458
686
  detail: !r.modal.detected
459
687
  ? "Modal not detected"
@@ -462,21 +690,25 @@ ${rows.join("\n")}
462
690
  : "No granular controls (checkboxes or panel) detected",
463
691
  });
464
692
  const infoChecks = [
465
- { key: "purposes", label: "Processing purposes mentioned", ref: "RGPD Art. 13-14" },
693
+ {
694
+ key: "purposes",
695
+ label: "Processing purposes mentioned",
696
+ ref: "[GDPR Art. 13-14](https://gdpr-info.eu/art-13-gdpr/)",
697
+ },
466
698
  {
467
699
  key: "third-parties",
468
700
  label: "Sub-processors / third parties mentioned",
469
- ref: "RGPD Art. 13-14",
701
+ ref: "[GDPR Art. 13-14](https://gdpr-info.eu/art-13-gdpr/)",
470
702
  },
471
703
  {
472
704
  key: "duration",
473
705
  label: "Retention period mentioned",
474
- ref: "RGPD Art. 13(2)(a)",
706
+ ref: "[GDPR Art. 13(2)(a)](https://gdpr-info.eu/art-13-gdpr/)",
475
707
  },
476
708
  {
477
709
  key: "withdrawal",
478
710
  label: "Right to withdraw consent mentioned",
479
- ref: "RGPD Art. 7(3)",
711
+ ref: "[GDPR Art. 7(3)](https://gdpr-info.eu/art-7-gdpr/)",
480
712
  },
481
713
  ];
482
714
  for (const { key, label, ref } of infoChecks) {
@@ -498,7 +730,7 @@ ${rows.join("\n")}
498
730
  rows.push({
499
731
  category: "Cookie behavior",
500
732
  rule: "No non-essential cookie before consent",
501
- reference: "RGPD Art. 7 · Dir. ePrivacy Art. 5(3)",
733
+ reference: "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [ePrivacy Dir. Art. 5(3)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32002L0058)",
502
734
  status: illegalPre.length === 0 ? ok : ko,
503
735
  detail: illegalPre.length === 0
504
736
  ? "No non-essential cookie set before interaction"
@@ -508,7 +740,7 @@ ${rows.join("\n")}
508
740
  rows.push({
509
741
  category: "Cookie behavior",
510
742
  rule: "Non-essential cookies removed after rejection",
511
- reference: "RGPD Art. 7 · CNIL Recommendation 2022",
743
+ reference: "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [CNIL Recommendation 2022](https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies/recommandation-sur-les-cookies-et-autres-traceurs)",
512
744
  status: persistAfterReject.length === 0 ? ok : ko,
513
745
  detail: persistAfterReject.length === 0
514
746
  ? "No non-essential cookie persisting after rejection"
@@ -518,7 +750,7 @@ ${rows.join("\n")}
518
750
  rows.push({
519
751
  category: "Cookie behavior",
520
752
  rule: "No network tracker before consent",
521
- reference: "RGPD Art. 7 · Dir. ePrivacy Art. 5(3)",
753
+ reference: "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [ePrivacy Dir. Art. 5(3)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32002L0058)",
522
754
  status: preTrackers.length === 0 ? ok : ko,
523
755
  detail: preTrackers.length === 0
524
756
  ? "No tracker request fired before interaction"