216 lines
7.9 KiB
Markdown
216 lines
7.9 KiB
Markdown
# 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.
|