@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.
Files changed (78) hide show
  1. package/.dockerignore +13 -0
  2. package/.github/workflows/ci.yml +8 -2
  3. package/.github/workflows/docker.yml +49 -0
  4. package/.github/workflows/pages.yml +40 -0
  5. package/.github/workflows/release.yml +1 -1
  6. package/.nvmrc +1 -0
  7. package/CHANGELOG.md +87 -0
  8. package/Dockerfile +36 -0
  9. package/README.md +44 -63
  10. package/dist/cli.js +21 -3
  11. package/dist/cli.js.map +1 -1
  12. package/dist/report/generator.d.ts +1 -4
  13. package/dist/report/generator.d.ts.map +1 -1
  14. package/dist/report/generator.js +45 -23
  15. package/dist/report/generator.js.map +1 -1
  16. package/dist/report/html.d.ts +3 -0
  17. package/dist/report/html.d.ts.map +1 -0
  18. package/dist/report/html.js +766 -0
  19. package/dist/report/html.js.map +1 -0
  20. package/dist/types.d.ts +2 -0
  21. package/dist/types.d.ts.map +1 -1
  22. package/docs/index.html +314 -0
  23. package/docs/reports/github.com/after-accept.png +0 -0
  24. package/docs/reports/github.com/after-reject.png +0 -0
  25. package/docs/reports/github.com/gdpr-checklist-github.com-2026-02-22.md +44 -0
  26. package/docs/reports/github.com/gdpr-cookies-github.com-2026-02-22.md +29 -0
  27. package/docs/reports/github.com/gdpr-report-github.com-2026-02-22.md +102 -0
  28. package/docs/reports/github.com/gdpr-report-github.com-2026-02-22.pdf +0 -0
  29. package/docs/reports/gitlab.com/after-accept.png +0 -0
  30. package/docs/reports/gitlab.com/after-reject.png +0 -0
  31. package/docs/reports/gitlab.com/gdpr-checklist-gitlab.com-2026-02-22.md +44 -0
  32. package/docs/reports/gitlab.com/gdpr-cookies-gitlab.com-2026-02-22.md +55 -0
  33. package/docs/reports/gitlab.com/gdpr-report-gitlab.com-2026-02-22.md +200 -0
  34. package/docs/reports/gitlab.com/gdpr-report-gitlab.com-2026-02-22.pdf +0 -0
  35. package/docs/reports/gitlab.com/modal-initial.png +0 -0
  36. package/docs/reports/npmjs.com/after-accept.png +0 -0
  37. package/docs/reports/npmjs.com/after-reject.png +0 -0
  38. package/docs/reports/npmjs.com/gdpr-checklist-npmjs.com-2026-02-22.md +44 -0
  39. package/docs/reports/npmjs.com/gdpr-cookies-npmjs.com-2026-02-22.md +25 -0
  40. package/docs/reports/npmjs.com/gdpr-report-npmjs.com-2026-02-22.md +88 -0
  41. package/docs/reports/npmjs.com/gdpr-report-npmjs.com-2026-02-22.pdf +0 -0
  42. package/docs/reports/reddit.com/after-accept.png +0 -0
  43. package/docs/reports/reddit.com/after-reject.png +0 -0
  44. package/docs/reports/reddit.com/gdpr-checklist-reddit.com-2026-02-22.md +44 -0
  45. package/docs/reports/reddit.com/gdpr-cookies-reddit.com-2026-02-22.md +33 -0
  46. package/docs/reports/reddit.com/gdpr-report-reddit.com-2026-02-22.md +148 -0
  47. package/docs/reports/reddit.com/gdpr-report-reddit.com-2026-02-22.pdf +0 -0
  48. package/docs/reports/reddit.com/modal-initial.png +0 -0
  49. package/docs/reports/stackoverflow.com/after-accept.png +0 -0
  50. package/docs/reports/stackoverflow.com/after-reject.png +0 -0
  51. package/docs/reports/stackoverflow.com/gdpr-checklist-stackoverflow.com-2026-02-22.md +44 -0
  52. package/docs/reports/stackoverflow.com/gdpr-cookies-stackoverflow.com-2026-02-22.md +67 -0
  53. package/docs/reports/stackoverflow.com/gdpr-report-stackoverflow.com-2026-02-22.md +206 -0
  54. package/docs/reports/stackoverflow.com/gdpr-report-stackoverflow.com-2026-02-22.pdf +0 -0
  55. package/docs/reports/stackoverflow.com/modal-initial.png +0 -0
  56. package/docs/reports/www.afp.com/after-accept.png +0 -0
  57. package/docs/reports/www.afp.com/after-reject.png +0 -0
  58. package/docs/reports/www.afp.com/gdpr-checklist-afp.com-2026-02-22.md +44 -0
  59. package/docs/reports/www.afp.com/gdpr-cookies-afp.com-2026-02-22.md +42 -0
  60. package/docs/reports/www.afp.com/gdpr-report-afp.com-2026-02-22.md +202 -0
  61. package/docs/reports/www.afp.com/gdpr-report-afp.com-2026-02-22.pdf +0 -0
  62. package/docs/reports/www.afp.com/modal-initial.png +0 -0
  63. package/docs/style.css +439 -0
  64. package/package.json +10 -7
  65. package/src/cli.ts +28 -4
  66. package/src/report/generator.ts +54 -29
  67. package/src/report/html.ts +940 -0
  68. package/src/types.ts +3 -0
  69. package/tests/e2e/fixtures/compliant-site.html +80 -0
  70. package/tests/e2e/fixtures/no-modal-site.html +17 -0
  71. package/tests/e2e/fixtures/non-compliant-site.html +54 -0
  72. package/tests/e2e/scanner.test.ts +135 -0
  73. package/tests/helpers/test-server.ts +57 -0
  74. package/tests/unit/compliance.test.ts +460 -0
  75. package/tests/unit/cookie-classifier.test.ts +192 -0
  76. package/tests/unit/network-classifier.test.ts +91 -0
  77. package/tests/unit/wording.test.ts +162 -0
  78. 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 { reportPath, pdfPath } = await generator.generate(result);
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
- console.log(chalk.green(` Report saved: ${reportPath}`));
85
- console.log(chalk.green(` PDF saved: ${pdfPath}`));
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);
@@ -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<{ reportPath: string; pdfPath: string }> {
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 filename = `gdpr-report-${hostname}-${date}.md`;
29
- const outputPath = join(this.options.outputDir, filename);
30
-
31
- const markdown = this.buildMarkdown(result);
32
- await writeFile(outputPath, markdown, "utf-8");
33
- await execFileAsync(oxfmtBin, [outputPath]).catch(() => {});
34
-
35
- const checklistFilename = `gdpr-checklist-${hostname}-${date}.md`;
36
- const checklistPath = join(this.options.outputDir, checklistFilename);
37
- const checklist = this.buildChecklist(result);
38
- await writeFile(checklistPath, checklist, "utf-8");
39
- await execFileAsync(oxfmtBin, [checklistPath]).catch(() => {});
40
-
41
- const cookiesFilename = `gdpr-cookies-${hostname}-${date}.md`;
42
- const cookiesPath = join(this.options.outputDir, cookiesFilename);
43
- const cookiesInventory = this.buildCookiesInventory(result);
44
- await writeFile(cookiesPath, cookiesInventory, "utf-8");
45
- await execFileAsync(oxfmtBin, [cookiesPath]).catch(() => {});
46
-
47
- const combined = [markdown, checklist, cookiesInventory].join("\n\n---\n\n");
48
- const rawBody = await this.buildHtmlBody(combined);
49
- const body = await this.inlineImages(rawBody, this.options.outputDir);
50
- const html = this.wrapHtml(body, hostname);
51
- const pdfFilename = `gdpr-report-${hostname}-${date}.pdf`;
52
- const pdfPath = join(this.options.outputDir, pdfFilename);
53
- await generatePdf(html, pdfPath);
54
-
55
- return { reportPath: outputPath, pdfPath };
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> {