From 558ded1ed52ada927adbaa4d0e60f9c17c14e6ae Mon Sep 17 00:00:00 2001 From: Meleki Date: Fri, 13 Mar 2026 09:33:18 +0100 Subject: [PATCH] =?UTF-8?q?feat:=203=20Scan-Modi=20(Quick/Web/Full)=20f?= =?UTF-8?q?=C3=BCr=20Key=20Scanner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Quick-Scan: nur GitHub mit Dotenv, Text, Markdown + .env - Web-Scan: nur Serper.dev (Google Dorks) - Full-Scan: GitHub + Serper komplett (wie bisher) - Backend: TriggerScan(mode), ScanGitHubQuick(), mode-basierte Quellsteuerung - Endpoint: ?mode=quick|web|full Query-Parameter - Frontend: 3 Buttons (⚡ Quick, 🌐 Web, 🔍 Full) mit gemeinsamer Scan-Logik - Status-Anzeige zeigt Scan-Modus im Ergebnis --- Endpoints/ApiKeysEndpoint.cs | 8 ++- Services/KeyScannerService.cs | 81 +++++++++++++++++++++++++++---- wwwroot/index.html | 4 +- wwwroot/js/api.js | 2 +- wwwroot/js/pages/api-keys-page.js | 47 +++++++++++++----- 5 files changed, 117 insertions(+), 25 deletions(-) diff --git a/Endpoints/ApiKeysEndpoint.cs b/Endpoints/ApiKeysEndpoint.cs index 66ffbbc..c935d9c 100644 --- a/Endpoints/ApiKeysEndpoint.cs +++ b/Endpoints/ApiKeysEndpoint.cs @@ -238,8 +238,12 @@ public static class ApiKeysEndpoint return ctx.Response.WriteAsJsonAsync(new { error = "Scan läuft bereits" }); } - scanner.TriggerScan(); - return ctx.Response.WriteAsJsonAsync(new { ok = true, message = "Scan gestartet" }); + var mode = ctx.Request.Query["mode"].FirstOrDefault() ?? "full"; + var validModes = new[] { "quick", "web", "full" }; + if (!validModes.Contains(mode)) mode = "full"; + + scanner.TriggerScan(mode); + return ctx.Response.WriteAsJsonAsync(new { ok = true, message = $"{mode}-Scan gestartet" }); } private static Task HandleScanStatus(HttpContext ctx, KeyScannerService scanner) diff --git a/Services/KeyScannerService.cs b/Services/KeyScannerService.cs index a196807..4b798d2 100644 --- a/Services/KeyScannerService.cs +++ b/Services/KeyScannerService.cs @@ -30,6 +30,7 @@ public class KeyScannerService : BackgroundService private string _lastRun; private string _lastResult; private readonly ManualResetEventSlim _triggerNow = new(false); + private string _scanMode = "full"; // full, quick, web public bool IsRunning { get { lock (_lock) return _isRunning; } } public string LastRun { get { lock (_lock) return _lastRun; } } @@ -131,37 +132,46 @@ public class KeyScannerService : BackgroundService } /// Wird vom Endpoint aufgerufen um einen sofortigen Scan auszulösen. - public void TriggerScan() + 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 } - _logger.LogInformation("KeyScannerService: Scan gestartet"); + var modeLabels = new Dictionary { ["quick"] = "Quick", ["web"] = "Web", ["full"] = "Full" }; + var modeLabel = modeLabels.ContainsKey(mode) ? modeLabels[mode] : "Full"; + _logger.LogInformation("KeyScannerService: {Mode}-Scan gestartet", modeLabel); var newKeysFound = 0; var checkedCount = 0; var totalFound = 0; try { - // Phase 1: Alle konfigurierten Quellen scannen + // Phase 1: Quellen je nach Scan-Modus scannen var allFoundKeys = new List<(string Key, string Url, string Source)>(); - if (!string.IsNullOrWhiteSpace(_githubToken)) + // Quick: nur GitHub mit reduzierten Sprachen/Pfaden + // Full: GitHub + Serper + // Web: nur Serper + if (mode != "web" && !string.IsNullOrWhiteSpace(_githubToken)) { - var ghKeys = await ScanGitHub(ct); + var ghKeys = mode == "quick" ? await ScanGitHubQuick(ct) : await ScanGitHub(ct); allFoundKeys.AddRange(ghKeys.Select(k => (k.Key, k.Url, "github"))); - _logger.LogInformation("GitHub-Scan: {Count} Key(s) gefunden", ghKeys.Count); + _logger.LogInformation("GitHub-Scan ({Mode}): {Count} Key(s) gefunden", modeLabel, ghKeys.Count); } - if (!string.IsNullOrWhiteSpace(_serperApiKey)) + if (mode != "quick" && !string.IsNullOrWhiteSpace(_serperApiKey)) { var spKeys = await ScanSerper(ct); allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper"))); @@ -207,7 +217,7 @@ public class KeyScannerService : BackgroundService lock (_lock) { _lastRun = DateTime.UtcNow.ToString("o"); - _lastResult = $"{totalFound} gefunden, {newKeysFound} neu, {checkedCount} geprüft"; + _lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu, {checkedCount} geprüft"; } _logger.LogInformation("KeyScannerService: Scan abgeschlossen – {Result}", _lastResult); @@ -228,7 +238,60 @@ public class KeyScannerService : BackgroundService } } - // ── GitHub Code Search (portiert aus ExampleScannerProject) ── + // ── 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)>(); + + foreach (var (pattern, searchTerm) in KeyPatterns) + { + 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]; + + 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) + { + _logger.LogWarning("GitHub Rate-Limit (Quick), warte 60s ..."); + await Task.Delay(60_000, ct); + } + catch (Exception ex) + { + _logger.LogWarning("GitHub-Suche fehlgeschlagen (Quick): {Msg}", ex.Message); + } + } + + return allKeys.Select(kv => (kv.Key, kv.Value)).ToList(); + } private async Task> ScanGitHub(CancellationToken ct) { diff --git a/wwwroot/index.html b/wwwroot/index.html index b0029ad..1396bc0 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -580,7 +580,9 @@

Key Scanner

- + + +
diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 4216ebf..900569d 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -226,7 +226,7 @@ function apiCheckApiKey(id) { return _post('/api/admin/api-keys/' + id + '/check // ── Key Scanner API (Admin) ── -function apiTriggerScan() { return _post('/api/admin/api-keys/scan', undefined, 'Fehler beim Starten des Scans'); } +function apiTriggerScan(mode) { return _post('/api/admin/api-keys/scan?mode=' + (mode || 'full'), undefined, 'Fehler beim Starten des Scans'); } function apiScanStatus() { return _get('/api/admin/api-keys/scan/status', {}); } function apiGetScannedKeys() { return _get('/api/admin/api-keys/scanned', []); } function apiRecheckScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/recheck', undefined, 'Fehler beim Prüfen'); } diff --git a/wwwroot/js/pages/api-keys-page.js b/wwwroot/js/pages/api-keys-page.js index ffd1610..597b6c4 100644 --- a/wwwroot/js/pages/api-keys-page.js +++ b/wwwroot/js/pages/api-keys-page.js @@ -318,7 +318,10 @@ registerPage('api-keys', { // ── Scanner UI ── - var scanBtn = document.getElementById('scanTriggerBtn'); + var scanQuickBtn = document.getElementById('scanQuickBtn'); + var scanWebBtn = document.getElementById('scanWebBtn'); + var scanFullBtn = document.getElementById('scanFullBtn'); + var scanBtns = [scanQuickBtn, scanWebBtn, scanFullBtn]; var scanStatusEl = document.getElementById('scanStatus'); var scannedList = document.getElementById('scannedKeysList'); var scannedKeys = []; @@ -435,14 +438,29 @@ registerPage('api-keys', { } catch (err) { showToast(err.message, 'error'); } } + var scanBtnDefaults = [ + { btn: scanQuickBtn, icon: 'fa-bolt', label: 'Quick' }, + { btn: scanWebBtn, icon: 'fa-globe', label: 'Web' }, + { btn: scanFullBtn, icon: 'fa-search', label: 'Full' } + ]; + + function setScanButtonsDisabled(disabled) { + scanBtnDefaults.forEach(function(d) { d.btn.disabled = disabled; }); + } + + function resetScanButtons() { + scanBtnDefaults.forEach(function(d) { + d.btn.innerHTML = ' ' + d.label; + d.btn.disabled = false; + }); + } + async function updateScanStatus() { try { var s = await apiScanStatus(); if (s.running) { scanStatusEl.textContent = '⏳ Scan läuft…'; - scanBtn.disabled = true; - scanBtn.innerHTML = ' Läuft…'; - // Polling starten wenn noch nicht aktiv + setScanButtonsDisabled(true); if (!pollTimer) { pollTimer = setInterval(async function() { var st = await apiScanStatus(); @@ -450,15 +468,13 @@ registerPage('api-keys', { if (!st.running) { clearInterval(pollTimer); pollTimer = null; - scanBtn.disabled = false; - scanBtn.innerHTML = ' Jetzt scannen'; + resetScanButtons(); scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : ''; } }, 3000); } } else { - scanBtn.disabled = false; - scanBtn.innerHTML = ' Jetzt scannen'; + resetScanButtons(); if (s.lastResult) { var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : ''; scanStatusEl.textContent = timeStr + s.lastResult; @@ -467,15 +483,22 @@ registerPage('api-keys', { } catch (err) { /* ignore */ } } - scanBtn.addEventListener('click', async function() { + async function triggerScan(mode, btn) { try { - await apiTriggerScan(); - showToast('Scan gestartet'); + btn.innerHTML = ' Läuft…'; + setScanButtonsDisabled(true); + await apiTriggerScan(mode); + showToast(mode.charAt(0).toUpperCase() + mode.slice(1) + '-Scan gestartet'); await updateScanStatus(); } catch (err) { showToast(err.message, 'error'); + resetScanButtons(); } - }); + } + + scanQuickBtn.addEventListener('click', function() { triggerScan('quick', scanQuickBtn); }); + scanWebBtn.addEventListener('click', function() { triggerScan('web', scanWebBtn); }); + scanFullBtn.addEventListener('click', function() { triggerScan('full', scanFullBtn); }); await loadScannedKeys(); await updateScanStatus();