diff --git a/Plans/SCAN_GOOGLE_DORKS.md b/Plans/SCAN_GOOGLE_DORKS.md new file mode 100644 index 0000000..3aec651 --- /dev/null +++ b/Plans/SCAN_GOOGLE_DORKS.md @@ -0,0 +1,165 @@ +# Umsetzungsplan: Google Dorks Scanner + +## Zusammenfassung +Suche nach geleakten API-Keys über Google Programmable Search Engine (Custom Search JSON API). +Google-Dork-Queries wie `"sk-ant-api03"` finden Keys auf beliebigen Webseiten, Foren, Blogs und Dokumentationen. + +## Warum Google Dorks? +- Google indexiert Milliarden von Seiten – Pastebin, StackOverflow, Foren, Blogs, Docs +- Dork-Queries mit exakten Key-Prefixes sind extrem präzise (wenig False Positives) +- Ergänzt Serper: Serper liefert Snippets, Google CSE liefert **andere Ergebnisse** (unterschiedliche Indexierung/Ranking) +- Findet Keys auf Seiten die Serper möglicherweise nicht abdeckt + +## ⚠️ Einschränkung: Closed for New Customers +- **Die Custom Search JSON API ist für Neukunden gesperrt** (seit ~2026) +- Bestehende Kunden haben bis **1. Januar 2027** Zugriff +- Google empfiehlt als Alternative: Vertex AI Search (bis 50 Domains) oder kostenpflichtige Full-Web-Search +- **Konsequenz:** Nur umsetzbar wenn der User bereits einen CSE-Account hat oder eine Alternative nutzt +- Der Plan wird trotzdem erstellt, da bestehende Accounts weiterhin funktionieren + +## API-Analyse + +### Custom Search JSON API +``` +GET https://www.googleapis.com/customsearch/v1?key={API_KEY}&cx={SEARCH_ENGINE_ID}&q="sk-ant-api03" +``` + +- **100 Queries/Tag gratis** (danach $5 pro 1.000 Queries, max 10.000/Tag) +- Benötigt: Google Cloud API Key + Programmable Search Engine ID (cx) +- PSE muss auf "Search the entire web" konfiguriert sein +- Max 10 Ergebnisse pro Query (paginierbar mit `start` Parameter, max 100 Ergebnisse pro Query) +- Response: JSON mit `items[].link`, `items[].snippet`, `items[].title` + +### Suchstrategie +Mit 100 Queries/Tag und max 10 Ergebnissen/Query → **max 1.000 URLs pro Tag**. + +Optimale Verteilung der 100 Queries: +- Anthropic-Patterns: ~20 Queries (höchste Priorität) +- OpenAI-Patterns: ~20 Queries +- Andere Key-Typen: ~20 Queries +- Varianten/Ergänzungen: ~20 Queries +- Reserve: ~20 Queries + +### Dork-Queries (Beispiele) +``` +"sk-ant-oat01-" +"sk-ant-api03-" +"sk-proj-" "ANTHROPIC_API_KEY" +"ANTHROPIC_API_KEY" filetype:env +"sk-ant-" site:pastebin.com +"sk-ant-" site:gist.github.com +"sk-ant-" site:stackoverflow.com +"AKIA" "secret" filetype:env +"ghp_" site:pastebin.com +``` + +## Implementierungsplan + +### Phase 1: Config – Server Settings + +Zwei neue Settings in `ServerSettingsService.cs`: +```csharp +("GoogleCseApiKey", "text", "", "API", "Google CSE API Key", "API Key für Google Custom Search JSON API"), +("GoogleCseEngineId", "text", "", "API", "Google CSE Engine ID", "Search Engine ID (cx) der Programmable Search Engine"), +``` + +### Phase 2: Neue Scan-Methode `ScanGoogleCse` + +```csharp +private async Task> ScanGoogleCse(CancellationToken ct) +``` + +**Ablauf:** +1. Settings `GoogleCseApiKey` und `GoogleCseEngineId` laden +2. Für jeden Suchbegriff aus `KeyPatterns.ForCodeSearch`: + - Query bauen: `"sk-ant-oat01"` (exakte Phrase) + - `GET https://www.googleapis.com/customsearch/v1?key={key}&cx={cx}&q={query}&num=10` + - Response parsen: `items[].snippet` + `items[].link` + - Snippets mit `KeyPatterns.Compiled` Regex scannen + - Bei Treffer: Key + URL sammeln + - Optional: Seite herunterladen und vollständig scannen (bei Snippet-Treffern) +3. Query-Budget tracken (max 100/Tag) +4. Zwischen Requests: 1s Pause (kein offizielles Rate-Limit, aber Best Practice) + +### Phase 3: Integration in RunScan + +```csharp +// In RunScan(), nach Serper-Scan: +if (mode != "quick") +{ + var cseApiKey = _settings.Get("GoogleCseApiKey"); + var cseEngineId = _settings.Get("GoogleCseEngineId"); + if (!string.IsNullOrWhiteSpace(cseApiKey) && !string.IsNullOrWhiteSpace(cseEngineId)) + { + SetProgress("Google Custom Search", "Starte…"); + var cseKeys = await ScanGoogleCse(ct); + allFoundKeys.AddRange(cseKeys.Select(k => (k.Key, k.Url, "google_cse"))); + AddLog("scan", "Google CSE: " + cseKeys.Count + " Key(s) gefunden"); + } +} +``` + +- **Mode `quick`:** Kein CSE-Scan (Query-Budget schonen) +- **Mode `web`:** CSE-Scan aktiv (Web-Quelle) +- **Mode `full`:** CSE-Scan aktiv + +### Phase 4: Query-Budget-Management + +Da nur 100 Queries/Tag gratis sind, braucht es ein Budget-Tracking: + +```csharp +// Tägliches Query-Budget +private int _cseQueriesUsedToday; +private string _cseLastResetDate; + +private bool CanUseCseQuery() +{ + var today = DateTime.UtcNow.ToString("yyyy-MM-dd"); + if (_cseLastResetDate != today) + { + _cseQueriesUsedToday = 0; + _cseLastResetDate = today; + } + return _cseQueriesUsedToday < 100; +} +``` + +### Phase 5: Snippet-Scanning vs. Page-Download + +**Zwei Strategien:** + +1. **Snippet-Only (empfohlen):** Google-Snippets enthalten oft genug vom Key um ihn zu erkennen. Spart Requests und ist schnell. +2. **Snippet + Page-Download:** Wenn Snippet einen Partial-Match hat (Key-Prefix aber nicht vollständig), die Seite herunterladen und vollständig scannen. Kostet zusätzliche HTTP-Requests, findet aber mehr Keys. + +**Empfehlung:** Snippet-Only als Standard, Page-Download optional. + +## Response-Format + +```json +{ + "items": [ + { + "title": "Some Forum Post", + "link": "https://example.com/post/123", + "snippet": "...my API key is sk-ant-api03-abc123..." + } + ], + "searchInformation": { + "totalResults": "42" + } +} +``` + +## Aufwandsschätzung +- **Implementierung:** ~150-200 Zeilen Code +- **Komplexität:** Niedrig (einfache REST API, Standard JSON) +- **Erwarteter Ertrag:** Mittel (ergänzt Serper, andere Ergebnisse möglich) +- **Kosten:** Gratis bis 100 Queries/Tag, danach $5/1.000 + +## Risiken +- ⚠️ **API ist für Neukunden gesperrt** – nur mit bestehendem Account nutzbar +- ⚠️ **Deadline: 1. Januar 2027** – danach muss auf Alternative migriert werden +- 100 Queries/Tag sind begrenzt → intelligente Query-Auswahl nötig +- Snippets können abgeschnittene Keys enthalten → Page-Download als Fallback +- Überschneidung mit Serper möglich → Deduplication über `_db.ScannedKeyExists()` +- Google kann Keys in Snippets maskieren (unwahrscheinlich, aber möglich) diff --git a/Plans/SCAN_PASTEBIN.md b/Plans/SCAN_PASTEBIN.md new file mode 100644 index 0000000..1a60983 --- /dev/null +++ b/Plans/SCAN_PASTEBIN.md @@ -0,0 +1,187 @@ +# Umsetzungsplan: Pastebin Scraping Scanner + +## Zusammenfassung +Kontinuierliches Scraping neuer öffentlicher Pastebin-Pastes über die Scraping API. +Pastebin ist eine der häufigsten Quellen für geleakte API-Keys, Credentials und Secrets. + +## Warum Pastebin? +- **Klassische Dump-Plattform** für Secrets, Credentials, Config-Dateien +- User posten Keys zum Teilen, Debuggen oder versehentlich +- Automated Dumps von Credential-Stealern landen häufig auf Pastebin +- Kein Suchindex → man muss den Firehose-Stream der neuen Pastes scannen +- Ergänzt alle anderen Scanner die nur per Suchquery arbeiten + +## Voraussetzung: Pastebin PRO Account +- **Pastebin PRO** ($29 Lifetime) ist **zwingend erforderlich** +- IP-Adresse muss auf https://pastebin.com/doc_scraping_api gewhitelisted werden +- Nur 1 IP pro Account, kann aber beliebig oft geändert werden (Änderungen greifen innerhalb 1 Minute) +- Ohne Whitelist → API-Zugriff wird blockiert + +## API-Analyse + +### Scraping API – 3 Endpoints + +#### 1. Recent Pastes (Firehose) +``` +GET https://scrape.pastebin.com/api_scraping.php?limit=250 +``` +- Liefert die **neuesten öffentlichen** Pastes (max 250 pro Request) +- Response: JSON-Array mit `scrape_url`, `full_url`, `date`, `key`, `size`, `expire`, `title`, `syntax`, `user` +- Caching: ~2 Minuten Verzögerung +- **Kein Suchfilter** – man bekommt ALLE neuen Pastes und muss lokal filtern + +#### 2. Paste Content +``` +GET https://scrape.pastebin.com/api_scrape_item.php?i={paste_key} +``` +- Liefert den Rohtext eines Pastes +- Nur über die Scraping-API abrufbar (nicht /raw/) + +#### 3. Paste Metadata +``` +GET https://scrape.pastebin.com/api_scrape_item_meta.php?i={paste_key} +``` +- Liefert erweiterte Metadaten (Hits, Syntax, etc.) +- Für unseren Use Case nicht zwingend nötig + +### Rate Limits +- Kein hartes Rate Limit dokumentiert +- Empfehlung: Max 1 Request/Sekunde +- IP-basiert – nur gewhitelistete IP hat Zugriff +- Bei Missbrauch → IP-Block + +## Implementierungsplan + +### Phase 1: Config – Server Settings + +Neues Setting in `ServerSettingsService.cs`: +```csharp +("PastebinScrapeEnabled", "toggle", "false", "Scanner", "Pastebin Scraping", "Pastebin Scraping API aktivieren (erfordert PRO Account + IP Whitelist)"), +``` + +Kein API-Key nötig – die Authentifizierung läuft über IP-Whitelist. + +### Phase 2: Neue Scan-Methode `ScanPastebin` + +```csharp +private async Task> ScanPastebin(CancellationToken ct) +``` + +**Ablauf:** +1. Neueste Pastes abrufen: `GET api_scraping.php?limit=250` +2. Für jeden Paste: + - Prüfen ob `paste_key` bereits gescannt (lokales HashSet + DB-Check) + - Paste-Content abrufen: `GET api_scrape_item.php?i={key}` + - Content mit `KeyPatterns.Compiled` Regex scannen + - Bei Treffer: Key + `full_url` sammeln +3. Gescannte Paste-Keys merken (um Re-Scans zu vermeiden) +4. Zwischen Content-Requests: 1s Pause + +### Phase 3: Paste-Key Tracking (Deduplication) + +Da die Firehose dieselben Pastes mehrfach liefern kann, brauchen wir ein Tracking: + +**Option A: In-Memory HashSet** (einfach, geht bei Neustart verloren) +```csharp +private readonly HashSet _scannedPasteKeys = new(); +``` + +**Option B: DB-Tabelle** (persistent, verhindert Re-Scans nach Neustart) +```sql +CREATE TABLE IF NOT EXISTS pastebin_scanned ( + paste_key TEXT PRIMARY KEY, + scanned_at TEXT NOT NULL +); +``` + +**Empfehlung:** Option A (In-Memory) mit TTL – die Pastes laufen sowieso ab. +Das HashSet wird beim Scan-Start befüllt und nach 24h geleert. + +### Phase 4: Integration in RunScan + +```csharp +// In RunScan(), nach HuggingFace: +if (mode != "quick") +{ + var pastebinEnabled = _settings.GetBool("PastebinScrapeEnabled", false); + if (pastebinEnabled) + { + SetProgress("Pastebin Scraping", "Starte…"); + var pbKeys = await ScanPastebin(ct); + allFoundKeys.AddRange(pbKeys.Select(k => (k.Key, k.Url, "pastebin"))); + AddLog("scan", "Pastebin-Scan: " + pbKeys.Count + " Key(s) gefunden"); + } +} +``` + +- **Mode `quick`:** Kein Pastebin-Scan (zu viele Requests) +- **Mode `web`:** Pastebin-Scan aktiv (Web-Quelle) +- **Mode `full`:** Pastebin-Scan aktiv + +### Phase 5: Intelligentes Filtern + +Nicht jeder Paste muss heruntergeladen werden. Vor-Filterung über Metadaten: + +```csharp +// Relevante Syntax-Typen (höhere Wahrscheinlichkeit für Keys) +private static readonly HashSet RelevantSyntax = new(StringComparer.OrdinalIgnoreCase) +{ + "", "text", "python", "javascript", "bash", "json", "yaml", "ini", + "php", "ruby", "java", "csharp", "go", "rust", "typescript", + "powershell", "shell", "markdown", "xml", "toml", "properties" +}; + +// Relevante Titel-Keywords +private static readonly string[] TitleKeywords = +{ + "key", "api", "token", "secret", "credential", "config", "env", + "anthropic", "openai", "aws", "claude", "password", "leak" +}; +``` + +**Strategie:** +1. **Titel-Match:** Wenn Titel ein Keyword enthält → immer scannen +2. **Syntax-Match:** Wenn Syntax relevant (Code/Text) → scannen +3. **Größen-Filter:** Nur Pastes < 500 KB (keine riesigen Dumps) +4. **Syntax-Ausschluss:** Binär-Formate, Bilder, etc. → überspringen + +### Phase 6: Scan-Frequenz + +Da der tägliche Full-Scan nur einmal läuft, würden wir zwischen zwei Scans viele Pastes verpassen (Pastebin hat ~10.000+ neue Pastes/Tag). + +**Zwei Optionen:** + +**A. Einmaliger Snapshot pro Full-Scan (einfach)** +- Beim Full-Scan einmal 250 neueste Pastes abrufen + scannen +- Verpfasst die meisten Pastes, aber fängt zumindest aktuelle ab +- Empfohlen für den Anfang + +**B. Kontinuierlicher Hintergrund-Scanner (komplex)** +- Eigener BackgroundService der alle 2-3 Minuten pollt +- Fängt nahezu alle neuen Pastes ab +- Deutlich mehr Requests (~720/Tag × 250 = 180.000 Pastes/Tag) +- Kann später hinzugefügt werden + +**Empfehlung:** Option A für Phase 1, Option B als optionales Follow-up. + +## Scan-Volumina (geschätzt) + +| Szenario | Pastes gescannt | API Requests | Dauer | +|---|---|---|---| +| Einmal 250 Pastes | 250 | ~251 (1 Liste + 250 Content) | ~5 Min | +| Einmal 250, nur gefiltert | ~100 | ~101 | ~2 Min | +| Kontinuierlich (alle 3 Min) | ~180.000/Tag | ~720 Listen + ~180.000 Content | Dauerhaft | + +## Aufwandsschätzung +- **Implementierung:** ~200 Zeilen Code +- **Komplexität:** Niedrig-Mittel (einfache API, aber Dedup + Filterung) +- **Erwarteter Ertrag:** 🔥 Hoch (Pastebin = klassische Secret-Dump-Plattform) +- **Kosten:** $29 einmalig für Pastebin PRO + +## Risiken +- IP-Whitelist → funktioniert nur von einer festen IP (Server) +- Nur **öffentliche** Pastes sind sichtbar (unlisted/private nicht) +- Pastes können schnell gelöscht werden → zeitnahes Scraping wichtig +- Hohe Paste-Menge → viele Keys sind False Positives (Test-Keys, bereits revoked) +- Bei zu aggressivem Polling → IP-Block möglich +- Pastebin kann API-Bedingungen jederzeit ändern diff --git a/Services/GitHubEventScannerService.cs b/Services/GitHubEventScannerService.cs index c95e82f..dfaa6fb 100644 --- a/Services/GitHubEventScannerService.cs +++ b/Services/GitHubEventScannerService.cs @@ -66,7 +66,7 @@ public class GitHubEventScannerService : BackgroundService lock (_lock) return _logEntries.ToList(); } - private void AddLog(string type, string message, string details = null) + private void AddLog(string type, string message, string details = null, bool persist = true) { lock (_lock) { @@ -80,6 +80,8 @@ public class GitHubEventScannerService : BackgroundService _logEntries.Dequeue(); } + if (!persist) return; + // Persistieren in DB (fire-and-forget) _ = Task.Run(async () => { @@ -163,7 +165,7 @@ public class GitHubEventScannerService : BackgroundService var events = await FetchPublicEvents(client, ct); if (events.Count == 0) { - AddLog("poll", "0 PushEvents empfangen"); + AddLog("poll", "0 PushEvents empfangen", persist: false); return 0; } @@ -178,7 +180,7 @@ public class GitHubEventScannerService : BackgroundService if (pushEvents.Count == 0) { - AddLog("poll", events.Count + " Events, 0 neue PushEvents"); + AddLog("poll", events.Count + " Events, 0 neue PushEvents", persist: false); return events.Count; } @@ -187,7 +189,7 @@ public class GitHubEventScannerService : BackgroundService ? pushEvents.Take(MaxCommitsPerPoll).ToList() : pushEvents; - AddLog("poll", events.Count + " Events, " + pushEvents.Count + " neue PushEvents → " + toScan.Count + " scannen"); + AddLog("poll", events.Count + " Events, " + pushEvents.Count + " neue PushEvents → " + toScan.Count + " scannen", persist: false); // Head-Commit-Diffs scannen var newKeyIds = new List(); @@ -259,7 +261,7 @@ public class GitHubEventScannerService : BackgroundService } } - AddLog("poll", scannedCount + " Diffs gescannt, " + newKeyIds.Count + " neue Keys"); + AddLog("poll", scannedCount + " Diffs gescannt, " + newKeyIds.Count + " neue Keys", persist: newKeyIds.Count > 0); if (newKeyIds.Count > 0) {