Coverage for src/alprina_cli/security_engine.py: 24%
139 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2Alprina Security Engine.
3AI-powered vulnerability detection and security analysis.
4Built on Alprina's proprietary security agent framework.
5"""
7import os
8import hashlib
9import asyncio
10from pathlib import Path
11from typing import Dict, Any, List, Optional
12from loguru import logger
14from .llm_provider import get_llm_client
16# Import Alprina security agents
17try:
18 from .agents.red_teamer import run_red_team_scan
19 from .agents.blue_teamer import run_blue_team_scan
20 from .agents.network_analyzer import run_network_analyzer_scan
21 from .agents.reverse_engineer import run_reverse_engineer_scan
22 from .agents.dfir import run_dfir_scan
23 from .agents.android_sast import run_android_sast_scan
24 from .agents.memory_analysis import run_memory_analysis_scan
25 from .agents.wifi_security import run_wifi_security_scan
26 from .agents.replay_attack import run_replay_attack_scan
27 from .agents.subghz_sdr import run_subghz_sdr_scan
28 from .agents.retester import run_retester_scan
29 from .agents.mail import run_mail_scan
30 from .agents.guardrails import run_guardrails_scan
31 AGENTS_AVAILABLE = True
32 logger.info("Alprina security engine initialized successfully")
33except ImportError as e:
34 AGENTS_AVAILABLE = False
35 logger.error(f"Alprina agents not available: {e}")
36 logger.warning("Using fallback LLM analysis methods")
38# Task to Alprina agent mapping
39AGENT_MAPPING = {
40 "offensive-security": run_red_team_scan,
41 "defensive-security": run_blue_team_scan,
42 "network-analysis": run_network_analyzer_scan,
43 "binary-analysis": run_reverse_engineer_scan,
44 "forensics": run_dfir_scan,
45 "android-scan": run_android_sast_scan,
46 "memory-forensics": run_memory_analysis_scan,
47 "wifi-test": run_wifi_security_scan,
48 "replay-check": run_replay_attack_scan,
49 "radio-security": run_subghz_sdr_scan,
50 "retest": run_retester_scan,
51 "email-report": run_mail_scan,
52 "safety-check": run_guardrails_scan,
53 # Aliases for common tasks
54 "code-audit": run_red_team_scan,
55 "web-recon": run_network_analyzer_scan,
56 "vuln-scan": run_red_team_scan,
57 "secret-detection": run_red_team_scan,
58 "config-audit": run_blue_team_scan
59}
62def run_agent(task: str, input_data: str, metadata: dict) -> dict:
63 """
64 Run an Alprina security agent for vulnerability analysis.
66 Args:
67 task: Task/agent type (e.g., 'code-audit', 'web-recon', 'vuln-scan')
68 input_data: Input data for the agent
69 metadata: Additional metadata for the scan
71 Returns:
72 Dict containing scan results
73 """
74 logger.info(f"Running Alprina agent: {task}")
75 logger.debug(f"Input length: {len(input_data)} chars")
76 logger.debug(f"Metadata: {metadata}")
78 # Use Alprina agents if available
79 if AGENTS_AVAILABLE and task in AGENT_MAPPING:
80 logger.info(f"Using Alprina agent for task: {task}")
81 agent_func = AGENT_MAPPING[task]
82 result = agent_func(
83 target=metadata.get("path", input_data),
84 safe_only=metadata.get("safe_only", True)
85 )
86 return result
88 # Fallback to LLM-based analysis if agents not available
89 logger.warning(f"Agent '{task}' not found in mapping, falling back to LLM analysis")
90 return _run_llm_analysis(task, input_data, metadata)
93def _run_llm_analysis(task: str, input_data: str, metadata: dict) -> dict:
94 """
95 Build analysis prompt for Alprina agent.
97 Args:
98 task: Task type
99 content: Code/content to analyze
100 metadata: Additional context
102 Returns:
103 Formatted prompt string
104 """
105 file_info = metadata.get("file", "unknown file")
106 safe_only = metadata.get("safe_only", True)
108 prompts = {
109 "code-audit": f"""Perform a comprehensive security audit of this code.
111File: {file_info}
112Safe Mode: {safe_only}
114Code to analyze:
115```
116{content[:5000]} # Limit to 5000 chars for efficiency
117```
119Please identify:
1201. Security vulnerabilities (SQL injection, XSS, etc.)
1212. Hardcoded secrets (API keys, passwords, tokens)
1223. Insecure configurations
1234. Authentication/authorization issues
1245. Input validation problems
1256. Cryptographic weaknesses
127For each finding, provide:
128- Severity (HIGH/MEDIUM/LOW)
129- Type/Category
130- Description
131- Location (line number if possible)
132- Remediation steps
134Format your response as a structured analysis.""",
136 "secret-detection": f"""Scan this code for hardcoded secrets and sensitive information.
138File: {file_info}
140Code:
141```
142{content[:5000]}
143```
145Look for:
146- API keys
147- Passwords
148- Tokens
149- Private keys
150- Database credentials
151- AWS/Cloud credentials
153Report each finding with severity and location.""",
155 "config-audit": f"""Audit this configuration file for security issues.
157File: {file_info}
159Configuration:
160```
161{content[:5000]}
162```
164Check for:
165- Insecure settings
166- Default credentials
167- Exposed secrets
168- Weak permissions
169- Security misconfigurations
171Provide remediation for each issue."""
172 }
174 return prompts.get(task, prompts["code-audit"])
177def _parse_alprina_result(result: str, metadata: dict) -> List[Dict[str, Any]]:
178 """
179 Parse Alprina agent result into structured findings.
181 Args:
182 result: Agent response string
183 metadata: Original metadata
185 Returns:
186 List of finding dictionaries
187 """
188 findings = []
190 # Simple parsing - look for severity keywords and extract findings
191 # This is a basic parser - can be enhanced based on actual Alprina agent output format
193 lines = result.split('\n')
194 current_finding = None
196 for line in lines:
197 line_lower = line.lower()
199 # Check for severity markers
200 if any(sev in line_lower for sev in ['high', 'critical', 'severe']):
201 if current_finding:
202 findings.append(current_finding)
203 current_finding = {
204 "severity": "HIGH",
205 "type": "Security Issue",
206 "description": line.strip(),
207 "location": metadata.get("file", "unknown"),
208 "line": None
209 }
210 elif any(sev in line_lower for sev in ['medium', 'moderate']):
211 if current_finding:
212 findings.append(current_finding)
213 current_finding = {
214 "severity": "MEDIUM",
215 "type": "Security Issue",
216 "description": line.strip(),
217 "location": metadata.get("file", "unknown"),
218 "line": None
219 }
220 elif any(sev in line_lower for sev in ['low', 'minor', 'info']):
221 if current_finding:
222 findings.append(current_finding)
223 current_finding = {
224 "severity": "LOW",
225 "type": "Security Issue",
226 "description": line.strip(),
227 "location": metadata.get("file", "unknown"),
228 "line": None
229 }
230 elif current_finding and line.strip():
231 # Add to current finding description
232 current_finding["description"] += " " + line.strip()
234 if current_finding:
235 findings.append(current_finding)
237 # If no findings parsed, create a summary finding
238 if not findings and len(result) > 50:
239 findings.append({
240 "severity": "INFO",
241 "type": "Analysis Complete",
242 "description": result[:500], # First 500 chars
243 "location": metadata.get("file", "unknown"),
244 "line": None
245 })
247 return findings
250def _run_llm_analysis(task: str, input_data: str, metadata: dict) -> dict:
251 """
252 Fallback LLM-based analysis when CAI not available.
254 Args:
255 task: Analysis task
256 input_data: Content to analyze
257 metadata: Additional context
259 Returns:
260 Dict with findings
261 """
262 logger.info("Using LLM fallback analysis")
264 try:
265 llm_client = get_llm_client()
267 if task in ["code-audit", "secret-detection", "config-audit"]:
268 filename = metadata.get("file", "unknown")
269 result = llm_client.analyze_code(input_data, filename, task)
270 return result
271 else:
272 # For other tasks, use basic pattern matching
273 return _pattern_based_analysis(input_data, metadata)
275 except Exception as e:
276 logger.error(f"LLM analysis error: {e}")
277 return _pattern_based_analysis(input_data, metadata)
280def _pattern_based_analysis(content: str, metadata: dict) -> dict:
281 """
282 Basic pattern-based security analysis as last resort fallback.
284 Args:
285 content: Content to analyze
286 metadata: Context metadata
288 Returns:
289 Dict with findings
290 """
291 findings = []
293 # Pattern-based detection
294 patterns = {
295 "Hardcoded Secret": [
296 (r"password\s*=\s*['\"][^'\"]+['\"]", "HIGH"),
297 (r"api_key\s*=\s*['\"][^'\"]+['\"]", "HIGH"),
298 (r"secret\s*=\s*['\"][^'\"]+['\"]", "HIGH"),
299 (r"token\s*=\s*['\"][^'\"]+['\"]", "MEDIUM"),
300 ],
301 "Debug Mode": [
302 (r"debug\s*=\s*true", "MEDIUM"),
303 (r"DEBUG\s*=\s*True", "MEDIUM"),
304 ],
305 "SQL Injection Risk": [
306 (r"execute\(['\"].*%s.*['\"]", "HIGH"),
307 (r"execute\(['\"].*\+.*['\"]", "HIGH"),
308 ],
309 "Insecure Function": [
310 (r"eval\(", "HIGH"),
311 (r"exec\(", "HIGH"),
312 ]
313 }
315 import re
317 for vuln_type, pattern_list in patterns.items():
318 for pattern, severity in pattern_list:
319 matches = re.finditer(pattern, content, re.IGNORECASE)
320 for match in matches:
321 findings.append({
322 "severity": severity,
323 "type": vuln_type,
324 "description": f"Pattern detected: {match.group(0)[:100]}",
325 "location": metadata.get("file", "unknown"),
326 "line": content[:match.start()].count('\n') + 1
327 })
329 # Environment file check
330 if ".env" in metadata.get("file", ""):
331 findings.append({
332 "severity": "LOW",
333 "type": "Environment File",
334 "description": "Environment file detected - ensure it's in .gitignore",
335 "location": metadata.get("file", "unknown"),
336 "line": None
337 })
339 return {
340 "findings": findings,
341 "metadata": metadata,
342 "alprina_enabled": False,
343 "analysis_method": "pattern_based"
344 }
347def run_local_scan(path: str, profile: str = "code-audit", safe_only: bool = True) -> dict:
348 """
349 Scan local files/directories for security issues using Alprina agents.
351 Args:
352 path: Path to file or directory
353 profile: Scan profile to use
354 safe_only: Only run safe, non-intrusive checks
356 Returns:
357 Dict containing scan results
358 """
359 logger.info(f"Starting local scan: {path} (Engine: {'active' if AGENTS_AVAILABLE else 'fallback'})")
361 target_path = Path(path)
363 if not target_path.exists():
364 raise FileNotFoundError(f"Path does not exist: {path}")
366 # Collect files to scan
367 if target_path.is_file():
368 files = [target_path]
369 else:
370 files = _collect_scannable_files(target_path)
372 logger.info(f"Found {len(files)} files to scan")
374 results = {
375 "mode": "local",
376 "target": str(path),
377 "profile": profile,
378 "files_scanned": len(files),
379 "findings": [],
380 "alprina_engine": "active" if AGENTS_AVAILABLE else "fallback"
381 }
383 # Scan each file
384 for i, file_path in enumerate(files, 1):
385 try:
386 logger.info(f"Scanning file {i}/{len(files)}: {file_path.name}")
387 file_results = _scan_file(file_path, profile, safe_only)
388 results["findings"].extend(file_results)
389 except Exception as e:
390 logger.error(f"Error scanning {file_path}: {e}")
392 logger.info(f"Scan complete: {len(results['findings'])} findings")
393 return results
396def run_remote_scan(target: str, profile: str = "web-recon", safe_only: bool = True) -> dict:
397 """
398 Scan remote target (URL, domain, or IP) using Alprina security agents.
400 Args:
401 target: Target URL, domain, or IP
402 profile: Scan profile to use
403 safe_only: Only run safe, non-intrusive checks
405 Returns:
406 Dict containing scan results
407 """
408 logger.info(f"Starting remote scan: {target} (Engine: {'active' if AGENTS_AVAILABLE else 'fallback'})")
410 # Use Alprina security agent for remote scanning
411 results = run_agent(
412 task=profile,
413 input_data=target,
414 metadata={
415 "safe_only": safe_only,
416 "mode": "remote"
417 }
418 )
420 return {
421 "mode": "remote",
422 "target": target,
423 "profile": profile,
424 "alprina_engine": "active" if AGENTS_AVAILABLE else "fallback",
425 **results
426 }
429def _collect_scannable_files(directory: Path) -> List[Path]:
430 """
431 Collect files that should be scanned for security issues.
432 """
433 scannable_extensions = (
434 ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rs",
435 ".env", ".yaml", ".yml", ".json", ".xml", ".ini", ".conf",
436 ".sh", ".bash", ".zsh", ".dockerfile", ".tf", ".hcl", ".php",
437 ".rb", ".c", ".cpp", ".h", ".cs", ".swift", ".kt"
438 )
440 scannable_names = (
441 "Dockerfile", "Makefile", "docker-compose.yml", "docker-compose.yaml",
442 ".env", ".env.local", ".env.production", ".env.development",
443 "config.json", "settings.json", "secrets.json"
444 )
446 files = []
448 for item in directory.rglob("*"):
449 if item.is_file():
450 # Check by extension or name
451 if item.suffix.lower() in scannable_extensions or item.name in scannable_names:
452 # Skip common directories
453 if not any(part.startswith(".") or part in ["node_modules", "venv", "__pycache__", "dist", "build", "target"]
454 for part in item.parts):
455 files.append(item)
457 return files
460def _scan_file(file_path: Path, profile: str, safe_only: bool) -> List[Dict[str, Any]]:
461 """
462 Scan a single file for security issues using Alprina agents.
463 """
464 findings = []
466 try:
467 content = file_path.read_text(errors="ignore")
468 file_hash = hashlib.md5(content.encode()).hexdigest()
470 # Run Alprina agent on file content
471 result = run_agent(
472 task=profile,
473 input_data=content,
474 metadata={
475 "file": str(file_path),
476 "hash": file_hash,
477 "safe_only": safe_only,
478 "file_type": file_path.suffix
479 }
480 )
482 # Extract findings from result
483 if "findings" in result:
484 for finding in result["findings"]:
485 finding["location"] = str(file_path)
486 findings.append(finding)
488 except Exception as e:
489 logger.error(f"Error scanning file {file_path}: {e}")
491 return findings