Files
gott/Plans/[DONE] SCAN_GITHUB_EVENTS.md

7.9 KiB
Raw Permalink Blame History

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 /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.

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

// 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:

  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.