Coverage for src/alprina_cli/scanner.py: 8%
407 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"""
2Scanner module for Alprina CLI.
3Handles remote and local security scanning using Alprina security agents.
4"""
6from pathlib import Path
7from typing import Optional
8import httpx
9import os
10from rich.console import Console
11from rich.progress import Progress, SpinnerColumn, TextColumn
12from rich.panel import Panel
13from rich.table import Table
15from .auth import is_authenticated, get_auth_headers, get_backend_url
16from .policy import validate_target
17from .security_engine import run_remote_scan, run_local_scan
18from .reporting import write_event
19from .report_generator import generate_security_reports
20from .services.cve_service import enrich_findings
21from .services.container_scanner import get_container_scanner
23console = Console()
26def scan_command(
27 target: str,
28 profile: str = "default",
29 safe_only: bool = True,
30 output: Optional[Path] = None,
31 quick: bool = False,
32 container: bool = False,
33 agent: Optional[list[str]] = None,
34 verbose: bool = False,
35 # Week 4: Unified scanner parameters
36 all_analyzers: bool = False,
37 symbolic: bool = False,
38 mev: bool = False,
39 cross_contract: bool = False,
40 gas: bool = False, # Week 4 Day 3
41 tvl: Optional[float] = None,
42 protocol_type: Optional[str] = None,
43 output_format: str = "json"
44):
45 """
46 Execute a security scan on a target (remote, local, or container).
48 Args:
49 target: Target to scan (URL, IP, local path, or Docker image)
50 profile: Scan profile to use
51 safe_only: Only run safe, non-intrusive scans
52 output: Output file path
53 quick: Run quick 5-second scan for critical issues only
54 container: Scan as Docker container image
55 agent: Specific agent(s) to use
56 verbose: Show detailed output
57 all_analyzers: Run all security analyzers (Week 4)
58 symbolic: Run symbolic execution (Week 4)
59 mev: Run MEV detection (Week 4)
60 cross_contract: Run cross-contract analysis (Week 4)
61 tvl: Protocol TVL for economic impact (Week 4)
62 protocol_type: Protocol type (Week 4)
63 output_format: Output format (Week 4)
64 """
65 # Week 4: Smart contract unified scanner mode
66 if all_analyzers or symbolic or mev or cross_contract or gas:
67 _run_unified_scanner(
68 target, all_analyzers, symbolic, mev, cross_contract, gas,
69 tvl, protocol_type, output, output_format, verbose
70 )
71 return
73 # NEW: Container scan mode
74 if container:
75 _run_container_scan(target, output)
76 return
78 # NEW: Quick scan mode
79 if quick:
80 from .quick_scanner import quick_scan
81 _run_quick_scan(target)
82 return
84 # CRITICAL: Require authentication for ALL scans (local and remote)
85 # Alprina's powerful backend with LLMs/agents requires backend processing
86 if not is_authenticated():
87 console.print(Panel(
88 "[bold red]🔒 Authentication Required[/bold red]\n\n"
89 "Alprina requires authentication to scan.\n\n"
90 "[bold cyan]Get Started:[/bold cyan]\n"
91 " 1. Visit: [bold]https://alprina.com/signup[/bold]\n"
92 " 2. Choose your plan (Free or Pro)\n"
93 " 3. Run: [bold]alprina auth login[/bold]\n"
94 " 4. Start scanning!\n\n"
95 "[green]✨ Free Tier:[/green] 10 scans/month\n"
96 "[cyan]🚀 Pro Tier:[/cyan] Unlimited scans + advanced agents\n\n"
97 "[dim]Already have an account? Run:[/dim] [bold]alprina auth login[/bold]",
98 title="Welcome to Alprina",
99 border_style="cyan"
100 ))
101 return
103 # Check if target is local or remote
104 target_path = Path(target)
105 is_local = target_path.exists()
107 console.print(Panel(
108 f"🔍 Starting scan on: [bold]{target}[/bold]\n"
109 f"Profile: [cyan]{profile}[/cyan]\n"
110 f"Mode: {'[green]Safe only[/green]' if safe_only else '[yellow]Full scan[/yellow]'}",
111 title="Alprina Security Scan"
112 ))
114 scan_id = None
115 try:
116 # Create scan entry in database (if authenticated)
117 if is_authenticated():
118 scan_id = _create_scan_entry(target, "local" if is_local else "remote", profile)
119 if scan_id:
120 console.print(f"[dim]Scan ID: {scan_id}[/dim]")
122 # Execute scan with specific agents if requested
123 if agent:
124 console.print(f"[cyan]→[/cyan] Using specific agents: {', '.join(agent)}")
125 results = _scan_with_agents(target, agent, verbose)
126 elif is_local:
127 console.print(f"[cyan]→[/cyan] Detected local target: {target}")
128 results = _scan_local(target, profile, safe_only)
129 else:
130 console.print(f"[cyan]→[/cyan] Detected remote target: {target}")
131 validate_target(target) # Check against policy
132 results = _scan_remote(target, profile, safe_only)
134 # Enrich findings with CVE/CWE/CVSS data
135 if results.get("findings"):
136 console.print("[dim]→ Enriching findings with CVE/CWE/CVSS data...[/dim]")
137 results["findings"] = enrich_findings(results["findings"])
139 # Save results to database (if authenticated and scan was created)
140 if is_authenticated() and scan_id:
141 _save_scan_results(scan_id, results)
142 console.print(f"[dim]✓ Scan saved to your account[/dim]")
144 # Log the scan event locally
145 write_event({
146 "type": "scan",
147 "target": target,
148 "profile": profile,
149 "mode": "local" if is_local else "remote",
150 "safe_only": safe_only,
151 "findings_count": len(results.get("findings", []))
152 })
154 # Display results
155 _display_results(results)
157 # Generate markdown security reports in .alprina/ folder
158 if is_local and results.get("findings", []):
159 try:
160 report_dir = generate_security_reports(results, target)
161 console.print(f"\n[green]✓[/green] Security reports generated in: [cyan]{report_dir}[/cyan]")
162 console.print("[dim]Files created:[/dim]")
163 console.print("[dim] • SECURITY-REPORT.md - Full vulnerability analysis[/dim]")
164 console.print("[dim] • FINDINGS.md - Detailed findings with code context[/dim]")
165 console.print("[dim] • REMEDIATION.md - Step-by-step fix instructions[/dim]")
166 console.print("[dim] • EXECUTIVE-SUMMARY.md - Non-technical overview[/dim]")
167 except Exception as report_error:
168 console.print(f"[yellow]⚠️ Could not generate reports: {report_error}[/yellow]")
170 if output:
171 _save_results(results, output)
173 except Exception as e:
174 console.print(f"[red]Scan failed: {e}[/red]")
177def _scan_local(target: str, profile: str, safe_only: bool) -> dict:
178 """Execute local file/directory scan."""
179 with Progress(
180 SpinnerColumn(),
181 TextColumn("[progress.description]{task.description}"),
182 console=console
183 ) as progress:
184 task = progress.add_task("Scanning local files...", total=None)
186 results = run_local_scan(target, profile, safe_only)
188 progress.update(task, completed=True)
190 return results
193def _scan_remote(target: str, profile: str, safe_only: bool) -> dict:
194 """Execute remote target scan."""
195 with Progress(
196 SpinnerColumn(),
197 TextColumn("[progress.description]{task.description}"),
198 console=console
199 ) as progress:
200 task = progress.add_task("Scanning remote target...", total=None)
202 results = run_remote_scan(target, profile, safe_only)
204 progress.update(task, completed=True)
206 return results
209def _run_quick_scan(target: str):
210 """Execute quick security scan."""
211 from .quick_scanner import quick_scan
213 console.print(Panel(
214 f"⚡ Quick Health Check on: [bold]{target}[/bold]\n"
215 f"Scanning for top 10 critical patterns...\n"
216 f"[dim]This takes ~5 seconds[/dim]",
217 title="Alprina Quick Scan",
218 style="cyan"
219 ))
221 with Progress(
222 SpinnerColumn(),
223 TextColumn("[progress.description]{task.description}"),
224 console=console
225 ) as progress:
226 task = progress.add_task("Scanning files...", total=None)
227 results = quick_scan(target)
228 progress.update(task, completed=True)
230 _display_quick_results(results)
233def _display_quick_results(results: dict):
234 """Display quick scan results."""
235 duration = results['duration_ms'] / 1000
237 console.print(f"\n⚡ Quick scan completed in [bold cyan]{duration:.1f}s[/bold cyan]")
238 console.print(f" Scanned [bold]{results['summary']['total_files_scanned']}[/bold] files")
240 critical = results['summary']['critical']
242 if critical == 0:
243 console.print("\n✅ [bold green]No critical issues found![/bold green]")
244 console.print("\n💡 [dim]Run full scan for comprehensive analysis:[/dim]")
245 console.print(" [bold cyan]alprina scan ./ [/bold cyan]")
246 else:
247 console.print(f"\n🚨 [bold red]Found {critical} critical issue{'s' if critical != 1 else ''}[/bold red]")
249 # Show first 5 findings
250 table = Table(show_header=True, header_style="bold magenta")
251 table.add_column("Issue", style="red", width=30)
252 table.add_column("File", style="cyan", width=25)
253 table.add_column("Line", justify="right", style="yellow", width=6)
255 for finding in results['findings'][:5]:
256 file_name = Path(finding['file']).name
257 table.add_row(
258 finding['title'],
259 file_name,
260 str(finding['line'])
261 )
263 console.print(table)
265 if len(results['findings']) > 5:
266 console.print(f"\n[dim]+ {len(results['findings']) - 5} more issues...[/dim]")
268 console.print("\n⚠️ [yellow]Quick scan only checks critical patterns[/yellow]")
269 console.print(" Run full scan to find all vulnerabilities:")
270 console.print(" [bold cyan]alprina scan ./[/bold cyan]")
273def _display_results(results: dict):
274 """Display scan results in a formatted table with CVE/CWE/CVSS data."""
275 findings = results.get("findings", [])
277 if not findings:
278 console.print("\n[green]✓ No security issues found![/green]")
279 return
281 console.print(f"\n[yellow]⚠ Found {len(findings)} issues[/yellow]\n")
283 table = Table(title="Security Findings", show_header=True, header_style="bold cyan")
284 table.add_column("Severity", style="bold", width=10)
285 table.add_column("Type", width=25)
286 table.add_column("CVSS", justify="right", width=6)
287 table.add_column("CWE", width=12)
288 table.add_column("Description", width=40)
289 table.add_column("Location", width=25)
291 severity_colors = {
292 "CRITICAL": "bold red",
293 "HIGH": "red",
294 "MEDIUM": "yellow",
295 "LOW": "blue",
296 "INFO": "dim"
297 }
299 for finding in findings:
300 severity = finding.get("severity", "INFO")
301 color = severity_colors.get(severity, "white")
303 # Get CVSS score
304 cvss = finding.get("cvss_score")
305 cvss_str = f"{cvss:.1f}" if cvss else "N/A"
307 # Get CWE
308 cwe = finding.get("cwe", "")
309 cwe_num = cwe.split("-")[1] if cwe and "-" in cwe else ""
311 table.add_row(
312 f"[{color}]{severity}[/{color}]",
313 finding.get("type", "Unknown"),
314 f"[{color}]{cvss_str}[/{color}]",
315 f"[cyan]{cwe_num}[/cyan]" if cwe_num else "[dim]N/A[/dim]",
316 finding.get("description", "N/A"),
317 finding.get("location", "N/A")
318 )
320 console.print(table)
322 # Show enhanced details for top 3 findings
323 console.print("\n[bold cyan]📋 Detailed Analysis (Top 3)[/bold cyan]\n")
325 for i, finding in enumerate(findings[:3], 1):
326 severity = finding.get("severity", "INFO")
327 color = severity_colors.get(severity, "white")
329 console.print(f"[bold]{i}. [{color}]{severity}[/{color}]: {finding.get('type', 'Issue')}[/bold]")
330 console.print(f" 📍 {finding.get('location', 'N/A')}")
332 if finding.get("cvss_score"):
333 console.print(f" 📊 CVSS: {finding['cvss_score']:.1f}/10.0 ({finding.get('cvss_severity', 'N/A')})")
335 if finding.get("cwe"):
336 cwe_name = finding.get("cwe_name", finding["cwe"])
337 console.print(f" 🔖 {finding['cwe']}: {cwe_name}")
339 if finding.get("owasp"):
340 console.print(f" ⚡ OWASP: {finding['owasp']}")
342 if finding.get("references"):
343 console.print(" 🔗 References:")
344 for ref in finding["references"][:3]:
345 console.print(f" • {ref['name']}: [link={ref['url']}]{ref['url']}[/link]")
347 console.print()
350def _save_results(results: dict, output: Path):
351 """Save scan results to file."""
352 import json
354 output.parent.mkdir(parents=True, exist_ok=True)
356 with open(output, "w") as f:
357 json.dump(results, f, indent=2)
359 console.print(f"\n[green]✓[/green] Results saved to: {output}")
362def recon_command(target: str, passive: bool = True):
363 """
364 Perform reconnaissance on a target.
365 """
366 if not is_authenticated():
367 console.print("[red]Please login first: alprina auth login[/red]")
368 return
370 console.print(Panel(
371 f"🕵️ Reconnaissance: [bold]{target}[/bold]\n"
372 f"Mode: {'[green]Passive[/green]' if passive else '[yellow]Active[/yellow]'}",
373 title="Alprina Recon"
374 ))
376 try:
377 validate_target(target)
379 with Progress(
380 SpinnerColumn(),
381 TextColumn("[progress.description]{task.description}"),
382 console=console
383 ) as progress:
384 task = progress.add_task("Gathering intelligence...", total=None)
386 # Use Alprina security agent for reconnaissance
387 from .security_engine import run_agent
389 results = run_agent(
390 task="web-recon",
391 input_data=target,
392 metadata={"passive": passive}
393 )
395 progress.update(task, completed=True)
397 # Log event
398 write_event({
399 "type": "recon",
400 "target": target,
401 "passive": passive,
402 "findings_count": len(results.get("findings", []))
403 })
405 console.print("\n[green]✓ Reconnaissance complete[/green]")
406 _display_results(results)
408 except Exception as e:
409 console.print(f"[red]Recon failed: {e}[/red]")
412def _create_scan_entry(target: str, scan_type: str, profile: str) -> Optional[str]:
413 """Create a scan entry in the database before execution."""
414 try:
415 headers = get_auth_headers()
416 backend_url = get_backend_url()
418 response = httpx.post(
419 f"{backend_url}/scans",
420 headers=headers,
421 json={
422 "target": target,
423 "scan_type": scan_type,
424 "profile": profile
425 },
426 timeout=10.0
427 )
429 if response.status_code == 201:
430 data = response.json()
431 return data.get("scan_id")
432 else:
433 console.print(f"[yellow]⚠️ Could not create scan entry: {response.status_code}[/yellow]")
434 return None
436 except Exception as e:
437 console.print(f"[yellow]⚠️ Could not create scan entry: {e}[/yellow]")
438 return None
441def _save_scan_results(scan_id: str, results: dict):
442 """Save scan results to the database after completion."""
443 try:
444 headers = get_auth_headers()
445 backend_url = get_backend_url()
447 response = httpx.patch(
448 f"{backend_url}/scans/{scan_id}",
449 headers=headers,
450 json={"results": results},
451 timeout=30.0
452 )
454 if response.status_code != 200:
455 console.print(f"[yellow]⚠️ Could not save scan results: {response.status_code}[/yellow]")
457 except Exception as e:
458 console.print(f"[yellow]⚠️ Could not save scan results: {e}[/yellow]")
461def _scan_with_agents(target: str, agents: list[str], verbose: bool = False) -> dict:
462 """Execute scan with specific agents."""
463 from .utils.agent_loader import get_local_agent
465 console.print(Panel(
466 f"🔧 Agent-Specific Security Scan\n\n"
467 f"Target: [bold]{target}[/bold]\n"
468 f"Agents: [cyan]{', '.join(agents)}[/cyan]",
469 title="Alprina Agent Scan",
470 style="cyan"
471 ))
473 all_results = []
475 for agent_name in agents:
476 try:
477 # Get the agent instance
478 agent = get_local_agent(agent_name)
479 if not agent:
480 console.print(f"[yellow]⚠️ Agent '{agent_name}' not available, skipping...[/yellow]")
481 continue
483 console.print(f"[cyan]→[/cyan] Running {agent_name}...")
485 # Run the agent
486 result = agent.analyze(target)
487 all_results.append(result)
489 if verbose:
490 console.print(f"[green]✓[/green] {agent_name} completed: {len(result.get('vulnerabilities', []))} findings")
492 except Exception as e:
493 console.print(f"[red]✗[/red] {agent_name} failed: {e}")
495 # Combine results from all agents
496 combined_results = {
497 "mode": "agent-specific",
498 "target": target,
499 "agents_used": agents,
500 "findings": [],
501 "agent_results": all_results
502 }
504 # Aggregate findings from all agents
505 for result in all_results:
506 if result.get('status') == 'success':
507 combined_results["findings"].extend(result.get('vulnerabilities', []))
509 return combined_results
512def _run_container_scan(image: str, output: Optional[Path] = None):
513 """Execute container security scan with Trivy."""
514 console.print(Panel(
515 f"🐳 Container Security Scan\n\n"
516 f"Image: [bold]{image}[/bold]\n"
517 f"Scanner: [cyan]Trivy (Aqua Security)[/cyan]",
518 title="Alprina Container Scan",
519 style="cyan"
520 ))
522 scanner = get_container_scanner()
524 with Progress(
525 SpinnerColumn(),
526 TextColumn("[progress.description]{task.description}"),
527 console=console
528 ) as progress:
529 task = progress.add_task("Scanning container image...", total=None)
531 # Scan the image
532 results = scanner.scan_image(image)
534 progress.update(task, completed=True)
536 if not results["success"]:
537 console.print(f"[red]✗ Container scan failed: {results.get('error')}[/red]")
539 if "not installed" in results.get("error", ""):
540 console.print("\n[yellow]📦 Installation Required:[/yellow]")
541 console.print(f" {results.get('install_command', '')}")
542 console.print(f"\nDocumentation: {results.get('install_url', '')}")
544 return
546 console.print("[green]✓ Container scan complete![/green]\n")
548 # Display summary
549 _display_container_results(results)
551 # Save results if output specified
552 if output:
553 import json
554 output.parent.mkdir(parents=True, exist_ok=True)
555 with open(output, 'w') as f:
556 json.dump(results, f, indent=2)
557 console.print(f"\n[green]✓[/green] Results saved to: {output}")
560def _display_container_results(results: dict):
561 """Display container scan results."""
562 summary = results.get("summary", {})
563 image = results.get("image", "unknown")
565 # Summary table
566 table = Table(title=f"Scan Results: {image}", show_header=False, box=None)
568 table.add_row("📦 Image:", f"[bold]{image}[/bold]")
569 table.add_row("🔍 Vulnerabilities:", f"[bold]{summary.get('total_vulnerabilities', 0)}[/bold]")
571 # Severity breakdown
572 by_severity = summary.get("by_severity", {})
573 critical = by_severity.get("CRITICAL", 0)
574 high = by_severity.get("HIGH", 0)
575 medium = by_severity.get("MEDIUM", 0)
576 low = by_severity.get("LOW", 0)
578 if critical > 0:
579 table.add_row(" 🔴 Critical:", f"[red bold]{critical}[/red bold]")
580 if high > 0:
581 table.add_row(" 🟠 High:", f"[red]{high}[/red]")
582 if medium > 0:
583 table.add_row(" 🟡 Medium:", f"[yellow]{medium}[/yellow]")
584 if low > 0:
585 table.add_row(" 🔵 Low:", f"[blue]{low}[/blue]")
587 if summary.get("secrets_found", 0) > 0:
588 table.add_row("🔐 Secrets Found:", f"[red bold]{summary['secrets_found']}[/red bold]")
590 if summary.get("packages_scanned", 0) > 0:
591 table.add_row("📦 Packages Scanned:", str(summary["packages_scanned"]))
593 if summary.get("layers", 0) > 0:
594 table.add_row("🗂️ Image Layers:", str(summary["layers"]))
596 console.print(table)
598 # Recommendations
599 recommendations = results.get("recommendations", [])
600 if recommendations:
601 console.print("\n[bold cyan]💡 Recommendations:[/bold cyan]")
602 for rec in recommendations:
603 console.print(f" {rec}")
605 # Risk assessment
606 if critical > 0 or high > 0:
607 console.print("\n[bold red]⚠️ HIGH RISK[/bold red]")
608 console.print("This image has critical security issues. Update immediately.")
609 elif medium > 0:
610 console.print("\n[bold yellow]⚠️ MEDIUM RISK[/bold yellow]")
611 console.print("Plan security updates within 1-2 weeks.")
612 else:
613 console.print("\n[bold green]✅ LOW RISK[/bold green]")
614 console.print("No critical issues found. Continue monitoring.")
617def _run_unified_scanner(
618 target: str,
619 all_analyzers: bool,
620 symbolic: bool,
621 mev: bool,
622 cross_contract: bool,
623 gas: bool,
624 tvl: Optional[float],
625 protocol_type: Optional[str],
626 output: Optional[Path],
627 output_format: str,
628 verbose: bool
629):
630 """
631 Run unified scanner for smart contract security analysis (Week 4)
633 Args:
634 target: Path to Solidity contract file or directory
635 all_analyzers: Run all analyzers
636 symbolic: Run symbolic execution
637 mev: Run MEV detection
638 cross_contract: Run cross-contract analysis
639 gas: Run gas optimization analysis
640 tvl: Protocol TVL for economic impact
641 protocol_type: Protocol type (dex, lending, etc.)
642 output: Output file path
643 output_format: Output format (json, markdown, html, text)
644 verbose: Show detailed output
645 """
646 from .unified_scanner import UnifiedScanner, ScanOptions
648 target_path = Path(target)
650 if not target_path.exists():
651 console.print(f"[bold red]Error:[/bold red] Target not found: {target}")
652 return
654 # Determine if single file or directory
655 if target_path.is_file():
656 if not target_path.suffix == '.sol':
657 console.print(f"[bold yellow]Warning:[/bold yellow] Target is not a Solidity file (.sol)")
658 console.print("Unified scanner is optimized for smart contract analysis.")
659 return
661 # Single file scan
662 contract_code = target_path.read_text()
663 file_name = target_path.name
665 # Create scan options
666 options = ScanOptions(
667 run_all=all_analyzers,
668 symbolic=symbolic,
669 mev=mev,
670 cross_contract=False, # Single file can't do cross-contract
671 gas_optimization=gas,
672 calculate_economic_impact=(tvl is not None),
673 tvl=tvl,
674 protocol_type=protocol_type,
675 output_file=str(output) if output else None,
676 output_format=output_format,
677 verbose=verbose,
678 parallel=True
679 )
681 # Run scan
682 scanner = UnifiedScanner()
684 if verbose:
685 console.print(f"\n🔍 [bold]Alprina Unified Security Scanner[/bold]")
686 console.print(f"{'='*60}")
687 console.print(f"Contract: [cyan]{file_name}[/cyan]")
688 if tvl:
689 console.print(f"Protocol: [cyan]{protocol_type or 'generic'}[/cyan] (TVL: ${tvl:,.0f})")
690 console.print()
692 with Progress(
693 SpinnerColumn(),
694 TextColumn("[progress.description]{task.description}"),
695 console=console
696 ) as progress:
697 task = progress.add_task("Running security analysis...", total=None)
699 report = scanner.scan(contract_code, str(target_path), options)
701 progress.update(task, completed=True)
703 # Display results
704 _display_unified_results(report, verbose)
706 elif target_path.is_dir():
707 # Directory scan - find all .sol files
708 sol_files = list(target_path.glob("**/*.sol"))
710 if not sol_files:
711 console.print(f"[bold yellow]Warning:[/bold yellow] No Solidity files found in {target}")
712 return
714 console.print(f"[cyan]→[/cyan] Found {len(sol_files)} Solidity files")
716 if cross_contract and len(sol_files) > 1:
717 # Multi-contract analysis
718 contracts = {}
719 for sol_file in sol_files:
720 contract_name = sol_file.stem
721 contract_code = sol_file.read_text()
722 contracts[contract_name] = contract_code
724 options = ScanOptions(
725 run_all=all_analyzers,
726 symbolic=symbolic,
727 mev=mev,
728 cross_contract=True,
729 gas_optimization=gas,
730 calculate_economic_impact=(tvl is not None),
731 tvl=tvl,
732 protocol_type=protocol_type,
733 output_file=str(output) if output else None,
734 output_format=output_format,
735 verbose=verbose,
736 parallel=True
737 )
739 scanner = UnifiedScanner()
741 if verbose:
742 console.print(f"\n🔍 [bold]Alprina Unified Security Scanner[/bold]")
743 console.print(f"{'='*60}")
744 console.print(f"Contracts: [cyan]{len(contracts)}[/cyan]")
745 console.print(f"Cross-contract analysis: [green]enabled[/green]")
746 if tvl:
747 console.print(f"Protocol: [cyan]{protocol_type or 'generic'}[/cyan] (TVL: ${tvl:,.0f})")
748 console.print()
750 with Progress(
751 SpinnerColumn(),
752 TextColumn("[progress.description]{task.description}"),
753 console=console
754 ) as progress:
755 task = progress.add_task("Running cross-contract analysis...", total=None)
757 report = scanner.scan_multi_contract(contracts, str(target_path), options)
759 progress.update(task, completed=True)
761 _display_unified_results(report, verbose)
763 else:
764 # Scan each file individually
765 console.print(f"[cyan]→[/cyan] Scanning {len(sol_files)} contracts individually...")
767 all_reports = []
769 for sol_file in sol_files:
770 contract_code = sol_file.read_text()
772 options = ScanOptions(
773 run_all=all_analyzers,
774 symbolic=symbolic,
775 mev=mev,
776 cross_contract=False,
777 calculate_economic_impact=(tvl is not None),
778 tvl=tvl,
779 protocol_type=protocol_type,
780 output_file=None, # Don't save individual reports
781 output_format=output_format,
782 verbose=False,
783 parallel=True
784 )
786 scanner = UnifiedScanner()
787 report = scanner.scan(contract_code, str(sol_file), options)
788 all_reports.append(report)
790 if verbose:
791 console.print(f"\n[cyan]{sol_file.name}:[/cyan] {report.total_vulnerabilities} findings")
793 # Aggregate results
794 total_vulns = sum(r.total_vulnerabilities for r in all_reports)
795 console.print(f"\n[bold]Total vulnerabilities across all contracts:[/bold] {total_vulns}")
797 # Display summary
798 if total_vulns > 0 and verbose:
799 console.print("\n[bold cyan]Top Vulnerabilities:[/bold cyan]")
800 all_vulns = []
801 for report in all_reports:
802 all_vulns.extend(report.vulnerabilities)
804 # Sort by severity and risk score
805 all_vulns.sort(key=lambda v: (
806 {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}.get(v.severity, 999),
807 -(v.risk_score or 0)
808 ))
810 for i, vuln in enumerate(all_vulns[:10], 1): # Top 10
811 icon = "🔴" if vuln.severity == "critical" else "🟠" if vuln.severity == "high" else "🟡"
812 console.print(f"{i}. {icon} {vuln.title} ({vuln.file_path})")
814 else:
815 console.print(f"[bold red]Error:[/bold red] Invalid target: {target}")
818def _display_unified_results(report, verbose: bool):
819 """Display results from unified scanner"""
820 from rich.table import Table
822 console.print(f"\n{'='*60}")
823 console.print(f"[bold]📊 Scan Results[/bold]")
824 console.print(f"{'='*60}\n")
826 # Summary table
827 summary_table = Table(show_header=False, box=None)
828 summary_table.add_column("Metric", style="cyan")
829 summary_table.add_column("Value", style="bold")
831 summary_table.add_row("Scan ID", report.scan_id)
832 summary_table.add_row("Scan Time", f"{report.total_scan_time:.2f}s")
833 summary_table.add_row("Total Vulnerabilities", str(report.total_vulnerabilities))
834 summary_table.add_row(" - Critical", str(report.vulnerabilities_by_severity['critical']))
835 summary_table.add_row(" - High", str(report.vulnerabilities_by_severity['high']))
836 summary_table.add_row(" - Medium", str(report.vulnerabilities_by_severity['medium']))
837 summary_table.add_row(" - Low", str(report.vulnerabilities_by_severity['low']))
839 if report.total_max_loss > 0:
840 summary_table.add_row("Estimated Max Loss", f"${report.total_max_loss:,.0f}")
841 summary_table.add_row("Average Risk Score", f"{report.average_risk_score:.1f}/100")
843 console.print(summary_table)
845 # Vulnerabilities by analyzer
846 if report.vulnerabilities_by_analyzer:
847 console.print(f"\n[bold cyan]Vulnerabilities by Analyzer:[/bold cyan]")
848 for analyzer, count in report.vulnerabilities_by_analyzer.items():
849 console.print(f" • {analyzer}: {count}")
851 # List vulnerabilities
852 if report.total_vulnerabilities > 0:
853 console.print(f"\n[bold cyan]Vulnerabilities:[/bold cyan]\n")
855 for i, vuln in enumerate(report.vulnerabilities, 1):
856 icon = "🔴" if vuln.severity == "critical" else "🟠" if vuln.severity == "high" else "🟡" if vuln.severity == "medium" else "⚪"
858 console.print(f"{i}. {icon} [bold]{vuln.title}[/bold] [{vuln.severity.upper()}]")
859 console.print(f" File: {vuln.file_path}:{vuln.line_number or '?'}")
860 console.print(f" Analyzer: {vuln.analyzer_type.value}")
862 if vuln.estimated_loss_max:
863 console.print(f" Financial Impact: ${vuln.estimated_loss_min:,.0f} - ${vuln.estimated_loss_max:,.0f}")
865 if verbose:
866 console.print(f" {vuln.description}")
867 console.print(f" [dim]Recommendation: {vuln.recommendation}[/dim]")
869 console.print()
871 else:
872 console.print("\n[bold green]✅ No vulnerabilities found![/bold green]\n")
874 # Performance metrics
875 if verbose and report.analyzer_times:
876 console.print(f"[bold cyan]Analyzer Performance:[/bold cyan]")
877 for analyzer, time in report.analyzer_times.items():
878 console.print(f" • {analyzer}: {time:.3f}s")
880 # Errors
881 if report.errors:
882 console.print(f"\n[bold yellow]⚠️ Warnings:[/bold yellow]")
883 for error in report.errors:
884 console.print(f" • {error}")
886 # Output file notification
887 if report.scan_options.output_file:
888 console.print(f"\n[green]✓[/green] Report saved to: [cyan]{report.scan_options.output_file}[/cyan]")
890 console.print(f"\n{'='*60}\n")