using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Gott.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Gott.Services; public class KeyScannerService : BackgroundService { private readonly DatabaseService _db; private readonly IHttpClientFactory _httpFactory; private readonly ILogger _logger; private readonly ServerSettingsService _settings; private string _githubToken => _settings.Get("GitHubToken"); private string _serperApiKey => _settings.Get("SerperApiKey"); private int _intervalHours => _settings.GetInt("ScanIntervalHours", 0); // Status-Tracking private readonly object _lock = new(); private bool _isRunning; private string _lastRun; private string _lastResult; private readonly ManualResetEventSlim _triggerNow = new(false); private string _scanMode = "full"; // full, quick, web // Progress-Tracking private string _progressPhase = ""; private string _progressDetail = ""; private int _progressCurrent; private int _progressTotal; private int _progressKeysFound; public bool IsRunning { get { lock (_lock) return _isRunning; } } public string LastRun { get { lock (_lock) return _lastRun; } } public string LastResult { get { lock (_lock) return _lastResult; } } public (string Phase, string Detail, int Current, int Total, int KeysFound) Progress { get { lock (_lock) return (_progressPhase, _progressDetail, _progressCurrent, _progressTotal, _progressKeysFound); } } private void SetProgress(string phase, string detail = "", int current = 0, int total = 0) { lock (_lock) { _progressPhase = phase; _progressDetail = detail; _progressCurrent = current; _progressTotal = total; } } private void IncrementKeysFound() { lock (_lock) _progressKeysFound++; } private void AddLog(string type, string message, string details = null) { _logger.LogInformation("[CodeSearch] [{Type}] {Message}", type, message); _ = Task.Run(async () => { try { await _db.CreateScannerLog(type, message, "codesearch", details); } catch { /* DB-Fehler ignorieren */ } }); } // ── GitHub Search Config ── /// Gibt die aktiven Search-Patterns zurück, gefiltert nach ScannerKeyTypes-Setting. private (string Pattern, string SearchTerm, string Type)[] GetActiveSearchPatterns() { var allowed = new HashSet(_settings.GetStringArray("ScannerKeyTypes")); if (allowed.Count == 0) return Array.Empty<(string, string, string)>(); return KeyPatterns.ForCodeSearch.Where(p => allowed.Contains(p.Type)).ToArray(); } /// Prüft ob ein Key-Typ in den aktiven Scanner-Typen enthalten ist. private bool IsKeyTypeAllowed(string type) { var allowed = new HashSet(_settings.GetStringArray("ScannerKeyTypes")); return allowed.Contains(type); } private static readonly string[] SearchLanguages = { "Dotenv", "Text", "JavaScript", "Python", "TypeScript", "Dockerfile", "Markdown", "Jupyter Notebook", "Shell", "Java", "Go", "C++", "PHP" }; private static readonly string[] PathExtensions = { ".env", ".envrc", ".prod", ".secret", ".private", ".key", ".xml", ".json", ".properties", ".sql", ".txt", ".log", ".yml", ".yaml", ".toml", ".ini", ".config", ".conf", ".cfg", ".tmp", ".backup", ".bak", ".enc", }; private const int RateLimitDelayMs = 6_500; private const int MaxResultsPerSearch = 1000; private const int ResultsPerPage = 100; public KeyScannerService( DatabaseService db, IHttpClientFactory httpFactory, ServerSettingsService settings, ILogger logger) { _db = db; _httpFactory = httpFactory; _settings = settings; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var hasGitHub = !string.IsNullOrWhiteSpace(_githubToken); var hasSerper = !string.IsNullOrWhiteSpace(_serperApiKey); if (!hasGitHub && !hasSerper) { _logger.LogInformation("KeyScannerService deaktiviert (kein GitHubToken/SerperApiKey konfiguriert)"); // Warte trotzdem auf manuelles Trigger-Signal await WaitForTriggerOnly(stoppingToken); return; } if (_intervalHours <= 0) { _logger.LogInformation("KeyScannerService: Kein Intervall konfiguriert, nur manueller Trigger aktiv"); await WaitForTriggerOnly(stoppingToken); return; } _logger.LogInformation("KeyScannerService gestartet (Intervall: {Hours}h)", _intervalHours); while (!stoppingToken.IsCancellationRequested) { await RunScan(stoppingToken); // Warte auf Intervall oder manuelles Signal var waitResult = await WaitWithTrigger(TimeSpan.FromHours(_intervalHours), stoppingToken); if (waitResult) _logger.LogInformation("KeyScannerService: Manueller Scan angefordert"); } } /// Wartet nur auf manuelles Trigger-Signal (kein automatisches Intervall). private async Task WaitForTriggerOnly(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await WaitWithTrigger(Timeout.InfiniteTimeSpan, stoppingToken); if (!stoppingToken.IsCancellationRequested) await RunScan(stoppingToken); } } /// Wartet auf Timeout oder Trigger-Signal. Gibt true zurück wenn getriggert. private Task WaitWithTrigger(TimeSpan timeout, CancellationToken ct) { return Task.Run(() => { _triggerNow.Reset(); var waitHandles = new[] { _triggerNow.WaitHandle, ct.WaitHandle }; var idx = WaitHandle.WaitAny(waitHandles, timeout); return idx == 0; // true = triggered }, ct); } /// Wird vom Endpoint aufgerufen um einen sofortigen Scan auszulösen. public void TriggerScan(string mode = "full") { lock (_lock) _scanMode = mode; _triggerNow.Set(); } private async Task RunScan(CancellationToken ct) { string mode; lock (_lock) { if (_isRunning) return; _isRunning = true; mode = _scanMode; _scanMode = "full"; // Reset für nächsten Lauf } var modeLabels = new Dictionary { ["quick"] = "Quick", ["web"] = "Web", ["full"] = "Full" }; var modeLabel = modeLabels.ContainsKey(mode) ? modeLabels[mode] : "Full"; AddLog("scan", modeLabel + "-Scan gestartet"); lock (_lock) _progressKeysFound = 0; SetProgress("Starte " + modeLabel + "-Scan…"); var newKeysFound = 0; var totalFound = 0; try { // Phase 1: Quellen je nach Scan-Modus scannen var allFoundKeys = new List<(string Key, string Url, string Source)>(); // Quick: nur GitHub mit reduzierten Sprachen/Pfaden // Full: GitHub + Serper // Web: nur Serper if (mode != "web" && !string.IsNullOrWhiteSpace(_githubToken)) { SetProgress("GitHub Code Search", "Starte…"); var ghKeys = mode == "quick" ? await ScanGitHubQuick(ct) : await ScanGitHub(ct); allFoundKeys.AddRange(ghKeys.Select(k => (k.Key, k.Url, "github"))); AddLog("scan", "GitHub-Scan (" + modeLabel + "): " + ghKeys.Count + " Key(s) gefunden"); } if (mode != "quick" && !string.IsNullOrWhiteSpace(_serperApiKey)) { SetProgress("Web-Suche (Serper)", "Starte…"); var spKeys = await ScanSerper(ct); allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper"))); AddLog("scan", "Serper-Scan: " + spKeys.Count + " Key(s) gefunden"); } if (mode != "quick") { SetProgress("HuggingFace Hub", "Starte…"); var hfKeys = await ScanHuggingFace(ct); allFoundKeys.AddRange(hfKeys.Select(k => (k.Key, k.Url, "huggingface"))); AddLog("scan", "HuggingFace-Scan: " + hfKeys.Count + " Key(s) gefunden"); } totalFound = allFoundKeys.Count; // Phase 2: Neue Keys in DB speichern (dedupliziert) SetProgress("Speichere Keys", totalFound + " gefunden, prüfe Duplikate…", 0, totalFound); var seen = new HashSet(StringComparer.Ordinal); var saveIdx = 0; foreach (var (key, sourceUrl, source) in allFoundKeys) { saveIdx++; SetProgress("Speichere Keys", totalFound + " gefunden, prüfe Duplikate…", saveIdx, totalFound); if (!seen.Add(key)) continue; var keyType = DetectKeyType(key); if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key)) continue; var keyPrefix = key[..Math.Min(20, key.Length)] + "..."; var typeName = KeyPatterns.DisplayName(keyType); newKeysFound++; IncrementKeysFound(); await _db.CreateScannedKey(new ScannedKeyRecord { ScannedKeyId = Guid.NewGuid().ToString("N"), ApiKey = key, Source = source, SourceUrl = sourceUrl, Type = keyType, Status = "unchecked", Plan = "Unknown", ScannedAt = DateTime.UtcNow.ToString("o") }); var logMsg = $"🔑 Neuer {typeName} Key! Quelle: {source} · URL: {sourceUrl} · Prefix: {keyPrefix}"; var detailsJson = JsonSerializer.Serialize(new { key_type = keyType, key_type_name = typeName, key_prefix = keyPrefix, key_length = key.Length, source, source_url = sourceUrl, scan_mode = modeLabel }); AddLog("key_found", logMsg, detailsJson); } AddLog("scan", newKeysFound + " neue Key(s) gespeichert"); lock (_lock) { _lastRun = DateTime.UtcNow.ToString("o"); _lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu"; } SetProgress("Abgeschlossen", _lastResult); AddLog("scan", "Scan abgeschlossen – " + _lastResult); } catch (OperationCanceledException) { /* shutdown */ } catch (Exception ex) { AddLog("error", "Scan-Fehler: " + ex.Message); lock (_lock) { _lastRun = DateTime.UtcNow.ToString("o"); _lastResult = $"Fehler: {ex.Message}"; } } finally { lock (_lock) _isRunning = false; } } // ── GitHub Code Search ── private static readonly string[] QuickLanguages = { "Dotenv", "Text", "Markdown" }; private static readonly string[] QuickPathExtensions = { ".env" }; /// Quick-Scan: nur die wichtigsten Sprachen/Pfade durchsuchen. private async Task> ScanGitHubQuick(CancellationToken ct) { var allKeys = new Dictionary(StringComparer.Ordinal); var queries = new List<(string Query, Regex Regex)>(); var activePatterns = GetActiveSearchPatterns(); foreach (var (pattern, searchTerm, _) in activePatterns) { var regex = new Regex(pattern, RegexOptions.Compiled); foreach (var lang in QuickLanguages) { var langFilter = lang.Contains(' ') ? $"language:\"{lang}\"" : $"language:{lang}"; queries.Add(($"{searchTerm} {langFilter}", regex)); } foreach (var pathExt in QuickPathExtensions) queries.Add(($"{searchTerm} path:{pathExt}", regex)); } var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("GottKeyScanner", "1.0")); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _githubToken); client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.text-match+json")); for (int i = 0; i < queries.Count; i++) { ct.ThrowIfCancellationRequested(); var (query, regex) = queries[i]; SetProgress("GitHub Code Search (Quick)", query, i + 1, queries.Count); try { var results = await SearchCodeAsync(client, query, regex, ct); foreach (var (key, url) in results) if (!allKeys.ContainsKey(key)) allKeys[key] = url; } catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden) { SetProgress("GitHub Code Search (Quick)", "⏳ Rate-Limit, warte 60s…", i + 1, queries.Count); AddLog("rate_limit", "GitHub Rate-Limit (Quick), warte 60s"); await Task.Delay(60_000, ct); } catch (Exception ex) { AddLog("error", "GitHub-Suche fehlgeschlagen (Quick): " + ex.Message); } } return allKeys.Select(kv => (kv.Key, kv.Value)).ToList(); } private async Task> ScanGitHub(CancellationToken ct) { var allKeys = new Dictionary(StringComparer.Ordinal); // Key → erste URL var queries = BuildSearchQueries(); var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("GottKeyScanner", "1.0")); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _githubToken); client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.text-match+json")); for (int i = 0; i < queries.Count; i++) { ct.ThrowIfCancellationRequested(); var (query, regex) = queries[i]; SetProgress("GitHub Code Search", query, i + 1, queries.Count); try { var results = await SearchCodeAsync(client, query, regex, ct); foreach (var (key, url) in results) if (!allKeys.ContainsKey(key)) allKeys[key] = url; } catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden) { SetProgress("GitHub Code Search", "⏳ Rate-Limit, warte 60s…", i + 1, queries.Count); AddLog("rate_limit", "GitHub Rate-Limit erreicht, warte 60s"); await Task.Delay(60_000, ct); try { var results = await SearchCodeAsync(client, query, regex, ct); foreach (var (key, url) in results) if (!allKeys.ContainsKey(key)) allKeys[key] = url; } catch (Exception retryEx) { AddLog("error", "Retry fehlgeschlagen: " + retryEx.Message); } } catch (Exception ex) { AddLog("error", "GitHub-Suche fehlgeschlagen: " + ex.Message); } } return allKeys.Select(kv => (kv.Key, kv.Value)).ToList(); } private static async Task> SearchCodeAsync(HttpClient client, string query, Regex regex, CancellationToken ct) { var keys = new List<(string Key, string Url)>(); int page = 1; int maxPages = MaxResultsPerSearch / ResultsPerPage; while (page <= maxPages) { ct.ThrowIfCancellationRequested(); var url = $"https://api.github.com/search/code?q={Uri.EscapeDataString(query)}&per_page={ResultsPerPage}&page={page}"; await Task.Delay(RateLimitDelayMs, ct); var resp = await client.GetAsync(url, ct); if (resp.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) break; resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(ct); var doc = JsonDocument.Parse(json); var root = doc.RootElement; var totalCount = root.GetProperty("total_count").GetInt32(); if (!root.TryGetProperty("items", out var items)) break; foreach (var item in items.EnumerateArray()) { var htmlUrl = BuildHtmlUrl(item); if (item.TryGetProperty("text_matches", out var textMatches)) { foreach (var tm in textMatches.EnumerateArray()) { if (tm.TryGetProperty("fragment", out var frag)) { var matches = regex.Matches(frag.GetString() ?? ""); foreach (Match m in matches) keys.Add((m.Value, htmlUrl)); } } } // Kein text_match → Raw-File fetchen var hasMatches = item.TryGetProperty("text_matches", out var tm2) && tm2.EnumerateArray().Any(); if (!hasMatches) { var rawKeys = await TryFetchRawFileKeysAsync(client, item, regex, ct); foreach (var k in rawKeys) keys.Add((k, htmlUrl)); } } if (items.GetArrayLength() < ResultsPerPage || page * ResultsPerPage >= totalCount) break; page++; } return keys.GroupBy(k => k.Key).Select(g => g.First()).ToList(); } /// Baut die GitHub HTML-URL aus einem Search-API-Item. private static string BuildHtmlUrl(JsonElement item) { if (item.TryGetProperty("html_url", out var htmlUrl)) return htmlUrl.GetString() ?? ""; // Fallback: aus repository.full_name + path zusammenbauen if (item.TryGetProperty("repository", out var repo) && repo.TryGetProperty("full_name", out var fullName) && item.TryGetProperty("path", out var path)) return $"https://github.com/{fullName.GetString()}/blob/HEAD/{path.GetString()}"; return ""; } private static async Task> TryFetchRawFileKeysAsync(HttpClient client, JsonElement item, Regex regex, CancellationToken ct) { var keys = new List(); try { if (!item.TryGetProperty("repository", out var repo) || !repo.TryGetProperty("full_name", out var fullName) || !item.TryGetProperty("path", out var path)) return keys; var contentsUrl = $"https://api.github.com/repos/{fullName.GetString()}/contents/{path.GetString()}"; var request = new HttpRequestMessage(HttpMethod.Get, contentsUrl); request.Headers.Accept.Clear(); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.raw+json")); await Task.Delay(500, ct); var resp = await client.SendAsync(request, ct); if (!resp.IsSuccessStatusCode) return keys; var content = await resp.Content.ReadAsStringAsync(ct); var matches = regex.Matches(content); foreach (Match m in matches) keys.Add(m.Value); } catch { /* Datei nicht lesbar – ignorieren */ } return keys; } private List<(string Query, Regex Regex)> BuildSearchQueries() { var queries = new List<(string, Regex)>(); var activePatterns = GetActiveSearchPatterns(); foreach (var (pattern, searchTerm, _) in activePatterns) { var regex = new Regex(pattern, RegexOptions.Compiled); foreach (var lang in SearchLanguages) { var langFilter = lang.Contains(' ') ? $"language:\"{lang}\"" : $"language:{lang}"; queries.Add(($"{searchTerm} {langFilter}", regex)); } foreach (var pathExt in PathExtensions) { queries.Add(($"{searchTerm} path:{pathExt}", regex)); } } return queries; } // ── Serper.dev Google Search ── /// /// Durchsucht Google via Serper.dev nach geleakten API-Keys. /// Jeder KeyPattern-SearchTerm wird als exakte Phrase gesucht. /// Snippets werden nach dem vollständigen Key-Pattern geparst. /// private async Task> ScanSerper(CancellationToken ct) { var allKeys = new Dictionary(StringComparer.Ordinal); var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.Add("X-API-KEY", _serperApiKey); var activePatterns = GetActiveSearchPatterns(); for (int i = 0; i < activePatterns.Length; i++) { var (pattern, searchTerm, _) = activePatterns[i]; ct.ThrowIfCancellationRequested(); SetProgress("Web-Suche (Serper)", searchTerm, i + 1, activePatterns.Length); var regex = new Regex(pattern, RegexOptions.Compiled); try { // Serper erlaubt bis zu 100 Ergebnisse pro Request (num: 100) var payload = JsonSerializer.Serialize(new { q = $"\"{searchTerm}\"", num = 100 }); var request = new HttpRequestMessage(HttpMethod.Post, "https://google.serper.dev/search") { Content = new StringContent(payload, Encoding.UTF8, "application/json") }; var resp = await client.SendAsync(request, ct); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(ct); var doc = JsonDocument.Parse(json); // Organic results durchsuchen if (doc.RootElement.TryGetProperty("organic", out var organic)) { foreach (var item in organic.EnumerateArray()) { var link = item.TryGetProperty("link", out var l) ? l.GetString() ?? "" : ""; var snippet = item.TryGetProperty("snippet", out var s) ? s.GetString() ?? "" : ""; var title = item.TryGetProperty("title", out var t) ? t.GetString() ?? "" : ""; // Keys aus Snippet und Title extrahieren var text = snippet + " " + title; var matches = regex.Matches(text); foreach (Match m in matches) if (!allKeys.ContainsKey(m.Value)) allKeys[m.Value] = link; } } AddLog("scan", "Serper '" + searchTerm + "': " + allKeys.Count + " Ergebnis(se)"); await Task.Delay(1000, ct); // Rate-Limit schonen } catch (Exception ex) { AddLog("error", "Serper-Suche fehlgeschlagen für '" + searchTerm + "': " + ex.Message); } } return allKeys.Select(kv => (kv.Key, kv.Value)).ToList(); } // ── Hugging Face Hub Scanner ── private static readonly string[] HuggingFaceSearchTerms = { "anthropic", "claude", "openai", "api-key", "chatgpt", "gpt4" }; /// Zu scannende Dateien – priorisiert nach Wahrscheinlichkeit für Keys. private static readonly string[] HuggingFaceScanFiles = { ".env", ".env.local", ".env.production", ".env.example", "app.py", "main.py", "config.py", "settings.py", "utils.py", ".streamlit/secrets.toml", "config.yaml", "config.yml", "config.json", "docker-compose.yml", "Dockerfile" }; /// Datei-Extensions die bei der Dateiliste zusätzlich gescannt werden. private static readonly string[] HuggingFaceScanExtensions = { ".py", ".env", ".toml", ".yaml", ".yml", ".json", ".txt", ".cfg", ".ini", ".conf", ".ipynb" }; private const int HuggingFaceMaxFilesPerRepo = 50; private const int HuggingFaceMaxFileSizeBytes = 500_000; // 500 KB private const int HuggingFaceRequestDelayMs = 300; /// /// Scannt Hugging Face Spaces und Models nach geleakten API-Keys. /// Sucht nach relevanten Repos, lädt Dateien herunter und prüft mit allen Key-Patterns. /// private async Task> ScanHuggingFace(CancellationToken ct) { var allKeys = new Dictionary(StringComparer.Ordinal); var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("GottKeyScanner", "1.0")); var hfToken = _settings.Get("HuggingFaceToken"); if (!string.IsNullOrWhiteSpace(hfToken)) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", hfToken); var scannedRepos = new HashSet(StringComparer.OrdinalIgnoreCase); var totalQueries = HuggingFaceSearchTerms.Length * 2; // spaces + models var queryIdx = 0; // Phase 1: Spaces durchsuchen foreach (var term in HuggingFaceSearchTerms) { ct.ThrowIfCancellationRequested(); queryIdx++; SetProgress("HuggingFace Spaces", term, queryIdx, totalQueries); var repos = await HuggingFaceSearchRepos(client, "spaces", term, ct); foreach (var repoId in repos) { if (!scannedRepos.Add(repoId)) continue; var keys = await HuggingFaceScanRepo(client, "spaces", repoId, ct); foreach (var (key, url) in keys) if (!allKeys.ContainsKey(key)) allKeys[key] = url; } } // Phase 2: Models durchsuchen foreach (var term in HuggingFaceSearchTerms) { ct.ThrowIfCancellationRequested(); queryIdx++; SetProgress("HuggingFace Models", term, queryIdx, totalQueries); var repos = await HuggingFaceSearchRepos(client, "models", term, ct); foreach (var repoId in repos) { if (!scannedRepos.Add(repoId)) continue; var keys = await HuggingFaceScanRepo(client, "models", repoId, ct); foreach (var (key, url) in keys) if (!allKeys.ContainsKey(key)) allKeys[key] = url; } } AddLog("scan", $"HuggingFace: {scannedRepos.Count} Repos gescannt, {allKeys.Count} Key(s) gefunden"); return allKeys.Select(kv => (kv.Key, kv.Value)).ToList(); } /// Sucht Repos (Spaces oder Models) auf HuggingFace. private async Task> HuggingFaceSearchRepos(HttpClient client, string repoType, string searchTerm, CancellationToken ct) { var repos = new List(); try { await Task.Delay(HuggingFaceRequestDelayMs, ct); var url = $"https://huggingface.co/api/{repoType}?search={Uri.EscapeDataString(searchTerm)}&limit=100"; var resp = await client.GetAsync(url, ct); if (!resp.IsSuccessStatusCode) return repos; var json = await resp.Content.ReadAsStringAsync(ct); var doc = JsonDocument.Parse(json); foreach (var item in doc.RootElement.EnumerateArray()) { if (item.TryGetProperty("id", out var id)) repos.Add(id.GetString()); } } catch (Exception ex) { AddLog("error", $"HuggingFace-Suche fehlgeschlagen ({repoType}/{searchTerm}): {ex.Message}"); } return repos; } /// Scannt ein einzelnes HuggingFace-Repo nach API-Keys. private async Task> HuggingFaceScanRepo(HttpClient client, string repoType, string repoId, CancellationToken ct) { var keys = new List<(string Key, string Url)>(); try { // Dateiliste abrufen await Task.Delay(HuggingFaceRequestDelayMs, ct); var treeUrl = $"https://huggingface.co/api/{repoType}/{repoId}/tree/main?recursive=true"; var treeResp = await client.GetAsync(treeUrl, ct); if (!treeResp.IsSuccessStatusCode) return keys; var treeJson = await treeResp.Content.ReadAsStringAsync(ct); var treeDoc = JsonDocument.Parse(treeJson); // Relevante Dateien filtern var filesToScan = new List<(string Path, long Size)>(); foreach (var file in treeDoc.RootElement.EnumerateArray()) { if (!file.TryGetProperty("type", out var type) || type.GetString() != "file") continue; if (!file.TryGetProperty("path", out var path)) continue; var filePath = path.GetString(); var fileSize = file.TryGetProperty("size", out var sz) ? sz.GetInt64() : 0; if (fileSize > HuggingFaceMaxFileSizeBytes) continue; // Prüfe ob die Datei in der Prioritätsliste ist oder eine relevante Extension hat var isPriority = HuggingFaceScanFiles.Any(f => filePath.EndsWith(f, StringComparison.OrdinalIgnoreCase) || filePath.Equals(f, StringComparison.OrdinalIgnoreCase)); var hasRelevantExt = HuggingFaceScanExtensions.Any(ext => filePath.EndsWith(ext, StringComparison.OrdinalIgnoreCase)); if (isPriority || hasRelevantExt) filesToScan.Add((filePath, fileSize)); } // Max Dateien pro Repo begrenzen if (filesToScan.Count > HuggingFaceMaxFilesPerRepo) filesToScan = filesToScan.Take(HuggingFaceMaxFilesPerRepo).ToList(); // Dateien herunterladen und scannen // repoType "spaces" → URL-Prefix "spaces", "models" → kein Prefix (nur repoId) var urlPrefix = repoType == "spaces" ? "spaces" : repoType == "models" ? "" : repoType; foreach (var (filePath, _) in filesToScan) { ct.ThrowIfCancellationRequested(); try { await Task.Delay(HuggingFaceRequestDelayMs, ct); var rawUrl = urlPrefix == "" ? $"https://huggingface.co/{repoId}/resolve/main/{filePath}" : $"https://huggingface.co/{urlPrefix}/{repoId}/resolve/main/{filePath}"; var request = new HttpRequestMessage(HttpMethod.Get, rawUrl); request.Headers.Add("Accept", "text/plain"); var resp = await client.SendAsync(request, ct); if (!resp.IsSuccessStatusCode) continue; var content = await resp.Content.ReadAsStringAsync(ct); if (string.IsNullOrWhiteSpace(content)) continue; // Alle Key-Patterns prüfen foreach (var regex in KeyPatterns.Compiled) { var matches = regex.Matches(content); foreach (Match m in matches) { var sourceUrl = urlPrefix == "" ? $"https://huggingface.co/{repoId}/blob/main/{filePath}" : $"https://huggingface.co/{urlPrefix}/{repoId}/blob/main/{filePath}"; keys.Add((m.Value, sourceUrl)); } } } catch { /* Einzelne Datei fehlgeschlagen – ignorieren */ } } } catch (Exception ex) { AddLog("error", $"HuggingFace-Repo-Scan fehlgeschlagen ({repoId}): {ex.Message}"); } return keys; } // ── Key Check ── public async Task CheckAndUpdateKey(ScannedKeyRecord sk) { var type = KeyPatterns.DetectType(sk.ApiKey); var keyPrefix = sk.ApiKey[..Math.Min(20, sk.ApiKey.Length)] + "..."; var typeName = KeyPatterns.DisplayName(type); try { if (KeyPatterns.IsAnthropic(type)) { var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h); var checkMsg = $"🔍 {typeName} geprüft: {status} · Plan: {result.Plan} · Usage 7d: {result.Usage7d?.ToString("F2") ?? "n/a"} · Usage 5h: {result.Usage5h?.ToString("F2") ?? "n/a"} · {keyPrefix}"; AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, plan = result.Plan, usage_7d = result.Usage7d, usage_5h = result.Usage5h, source_url = sk.SourceUrl })); } else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy") { var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); var status = result.Status == "ok" ? "valid" : result.Status; await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null); var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}"; AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl })); } else if (type == "google_api") { var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); var status = result.Status == "ok" ? "valid" : result.Status; await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null); var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}"; AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl })); } else if (type == "stripe_live" || type == "stripe_test") { var result = await StripeKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); var status = result.Status == "ok" ? "valid" : result.Status; await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null); var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}"; AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, is_live = result.IsLiveKey, account_id = result.AccountId, account_name = result.AccountName, account_email = result.AccountEmail, country = result.AccountCountry, balance_available = result.BalanceAvailable, balance_pending = result.BalancePending, currency = result.Currency, charges_enabled = result.ChargesEnabled, payouts_enabled = result.PayoutsEnabled, source_url = sk.SourceUrl })); } else { // Andere Key-Typen: Typ korrekt setzen, kein Check möglich await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "unchecked", type, null, null); AddLog("key_checked", $"⏭️ {typeName} übersprungen (kein Checker) · {keyPrefix}"); } } catch (Exception ex) { AddLog("error", $"Key-Check fehlgeschlagen für {typeName} ({keyPrefix}): {ex.Message}"); await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "error", "Unknown", null, null); } } private static string DetectKeyType(string key) => KeyPatterns.DetectType(key); }