# Umsetzungsplan: GitHub Events API (Echtzeit-Scanner) ## Zusammenfassung Echtzeit-Monitoring öffentlicher GitHub Push-Events um geleakte API-Keys **sofort** nach dem Commit zu finden. GitGuardian nutzt genau diesen Ansatz und findet laut eigenen Angaben ~1800 neue Secrets pro Tag. ## Warum Events? - **Echtzeit:** Keys werden Sekunden nach dem Push gefunden (statt Stunden/Tage bei Code Search) - **Kein Suchindex-Delay:** GitHub Code Search indexiert neue Commits erst nach Minuten/Stunden - **Force-Push-Catches:** Selbst wenn ein User den Commit sofort mit Force-Push löscht – der Event bleibt kurz sichtbar - **Breite Abdeckung:** Alle öffentlichen Pushes weltweit, nicht nur bestimmte Sprachen/Pfade - **Ergänzend:** Findet Keys die der periodische Scan erst beim nächsten Lauf erwischt hätte ## API-Analyse ### GitHub Public Events API ``` GET https://api.github.com/events?per_page=100 ``` - Liefert die neuesten **öffentlichen** Events (PushEvent, CreateEvent, etc.) - Response enthält bis zu 300 Events (3 Seiten à 100) - Events werden **alle 60 Sekunden** aktualisiert - **Kein Authentication nötig** (aber empfohlen für höhere Rate-Limits) - Events bleiben **maximal 90 Tage** in der Timeline ### PushEvent-Struktur ```json { "type": "PushEvent", "repo": { "name": "user/repo" }, "payload": { "commits": [ { "sha": "abc123...", "message": "added config", "url": "https://api.github.com/repos/user/repo/commits/abc123" } ] } } ``` ### Commit-Diff abrufen ``` GET https://api.github.com/repos/{owner}/{repo}/commits/{sha} Accept: application/vnd.github.diff ``` → Gibt den unified diff als Plaintext zurück → kann mit Regex gescannt werden. **Alternativ:** Commit-Detail mit Patch: ``` GET https://api.github.com/repos/{owner}/{repo}/commits/{sha} ``` → JSON mit `files[].patch` (unified diff per Datei) ## Architektur-Entscheidung ### Option A: Polling-Loop (empfohlen) ✅ - Eigener Background-Worker, pollt `/events` alle 60-90 Sekunden - Filtert auf `PushEvent` - Für jeden Commit: Diff abrufen, mit Regex scannen - **Vorteil:** Einfach, zuverlässig, keine externe Abhängigkeit - **Nachteil:** Max 300 Events pro Poll → bei >300 Events/Minute gehen manche verloren ### Option B: GitHub Webhooks - Erfordert öffentlichen HTTPS-Endpoint auf unserem Server - GitHub sendet Events per POST an unseren Server - **Vorteil:** Kein Polling, kein Event-Verlust - **Nachteil:** Nur für eigene Repos/Orgs, nicht für "alle öffentlichen Events" - **Fazit:** ❌ Nicht für unseren Use-Case geeignet ### Option C: GitHub Firehose (gharchive.org) - GH Archive sammelt alle öffentlichen Events als JSON-Lines - Kann historisch abgefragt werden: `https://data.gharchive.org/2024-01-01-15.json.gz` - **Vorteil:** Kein Event-Verlust, historische Daten - **Nachteil:** Hoher Bandbreitenverbrauch, ~500MB/Stunde komprimiert - **Fazit:** ⚠️ Für späteren Ausbau, nicht für v1 ## Implementierungsplan ### Phase 1: Neuer BackgroundService `GitHubEventScannerService` Separater Service (nicht in `KeyScannerService`), da er **kontinuierlich** läuft statt periodisch. ```csharp public class GitHubEventScannerService : BackgroundService { private const int PollIntervalMs = 75_000; // 75 Sekunden (Events aktualisieren alle 60s) private const int MaxCommitsPerPoll = 50; // Max Commits pro Poll analysieren private string _lastEventId; // ETag/Last-Event-ID für Dedup } ``` **Ablauf pro Poll-Zyklus:** 1. `GET /events?per_page=100` → Alle Events abrufen 2. Filtern auf `PushEvent` 3. Für jeden PushEvent: - Commits extrahieren (`payload.commits[]`) - Pro Commit: Diff abrufen (`GET /repos/{owner}/{repo}/commits/{sha}` mit `Accept: diff`) - Diff mit Regex scannen - Bei Treffer: Key + Commit-URL + Repo-Name speichern 4. Deduplication: `sha` als Schlüssel → gleichen Commit nie doppelt scannen 5. Neue Keys in DB speichern (wie beim normalen Scanner) ### Phase 2: Event-Deduplication ```csharp // Kreisförmiger Buffer der letzten 10.000 verarbeiteten Event-IDs private readonly HashSet _processedEventIds = new(); private readonly Queue _eventIdQueue = new(); private const int MaxProcessedIds = 10_000; ``` ### Phase 3: Commit-Diff-Scanning ```csharp private async Task> ScanCommitDiff( HttpClient client, string repo, string sha, CancellationToken ct) { var keys = new List<(string Key, string Url)>(); // Diff als Plaintext abrufen var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/{repo}/commits/{sha}"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.diff")); var resp = await client.SendAsync(request, ct); if (!resp.IsSuccessStatusCode) return keys; var diff = await resp.Content.ReadAsStringAsync(ct); // Nur hinzugefügte Zeilen scannen (die mit "+" beginnen) foreach (var line in diff.Split('\n')) { if (!line.StartsWith("+")) continue; foreach (var (pattern, _) in KeyPatterns) { var regex = new Regex(pattern); foreach (Match m in regex.Matches(line)) { var url = $"https://github.com/{repo}/commit/{sha}"; keys.Add((m.Value, url)); } } } return keys; } ``` ### Phase 4: Integration & Config ```csharp // Program.cs builder.Services.AddHostedService(); ``` ```json { "GitHubEventScanner": true, // Feature-Toggle "GitHubToken": "ghp_xxx" // Gemeinsam mit Code Search } ``` ### Phase 5: Admin-UI - Status-Anzeige: "Event Scanner: Aktiv | Letzter Poll: vor 45s | Events/h: 2400 | Keys gefunden: 3" - Toggle zum Ein-/Ausschalten - Log der letzten gefundenen Keys mit Timestamp ## Rate-Limit-Strategie | Aktion | Rate Limit | Verbrauch pro Poll | |---|---|---| | List Events | 60 req/h (unauth) / 5000 req/h (auth) | 1-3 Requests | | Commit Diff | 5000 req/h (auth) | Max 50 Requests | | **Gesamt pro Stunde** | | ~2500 Requests (50 × 48 Polls) | **Optimierung:** - Nur PushEvents verarbeiten (~30% aller Events) - Nur Commits scannen deren Diff < 100 KB ist (keine Merge-Commits etc.) - Nur hinzugefügte Zeilen (`+`) scannen - Bei Rate-Limit-Warning: Poll-Intervall dynamisch erhöhen ## Volumenschätzung - ~5.000-10.000 Events/Minute auf GitHub (öffentlich) - Davon ~30% PushEvents = ~2.000/Minute - Wir sehen maximal 300 Events pro Poll = ~200 PushEvents - ~3-5 Commits pro Push im Schnitt = ~800 Commits - Davon scannen wir max 50 pro Poll (Priorität: kleine Diffs, .env-Dateien etc.) ## Aufwandsschätzung - **Implementierung:** ~300-400 Zeilen Code (neuer BackgroundService) - **Komplexität:** Mittel-Hoch (kontinuierlicher Worker, Rate-Limit-Management, Dedup) - **Erwarteter Ertrag:** 🔥🔥 Sehr hoch (Echtzeit-Catches, bevor Keys rotiert werden) ## Risiken - **Event-Verlust:** Bei >300 Events/Minute (≈5/Sekunde) werden Events verpasst → Akzeptabel, der periodische Code-Search-Scan fängt den Rest auf - **Hoher API-Verbrauch:** ~2500 req/h für Commit-Diffs → Hälfte des Rate-Limits → Kann mit smarter Filterung (nur kleine Diffs, nur bestimmte Dateien) reduziert werden - **False Positives:** Diffs können Test-/Mock-Keys enthalten → Wird durch den bestehenden Key-Check (`AnthropicKeyChecker`) aufgefangen - **Server-Last:** Kontinuierlicher Polling-Loop → minimal (1 HTTP-Request/75s + Diffs) ## Priorisierung innerhalb des Scans Nicht alle Commits sind gleich interessant. Reihenfolge: 1. 🔥 Commits die `.env`, `.envrc`, `config.*`, `secrets.*` ändern 2. 🔥 Commits mit kleinem Diff (< 10 KB) – wahrscheinlich manuelle Änderungen 3. 🟡 Commits die `*.py`, `*.js`, `*.ts` ändern 4. ⬜ Alles andere (bei Zeit/Budget) → Die Commit-Detail-Response enthält `files[].filename` → Filterung vor Diff-Download möglich.