feat: 3 Scan-Modi (Quick/Web/Full) für Key Scanner
- 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
This commit is contained in:
1 parent
ed8e5af428
commit
558ded1ed5
5 files changed
+117
-25
No files matched your search
@@ -238,8 +238,12 @@ public static class ApiKeysEndpoint
|
|||||||
return ctx.Response.WriteAsJsonAsync(new { error = "Scan läuft bereits" });
|
return ctx.Response.WriteAsJsonAsync(new { error = "Scan läuft bereits" });
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner.TriggerScan();
|
var mode = ctx.Request.Query["mode"].FirstOrDefault() ?? "full";
|
||||||
return ctx.Response.WriteAsJsonAsync(new { ok = true, message = "Scan gestartet" });
|
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)
|
private static Task HandleScanStatus(HttpContext ctx, KeyScannerService scanner)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
private string _lastRun;
|
private string _lastRun;
|
||||||
private string _lastResult;
|
private string _lastResult;
|
||||||
private readonly ManualResetEventSlim _triggerNow = new(false);
|
private readonly ManualResetEventSlim _triggerNow = new(false);
|
||||||
|
private string _scanMode = "full"; // full, quick, web
|
||||||
|
|
||||||
public bool IsRunning { get { lock (_lock) return _isRunning; } }
|
public bool IsRunning { get { lock (_lock) return _isRunning; } }
|
||||||
public string LastRun { get { lock (_lock) return _lastRun; } }
|
public string LastRun { get { lock (_lock) return _lastRun; } }
|
||||||
@@ -131,37 +132,46 @@ public class KeyScannerService : BackgroundService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Wird vom Endpoint aufgerufen um einen sofortigen Scan auszulösen.</summary>
|
/// <summary>Wird vom Endpoint aufgerufen um einen sofortigen Scan auszulösen.</summary>
|
||||||
public void TriggerScan()
|
public void TriggerScan(string mode = "full")
|
||||||
{
|
{
|
||||||
|
lock (_lock) _scanMode = mode;
|
||||||
_triggerNow.Set();
|
_triggerNow.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunScan(CancellationToken ct)
|
private async Task RunScan(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
string mode;
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_isRunning) return;
|
if (_isRunning) return;
|
||||||
_isRunning = true;
|
_isRunning = true;
|
||||||
|
mode = _scanMode;
|
||||||
|
_scanMode = "full"; // Reset für nächsten Lauf
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("KeyScannerService: Scan gestartet");
|
var modeLabels = new Dictionary<string, string> { ["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 newKeysFound = 0;
|
||||||
var checkedCount = 0;
|
var checkedCount = 0;
|
||||||
var totalFound = 0;
|
var totalFound = 0;
|
||||||
|
|
||||||
try
|
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)>();
|
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")));
|
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);
|
var spKeys = await ScanSerper(ct);
|
||||||
allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper")));
|
allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper")));
|
||||||
@@ -207,7 +217,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_lastRun = DateTime.UtcNow.ToString("o");
|
_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);
|
_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" };
|
||||||
|
|
||||||
|
/// <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 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<List<(string Key, string Url)>> ScanGitHub(CancellationToken ct)
|
private async Task<List<(string Key, string Url)>> ScanGitHub(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-1
@@ -580,7 +580,9 @@
|
|||||||
<h2>Key Scanner</h2>
|
<h2>Key Scanner</h2>
|
||||||
<div style="display:flex;gap:.5rem;align-items:center">
|
<div style="display:flex;gap:.5rem;align-items:center">
|
||||||
<span id="scanStatus" class="admin-user-meta" style="font-size:.85em"></span>
|
<span id="scanStatus" class="admin-user-meta" style="font-size:.85em"></span>
|
||||||
<button id="scanTriggerBtn" class="section-add-btn"><i class="fa fa-search"></i> Jetzt scannen</button>
|
<button id="scanQuickBtn" class="section-add-btn"><i class="fa fa-bolt"></i> Quick</button>
|
||||||
|
<button id="scanWebBtn" class="section-add-btn"><i class="fa fa-globe"></i> Web</button>
|
||||||
|
<button id="scanFullBtn" class="section-add-btn"><i class="fa fa-search"></i> Full</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="scannedKeysList" class="admin-user-list"></div>
|
<div id="scannedKeysList" class="admin-user-list"></div>
|
||||||
|
|||||||
+1
-1
@@ -226,7 +226,7 @@ function apiCheckApiKey(id) { return _post('/api/admin/api-keys/' + id + '/check
|
|||||||
|
|
||||||
// ── Key Scanner API (Admin) ──
|
// ── 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 apiScanStatus() { return _get('/api/admin/api-keys/scan/status', {}); }
|
||||||
function apiGetScannedKeys() { return _get('/api/admin/api-keys/scanned', []); }
|
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'); }
|
function apiRecheckScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/recheck', undefined, 'Fehler beim Prüfen'); }
|
||||||
|
|||||||
@@ -318,7 +318,10 @@ registerPage('api-keys', {
|
|||||||
|
|
||||||
// ── Scanner UI ──
|
// ── 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 scanStatusEl = document.getElementById('scanStatus');
|
||||||
var scannedList = document.getElementById('scannedKeysList');
|
var scannedList = document.getElementById('scannedKeysList');
|
||||||
var scannedKeys = [];
|
var scannedKeys = [];
|
||||||
@@ -435,14 +438,29 @@ registerPage('api-keys', {
|
|||||||
} catch (err) { showToast(err.message, 'error'); }
|
} 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 = '<i class="fa ' + d.icon + '"></i> ' + d.label;
|
||||||
|
d.btn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function updateScanStatus() {
|
async function updateScanStatus() {
|
||||||
try {
|
try {
|
||||||
var s = await apiScanStatus();
|
var s = await apiScanStatus();
|
||||||
if (s.running) {
|
if (s.running) {
|
||||||
scanStatusEl.textContent = '⏳ Scan läuft…';
|
scanStatusEl.textContent = '⏳ Scan läuft…';
|
||||||
scanBtn.disabled = true;
|
setScanButtonsDisabled(true);
|
||||||
scanBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Läuft…';
|
|
||||||
// Polling starten wenn noch nicht aktiv
|
|
||||||
if (!pollTimer) {
|
if (!pollTimer) {
|
||||||
pollTimer = setInterval(async function() {
|
pollTimer = setInterval(async function() {
|
||||||
var st = await apiScanStatus();
|
var st = await apiScanStatus();
|
||||||
@@ -450,15 +468,13 @@ registerPage('api-keys', {
|
|||||||
if (!st.running) {
|
if (!st.running) {
|
||||||
clearInterval(pollTimer);
|
clearInterval(pollTimer);
|
||||||
pollTimer = null;
|
pollTimer = null;
|
||||||
scanBtn.disabled = false;
|
resetScanButtons();
|
||||||
scanBtn.innerHTML = '<i class="fa fa-search"></i> Jetzt scannen';
|
|
||||||
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
|
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scanBtn.disabled = false;
|
resetScanButtons();
|
||||||
scanBtn.innerHTML = '<i class="fa fa-search"></i> Jetzt scannen';
|
|
||||||
if (s.lastResult) {
|
if (s.lastResult) {
|
||||||
var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : '';
|
var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : '';
|
||||||
scanStatusEl.textContent = timeStr + s.lastResult;
|
scanStatusEl.textContent = timeStr + s.lastResult;
|
||||||
@@ -467,15 +483,22 @@ registerPage('api-keys', {
|
|||||||
} catch (err) { /* ignore */ }
|
} catch (err) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
scanBtn.addEventListener('click', async function() {
|
async function triggerScan(mode, btn) {
|
||||||
try {
|
try {
|
||||||
await apiTriggerScan();
|
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Läuft…';
|
||||||
showToast('Scan gestartet');
|
setScanButtonsDisabled(true);
|
||||||
|
await apiTriggerScan(mode);
|
||||||
|
showToast(mode.charAt(0).toUpperCase() + mode.slice(1) + '-Scan gestartet');
|
||||||
await updateScanStatus();
|
await updateScanStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
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 loadScannedKeys();
|
||||||
await updateScanStatus();
|
await updateScanStatus();
|
||||||
|
|||||||
Reference in new issue
Block a user