chore: erledigte Pläne ins Repo aufnehmen (Plans/ aus .gitignore entfernt)

This commit is contained in:
Meleki committed 2026-03-15 09:55:44 +01:00
1 parent 8e94975e61
commit 30662caab7
6 files changed
+767 -3

No files matched your search

-3
View File
@@ -5,6 +5,3 @@ data/
Temp/
appsettings.Production.json
DEVY.md
/ExampleScannerProject/
/Plans/
+55
View File
@@ -0,0 +1,55 @@
# Plan: API Key pro Agent
## Übersicht
Statt einem globalen API-Key pro User wird der Key **pro Agent** zugewiesen.
Kein Key = kein Chat. Saubere Trennung.
## Datenbank
### Neue Tabelle `agent_api_keys`
```sql
CREATE TABLE agent_api_keys (
user_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
key_id TEXT NOT NULL,
UNIQUE(user_id, agent_id)
);
```
### Neue DB-Methoden
- `SetAgentApiKey(string userId, string agentId, string keyId)`
- `GetAgentApiKey(string userId, string agentId)` → key_id oder null
- `GetAgentApiKeyRecord(string userId, string agentId)` → vollständiges ApiKey-Objekt
### Migration
- Bestehende `users.selected_api_key_id` Werte → in `agent_api_keys` für alle Agents des Users übernehmen (einmalig)
- `users.selected_api_key_id` bleibt vorerst in der Tabelle (kein DROP), wird aber nicht mehr aktiv genutzt
## Backend (Endpoints)
### Angepasste Endpoints
- **`PUT /api/api-keys/select/{keyId}`** → neuer Query-Parameter `?agentId=xxx`
- **`GET /api/api-keys/selected`** → neuer Query-Parameter `?agentId=xxx`
### Chat/Services
- **`ChatEndpoint`** → `GetAgentApiKeyRecord(userId, agentId)` statt `GetUserSelectedApiKey(userId)`
- **`TelegramService`** → `GetAgentApiKeyRecord(userId, agentId)` statt `GetUserSelectedApiKey(userId)`
- **`CronjobService`** → `GetAgentApiKeyRecord(userId, agentId)` statt `GetUserSelectedApiKey(userId)`
### Fehler-Handling
- Kein Key für Agent zugewiesen → Fehlermeldung: "Kein API Key für diesen Agent zugewiesen"
- Kein Fallback auf globalen Key
## Frontend
### Key-Switch-Modal
- Bekommt `agentId` als Parameter (aus aktuellem Chat/Session)
- Select-Call sendet `agentId` mit
- Zeigt aktuell zugewiesenen Key für diesen Agent
### Usage-Badge
- Zeigt den Key des aktuellen Agents (nicht den globalen)
### API-Calls
- `selectApiKey(keyId, agentId)` → `PUT /api/api-keys/select/{keyId}?agentId={agentId}`
- `getSelectedApiKey(agentId)` → `GET /api/api-keys/selected?agentId={agentId}`
+253
View File
@@ -0,0 +1,253 @@
# Plan: OpenAI als nativer Provider
## Übersicht
OpenAI (GPT-4o, GPT-5, o3, etc.) als vollwertigen Provider integrieren – gleichwertig mit Anthropic.
Der Key kommt aus der `api_keys`-Tabelle (Provider: `openai`), nicht aus `appsettings.json`.
## Ist-Zustand
### Architektur
- **AnthropicService** – Haupt-Provider, voller Feature-Support (Tools, Review, Preparer, Finalizer, Web-Search, Memory, ask_user_choice, Plan-Mode)
- **OpenAiService** – "Neben-Provider" für Nvidia, Pollinations, Gemini. Limitiert:
- Nur 6 Tools (list_files, read_file, write_file, edit_file, delete_file, execute)
- Kein Review/Preparer/Finalizer
- Kein Memory, Web-Search, ask_user_choice
- API-Key hardcoded im `Providers`-Dictionary (nicht aus DB)
- `maxIterations` fest auf 10
### API-Key-System
- User wählt einen Key aus der `api_keys`-Tabelle (`selected_api_key_id`)
- Aktuell nur für Anthropic genutzt (`userApiKey.ApiKey` → `anthropic.StreamChat()`)
- OpenAi-Provider nutzen eigene Keys aus `Providers`-Dict (Gemini aus `appsettings.json`, Nvidia/Pollinations keyless)
### Modell-Auswahl
- Agent hat `Model`-Feld, z.B. `sonnet-4-6` oder `nvidia:model-id`
- `ChatService.ResolveModelId()` mapped Anthropic-Kurzformen, OpenAI-Provider werden durchgereicht
- Frontend: `<select>` mit Anthropic-Optionen + Optgroups für Nvidia/Pollinations/Gemini
## Soll-Zustand
### Ziel
Ein Agent mit Model `openai:gpt-4o` nutzt den aktiven OpenAI-Key des Users aus der `api_keys`-Tabelle.
Voller Tool-Support, alle 17 Tools.
### Architektur-Entscheidung
**Option A: OpenAI als neuen Provider in OpenAiService einbauen**
→ Einfach, aber OpenAiService hat nur 6 Tools und kein Review/Preparer/Finalizer.
**Option B: OpenAI direkt über OpenAiService mit vollem Tool-Support**
→ Alle 17 Tools + Memory + Review etc. erfordert massiven Umbau des OpenAiService.
**Option C: OpenAI über AnthropicService (Anthropic-API-kompatibel)**
→ Nicht möglich, OpenAI hat ein anderes API-Format.
**→ Empfehlung: Option A (erweitert)**
OpenAI als Provider in `OpenAiService` mit ALLEN Tools (nicht nur 6).
Review/Preparer/Finalizer erstmal nicht für OpenAI – das kann später kommen.
## Umsetzungsplan
### Phase 1: Backend – OpenAI Provider Registrierung
**Datei: `Services/OpenAiService.cs`**
1. `"openai"` zu `ProviderKeys` hinzufügen:
```csharp
public static readonly HashSet<string> ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai" };
```
2. OpenAI Provider-Config NICHT hardcoden, sondern dynamisch aus DB laden.
Problem: `Providers`-Dict wird im Konstruktor befüllt, aber der API-Key
kommt zur Laufzeit aus `api_keys` (user-spezifisch).
**Lösung:** `StreamChat()` bekommt optionalen `apiKey`-Parameter.
Wenn Provider = `openai` → BaseUrl `https://api.openai.com/v1`, Key aus Parameter.
```csharp
// Im Konstruktor:
["openai"] = ("https://api.openai.com/v1", "") // Key kommt zur Laufzeit
// In StreamChat():
public async Task StreamChat(..., string apiKey = null, ...)
{
var providerConfig = Providers[provider];
var effectiveKey = !string.IsNullOrEmpty(apiKey) ? apiKey : providerConfig.ApiKey;
var client = CreateClient(effectiveKey);
}
```
3. Tools erweitern – ALLE 17 Tools statt nur 6.
Problem: Aktuell sind die Tools als statische anonyme Objekte definiert (`OpenAiTools`).
Besser: Tools dynamisch aus `ToolService.GetToolDefinitions()` konvertieren.
```csharp
// Neue Methode: Anthropic-Tool-Defs → OpenAI-Format konvertieren
private static List<object> ConvertToolsToOpenAiFormat(object[] anthropicTools)
{
// Anthropic: { name, description, input_schema: { type, properties, required } }
// OpenAI: { type: "function", function: { name, description, parameters: { type, properties, required } } }
}
```
4. `maxIterations` aus ChatContext übernehmen statt fest 10.
### Phase 2: Backend – API-Key Routing
**Datei: `Endpoints/ChatEndpoint.cs`**
Aktuell:
```csharp
if (!string.IsNullOrEmpty(ctx.Provider))
await openAi.StreamChat(...); // kein API-Key
else
await anthropic.StreamChat(..., apiToken: userApiKey.ApiKey);
```
Neu: Wenn Provider = `openai` → Key aus `api_keys`-Tabelle durchreichen:
```csharp
if (!string.IsNullOrEmpty(ctx.Provider))
{
// Bei OpenAI: User-API-Key aus DB verwenden
var providerApiKey = ctx.Provider == "openai" ? userApiKey?.ApiKey : null;
await openAi.StreamChat(..., apiKey: providerApiKey, maxIterations: ctx.MaxIterations, ...);
}
```
**Problem:** Der User hat aktuell EINEN `selected_api_key_id`. Wenn er Anthropic nutzt, ist
das ein Anthropic-Key. Wenn er OpenAI nutzt, braucht er einen OpenAI-Key.
**Lösung:** `GetUserSelectedApiKey` um Provider-Filterung erweitern:
- ODER: Automatisch den passenden Key basierend auf Agent-Model wählen.
- ODER: Pro User mehrere Keys (einen pro Provider) erlauben.
**→ Empfehlung:** Automatische Key-Auswahl basierend auf Provider.
Neuer DB-Call: `GetActiveApiKeyForProvider(userId, provider)` – gibt den ersten
aktiven Key des Users für diesen Provider zurück.
```sql
SELECT k.* FROM api_keys k
WHERE k.provider = @provider
AND k.is_active = 1
AND (k.is_public = 1 OR k.key_id = (SELECT selected_api_key_id FROM users WHERE user_id = @userId))
ORDER BY k.is_public ASC -- User-eigener Key bevorzugt
LIMIT 1
```
Noch einfacher: Der User wählt weiterhin EINEN Key, aber das System erkennt
den Provider anhand des Keys und routet automatisch.
→ **User braucht einen OpenAI-Key in der `api_keys`-Tabelle.**
**Einfachste Lösung:**
```csharp
// In ChatEndpoint:
ApiKeyRecord resolvedKey;
if (ctx.Provider == "openai")
resolvedKey = await db.GetFirstActiveKeyByProvider("openai");
else
resolvedKey = userApiKey; // Anthropic wie bisher
if (resolvedKey == null) { /* Fehler: Kein Key für Provider */ }
```
### Phase 3: Backend – Modell-Liste
**Datei: `Services/OpenAiService.cs`**
OpenAI-Modelle dynamisch laden:
```csharp
private List<ProviderModel> _openaiModels = new();
// In RefreshModelsFromApis():
var openAiKey = await _db.GetFirstActiveKeyByProvider("openai");
if (openAiKey != null)
_openaiModels = await FetchModels("https://api.openai.com/v1/models", "OpenAI", openAiKey.ApiKey);
```
Problem: `RefreshModelsFromApis()` hat keinen Zugriff auf user-spezifische Keys.
→ Den ersten aktiven OpenAI-Key aus der DB nehmen (egal welcher User).
**Datei: `Endpoints/ModelsEndpoint.cs`**
Neuen Endpoint:
```csharp
app.MapGet("/api/models/openai", HandleOpenAiModels);
```
### Phase 4: Frontend – Modell-Auswahl
**Datei: `wwwroot/index.html`**
Neue Optgroup im Agent-Model-Select:
```html
<optgroup label="OpenAI" id="openaiOptgroup">
<option value="" disabled>Wird geladen...</option>
</optgroup>
```
**Datei: `wwwroot/js/pages/agent-page.js`**
OpenAI-Modelle laden:
```javascript
loadProviderModels('/api/models/openai', 'openai', openaiOptgroup)
```
### Phase 5: Frontend – Key-Switch-Modal
**Datei: `wwwroot/js/pages/chat-page.js`**
Im Key-Switch-Modal den Provider des Keys anzeigen:
- Anthropic-Keys: `🟤 Anthropic · Pro · ✅`
- OpenAI-Keys: `🟢 OpenAI · Tier 5 · ✅`
Aktuell zeigt das Modal alle Keys unabhängig vom Provider.
→ Keys nach Provider gruppieren oder filtern (nur passende Keys zeigen).
### Phase 6: Usage-Tracking (optional)
**Datei: `Services/OpenAiService.cs`**
Nach jedem OpenAI-API-Call die Usage aus dem Response extrahieren
und in `api_keys` updaten (analog zu `AnthropicService.TryUpdateKeyMeta()`).
OpenAI Response-Header enthalten keine Usage-Infos wie Anthropic.
Aber der letzte SSE-Chunk enthält `usage: { prompt_tokens, completion_tokens }`.
→ Nach dem Stream die Token-Counts aus dem letzten Chunk in die DB schreiben.
## Dateien & Aufwand
| Datei | Änderung | Aufwand |
|---|---|---|
| `Services/OpenAiService.cs` | `openai` Provider, API-Key-Parameter, alle Tools, maxIterations | Mittel |
| `Endpoints/ChatEndpoint.cs` | API-Key-Routing basierend auf Provider | Niedrig |
| `Endpoints/ModelsEndpoint.cs` | `GET /api/models/openai` | Niedrig |
| `Services/DatabaseService.Integrations.cs` | `GetFirstActiveKeyByProvider(provider)` | Niedrig |
| `wwwroot/index.html` | OpenAI Optgroup | Minimal |
| `wwwroot/js/pages/agent-page.js` | OpenAI-Modelle laden | Minimal |
| `wwwroot/js/pages/chat-page.js` | Provider-Info im Key-Switch-Modal | Niedrig |
**Gesamt: ~200-250 Zeilen Änderungen, Aufwand mittel**
## Risiken
1. **Tool-Format-Differenzen** – OpenAI und Anthropic haben leicht unterschiedliche
Tool-Call/Result-Formate. Die Konvertierung in `ConvertMessage()` muss stimmen.
→ Besteht schon für Nvidia/Gemini, sollte auch für OpenAI funktionieren.
2. **Streaming-Format** – OpenAI SSE-Format unterscheidet sich von Anthropic.
→ Wird bereits im `OpenAiService` korrekt gehandelt.
3. **Rate-Limits** – OpenAI Rate-Limits sind pro Organisation, nicht pro Key.
→ Kein spezifisches Handling nötig, 429 wird als Fehler angezeigt.
4. **Modell-Liste zu lang** – OpenAI hat ~43 Modelle (inkl. Legacy, Embedding, TTS).
→ Filtern auf Chat-Modelle: nur `gpt-*`, `o1-*`, `o3-*`, `chatgpt-*`.
## Nicht im Scope (später)
- Review/Preparer/Finalizer für OpenAI-Provider
- ask_user_choice für OpenAI-Provider
- Web-Search für OpenAI-Provider
- Memory-Tools für OpenAI-Provider (aktuell nur 6 Tools)
→ Diese Features erfordern einen größeren Umbau des OpenAiService
+215
View File
@@ -0,0 +1,215 @@
# Umsetzungsplan: GitHub Events API (Echtzeit-Scanner)
## Zusammenfassung
Echtzeit-Monitoring öffentlicher GitHub Push-Events um geleakte API-Keys **sofort** nach dem Commit zu finden.
GitGuardian nutzt genau diesen Ansatz und findet laut eigenen Angaben ~1800 neue Secrets pro Tag.
## Warum Events?
- **Echtzeit:** Keys werden Sekunden nach dem Push gefunden (statt Stunden/Tage bei Code Search)
- **Kein Suchindex-Delay:** GitHub Code Search indexiert neue Commits erst nach Minuten/Stunden
- **Force-Push-Catches:** Selbst wenn ein User den Commit sofort mit Force-Push löscht – der Event bleibt kurz sichtbar
- **Breite Abdeckung:** Alle öffentlichen Pushes weltweit, nicht nur bestimmte Sprachen/Pfade
- **Ergänzend:** Findet Keys die der periodische Scan erst beim nächsten Lauf erwischt hätte
## API-Analyse
### GitHub Public Events API
```
GET https://api.github.com/events?per_page=100
```
- Liefert die neuesten **öffentlichen** Events (PushEvent, CreateEvent, etc.)
- Response enthält bis zu 300 Events (3 Seiten à 100)
- Events werden **alle 60 Sekunden** aktualisiert
- **Kein Authentication nötig** (aber empfohlen für höhere Rate-Limits)
- Events bleiben **maximal 90 Tage** in der Timeline
### PushEvent-Struktur
```json
{
"type": "PushEvent",
"repo": { "name": "user/repo" },
"payload": {
"commits": [
{
"sha": "abc123...",
"message": "added config",
"url": "https://api.github.com/repos/user/repo/commits/abc123"
}
]
}
}
```
### Commit-Diff abrufen
```
GET https://api.github.com/repos/{owner}/{repo}/commits/{sha}
Accept: application/vnd.github.diff
```
→ Gibt den unified diff als Plaintext zurück → kann mit Regex gescannt werden.
**Alternativ:** Commit-Detail mit Patch:
```
GET https://api.github.com/repos/{owner}/{repo}/commits/{sha}
```
→ JSON mit `files[].patch` (unified diff per Datei)
## Architektur-Entscheidung
### Option A: Polling-Loop (empfohlen) ✅
- Eigener Background-Worker, pollt `/events` alle 60-90 Sekunden
- Filtert auf `PushEvent`
- Für jeden Commit: Diff abrufen, mit Regex scannen
- **Vorteil:** Einfach, zuverlässig, keine externe Abhängigkeit
- **Nachteil:** Max 300 Events pro Poll → bei >300 Events/Minute gehen manche verloren
### Option B: GitHub Webhooks
- Erfordert öffentlichen HTTPS-Endpoint auf unserem Server
- GitHub sendet Events per POST an unseren Server
- **Vorteil:** Kein Polling, kein Event-Verlust
- **Nachteil:** Nur für eigene Repos/Orgs, nicht für "alle öffentlichen Events"
- **Fazit:** ❌ Nicht für unseren Use-Case geeignet
### Option C: GitHub Firehose (gharchive.org)
- GH Archive sammelt alle öffentlichen Events als JSON-Lines
- Kann historisch abgefragt werden: `https://data.gharchive.org/2024-01-01-15.json.gz`
- **Vorteil:** Kein Event-Verlust, historische Daten
- **Nachteil:** Hoher Bandbreitenverbrauch, ~500MB/Stunde komprimiert
- **Fazit:** ⚠️ Für späteren Ausbau, nicht für v1
## Implementierungsplan
### Phase 1: Neuer BackgroundService `GitHubEventScannerService`
Separater Service (nicht in `KeyScannerService`), da er **kontinuierlich** läuft statt periodisch.
```csharp
public class GitHubEventScannerService : BackgroundService
{
private const int PollIntervalMs = 75_000; // 75 Sekunden (Events aktualisieren alle 60s)
private const int MaxCommitsPerPoll = 50; // Max Commits pro Poll analysieren
private string _lastEventId; // ETag/Last-Event-ID für Dedup
}
```
**Ablauf pro Poll-Zyklus:**
1. `GET /events?per_page=100` → Alle Events abrufen
2. Filtern auf `PushEvent`
3. Für jeden PushEvent:
- Commits extrahieren (`payload.commits[]`)
- Pro Commit: Diff abrufen (`GET /repos/{owner}/{repo}/commits/{sha}` mit `Accept: diff`)
- Diff mit Regex scannen
- Bei Treffer: Key + Commit-URL + Repo-Name speichern
4. Deduplication: `sha` als Schlüssel → gleichen Commit nie doppelt scannen
5. Neue Keys in DB speichern (wie beim normalen Scanner)
### Phase 2: Event-Deduplication
```csharp
// Kreisförmiger Buffer der letzten 10.000 verarbeiteten Event-IDs
private readonly HashSet<string> _processedEventIds = new();
private readonly Queue<string> _eventIdQueue = new();
private const int MaxProcessedIds = 10_000;
```
### Phase 3: Commit-Diff-Scanning
```csharp
private async Task<List<(string Key, string Url)>> ScanCommitDiff(
HttpClient client, string repo, string sha, CancellationToken ct)
{
var keys = new List<(string Key, string Url)>();
// Diff als Plaintext abrufen
var request = new HttpRequestMessage(HttpMethod.Get,
$"https://api.github.com/repos/{repo}/commits/{sha}");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.diff"));
var resp = await client.SendAsync(request, ct);
if (!resp.IsSuccessStatusCode) return keys;
var diff = await resp.Content.ReadAsStringAsync(ct);
// Nur hinzugefügte Zeilen scannen (die mit "+" beginnen)
foreach (var line in diff.Split('\n'))
{
if (!line.StartsWith("+")) continue;
foreach (var (pattern, _) in KeyPatterns)
{
var regex = new Regex(pattern);
foreach (Match m in regex.Matches(line))
{
var url = $"https://github.com/{repo}/commit/{sha}";
keys.Add((m.Value, url));
}
}
}
return keys;
}
```
### Phase 4: Integration & Config
```csharp
// Program.cs
builder.Services.AddHostedService<GitHubEventScannerService>();
```
```json
{
"GitHubEventScanner": true, // Feature-Toggle
"GitHubToken": "ghp_xxx" // Gemeinsam mit Code Search
}
```
### Phase 5: Admin-UI
- Status-Anzeige: "Event Scanner: Aktiv | Letzter Poll: vor 45s | Events/h: 2400 | Keys gefunden: 3"
- Toggle zum Ein-/Ausschalten
- Log der letzten gefundenen Keys mit Timestamp
## Rate-Limit-Strategie
| Aktion | Rate Limit | Verbrauch pro Poll |
|---|---|---|
| List Events | 60 req/h (unauth) / 5000 req/h (auth) | 1-3 Requests |
| Commit Diff | 5000 req/h (auth) | Max 50 Requests |
| **Gesamt pro Stunde** | | ~2500 Requests (50 × 48 Polls) |
**Optimierung:**
- Nur PushEvents verarbeiten (~30% aller Events)
- Nur Commits scannen deren Diff < 100 KB ist (keine Merge-Commits etc.)
- Nur hinzugefügte Zeilen (`+`) scannen
- Bei Rate-Limit-Warning: Poll-Intervall dynamisch erhöhen
## Volumenschätzung
- ~5.000-10.000 Events/Minute auf GitHub (öffentlich)
- Davon ~30% PushEvents = ~2.000/Minute
- Wir sehen maximal 300 Events pro Poll = ~200 PushEvents
- ~3-5 Commits pro Push im Schnitt = ~800 Commits
- Davon scannen wir max 50 pro Poll (Priorität: kleine Diffs, .env-Dateien etc.)
## Aufwandsschätzung
- **Implementierung:** ~300-400 Zeilen Code (neuer BackgroundService)
- **Komplexität:** Mittel-Hoch (kontinuierlicher Worker, Rate-Limit-Management, Dedup)
- **Erwarteter Ertrag:** 🔥🔥 Sehr hoch (Echtzeit-Catches, bevor Keys rotiert werden)
## Risiken
- **Event-Verlust:** Bei >300 Events/Minute (≈5/Sekunde) werden Events verpasst
→ Akzeptabel, der periodische Code-Search-Scan fängt den Rest auf
- **Hoher API-Verbrauch:** ~2500 req/h für Commit-Diffs → Hälfte des Rate-Limits
→ Kann mit smarter Filterung (nur kleine Diffs, nur bestimmte Dateien) reduziert werden
- **False Positives:** Diffs können Test-/Mock-Keys enthalten
→ Wird durch den bestehenden Key-Check (`AnthropicKeyChecker`) aufgefangen
- **Server-Last:** Kontinuierlicher Polling-Loop → minimal (1 HTTP-Request/75s + Diffs)
## Priorisierung innerhalb des Scans
Nicht alle Commits sind gleich interessant. Reihenfolge:
1. 🔥 Commits die `.env`, `.envrc`, `config.*`, `secrets.*` ändern
2. 🔥 Commits mit kleinem Diff (< 10 KB) – wahrscheinlich manuelle Änderungen
3. 🟡 Commits die `*.py`, `*.js`, `*.ts` ändern
4. ⬜ Alles andere (bei Zeit/Budget)
→ Die Commit-Detail-Response enthält `files[].filename` → Filterung vor Diff-Download möglich.
+126
View File
@@ -0,0 +1,126 @@
# Umsetzungsplan: GitHub Issues & PR Comments Scanner
## Zusammenfassung
Scan von öffentlichen GitHub Issue- und PR-Kommentaren nach geleakten API-Keys.
TruffleHog-Studien zeigen Zehntausende Secrets in GitHub-Kommentaren – eine bisher kaum gescannte Quelle.
## Warum Issues/Comments?
- User posten Keys in Issues ("mein Key funktioniert nicht: sk-ant-oat01-...")
- Keys in PR-Kommentaren, Code-Reviews, Fehlermeldungen
- Selbst nach Edit bleiben alte Versionen in der Comment-History sichtbar
- GitHub Code Search (`/search/code`) durchsucht **keine** Issues/Comments
- Unser bestehender GitHub-Scanner und Serper decken diese Quelle **nicht** ab
## API-Analyse
### GitHub Search Issues API ✅ (Hauptansatz)
```
GET https://api.github.com/search/issues?q="sk-ant-oat01"+in:comments&per_page=100
```
- Durchsucht Issue-Titel, Body UND Kommentare
- `in:comments` filtert auf Kommentare
- Gibt Issues zurück, nicht die Kommentare selbst
- Max 1000 Ergebnisse (10 Seiten à 100)
- **Rate Limit:** 30 Requests/Minute (authentifiziert), 10/min unauthentifiziert
### GitHub List Issue Comments API (Ergänzung)
```
GET https://api.github.com/repos/{owner}/{repo}/issues/comments?since=2024-01-01&per_page=100
```
- Listet alle Kommentare eines Repos (mit `since`-Filter für inkrementellen Scan)
- Max ~30.000 Kommentare pro Repo via REST
- Response enthält `body` → kann mit Regex gescannt werden
- Nützlich um **nach** dem Search-API-Treffer die tatsächlichen Keys aus den Comments zu extrahieren
### Drei Arten von Kommentaren
| Typ | API-Endpoint | Beschreibung |
|---|---|---|
| Issue Comments | `/repos/{owner}/{repo}/issues/comments` | Kommentare auf Issues UND PR-Diskussionen |
| PR Review Comments | `/repos/{owner}/{repo}/pulls/comments` | Inline-Code-Review-Kommentare |
| Commit Comments | `/repos/{owner}/{repo}/comments` | Kommentare auf einzelne Commits |
## Implementierungsplan
### Phase 1: Search Issues Scan
```csharp
private async Task<List<(string Key, string Url)>> ScanGitHubIssues(CancellationToken ct)
```
**Ablauf:**
1. **Issue Search** für jeden Key-Pattern-SearchTerm:
```
GET /search/issues?q="sk-ant-oat01"+in:comments&sort=created&order=desc&per_page=100
GET /search/issues?q="sk-ant-oat01"+in:body&sort=created&order=desc&per_page=100
```
2. Für jeden Treffer:
- Issue-`body` mit Regex scannen
- Kommentare des Issues abrufen:
```
GET /repos/{owner}/{repo}/issues/{number}/comments?per_page=100
```
- Jeden Kommentar-`body` mit Regex scannen
3. **PR Review Comments** für Treffer-Repos:
```
GET /repos/{owner}/{repo}/pulls/comments?per_page=100&since={last_scan}
```
4. Gefundene Keys mit HTML-URL des Kommentars zurückgeben
### Phase 2: Integration in RunScan
```csharp
// In RunScan(), nach GitHub Code Search:
if (mode != "web" && !string.IsNullOrWhiteSpace(_githubToken))
{
var issueKeys = await ScanGitHubIssues(ct);
allFoundKeys.AddRange(issueKeys.Select(k => (k.Key, k.Url, "github_issues")));
}
```
- **Mode `quick`:** Nur Search Issues API (schnell, max 10 Requests)
- **Mode `web`:** Kein Issue-Scan (gehört zu GitHub)
- **Mode `full`:** Search Issues + Comment-Scan der Treffer
### Phase 3: Deduplication mit bestehendem GitHub-Scan
Keys die bereits über GitHub Code Search gefunden wurden, werden über `_db.ScannedKeyExists()` dedupliziert.
Source wird als `"github_issues"` gespeichert (statt `"github"`), damit die Quelle unterscheidbar ist.
## Rate-Limit-Strategie
| API | Rate Limit | Strategie |
|---|---|---|
| Search Issues | 30 req/min | Max 10 Seiten × Anzahl Patterns = ~20 Requests |
| Issue Comments | 5000 req/h | Pro Treffer 1 Request, max 100 Treffer |
| PR Review Comments | 5000 req/h | Nur für Treffer-Repos |
**Gesamtverbrauch pro Full-Scan:** ~120-200 Requests (gut innerhalb der Limits)
**Wartezeiten:**
- Zwischen Search-Requests: 2s (30 req/min = 1 req/2s)
- Zwischen Comment-Requests: 1s
## Suchqueries
```csharp
private static readonly string[] IssueSearchQueries = {
"\"sk-ant-oat01\" in:comments",
"\"sk-ant-oat01\" in:body",
"\"sk-ant-api03\" in:comments",
"\"sk-ant-api03\" in:body",
"\"ANTHROPIC_API_KEY\" in:comments", // Bonus: env-var-Name → könnte Key im selben Comment haben
};
```
## Aufwandsschätzung
- **Implementierung:** ~250 Zeilen Code
- **Komplexität:** Mittel (mehrere API-Endpoints, Comment-Extraction)
- **Erwarteter Ertrag:** 🔥 Hoch (nachgewiesene Secret-Quelle, kaum gescannt)
## Risiken
- Search Issues API gibt max 1000 Ergebnisse → ältere Issues nicht erreichbar
- Kommentare können editiert/gelöscht sein → Key evtl. nicht mehr im Body
- Rate-Limits bei vielen Treffern → braucht gute Throttling-Logik
- Manche Keys sind absichtlich gepostet (Test-Keys, bereits revoked) → False Positives
+118
View File
@@ -0,0 +1,118 @@
# Umsetzungsplan: Hugging Face Hub Scanner
## Zusammenfassung
Scan von öffentlichen Hugging Face Spaces, Models und Datasets nach geleakten API-Keys.
AI/ML-Entwickler nutzen häufig Anthropic-Keys in ihren Projekten – besonders in Spaces (Gradio-Apps) und Notebooks.
## Warum Hugging Face?
- **2M+ Models, 500k+ Datasets, 1M+ Spaces** auf der Plattform
- ML/AI-Entwickler sind die Hauptnutzer von Anthropic-Keys
- Viele Anfänger hardcoden Keys in `app.py`, `.env`, Notebooks
- Hugging Face Full-Text-Suche indexiert Model Cards, Dataset Cards und `app.py`-Dateien
- Öffentliche REST API ohne Authentifizierung nutzbar
## API-Analyse
### Ansatz 1: Full-Text Search (Web-Scraping)
- **URL:** `https://huggingface.co/search/full-text?q=sk-ant-oat01&type=space`
- Indexiert: Model Cards, Dataset Cards, Spaces `app.py`
- **Problem:** Kein offizieller API-Endpoint, HTML-Scraping nötig → fragil
- **Fazit:** ❌ Nicht empfohlen
### Ansatz 2: Hub REST API + Datei-Download ✅ (Empfohlen)
Die HF Hub API erlaubt:
1. **Spaces auflisten** mit Suchfilter: `GET https://huggingface.co/api/spaces?search=anthropic&limit=100`
2. **Dateien eines Repos auflisten:** `GET https://huggingface.co/api/spaces/{repo_id}/tree/main`
3. **Datei-Inhalt herunterladen:** `GET https://huggingface.co/spaces/{repo_id}/resolve/main/{path}`
**Strategie:**
- Spaces die "anthropic", "claude", "sk-ant" im Namen/Description haben → Dateien scannen
- Gezielt nach `.env`, `app.py`, `*.py`, `config.*`, `.streamlit/secrets.toml` suchen
- Datei-Inhalt mit Regex auf Key-Patterns prüfen
### Rate Limits
- Unauthentifiziert: ~100 Requests/Minute (geschätzt)
- Authentifiziert (HF Token): Höhere Limits
- Kein offizielles Rate-Limit-Dokument, aber großzügig
## Implementierungsplan
### Phase 1: Neue Scan-Methode `ScanHuggingFace`
```
private async Task<List<(string Key, string Url)>> ScanHuggingFace(CancellationToken ct)
```
**Ablauf:**
1. Spaces suchen mit relevanten Keywords:
- `GET /api/spaces?search=anthropic&sort=modified&direction=-1&limit=100`
- `GET /api/spaces?search=claude&sort=modified&direction=-1&limit=100`
- `GET /api/spaces?search=sk-ant&sort=modified&direction=-1&limit=100`
- `GET /api/spaces?search=api+key&sort=modified&direction=-1&limit=100`
2. Für jeden gefundenen Space:
- Dateiliste abrufen: `GET /api/spaces/{id}/tree/main`
- Relevante Dateien filtern (`.env`, `*.py`, `*.yaml`, `*.toml`, `*.json`, `*.md`, `*.txt`, `*.ipynb`)
- Datei-Inhalt herunterladen und mit Regex scannen
3. Gleiche Prozedur für Models (viele haben Python-Inferenz-Scripts):
- `GET /api/models?search=anthropic&sort=modified&direction=-1&limit=100`
4. Gefundene Keys mit Source-URL zurückgeben
### Phase 2: Integration in RunScan
```csharp
// In RunScan(), nach Serper-Scan:
if (mode != "quick" && !string.IsNullOrWhiteSpace(_huggingFaceEnabled))
{
var hfKeys = await ScanHuggingFace(ct);
allFoundKeys.AddRange(hfKeys.Select(k => (k.Key, k.Url, "huggingface")));
}
```
- **Mode `quick`:** Kein HF-Scan (zu langsam)
- **Mode `web`:** HF-Scan aktiv (gehört zu "Web"-Quellen)
- **Mode `full`:** HF-Scan aktiv
### Phase 3: Config
```json
{
"HuggingFaceToken": "hf_xxx" // Optional, erhöht Rate-Limits
}
```
Ein HF-Token ist **nicht zwingend nötig** – die API funktioniert auch ohne Auth.
Mit Token aber höhere Rate-Limits und Zugriff auf gated Repos.
## Scan-Targets (Dateien die gescannt werden)
| Datei-Pattern | Wahrscheinlichkeit für Keys |
|---|---|
| `.env`, `.env.*` | 🔥 Sehr hoch |
| `app.py` | 🔥 Hoch (Gradio Spaces) |
| `*.py` | 🔥 Hoch |
| `*.ipynb` | 🔥 Hoch (Notebooks) |
| `config.*`, `*.yaml`, `*.toml` | 🟡 Mittel |
| `*.json` | 🟡 Mittel |
| `*.md` (README) | 🟡 Niedrig |
| `*.txt` | 🟡 Niedrig |
**Dateigröße-Limit:** Nur Dateien < 500 KB herunterladen (keine Model-Weights etc.)
## Suchbegriffe
```csharp
private static readonly string[] HuggingFaceSearchTerms = {
"anthropic", "claude", "sk-ant", "api key anthropic",
"claude api", "anthropic key"
};
```
## Aufwandsschätzung
- **Implementierung:** ~200 Zeilen Code (neue Methode + Config)
- **Komplexität:** Niedrig (einfache REST API, kein Auth nötig)
- **Erwarteter Ertrag:** Mittel-Hoch (AI/ML-Entwickler = Hauptnutzer von Anthropic)
## Risiken
- Kein offizielles Rate-Limit-Dokument → könnte throttlen
- Spaces die nur im Runtime env-Vars setzen → Keys nicht in Dateien sichtbar
- Große Repos mit vielen Dateien → Timeout-Gefahr → Max 50 Dateien pro Repo scannen