diff --git a/Services/KeyScannerService.cs b/Services/KeyScannerService.cs
index 9559780..3cc3330 100644
--- a/Services/KeyScannerService.cs
+++ b/Services/KeyScannerService.cs
@@ -220,6 +220,15 @@ public class KeyScannerService : BackgroundService
AddLog("scan", "GitHub-Scan (" + modeLabel + "): " + ghKeys.Count + " Key(s) gefunden");
}
+ // GitHub Issues & PR Comments (benötigt GitHub Token)
+ if (mode != "web" && !string.IsNullOrWhiteSpace(_githubToken))
+ {
+ SetProgress("GitHub Issues & Comments", "Starte…");
+ var issueKeys = await ScanGitHubIssues(mode == "quick", ct);
+ allFoundKeys.AddRange(issueKeys.Select(k => (k.Key, k.Url, "github_issues")));
+ AddLog("scan", "GitHub Issues (" + modeLabel + "): " + issueKeys.Count + " Key(s) gefunden");
+ }
+
if (mode != "quick" && !string.IsNullOrWhiteSpace(_serperApiKey))
{
SetProgress("Web-Suche (Serper)", "Starte…");
@@ -536,6 +545,162 @@ public class KeyScannerService : BackgroundService
// ── Serper.dev Google Search ──
+ // ── GitHub Issues & PR Comments Scanner ──
+
+ private const int IssueSearchDelayMs = 2500; // 30 req/min = 1 req/2s + buffer
+ private const int CommentFetchDelayMs = 1000; // 5000 req/h = locker
+
+ ///
+ /// Durchsucht GitHub Issues, PR Bodies und Kommentare nach geleakten API-Keys.
+ /// Quick-Mode: Nur Search API (Issue/PR body + comment search).
+ /// Full-Mode: Search API + Kommentare der Treffer abrufen und scannen.
+ ///
+ private async Task> ScanGitHubIssues(bool quickMode, CancellationToken ct)
+ {
+ var allKeys = new Dictionary(StringComparer.Ordinal);
+ 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");
+
+ var activePatterns = GetActiveSearchPatterns();
+ // Queries: jeder SearchTerm × (in:body, in:comments)
+ var searchScopes = quickMode
+ ? new[] { "in:body" } // Quick: nur Body
+ : new[] { "in:body", "in:comments" }; // Full: Body + Comments
+
+ var queries = new List<(string Query, Regex Regex)>();
+ foreach (var (pattern, searchTerm, _) in activePatterns)
+ {
+ var regex = new Regex(pattern, RegexOptions.Compiled);
+ foreach (var scope in searchScopes)
+ queries.Add(($"\"{searchTerm}\" {scope}", regex));
+ }
+
+ // Phase 1: Issue Search API
+ var issueUrls = new HashSet(StringComparer.OrdinalIgnoreCase); // Deduplizierung
+ var allRegexes = activePatterns.Select(p => new Regex(p.Pattern, RegexOptions.Compiled)).ToArray();
+ var issuesWithComments = new List<(string Owner, string Repo, int Number)>();
+
+ for (int i = 0; i < queries.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+ var (query, regex) = queries[i];
+ SetProgress("GitHub Issues Search", query, i + 1, queries.Count);
+
+ try
+ {
+ await Task.Delay(IssueSearchDelayMs, ct);
+ var url = $"https://api.github.com/search/issues?q={Uri.EscapeDataString(query)}&sort=created&order=desc&per_page=100";
+ var resp = await client.GetAsync(url, ct);
+
+ if (resp.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ SetProgress("GitHub Issues Search", "⏳ Rate-Limit, warte 60s…", i + 1, queries.Count);
+ AddLog("rate_limit", "GitHub Issues Rate-Limit, warte 60s");
+ await Task.Delay(60_000, ct);
+ continue;
+ }
+ if (resp.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) continue;
+ resp.EnsureSuccessStatusCode();
+
+ var json = await resp.Content.ReadAsStringAsync(ct);
+ var doc = JsonDocument.Parse(json);
+
+ if (!doc.RootElement.TryGetProperty("items", out var items)) continue;
+
+ foreach (var item in items.EnumerateArray())
+ {
+ var htmlUrl = item.TryGetProperty("html_url", out var hu) ? hu.GetString() ?? "" : "";
+ var body = item.TryGetProperty("body", out var b) ? b.GetString() ?? "" : "";
+
+ // Keys aus Issue/PR Body extrahieren
+ var matches = regex.Matches(body);
+ foreach (Match m in matches)
+ if (!allKeys.ContainsKey(m.Value)) allKeys[m.Value] = htmlUrl;
+
+ // Für Full-Mode: Issue merken um Kommentare abzurufen
+ if (!quickMode && !string.IsNullOrEmpty(htmlUrl) && issueUrls.Add(htmlUrl))
+ {
+ // Owner/Repo/Number aus URL parsen: https://github.com/{owner}/{repo}/issues/{number}
+ var parts = htmlUrl.Split('/');
+ if (parts.Length >= 7 && int.TryParse(parts[^1], out var number))
+ {
+ var owner = parts[^4];
+ var repo = parts[^3];
+ issuesWithComments.Add((owner, repo, number));
+ }
+ }
+ }
+ }
+ catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ AddLog("rate_limit", "GitHub Issues Rate-Limit: " + ex.Message);
+ await Task.Delay(60_000, ct);
+ }
+ catch (Exception ex)
+ {
+ AddLog("error", "GitHub Issues-Suche fehlgeschlagen: " + ex.Message);
+ }
+ }
+
+ // Phase 2 (nur Full-Mode): Kommentare der Treffer abrufen
+ if (!quickMode && issuesWithComments.Count > 0)
+ {
+ // Max 100 Issues abrufen (Rate-Limit schonen)
+ var issuesToFetch = issuesWithComments.Take(100).ToList();
+ for (int i = 0; i < issuesToFetch.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+ var (owner, repo, number) = issuesToFetch[i];
+ SetProgress("GitHub Issue Comments", $"{owner}/{repo}#{number}", i + 1, issuesToFetch.Count);
+
+ try
+ {
+ await Task.Delay(CommentFetchDelayMs, ct);
+ var commentsUrl = $"https://api.github.com/repos/{owner}/{repo}/issues/{number}/comments?per_page=100";
+ var resp = await client.GetAsync(commentsUrl, ct);
+
+ if (resp.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ AddLog("rate_limit", "GitHub Comments Rate-Limit, warte 60s");
+ await Task.Delay(60_000, ct);
+ continue;
+ }
+ if (!resp.IsSuccessStatusCode) continue;
+
+ var json = await resp.Content.ReadAsStringAsync(ct);
+ var doc = JsonDocument.Parse(json);
+
+ foreach (var comment in doc.RootElement.EnumerateArray())
+ {
+ var commentBody = comment.TryGetProperty("body", out var cb) ? cb.GetString() ?? "" : "";
+ var commentUrl = comment.TryGetProperty("html_url", out var cu) ? cu.GetString() ?? "" : "";
+ if (string.IsNullOrEmpty(commentUrl))
+ commentUrl = $"https://github.com/{owner}/{repo}/issues/{number}";
+
+ foreach (var regex in allRegexes)
+ {
+ var matches = regex.Matches(commentBody);
+ foreach (Match m in matches)
+ if (!allKeys.ContainsKey(m.Value)) allKeys[m.Value] = commentUrl;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ AddLog("error", $"GitHub Comments fehlgeschlagen ({owner}/{repo}#{number}): {ex.Message}");
+ }
+ }
+ }
+
+ AddLog("scan", $"GitHub Issues: {issueUrls.Count} Issues durchsucht, {allKeys.Count} Key(s) gefunden");
+ return allKeys.Select(kv => (kv.Key, kv.Value)).ToList();
+ }
+
+ // ── Serper.dev Google Search ──
+
///
/// Durchsucht Google via Serper.dev nach geleakten API-Keys.
/// Jeder KeyPattern-SearchTerm wird als exakte Phrase gesucht.