Files
CyberSecurityUP e0935793c5 NeuroSploit v3.2 - Autonomous AI Penetration Testing Platform
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
2026-02-22 17:59:28 -03:00

607 lines
24 KiB
Python

"""
NeuroSploit v3 - Smart Router
Core routing engine with 4 API format translators, tier-based failover,
round-robin load balancing, and quota tracking.
"""
import asyncio
import logging
import time
from typing import Any, Dict, List, Optional, Tuple
from .provider_registry import Account, Provider, ProviderRegistry
logger = logging.getLogger(__name__)
class QuotaTracker:
"""Tracks per-account quota exhaustion with automatic recovery."""
def __init__(self):
self._exhausted: Dict[str, float] = {} # acct_id -> retry_after_timestamp
self._round_robin: Dict[str, int] = {} # provider_id -> last_index
def record_exhausted(self, account_id: str, retry_after: int = 300):
"""Mark an account as quota-exhausted."""
self._exhausted[account_id] = time.time() + retry_after
logger.warning(f"QuotaTracker: {account_id} exhausted, retry after {retry_after}s")
def is_available(self, account_id: str) -> bool:
"""Check if an account is available (not exhausted or recovered)."""
if account_id not in self._exhausted:
return True
if time.time() > self._exhausted[account_id]:
del self._exhausted[account_id]
return True
return False
def next_account(self, provider_id: str, accounts: List[Account]) -> Optional[Account]:
"""Round-robin selection from available accounts."""
available = [a for a in accounts if self.is_available(a.id)]
if not available:
return None
idx = self._round_robin.get(provider_id, -1) + 1
idx = idx % len(available)
self._round_robin[provider_id] = idx
return available[idx]
class SmartRouter:
"""Smart LLM router with format translation, failover, and load balancing.
Routing priority:
1. Preferred provider (if specified)
2. Tier 1 providers (subscription + paid API keys)
3. Tier 2 providers (cheap API keys)
4. Tier 3 providers (free/local)
"""
def __init__(self, registry: ProviderRegistry):
self.registry = registry
self.quota = QuotaTracker()
self._total_requests = 0
self._total_tokens = 0
self._failover_count = 0
self._last_provider: Optional[str] = None # Last provider used
self._last_model: Optional[str] = None # Last model used
self._last_account_label: Optional[str] = None # Last account label
self._gemini_project_id: Optional[str] = None # Cached Gemini CLI project ID
async def generate(
self,
prompt: str,
system: str = "",
max_tokens: int = 4096,
preferred_provider: Optional[str] = None,
model: Optional[str] = None,
temperature: float = 0.7,
) -> str:
"""Route a generation request through available providers.
Tries providers in tier order, fails over on errors.
Returns generated text or raises if all providers fail.
"""
candidates = self._build_candidate_list(preferred_provider)
if not candidates:
raise RuntimeError(
f"SmartRouter: No providers available "
f"(preferred={preferred_provider}, model={model})"
)
logger.info(
f"SmartRouter: {len(candidates)} candidates "
f"[{', '.join(f'{p.id}/{a.label}' for p, a in candidates)}]"
)
last_error = None
for provider, account in candidates:
try:
use_model = model or account.model_override or provider.default_model
logger.info(f"SmartRouter: Trying {provider.name} ({use_model}) via {account.label}")
text, tokens_used = await self._call_provider(
provider=provider,
account=account,
prompt=prompt,
system=system,
max_tokens=max_tokens,
model=use_model,
temperature=temperature,
)
# Record success + track which provider/model served the request
self._total_requests += 1
self._total_tokens += tokens_used
self._last_provider = provider.id
self._last_model = use_model
self._last_account_label = account.label
self.registry.record_usage(account.id, tokens_used)
logger.info(f"SmartRouter: OK — served by {provider.name} ({use_model})")
return text
except QuotaExhaustedError as e:
retry_after = getattr(e, "retry_after", 300)
self.quota.record_exhausted(account.id, retry_after)
self._failover_count += 1
last_error = e
logger.info(f"SmartRouter: {provider.name}/{account.label} quota exhausted, trying next")
except AuthenticationError as e:
# For CLI/OAuth accounts, try re-extracting token and RETRY immediately
if account.source == "cli_detect":
refreshed = await self._try_reextract_cli_token(provider.id, account.id)
if refreshed:
logger.info(f"SmartRouter: {provider.name}/{account.label} token re-extracted, retrying NOW")
try:
text, tokens_used = await self._call_provider(
provider=provider,
account=account,
prompt=prompt,
system=system,
max_tokens=max_tokens,
model=use_model,
temperature=temperature,
)
self._total_requests += 1
self._total_tokens += tokens_used
self._last_provider = provider.id
self._last_model = use_model
self._last_account_label = account.label
self.registry.record_usage(account.id, tokens_used)
logger.info(f"SmartRouter: OK — served by {provider.name} ({use_model}) after token re-extract")
return text
except Exception as retry_err:
logger.warning(f"SmartRouter: {provider.name} retry after re-extract also failed: {retry_err}")
last_error = retry_err
self.registry.deactivate_account(account.id)
self._failover_count += 1
if last_error is None:
last_error = e
logger.warning(f"SmartRouter: {provider.name}/{account.label} auth failed, deactivated")
except Exception as e:
self._failover_count += 1
last_error = e
logger.warning(f"SmartRouter: {provider.name}/{account.label} error: {e}")
raise RuntimeError(f"SmartRouter: All providers failed. Last error: {last_error}")
def _build_candidate_list(
self, preferred: Optional[str] = None
) -> List[Tuple[Provider, Account]]:
"""Build ordered list of (provider, account) candidates.
If preferred is set, ONLY that provider is used (no fallback to others).
This ensures the user's explicit choice is respected.
If preferred is not set, all providers are tried by tier.
"""
candidates = []
if preferred:
# Strict mode: only the preferred provider
provider = self.registry.get_provider(preferred)
if provider:
accounts = self.registry.get_active_accounts(preferred)
for acct in accounts:
if self.quota.is_available(acct.id):
candidates.append((provider, acct))
if not candidates:
logger.warning(
f"SmartRouter: Preferred provider '{preferred}' has no active accounts! "
f"Falling back to all providers."
)
else:
return candidates # Only preferred provider candidates
# Auto mode or preferred has no active accounts: try all by tier
for tier in (1, 2, 3):
providers = self.registry.get_providers_by_tier(tier)
for provider in providers:
# Skip disabled providers
if not getattr(provider, "enabled", True):
continue
acct = self.quota.next_account(
provider.id,
self.registry.get_active_accounts(provider.id),
)
if acct:
candidates.append((provider, acct))
return candidates
async def _call_provider(
self,
provider: Provider,
account: Account,
prompt: str,
system: str,
max_tokens: int,
model: str,
temperature: float,
) -> Tuple[str, int]:
"""Make an API call to a provider. Returns (text, tokens_used)."""
credential = self.registry.get_credential(account.id)
if not credential:
raise AuthenticationError(f"No credential for {account.id}")
format_map = {
"anthropic": (self._build_anthropic_request, self._parse_anthropic_response),
"openai_compat": (self._build_openai_request, self._parse_openai_response),
"gemini": (self._build_gemini_request, self._parse_gemini_response),
"gemini_code_assist": (self._build_gemini_code_assist_request, self._parse_gemini_code_assist_response),
"ollama": (self._build_ollama_request, self._parse_ollama_response),
}
builder, parser = format_map.get(provider.api_format, (None, None))
if not builder:
raise ValueError(f"Unknown API format: {provider.api_format}")
url, headers, payload = builder(
base_url=provider.base_url,
credential=credential,
credential_type=account.credential_type,
prompt=prompt,
system=system,
max_tokens=max_tokens,
model=model,
temperature=temperature,
)
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.post(
url,
headers=headers,
json=payload,
timeout=aiohttp.ClientTimeout(total=120),
) as resp:
if resp.status == 429:
retry_after = int(resp.headers.get("Retry-After", "300"))
raise QuotaExhaustedError(
f"Rate limited by {provider.name}",
retry_after=retry_after,
)
if resp.status in (401, 403):
raise AuthenticationError(
f"Authentication failed for {provider.name}: {resp.status}"
)
if resp.status >= 400:
body = await resp.text()
raise RuntimeError(
f"{provider.name} returned {resp.status}: {body[:300]}"
)
data = await resp.json()
return parser(data)
# ── Format Builders ──────────────────────────────────────
def _build_anthropic_request(
self, base_url: str, credential: str, credential_type: str,
prompt: str, system: str, max_tokens: int, model: str, temperature: float,
) -> Tuple[str, Dict, Dict]:
url = f"{base_url}/v1/messages"
headers = {
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
}
if credential_type == "oauth":
headers["Authorization"] = f"Bearer {credential}"
else:
headers["x-api-key"] = credential
messages = [{"role": "user", "content": prompt}]
payload = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
}
if system:
payload["system"] = system
return url, headers, payload
def _build_openai_request(
self, base_url: str, credential: str, credential_type: str,
prompt: str, system: str, max_tokens: int, model: str, temperature: float,
) -> Tuple[str, Dict, Dict]:
url = f"{base_url}/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {credential}",
}
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
payload = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
}
return url, headers, payload
def _build_gemini_request(
self, base_url: str, credential: str, credential_type: str,
prompt: str, system: str, max_tokens: int, model: str, temperature: float,
) -> Tuple[str, Dict, Dict]:
if credential_type == "oauth":
url = f"{base_url}/models/{model}:generateContent"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {credential}",
}
else:
url = f"{base_url}/models/{model}:generateContent?key={credential}"
headers = {"Content-Type": "application/json"}
contents = [{"parts": [{"text": prompt}]}]
payload = {
"contents": contents,
"generationConfig": {
"maxOutputTokens": max_tokens,
"temperature": temperature,
},
}
if system:
payload["systemInstruction"] = {"parts": [{"text": system}]}
return url, headers, payload
def _build_gemini_code_assist_request(
self, base_url: str, credential: str, credential_type: str,
prompt: str, system: str, max_tokens: int, model: str, temperature: float,
) -> Tuple[str, Dict, Dict]:
"""Build request for Google Cloud Code Assist API (Gemini CLI endpoint)."""
url = f"{base_url}/v1internal:generateContent"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {credential}",
"User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"X-Goog-Api-Client": "gl-node/22.17.0",
}
inner_request: Dict[str, Any] = {
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
"generationConfig": {
"maxOutputTokens": max_tokens,
"temperature": temperature,
},
}
if system:
inner_request["systemInstruction"] = {"parts": [{"text": system}]}
payload: Dict[str, Any] = {
"model": model,
"request": inner_request,
}
# Include project ID if discovered
if self._gemini_project_id:
payload["project"] = self._gemini_project_id
return url, headers, payload
def _build_ollama_request(
self, base_url: str, credential: str, credential_type: str,
prompt: str, system: str, max_tokens: int, model: str, temperature: float,
) -> Tuple[str, Dict, Dict]:
url = f"{base_url}/api/generate"
headers = {"Content-Type": "application/json"}
payload = {
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"num_predict": max_tokens,
"temperature": temperature,
},
}
if system:
payload["system"] = system
return url, headers, payload
# ── Response Parsers ─────────────────────────────────────
def _parse_anthropic_response(self, data: Dict) -> Tuple[str, int]:
text = ""
for block in data.get("content", []):
if block.get("type") == "text":
text += block.get("text", "")
usage = data.get("usage", {})
tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
return text, tokens
def _parse_openai_response(self, data: Dict) -> Tuple[str, int]:
choices = data.get("choices", [])
text = choices[0]["message"]["content"] if choices else ""
usage = data.get("usage", {})
tokens = usage.get("total_tokens", 0)
return text, tokens
def _parse_gemini_response(self, data: Dict) -> Tuple[str, int]:
candidates = data.get("candidates", [])
text = ""
if candidates:
parts = candidates[0].get("content", {}).get("parts", [])
text = "".join(p.get("text", "") for p in parts)
usage = data.get("usageMetadata", {})
tokens = usage.get("totalTokenCount", 0)
return text, tokens
def _parse_gemini_code_assist_response(self, data: Dict) -> Tuple[str, int]:
"""Parse Cloud Code Assist API response (wraps standard Gemini in 'response' field)."""
response = data.get("response", data)
candidates = response.get("candidates", [])
text = ""
if candidates:
parts = candidates[0].get("content", {}).get("parts", [])
text = "".join(p.get("text", "") for p in parts)
usage = response.get("usageMetadata", {})
tokens = usage.get("totalTokenCount", 0)
return text, tokens
def _parse_ollama_response(self, data: Dict) -> Tuple[str, int]:
text = data.get("response", "")
# Ollama doesn't always return token counts
tokens = data.get("eval_count", 0) + data.get("prompt_eval_count", 0)
return text, tokens
# ── Gemini CLI Project Discovery ────────────────────────────
async def discover_gemini_project(self) -> Optional[str]:
"""Discover Gemini CLI project ID via loadCodeAssist API call."""
if self._gemini_project_id:
return self._gemini_project_id
provider = self.registry.get_provider("gemini_cli")
if not provider or not provider.accounts:
return None
account = next(iter(provider.accounts.values()), None)
if not account or not account.is_active:
return None
credential = self.registry.get_credential(account.id)
if not credential:
return None
try:
import aiohttp
url = f"{provider.base_url}/v1internal:loadCodeAssist"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {credential}",
"User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
}
payload = {
"metadata": {
"ideType": "IDE_UNSPECIFIED",
"platform": "PLATFORM_UNSPECIFIED",
"pluginType": "GEMINI",
}
}
async with aiohttp.ClientSession() as session:
async with session.post(
url, headers=headers, json=payload,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
if resp.status == 200:
data = await resp.json()
project_data = data.get("cloudaicompanionProject", {})
project_id = (
project_data.get("id", "")
if isinstance(project_data, dict)
else str(project_data)
)
if project_id:
self._gemini_project_id = project_id
logger.info(f"SmartRouter: Gemini CLI project: {project_id}")
return project_id
except Exception as e:
logger.debug(f"SmartRouter: Gemini project discovery failed: {e}")
return None
# ── CLI Token Recovery ─────────────────────────────────────
async def _try_reextract_cli_token(self, provider_id: str, account_id: str) -> bool:
"""Try to re-extract/refresh a CLI token when auth fails.
Strategy:
1. Re-extract from CLI credential files (may have been refreshed externally)
2. If refresh_token is available, try OAuth refresh
"""
# Strategy 1: Re-extract from disk
try:
from .token_extractor import TokenExtractor
extractor = TokenExtractor()
token = extractor.detect(provider_id)
if token and token.token:
self.registry.update_credential(account_id, token.token, token.expires_at)
if token.refresh_token:
self.registry._refresh_tokens[account_id] = token.refresh_token
self.registry.reactivate_account(account_id)
logger.info(f"SmartRouter: Re-extracted CLI token for {provider_id}")
return True
except Exception as e:
logger.debug(f"SmartRouter: CLI re-extraction failed for {provider_id}: {e}")
# Strategy 2: OAuth refresh if we have a refresh_token
refresh_token = self.registry.get_refresh_token(account_id)
if refresh_token:
try:
from .token_refresher import TokenRefresher
refresher = TokenRefresher(self.registry)
success = await refresher._refresh_token(provider_id, account_id, refresh_token)
if success:
self.registry.reactivate_account(account_id)
logger.info(f"SmartRouter: OAuth-refreshed token for {provider_id}")
return True
except Exception as e:
logger.debug(f"SmartRouter: OAuth refresh failed for {provider_id}: {e}")
return False
# ── Testing & Status ─────────────────────────────────────
async def test_account(
self, provider_id: str, account_id: str
) -> Tuple[bool, str]:
"""Test connectivity for a specific account. Returns (success, message)."""
provider = self.registry.get_provider(provider_id)
if not provider:
return False, f"Unknown provider: {provider_id}"
account = provider.accounts.get(account_id)
if not account:
return False, f"Unknown account: {account_id}"
try:
text, tokens = await self._call_provider(
provider=provider,
account=account,
prompt="Say 'OK' and nothing else.",
system="",
max_tokens=10,
model=account.model_override or provider.default_model,
temperature=0,
)
return True, f"Connected. Response: {text[:50]}"
except Exception as e:
return False, str(e)
def get_status(self) -> Dict:
"""Get router statistics."""
return {
"total_requests": self._total_requests,
"total_tokens": self._total_tokens,
"failover_count": self._failover_count,
"last_provider": self._last_provider,
"last_model": self._last_model,
"last_account": self._last_account_label,
"providers": self.registry.get_all_status(),
}
# ── Custom Exceptions ────────────────────────────────────────
class QuotaExhaustedError(Exception):
def __init__(self, message: str, retry_after: int = 300):
super().__init__(message)
self.retry_after = retry_after
class AuthenticationError(Exception):
pass