@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.
- package/.github/workflows/update-cookie-db.yml +94 -0
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +18 -0
- package/README.md +11 -10
- package/dist/classifiers/cookie-lookup.d.ts +8 -0
- package/dist/classifiers/cookie-lookup.d.ts.map +1 -0
- package/dist/classifiers/cookie-lookup.js +50 -0
- package/dist/classifiers/cookie-lookup.js.map +1 -0
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/data/open-cookie-database.json +25614 -0
- package/dist/report/generator.d.ts +1 -0
- package/dist/report/generator.d.ts.map +1 -1
- package/dist/report/generator.js +100 -4
- package/dist/report/generator.js.map +1 -1
- package/dist/report/html.d.ts.map +1 -1
- package/dist/report/html.js +16 -2
- package/dist/report/html.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -3
- package/scripts/copy-data.mjs +4 -0
- package/scripts/update-cookie-db.mjs +15 -0
- package/src/classifiers/cookie-lookup.ts +72 -0
- package/src/cli.ts +8 -3
- package/src/data/open-cookie-database.json +25614 -0
- package/src/report/generator.ts +119 -6
- package/src/report/html.ts +16 -2
- package/src/types.ts +1 -1
package/src/report/generator.ts
CHANGED
|
@@ -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
|
-
|
|
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} |
|
|
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
|
}
|
package/src/report/html.ts
CHANGED
|
@@ -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
|
|