chore: erledigte Pläne ins Repo aufnehmen (Plans/ aus .gitignore entfernt)
This commit is contained in:
1 parent
8e94975e61
commit
30662caab7
6 files changed
+767
-3
No files matched your search
@@ -5,6 +5,3 @@ data/
|
||||
Temp/
|
||||
appsettings.Production.json
|
||||
DEVY.md
|
||||
|
||||
/ExampleScannerProject/
|
||||
/Plans/
|
||||
@@ -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}`
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user