mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-03-21 17:53:24 +00:00
116 modules | 100 vuln types | 18 API routes | 18 frontend pages Major features: - VulnEngine: 100 vuln types, 526+ payloads, 12 testers, anti-hallucination prompts - Autonomous Agent: 3-stream auto pentest, multi-session (5 concurrent), pause/resume/stop - CLI Agent: Claude Code / Gemini CLI / Codex CLI inside Kali containers - Validation Pipeline: negative controls, proof of execution, confidence scoring, judge - AI Reasoning: ReACT engine, token budget, endpoint classifier, CVE hunter, deep recon - Multi-Agent: 5 specialists + orchestrator + researcher AI + vuln type agents - RAG System: BM25/TF-IDF/ChromaDB vectorstore, few-shot, reasoning templates - Smart Router: 20 providers (8 CLI OAuth + 12 API), tier failover, token refresh - Kali Sandbox: container-per-scan, 56 tools, VPN support, on-demand install - Full IA Testing: methodology-driven comprehensive pentest sessions - Notifications: Discord, Telegram, WhatsApp/Twilio multi-channel alerts - Frontend: React/TypeScript with 18 pages, real-time WebSocket updates
404 lines
12 KiB
Python
404 lines
12 KiB
Python
"""
|
|
NeuroSploit v3 - Providers API
|
|
|
|
REST endpoints for managing LLM providers and accounts.
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class ConnectRequest(BaseModel):
|
|
label: str = "Manual API Key"
|
|
credential: str
|
|
credential_type: str = "api_key"
|
|
model_override: Optional[str] = None
|
|
|
|
|
|
@router.get("")
|
|
async def list_providers():
|
|
"""List all providers with their accounts and status."""
|
|
from backend.core.smart_router import get_registry
|
|
registry = get_registry()
|
|
if not registry:
|
|
return {"enabled": False, "providers": []}
|
|
|
|
providers = []
|
|
for p in registry.get_all_providers():
|
|
accounts = []
|
|
for a in p.accounts.values():
|
|
accounts.append({
|
|
"id": a.id,
|
|
"label": a.label,
|
|
"source": a.source,
|
|
"credential_type": a.credential_type,
|
|
"is_active": a.is_active,
|
|
"tokens_used": a.tokens_used,
|
|
"last_used": a.last_used,
|
|
"expires_at": a.expires_at,
|
|
"model_override": a.model_override,
|
|
})
|
|
providers.append({
|
|
"id": p.id,
|
|
"name": p.name,
|
|
"auth_type": p.auth_type,
|
|
"api_format": p.api_format,
|
|
"tier": p.tier,
|
|
"default_model": p.default_model,
|
|
"accounts": accounts,
|
|
"connected": any(
|
|
a.is_active and a.id in registry._credentials
|
|
for a in p.accounts.values()
|
|
),
|
|
"enabled": getattr(p, "enabled", True),
|
|
})
|
|
|
|
return {"enabled": True, "providers": providers}
|
|
|
|
|
|
@router.get("/status")
|
|
async def providers_status():
|
|
"""Get quota and usage summary."""
|
|
from backend.core.smart_router import get_router
|
|
router_instance = get_router()
|
|
if not router_instance:
|
|
return {"enabled": False}
|
|
return {"enabled": True, **router_instance.get_status()}
|
|
|
|
|
|
@router.post("/{provider_id}/detect")
|
|
async def detect_cli_token(provider_id: str):
|
|
"""Auto-detect CLI token for a specific provider."""
|
|
from backend.core.smart_router import get_registry, get_extractor
|
|
registry = get_registry()
|
|
extractor = get_extractor()
|
|
if not registry or not extractor:
|
|
raise HTTPException(400, "Smart Router not enabled")
|
|
|
|
token = extractor.detect(provider_id)
|
|
if not token:
|
|
return {"detected": False, "message": f"No CLI token found for {provider_id}"}
|
|
|
|
# Add to registry
|
|
acct_id = registry.add_account(
|
|
provider_id=provider_id,
|
|
label=token.label,
|
|
credential=token.token,
|
|
credential_type=token.credential_type,
|
|
source="cli_detect",
|
|
refresh_token=token.refresh_token,
|
|
expires_at=token.expires_at,
|
|
)
|
|
|
|
return {
|
|
"detected": True,
|
|
"account_id": acct_id,
|
|
"label": token.label,
|
|
"credential_type": token.credential_type,
|
|
"has_refresh_token": token.refresh_token is not None,
|
|
"expires_at": token.expires_at,
|
|
}
|
|
|
|
|
|
@router.post("/{provider_id}/connect")
|
|
async def connect_provider(provider_id: str, req: ConnectRequest):
|
|
"""Manually add an API key or credential."""
|
|
from backend.core.smart_router import get_registry
|
|
registry = get_registry()
|
|
if not registry:
|
|
raise HTTPException(400, "Smart Router not enabled")
|
|
|
|
acct_id = registry.add_account(
|
|
provider_id=provider_id,
|
|
label=req.label,
|
|
credential=req.credential,
|
|
credential_type=req.credential_type,
|
|
source="manual",
|
|
model_override=req.model_override,
|
|
)
|
|
if not acct_id:
|
|
raise HTTPException(404, f"Unknown provider: {provider_id}")
|
|
|
|
return {"success": True, "account_id": acct_id}
|
|
|
|
|
|
@router.delete("/{provider_id}/accounts/{account_id}")
|
|
async def remove_account(provider_id: str, account_id: str):
|
|
"""Remove an account from a provider."""
|
|
from backend.core.smart_router import get_registry
|
|
registry = get_registry()
|
|
if not registry:
|
|
raise HTTPException(400, "Smart Router not enabled")
|
|
|
|
success = registry.remove_account(provider_id, account_id)
|
|
if not success:
|
|
raise HTTPException(404, "Account not found")
|
|
return {"success": True}
|
|
|
|
|
|
@router.post("/test/{provider_id}/{account_id}")
|
|
async def test_connection(provider_id: str, account_id: str):
|
|
"""Test connectivity for a specific account."""
|
|
from backend.core.smart_router import get_router
|
|
router_instance = get_router()
|
|
if not router_instance:
|
|
raise HTTPException(400, "Smart Router not enabled")
|
|
|
|
success, message = await router_instance.test_account(provider_id, account_id)
|
|
return {"success": success, "message": message}
|
|
|
|
|
|
# Known models per provider for dropdown selection
|
|
PROVIDER_MODELS = {
|
|
"claude_code": [
|
|
"claude-opus-4-6-20250918",
|
|
"claude-sonnet-4-5-20250929",
|
|
"claude-haiku-4-5-20251001",
|
|
"claude-sonnet-4-20250514",
|
|
"claude-opus-4-20250514",
|
|
"claude-haiku-4-20250514",
|
|
],
|
|
"kiro": [
|
|
"claude-opus-4-6-20250918",
|
|
"claude-sonnet-4-5-20250929",
|
|
"claude-haiku-4-5-20251001",
|
|
"claude-sonnet-4-20250514",
|
|
"claude-opus-4-20250514",
|
|
"claude-haiku-4-20250514",
|
|
],
|
|
"anthropic": [
|
|
"claude-opus-4-6-20250918",
|
|
"claude-sonnet-4-5-20250929",
|
|
"claude-haiku-4-5-20251001",
|
|
"claude-sonnet-4-20250514",
|
|
"claude-opus-4-20250514",
|
|
"claude-haiku-4-20250514",
|
|
"claude-3-5-sonnet-20241022",
|
|
],
|
|
"codex_cli": [
|
|
"gpt-4o",
|
|
"gpt-4o-mini",
|
|
"o3-mini",
|
|
"o4-mini",
|
|
"gpt-4.1",
|
|
"gpt-4.1-mini",
|
|
"gpt-4.1-nano",
|
|
],
|
|
"openai": [
|
|
"gpt-4o",
|
|
"gpt-4o-mini",
|
|
"o3-mini",
|
|
"o4-mini",
|
|
"gpt-4.1",
|
|
"gpt-4.1-mini",
|
|
"gpt-4.1-nano",
|
|
],
|
|
"gemini_cli": [
|
|
"gemini-3.0-pro",
|
|
"gemini-2.5-pro",
|
|
"gemini-2.5-flash",
|
|
"gemini-2.0-flash",
|
|
"gemini-2.0-flash-lite",
|
|
],
|
|
"gemini": [
|
|
"gemini-3.0-pro",
|
|
"gemini-2.5-pro",
|
|
"gemini-2.5-flash",
|
|
"gemini-2.0-flash",
|
|
"gemini-2.0-flash-lite",
|
|
],
|
|
"cursor": [
|
|
"cursor-fast",
|
|
"cursor-small",
|
|
"gpt-4o",
|
|
"claude-sonnet-4-5-20250929",
|
|
"claude-3-5-sonnet-20241022",
|
|
],
|
|
"copilot": [
|
|
"gpt-4o",
|
|
"gpt-4o-mini",
|
|
"claude-sonnet-4-5-20250929",
|
|
"claude-3-5-sonnet-20241022",
|
|
],
|
|
"openrouter": [
|
|
"anthropic/claude-opus-4-6",
|
|
"anthropic/claude-sonnet-4-5",
|
|
"anthropic/claude-haiku-4-5",
|
|
"anthropic/claude-sonnet-4",
|
|
"anthropic/claude-opus-4",
|
|
"openai/gpt-4o",
|
|
"google/gemini-3.0-pro",
|
|
"google/gemini-2.5-pro",
|
|
"google/gemini-2.5-flash",
|
|
"meta-llama/llama-4-maverick",
|
|
"deepseek/deepseek-r1",
|
|
],
|
|
"together": [
|
|
"meta-llama/Llama-3-70b-chat-hf",
|
|
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
|
"deepseek-ai/DeepSeek-R1",
|
|
"Qwen/Qwen2.5-72B-Instruct-Turbo",
|
|
],
|
|
"fireworks": [
|
|
"accounts/fireworks/models/llama-v3p1-70b-instruct",
|
|
"accounts/fireworks/models/llama-v3p3-70b-instruct",
|
|
"accounts/fireworks/models/deepseek-r1",
|
|
],
|
|
"iflow": ["kimi-k2"],
|
|
"qwen_code": ["qwen3-coder", "qwen-max"],
|
|
"ollama": ["llama3", "llama3.2", "mistral", "codellama", "deepseek-r1"],
|
|
"lmstudio": ["local-model"],
|
|
}
|
|
|
|
|
|
@router.get("/available-models")
|
|
async def available_models():
|
|
"""Get list of available provider+model combinations for selection dropdowns."""
|
|
from backend.core.smart_router import get_registry
|
|
registry = get_registry()
|
|
if not registry:
|
|
return {"models": []}
|
|
|
|
models = []
|
|
for p in registry.get_all_providers():
|
|
active = registry.get_active_accounts(p.id)
|
|
if not active:
|
|
continue
|
|
models.append({
|
|
"provider_id": p.id,
|
|
"provider_name": p.name,
|
|
"default_model": p.default_model,
|
|
"tier": p.tier,
|
|
"available_models": PROVIDER_MODELS.get(p.id, [p.default_model]),
|
|
})
|
|
|
|
# Sort by tier (paid first) then name
|
|
models.sort(key=lambda m: (m["tier"], m["provider_name"]))
|
|
return {"models": models}
|
|
|
|
|
|
@router.post("/detect-all")
|
|
async def detect_all_tokens():
|
|
"""Scan all CLI tools for available tokens."""
|
|
from backend.core.smart_router import get_registry, get_extractor
|
|
registry = get_registry()
|
|
extractor = get_extractor()
|
|
if not registry or not extractor:
|
|
raise HTTPException(400, "Smart Router not enabled")
|
|
|
|
tokens = extractor.detect_all()
|
|
results = []
|
|
for token in tokens:
|
|
acct_id = registry.add_account(
|
|
provider_id=token.provider_id,
|
|
label=token.label,
|
|
credential=token.token,
|
|
credential_type=token.credential_type,
|
|
source="cli_detect",
|
|
refresh_token=token.refresh_token,
|
|
expires_at=token.expires_at,
|
|
)
|
|
results.append({
|
|
"provider_id": token.provider_id,
|
|
"label": token.label,
|
|
"account_id": acct_id,
|
|
})
|
|
|
|
return {
|
|
"detected_count": len(results),
|
|
"results": results,
|
|
}
|
|
|
|
|
|
class ToggleRequest(BaseModel):
|
|
enabled: bool
|
|
|
|
|
|
@router.post("/{provider_id}/toggle")
|
|
async def toggle_provider(provider_id: str, req: ToggleRequest):
|
|
"""Enable or disable a provider. Disabled providers are skipped by the router."""
|
|
from backend.core.smart_router import get_registry
|
|
registry = get_registry()
|
|
if not registry:
|
|
raise HTTPException(400, "Smart Router not enabled")
|
|
|
|
success = registry.toggle_provider(provider_id, req.enabled)
|
|
if not success:
|
|
raise HTTPException(404, f"Unknown provider: {provider_id}")
|
|
|
|
return {"success": True, "provider_id": provider_id, "enabled": req.enabled}
|
|
|
|
|
|
# Whitelist of env keys that can be modified via UI
|
|
ALLOWED_ENV_KEYS = {
|
|
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY",
|
|
"OPENROUTER_API_KEY", "TOGETHER_API_KEY", "FIREWORKS_API_KEY",
|
|
"OLLAMA_HOST", "LMSTUDIO_HOST",
|
|
"ENABLE_SMART_ROUTER", "ENABLE_REASONING", "ENABLE_CVE_HUNT",
|
|
"ENABLE_MULTI_AGENT", "ENABLE_RESEARCHER_AI",
|
|
"NVD_API_KEY", "GITHUB_TOKEN", "TOKEN_BUDGET",
|
|
}
|
|
|
|
|
|
class EnvUpdateRequest(BaseModel):
|
|
key: str
|
|
value: str
|
|
|
|
|
|
@router.get("/env")
|
|
async def get_env_keys():
|
|
"""Get current values of allowed env keys (masked for secrets)."""
|
|
import os
|
|
result = {}
|
|
for key in sorted(ALLOWED_ENV_KEYS):
|
|
val = os.getenv(key, "")
|
|
if val and "KEY" in key and key not in ("ENABLE_SMART_ROUTER", "ENABLE_REASONING",
|
|
"ENABLE_CVE_HUNT", "ENABLE_MULTI_AGENT",
|
|
"ENABLE_RESEARCHER_AI", "TOKEN_BUDGET"):
|
|
# Mask API keys: show first 8 and last 4 chars
|
|
if len(val) > 16:
|
|
result[key] = val[:8] + "..." + val[-4:]
|
|
else:
|
|
result[key] = "****"
|
|
else:
|
|
result[key] = val
|
|
return {"env": result, "allowed_keys": sorted(ALLOWED_ENV_KEYS)}
|
|
|
|
|
|
@router.post("/env")
|
|
async def update_env_key(req: EnvUpdateRequest):
|
|
"""Update an env var and persist to .env file."""
|
|
import os
|
|
from pathlib import Path
|
|
|
|
if req.key not in ALLOWED_ENV_KEYS:
|
|
raise HTTPException(400, f"Key '{req.key}' is not in the allowed whitelist")
|
|
|
|
# Update in-process env
|
|
os.environ[req.key] = req.value
|
|
|
|
# Persist to .env file
|
|
env_path = Path(__file__).parent.parent.parent.parent / ".env"
|
|
try:
|
|
lines = []
|
|
found = False
|
|
if env_path.exists():
|
|
for line in env_path.read_text().splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith(f"{req.key}=") or stripped.startswith(f"# {req.key}="):
|
|
lines.append(f"{req.key}={req.value}")
|
|
found = True
|
|
else:
|
|
lines.append(line)
|
|
if not found:
|
|
lines.append(f"{req.key}={req.value}")
|
|
env_path.write_text("\n".join(lines) + "\n")
|
|
except Exception as e:
|
|
# Still updated in-process even if file write failed
|
|
return {"success": True, "persisted": False, "error": str(e)}
|
|
|
|
return {"success": True, "persisted": True}
|