privalint 0.0.0-stage → 0.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.de.md +89 -0
  3. package/README.md +222 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +183 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/consent.d.ts +12 -0
  8. package/dist/consent.js +185 -0
  9. package/dist/consent.js.map +1 -0
  10. package/dist/data/third-parties.d.ts +13 -0
  11. package/dist/data/third-parties.js +42 -0
  12. package/dist/data/third-parties.js.map +1 -0
  13. package/dist/data/trackers.d.ts +21 -0
  14. package/dist/data/trackers.js +135 -0
  15. package/dist/data/trackers.js.map +1 -0
  16. package/dist/i18n.d.ts +5 -0
  17. package/dist/i18n.js +3 -0
  18. package/dist/i18n.js.map +1 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +6 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/legal.d.ts +10 -0
  23. package/dist/legal.js +99 -0
  24. package/dist/legal.js.map +1 -0
  25. package/dist/meta.d.ts +3 -0
  26. package/dist/meta.js +6 -0
  27. package/dist/meta.js.map +1 -0
  28. package/dist/reporters/index.d.ts +11 -0
  29. package/dist/reporters/index.js +19 -0
  30. package/dist/reporters/index.js.map +1 -0
  31. package/dist/reporters/json.d.ts +2 -0
  32. package/dist/reporters/json.js +2 -0
  33. package/dist/reporters/json.js.map +1 -0
  34. package/dist/reporters/markdown.d.ts +9 -0
  35. package/dist/reporters/markdown.js +49 -0
  36. package/dist/reporters/markdown.js.map +1 -0
  37. package/dist/reporters/messages.d.ts +21 -0
  38. package/dist/reporters/messages.js +62 -0
  39. package/dist/reporters/messages.js.map +1 -0
  40. package/dist/reporters/sarif.d.ts +10 -0
  41. package/dist/reporters/sarif.js +84 -0
  42. package/dist/reporters/sarif.js.map +1 -0
  43. package/dist/reporters/shared.d.ts +3 -0
  44. package/dist/reporters/shared.js +17 -0
  45. package/dist/reporters/shared.js.map +1 -0
  46. package/dist/reporters/text.d.ts +2 -0
  47. package/dist/reporters/text.js +56 -0
  48. package/dist/reporters/text.js.map +1 -0
  49. package/dist/rules/google-fonts.d.ts +2 -0
  50. package/dist/rules/google-fonts.js +68 -0
  51. package/dist/rules/google-fonts.js.map +1 -0
  52. package/dist/rules/impressum-link.d.ts +2 -0
  53. package/dist/rules/impressum-link.js +59 -0
  54. package/dist/rules/impressum-link.js.map +1 -0
  55. package/dist/rules/index.d.ts +11 -0
  56. package/dist/rules/index.js +20 -0
  57. package/dist/rules/index.js.map +1 -0
  58. package/dist/rules/links.d.ts +8 -0
  59. package/dist/rules/links.js +16 -0
  60. package/dist/rules/links.js.map +1 -0
  61. package/dist/rules/pre-consent-trackers.d.ts +2 -0
  62. package/dist/rules/pre-consent-trackers.js +45 -0
  63. package/dist/rules/pre-consent-trackers.js.map +1 -0
  64. package/dist/rules/privacy-policy-link.d.ts +2 -0
  65. package/dist/rules/privacy-policy-link.js +33 -0
  66. package/dist/rules/privacy-policy-link.js.map +1 -0
  67. package/dist/rules/reject-option.d.ts +2 -0
  68. package/dist/rules/reject-option.js +66 -0
  69. package/dist/rules/reject-option.js.map +1 -0
  70. package/dist/rules/third-party-cdns.d.ts +2 -0
  71. package/dist/rules/third-party-cdns.js +30 -0
  72. package/dist/rules/third-party-cdns.js.map +1 -0
  73. package/dist/rules/third-party-embeds.d.ts +2 -0
  74. package/dist/rules/third-party-embeds.js +30 -0
  75. package/dist/rules/third-party-embeds.js.map +1 -0
  76. package/dist/rules/third-party-hits.d.ts +4 -0
  77. package/dist/rules/third-party-hits.js +13 -0
  78. package/dist/rules/third-party-hits.js.map +1 -0
  79. package/dist/rules/tracker-hits.d.ts +11 -0
  80. package/dist/rules/tracker-hits.js +40 -0
  81. package/dist/rules/tracker-hits.js.map +1 -0
  82. package/dist/rules/trackers-after-reject.d.ts +2 -0
  83. package/dist/rules/trackers-after-reject.js +39 -0
  84. package/dist/rules/trackers-after-reject.js.map +1 -0
  85. package/dist/scanner.d.ts +39 -0
  86. package/dist/scanner.js +175 -0
  87. package/dist/scanner.js.map +1 -0
  88. package/dist/types.d.ts +105 -0
  89. package/dist/types.js +2 -0
  90. package/dist/types.js.map +1 -0
  91. package/dist/url.d.ts +26 -0
  92. package/dist/url.js +71 -0
  93. package/dist/url.js.map +1 -0
  94. package/package.json +65 -4
@@ -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,49 @@
1
+ import { messagesFor } from './messages.js';
2
+ import { countBySeverity, groupByPage } from './shared.js';
3
+ const ICONS = { error: '🔴', warning: '🟡', info: '🔵' };
4
+ /**
5
+ * Compact report for GitHub job summaries and PR comments.
6
+ *
7
+ * Page URLs come from the scanned site and may contain Markdown such as
8
+ * `![x](https://evil.example)` in their query string, so they are only ever
9
+ * rendered inside code spans.
10
+ */
11
+ export function formatMarkdown(result) {
12
+ const m = messagesFor(result.lang);
13
+ const counts = countBySeverity(result.findings);
14
+ const lines = [
15
+ `### privalint: ${code(result.siteUrl)}`,
16
+ '',
17
+ `${m.errors(counts.error)} · ${m.warnings(counts.warning)} · ${m.scanned(m.pages(result.pages.length))}`,
18
+ ];
19
+ if (result.consent !== undefined) {
20
+ lines.push('', escapeText(m.consent(result.consent)));
21
+ }
22
+ for (const [pageUrl, findings] of groupByPage(result.findings)) {
23
+ lines.push('', `#### ${code(pageUrl)}`, '', `| | ${m.rule} | ${m.finding} |`, '|---|---|---|');
24
+ for (const finding of findings) {
25
+ lines.push(`| ${ICONS[finding.severity]} | \`${finding.ruleId}\` | ${escapeText(finding.message)} |`);
26
+ }
27
+ }
28
+ const failed = result.pages.filter((page) => page.error !== undefined);
29
+ if (failed.length > 0) {
30
+ lines.push('', m.couldNotScan(failed.length), '');
31
+ for (const page of failed) {
32
+ lines.push(`- ${code(page.url)}: ${escapeText(page.error ?? '')}`);
33
+ }
34
+ }
35
+ const fixes = new Map(result.findings.filter((f) => f.fix !== undefined).map((f) => [f.ruleId, f]));
36
+ if (fixes.size > 0) {
37
+ lines.push('', `<details><summary>${m.howToFix}</summary>`, '');
38
+ for (const [ruleId, { fix, references }] of fixes) {
39
+ const legal = references?.length ? ` (${m.legalBasis} ${references.join('; ')})` : '';
40
+ lines.push(`- **${ruleId}**: ${escapeText(`${fix ?? ''}${legal}`)}`);
41
+ }
42
+ lines.push('', '</details>');
43
+ }
44
+ lines.push('', `<sub>${m.disclaimer}</sub>`);
45
+ return `${lines.join('\n')}\n`;
46
+ }
47
+ const code = (text) => `\`${text.replaceAll('`', '%60').replaceAll('\n', ' ')}\``;
48
+ const escapeText = (text) => text.replace(/[\\`*_[\]()!<>|#~]/g, '\\$&').replaceAll('\n', ' ');
49
+ //# 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,21 @@
1
+ import type { Lang } from '../i18n.js';
2
+ import type { ConsentSummary } from '../types.js';
3
+ /** Fixed wording of the text and Markdown reports. */
4
+ export interface ReportMessages {
5
+ errors(count: number): string;
6
+ warnings(count: number): string;
7
+ pages(count: number): string;
8
+ duration(ms: number): string;
9
+ noProblems(pages: string, duration: string): string;
10
+ on: string;
11
+ scanned(pages: string): string;
12
+ couldNotScan(count: number): string;
13
+ fix: string;
14
+ legalBasis: string;
15
+ rule: string;
16
+ finding: string;
17
+ howToFix: string;
18
+ disclaimer: string;
19
+ consent(consent: ConsentSummary): string;
20
+ }
21
+ export declare const messagesFor: (lang: Lang) => ReportMessages;
@@ -0,0 +1,62 @@
1
+ const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
2
+ const en = {
3
+ errors: (n) => count(n, 'error', 'errors'),
4
+ warnings: (n) => count(n, 'warning', 'warnings'),
5
+ pages: (n) => count(n, 'page', 'pages'),
6
+ duration: (ms) => `${(ms / 1000).toFixed(1)}s`,
7
+ noProblems: (pages, duration) => `No problems found on ${pages} (${duration}).`,
8
+ on: 'on',
9
+ scanned: (pages) => `${pages} scanned`,
10
+ couldNotScan: (n) => `Could not scan ${count(n, 'page', 'pages')}:`,
11
+ fix: 'fix:',
12
+ legalBasis: 'legal basis:',
13
+ rule: 'Rule',
14
+ finding: 'Finding',
15
+ howToFix: 'How to fix',
16
+ disclaimer: 'Automated checks only. A clean result is not proof of GDPR compliance.',
17
+ consent(consent) {
18
+ if (!consent.bannerFound) {
19
+ return 'Consent banner: none found';
20
+ }
21
+ const vendor = consent.cmp === undefined ? '' : ` (${consent.cmp})`;
22
+ if (consent.error !== undefined) {
23
+ return `Consent banner${vendor}: found, but rejecting failed: ${consent.error}`;
24
+ }
25
+ if (consent.rejected) {
26
+ return `Consent banner${vendor}: clicked "${consent.rejectLabel}" and reloaded`;
27
+ }
28
+ return `Consent banner${vendor}: found, no reject button`;
29
+ },
30
+ };
31
+ const de = {
32
+ errors: (n) => count(n, 'Fehler', 'Fehler'),
33
+ warnings: (n) => count(n, 'Warnung', 'Warnungen'),
34
+ pages: (n) => count(n, 'Seite', 'Seiten'),
35
+ duration: (ms) => `${(ms / 1000).toFixed(1).replace('.', ',')} s`,
36
+ noProblems: (pages, duration) => `Keine Probleme auf ${pages} gefunden (${duration}).`,
37
+ on: 'auf',
38
+ scanned: (pages) => `${pages} geprüft`,
39
+ couldNotScan: (n) => `${count(n, 'Seite', 'Seiten')} ${n === 1 ? 'konnte' : 'konnten'} nicht geprüft werden:`,
40
+ fix: 'Lösung:',
41
+ legalBasis: 'Rechtsgrundlage:',
42
+ rule: 'Regel',
43
+ finding: 'Befund',
44
+ howToFix: 'So beheben Sie es',
45
+ disclaimer: 'Nur automatisierte Prüfungen. Ein sauberes Ergebnis ist kein Nachweis der DSGVO-Konformität.',
46
+ consent(consent) {
47
+ if (!consent.bannerFound) {
48
+ return 'Cookie-Banner: keines gefunden';
49
+ }
50
+ const vendor = consent.cmp === undefined ? '' : ` (${consent.cmp})`;
51
+ if (consent.error !== undefined) {
52
+ return `Cookie-Banner${vendor}: gefunden, aber Ablehnen ist fehlgeschlagen: ${consent.error}`;
53
+ }
54
+ if (consent.rejected) {
55
+ return `Cookie-Banner${vendor}: „${consent.rejectLabel}“ geklickt und neu geladen`;
56
+ }
57
+ return `Cookie-Banner${vendor}: gefunden, kein Ablehnen-Button`;
58
+ },
59
+ };
60
+ const MESSAGES = { en, de };
61
+ export const messagesFor = (lang) => MESSAGES[lang];
62
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/reporters/messages.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,84 @@
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: [
54
+ finding.message,
55
+ `Page: ${finding.pageUrl}`,
56
+ evidence,
57
+ finding.fix,
58
+ finding.references?.join('; '),
59
+ ]
60
+ .filter(Boolean)
61
+ .join('\n\n'),
62
+ },
63
+ locations: [
64
+ {
65
+ physicalLocation: {
66
+ artifactLocation: { uri: pseudoPath(finding.pageUrl) },
67
+ region: { startLine: 1 },
68
+ },
69
+ },
70
+ ],
71
+ partialFingerprints: {
72
+ primaryLocationLineHash: createHash('sha256')
73
+ .update(`${finding.ruleId}\n${finding.pageUrl}\n${finding.subject ?? ''}`)
74
+ .digest('hex')
75
+ .slice(0, 32),
76
+ },
77
+ };
78
+ }
79
+ function pseudoPath(pageUrl) {
80
+ const url = new URL(pageUrl);
81
+ const path = url.pathname.endsWith('/') ? `${url.pathname}index` : url.pathname;
82
+ return `privalint/${url.host}${path}`.replace(/[^\w./-]/g, '_');
83
+ }
84
+ //# 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,3 @@
1
+ import type { 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>;
@@ -0,0 +1,17 @@
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
+ //# 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"}
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from '../types.js';
2
+ export declare function formatText(result: ScanResult, color?: boolean): string;
@@ -0,0 +1,56 @@
1
+ import { styleText } from 'node:util';
2
+ import { messagesFor } from './messages.js';
3
+ import { countBySeverity, groupByPage } from './shared.js';
4
+ const MARKS = {
5
+ error: ['✖', 'red'],
6
+ warning: ['▲', 'yellow'],
7
+ info: ['ℹ', 'blue'],
8
+ };
9
+ export function formatText(result, color = false) {
10
+ const paint = (style, text) => (color ? styleText(style, text) : text);
11
+ const m = messagesFor(result.lang);
12
+ const lines = [];
13
+ for (const [pageUrl, findings] of groupByPage(result.findings)) {
14
+ lines.push('', paint('underline', pageUrl));
15
+ for (const finding of findings) {
16
+ lines.push(...formatFinding(finding, paint, m));
17
+ }
18
+ }
19
+ const failed = result.pages.filter((page) => page.error !== undefined);
20
+ if (failed.length > 0) {
21
+ lines.push('', paint('yellow', m.couldNotScan(failed.length)));
22
+ for (const page of failed) {
23
+ lines.push(` ${page.url} ${paint('dim', page.error ?? '')}`);
24
+ }
25
+ }
26
+ const counts = countBySeverity(result.findings);
27
+ const pages = m.pages(result.pages.length);
28
+ const duration = m.duration(result.durationMs);
29
+ lines.push('');
30
+ if (result.consent !== undefined) {
31
+ lines.push(paint('dim', m.consent(result.consent)));
32
+ }
33
+ if (result.findings.length === 0) {
34
+ lines.push(paint('green', `✔ ${m.noProblems(pages, duration)}`));
35
+ }
36
+ else {
37
+ lines.push(`${paint('red', m.errors(counts.error))}, ${paint('yellow', m.warnings(counts.warning))} ${m.on} ${pages} (${duration}).`);
38
+ }
39
+ lines.push(paint('dim', m.disclaimer));
40
+ return lines.join('\n');
41
+ }
42
+ function formatFinding(finding, paint, m) {
43
+ const [mark, style] = MARKS[finding.severity];
44
+ const lines = [` ${paint(style, mark)} ${finding.message} ${paint('dim', finding.ruleId)}`];
45
+ for (const item of finding.evidence) {
46
+ lines.push(paint('dim', ` ${item}`));
47
+ }
48
+ if (finding.fix) {
49
+ lines.push(` ${paint('cyan', m.fix)} ${finding.fix}`);
50
+ }
51
+ if (finding.references !== undefined && finding.references.length > 0) {
52
+ lines.push(paint('dim', ` ${m.legalBasis} ${finding.references.join('; ')}`));
53
+ }
54
+ return lines;
55
+ }
56
+ //# 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,68 @@
1
+ import { say } from '../i18n.js';
2
+ import { hostMatches, safeHost } from '../url.js';
3
+ const ID = 'google-fonts';
4
+ const GOOGLE_FONT_HOSTS = ['fonts.googleapis.com', 'fonts.gstatic.com'];
5
+ /** dns-prefetch only resolves the name; these hints open a real connection. */
6
+ const CONNECTING_HINTS = new Set(['preconnect', 'preload', 'prefetch', 'stylesheet']);
7
+ const FIX = {
8
+ en: 'Self-host the font files. In Next.js, next/font/google downloads fonts at build time and ' +
9
+ 'serves them from your own domain. Elsewhere, install the family from @fontsource or download ' +
10
+ 'it via google-webfonts-helper, then remove every reference to fonts.googleapis.com and ' +
11
+ 'fonts.gstatic.com.',
12
+ de: 'Die Schriftdateien selbst hosten. In Next.js lädt next/font/google die Schriften beim Build ' +
13
+ 'herunter und liefert sie von der eigenen Domain aus. Sonst die Schrift über @fontsource ' +
14
+ 'installieren oder mit dem google-webfonts-helper herunterladen und danach alle Verweise auf ' +
15
+ 'fonts.googleapis.com und fonts.gstatic.com entfernen.',
16
+ };
17
+ const isGoogleFontUrl = (url) => {
18
+ const host = safeHost(url);
19
+ return host !== undefined && GOOGLE_FONT_HOSTS.some((domain) => hostMatches(host, domain));
20
+ };
21
+ export const googleFonts = {
22
+ id: ID,
23
+ title: 'Google Fonts loaded from Google servers',
24
+ description: "Loading fonts from Google's CDN sends every visitor's IP address to Google without " +
25
+ 'consent. LG München I awarded a visitor damages for this in 2022.',
26
+ references: ['Art. 6(1) GDPR', 'LG München I, 20.01.2022, Az. 3 O 17493/20'],
27
+ check({ page, lang }) {
28
+ const requests = page.requests.filter((request) => isGoogleFontUrl(request.url));
29
+ if (requests.length > 0) {
30
+ const count = requests.length;
31
+ return [
32
+ {
33
+ ruleId: ID,
34
+ severity: 'error',
35
+ message: say(lang, {
36
+ en: `Fonts are loaded from Google (${count} request${count === 1 ? '' : 's'}).`,
37
+ de: `Schriften werden von Google geladen (${count} ${count === 1 ? 'Anfrage' : 'Anfragen'}).`,
38
+ }),
39
+ pageUrl: page.finalUrl,
40
+ evidence: [...new Set(requests.map((request) => request.url))].slice(0, 5),
41
+ fix: say(lang, FIX),
42
+ },
43
+ ];
44
+ }
45
+ const hints = page.resourceHints.filter((hint) => isGoogleFontUrl(hint.href) &&
46
+ hint.rel.split(/\s+/).some((rel) => CONNECTING_HINTS.has(rel)));
47
+ if (hints.length > 0) {
48
+ return [
49
+ {
50
+ ruleId: ID,
51
+ severity: 'warning',
52
+ message: say(lang, {
53
+ en: 'The page preconnects to Google Fonts. No font was fetched, but the browser still opens a connection and transmits the IP address.',
54
+ de: 'Die Seite baut vorab eine Verbindung zu Google Fonts auf. Es wurde keine Schrift geladen, aber der Browser öffnet trotzdem eine Verbindung und überträgt die IP-Adresse.',
55
+ }),
56
+ pageUrl: page.finalUrl,
57
+ evidence: hints.map((hint) => `<link rel="${hint.rel}" href="${hint.href}">`),
58
+ fix: say(lang, {
59
+ en: 'Remove the leftover <link> tags pointing to Google Fonts.',
60
+ de: 'Die übrig gebliebenen <link>-Tags zu Google Fonts entfernen.',
61
+ }),
62
+ },
63
+ ];
64
+ }
65
+ return [];
66
+ },
67
+ };
68
+ //# sourceMappingURL=google-fonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-fonts.js","sourceRoot":"","sources":["../../src/rules/google-fonts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const impressumLink: Rule;
@@ -0,0 +1,59 @@
1
+ import { say } from '../i18n.js';
2
+ import { linkMatches, linksChecked } from './links.js';
3
+ const ID = 'impressum-link';
4
+ /**
5
+ * Link texts and URL slugs used for a legal notice across the EU.
6
+ * Matched against the visible text and the URL path, so `/impressum`
7
+ * behind an icon-only link is still found.
8
+ */
9
+ const LEGAL_NOTICE = /impressum|imprint|legal[\s_-]?notice|site[\s_-]?notice|anbieterkennzeichnung|mentions[\s_-]?l[eé]gales|aviso[\s_-]?legal|note[\s_-]?legali|colofon|informacje[\s_-]?prawne/i;
10
+ const CONTACT = /kontakt|contact/i;
11
+ export const impressumLink = {
12
+ id: ID,
13
+ title: 'No link to an Impressum',
14
+ description: 'Commercial websites in Germany must provide a legal notice that is easy to recognise, ' +
15
+ 'directly reachable and always available, which in practice means a link on every page. ' +
16
+ 'Disable this rule for purely private sites or sites outside its scope.',
17
+ references: ['§ 5 DDG', 'BGH, 20.07.2006, Az. I ZR 228/03'],
18
+ check({ page, lang }) {
19
+ if (page.links.some(linkMatches(LEGAL_NOTICE))) {
20
+ return [];
21
+ }
22
+ const contact = page.links.find(linkMatches(CONTACT));
23
+ if (contact !== undefined) {
24
+ return [
25
+ {
26
+ ruleId: ID,
27
+ severity: 'info',
28
+ message: say(lang, {
29
+ en: 'No direct Impressum link, only a contact link.',
30
+ de: 'Kein direkter Impressum-Link, nur ein Kontakt-Link.',
31
+ }),
32
+ pageUrl: page.finalUrl,
33
+ evidence: [say(lang, { en: 'contact link: ', de: 'Kontakt-Link: ' }) + contact.href],
34
+ fix: say(lang, {
35
+ en: 'Two clicks via "Kontakt" can be enough if the Impressum is clearly linked from there (BGH I ZR 228/03), but a direct "Impressum" link next to it is safer.',
36
+ de: 'Zwei Klicks über „Kontakt“ können reichen, wenn das Impressum dort klar verlinkt ist (BGH I ZR 228/03). Sicherer ist ein direkter Link „Impressum“ daneben.',
37
+ }),
38
+ },
39
+ ];
40
+ }
41
+ return [
42
+ {
43
+ ruleId: ID,
44
+ severity: 'warning',
45
+ message: say(lang, {
46
+ en: 'No link to an Impressum or legal notice was found on this page.',
47
+ de: 'Auf dieser Seite wurde kein Link zu einem Impressum gefunden.',
48
+ }),
49
+ pageUrl: page.finalUrl,
50
+ evidence: [linksChecked(page.links.length, lang)],
51
+ fix: say(lang, {
52
+ en: 'Add a link labelled "Impressum" to a part of the layout that appears on every page, usually the footer.',
53
+ de: 'Einen Link „Impressum“ in einen Bereich einbauen, der auf jeder Seite erscheint, meist den Footer.',
54
+ }),
55
+ },
56
+ ];
57
+ },
58
+ };
59
+ //# sourceMappingURL=impressum-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impressum-link.js","sourceRoot":"","sources":["../../src/rules/impressum-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,EAAE,GAAG,gBAAgB,CAAC;AAE5B;;;;GAIG;AACH,MAAM,YAAY,GAChB,6KAA6K,CAAC;AAEhL,MAAM,OAAO,GAAG,kBAAkB,CAAC;AAEnC,MAAM,CAAC,MAAM,aAAa,GAAS;IACjC,EAAE,EAAE,EAAE;IACN,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,wFAAwF;QACxF,yFAAyF;QACzF,wEAAwE;IAC1E,UAAU,EAAE,CAAC,SAAS,EAAE,kCAAkC,CAAC;IAC3D,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;QAClB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;QACtD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO;gBACL;oBACE,MAAM,EAAE,EAAE;oBACV,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE;wBACjB,EAAE,EAAE,gDAAgD;wBACpD,EAAE,EAAE,qDAAqD;qBAC1D,CAAC;oBACF,OAAO,EAAE,IAAI,CAAC,QAAQ;oBACtB,QAAQ,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,gBAAgB,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;oBACpF,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE;wBACb,EAAE,EAAE,4JAA4J;wBAChK,EAAE,EAAE,6JAA6J;qBAClK,CAAC;iBACH;aACF,CAAC;QACJ,CAAC;QAED,OAAO;YACL;gBACE,MAAM,EAAE,EAAE;gBACV,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE;oBACjB,EAAE,EAAE,iEAAiE;oBACrE,EAAE,EAAE,+DAA+D;iBACpE,CAAC;gBACF,OAAO,EAAE,IAAI,CAAC,QAAQ;gBACtB,QAAQ,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBACjD,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE;oBACb,EAAE,EAAE,yGAAyG;oBAC7G,EAAE,EAAE,oGAAoG;iBACzG,CAAC;aACH;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Rule } from '../types.js';
2
+ import { googleFonts } from './google-fonts.js';
3
+ import { impressumLink } from './impressum-link.js';
4
+ import { preConsentTrackers } from './pre-consent-trackers.js';
5
+ import { privacyPolicyLink } from './privacy-policy-link.js';
6
+ import { rejectOption } from './reject-option.js';
7
+ import { thirdPartyCdns } from './third-party-cdns.js';
8
+ import { thirdPartyEmbeds } from './third-party-embeds.js';
9
+ import { trackersAfterReject } from './trackers-after-reject.js';
10
+ export declare const defaultRules: readonly Rule[];
11
+ export { googleFonts, impressumLink, preConsentTrackers, privacyPolicyLink, rejectOption, thirdPartyCdns, thirdPartyEmbeds, trackersAfterReject, };
@@ -0,0 +1,20 @@
1
+ import { googleFonts } from './google-fonts.js';
2
+ import { impressumLink } from './impressum-link.js';
3
+ import { preConsentTrackers } from './pre-consent-trackers.js';
4
+ import { privacyPolicyLink } from './privacy-policy-link.js';
5
+ import { rejectOption } from './reject-option.js';
6
+ import { thirdPartyCdns } from './third-party-cdns.js';
7
+ import { thirdPartyEmbeds } from './third-party-embeds.js';
8
+ import { trackersAfterReject } from './trackers-after-reject.js';
9
+ export const defaultRules = [
10
+ preConsentTrackers,
11
+ trackersAfterReject,
12
+ rejectOption,
13
+ googleFonts,
14
+ thirdPartyEmbeds,
15
+ thirdPartyCdns,
16
+ impressumLink,
17
+ privacyPolicyLink,
18
+ ];
19
+ export { googleFonts, impressumLink, preConsentTrackers, privacyPolicyLink, rejectOption, thirdPartyCdns, thirdPartyEmbeds, trackersAfterReject, };
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,MAAM,CAAC,MAAM,YAAY,GAAoB;IAC3C,kBAAkB;IAClB,mBAAmB;IACnB,YAAY;IACZ,WAAW;IACX,gBAAgB;IAChB,cAAc;IACd,aAAa;IACb,iBAAiB;CAClB,CAAC;AAEF,OAAO,EACL,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,GACpB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type Lang } from '../i18n.js';
2
+ import type { PageLink } from '../types.js';
3
+ /**
4
+ * Matches a link by its visible text or its URL path. Only the path counts,
5
+ * so a domain like impressum-example.de doesn't satisfy every page.
6
+ */
7
+ export declare const linkMatches: (pattern: RegExp) => (link: PageLink) => boolean;
8
+ export declare const linksChecked: (count: number, lang: Lang) => string;
@@ -0,0 +1,16 @@
1
+ import { say } from '../i18n.js';
2
+ /**
3
+ * Matches a link by its visible text or its URL path. Only the path counts,
4
+ * so a domain like impressum-example.de doesn't satisfy every page.
5
+ */
6
+ export const linkMatches = (pattern) => (link) => pattern.test(link.text) || pattern.test(pathOf(link.href));
7
+ export const linksChecked = (count, lang) => say(lang, { en: `${count} links checked`, de: `${count} Links geprüft` });
8
+ function pathOf(href) {
9
+ try {
10
+ return decodeURIComponent(new URL(href).pathname);
11
+ }
12
+ catch {
13
+ return '';
14
+ }
15
+ }
16
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/rules/links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,GAAG,EAAE,MAAM,YAAY,CAAC;AAG5C;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,CAAC,IAAc,EAAE,EAAE,CACjE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAE7D,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,IAAU,EAAE,EAAE,CACxD,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,KAAK,gBAAgB,EAAE,EAAE,EAAE,GAAG,KAAK,gBAAgB,EAAE,CAAC,CAAC;AAE5E,SAAS,MAAM,CAAC,IAAY;IAC1B,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const preConsentTrackers: Rule;