Spaces:
Running
Running
| import asyncio | |
| import json | |
| import logging | |
| import os | |
| from typing import Any, Dict, List, Optional | |
| from ..core.models import Finding, Severity | |
| from core.llm_service import LLMService | |
| logger = logging.getLogger(__name__) | |
| class LLMAnalyzer: | |
| """ | |
| Open-source LLM analyzer for security scanning. | |
| Supports: | |
| - BYOK (Bring Your Own Key) for OpenAI and Anthropic. | |
| - Local CPU inference for privacy and offline use. | |
| """ | |
| def __init__( | |
| self, | |
| mode: str = "local", | |
| model: Optional[str] = None, | |
| api_key: Optional[str] = None, | |
| provider: Optional[str] = None | |
| ): | |
| """ | |
| Initialize the LLM Analyzer. | |
| Args: | |
| mode: "local" or "byok" | |
| model: Model name (e.g., "gpt-4o", "claude-3-5-sonnet", or local model path) | |
| api_key: API key for the provider | |
| provider: "openai" or "anthropic" (for byok mode) | |
| """ | |
| self.mode = mode | |
| self.model = model or ("Qwen/Qwen2.5-1.5B-Instruct" if mode == "local" else "gpt-4o") | |
| self.api_key = api_key or os.getenv("ATOM_SECURITY_LLM_API_KEY") | |
| self.provider = provider or os.getenv("ATOM_SECURITY_LLM_PROVIDER", "openai") | |
| self.pipeline = None | |
| # Initialize LLMService for unified LLM interactions (replaces direct clients) | |
| self.llm_service = LLMService(workspace_id="default") | |
| if self.mode == "local": | |
| self._init_local() | |
| else: | |
| self._init_byok() | |
| def _init_local(self): | |
| """Initialize local transformers pipeline.""" | |
| try: | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline | |
| logger.info(f"Loading local model: {self.model}...") | |
| self.tokenizer = AutoTokenizer.from_pretrained(self.model, trust_remote_code=True) | |
| self.model_obj = AutoModelForCausalLM.from_pretrained( | |
| self.model, | |
| device_map="cpu", | |
| torch_dtype=torch.float32, | |
| trust_remote_code=True | |
| ) | |
| self.pipeline = pipeline( | |
| "text-generation", | |
| model=self.model_obj, | |
| tokenizer=self.tokenizer, | |
| max_new_tokens=512, | |
| temperature=0.1 | |
| ) | |
| except Exception as e: | |
| logger.error(f"Failed to load local model: {e}") | |
| raise | |
| def _init_byok(self): | |
| """ | |
| Initialize BYOK mode using LLMService. | |
| LLMService handles provider selection, API key resolution, | |
| and client creation internally via BYOKHandler. | |
| """ | |
| # LLMService initialized in __init__ handles all BYOK configuration | |
| # No direct client creation needed | |
| pass | |
| async def analyze(self, skill_name: str, content: str) -> List[Finding]: | |
| """Run analysis on skill content.""" | |
| system_prompt = ( | |
| "You are a security expert. Analyze the AI agent skill for:\n" | |
| "1. Prompt Injection\n2. Code Injection\n3. Data Exfiltration\n\n" | |
| "Return JSON: {\"findings\": [{\"category\": \"...\", \"severity\": \"...\", \"description\": \"...\"}]}" | |
| ) | |
| user_prompt = f"Skill: {skill_name}\n\nContent:\n{content[:4000]}" | |
| if self.mode == "local": | |
| return await self._analyze_local(system_prompt, user_prompt) | |
| else: | |
| return await self._analyze_byok(system_prompt, user_prompt) | |
| async def _analyze_local(self, system_prompt: str, user_prompt: str) -> List[Finding]: | |
| """Local inference.""" | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ] | |
| prompt = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
| outputs = await asyncio.to_thread(self.pipeline, prompt) | |
| text = outputs[0]["generated_text"].replace(prompt, "") | |
| return self._parse_json(text) | |
| async def _analyze_byok(self, system_prompt: str, user_prompt: str) -> List[Finding]: | |
| """ | |
| BYOK API call via LLMService. | |
| Uses unified LLMService interface for all providers (OpenAI, Anthropic). | |
| LLMService handles provider selection, API key resolution, and cost tracking. | |
| """ | |
| # Build messages in OpenAI format | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ] | |
| # Use model parameter (gpt-4o, claude-3-5-sonnet, etc.) | |
| # Note: response_format not supported yet, JSON mode requested in system prompt instead | |
| response = await self.llm_service.generate_completion( | |
| messages=messages, | |
| model=self.model, | |
| temperature=0.1, | |
| max_tokens=1024 | |
| ) | |
| # Extract content from LLMService response format | |
| text = response.get("content", "") | |
| return self._parse_json(text) | |
| def _parse_json(self, text: str) -> List[Finding]: | |
| """Parse findings from LLM output.""" | |
| try: | |
| # Simple cleanup for markdown | |
| if "```json" in text: | |
| text = text.split("```json")[1].split("```")[0] | |
| elif "```" in text: | |
| text = text.split("```")[1].split("```")[0] | |
| data = json.loads(text) | |
| findings = [] | |
| for f in data.get("findings", []): | |
| findings.append(Finding( | |
| rule_id=f.get("category", "LLM_DETECTED"), | |
| category=f.get("category", "OTHER"), | |
| severity=Severity(f.get("severity", "MEDIUM").upper()), | |
| title=f.get("category", "Security issue"), | |
| description=f.get("description", ""), | |
| analyzer="llm" | |
| )) | |
| return findings | |
| except Exception as e: | |
| logger.warning(f"Failed to parse LLM response: {e}") | |
| return [] | |