@slashgear/gdpr-cookie-scanner 3.4.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.
@@ -17,6 +17,7 @@ import type {
17
17
  ConsentButton,
18
18
  } from "../types.js";
19
19
  import type { ScanOptions } from "../types.js";
20
+ import { lookupCookie } from "../classifiers/cookie-lookup.js";
20
21
 
21
22
  export class ReportGenerator {
22
23
  constructor(private readonly options: ScanOptions) {}
@@ -66,6 +67,13 @@ export class ReportGenerator {
66
67
  paths.json = jsonPath;
67
68
  }
68
69
 
70
+ // ── CSV ───────────────────────────────────────────────────────
71
+ if (formats.includes("csv")) {
72
+ const csvPath = join(outputDir, `gdpr-cookies-${hostname}-${date}.csv`);
73
+ await writeFile(csvPath, this.buildCookiesCsv(result), "utf-8");
74
+ paths.csv = csvPath;
75
+ }
76
+
69
77
  // ── PDF (via Markdown → HTML → Playwright) ────────────────────
70
78
  if (formats.includes("pdf")) {
71
79
  const markdown = paths.md
@@ -512,12 +520,14 @@ ${row("Cookie behavior", breakdown.cookieBehavior, 25)}
512
520
  return `${Math.round(days / 30)} months`;
513
521
  };
514
522
 
515
- const rows = filtered.map(
516
- (c) => `| \`${c.name}\` | ${c.domain} | ${c.category} | ${expires(c)} | ${consent(c)} |`,
517
- );
523
+ const rows = filtered.map((c) => {
524
+ const ocd = lookupCookie(c.name);
525
+ const desc = ocd ? ocd.description : "—";
526
+ return `| \`${c.name}\` | ${c.domain} | ${c.category} | ${expires(c)} | ${consent(c)} | ${desc} |`;
527
+ });
518
528
 
519
- return `| Name | Domain | Category | Expiry | Consent required |
520
- |------|--------|----------|--------|------------------|
529
+ return `| Name | Domain | Category | Expiry | Consent required | Description |
530
+ |------|--------|----------|--------|------------------|-------------|
521
531
  ${rows.join("\n")}
522
532
  `;
523
533
  }
@@ -762,8 +772,10 @@ The **Description / Purpose** column is to be filled in by the DPO or technical
762
772
  const phases = [...entry.phases].join(", ");
763
773
  const consent = entry.requiresConsent ? "⚠️ Yes" : "✅ No";
764
774
  const cat = categoryLabel[entry.category] ?? entry.category;
775
+ const ocd = lookupCookie(entry.name);
776
+ const desc = ocd ? ocd.description : "<!-- fill in -->";
765
777
  lines.push(
766
- `| \`${entry.name}\` | ${entry.domain} | ${cat} | ${phases} | ${expires(entry)} | ${consent} | <!-- fill in --> |`,
778
+ `| \`${entry.name}\` | ${entry.domain} | ${cat} | ${phases} | ${expires(entry)} | ${consent} | ${desc} |`,
767
779
  );
768
780
  }
769
781
 
@@ -1076,4 +1088,105 @@ The **Description / Purpose** column is to be filled in by the DPO or technical
1076
1088
 
1077
1089
  return lines.join("\n") + "\n";
1078
1090
  }
1091
+
1092
+ private buildCookiesCsv(r: ScanResult): string {
1093
+ type CsvEntry = {
1094
+ name: string;
1095
+ domain: string;
1096
+ category: string;
1097
+ phases: Set<string>;
1098
+ expires: number | null;
1099
+ httpOnly: boolean;
1100
+ secure: boolean;
1101
+ sameSite: string | null;
1102
+ requiresConsent: boolean;
1103
+ type: string;
1104
+ };
1105
+
1106
+ const cookieMap = new Map<string, CsvEntry>();
1107
+
1108
+ const phaseLabel: Record<ScannedCookie["capturedAt"], string> = {
1109
+ "before-interaction": "before consent",
1110
+ "after-accept": "after acceptance",
1111
+ "after-reject": "after rejection",
1112
+ };
1113
+
1114
+ const allCookies = [
1115
+ ...r.cookiesBeforeInteraction,
1116
+ ...r.cookiesAfterAccept,
1117
+ ...r.cookiesAfterReject,
1118
+ ];
1119
+
1120
+ for (const c of allCookies) {
1121
+ const key = `${c.name}||${c.domain}`;
1122
+ if (!cookieMap.has(key)) {
1123
+ cookieMap.set(key, {
1124
+ name: c.name,
1125
+ domain: c.domain,
1126
+ category: c.category,
1127
+ phases: new Set(),
1128
+ expires: c.expires,
1129
+ httpOnly: c.httpOnly,
1130
+ secure: c.secure,
1131
+ sameSite: c.sameSite,
1132
+ requiresConsent: c.requiresConsent,
1133
+ type: c.expires === null ? "Session" : "Persistent",
1134
+ });
1135
+ }
1136
+ cookieMap.get(key)!.phases.add(phaseLabel[c.capturedAt]);
1137
+ }
1138
+
1139
+ const expiryStr = (entry: CsvEntry): string => {
1140
+ if (entry.expires === null) return "Session";
1141
+ const days = Math.round((entry.expires * 1000 - Date.now()) / 86400000);
1142
+ if (days < 0) return "Expired";
1143
+ if (days === 0) return "< 1 day";
1144
+ if (days < 30) return `${days} days`;
1145
+ return `${Math.round(days / 30)} months`;
1146
+ };
1147
+
1148
+ const header =
1149
+ '"name","domain","category","description","platform","ocd_retention_period","privacy_link","expiry","type","consent_required","phases","http_only","secure","same_site"';
1150
+
1151
+ const rows = [...cookieMap.values()]
1152
+ .sort((a, b) => {
1153
+ const order = [
1154
+ "strictly-necessary",
1155
+ "analytics",
1156
+ "advertising",
1157
+ "social",
1158
+ "personalization",
1159
+ "unknown",
1160
+ ];
1161
+ const oa = order.indexOf(a.category);
1162
+ const ob = order.indexOf(b.category);
1163
+ if (oa !== ob) return oa - ob;
1164
+ return a.name.localeCompare(b.name);
1165
+ })
1166
+ .map((entry) => {
1167
+ const ocd = lookupCookie(entry.name);
1168
+ return [
1169
+ csvEscape(entry.name),
1170
+ csvEscape(entry.domain),
1171
+ csvEscape(entry.category),
1172
+ csvEscape(ocd?.description ?? ""),
1173
+ csvEscape(ocd?.platform ?? ""),
1174
+ csvEscape(ocd?.retentionPeriod ?? ""),
1175
+ csvEscape(ocd?.privacyLink ?? ""),
1176
+ csvEscape(expiryStr(entry)),
1177
+ csvEscape(entry.type),
1178
+ csvEscape(entry.requiresConsent ? "yes" : "no"),
1179
+ csvEscape([...entry.phases].join("; ")),
1180
+ csvEscape(entry.httpOnly ? "true" : "false"),
1181
+ csvEscape(entry.secure ? "true" : "false"),
1182
+ csvEscape(entry.sameSite ?? ""),
1183
+ ].join(",");
1184
+ });
1185
+
1186
+ return [header, ...rows].join("\n") + "\n";
1187
+ }
1188
+ }
1189
+
1190
+ function csvEscape(value: string): string {
1191
+ return `"${value.replace(/"/g, '""')}"`;
1079
1192
  }
@@ -1,4 +1,5 @@
1
1
  import type { ScanResult, ScannedCookie, DarkPatternIssue, ConsentButton } from "../types.js";
2
+ import { lookupCookie } from "../classifiers/cookie-lookup.js";
2
3
 
3
4
  const GRADE_COLOR: Record<string, string> = {
4
5
  A: "#16a34a",
@@ -279,6 +280,14 @@ export function generateHtmlReport(result: ScanResult): string {
279
280
  border-radius: 4px;
280
281
  border: 1px solid var(--border);
281
282
  }
283
+ .cookie-name {
284
+ display: inline-block;
285
+ max-width: 220px;
286
+ overflow: hidden;
287
+ text-overflow: ellipsis;
288
+ white-space: nowrap;
289
+ vertical-align: bottom;
290
+ }
282
291
  .empty-state {
283
292
  text-align: center;
284
293
  padding: 32px;
@@ -616,10 +625,15 @@ function cookieTable(cookies: ScannedCookie[]): string {
616
625
  const consent = c.requiresConsent
617
626
  ? `<span class="badge badge-warning">Required</span>`
618
627
  : `<span class="badge badge-muted">No</span>`;
628
+ const ocd = lookupCookie(c.name);
629
+ const descCell = ocd
630
+ ? `<span title="${esc(ocd.platform)}${ocd.privacyLink ? ` — ${esc(ocd.privacyLink)}` : ""}">${esc(ocd.description)}</span>`
631
+ : `<span style="color:var(--text-muted)">—</span>`;
619
632
  return `<tr>
620
- <td><code>${esc(c.name)}</code></td>
633
+ <td><code class="cookie-name" title="${esc(c.name)}">${esc(c.name)}</code></td>
621
634
  <td style="color:var(--text-muted)">${esc(c.domain)}</td>
622
635
  <td><span class="badge badge-muted">${esc(c.category)}</span></td>
636
+ <td>${descCell}</td>
623
637
  <td style="color:var(--text-muted)">${formatExpiry(c)}</td>
624
638
  <td>${consent}</td>
625
639
  </tr>`;
@@ -628,7 +642,7 @@ function cookieTable(cookies: ScannedCookie[]): string {
628
642
 
629
643
  return `<table class="data-table">
630
644
  <thead><tr>
631
- <th>Name</th><th>Domain</th><th>Category</th><th>Expiry</th><th>Consent</th>
645
+ <th>Name</th><th>Domain</th><th>Category</th><th>Description</th><th>Expiry</th><th>Consent</th>
632
646
  </tr></thead>
633
647
  <tbody>${rows}</tbody>
634
648
  </table>`;
package/src/types.ts CHANGED
@@ -109,7 +109,7 @@ export interface ComplianceScore {
109
109
  grade: "A" | "B" | "C" | "D" | "F";
110
110
  }
111
111
 
112
- export type ReportFormat = "md" | "html" | "json" | "pdf";
112
+ export type ReportFormat = "md" | "html" | "json" | "pdf" | "csv";
113
113
 
114
114
  export type ViewportPreset = "desktop" | "tablet" | "mobile";
115
115