7.9 KiB
7.9 KiB
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
{
"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
/eventsalle 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.
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:
GET /events?per_page=100→ Alle Events abrufen- Filtern auf
PushEvent - Für jeden PushEvent:
- Commits extrahieren (
payload.commits[]) - Pro Commit: Diff abrufen (
GET /repos/{owner}/{repo}/commits/{sha}mitAccept: diff) - Diff mit Regex scannen
- Bei Treffer: Key + Commit-URL + Repo-Name speichern
- Commits extrahieren (
- Deduplication:
shaals Schlüssel → gleichen Commit nie doppelt scannen - Neue Keys in DB speichern (wie beim normalen Scanner)
Phase 2: Event-Deduplication
// Kreisförmiger Buffer der letzten 10.000 verarbeiteten Event-IDs
private readonly HashSet<string> _processedEventIds = new();
private readonly Queue<string> _eventIdQueue = new();
private const int MaxProcessedIds = 10_000;
Phase 3: Commit-Diff-Scanning
private async Task<List<(string Key, string Url)>> 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
// Program.cs
builder.Services.AddHostedService<GitHubEventScannerService>();
{
"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:
- 🔥 Commits die
.env,.envrc,config.*,secrets.*ändern - 🔥 Commits mit kleinem Diff (< 10 KB) – wahrscheinlich manuelle Änderungen
- 🟡 Commits die
*.py,*.js,*.tsändern - ⬜ Alles andere (bei Zeit/Budget)
→ Die Commit-Detail-Response enthält files[].filename → Filterung vor Diff-Download möglich.