@slashgear/gdpr-cookie-scanner 3.0.0 → 3.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.
Files changed (38) hide show
  1. package/.github/workflows/update-trackers.yml +95 -0
  2. package/CHANGELOG.md +79 -0
  3. package/README.md +158 -9
  4. package/dist/analyzers/compliance.d.ts.map +1 -1
  5. package/dist/analyzers/compliance.js +33 -0
  6. package/dist/analyzers/compliance.js.map +1 -1
  7. package/dist/classifiers/cookie-classifier.d.ts.map +1 -1
  8. package/dist/classifiers/cookie-classifier.js +2 -1
  9. package/dist/classifiers/cookie-classifier.js.map +1 -1
  10. package/dist/classifiers/tracker-list.js +1 -1
  11. package/dist/classifiers/tracker-list.js.map +1 -1
  12. package/dist/cli.js +21 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/index.d.ts +51 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +46 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/report/generator.d.ts.map +1 -1
  19. package/dist/report/generator.js +12 -8
  20. package/dist/report/generator.js.map +1 -1
  21. package/dist/scanner/index.js +4 -4
  22. package/dist/scanner/index.js.map +1 -1
  23. package/dist/types.d.ts +1 -1
  24. package/dist/types.d.ts.map +1 -1
  25. package/package.json +11 -2
  26. package/scripts/update-trackers.ts +273 -0
  27. package/src/analyzers/compliance.ts +34 -0
  28. package/src/classifiers/cookie-classifier.ts +2 -1
  29. package/src/classifiers/tracker-list.ts +1 -1
  30. package/src/cli.ts +37 -1
  31. package/src/index.ts +87 -0
  32. package/src/report/generator.ts +13 -8
  33. package/src/scanner/index.ts +4 -4
  34. package/src/types.ts +1 -1
  35. package/tests/analyzers/compliance.test.ts +489 -0
  36. package/tests/analyzers/wording.test.ts +160 -0
  37. package/tests/classifiers/cookie-classifier.test.ts +270 -0
  38. package/tests/classifiers/network-classifier.test.ts +140 -0
@@ -0,0 +1,270 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { classifyCookie } from "../../src/classifiers/cookie-classifier.js";
3
+
4
+ describe("classifyCookie", () => {
5
+ // ── Strictly necessary ───────────────────────────────────────────
6
+
7
+ describe("strictly-necessary", () => {
8
+ it.each(["PHPSESSID", "JSESSIONID", "ASP.NET_SessionId", "__session"])(
9
+ "classifies %s as strictly-necessary",
10
+ (name) => {
11
+ const result = classifyCookie(name, "example.com", "abc123");
12
+ expect(result).toEqual({ category: "strictly-necessary", requiresConsent: false });
13
+ },
14
+ );
15
+
16
+ it("classifies session_id as strictly-necessary", () => {
17
+ expect(classifyCookie("session_id", "example.com", "xyz")).toEqual({
18
+ category: "strictly-necessary",
19
+ requiresConsent: false,
20
+ });
21
+ });
22
+
23
+ it.each(["csrf_token", "xsrf_token", "_token", "authenticity_token"])(
24
+ "classifies CSRF cookie %s as strictly-necessary",
25
+ (name) => {
26
+ expect(classifyCookie(name, "example.com", "token")).toEqual({
27
+ category: "strictly-necessary",
28
+ requiresConsent: false,
29
+ });
30
+ },
31
+ );
32
+
33
+ it.each(["auth_token", "authenticated", "login_session", "logged_in"])(
34
+ "classifies auth cookie %s as strictly-necessary",
35
+ (name) => {
36
+ expect(classifyCookie(name, "example.com", "1")).toEqual({
37
+ category: "strictly-necessary",
38
+ requiresConsent: false,
39
+ });
40
+ },
41
+ );
42
+
43
+ it.each(["cart_id", "basket", "checkout_token"])(
44
+ "classifies cart/checkout cookie %s as strictly-necessary",
45
+ (name) => {
46
+ expect(classifyCookie(name, "example.com", "abc")).toEqual({
47
+ category: "strictly-necessary",
48
+ requiresConsent: false,
49
+ });
50
+ },
51
+ );
52
+
53
+ it.each(["lang", "locale", "language", "country", "currency"])(
54
+ "classifies preference cookie %s as strictly-necessary",
55
+ (name) => {
56
+ expect(classifyCookie(name, "example.com", "fr")).toEqual({
57
+ category: "strictly-necessary",
58
+ requiresConsent: false,
59
+ });
60
+ },
61
+ );
62
+
63
+ it.each(["cookieconsent_status", "cookie_consent", "cc_cookie"])(
64
+ "classifies CMP storage cookie %s as strictly-necessary",
65
+ (name) => {
66
+ expect(classifyCookie(name, "example.com", "granted")).toEqual({
67
+ category: "strictly-necessary",
68
+ requiresConsent: false,
69
+ });
70
+ },
71
+ );
72
+
73
+ it.each(["axeptio_cookies", "didomi_token", "CookieConsent", "tarteaucitron"])(
74
+ "classifies known CMP cookie %s as strictly-necessary",
75
+ (name) => {
76
+ expect(classifyCookie(name, "example.com", "{}")).toEqual({
77
+ category: "strictly-necessary",
78
+ requiresConsent: false,
79
+ });
80
+ },
81
+ );
82
+ });
83
+
84
+ // ── Analytics ────────────────────────────────────────────────────
85
+
86
+ describe("analytics", () => {
87
+ it.each(["_ga", "_gid"])(
88
+ "classifies Google Analytics cookie %s as analytics requiring consent",
89
+ (name) => {
90
+ expect(classifyCookie(name, "example.com", "GA1.2.xxx")).toEqual({
91
+ category: "analytics",
92
+ requiresConsent: true,
93
+ });
94
+ },
95
+ );
96
+
97
+ it("classifies _ga_XXXXX (GA4 stream) as analytics", () => {
98
+ expect(classifyCookie("_ga_K1A2B3C4D5", "example.com", "GS1.1.xxx")).toEqual({
99
+ category: "analytics",
100
+ requiresConsent: true,
101
+ });
102
+ });
103
+
104
+ it.each(["_gat", "_gat_UA-123456"])(
105
+ "classifies GA rate-limiter cookie %s as analytics",
106
+ (name) => {
107
+ expect(classifyCookie(name, "example.com", "1")).toEqual({
108
+ category: "analytics",
109
+ requiresConsent: true,
110
+ });
111
+ },
112
+ );
113
+
114
+ it.each(["_utma", "_utmb", "_utmz"])("classifies legacy UTM cookie %s as analytics", (name) => {
115
+ expect(classifyCookie(name, "example.com", "xxx")).toEqual({
116
+ category: "analytics",
117
+ requiresConsent: true,
118
+ });
119
+ });
120
+
121
+ it("classifies __utmz as analytics", () => {
122
+ expect(classifyCookie("__utmz", "example.com", "xxx")).toEqual({
123
+ category: "analytics",
124
+ requiresConsent: true,
125
+ });
126
+ });
127
+
128
+ it("classifies Matomo _pk_ cookie as analytics", () => {
129
+ expect(classifyCookie("_pk_id.1.1fff", "example.com", "xxx")).toEqual({
130
+ category: "analytics",
131
+ requiresConsent: true,
132
+ });
133
+ });
134
+
135
+ it("classifies Amplitude amp_ cookie as analytics", () => {
136
+ expect(classifyCookie("amp_abc123", "example.com", "xxx")).toEqual({
137
+ category: "analytics",
138
+ requiresConsent: true,
139
+ });
140
+ });
141
+
142
+ it("classifies Hotjar _hj cookie as analytics", () => {
143
+ expect(classifyCookie("_hjSessionUser_123", "example.com", "xxx")).toEqual({
144
+ category: "analytics",
145
+ requiresConsent: true,
146
+ });
147
+ });
148
+
149
+ it("classifies Microsoft Clarity CLID as analytics", () => {
150
+ expect(classifyCookie("CLID", "example.com", "xxx")).toEqual({
151
+ category: "analytics",
152
+ requiresConsent: true,
153
+ });
154
+ });
155
+ });
156
+
157
+ // ── Advertising ──────────────────────────────────────────────────
158
+
159
+ describe("advertising", () => {
160
+ it.each(["_fbp", "_fbc", "fb_id"])(
161
+ "classifies Meta/Facebook cookie %s as advertising",
162
+ (name) => {
163
+ expect(classifyCookie(name, "example.com", "xxx")).toEqual({
164
+ category: "advertising",
165
+ requiresConsent: true,
166
+ });
167
+ },
168
+ );
169
+
170
+ it.each(["IDE", "NID", "DSID", "ANID", "__gads", "__gpi", "FCNEC"])(
171
+ "classifies Google Ads cookie %s as advertising",
172
+ (name) => {
173
+ expect(classifyCookie(name, "example.com", "xxx")).toEqual({
174
+ category: "advertising",
175
+ requiresConsent: true,
176
+ });
177
+ },
178
+ );
179
+
180
+ it("classifies MUID (Microsoft) as advertising", () => {
181
+ expect(classifyCookie("MUID", "example.com", "xxx")).toEqual({
182
+ category: "advertising",
183
+ requiresConsent: true,
184
+ });
185
+ });
186
+
187
+ it("classifies li_fat_id (LinkedIn) as advertising", () => {
188
+ expect(classifyCookie("li_fat_id", "example.com", "xxx")).toEqual({
189
+ category: "advertising",
190
+ requiresConsent: true,
191
+ });
192
+ });
193
+
194
+ it("classifies _ttp (TikTok) as advertising", () => {
195
+ expect(classifyCookie("_ttp", "example.com", "xxx")).toEqual({
196
+ category: "advertising",
197
+ requiresConsent: true,
198
+ });
199
+ });
200
+ });
201
+
202
+ // ── Social ───────────────────────────────────────────────────────
203
+
204
+ describe("social", () => {
205
+ it("classifies fbsr_ (Facebook login) as social", () => {
206
+ expect(classifyCookie("fbsr_123456", "example.com", "xxx")).toEqual({
207
+ category: "social",
208
+ requiresConsent: true,
209
+ });
210
+ });
211
+
212
+ it.each(["YSC", "VISITOR_INFO1_LIVE", "GPS"])(
213
+ "classifies YouTube cookie %s as social",
214
+ (name) => {
215
+ expect(classifyCookie(name, "example.com", "xxx")).toEqual({
216
+ category: "social",
217
+ requiresConsent: true,
218
+ });
219
+ },
220
+ );
221
+ });
222
+
223
+ // ── Personalization ──────────────────────────────────────────────
224
+
225
+ describe("personalization", () => {
226
+ it.each(["ab_test", "abt_variant", "abtest_bucket"])(
227
+ "classifies A/B test cookie %s as personalization",
228
+ (name) => {
229
+ expect(classifyCookie(name, "example.com", "variant_b")).toEqual({
230
+ category: "personalization",
231
+ requiresConsent: true,
232
+ });
233
+ },
234
+ );
235
+
236
+ it("classifies optimizely cookie as personalization", () => {
237
+ expect(classifyCookie("optimizely_data", "example.com", "xxx")).toEqual({
238
+ category: "personalization",
239
+ requiresConsent: true,
240
+ });
241
+ });
242
+ });
243
+
244
+ // ── Unknown / heuristics ─────────────────────────────────────────
245
+
246
+ describe("unknown", () => {
247
+ it("classifies unrecognised long-name cookie as unknown without consent", () => {
248
+ expect(classifyCookie("my_custom_app_cookie", "example.com", "somevalue=abc")).toEqual({
249
+ category: "unknown",
250
+ requiresConsent: false,
251
+ });
252
+ });
253
+
254
+ it("classifies short-name cookie (≤4 chars, no = in value) as unknown requiring consent", () => {
255
+ // Heuristic: suspicious session-like short cookie
256
+ expect(classifyCookie("sid", "example.com", "abc123")).toEqual({
257
+ category: "unknown",
258
+ requiresConsent: true,
259
+ });
260
+ });
261
+
262
+ it("does NOT flag short-name cookie as suspicious when value contains =", () => {
263
+ // Base64-encoded values contain = — should not be flagged
264
+ expect(classifyCookie("tok", "example.com", "abc=def")).toEqual({
265
+ category: "unknown",
266
+ requiresConsent: false,
267
+ });
268
+ });
269
+ });
270
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { classifyNetworkRequest } from "../../src/classifiers/network-classifier.js";
3
+
4
+ describe("classifyNetworkRequest", () => {
5
+ // ── Known trackers (exact hostname match) ────────────────────────
6
+
7
+ describe("tracker database — exact match", () => {
8
+ it("identifies Google Analytics", () => {
9
+ const result = classifyNetworkRequest("https://google-analytics.com/collect", "xhr");
10
+ expect(result.trackerCategory).toBe("analytics");
11
+ expect(result.trackerName).toBe("Google Analytics");
12
+ expect(result.requiresConsent).toBe(true);
13
+ expect(result.isThirdParty).toBe(true);
14
+ });
15
+
16
+ it("identifies Google Tag Manager", () => {
17
+ const result = classifyNetworkRequest(
18
+ "https://googletagmanager.com/gtm.js?id=GTM-XXXX",
19
+ "script",
20
+ );
21
+ expect(result.trackerCategory).toBe("analytics");
22
+ expect(result.trackerName).toBe("Google Tag Manager");
23
+ expect(result.requiresConsent).toBe(true);
24
+ });
25
+
26
+ it("identifies Meta Pixel (pixel.facebook.com)", () => {
27
+ const result = classifyNetworkRequest("https://pixel.facebook.com/tr?id=123", "xhr");
28
+ expect(result.trackerCategory).toBe("advertising");
29
+ expect(result.trackerName).toBe("Meta Pixel");
30
+ expect(result.requiresConsent).toBe(true);
31
+ });
32
+
33
+ it("identifies LinkedIn Insight Tag", () => {
34
+ const result = classifyNetworkRequest(
35
+ "https://snap.licdn.com/li.lms-analytics/insight.min.js",
36
+ "script",
37
+ );
38
+ expect(result.trackerCategory).toBe("advertising");
39
+ expect(result.requiresConsent).toBe(true);
40
+ });
41
+
42
+ it("identifies Hotjar", () => {
43
+ const result = classifyNetworkRequest("https://hotjar.com/api/v2/event", "xhr");
44
+ expect(result.trackerCategory).toBe("analytics");
45
+ expect(result.trackerName).toBe("Hotjar");
46
+ });
47
+
48
+ it("identifies Microsoft Clarity", () => {
49
+ const result = classifyNetworkRequest("https://clarity.ms/collect", "xhr");
50
+ expect(result.trackerCategory).toBe("analytics");
51
+ expect(result.requiresConsent).toBe(true);
52
+ });
53
+ });
54
+
55
+ // ── Subdomain suffix match ────────────────────────────────────────
56
+
57
+ describe("tracker database — subdomain match", () => {
58
+ it("matches subdomain of google-analytics.com", () => {
59
+ const result = classifyNetworkRequest("https://stats.google-analytics.com/r/collect", "xhr");
60
+ expect(result.trackerName).toBe("Google Analytics");
61
+ expect(result.isThirdParty).toBe(true);
62
+ });
63
+
64
+ it("strips www. before matching", () => {
65
+ const result = classifyNetworkRequest("https://www.google-analytics.com/collect", "xhr");
66
+ expect(result.trackerCategory).toBe("analytics");
67
+ });
68
+ });
69
+
70
+ // ── Pixel pattern matching ────────────────────────────────────────
71
+
72
+ describe("pixel patterns", () => {
73
+ it("identifies tracking pixels by URL pattern", () => {
74
+ const result = classifyNetworkRequest(
75
+ "https://track.example.com/pixel.gif?uid=123&ev=pageview",
76
+ "image",
77
+ );
78
+ // Either caught by pixel pattern or image heuristic
79
+ expect(result.trackerCategory).toBe("pixel");
80
+ expect(result.requiresConsent).toBe(true);
81
+ });
82
+ });
83
+
84
+ // ── Image heuristic ───────────────────────────────────────────────
85
+
86
+ describe("image resource heuristic", () => {
87
+ it("flags 1×1 gif with tracking params as pixel", () => {
88
+ const result = classifyNetworkRequest(
89
+ "https://metrics.unknown-vendor.com/track.gif?uid=abc&ts=1234",
90
+ "image",
91
+ );
92
+ expect(result.trackerCategory).toBe("pixel");
93
+ expect(result.requiresConsent).toBe(true);
94
+ });
95
+
96
+ it("does NOT flag regular image without tracking params", () => {
97
+ const result = classifyNetworkRequest("https://cdn.unknown-vendor.com/hero.png", "image");
98
+ expect(result.trackerCategory).toBeNull();
99
+ expect(result.requiresConsent).toBe(false);
100
+ });
101
+ });
102
+
103
+ // ── CDN — consent not required ────────────────────────────────────
104
+
105
+ describe("CDN entries", () => {
106
+ it("marks CDN requests as not requiring consent", () => {
107
+ // fbcdn.net is category 'social' not cdn, but let's test the cdn logic
108
+ // by checking that consentRequired=false entries in TRACKER_DB are respected
109
+ // Google fonts is not in the DB, so test a generic unknown
110
+ const result = classifyNetworkRequest("https://totally-unknown-cdn.io/lib.js", "script");
111
+ expect(result.requiresConsent).toBe(false);
112
+ expect(result.trackerCategory).toBeNull();
113
+ });
114
+ });
115
+
116
+ // ── Unknown / safe requests ───────────────────────────────────────
117
+
118
+ describe("unknown requests", () => {
119
+ it("returns null classification for an unknown domain", () => {
120
+ const result = classifyNetworkRequest("https://api.my-own-backend.com/data", "xhr");
121
+ expect(result.trackerCategory).toBeNull();
122
+ expect(result.trackerName).toBeNull();
123
+ expect(result.requiresConsent).toBe(false);
124
+ expect(result.isThirdParty).toBe(false);
125
+ });
126
+
127
+ it("returns safe fallback for an invalid URL", () => {
128
+ const result = classifyNetworkRequest("not-a-url", "xhr");
129
+ expect(result.trackerCategory).toBeNull();
130
+ expect(result.requiresConsent).toBe(false);
131
+ expect(result.isThirdParty).toBe(false);
132
+ });
133
+
134
+ it("returns safe fallback for an empty string", () => {
135
+ const result = classifyNetworkRequest("", "xhr");
136
+ expect(result.trackerCategory).toBeNull();
137
+ expect(result.requiresConsent).toBe(false);
138
+ });
139
+ });
140
+ });