Routine-Poll-Logs nicht in DB persistieren

- AddLog() neuer Parameter 'persist' (default true)
- 4 Routine-Poll-Logs nur noch im In-Memory-Log:
  - '0 PushEvents empfangen'
  - 'X Events, 0 neue PushEvents'
  - 'X Events, X neue PushEvents → X scannen'
  - 'X Diffs gescannt, 0 neue Keys'
- 'X Diffs gescannt, X neue Keys' wird bei Keys > 0 weiterhin persistiert
This commit is contained in:
Meleki committed 2026-03-15 10:13:14 +01:00
1 parent 30662caab7
commit fea605dfe5
3 files changed
+359 -5

No files matched your search

+165
View File
@@ -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<List<(string Key, string Url)>> 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)
+187
View File
@@ -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<List<(string Key, string Url)>> 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<string> _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<string> 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
+7 -5
View File
@@ -66,7 +66,7 @@ public class GitHubEventScannerService : BackgroundService
lock (_lock) return _logEntries.ToList(); 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) lock (_lock)
{ {
@@ -80,6 +80,8 @@ public class GitHubEventScannerService : BackgroundService
_logEntries.Dequeue(); _logEntries.Dequeue();
} }
if (!persist) return;
// Persistieren in DB (fire-and-forget) // Persistieren in DB (fire-and-forget)
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
@@ -163,7 +165,7 @@ public class GitHubEventScannerService : BackgroundService
var events = await FetchPublicEvents(client, ct); var events = await FetchPublicEvents(client, ct);
if (events.Count == 0) if (events.Count == 0)
{ {
AddLog("poll", "0 PushEvents empfangen"); AddLog("poll", "0 PushEvents empfangen", persist: false);
return 0; return 0;
} }
@@ -178,7 +180,7 @@ public class GitHubEventScannerService : BackgroundService
if (pushEvents.Count == 0) 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; return events.Count;
} }
@@ -187,7 +189,7 @@ public class GitHubEventScannerService : BackgroundService
? pushEvents.Take(MaxCommitsPerPoll).ToList() ? pushEvents.Take(MaxCommitsPerPoll).ToList()
: pushEvents; : 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 // Head-Commit-Diffs scannen
var newKeyIds = new List<string>(); var newKeyIds = new List<string>();
@@ -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) if (newKeyIds.Count > 0)
{ {