@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.
- package/.github/workflows/release.yml +2 -17
- package/.prettierignore +3 -0
- package/CHANGELOG.md +38 -0
- package/CLAUDE.md +16 -4
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/report/generator.d.ts +8 -1
- package/dist/report/generator.d.ts.map +1 -1
- package/dist/report/generator.js +251 -19
- package/dist/report/generator.js.map +1 -1
- package/dist/report/pdf.d.ts +2 -0
- package/dist/report/pdf.d.ts.map +1 -0
- package/dist/report/pdf.js +22 -0
- package/dist/report/pdf.js.map +1 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +8 -2
- package/dist/scanner/index.js.map +1 -1
- package/package.json +23 -13
- package/src/cli.ts +2 -1
- package/src/report/generator.ts +307 -20
- package/src/report/pdf.ts +21 -0
- package/src/scanner/index.ts +8 -2
- package/.idea/gdpr-report.iml +0 -8
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
|
@@ -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
|
-
|
|
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 }}
|
package/.prettierignore
ADDED
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**:
|
|
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
|
|
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":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,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,CAAC,kBAAkB,EAAE,4BAA4B,CAAC;KACxD,MAAM,CAAC,uBAAuB,EAAE,uCAAuC,EAAE,OAAO,CAAC;KACjF,MAAM,CAAC,eAAe,EAAE,sBAAsB,EAAE,KAAK,CAAC;KACtD,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,IAAI,EAAE,EAAE;IAClC,OAAO,CAAC,GAAG,EAAE,CAAC;IACd,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;IACtD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC,CAAC;IACnE,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,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,SAAS,EAAE,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,EAAE,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,IAAI,CAAC,WAAW,KAAK,KAAK;QACvC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC;IAEF,MAAM,OAAO,GAAG,GAAG,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,CAAC;IAEpD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QAErC,OAAO,CAAC,IAAI,GAAG,sCAAsC,CAAC;QACtD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACzC,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QACjC,OAAO,CAAC,GAAG,EAAE,CAAC;QAEd,MAAM,SAAS,GAAG,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;
|
|
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<
|
|
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":"
|
|
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"}
|
package/dist/report/generator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
{
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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"
|