@aegis-scan/skills 0.2.0 → 0.4.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/ATTRIBUTION.md +60 -4
- package/CHANGELOG.md +78 -0
- package/README.md +27 -0
- package/dist/bin.js +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +9 -2
- package/dist/commands/list.js.map +1 -1
- package/dist/skills-loader.d.ts +43 -0
- package/dist/skills-loader.d.ts.map +1 -1
- package/dist/skills-loader.js +102 -0
- package/dist/skills-loader.js.map +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
- package/skills/compliance/_INDEX.md +49 -0
- package/skills/compliance/aegis-native/brutaler-anwalt/SKILL.md +100 -3
- package/skills/defensive/aegis-native/rls-defense/SKILL.md +25 -0
- package/skills/defensive/aegis-native/tenant-isolation-defense/SKILL.md +26 -0
- package/skills/foundation/_INDEX.md +73 -0
- package/skills/foundation/aegis-native/aegis-audit/SKILL.md +194 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-1-headers.md +138 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-2-html.md +153 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-3-impressum.md +159 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-4-dse.md +178 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-5-cookie.md +180 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-6-branche.md +204 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-7-code-cross-check.md +212 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-8-schadens-diagnose.md +232 -0
- package/skills/foundation/aegis-native/aegis-customer-build/SKILL.md +232 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-1-recon.md +147 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-2-architecture.md +164 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-3-component-build.md +231 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-4-content.md +196 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-5-integration.md +273 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-6-mid-audit.md +200 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-7-final-verify.md +258 -0
- package/skills/foundation/aegis-native/aegis-handover-writer/SKILL.md +128 -0
- package/skills/foundation/aegis-native/aegis-module-builder/SKILL.md +251 -0
- package/skills/foundation/aegis-native/aegis-orchestrator/SKILL.md +146 -0
- package/skills/foundation/aegis-native/aegis-quality-gates/SKILL.md +122 -0
- package/skills/foundation/aegis-native/aegis-skill-creator/SKILL.md +223 -0
- package/skills/foundation/aegis-native/aegis-skill-creator/references/hard-constraint-template.md +213 -0
- package/skills/foundation/aegis-native/aegis-skill-creator/references/skillforge-methodology.md +220 -0
- package/skills/foundation/aegis-native/dsgvo-compliance/SKILL.md +185 -0
- package/skills/foundation/aegis-native/dsgvo-compliance/references/art-13-15-templates.md +309 -0
- package/skills/foundation/aegis-native/dsgvo-compliance/references/datenpanne-runbook.md +291 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Layer 2 Reference — HTML-Live-Probe
|
|
2
|
+
|
|
3
|
+
Layer 2 fetches the rendered HTML and analyzes structural + content patterns. Catches: missing alt-texts, broken meta-tags, SSR-skeleton-only pages, inline-script-leaks, missing footer/legal links. **Time:** ~3-5 min per target.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Probe Pattern
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Static fetch (curl)
|
|
11
|
+
curl -sL -A "Mozilla/5.0 (compatible; aegis-audit/1.0)" "$TARGET" > /tmp/audit-html-static.html
|
|
12
|
+
|
|
13
|
+
# Dynamic fetch (Playwright — captures JS-rendered content)
|
|
14
|
+
npx -y playwright-core@latest <<EOF
|
|
15
|
+
const { chromium } = require('playwright-core');
|
|
16
|
+
(async () => {
|
|
17
|
+
const b = await chromium.launch();
|
|
18
|
+
const ctx = await b.newContext({ userAgent: 'aegis-audit/1.0' });
|
|
19
|
+
const p = await ctx.newPage();
|
|
20
|
+
await p.goto('$TARGET', { waitUntil: 'networkidle', timeout: 30000 });
|
|
21
|
+
console.log(await p.content());
|
|
22
|
+
await b.close();
|
|
23
|
+
})();
|
|
24
|
+
EOF > /tmp/audit-html-dynamic.html
|
|
25
|
+
|
|
26
|
+
# Diff — large diff = heavy client-rendering (SSR/CSR balance check)
|
|
27
|
+
wc -l /tmp/audit-html-*.html
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Structural Checklist
|
|
33
|
+
|
|
34
|
+
| Check | How | Severity |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `<!doctype html>` present | `head -1 audit-html-static.html` | HOCH (missing) |
|
|
37
|
+
| `<html lang="...">` set | `grep -oE '<html[^>]*lang=' audit-html-static.html` | MITTEL (missing) |
|
|
38
|
+
| `<title>` present + non-empty + < 60 chars | `grep -oE '<title>[^<]*</title>'` | HOCH (missing or empty) |
|
|
39
|
+
| `<meta charset="utf-8">` | `grep -oE '<meta[^>]*charset=' audit-html-static.html \| head -1` | MITTEL (missing) |
|
|
40
|
+
| `<meta name="viewport">` | `grep -oE '<meta[^>]*viewport='` | HOCH (missing — mobile broken) |
|
|
41
|
+
| `<meta name="description">` | `grep -oE '<meta[^>]*description='` | HOCH (missing) |
|
|
42
|
+
| Heading-hierarchy (one h1, h2 → h3 ordering) | parse + check | MITTEL (multiple h1) / LOW (skip h2 → h3) |
|
|
43
|
+
| Open-Graph tags (og:title, og:description, og:image, og:url) | `grep -E 'og:(title\|description\|image\|url)'` | MITTEL (missing) |
|
|
44
|
+
| Twitter card tags | `grep -E 'twitter:card'` | LOW (missing) |
|
|
45
|
+
| Schema.org JSON-LD | `grep -oE 'application/ld\+json'` | LOW (missing) |
|
|
46
|
+
| Canonical link | `grep -oE 'rel="canonical"'` | MITTEL (missing) |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Image Checks (alt-text, lazy-load, dimensions)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Find all img tags
|
|
54
|
+
grep -oE '<img[^>]*>' /tmp/audit-html-static.html > /tmp/audit-imgs.txt
|
|
55
|
+
|
|
56
|
+
# Count
|
|
57
|
+
total=$(wc -l < /tmp/audit-imgs.txt)
|
|
58
|
+
|
|
59
|
+
# Missing alt-text
|
|
60
|
+
missing_alt=$(grep -cv 'alt=' /tmp/audit-imgs.txt)
|
|
61
|
+
empty_alt=$(grep -c 'alt=""' /tmp/audit-imgs.txt)
|
|
62
|
+
|
|
63
|
+
# Missing dimensions (CLS issue)
|
|
64
|
+
missing_dim=$(grep -cE 'width=' /tmp/audit-imgs.txt | head -1)
|
|
65
|
+
|
|
66
|
+
echo "Images: $total, missing alt: $missing_alt, empty alt: $empty_alt"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| Check | Severity |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Image without `alt=` | HOCH (A11y violation) |
|
|
72
|
+
| Image with `alt=""` (and not decorative-only) | MITTEL (A11y) |
|
|
73
|
+
| Image without `width` + `height` (causes CLS) | MITTEL |
|
|
74
|
+
| Image without `loading="lazy"` (below fold) | LOW (perf) |
|
|
75
|
+
| Image larger than render-size (e.g., 4000px-image rendered at 800px) | MITTEL (perf) |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## SSR-Skeleton Detection
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Static HTML body length vs dynamic
|
|
83
|
+
static_body_len=$(grep -oE '<body[^>]*>.*</body>' /tmp/audit-html-static.html | wc -c)
|
|
84
|
+
dynamic_body_len=$(grep -oE '<body[^>]*>.*</body>' /tmp/audit-html-dynamic.html | wc -c)
|
|
85
|
+
|
|
86
|
+
# If static body is < 20% of dynamic body — SSR-skeleton (SEO-bad)
|
|
87
|
+
ratio=$(echo "scale=2; $static_body_len / $dynamic_body_len" | bc)
|
|
88
|
+
[ $(echo "$ratio < 0.2" | bc) = 1 ] && echo "L2-SSR-SKELETON-ONLY: HOCH"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For Next.js App Router: SSR-skeleton + heavy client-side rendering = SEO penalty. Phase 2 architecture decisions should favor RSC.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Inline-Script Detection (CSP cross-check)
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Count inline scripts (no src attr)
|
|
99
|
+
inline_count=$(grep -oE '<script[^s]*>' /tmp/audit-html-static.html | wc -l)
|
|
100
|
+
|
|
101
|
+
# Big inline scripts may indicate injected analytics or unsafe-inline patterns
|
|
102
|
+
grep -oE '<script[^s][^>]*>[^<]{100,}' /tmp/audit-html-static.html | head -5
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If inline-scripts found AND Layer 1 CSP allows `'unsafe-inline'` on `script-src` → composite HOCH.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Footer-Link Resolution (feeds Layer 3)
|
|
110
|
+
|
|
111
|
+
Layer 3 (Impressum) needs the footer-link to /impressum. Resolve via:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Find footer
|
|
115
|
+
footer=$(grep -oE '<footer[^>]*>.*</footer>' /tmp/audit-html-static.html | head -1)
|
|
116
|
+
|
|
117
|
+
# Find links in footer
|
|
118
|
+
echo "$footer" | grep -oE '<a[^>]*href="[^"]*"[^>]*>[^<]*</a>'
|
|
119
|
+
|
|
120
|
+
# Extract the impressum-href
|
|
121
|
+
echo "$footer" | grep -oE 'href="[^"]*impressum[^"]*"' | head -1
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If no `/impressum` link in footer or in main-nav — Layer 3 reports L3-IMPRESSUM-NO-FOOTER-LINK: HOCH.
|
|
125
|
+
|
|
126
|
+
If link exists but points to 404 — Layer 3 reports L3-IMPRESSUM-LINK-BROKEN: KRITISCH.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Findings Format
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
- id: L2-IMG-MISSING-ALT
|
|
134
|
+
layer: 2
|
|
135
|
+
severity: HOCH
|
|
136
|
+
evidence:
|
|
137
|
+
url: <target>
|
|
138
|
+
img_count: 14
|
|
139
|
+
missing_alt: 3
|
|
140
|
+
affected: ["img[1]", "img[7]", "img[12]"] # XPath-ish
|
|
141
|
+
recommendation: "Add meaningful alt-text to each img per WCAG 2.1 1.1.1"
|
|
142
|
+
citation: "WCAG 2.1 1.1.1, BFSG §3"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Anti-Patterns specific to Layer 2
|
|
148
|
+
|
|
149
|
+
- ❌ Skipping Playwright fetch "because curl is faster" — JS-rendered content invisible to curl-only.
|
|
150
|
+
- ❌ Reporting "no h1" without parsing — DOM-order matters; first h1 might be inside a `<header role="banner">`.
|
|
151
|
+
- ❌ Marking `alt=""` as KRITISCH — empty alt is correct for purely-decorative images per WCAG; downgrade.
|
|
152
|
+
- ❌ Reporting "missing OG-tags" for an internal admin-page — admin-pages don't need OG.
|
|
153
|
+
- ❌ Inferring SSR-skeleton from static-body length alone — also check if `<noscript>` content fills the gap.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Layer 3 Reference — Impressum (DDG §5)
|
|
2
|
+
|
|
3
|
+
Layer 3 verifies the Impressum (legal notice) per DDG §5 (formerly TMG §5, renamed 2024-05-14 with TDDDG enactment). Catches: missing pflichtangaben, broken links, browser-vs-bot-walled pages. **Time:** ~3-5 min per target.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Pflichtangaben Checklist (DDG §5)
|
|
8
|
+
|
|
9
|
+
| # | Pflichtangabe | Field-name | Required when |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| 1 | Name + Anschrift | Geschäftsbezeichnung + Straße + PLZ + Ort | always (geschäftsmäßig) |
|
|
12
|
+
| 2 | Vertretungsberechtigter | Geschäftsführer (bei juristischen Personen) | for GmbH/AG/UG/etc. |
|
|
13
|
+
| 3 | Kontakt: E-Mail + Telefon | E-Mail + Tel. | always |
|
|
14
|
+
| 4 | Handelsregister | HRB-Nummer + Registergericht | for handelsregister-pflichtige Rechtsformen |
|
|
15
|
+
| 5 | Umsatzsteuer-ID | USt-IdNr. (`DE...`) | when § 27a UStG applies |
|
|
16
|
+
| 6 | Wirtschafts-ID | W-IdNr. | when applicable |
|
|
17
|
+
| 7 | Aufsichtsbehörde | Name + Adresse | for state-licensed industries (Anwalt, Arzt, Architekt, Steuerberater, ...) |
|
|
18
|
+
| 8 | Berufsbezeichnung + Kammer | Bezeichnung + verleihender Staat + Kammer | for regulated professions |
|
|
19
|
+
| 9 | Berufshaftpflicht | Versicherer + räumlicher Geltungsbereich | for regulated professions (Anwalt, Arzt, ...) |
|
|
20
|
+
| 10 | Online-Streitbeilegung Hinweis | Link zu ec.europa.eu/consumers/odr | for B2C with online-business |
|
|
21
|
+
| 11 | Verbraucherstreitbeilegung Hinweis | Bereit-/Nicht-Bereit-Erklärung | for B2C |
|
|
22
|
+
| 12 | Inhaltlich Verantwortlicher (§ 18 Abs. 2 MStV) | Name + Anschrift | for journalistic-redaktional Angebote |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Probe Pattern
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Resolve impressum-URL
|
|
30
|
+
IMPRESSUM_URL=$(node -e "
|
|
31
|
+
const html = require('fs').readFileSync('/tmp/audit-html-static.html', 'utf-8');
|
|
32
|
+
const match = html.match(/href=['\"]([^'\"]*impressum[^'\"]*)['\"]/i);
|
|
33
|
+
console.log(match ? new URL(match[1], '$TARGET').href : '');
|
|
34
|
+
")
|
|
35
|
+
|
|
36
|
+
if [ -z "$IMPRESSUM_URL" ]; then
|
|
37
|
+
echo "L3-IMPRESSUM-NO-FOOTER-LINK: KRITISCH"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Fetch with browser-UA (some sites bot-wall)
|
|
42
|
+
curl -sL -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" "$IMPRESSUM_URL" > /tmp/audit-impressum.html
|
|
43
|
+
|
|
44
|
+
# Or fall-back to Playwright
|
|
45
|
+
[ "$(wc -c < /tmp/audit-impressum.html)" -lt 1000 ] && {
|
|
46
|
+
npx -y playwright-core@latest <<EOF | tee /tmp/audit-impressum.html
|
|
47
|
+
const { chromium } = require('playwright-core');
|
|
48
|
+
(async () => {
|
|
49
|
+
const b = await chromium.launch();
|
|
50
|
+
const p = await (await b.newContext()).newPage();
|
|
51
|
+
await p.goto('$IMPRESSUM_URL', { waitUntil: 'networkidle' });
|
|
52
|
+
console.log(await p.content());
|
|
53
|
+
await b.close();
|
|
54
|
+
})();
|
|
55
|
+
EOF
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Per-Field Detection Patterns
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Geschäftsbezeichnung + Anschrift (presence check)
|
|
65
|
+
grep -E '(GmbH|UG|AG|e\.K\.|[A-ZÄÖÜ][a-zäöü]+ [A-ZÄÖÜ][a-zäöü]+)' /tmp/audit-impressum.html | head -3
|
|
66
|
+
grep -E '\b[0-9]{5}\b' /tmp/audit-impressum.html # German postal code
|
|
67
|
+
|
|
68
|
+
# E-Mail + Telefon
|
|
69
|
+
grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' /tmp/audit-impressum.html | head -3
|
|
70
|
+
grep -oE '\+?49[ /-]?[0-9 /-]{7,}' /tmp/audit-impressum.html | head -3
|
|
71
|
+
|
|
72
|
+
# USt-IdNr.
|
|
73
|
+
grep -oE 'DE[ ]?[0-9]{9}' /tmp/audit-impressum.html
|
|
74
|
+
|
|
75
|
+
# Handelsregister
|
|
76
|
+
grep -oE 'HRB[ ]?[0-9]+' /tmp/audit-impressum.html
|
|
77
|
+
grep -oE 'Amtsgericht [A-ZÄÖÜ][a-zäöü]+' /tmp/audit-impressum.html
|
|
78
|
+
|
|
79
|
+
# OS-Streitbeilegung Link
|
|
80
|
+
grep -E 'ec\.europa\.eu/consumers/odr' /tmp/audit-impressum.html
|
|
81
|
+
|
|
82
|
+
# Verbraucherstreitbeilegung Hinweis
|
|
83
|
+
grep -E '(Verbraucherstreitbeilegung|Streitbeilegungsverfahren)' /tmp/audit-impressum.html
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Severity Matrix
|
|
89
|
+
|
|
90
|
+
| Missing field | Severity |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Geschäftsbezeichnung + Anschrift | KRITISCH |
|
|
93
|
+
| Vertretungsberechtigter (when GmbH/AG/UG) | KRITISCH |
|
|
94
|
+
| E-Mail | KRITISCH |
|
|
95
|
+
| Telefon | HOCH (some courts accept E-Mail-only; abmahn-risk) |
|
|
96
|
+
| HRB + Registergericht (when handelsregister-pflichtig) | KRITISCH |
|
|
97
|
+
| USt-IdNr. (when § 27a UStG applies) | KRITISCH |
|
|
98
|
+
| Aufsichtsbehörde (for regulated industries) | KRITISCH |
|
|
99
|
+
| Berufshaftpflicht (for regulated professions) | KRITISCH |
|
|
100
|
+
| OS-Streitbeilegung Link (when B2C online) | HOCH |
|
|
101
|
+
| Verbraucherstreitbeilegung Hinweis (B2C) | MITTEL |
|
|
102
|
+
| Inhaltlich Verantwortlicher (when journalistic) | KRITISCH |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Cross-Check: Code-Repo (when Layer 7 enabled)
|
|
107
|
+
|
|
108
|
+
If aegis-audit runs against a local repo (Layer 7 enabled), cross-check:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Find impressum data in code
|
|
112
|
+
find . -path ./node_modules -prune -o -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.json' -o -name '*.md' \) -print | xargs grep -lE 'impressum|HRB|USt-IdNr' 2>/dev/null
|
|
113
|
+
|
|
114
|
+
# Verify code-data matches rendered-data
|
|
115
|
+
# E.g., site says HRB 12345 but code has HRB 67890 — drift
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If drift detected → L3-IMPRESSUM-CODE-DRIFT: HOCH (data inconsistency).
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Court-Decision References
|
|
123
|
+
|
|
124
|
+
For findings, cite court-decisions where applicable:
|
|
125
|
+
|
|
126
|
+
- KG Berlin 2010-04-21 (5 W 39/10) — vollständiger Name + Anschrift Pflicht
|
|
127
|
+
- LG Düsseldorf 2008-05-21 (12 O 250/07) — Telefon Pflicht für unmittelbare Kontaktaufnahme
|
|
128
|
+
- EuGH 2008-10-16 (C-298/07) — § 5 TMG (= jetzt DDG §5) ist Verbraucherinformations-Pflicht; B2B + B2C
|
|
129
|
+
- BGH 2007-09-20 (I ZR 88/05) — Impressum 2-clicks-rule (vom Footer aus erreichbar)
|
|
130
|
+
- LG München I 2022-01-20 (3 O 17493/20) — Google-Fonts (cross-layer with L4 + L5)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Findings Format
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
- id: L3-IMPRESSUM-VAT-MISSING
|
|
138
|
+
layer: 3
|
|
139
|
+
severity: KRITISCH
|
|
140
|
+
evidence:
|
|
141
|
+
url: <impressum-url>
|
|
142
|
+
field: USt-IdNr.
|
|
143
|
+
matches: []
|
|
144
|
+
detection: "no DE-prefixed VAT-ID found in /impressum HTML"
|
|
145
|
+
recommendation: "Add 'Umsatzsteuer-Identifikationsnummer: DE123456789' under § 27a UStG section"
|
|
146
|
+
citation: "DDG § 5 Abs. 1 Nr. 6, § 27a UStG"
|
|
147
|
+
abmahn_risk: "€500-2000 per finding (industry × visibility-dependent)"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Anti-Patterns specific to Layer 3
|
|
153
|
+
|
|
154
|
+
- ❌ Reporting "USt-IdNr. missing" for a Kleinunternehmer (§ 19 UStG) — only § 27a UStG businesses need to publish.
|
|
155
|
+
- ❌ Reporting "OS-Streitbeilegung missing" for B2B-only sites — only B2C requires this.
|
|
156
|
+
- ❌ Skipping browser-UA fallback — many sites bot-wall scanners; without browser-UA the impressum returns 403.
|
|
157
|
+
- ❌ Inferring "Aufsichtsbehörde missing" without first detecting industry — non-regulated industries don't need this.
|
|
158
|
+
- ❌ Reporting on Inhaltlich-Verantwortlicher (§ 18 MStV) for non-journalistic sites — pure commercial sites don't need.
|
|
159
|
+
- ❌ Reporting "missing Berufshaftpflicht" for unregulated professions — pflicht only for Anwalt / Arzt / Steuerberater / Architekt.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Layer 4 Reference — DSE (Datenschutzerklärung, DSGVO Art. 13/14)
|
|
2
|
+
|
|
3
|
+
Layer 4 verifies the Datenschutzerklärung against DSGVO Art. 13 (Informationspflicht bei Erhebung beim Betroffenen) + Art. 14 (Erhebung bei Dritten) + Drittlandtransfer-Konformität (Schrems-II / SCC). **Time:** ~5-10 min per target.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Art. 13 Pflichtangaben Checklist
|
|
8
|
+
|
|
9
|
+
| # | Field | Severity if missing |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 1 | Verantwortlicher (Name + Anschrift) | KRITISCH |
|
|
12
|
+
| 2 | Vertreter in der EU (when applicable) | HOCH |
|
|
13
|
+
| 3 | Datenschutzbeauftragter (when applicable per Art. 37) | HOCH (when pflicht) |
|
|
14
|
+
| 4 | Verarbeitungszwecke + Rechtsgrundlage (Art. 6 / Art. 9) | KRITISCH |
|
|
15
|
+
| 5 | Berechtigte Interessen (when Art. 6 Abs. 1 lit. f) | HOCH |
|
|
16
|
+
| 6 | Empfänger / Empfängerkategorien | HOCH |
|
|
17
|
+
| 7 | Drittlandtransfer + Schutzgarantien | KRITISCH (when transfer happens but DSE absent) |
|
|
18
|
+
| 8 | Speicherdauer / Löschkonzept | HOCH |
|
|
19
|
+
| 9 | Betroffenenrechte (Art. 15-22) | KRITISCH |
|
|
20
|
+
| 10 | Beschwerderecht bei Aufsichtsbehörde | HOCH |
|
|
21
|
+
| 11 | Pflicht zur Bereitstellung + Folgen | MITTEL |
|
|
22
|
+
| 12 | Automatisierte Entscheidung / Profiling | HOCH (when applicable) |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Probe Pattern
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Resolve DSE-URL (often /datenschutz, /privacy, /datenschutzerklaerung)
|
|
30
|
+
DSE_URL=$(node -e "
|
|
31
|
+
const html = require('fs').readFileSync('/tmp/audit-html-static.html', 'utf-8');
|
|
32
|
+
const match = html.match(/href=['\"]([^'\"]*(datenschutz|privacy)[^'\"]*)['\"]/i);
|
|
33
|
+
console.log(match ? new URL(match[1], '$TARGET').href : '');
|
|
34
|
+
")
|
|
35
|
+
|
|
36
|
+
curl -sL -A "Mozilla/5.0" "$DSE_URL" > /tmp/audit-dse.html
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Per-Field Detection Patterns
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Verantwortlicher (matches typical phrasing)
|
|
45
|
+
grep -E 'Verantwortliche[r]?\s+i[mn]?\s+Sinne' /tmp/audit-dse.html
|
|
46
|
+
|
|
47
|
+
# Datenschutzbeauftragter
|
|
48
|
+
grep -iE '(Datenschutzbeauftragte[rn]|Data Protection Officer|DSB)' /tmp/audit-dse.html
|
|
49
|
+
|
|
50
|
+
# Verarbeitungszwecke (look for Art. 6 references)
|
|
51
|
+
grep -E 'Art\.\s*6\s*Abs\.\s*1\s*lit\.\s*[abcdef]' /tmp/audit-dse.html
|
|
52
|
+
|
|
53
|
+
# Drittlandtransfer (US, GB post-Brexit)
|
|
54
|
+
grep -iE '(Drittland|USA|United States|Standardvertragsklauseln|SCC|Privacy Shield|TIA)' /tmp/audit-dse.html
|
|
55
|
+
|
|
56
|
+
# Speicherdauer
|
|
57
|
+
grep -iE '(Speicherdauer|Löschkonzept|Aufbewahrungsfrist)' /tmp/audit-dse.html
|
|
58
|
+
|
|
59
|
+
# Betroffenenrechte (Art. 15-22)
|
|
60
|
+
grep -E 'Art\.\s*1[5-9]|Art\.\s*2[0-2]' /tmp/audit-dse.html
|
|
61
|
+
grep -iE '(Auskunftsrecht|Berichtigung|Löschung|Einschränkung|Datenübertragbarkeit|Widerspruch)' /tmp/audit-dse.html
|
|
62
|
+
|
|
63
|
+
# Beschwerderecht
|
|
64
|
+
grep -iE '(Beschwerderecht|Aufsichtsbehörde|Datenschutzbeauftragte des Landes)' /tmp/audit-dse.html
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Drittland-Transfer Detection (cross-check Layer 1 + Layer 5)
|
|
70
|
+
|
|
71
|
+
Layer 4 cross-references with Layer 1 (CDN / 3rd-party domains in HTTP-headers) + Layer 5 (cookies set by 3rd-party trackers) to detect:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# 3rd-party domains active on the page (from Layer 1 / Layer 5)
|
|
75
|
+
3p_domains=$(grep -oE 'https?://[^/]+' /tmp/audit-html-dynamic.html | sort -u | grep -v "$TARGET_DOMAIN")
|
|
76
|
+
|
|
77
|
+
# For each 3rd-party, check if DSE mentions it
|
|
78
|
+
for domain in $3p_domains; do
|
|
79
|
+
if grep -q "$domain" /tmp/audit-dse.html; then
|
|
80
|
+
echo "L4-DSE-MENTIONS: $domain ✓"
|
|
81
|
+
else
|
|
82
|
+
echo "L4-DSE-3P-NOT-MENTIONED: $domain (HOCH)"
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
US-headquartered 3rd-parties (Google, Meta, Cloudflare, AWS) trigger Drittlandtransfer concerns. DSE MUST mention:
|
|
88
|
+
|
|
89
|
+
- The 3rd-party (by name or category)
|
|
90
|
+
- The country of transfer
|
|
91
|
+
- The schutzgarantien (SCC + TIA per Schrems-II requirements post-2020-07-16 EuGH C-311/18)
|
|
92
|
+
|
|
93
|
+
If 3rd-party transfer happens AND DSE doesn't address it → KRITISCH (Schrems-II).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Common Schrems-II Findings
|
|
98
|
+
|
|
99
|
+
| 3rd-party | DSE-required addressing |
|
|
100
|
+
|---|---|
|
|
101
|
+
| Google Fonts (when loaded from fonts.googleapis.com) | KRITISCH if not local-bundled (LG München I 2022-01-20 3 O 17493/20) |
|
|
102
|
+
| Google Analytics (without IP-anonymization + consent) | KRITISCH |
|
|
103
|
+
| Google Maps embed (without consent OR static-image-fallback) | HOCH |
|
|
104
|
+
| reCAPTCHA v3 (silent tracking) | KRITISCH |
|
|
105
|
+
| Cloudflare (when used as CDN with EU-traffic) | MITTEL (DPA exists; SCC referenced) |
|
|
106
|
+
| AWS / Azure / GCP (US-region) | HOCH (require SCC-DPA) |
|
|
107
|
+
| Vercel (US-hosted) | MITTEL (Vercel offers DPA) |
|
|
108
|
+
| Mailchimp / Sendinblue / Brevo | HOCH (US/EU split, requires SCC) |
|
|
109
|
+
|
|
110
|
+
For each, check if DSE has the required transfer-section AND processor-AVV (Auftragsverarbeitung) listing.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## AVV-List Detection
|
|
115
|
+
|
|
116
|
+
DSGVO Art. 28 requires AVV (Auftragsverarbeitung-Vertrag) for every processor handling personal data. DSE should reference:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
grep -iE '(Auftragsverarbeitung|Art\.\s*28|AVV)' /tmp/audit-dse.html
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For each 3rd-party from Layer 1 + 5: AVV must exist (signed contract); DSE should mention by name.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Severity Matrix
|
|
127
|
+
|
|
128
|
+
| Missing | Severity |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Verantwortlicher (Art. 13 Abs. 1 lit. a) | KRITISCH |
|
|
131
|
+
| Verarbeitungszwecke + Rechtsgrundlage (Art. 13 Abs. 1 lit. c-d) | KRITISCH |
|
|
132
|
+
| Betroffenenrechte (Art. 15-22) | KRITISCH |
|
|
133
|
+
| Drittlandtransfer + SCC + TIA (when transfer happens) | KRITISCH |
|
|
134
|
+
| Speicherdauer (Art. 13 Abs. 2 lit. a) | HOCH |
|
|
135
|
+
| Empfänger (Art. 13 Abs. 1 lit. e) | HOCH |
|
|
136
|
+
| Beschwerderecht (Art. 13 Abs. 2 lit. d) | HOCH |
|
|
137
|
+
| Datenschutzbeauftragter (when pflicht per Art. 37) | HOCH |
|
|
138
|
+
| Vertreter in der EU (when ext.-EU controller) | HOCH |
|
|
139
|
+
| Automatisierte Entscheidung (when applicable) | HOCH |
|
|
140
|
+
| Pflicht zur Bereitstellung (Art. 13 Abs. 2 lit. e) | MITTEL |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Court-Decision References
|
|
145
|
+
|
|
146
|
+
- EuGH 2020-07-16 (C-311/18) — Schrems-II — invalidation of Privacy Shield; SCC + TIA required
|
|
147
|
+
- LG München I 2022-01-20 (3 O 17493/20) — Google-Fonts via Google CDN = Drittlandtransfer ohne Rechtsgrundlage; 100€ Schadensersatz pro Betroffenem
|
|
148
|
+
- BGH 2020-05-28 (I ZR 7/16) — Cookie-Banner BGB
|
|
149
|
+
- OLG Düsseldorf 2019-03-26 (I-20 U 75/18) — DSE als wettbewerbsrechtliche Pflicht (UWG abmahnbar)
|
|
150
|
+
- EDSA Recommendations 01/2020 — Schutzgarantien-Beurteilung TIA-Methodik
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Findings Format
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
- id: L4-DSE-DRITTLAND-MISSING
|
|
158
|
+
layer: 4
|
|
159
|
+
severity: KRITISCH
|
|
160
|
+
evidence:
|
|
161
|
+
dse_url: <dse-url>
|
|
162
|
+
detected_3p: ["fonts.googleapis.com", "www.google-analytics.com"]
|
|
163
|
+
dse_mentions_drittland: false
|
|
164
|
+
dse_mentions_scc: false
|
|
165
|
+
recommendation: "Add Drittlandtransfer-section listing US-3rd-parties (Google Fonts, GA), reference SCC + TIA per Schrems-II"
|
|
166
|
+
citation: "DSGVO Art. 13 Abs. 1 lit. f, Art. 46; EuGH C-311/18 (Schrems-II); LG München I 3 O 17493/20"
|
|
167
|
+
abmahn_risk: "€2000-15000 per Betroffenem (LG München-Linie); aggregated risk per visitor-month"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Anti-Patterns specific to Layer 4
|
|
173
|
+
|
|
174
|
+
- ❌ Reporting "Drittlandtransfer missing" without verifying transfer actually happens — first detect 3rd-parties via Layer 1 + 5.
|
|
175
|
+
- ❌ Skipping AVV-cross-check — Art. 28 AVV is separate from Art. 13 DSE.
|
|
176
|
+
- ❌ Reporting "DSB missing" for small businesses (< 20 employees handling personal data routinely) — Art. 37 thresholds apply.
|
|
177
|
+
- ❌ Inferring DSE-completeness from word-count — short DSE can be complete; long DSE can miss fields.
|
|
178
|
+
- ❌ Reporting on automatisierte Entscheidung when site has no profiling — only applies when profiling/decisions actually happen.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Layer 5 Reference — Cookie + Consent (TTDSG/TDDDG §25)
|
|
2
|
+
|
|
3
|
+
Layer 5 verifies cookie-banner + consent-flow against TTDSG/TDDDG §25 (formerly TTDSG, renamed 2024-05-14 with TDDDG enactment) + BGH cookie-decision (2020-05-28) + EU-Cookie-Banner-Guidelines. **Time:** ~5-10 min per target.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Probe Pattern
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Fresh page-load (no prior consent-cookies)
|
|
11
|
+
mkdir -p /tmp/audit-cookie-jar
|
|
12
|
+
rm -f /tmp/audit-cookie-jar/*
|
|
13
|
+
curl -sL -A "Mozilla/5.0" -c /tmp/audit-cookie-jar/cookies.txt "$TARGET" > /tmp/audit-fresh.html
|
|
14
|
+
|
|
15
|
+
# What cookies were set BEFORE consent?
|
|
16
|
+
cat /tmp/audit-cookie-jar/cookies.txt
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then a Playwright-based probe to detect dynamic cookies (set by JS) AND verify the consent-banner appears + works:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y playwright-core@latest <<EOF
|
|
23
|
+
const { chromium } = require('playwright-core');
|
|
24
|
+
(async () => {
|
|
25
|
+
const b = await chromium.launch();
|
|
26
|
+
const ctx = await b.newContext();
|
|
27
|
+
const p = await ctx.newPage();
|
|
28
|
+
|
|
29
|
+
// Capture all requests + responses
|
|
30
|
+
const requests = [];
|
|
31
|
+
p.on('request', r => requests.push({ url: r.url(), type: r.resourceType() }));
|
|
32
|
+
|
|
33
|
+
// Navigate
|
|
34
|
+
await p.goto('$TARGET', { waitUntil: 'networkidle' });
|
|
35
|
+
|
|
36
|
+
// Capture cookies pre-consent
|
|
37
|
+
const preConsentCookies = await ctx.cookies();
|
|
38
|
+
|
|
39
|
+
// Check for consent-banner
|
|
40
|
+
const bannerSelector = 'div[id*="cookie"], div[class*="cookie"], div[class*="consent"]';
|
|
41
|
+
const banner = await p.$(bannerSelector);
|
|
42
|
+
const bannerHasBoth = banner ? await banner.evaluate(el => {
|
|
43
|
+
const text = el.innerText.toLowerCase();
|
|
44
|
+
return text.includes('akzeptieren') && (text.includes('ablehnen') || text.includes('reject'));
|
|
45
|
+
}) : false;
|
|
46
|
+
|
|
47
|
+
console.log(JSON.stringify({
|
|
48
|
+
preConsentCookies,
|
|
49
|
+
requests: requests.length,
|
|
50
|
+
bannerPresent: !!banner,
|
|
51
|
+
bannerHasBoth,
|
|
52
|
+
}, null, 2));
|
|
53
|
+
|
|
54
|
+
await b.close();
|
|
55
|
+
})();
|
|
56
|
+
EOF > /tmp/audit-cookie-probe.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## TTDSG/TDDDG §25 Pre-Consent-Tracker Detection
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Pre-consent cookies (any technically-non-required cookie set before consent = §25 violation)
|
|
65
|
+
pre_count=$(jq -r '.preConsentCookies | length' /tmp/audit-cookie-probe.json)
|
|
66
|
+
|
|
67
|
+
# Filter for tracking-cookies (heuristic: 3rd-party domain or known-tracking-pattern)
|
|
68
|
+
tracking_pre=$(jq -r '.preConsentCookies[] | select(.domain | test("google|facebook|meta|doubleclick|hotjar|matomo|fathom|plausible|gtag|gads|fb_pixel|tiktok"))' /tmp/audit-cookie-probe.json)
|
|
69
|
+
|
|
70
|
+
[ -n "$tracking_pre" ] && echo "L5-PRE-CONSENT-TRACKER: KRITISCH"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Banner-Pattern Compliance
|
|
76
|
+
|
|
77
|
+
| Check | Severity if violated |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Banner appears on first page-load | KRITISCH (no banner = no consent collected) |
|
|
80
|
+
| Banner has equal-prominence "Accept" + "Reject all" | KRITISCH (BGH I ZR 7/16 — dark-pattern) |
|
|
81
|
+
| Banner is granular (per-vendor opt-in for non-essential) | HOCH (BGH 2020-05-28 — global-opt-in invalid) |
|
|
82
|
+
| Banner appears BEFORE any tracking-cookie set | KRITISCH (TTDSG §25) |
|
|
83
|
+
| User can opt-out at any time (e.g., footer-link to /cookies/einstellungen) | HOCH |
|
|
84
|
+
| Pre-checked checkboxes for opt-in | KRITISCH (DSGVO Art. 7 Abs. 2 — explicit consent required) |
|
|
85
|
+
| Bundling consent (e.g., AGB + Cookie consent in one checkbox) | HOCH |
|
|
86
|
+
| Continuing-to-use-site interpreted as consent | KRITISCH (BGH 2020-05-28 — mere browsing ≠ consent) |
|
|
87
|
+
| Re-prompt period reasonable (≥ 6 months between prompts unless settings changed) | LOW |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Granular vs Global
|
|
92
|
+
|
|
93
|
+
| Pattern | Compliance |
|
|
94
|
+
|---|---|
|
|
95
|
+
| "Akzeptieren" + "Ablehnen" only (no granular) | HOCH (BGH-line — granular better) |
|
|
96
|
+
| "Akzeptieren" + "Ablehnen" + per-vendor toggle | OK (granular) |
|
|
97
|
+
| "Akzeptieren" + "Einstellungen" (where settings = granular) | OK if reachable in 1 click |
|
|
98
|
+
| "Akzeptieren" + tiny "Ablehnen" (visual asymmetry) | KRITISCH (dark-pattern, BGH-line) |
|
|
99
|
+
| "Akzeptieren" only (no reject-button on first banner) | KRITISCH |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Consent-Mode-v2 (Google) Detection
|
|
104
|
+
|
|
105
|
+
If site uses Google products (Analytics, Ads, Tag Manager), check for Consent-Mode-v2:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
grep -E 'gtag\(.consent.,\s*.default.|consentDefault' /tmp/audit-fresh.html
|
|
109
|
+
|
|
110
|
+
# default state should be 'denied' for ad_storage, ad_user_data, ad_personalization, analytics_storage
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Default-`granted` for analytics_storage = pre-consent-tracking even with Consent-Mode = KRITISCH.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Cookie-Categories Mapping
|
|
118
|
+
|
|
119
|
+
For each cookie set on the site, classify:
|
|
120
|
+
|
|
121
|
+
| Category | Consent-required | Examples |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| Strictly necessary | NO | session-id, csrf-token, cookie-consent-state |
|
|
124
|
+
| Functional | Implicitly OK | language-preference, theme |
|
|
125
|
+
| Analytics (anonymized) | YES (per §25) | matomo (with anonymize_ip), google-analytics-with-anonymize |
|
|
126
|
+
| Marketing / Advertising | YES (explicit, granular) | _ga, _gid (without anonymize_ip), _fbp, fr, doubleclick |
|
|
127
|
+
| Third-party content | YES (depending on data) | YouTube embed, Google Maps with API-key |
|
|
128
|
+
|
|
129
|
+
If a cookie is "Strictly necessary" — must be on a justified business-need; not just "we want it".
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Cookie-Banner-Library Detection
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Common libraries used on DACH sites
|
|
137
|
+
grep -iE '(usercentrics|cookiefirst|cookieyes|borlabs|webcm|consent-manager|onetrust|cookiepro|privacymanager|cookieinformation|complianz|iubenda)' /tmp/audit-fresh.html | head -3
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If a recognized library is used — check its current-version compliance (via library-CHANGELOG cross-reference; e.g., Borlabs Cookie ≥ 2.2.0 is compliant; older versions had known dark-pattern bugs).
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## CMP IAB-Framework Detection
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# IAB-TCF-API presence
|
|
148
|
+
grep -E '__tcfapi\(|TCF_API|cmp\.openWindow' /tmp/audit-fresh.html
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If TCF-v2 used — verify Vendor-List up-to-date + Purposes match what's actually loaded.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Findings Format
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
- id: L5-PRE-CONSENT-TRACKER
|
|
159
|
+
layer: 5
|
|
160
|
+
severity: KRITISCH
|
|
161
|
+
evidence:
|
|
162
|
+
target: <target>
|
|
163
|
+
pre_consent_cookies: ["_ga", "_gid", "_fbp"]
|
|
164
|
+
banner_present: true
|
|
165
|
+
banner_appears_after_load: true # but cookies were already set
|
|
166
|
+
recommendation: "Move all tracking-cookies behind cookie-banner consent. Use Google Consent-Mode-v2 with default='denied'. Verify with fresh page-load + cookies.txt empty."
|
|
167
|
+
citation: "TTDSG/TDDDG §25 Abs. 1; DSGVO Art. 7 Abs. 1; BGH I ZR 7/16 (2020-05-28); EuGH C-673/17 (Planet49)"
|
|
168
|
+
abmahn_risk: "€500-5000 per finding (industry × visibility); composite with L4-DSE-DRITTLAND-MISSING bumps to €5000-15000"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Anti-Patterns specific to Layer 5
|
|
174
|
+
|
|
175
|
+
- ❌ Probing with stale cookie-jar — always use fresh /tmp/audit-cookie-jar.
|
|
176
|
+
- ❌ Skipping Playwright probe "because curl is enough" — JS-set cookies invisible to curl.
|
|
177
|
+
- ❌ Reporting "no banner" for sites with technically-only cookies (no consent-required) — first verify what cookies are actually set.
|
|
178
|
+
- ❌ Marking "_ga" as KRITISCH if site uses Consent-Mode-v2 with default-denied — first check the consent-default-state.
|
|
179
|
+
- ❌ Reporting "global opt-in invalid" without checking BGH-line context — granular is preferred but global-only is HOCH not KRITISCH per current BGH-Linie.
|
|
180
|
+
- ❌ Inferring tracking-cookie from name alone — verify domain + actual purpose; some "_ga"-prefixed cookies are 1st-party-only.
|