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

1""" 

2Scanner module for Alprina CLI. 

3Handles remote and local security scanning using Alprina security agents. 

4""" 

5 

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 

14 

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 

22 

23console = Console() 

24 

25 

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). 

47 

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 

72 

73 # NEW: Container scan mode 

74 if container: 

75 _run_container_scan(target, output) 

76 return 

77 

78 # NEW: Quick scan mode 

79 if quick: 

80 from .quick_scanner import quick_scan 

81 _run_quick_scan(target) 

82 return 

83 

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 

102 

103 # Check if target is local or remote 

104 target_path = Path(target) 

105 is_local = target_path.exists() 

106 

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 )) 

113 

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]") 

121 

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) 

133 

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"]) 

138 

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]") 

143 

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 }) 

153 

154 # Display results 

155 _display_results(results) 

156 

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]") 

169 

170 if output: 

171 _save_results(results, output) 

172 

173 except Exception as e: 

174 console.print(f"[red]Scan failed: {e}[/red]") 

175 

176 

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) 

185 

186 results = run_local_scan(target, profile, safe_only) 

187 

188 progress.update(task, completed=True) 

189 

190 return results 

191 

192 

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) 

201 

202 results = run_remote_scan(target, profile, safe_only) 

203 

204 progress.update(task, completed=True) 

205 

206 return results 

207 

208 

209def _run_quick_scan(target: str): 

210 """Execute quick security scan.""" 

211 from .quick_scanner import quick_scan 

212 

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 )) 

220 

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) 

229 

230 _display_quick_results(results) 

231 

232 

233def _display_quick_results(results: dict): 

234 """Display quick scan results.""" 

235 duration = results['duration_ms'] / 1000 

236 

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") 

239 

240 critical = results['summary']['critical'] 

241 

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]") 

248 

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) 

254 

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 ) 

262 

263 console.print(table) 

264 

265 if len(results['findings']) > 5: 

266 console.print(f"\n[dim]+ {len(results['findings']) - 5} more issues...[/dim]") 

267 

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]") 

271 

272 

273def _display_results(results: dict): 

274 """Display scan results in a formatted table with CVE/CWE/CVSS data.""" 

275 findings = results.get("findings", []) 

276 

277 if not findings: 

278 console.print("\n[green]✓ No security issues found![/green]") 

279 return 

280 

281 console.print(f"\n[yellow]⚠ Found {len(findings)} issues[/yellow]\n") 

282 

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) 

290 

291 severity_colors = { 

292 "CRITICAL": "bold red", 

293 "HIGH": "red", 

294 "MEDIUM": "yellow", 

295 "LOW": "blue", 

296 "INFO": "dim" 

297 } 

298 

299 for finding in findings: 

300 severity = finding.get("severity", "INFO") 

301 color = severity_colors.get(severity, "white") 

302 

303 # Get CVSS score 

304 cvss = finding.get("cvss_score") 

305 cvss_str = f"{cvss:.1f}" if cvss else "N/A" 

306 

307 # Get CWE 

308 cwe = finding.get("cwe", "") 

309 cwe_num = cwe.split("-")[1] if cwe and "-" in cwe else "" 

310 

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 ) 

319 

320 console.print(table) 

321 

322 # Show enhanced details for top 3 findings 

323 console.print("\n[bold cyan]📋 Detailed Analysis (Top 3)[/bold cyan]\n") 

324 

325 for i, finding in enumerate(findings[:3], 1): 

326 severity = finding.get("severity", "INFO") 

327 color = severity_colors.get(severity, "white") 

328 

329 console.print(f"[bold]{i}. [{color}]{severity}[/{color}]: {finding.get('type', 'Issue')}[/bold]") 

330 console.print(f" 📍 {finding.get('location', 'N/A')}") 

331 

332 if finding.get("cvss_score"): 

333 console.print(f" 📊 CVSS: {finding['cvss_score']:.1f}/10.0 ({finding.get('cvss_severity', 'N/A')})") 

334 

335 if finding.get("cwe"): 

336 cwe_name = finding.get("cwe_name", finding["cwe"]) 

337 console.print(f" 🔖 {finding['cwe']}: {cwe_name}") 

338 

339 if finding.get("owasp"): 

340 console.print(f" ⚡ OWASP: {finding['owasp']}") 

341 

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]") 

346 

347 console.print() 

348 

349 

350def _save_results(results: dict, output: Path): 

351 """Save scan results to file.""" 

352 import json 

353 

354 output.parent.mkdir(parents=True, exist_ok=True) 

355 

356 with open(output, "w") as f: 

357 json.dump(results, f, indent=2) 

358 

359 console.print(f"\n[green]✓[/green] Results saved to: {output}") 

360 

361 

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 

369 

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 )) 

375 

376 try: 

377 validate_target(target) 

378 

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) 

385 

386 # Use Alprina security agent for reconnaissance 

387 from .security_engine import run_agent 

388 

389 results = run_agent( 

390 task="web-recon", 

391 input_data=target, 

392 metadata={"passive": passive} 

393 ) 

394 

395 progress.update(task, completed=True) 

396 

397 # Log event 

398 write_event({ 

399 "type": "recon", 

400 "target": target, 

401 "passive": passive, 

402 "findings_count": len(results.get("findings", [])) 

403 }) 

404 

405 console.print("\n[green]✓ Reconnaissance complete[/green]") 

406 _display_results(results) 

407 

408 except Exception as e: 

409 console.print(f"[red]Recon failed: {e}[/red]") 

410 

411 

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() 

417 

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 ) 

428 

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 

435 

436 except Exception as e: 

437 console.print(f"[yellow]⚠️ Could not create scan entry: {e}[/yellow]") 

438 return None 

439 

440 

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() 

446 

447 response = httpx.patch( 

448 f"{backend_url}/scans/{scan_id}", 

449 headers=headers, 

450 json={"results": results}, 

451 timeout=30.0 

452 ) 

453 

454 if response.status_code != 200: 

455 console.print(f"[yellow]⚠️ Could not save scan results: {response.status_code}[/yellow]") 

456 

457 except Exception as e: 

458 console.print(f"[yellow]⚠️ Could not save scan results: {e}[/yellow]") 

459 

460 

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 

464 

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 )) 

472 

473 all_results = [] 

474 

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 

482 

483 console.print(f"[cyan]→[/cyan] Running {agent_name}...") 

484 

485 # Run the agent 

486 result = agent.analyze(target) 

487 all_results.append(result) 

488 

489 if verbose: 

490 console.print(f"[green]✓[/green] {agent_name} completed: {len(result.get('vulnerabilities', []))} findings") 

491 

492 except Exception as e: 

493 console.print(f"[red]✗[/red] {agent_name} failed: {e}") 

494 

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 } 

503 

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', [])) 

508 

509 return combined_results 

510 

511 

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 )) 

521 

522 scanner = get_container_scanner() 

523 

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) 

530 

531 # Scan the image 

532 results = scanner.scan_image(image) 

533 

534 progress.update(task, completed=True) 

535 

536 if not results["success"]: 

537 console.print(f"[red]✗ Container scan failed: {results.get('error')}[/red]") 

538 

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', '')}") 

543 

544 return 

545 

546 console.print("[green]✓ Container scan complete![/green]\n") 

547 

548 # Display summary 

549 _display_container_results(results) 

550 

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}") 

558 

559 

560def _display_container_results(results: dict): 

561 """Display container scan results.""" 

562 summary = results.get("summary", {}) 

563 image = results.get("image", "unknown") 

564 

565 # Summary table 

566 table = Table(title=f"Scan Results: {image}", show_header=False, box=None) 

567 

568 table.add_row("📦 Image:", f"[bold]{image}[/bold]") 

569 table.add_row("🔍 Vulnerabilities:", f"[bold]{summary.get('total_vulnerabilities', 0)}[/bold]") 

570 

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) 

577 

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]") 

586 

587 if summary.get("secrets_found", 0) > 0: 

588 table.add_row("🔐 Secrets Found:", f"[red bold]{summary['secrets_found']}[/red bold]") 

589 

590 if summary.get("packages_scanned", 0) > 0: 

591 table.add_row("📦 Packages Scanned:", str(summary["packages_scanned"])) 

592 

593 if summary.get("layers", 0) > 0: 

594 table.add_row("🗂️ Image Layers:", str(summary["layers"])) 

595 

596 console.print(table) 

597 

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}") 

604 

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.") 

615 

616 

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) 

632 

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 

647 

648 target_path = Path(target) 

649 

650 if not target_path.exists(): 

651 console.print(f"[bold red]Error:[/bold red] Target not found: {target}") 

652 return 

653 

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 

660 

661 # Single file scan 

662 contract_code = target_path.read_text() 

663 file_name = target_path.name 

664 

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 ) 

680 

681 # Run scan 

682 scanner = UnifiedScanner() 

683 

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() 

691 

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) 

698 

699 report = scanner.scan(contract_code, str(target_path), options) 

700 

701 progress.update(task, completed=True) 

702 

703 # Display results 

704 _display_unified_results(report, verbose) 

705 

706 elif target_path.is_dir(): 

707 # Directory scan - find all .sol files 

708 sol_files = list(target_path.glob("**/*.sol")) 

709 

710 if not sol_files: 

711 console.print(f"[bold yellow]Warning:[/bold yellow] No Solidity files found in {target}") 

712 return 

713 

714 console.print(f"[cyan]→[/cyan] Found {len(sol_files)} Solidity files") 

715 

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 

723 

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 ) 

738 

739 scanner = UnifiedScanner() 

740 

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() 

749 

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) 

756 

757 report = scanner.scan_multi_contract(contracts, str(target_path), options) 

758 

759 progress.update(task, completed=True) 

760 

761 _display_unified_results(report, verbose) 

762 

763 else: 

764 # Scan each file individually 

765 console.print(f"[cyan]→[/cyan] Scanning {len(sol_files)} contracts individually...") 

766 

767 all_reports = [] 

768 

769 for sol_file in sol_files: 

770 contract_code = sol_file.read_text() 

771 

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 ) 

785 

786 scanner = UnifiedScanner() 

787 report = scanner.scan(contract_code, str(sol_file), options) 

788 all_reports.append(report) 

789 

790 if verbose: 

791 console.print(f"\n[cyan]{sol_file.name}:[/cyan] {report.total_vulnerabilities} findings") 

792 

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}") 

796 

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) 

803 

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 )) 

809 

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})") 

813 

814 else: 

815 console.print(f"[bold red]Error:[/bold red] Invalid target: {target}") 

816 

817 

818def _display_unified_results(report, verbose: bool): 

819 """Display results from unified scanner""" 

820 from rich.table import Table 

821 

822 console.print(f"\n{'='*60}") 

823 console.print(f"[bold]📊 Scan Results[/bold]") 

824 console.print(f"{'='*60}\n") 

825 

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") 

830 

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'])) 

838 

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") 

842 

843 console.print(summary_table) 

844 

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}") 

850 

851 # List vulnerabilities 

852 if report.total_vulnerabilities > 0: 

853 console.print(f"\n[bold cyan]Vulnerabilities:[/bold cyan]\n") 

854 

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 "⚪" 

857 

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}") 

861 

862 if vuln.estimated_loss_max: 

863 console.print(f" Financial Impact: ${vuln.estimated_loss_min:,.0f} - ${vuln.estimated_loss_max:,.0f}") 

864 

865 if verbose: 

866 console.print(f" {vuln.description}") 

867 console.print(f" [dim]Recommendation: {vuln.recommendation}[/dim]") 

868 

869 console.print() 

870 

871 else: 

872 console.print("\n[bold green]✅ No vulnerabilities found![/bold green]\n") 

873 

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") 

879 

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}") 

885 

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]") 

889 

890 console.print(f"\n{'='*60}\n")