@slashgear/gdpr-cookie-scanner 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +13 -0
- package/.github/workflows/ci.yml +8 -2
- package/.github/workflows/docker.yml +49 -0
- package/.github/workflows/pages.yml +40 -0
- package/.github/workflows/release.yml +1 -1
- package/.nvmrc +1 -0
- package/CHANGELOG.md +87 -0
- package/Dockerfile +36 -0
- package/README.md +44 -63
- package/dist/cli.js +21 -3
- package/dist/cli.js.map +1 -1
- package/dist/report/generator.d.ts +1 -4
- package/dist/report/generator.d.ts.map +1 -1
- package/dist/report/generator.js +45 -23
- package/dist/report/generator.js.map +1 -1
- package/dist/report/html.d.ts +3 -0
- package/dist/report/html.d.ts.map +1 -0
- package/dist/report/html.js +766 -0
- package/dist/report/html.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/index.html +314 -0
- package/docs/reports/github.com/after-accept.png +0 -0
- package/docs/reports/github.com/after-reject.png +0 -0
- package/docs/reports/github.com/gdpr-checklist-github.com-2026-02-22.md +44 -0
- package/docs/reports/github.com/gdpr-cookies-github.com-2026-02-22.md +29 -0
- package/docs/reports/github.com/gdpr-report-github.com-2026-02-22.md +102 -0
- package/docs/reports/github.com/gdpr-report-github.com-2026-02-22.pdf +0 -0
- package/docs/reports/gitlab.com/after-accept.png +0 -0
- package/docs/reports/gitlab.com/after-reject.png +0 -0
- package/docs/reports/gitlab.com/gdpr-checklist-gitlab.com-2026-02-22.md +44 -0
- package/docs/reports/gitlab.com/gdpr-cookies-gitlab.com-2026-02-22.md +55 -0
- package/docs/reports/gitlab.com/gdpr-report-gitlab.com-2026-02-22.md +200 -0
- package/docs/reports/gitlab.com/gdpr-report-gitlab.com-2026-02-22.pdf +0 -0
- package/docs/reports/gitlab.com/modal-initial.png +0 -0
- package/docs/reports/npmjs.com/after-accept.png +0 -0
- package/docs/reports/npmjs.com/after-reject.png +0 -0
- package/docs/reports/npmjs.com/gdpr-checklist-npmjs.com-2026-02-22.md +44 -0
- package/docs/reports/npmjs.com/gdpr-cookies-npmjs.com-2026-02-22.md +25 -0
- package/docs/reports/npmjs.com/gdpr-report-npmjs.com-2026-02-22.md +88 -0
- package/docs/reports/npmjs.com/gdpr-report-npmjs.com-2026-02-22.pdf +0 -0
- package/docs/reports/reddit.com/after-accept.png +0 -0
- package/docs/reports/reddit.com/after-reject.png +0 -0
- package/docs/reports/reddit.com/gdpr-checklist-reddit.com-2026-02-22.md +44 -0
- package/docs/reports/reddit.com/gdpr-cookies-reddit.com-2026-02-22.md +33 -0
- package/docs/reports/reddit.com/gdpr-report-reddit.com-2026-02-22.md +148 -0
- package/docs/reports/reddit.com/gdpr-report-reddit.com-2026-02-22.pdf +0 -0
- package/docs/reports/reddit.com/modal-initial.png +0 -0
- package/docs/reports/stackoverflow.com/after-accept.png +0 -0
- package/docs/reports/stackoverflow.com/after-reject.png +0 -0
- package/docs/reports/stackoverflow.com/gdpr-checklist-stackoverflow.com-2026-02-22.md +44 -0
- package/docs/reports/stackoverflow.com/gdpr-cookies-stackoverflow.com-2026-02-22.md +67 -0
- package/docs/reports/stackoverflow.com/gdpr-report-stackoverflow.com-2026-02-22.md +206 -0
- package/docs/reports/stackoverflow.com/gdpr-report-stackoverflow.com-2026-02-22.pdf +0 -0
- package/docs/reports/stackoverflow.com/modal-initial.png +0 -0
- package/docs/reports/www.afp.com/after-accept.png +0 -0
- package/docs/reports/www.afp.com/after-reject.png +0 -0
- package/docs/reports/www.afp.com/gdpr-checklist-afp.com-2026-02-22.md +44 -0
- package/docs/reports/www.afp.com/gdpr-cookies-afp.com-2026-02-22.md +42 -0
- package/docs/reports/www.afp.com/gdpr-report-afp.com-2026-02-22.md +202 -0
- package/docs/reports/www.afp.com/gdpr-report-afp.com-2026-02-22.pdf +0 -0
- package/docs/reports/www.afp.com/modal-initial.png +0 -0
- package/docs/style.css +439 -0
- package/package.json +10 -7
- package/src/cli.ts +28 -4
- package/src/report/generator.ts +54 -29
- package/src/report/html.ts +940 -0
- package/src/types.ts +3 -0
- package/tests/e2e/fixtures/compliant-site.html +80 -0
- package/tests/e2e/fixtures/no-modal-site.html +17 -0
- package/tests/e2e/fixtures/non-compliant-site.html +54 -0
- package/tests/e2e/scanner.test.ts +135 -0
- package/tests/helpers/test-server.ts +57 -0
- package/tests/unit/compliance.test.ts +460 -0
- package/tests/unit/cookie-classifier.test.ts +192 -0
- package/tests/unit/network-classifier.test.ts +91 -0
- package/tests/unit/wording.test.ts +162 -0
- package/vitest.config.ts +9 -0
package/src/cli.ts
CHANGED
|
@@ -5,7 +5,7 @@ import ora from "ora";
|
|
|
5
5
|
import { join, resolve } from "path";
|
|
6
6
|
import { Scanner } from "./scanner/index.js";
|
|
7
7
|
import { ReportGenerator } from "./report/generator.js";
|
|
8
|
-
import type { ScanOptions } from "./types.js";
|
|
8
|
+
import type { ScanOptions, ReportFormat } from "./types.js";
|
|
9
9
|
|
|
10
10
|
const program = new Command();
|
|
11
11
|
|
|
@@ -23,6 +23,11 @@ program
|
|
|
23
23
|
.option("--no-screenshots", "Disable screenshot capture")
|
|
24
24
|
.option("-l, --locale <locale>", "Browser locale for language detection", "fr-FR")
|
|
25
25
|
.option("-v, --verbose", "Show detailed output", false)
|
|
26
|
+
.option(
|
|
27
|
+
"-f, --format <formats>",
|
|
28
|
+
"Output formats: md, html, json, pdf (comma-separated)",
|
|
29
|
+
"md,pdf",
|
|
30
|
+
)
|
|
26
31
|
.action(async (url: string, opts) => {
|
|
27
32
|
console.log();
|
|
28
33
|
console.log(chalk.bold.blue(" GDPR Cookie Scanner"));
|
|
@@ -35,6 +40,17 @@ program
|
|
|
35
40
|
console.log(chalk.gray(` Output : ${outputDir}`));
|
|
36
41
|
console.log();
|
|
37
42
|
|
|
43
|
+
const validFormats = new Set<ReportFormat>(["md", "html", "json", "pdf"]);
|
|
44
|
+
const formats = (opts.format as string)
|
|
45
|
+
.split(",")
|
|
46
|
+
.map((f) => f.trim().toLowerCase())
|
|
47
|
+
.filter((f): f is ReportFormat => validFormats.has(f as ReportFormat));
|
|
48
|
+
|
|
49
|
+
if (formats.length === 0) {
|
|
50
|
+
console.error(chalk.red(" Invalid --format value. Valid options: md, html, json, pdf"));
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
const options: ScanOptions = {
|
|
39
55
|
url: normalizedUrl,
|
|
40
56
|
outputDir,
|
|
@@ -42,6 +58,7 @@ program
|
|
|
42
58
|
screenshots: opts.screenshots !== false,
|
|
43
59
|
locale: opts.locale,
|
|
44
60
|
verbose: opts.verbose,
|
|
61
|
+
formats,
|
|
45
62
|
};
|
|
46
63
|
|
|
47
64
|
const spinner = ora("Launching browser...").start();
|
|
@@ -58,7 +75,7 @@ program
|
|
|
58
75
|
console.log();
|
|
59
76
|
|
|
60
77
|
const generator = new ReportGenerator(options);
|
|
61
|
-
const
|
|
78
|
+
const paths = await generator.generate(result);
|
|
62
79
|
|
|
63
80
|
console.log(
|
|
64
81
|
chalk.bold(
|
|
@@ -81,8 +98,15 @@ program
|
|
|
81
98
|
console.log();
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
const labels: Record<string, string> = {
|
|
102
|
+
md: "Markdown",
|
|
103
|
+
html: "HTML",
|
|
104
|
+
json: "JSON",
|
|
105
|
+
pdf: "PDF",
|
|
106
|
+
};
|
|
107
|
+
for (const [fmt, path] of Object.entries(paths)) {
|
|
108
|
+
console.log(chalk.green(` ${(labels[fmt] ?? fmt).padEnd(8)} ${path}`));
|
|
109
|
+
}
|
|
86
110
|
console.log();
|
|
87
111
|
|
|
88
112
|
process.exit(result.compliance.grade === "F" ? 1 : 0);
|
package/src/report/generator.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { promisify } from "util";
|
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { Marked } from "marked";
|
|
7
7
|
import { generatePdf } from "./pdf.js";
|
|
8
|
+
import { generateHtmlReport } from "./html.js";
|
|
8
9
|
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const oxfmtBin = join(dirname(fileURLToPath(import.meta.url)), "../../node_modules/.bin/oxfmt");
|
|
@@ -20,39 +21,63 @@ import type { ScanOptions } from "../types.js";
|
|
|
20
21
|
export class ReportGenerator {
|
|
21
22
|
constructor(private readonly options: ScanOptions) {}
|
|
22
23
|
|
|
23
|
-
async generate(result: ScanResult): Promise<
|
|
24
|
+
async generate(result: ScanResult): Promise<Record<string, string>> {
|
|
24
25
|
await mkdir(this.options.outputDir, { recursive: true });
|
|
25
26
|
|
|
26
27
|
const hostname = new URL(result.url).hostname.replace(/^www\./, "");
|
|
27
28
|
const date = new Date(result.scanDate).toISOString().split("T")[0];
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
29
|
+
const base = `gdpr-report-${hostname}-${date}`;
|
|
30
|
+
const formats = this.options.formats;
|
|
31
|
+
|
|
32
|
+
const paths: Record<string, string> = {};
|
|
33
|
+
|
|
34
|
+
// ── Markdown ──────────────────────────────────────────────────
|
|
35
|
+
if (formats.includes("md")) {
|
|
36
|
+
const mdPath = join(this.options.outputDir, `${base}.md`);
|
|
37
|
+
await writeFile(mdPath, this.buildMarkdown(result), "utf-8");
|
|
38
|
+
await execFileAsync(oxfmtBin, [mdPath]).catch(() => {});
|
|
39
|
+
paths.md = mdPath;
|
|
40
|
+
|
|
41
|
+
const checklistPath = join(this.options.outputDir, `gdpr-checklist-${hostname}-${date}.md`);
|
|
42
|
+
await writeFile(checklistPath, this.buildChecklist(result), "utf-8");
|
|
43
|
+
await execFileAsync(oxfmtBin, [checklistPath]).catch(() => {});
|
|
44
|
+
|
|
45
|
+
const cookiesPath = join(this.options.outputDir, `gdpr-cookies-${hostname}-${date}.md`);
|
|
46
|
+
await writeFile(cookiesPath, this.buildCookiesInventory(result), "utf-8");
|
|
47
|
+
await execFileAsync(oxfmtBin, [cookiesPath]).catch(() => {});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── HTML ──────────────────────────────────────────────────────
|
|
51
|
+
if (formats.includes("html")) {
|
|
52
|
+
const htmlPath = join(this.options.outputDir, `${base}.html`);
|
|
53
|
+
await writeFile(htmlPath, generateHtmlReport(result), "utf-8");
|
|
54
|
+
paths.html = htmlPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── JSON ──────────────────────────────────────────────────────
|
|
58
|
+
if (formats.includes("json")) {
|
|
59
|
+
const jsonPath = join(this.options.outputDir, `${base}.json`);
|
|
60
|
+
await writeFile(jsonPath, JSON.stringify(result, null, 2), "utf-8");
|
|
61
|
+
paths.json = jsonPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── PDF (via Markdown → HTML → Playwright) ────────────────────
|
|
65
|
+
if (formats.includes("pdf")) {
|
|
66
|
+
const markdown = paths.md
|
|
67
|
+
? await import("fs/promises").then((m) => m.readFile(paths.md!, "utf-8"))
|
|
68
|
+
: this.buildMarkdown(result);
|
|
69
|
+
const checklist = this.buildChecklist(result);
|
|
70
|
+
const cookiesInventory = this.buildCookiesInventory(result);
|
|
71
|
+
const combined = [markdown, checklist, cookiesInventory].join("\n\n---\n\n");
|
|
72
|
+
const rawBody = await this.buildHtmlBody(combined);
|
|
73
|
+
const body = await this.inlineImages(rawBody, this.options.outputDir);
|
|
74
|
+
const html = this.wrapHtml(body, hostname);
|
|
75
|
+
const pdfPath = join(this.options.outputDir, `${base}.pdf`);
|
|
76
|
+
await generatePdf(html, pdfPath);
|
|
77
|
+
paths.pdf = pdfPath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return paths;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
private async buildHtmlBody(markdown: string): Promise<string> {
|