Files
gott/Plans/[DONE] SCAN_GITHUB_EVENTS.md
T

216 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<string> _processedEventIds = new();
private readonly Queue<string> _eventIdQueue = new();
private const int MaxProcessedIds = 10_000;
```
### Phase 3: Commit-Diff-Scanning
```csharp
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
```csharp
// Program.cs
builder.Services.AddHostedService<GitHubEventScannerService>();
```
```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.