privalint 0.0.0-stage → 0.1.1

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +175 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/consent.d.ts +12 -0
  7. package/dist/consent.js +185 -0
  8. package/dist/consent.js.map +1 -0
  9. package/dist/data/third-parties.d.ts +13 -0
  10. package/dist/data/third-parties.js +42 -0
  11. package/dist/data/third-parties.js.map +1 -0
  12. package/dist/data/trackers.d.ts +21 -0
  13. package/dist/data/trackers.js +135 -0
  14. package/dist/data/trackers.js.map +1 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/meta.d.ts +3 -0
  19. package/dist/meta.js +6 -0
  20. package/dist/meta.js.map +1 -0
  21. package/dist/reporters/index.d.ts +11 -0
  22. package/dist/reporters/index.js +19 -0
  23. package/dist/reporters/index.js.map +1 -0
  24. package/dist/reporters/json.d.ts +2 -0
  25. package/dist/reporters/json.js +2 -0
  26. package/dist/reporters/json.js.map +1 -0
  27. package/dist/reporters/markdown.d.ts +9 -0
  28. package/dist/reporters/markdown.js +47 -0
  29. package/dist/reporters/markdown.js.map +1 -0
  30. package/dist/reporters/sarif.d.ts +10 -0
  31. package/dist/reporters/sarif.js +78 -0
  32. package/dist/reporters/sarif.js.map +1 -0
  33. package/dist/reporters/shared.d.ts +5 -0
  34. package/dist/reporters/shared.js +34 -0
  35. package/dist/reporters/shared.js.map +1 -0
  36. package/dist/reporters/text.d.ts +2 -0
  37. package/dist/reporters/text.js +52 -0
  38. package/dist/reporters/text.js.map +1 -0
  39. package/dist/rules/google-fonts.d.ts +2 -0
  40. package/dist/rules/google-fonts.js +51 -0
  41. package/dist/rules/google-fonts.js.map +1 -0
  42. package/dist/rules/impressum-link.d.ts +2 -0
  43. package/dist/rules/impressum-link.js +46 -0
  44. package/dist/rules/impressum-link.js.map +1 -0
  45. package/dist/rules/index.d.ts +11 -0
  46. package/dist/rules/index.js +20 -0
  47. package/dist/rules/index.js.map +1 -0
  48. package/dist/rules/links.d.ts +6 -0
  49. package/dist/rules/links.js +14 -0
  50. package/dist/rules/links.js.map +1 -0
  51. package/dist/rules/pre-consent-trackers.d.ts +2 -0
  52. package/dist/rules/pre-consent-trackers.js +32 -0
  53. package/dist/rules/pre-consent-trackers.js.map +1 -0
  54. package/dist/rules/privacy-policy-link.d.ts +2 -0
  55. package/dist/rules/privacy-policy-link.js +26 -0
  56. package/dist/rules/privacy-policy-link.js.map +1 -0
  57. package/dist/rules/reject-option.d.ts +2 -0
  58. package/dist/rules/reject-option.js +46 -0
  59. package/dist/rules/reject-option.js.map +1 -0
  60. package/dist/rules/third-party-cdns.d.ts +2 -0
  61. package/dist/rules/third-party-cdns.js +23 -0
  62. package/dist/rules/third-party-cdns.js.map +1 -0
  63. package/dist/rules/third-party-embeds.d.ts +2 -0
  64. package/dist/rules/third-party-embeds.js +23 -0
  65. package/dist/rules/third-party-embeds.js.map +1 -0
  66. package/dist/rules/third-party-hits.d.ts +4 -0
  67. package/dist/rules/third-party-hits.js +13 -0
  68. package/dist/rules/third-party-hits.js.map +1 -0
  69. package/dist/rules/tracker-hits.d.ts +9 -0
  70. package/dist/rules/tracker-hits.js +29 -0
  71. package/dist/rules/tracker-hits.js.map +1 -0
  72. package/dist/rules/trackers-after-reject.d.ts +2 -0
  73. package/dist/rules/trackers-after-reject.js +27 -0
  74. package/dist/rules/trackers-after-reject.js.map +1 -0
  75. package/dist/scanner.d.ts +33 -0
  76. package/dist/scanner.js +167 -0
  77. package/dist/scanner.js.map +1 -0
  78. package/dist/types.d.ts +97 -0
  79. package/dist/types.js +2 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/url.d.ts +26 -0
  82. package/dist/url.js +71 -0
  83. package/dist/url.js.map +1 -0
  84. package/package.json +60 -4
@@ -0,0 +1,42 @@
1
+ import { findByUrl } from '../url.js';
2
+ /**
3
+ * Embedded content that transfers the visitor's IP address (and often sets
4
+ * cookies) as soon as it loads. Trackers are listed separately in trackers.ts.
5
+ */
6
+ export const embeds = [
7
+ { vendor: 'YouTube', domains: ['youtube.com', 'youtube-nocookie.com', 'ytimg.com'] },
8
+ { vendor: 'Vimeo', domains: ['player.vimeo.com', 'vimeocdn.com'] },
9
+ {
10
+ vendor: 'Google Maps',
11
+ domains: ['maps.googleapis.com', 'maps.gstatic.com', 'maps.google.com'],
12
+ },
13
+ { vendor: 'Google Maps', domains: ['google.com'], paths: ['/maps/'] },
14
+ { vendor: 'Google reCAPTCHA', domains: ['recaptcha.net'] },
15
+ { vendor: 'Google reCAPTCHA', domains: ['google.com', 'gstatic.com'], paths: ['/recaptcha/'] },
16
+ { vendor: 'Gravatar', domains: ['gravatar.com'] },
17
+ { vendor: 'X (Twitter) embed', domains: ['platform.twitter.com', 'syndication.twitter.com'] },
18
+ { vendor: 'Instagram embed', domains: ['instagram.com', 'cdninstagram.com'] },
19
+ { vendor: 'Facebook plugin', domains: ['facebook.com'], paths: ['/plugins/'] },
20
+ { vendor: 'Spotify embed', domains: ['open.spotify.com'], paths: ['/embed/'] },
21
+ { vendor: 'Disqus', domains: ['disqus.com', 'disquscdn.com'] },
22
+ ];
23
+ /** Public CDNs for scripts, styles and icon fonts. */
24
+ export const cdns = [
25
+ { vendor: 'jsDelivr', domains: ['cdn.jsdelivr.net'] },
26
+ { vendor: 'unpkg', domains: ['unpkg.com'] },
27
+ { vendor: 'cdnjs', domains: ['cdnjs.cloudflare.com'] },
28
+ { vendor: 'Google Hosted Libraries', domains: ['ajax.googleapis.com'] },
29
+ { vendor: 'jQuery CDN', domains: ['code.jquery.com'] },
30
+ {
31
+ vendor: 'BootstrapCDN',
32
+ domains: ['stackpath.bootstrapcdn.com', 'maxcdn.bootstrapcdn.com'],
33
+ },
34
+ { vendor: 'Tailwind Play CDN', domains: ['cdn.tailwindcss.com'] },
35
+ {
36
+ vendor: 'Font Awesome CDN',
37
+ domains: ['use.fontawesome.com', 'kit.fontawesome.com', 'ka-f.fontawesome.com'],
38
+ },
39
+ ];
40
+ export const findEmbed = (url) => findByUrl(embeds, url);
41
+ export const findCdn = (url) => findByUrl(cdns, url);
42
+ //# sourceMappingURL=third-parties.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"third-parties.js","sourceRoot":"","sources":["../../src/data/third-parties.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,WAAW,CAAC;AAMxD;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAA0B;IAC3C,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,aAAa,EAAE,sBAAsB,EAAE,WAAW,CAAC,EAAE;IACpF,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,kBAAkB,EAAE,cAAc,CAAC,EAAE;IAClE;QACE,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE,CAAC,qBAAqB,EAAE,kBAAkB,EAAE,iBAAiB,CAAC;KACxE;IACD,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE;IACrE,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAC,eAAe,CAAC,EAAE;IAC1D,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAC,YAAY,EAAE,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE;IAC9F,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,cAAc,CAAC,EAAE;IACjD,EAAE,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAC,sBAAsB,EAAE,yBAAyB,CAAC,EAAE;IAC7F,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,eAAe,EAAE,kBAAkB,CAAC,EAAE;IAC7E,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC,WAAW,CAAC,EAAE;IAC9E,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,kBAAkB,CAAC,EAAE,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE;IAC9E,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,YAAY,EAAE,eAAe,CAAC,EAAE;CAC/D,CAAC;AAEF,sDAAsD;AACtD,MAAM,CAAC,MAAM,IAAI,GAA0B;IACzC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,kBAAkB,CAAC,EAAE;IACrD,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE;IAC3C,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,sBAAsB,CAAC,EAAE;IACtD,EAAE,MAAM,EAAE,yBAAyB,EAAE,OAAO,EAAE,CAAC,qBAAqB,CAAC,EAAE;IACvE,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,iBAAiB,CAAC,EAAE;IACtD;QACE,MAAM,EAAE,cAAc;QACtB,OAAO,EAAE,CAAC,4BAA4B,EAAE,yBAAyB,CAAC;KACnE;IACD,EAAE,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAC,qBAAqB,CAAC,EAAE;IACjE;QACE,MAAM,EAAE,kBAAkB;QAC1B,OAAO,EAAE,CAAC,qBAAqB,EAAE,qBAAqB,EAAE,sBAAsB,CAAC;KAChF;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACjE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { type HostPattern } from '../url.js';
2
+ export type TrackerCategory = 'analytics' | 'advertising' | 'session-replay' | 'social' | 'marketing-automation' | 'tag-manager';
3
+ export interface Tracker extends HostPattern {
4
+ vendor: string;
5
+ category: TrackerCategory;
6
+ /**
7
+ * Cookies the vendor's script sets, including on the first-party domain.
8
+ * Strings are name prefixes; use a RegExp where a prefix would be too broad.
9
+ */
10
+ cookies?: Array<string | RegExp>;
11
+ }
12
+ /**
13
+ * Hand-maintained and MIT licensed, so it can ship with the package.
14
+ * Order matters: the first entry that matches wins, so put path-specific
15
+ * entries before broader ones on the same domain.
16
+ *
17
+ * Adding a vendor? See CONTRIBUTING.md.
18
+ */
19
+ export declare const trackers: readonly Tracker[];
20
+ export declare function findTrackerByUrl(url: string): Tracker | undefined;
21
+ export declare function findTrackerByCookie(name: string): Tracker | undefined;
@@ -0,0 +1,135 @@
1
+ import { findByUrl } from '../url.js';
2
+ /**
3
+ * Hand-maintained and MIT licensed, so it can ship with the package.
4
+ * Order matters: the first entry that matches wins, so put path-specific
5
+ * entries before broader ones on the same domain.
6
+ *
7
+ * Adding a vendor? See CONTRIBUTING.md.
8
+ */
9
+ export const trackers = [
10
+ {
11
+ vendor: 'Google Analytics',
12
+ category: 'analytics',
13
+ domains: ['google-analytics.com', 'analytics.google.com'],
14
+ cookies: ['_ga', '_gid', '_gat'],
15
+ },
16
+ {
17
+ vendor: 'Google Analytics',
18
+ category: 'analytics',
19
+ domains: ['googletagmanager.com'],
20
+ paths: ['/gtag/'],
21
+ },
22
+ {
23
+ vendor: 'Google Tag Manager',
24
+ category: 'tag-manager',
25
+ domains: ['googletagmanager.com'],
26
+ },
27
+ {
28
+ vendor: 'Google Ads',
29
+ category: 'advertising',
30
+ domains: ['googleadservices.com', 'doubleclick.net', 'googlesyndication.com'],
31
+ cookies: ['_gcl_'],
32
+ },
33
+ {
34
+ vendor: 'Meta Pixel',
35
+ category: 'advertising',
36
+ domains: ['connect.facebook.net'],
37
+ cookies: ['_fbp', '_fbc'],
38
+ },
39
+ {
40
+ vendor: 'Meta Pixel',
41
+ category: 'advertising',
42
+ domains: ['facebook.com'],
43
+ paths: ['/tr'],
44
+ },
45
+ {
46
+ vendor: 'Hotjar',
47
+ category: 'session-replay',
48
+ domains: ['hotjar.com', 'hotjar.io'],
49
+ cookies: ['_hj'],
50
+ },
51
+ {
52
+ vendor: 'Microsoft Clarity',
53
+ category: 'session-replay',
54
+ domains: ['clarity.ms'],
55
+ cookies: ['_clck', '_clsk'],
56
+ },
57
+ {
58
+ vendor: 'Microsoft Advertising',
59
+ category: 'advertising',
60
+ domains: ['bat.bing.com'],
61
+ cookies: ['_uetsid', '_uetvid'],
62
+ },
63
+ {
64
+ vendor: 'LinkedIn Insight',
65
+ category: 'advertising',
66
+ domains: ['snap.licdn.com', 'px.ads.linkedin.com'],
67
+ cookies: ['li_fat_id'],
68
+ },
69
+ {
70
+ vendor: 'TikTok Pixel',
71
+ category: 'advertising',
72
+ domains: ['analytics.tiktok.com'],
73
+ cookies: ['_ttp'],
74
+ },
75
+ {
76
+ vendor: 'X (Twitter) Pixel',
77
+ category: 'advertising',
78
+ domains: ['static.ads-twitter.com', 'analytics.twitter.com'],
79
+ },
80
+ {
81
+ vendor: 'Pinterest Tag',
82
+ category: 'advertising',
83
+ domains: ['ct.pinterest.com'],
84
+ cookies: ['_pin_unauth'],
85
+ },
86
+ {
87
+ vendor: 'Snap Pixel',
88
+ category: 'advertising',
89
+ domains: ['sc-static.net', 'tr.snapchat.com'],
90
+ cookies: ['_scid'],
91
+ },
92
+ {
93
+ vendor: 'Yandex Metrica',
94
+ category: 'analytics',
95
+ domains: ['mc.yandex.ru', 'mc.yandex.com'],
96
+ cookies: ['_ym_'],
97
+ },
98
+ {
99
+ vendor: 'HubSpot',
100
+ category: 'marketing-automation',
101
+ domains: ['js.hs-scripts.com', 'js.hs-analytics.net', 'track.hubspot.com'],
102
+ cookies: ['__hstc', 'hubspotutk'],
103
+ },
104
+ {
105
+ vendor: 'Segment',
106
+ category: 'analytics',
107
+ domains: ['cdn.segment.com', 'api.segment.io'],
108
+ cookies: ['ajs_'],
109
+ },
110
+ {
111
+ vendor: 'Mixpanel',
112
+ category: 'analytics',
113
+ domains: ['cdn.mxpnl.com', 'api-js.mixpanel.com'],
114
+ cookies: [/^mp_\w+_mixpanel$/],
115
+ },
116
+ {
117
+ vendor: 'Amplitude',
118
+ category: 'analytics',
119
+ domains: ['cdn.amplitude.com', 'api2.amplitude.com'],
120
+ cookies: ['AMP_'],
121
+ },
122
+ {
123
+ vendor: 'Matomo',
124
+ category: 'analytics',
125
+ domains: ['matomo.cloud'],
126
+ cookies: ['_pk_'],
127
+ },
128
+ ];
129
+ export function findTrackerByUrl(url) {
130
+ return findByUrl(trackers, url);
131
+ }
132
+ export function findTrackerByCookie(name) {
133
+ return trackers.find((tracker) => tracker.cookies?.some((pattern) => typeof pattern === 'string' ? name.startsWith(pattern) : pattern.test(name)));
134
+ }
135
+ //# sourceMappingURL=trackers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trackers.js","sourceRoot":"","sources":["../../src/data/trackers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { type Tracker, type TrackerCategory, trackers } from './data/trackers.js';
2
+ export { defaultRules, googleFonts, impressumLink, preConsentTrackers, privacyPolicyLink, rejectOption, thirdPartyCdns, thirdPartyEmbeds, trackersAfterReject, } from './rules/index.js';
3
+ export { type ScanOptions, scan } from './scanner.js';
4
+ export type * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { trackers } from './data/trackers.js';
2
+ export { defaultRules, googleFonts, impressumLink, preConsentTrackers, privacyPolicyLink, rejectOption, thirdPartyCdns, thirdPartyEmbeds, trackersAfterReject, } from './rules/index.js';
3
+ export { scan } from './scanner.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EACL,YAAY,EACZ,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAoB,IAAI,EAAE,MAAM,cAAc,CAAC"}
package/dist/meta.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const VERSION: string;
2
+ export declare const REPOSITORY_URL = "https://github.com/jexcellence/privalint";
3
+ export declare const ruleDocsUrl: (ruleId: string) => string;
package/dist/meta.js ADDED
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from 'node:fs';
2
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
3
+ export const VERSION = pkg.version;
4
+ export const REPOSITORY_URL = 'https://github.com/jexcellence/privalint';
5
+ export const ruleDocsUrl = (ruleId) => `${REPOSITORY_URL}/blob/main/docs/rules/${ruleId}.md`;
6
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../src/meta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAEvF,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;AACnC,MAAM,CAAC,MAAM,cAAc,GAAG,0CAA0C,CAAC;AAEzE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAc,EAAE,EAAE,CAC5C,GAAG,cAAc,yBAAyB,MAAM,KAAK,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ScanResult } from '../types.js';
2
+ import { formatJson } from './json.js';
3
+ import { formatMarkdown } from './markdown.js';
4
+ import { formatSarif } from './sarif.js';
5
+ import { formatText } from './text.js';
6
+ export declare const FORMATS: readonly ['text', 'json', 'markdown', 'sarif'];
7
+ export type Format = (typeof FORMATS)[number];
8
+ export declare function format(result: ScanResult, kind: Format, options?: {
9
+ color?: boolean;
10
+ }): string;
11
+ export { formatJson, formatMarkdown, formatSarif, formatText };
@@ -0,0 +1,19 @@
1
+ import { formatJson } from './json.js';
2
+ import { formatMarkdown } from './markdown.js';
3
+ import { formatSarif } from './sarif.js';
4
+ import { formatText } from './text.js';
5
+ export const FORMATS = ['text', 'json', 'markdown', 'sarif'];
6
+ export function format(result, kind, options = {}) {
7
+ switch (kind) {
8
+ case 'json':
9
+ return formatJson(result);
10
+ case 'markdown':
11
+ return formatMarkdown(result);
12
+ case 'sarif':
13
+ return formatSarif(result);
14
+ default:
15
+ return formatText(result, options.color);
16
+ }
17
+ }
18
+ export { formatJson, formatMarkdown, formatSarif, formatText };
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/reporters/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAU,CAAC;AAGtE,MAAM,UAAU,MAAM,CAAC,MAAkB,EAAE,IAAY,EAAE,OAAO,GAAwB,EAAE;IACxF,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM;YACT,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5B,KAAK,UAAU;YACb,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;QAChC,KAAK,OAAO;YACV,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;QAC7B;YACE,OAAO,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from '../types.js';
2
+ export declare const formatJson: (result: ScanResult) => string;
@@ -0,0 +1,2 @@
1
+ export const formatJson = (result) => `${JSON.stringify(result, null, 2)}\n`;
2
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/reporters/json.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAkB,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ScanResult } from '../types.js';
2
+ /**
3
+ * Compact report for GitHub job summaries and PR comments.
4
+ *
5
+ * Page URLs come from the scanned site and may contain Markdown such as
6
+ * `![x](https://evil.example)` in their query string, so they are only ever
7
+ * rendered inside code spans.
8
+ */
9
+ export declare function formatMarkdown(result: ScanResult): string;
@@ -0,0 +1,47 @@
1
+ import { countBySeverity, describeConsent, groupByPage, plural } from './shared.js';
2
+ const ICONS = { error: '🔴', warning: '🟡', info: '🔵' };
3
+ /**
4
+ * Compact report for GitHub job summaries and PR comments.
5
+ *
6
+ * Page URLs come from the scanned site and may contain Markdown such as
7
+ * `![x](https://evil.example)` in their query string, so they are only ever
8
+ * rendered inside code spans.
9
+ */
10
+ export function formatMarkdown(result) {
11
+ const counts = countBySeverity(result.findings);
12
+ const lines = [
13
+ `### privalint: ${code(result.siteUrl)}`,
14
+ '',
15
+ `${plural(counts.error, 'error')} · ${plural(counts.warning, 'warning')} · ${plural(result.pages.length, 'page')} scanned`,
16
+ ];
17
+ const consent = describeConsent(result.consent);
18
+ if (consent !== undefined) {
19
+ lines.push('', escapeText(consent));
20
+ }
21
+ for (const [pageUrl, findings] of groupByPage(result.findings)) {
22
+ lines.push('', `#### ${code(pageUrl)}`, '', '| | Rule | Finding |', '|---|---|---|');
23
+ for (const finding of findings) {
24
+ lines.push(`| ${ICONS[finding.severity]} | \`${finding.ruleId}\` | ${escapeText(finding.message)} |`);
25
+ }
26
+ }
27
+ const failed = result.pages.filter((page) => page.error !== undefined);
28
+ if (failed.length > 0) {
29
+ lines.push('', `${plural(failed.length, 'page')} could not be scanned:`, '');
30
+ for (const page of failed) {
31
+ lines.push(`- ${code(page.url)}: ${escapeText(page.error ?? '')}`);
32
+ }
33
+ }
34
+ const fixes = new Map(result.findings.filter((f) => f.fix !== undefined).map((f) => [f.ruleId, f.fix ?? '']));
35
+ if (fixes.size > 0) {
36
+ lines.push('', '<details><summary>How to fix</summary>', '');
37
+ for (const [ruleId, fix] of fixes) {
38
+ lines.push(`- **${ruleId}**: ${escapeText(fix)}`);
39
+ }
40
+ lines.push('', '</details>');
41
+ }
42
+ lines.push('', '<sub>Automated checks only. A clean result is not proof of GDPR compliance.</sub>');
43
+ return `${lines.join('\n')}\n`;
44
+ }
45
+ const code = (text) => `\`${text.replaceAll('`', '%60').replaceAll('\n', ' ')}\``;
46
+ const escapeText = (text) => text.replace(/[\\`*_[\]()!<>|#~]/g, '\\$&').replaceAll('\n', ' ');
47
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/reporters/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ScanResult } from '../types.js';
2
+ /**
3
+ * SARIF 2.1.0 for GitHub code scanning.
4
+ *
5
+ * Code scanning expects every result to point at a file in the repository,
6
+ * which a live website does not have. Each page is mapped to a stable
7
+ * pseudo path (`privalint/<host>/<path>`) so alerts stay grouped per page
8
+ * and keep their identity between runs.
9
+ */
10
+ export declare function formatSarif(result: ScanResult): string;
@@ -0,0 +1,78 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { REPOSITORY_URL, ruleDocsUrl, VERSION } from '../meta.js';
3
+ const LEVELS = {
4
+ error: 'error',
5
+ warning: 'warning',
6
+ info: 'note',
7
+ };
8
+ /**
9
+ * SARIF 2.1.0 for GitHub code scanning.
10
+ *
11
+ * Code scanning expects every result to point at a file in the repository,
12
+ * which a live website does not have. Each page is mapped to a stable
13
+ * pseudo path (`privalint/<host>/<path>`) so alerts stay grouped per page
14
+ * and keep their identity between runs.
15
+ */
16
+ export function formatSarif(result) {
17
+ const sarif = {
18
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
19
+ version: '2.1.0',
20
+ runs: [
21
+ {
22
+ tool: {
23
+ driver: {
24
+ name: 'privalint',
25
+ semanticVersion: VERSION,
26
+ informationUri: REPOSITORY_URL,
27
+ rules: result.rules.map((rule) => ({
28
+ id: rule.id,
29
+ name: rule.id,
30
+ shortDescription: { text: rule.title },
31
+ fullDescription: { text: rule.description },
32
+ help: {
33
+ text: `${rule.description}\n\nReferences: ${rule.references.join('; ')}`,
34
+ markdown: `${rule.description}\n\n**References:** ${rule.references.join('; ')}`,
35
+ },
36
+ helpUri: ruleDocsUrl(rule.id),
37
+ properties: { tags: ['privacy', 'gdpr'], precision: 'high' },
38
+ })),
39
+ },
40
+ },
41
+ results: result.findings.map(toResult),
42
+ },
43
+ ],
44
+ };
45
+ return `${JSON.stringify(sarif, null, 2)}\n`;
46
+ }
47
+ function toResult(finding) {
48
+ const evidence = finding.evidence.map((item) => `- ${item}`).join('\n');
49
+ return {
50
+ ruleId: finding.ruleId,
51
+ level: LEVELS[finding.severity],
52
+ message: {
53
+ text: [finding.message, `Page: ${finding.pageUrl}`, evidence, finding.fix]
54
+ .filter(Boolean)
55
+ .join('\n\n'),
56
+ },
57
+ locations: [
58
+ {
59
+ physicalLocation: {
60
+ artifactLocation: { uri: pseudoPath(finding.pageUrl) },
61
+ region: { startLine: 1 },
62
+ },
63
+ },
64
+ ],
65
+ partialFingerprints: {
66
+ primaryLocationLineHash: createHash('sha256')
67
+ .update(`${finding.ruleId}\n${finding.pageUrl}\n${finding.subject ?? ''}`)
68
+ .digest('hex')
69
+ .slice(0, 32),
70
+ },
71
+ };
72
+ }
73
+ function pseudoPath(pageUrl) {
74
+ const url = new URL(pageUrl);
75
+ const path = url.pathname.endsWith('/') ? `${url.pathname}index` : url.pathname;
76
+ return `privalint/${url.host}${path}`.replace(/[^\w./-]/g, '_');
77
+ }
78
+ //# sourceMappingURL=sarif.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sarif.js","sourceRoot":"","sources":["../../src/reporters/sarif.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { ConsentSummary, Finding, Severity } from '../types.js';
2
+ export declare function groupByPage(findings: readonly Finding[]): Map<string, Finding[]>;
3
+ export declare function countBySeverity(findings: readonly Finding[]): Record<Severity, number>;
4
+ export declare function describeConsent(consent: ConsentSummary | undefined): string | undefined;
5
+ export declare const plural: (count: number, word: string) => string;
@@ -0,0 +1,34 @@
1
+ export function groupByPage(findings) {
2
+ const pages = new Map();
3
+ for (const finding of findings) {
4
+ const list = pages.get(finding.pageUrl) ?? [];
5
+ list.push(finding);
6
+ pages.set(finding.pageUrl, list);
7
+ }
8
+ return pages;
9
+ }
10
+ export function countBySeverity(findings) {
11
+ const counts = { error: 0, warning: 0, info: 0 };
12
+ for (const finding of findings) {
13
+ counts[finding.severity]++;
14
+ }
15
+ return counts;
16
+ }
17
+ export function describeConsent(consent) {
18
+ if (consent === undefined) {
19
+ return undefined;
20
+ }
21
+ if (!consent.bannerFound) {
22
+ return 'Consent banner: none found';
23
+ }
24
+ const vendor = consent.cmp === undefined ? '' : ` (${consent.cmp})`;
25
+ if (consent.error !== undefined) {
26
+ return `Consent banner${vendor}: found, but rejecting failed: ${consent.error}`;
27
+ }
28
+ if (consent.rejected) {
29
+ return `Consent banner${vendor}: clicked "${consent.rejectLabel}" and reloaded`;
30
+ }
31
+ return `Consent banner${vendor}: found, no reject button`;
32
+ }
33
+ export const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
34
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/reporters/shared.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,QAA4B;IACtD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC3C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACnB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAA4B;IAC1D,MAAM,MAAM,GAA6B,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC3E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC7B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAmC;IACjE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACzB,OAAO,4BAA4B,CAAC;IACtC,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,GAAG,GAAG,CAAC;IACpE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,iBAAiB,MAAM,kCAAkC,OAAO,CAAC,KAAK,EAAE,CAAC;IAClF,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,OAAO,iBAAiB,MAAM,cAAc,OAAO,CAAC,WAAW,gBAAgB,CAAC;IAClF,CAAC;IACD,OAAO,iBAAiB,MAAM,2BAA2B,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,IAAY,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from '../types.js';
2
+ export declare function formatText(result: ScanResult, color?: boolean): string;
@@ -0,0 +1,52 @@
1
+ import { styleText } from 'node:util';
2
+ import { countBySeverity, describeConsent, groupByPage, plural } from './shared.js';
3
+ const MARKS = {
4
+ error: ['✖', 'red'],
5
+ warning: ['▲', 'yellow'],
6
+ info: ['ℹ', 'blue'],
7
+ };
8
+ export function formatText(result, color = false) {
9
+ const paint = (style, text) => (color ? styleText(style, text) : text);
10
+ const lines = [];
11
+ for (const [pageUrl, findings] of groupByPage(result.findings)) {
12
+ lines.push('', paint('underline', pageUrl));
13
+ for (const finding of findings) {
14
+ lines.push(...formatFinding(finding, paint));
15
+ }
16
+ }
17
+ const failed = result.pages.filter((page) => page.error !== undefined);
18
+ if (failed.length > 0) {
19
+ lines.push('', paint('yellow', `Could not scan ${plural(failed.length, 'page')}:`));
20
+ for (const page of failed) {
21
+ lines.push(` ${page.url} ${paint('dim', page.error ?? '')}`);
22
+ }
23
+ }
24
+ const counts = countBySeverity(result.findings);
25
+ const pages = plural(result.pages.length, 'page');
26
+ const seconds = (result.durationMs / 1000).toFixed(1);
27
+ lines.push('');
28
+ const consent = describeConsent(result.consent);
29
+ if (consent !== undefined) {
30
+ lines.push(paint('dim', consent));
31
+ }
32
+ if (result.findings.length === 0) {
33
+ lines.push(paint('green', `✔ No problems found on ${pages} (${seconds}s).`));
34
+ }
35
+ else {
36
+ lines.push(`${paint('red', plural(counts.error, 'error'))}, ${paint('yellow', plural(counts.warning, 'warning'))} on ${pages} (${seconds}s).`);
37
+ }
38
+ lines.push(paint('dim', 'Automated checks only. A clean result is not proof of GDPR compliance.'));
39
+ return lines.join('\n');
40
+ }
41
+ function formatFinding(finding, paint) {
42
+ const [mark, style] = MARKS[finding.severity];
43
+ const lines = [` ${paint(style, mark)} ${finding.message} ${paint('dim', finding.ruleId)}`];
44
+ for (const item of finding.evidence) {
45
+ lines.push(paint('dim', ` ${item}`));
46
+ }
47
+ if (finding.fix) {
48
+ lines.push(` ${paint('cyan', 'fix:')} ${finding.fix}`);
49
+ }
50
+ return lines;
51
+ }
52
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/reporters/text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const googleFonts: Rule;
@@ -0,0 +1,51 @@
1
+ import { hostMatches, safeHost } from '../url.js';
2
+ const ID = 'google-fonts';
3
+ const GOOGLE_FONT_HOSTS = ['fonts.googleapis.com', 'fonts.gstatic.com'];
4
+ /** dns-prefetch only resolves the name; these hints open a real connection. */
5
+ const CONNECTING_HINTS = new Set(['preconnect', 'preload', 'prefetch', 'stylesheet']);
6
+ const FIX = 'Self-host the font files. In Next.js, next/font/google downloads fonts at build time and ' +
7
+ 'serves them from your own domain. Elsewhere, install the family from @fontsource or download ' +
8
+ 'it via google-webfonts-helper, then remove every reference to fonts.googleapis.com and ' +
9
+ 'fonts.gstatic.com.';
10
+ const isGoogleFontUrl = (url) => {
11
+ const host = safeHost(url);
12
+ return host !== undefined && GOOGLE_FONT_HOSTS.some((domain) => hostMatches(host, domain));
13
+ };
14
+ export const googleFonts = {
15
+ id: ID,
16
+ title: 'Google Fonts loaded from Google servers',
17
+ description: "Loading fonts from Google's CDN sends every visitor's IP address to Google without " +
18
+ 'consent. LG München I awarded a visitor damages for this in 2022.',
19
+ references: ['Art. 6(1) GDPR', 'LG München I, 20.01.2022, Az. 3 O 17493/20'],
20
+ check({ page }) {
21
+ const requests = page.requests.filter((request) => isGoogleFontUrl(request.url));
22
+ if (requests.length > 0) {
23
+ return [
24
+ {
25
+ ruleId: ID,
26
+ severity: 'error',
27
+ message: `Fonts are loaded from Google (${requests.length} request${requests.length === 1 ? '' : 's'}).`,
28
+ pageUrl: page.finalUrl,
29
+ evidence: [...new Set(requests.map((request) => request.url))].slice(0, 5),
30
+ fix: FIX,
31
+ },
32
+ ];
33
+ }
34
+ const hints = page.resourceHints.filter((hint) => isGoogleFontUrl(hint.href) &&
35
+ hint.rel.split(/\s+/).some((rel) => CONNECTING_HINTS.has(rel)));
36
+ if (hints.length > 0) {
37
+ return [
38
+ {
39
+ ruleId: ID,
40
+ severity: 'warning',
41
+ message: 'The page preconnects to Google Fonts. No font was fetched, but the browser still opens a connection and transmits the IP address.',
42
+ pageUrl: page.finalUrl,
43
+ evidence: hints.map((hint) => `<link rel="${hint.rel}" href="${hint.href}">`),
44
+ fix: 'Remove the leftover <link> tags pointing to Google Fonts.',
45
+ },
46
+ ];
47
+ }
48
+ return [];
49
+ },
50
+ };
51
+ //# sourceMappingURL=google-fonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-fonts.js","sourceRoot":"","sources":["../../src/rules/google-fonts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAElD,MAAM,EAAE,GAAG,cAAc,CAAC;AAC1B,MAAM,iBAAiB,GAAG,CAAC,sBAAsB,EAAE,mBAAmB,CAAC,CAAC;AAExE,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC;AAEtF,MAAM,GAAG,GACP,2FAA2F;IAC3F,+FAA+F;IAC/F,yFAAyF;IACzF,oBAAoB,CAAC;AAEvB,MAAM,eAAe,GAAG,CAAC,GAAW,EAAW,EAAE;IAC/C,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC3B,OAAO,IAAI,KAAK,SAAS,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AAC7F,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAS;IAC/B,EAAE,EAAE,EAAE;IACN,KAAK,EAAE,yCAAyC;IAChD,WAAW,EACT,qFAAqF;QACrF,mEAAmE;IACrE,UAAU,EAAE,CAAC,gBAAgB,EAAE,4CAA4C,CAAC;IAC5E,KAAK,CAAC,EAAE,IAAI,EAAE;QACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACjF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO;gBACL;oBACE,MAAM,EAAE,EAAE;oBACV,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,iCAAiC,QAAQ,CAAC,MAAM,WAAW,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI;oBACxG,OAAO,EAAE,IAAI,CAAC,QAAQ;oBACtB,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;oBAC1E,GAAG,EAAE,GAAG;iBACT;aACF,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CACrC,CAAC,IAAI,EAAE,EAAE,CACP,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CACjE,CAAC;QACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO;gBACL;oBACE,MAAM,EAAE,EAAE;oBACV,QAAQ,EAAE,SAAS;oBACnB,OAAO,EACL,mIAAmI;oBACrI,OAAO,EAAE,IAAI,CAAC,QAAQ;oBACtB,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,IAAI,CAAC,GAAG,WAAW,IAAI,CAAC,IAAI,IAAI,CAAC;oBAC7E,GAAG,EAAE,2DAA2D;iBAC/C;aACpB,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}