Coverage for src/alprina_cli/unified_scanner.py: 27%

316 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-14 11:27 +0100

1""" 

2Unified Security Scanner - Week 4 Day 1 

3 

4Orchestrates all Week 1-3 security analyzers: 

5- Week 1: PPE Detection, CVE Database 

6- Week 2: Oracle Manipulation, Input Validation, Economic Impact 

7- Week 3: Symbolic Execution, MEV Detection, Cross-Contract Analysis 

8 

9Author: Alprina Development Team 

10Date: 2025-11-13 

11""" 

12 

13import time 

14from concurrent.futures import ThreadPoolExecutor, as_completed 

15from dataclasses import dataclass, field 

16from typing import List, Dict, Optional, Set, Tuple 

17from enum import Enum 

18import json 

19from pathlib import Path 

20from datetime import datetime 

21 

22# Import all Week 1-4 analyzers 

23try: 

24 from .agents.web3_auditor.symbolic_executor import SymbolicExecutor 

25 from .agents.web3_auditor.mev_detector import MEVDetector 

26 from .agents.web3_auditor.cross_contract_analyzer import CrossContractAnalyzer 

27 from .agents.web3_auditor.economic_impact_calculator import EconomicImpactCalculator 

28 from .agents.web3_auditor.gas_optimizer import GasOptimizationAnalyzer # Week 4 Day 3 

29 from .agents.web3_auditor.solidity_analyzer import ( 

30 SolidityStaticAnalyzer, 

31 SolidityVulnerability, 

32 VulnerabilityType 

33 ) 

34except ImportError: 

35 import sys 

36 from pathlib import Path 

37 sys.path.insert(0, str(Path(__file__).parent)) 

38 from agents.web3_auditor.symbolic_executor import SymbolicExecutor 

39 from agents.web3_auditor.mev_detector import MEVDetector 

40 from agents.web3_auditor.cross_contract_analyzer import CrossContractAnalyzer 

41 from agents.web3_auditor.economic_impact_calculator import EconomicImpactCalculator 

42 from agents.web3_auditor.gas_optimizer import GasOptimizationAnalyzer # Week 4 Day 3 

43 from agents.web3_auditor.solidity_analyzer import ( 

44 SolidityStaticAnalyzer, 

45 SolidityVulnerability, 

46 VulnerabilityType 

47 ) 

48 

49 

50class AnalyzerType(Enum): 

51 """Types of security analyzers""" 

52 SYMBOLIC_EXECUTION = "symbolic" 

53 MEV_DETECTION = "mev" 

54 CROSS_CONTRACT = "cross_contract" 

55 ORACLE_MANIPULATION = "oracle" 

56 INPUT_VALIDATION = "input_validation" 

57 ECONOMIC_IMPACT = "economic" 

58 STATIC_ANALYSIS = "static" 

59 GAS_OPTIMIZATION = "gas" # Week 4 Day 3 

60 

61 

62@dataclass 

63class ScanOptions: 

64 """Options for unified security scan""" 

65 # Analyzer selection 

66 run_all: bool = False 

67 symbolic: bool = False 

68 mev: bool = False 

69 cross_contract: bool = False 

70 oracle: bool = False 

71 input_validation: bool = False 

72 static_analysis: bool = True # Always run by default 

73 gas_optimization: bool = False # Week 4 Day 3 

74 

75 # Economic impact options 

76 calculate_economic_impact: bool = True 

77 tvl: Optional[float] = None # Total Value Locked 

78 protocol_type: Optional[str] = None # dex, lending, bridge, etc. 

79 

80 # Output options 

81 output_file: Optional[str] = None 

82 output_format: str = "json" # json, html, markdown, text 

83 verbose: bool = False 

84 

85 # Performance options 

86 parallel: bool = True 

87 max_workers: int = 4 

88 timeout_per_analyzer: int = 60 # seconds 

89 

90 

91@dataclass 

92class VulnerabilityReport: 

93 """Unified vulnerability report from all analyzers""" 

94 # Vulnerability details 

95 id: str 

96 title: str 

97 severity: str # critical, high, medium, low 

98 description: str 

99 file_path: str 

100 line_number: Optional[int] = None 

101 function_name: Optional[str] = None 

102 

103 # Source analyzer 

104 analyzer_type: AnalyzerType = AnalyzerType.STATIC_ANALYSIS 

105 

106 # Economic impact (if calculated) 

107 estimated_loss_min: Optional[float] = None 

108 estimated_loss_max: Optional[float] = None 

109 risk_score: Optional[float] = None 

110 

111 # Evidence 

112 code_snippet: Optional[str] = None 

113 proof: Optional[str] = None # Z3 proof, attack scenario, etc. 

114 

115 # Remediation 

116 recommendation: str = "" 

117 references: List[str] = field(default_factory=list) 

118 

119 # Metadata 

120 detection_time: float = 0.0 # seconds 

121 confidence: str = "high" # high, medium, low 

122 

123 

124@dataclass 

125class ScanReport: 

126 """Complete security scan report""" 

127 # Scan metadata 

128 scan_id: str 

129 timestamp: datetime 

130 contract_files: List[str] 

131 scan_options: ScanOptions 

132 

133 # Results 

134 vulnerabilities: List[VulnerabilityReport] 

135 total_vulnerabilities: int 

136 vulnerabilities_by_severity: Dict[str, int] 

137 vulnerabilities_by_analyzer: Dict[str, int] 

138 

139 # Economic impact summary 

140 total_max_loss: float = 0.0 

141 average_risk_score: float = 0.0 

142 

143 # Performance metrics 

144 total_scan_time: float = 0.0 

145 analyzer_times: Dict[str, float] = field(default_factory=dict) 

146 

147 # Status 

148 success: bool = True 

149 errors: List[str] = field(default_factory=list) 

150 

151 

152class UnifiedScanner: 

153 """ 

154 Unified Security Scanner 

155 

156 Orchestrates all Week 1-3 security analyzers with: 

157 - Parallel execution for performance 

158 - Result aggregation and deduplication 

159 - Economic impact calculation 

160 - Multi-format report generation 

161 """ 

162 

163 def __init__(self): 

164 """Initialize all analyzers""" 

165 self.symbolic_executor = SymbolicExecutor() 

166 self.mev_detector = MEVDetector() 

167 self.cross_contract_analyzer = CrossContractAnalyzer() 

168 self.economic_calculator = EconomicImpactCalculator() 

169 self.static_analyzer = SolidityStaticAnalyzer() 

170 self.gas_optimizer = GasOptimizationAnalyzer() # Week 4 Day 3 

171 

172 self.scan_start_time = 0.0 

173 self.vulnerabilities: List[VulnerabilityReport] = [] 

174 

175 def scan( 

176 self, 

177 contract_code: str, 

178 file_path: str, 

179 options: ScanOptions 

180 ) -> ScanReport: 

181 """ 

182 Run unified security scan 

183 

184 Args: 

185 contract_code: Solidity source code 

186 file_path: Path to contract file 

187 options: Scan options 

188 

189 Returns: 

190 ScanReport with all findings 

191 """ 

192 self.scan_start_time = time.time() 

193 self.vulnerabilities = [] 

194 errors = [] 

195 analyzer_times = {} 

196 

197 # Determine which analyzers to run 

198 analyzers_to_run = self._select_analyzers(options) 

199 

200 if options.verbose: 

201 print(f"\n🔍 Alprina Security Scan") 

202 print(f"{'='*60}") 

203 print(f"Contract: {file_path}") 

204 if options.tvl: 

205 print(f"Protocol: {options.protocol_type or 'unknown'} (${options.tvl:,.0f} TVL)") 

206 print(f"Analyzers: {', '.join([a.value for a in analyzers_to_run])}") 

207 print(f"\n⏳ Scanning...\n") 

208 

209 # Run analyzers in parallel or sequentially 

210 if options.parallel and len(analyzers_to_run) > 1: 

211 results = self._run_parallel( 

212 contract_code, 

213 file_path, 

214 analyzers_to_run, 

215 options 

216 ) 

217 else: 

218 results = self._run_sequential( 

219 contract_code, 

220 file_path, 

221 analyzers_to_run, 

222 options 

223 ) 

224 

225 # Process results 

226 for analyzer_type, result in results.items(): 

227 if result['success']: 

228 analyzer_times[analyzer_type.value] = result['time'] 

229 self._process_analyzer_results( 

230 result['vulnerabilities'], 

231 analyzer_type, 

232 file_path, 

233 result['time'] 

234 ) 

235 else: 

236 errors.append(f"{analyzer_type.value}: {result['error']}") 

237 

238 # Calculate economic impact if requested 

239 if options.calculate_economic_impact and options.tvl: 

240 self._calculate_economic_impact_for_all(options) 

241 

242 # Deduplicate vulnerabilities 

243 self.vulnerabilities = self._deduplicate_vulnerabilities(self.vulnerabilities) 

244 

245 # Sort by severity and risk score 

246 self.vulnerabilities = self._sort_vulnerabilities(self.vulnerabilities) 

247 

248 # Generate report 

249 total_time = time.time() - self.scan_start_time 

250 

251 report = self._generate_report( 

252 file_path, 

253 options, 

254 total_time, 

255 analyzer_times, 

256 errors 

257 ) 

258 

259 # Save report if output file specified 

260 if options.output_file: 

261 self._save_report(report, options.output_file, options.output_format) 

262 

263 return report 

264 

265 def scan_multi_contract( 

266 self, 

267 contracts: Dict[str, str], 

268 file_path: str, 

269 options: ScanOptions 

270 ) -> ScanReport: 

271 """ 

272 Run unified security scan on multiple contracts 

273 

274 Args: 

275 contracts: Dict of {contract_name: source_code} 

276 file_path: Base file path 

277 options: Scan options 

278 

279 Returns: 

280 ScanReport with all findings 

281 """ 

282 self.scan_start_time = time.time() 

283 self.vulnerabilities = [] 

284 errors = [] 

285 analyzer_times = {} 

286 

287 # Run single-contract analyzers on each contract 

288 for contract_name, contract_code in contracts.items(): 

289 single_options = ScanOptions( 

290 symbolic=options.symbolic, 

291 mev=options.mev, 

292 static_analysis=options.static_analysis, 

293 calculate_economic_impact=False, # Calculate later 

294 parallel=False, # Already parallelizing at top level 

295 verbose=False 

296 ) 

297 

298 analyzers = self._select_analyzers(single_options) 

299 

300 results = self._run_sequential( 

301 contract_code, 

302 f"{file_path}:{contract_name}", 

303 analyzers, 

304 single_options 

305 ) 

306 

307 for analyzer_type, result in results.items(): 

308 if result['success']: 

309 self._process_analyzer_results( 

310 result['vulnerabilities'], 

311 analyzer_type, 

312 f"{file_path}:{contract_name}", 

313 result['time'] 

314 ) 

315 

316 # Run cross-contract analysis 

317 if options.cross_contract: 

318 start = time.time() 

319 try: 

320 cross_vulns = self.cross_contract_analyzer.analyze_contracts( 

321 contracts, 

322 file_path 

323 ) 

324 elapsed = time.time() - start 

325 analyzer_times['cross_contract'] = elapsed 

326 

327 self._process_analyzer_results( 

328 cross_vulns, 

329 AnalyzerType.CROSS_CONTRACT, 

330 file_path, 

331 elapsed 

332 ) 

333 except Exception as e: 

334 errors.append(f"cross_contract: {str(e)}") 

335 

336 # Calculate economic impact 

337 if options.calculate_economic_impact and options.tvl: 

338 self._calculate_economic_impact_for_all(options) 

339 

340 # Deduplicate and sort 

341 self.vulnerabilities = self._deduplicate_vulnerabilities(self.vulnerabilities) 

342 self.vulnerabilities = self._sort_vulnerabilities(self.vulnerabilities) 

343 

344 # Generate report 

345 total_time = time.time() - self.scan_start_time 

346 

347 report = self._generate_report( 

348 file_path, 

349 options, 

350 total_time, 

351 analyzer_times, 

352 errors 

353 ) 

354 

355 if options.output_file: 

356 self._save_report(report, options.output_file, options.output_format) 

357 

358 return report 

359 

360 def _select_analyzers(self, options: ScanOptions) -> List[AnalyzerType]: 

361 """Determine which analyzers to run based on options""" 

362 analyzers = [] 

363 

364 if options.run_all: 

365 return [ 

366 AnalyzerType.STATIC_ANALYSIS, 

367 AnalyzerType.SYMBOLIC_EXECUTION, 

368 AnalyzerType.MEV_DETECTION, 

369 AnalyzerType.INPUT_VALIDATION, 

370 AnalyzerType.ORACLE_MANIPULATION, 

371 AnalyzerType.GAS_OPTIMIZATION, # Week 4 Day 3 

372 ] 

373 

374 if options.static_analysis: 

375 analyzers.append(AnalyzerType.STATIC_ANALYSIS) 

376 

377 if options.symbolic: 

378 analyzers.append(AnalyzerType.SYMBOLIC_EXECUTION) 

379 

380 if options.mev: 

381 analyzers.append(AnalyzerType.MEV_DETECTION) 

382 

383 if options.input_validation: 

384 analyzers.append(AnalyzerType.INPUT_VALIDATION) 

385 

386 if options.oracle: 

387 analyzers.append(AnalyzerType.ORACLE_MANIPULATION) 

388 

389 if options.gas_optimization: 

390 analyzers.append(AnalyzerType.GAS_OPTIMIZATION) 

391 

392 return analyzers 

393 

394 def _run_parallel( 

395 self, 

396 contract_code: str, 

397 file_path: str, 

398 analyzers: List[AnalyzerType], 

399 options: ScanOptions 

400 ) -> Dict[AnalyzerType, Dict]: 

401 """Run analyzers in parallel""" 

402 results = {} 

403 

404 with ThreadPoolExecutor(max_workers=options.max_workers) as executor: 

405 futures = {} 

406 

407 for analyzer_type in analyzers: 

408 future = executor.submit( 

409 self._run_analyzer, 

410 analyzer_type, 

411 contract_code, 

412 file_path 

413 ) 

414 futures[future] = analyzer_type 

415 

416 for future in as_completed(futures, timeout=options.timeout_per_analyzer * len(analyzers)): 

417 analyzer_type = futures[future] 

418 try: 

419 result = future.result(timeout=options.timeout_per_analyzer) 

420 results[analyzer_type] = result 

421 except Exception as e: 

422 results[analyzer_type] = { 

423 'success': False, 

424 'error': str(e), 

425 'time': 0.0, 

426 'vulnerabilities': [] 

427 } 

428 

429 return results 

430 

431 def _run_sequential( 

432 self, 

433 contract_code: str, 

434 file_path: str, 

435 analyzers: List[AnalyzerType], 

436 options: ScanOptions 

437 ) -> Dict[AnalyzerType, Dict]: 

438 """Run analyzers sequentially""" 

439 results = {} 

440 

441 for analyzer_type in analyzers: 

442 try: 

443 result = self._run_analyzer(analyzer_type, contract_code, file_path) 

444 results[analyzer_type] = result 

445 except Exception as e: 

446 results[analyzer_type] = { 

447 'success': False, 

448 'error': str(e), 

449 'time': 0.0, 

450 'vulnerabilities': [] 

451 } 

452 

453 return results 

454 

455 def _run_analyzer( 

456 self, 

457 analyzer_type: AnalyzerType, 

458 contract_code: str, 

459 file_path: str 

460 ) -> Dict: 

461 """Run a single analyzer""" 

462 start = time.time() 

463 

464 try: 

465 if analyzer_type == AnalyzerType.STATIC_ANALYSIS: 

466 vulns = self.static_analyzer.analyze_contract(contract_code, file_path) 

467 

468 elif analyzer_type == AnalyzerType.SYMBOLIC_EXECUTION: 

469 vulns = self.symbolic_executor.analyze_contract(contract_code, file_path) 

470 

471 elif analyzer_type == AnalyzerType.MEV_DETECTION: 

472 vulns = self.mev_detector.analyze_contract(contract_code, file_path) 

473 

474 elif analyzer_type == AnalyzerType.INPUT_VALIDATION: 

475 # Use static analyzer's input validation 

476 all_vulns = self.static_analyzer.analyze_contract(contract_code, file_path) 

477 vulns = [v for v in all_vulns if 'input' in v.title.lower() or 'validation' in v.title.lower()] 

478 

479 elif analyzer_type == AnalyzerType.ORACLE_MANIPULATION: 

480 # Use static analyzer's oracle checks 

481 all_vulns = self.static_analyzer.analyze_contract(contract_code, file_path) 

482 vulns = [v for v in all_vulns if 'oracle' in v.title.lower() or 'price' in v.title.lower()] 

483 

484 elif analyzer_type == AnalyzerType.GAS_OPTIMIZATION: 

485 # Week 4 Day 3: Gas optimization analysis 

486 gas_opts = self.gas_optimizer.analyze_contract(contract_code, file_path) 

487 # Convert GasOptimization objects to SolidityVulnerability-like objects 

488 vulns = [] 

489 for opt in gas_opts: 

490 # Create a simple object that matches the expected interface 

491 class GasVuln: 

492 def __init__(self, opt): 

493 self.title = opt.title 

494 self.severity = opt.severity 

495 self.description = opt.description 

496 self.line_number = opt.line_number 

497 self.function_name = opt.function_name 

498 self.code_snippet = opt.code_before 

499 self.remediation = opt.recommendation 

500 self.confidence = 100 

501 vulns.append(GasVuln(opt)) 

502 

503 else: 

504 vulns = [] 

505 

506 elapsed = time.time() - start 

507 

508 return { 

509 'success': True, 

510 'time': elapsed, 

511 'vulnerabilities': vulns, 

512 'error': None 

513 } 

514 

515 except Exception as e: 

516 elapsed = time.time() - start 

517 return { 

518 'success': False, 

519 'time': elapsed, 

520 'vulnerabilities': [], 

521 'error': str(e) 

522 } 

523 

524 def _process_analyzer_results( 

525 self, 

526 vulnerabilities: List[SolidityVulnerability], 

527 analyzer_type: AnalyzerType, 

528 file_path: str, 

529 detection_time: float 

530 ): 

531 """Convert analyzer vulnerabilities to unified format""" 

532 for vuln in vulnerabilities: 

533 report = VulnerabilityReport( 

534 id=f"{analyzer_type.value}_{len(self.vulnerabilities)}", 

535 title=vuln.title, 

536 severity=vuln.severity, 

537 description=vuln.description, 

538 file_path=file_path, 

539 line_number=vuln.line_number, 

540 function_name=vuln.function_name, 

541 analyzer_type=analyzer_type, 

542 code_snippet=vuln.code_snippet, 

543 proof=vuln.code_snippet if vuln.code_snippet and 'Z3' in str(vuln.code_snippet) else None, 

544 recommendation=getattr(vuln, 'remediation', 'Review and fix this vulnerability'), 

545 references=getattr(vuln, 'references', []), 

546 detection_time=detection_time, 

547 confidence="high" if getattr(vuln, 'confidence', 100) >= 80 else "medium" 

548 ) 

549 

550 self.vulnerabilities.append(report) 

551 

552 def _calculate_economic_impact_for_all(self, options: ScanOptions): 

553 """Calculate economic impact for all vulnerabilities""" 

554 contract_context = { 

555 'tvl': options.tvl, 

556 'protocol_type': options.protocol_type or 'generic' 

557 } 

558 

559 for vuln in self.vulnerabilities: 

560 # Map vulnerability type 

561 vuln_type = self._map_vulnerability_type(vuln.title) 

562 

563 try: 

564 impact = self.economic_calculator.calculate_impact( 

565 vulnerability_type=vuln_type, 

566 severity=vuln.severity, 

567 contract_context=contract_context 

568 ) 

569 

570 vuln.estimated_loss_min = impact.estimated_loss_usd[0] 

571 vuln.estimated_loss_max = impact.estimated_loss_usd[1] 

572 vuln.risk_score = impact.risk_score 

573 

574 except Exception: 

575 # Skip if economic calculation fails 

576 pass 

577 

578 def _map_vulnerability_type(self, title: str) -> str: 

579 """Map vulnerability title to economic impact type""" 

580 title_lower = title.lower() 

581 

582 if 'reentrancy' in title_lower: 

583 return 'reentrancy' 

584 elif 'overflow' in title_lower or 'underflow' in title_lower: 

585 return 'integer_overflow' 

586 elif 'oracle' in title_lower: 

587 return 'oracle_manipulation' 

588 elif 'access' in title_lower or 'authorization' in title_lower: 

589 return 'access_control' 

590 elif 'mev' in title_lower or 'front' in title_lower or 'sandwich' in title_lower: 

591 return 'frontrunning_mev' 

592 else: 

593 return 'logic_error' 

594 

595 def _deduplicate_vulnerabilities( 

596 self, 

597 vulnerabilities: List[VulnerabilityReport] 

598 ) -> List[VulnerabilityReport]: 

599 """Remove duplicate vulnerabilities""" 

600 seen: Set[Tuple[str, str, Optional[int]]] = set() 

601 unique = [] 

602 

603 for vuln in vulnerabilities: 

604 key = (vuln.title, vuln.file_path, vuln.line_number) 

605 

606 if key not in seen: 

607 seen.add(key) 

608 unique.append(vuln) 

609 

610 return unique 

611 

612 def _sort_vulnerabilities( 

613 self, 

614 vulnerabilities: List[VulnerabilityReport] 

615 ) -> List[VulnerabilityReport]: 

616 """Sort vulnerabilities by severity and risk score""" 

617 severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} 

618 

619 return sorted( 

620 vulnerabilities, 

621 key=lambda v: ( 

622 severity_order.get(v.severity, 999), 

623 -(v.risk_score or 0) 

624 ) 

625 ) 

626 

627 def _generate_report( 

628 self, 

629 file_path: str, 

630 options: ScanOptions, 

631 total_time: float, 

632 analyzer_times: Dict[str, float], 

633 errors: List[str] 

634 ) -> ScanReport: 

635 """Generate final scan report""" 

636 # Count vulnerabilities by severity 

637 by_severity = { 

638 'critical': sum(1 for v in self.vulnerabilities if v.severity == 'critical'), 

639 'high': sum(1 for v in self.vulnerabilities if v.severity == 'high'), 

640 'medium': sum(1 for v in self.vulnerabilities if v.severity == 'medium'), 

641 'low': sum(1 for v in self.vulnerabilities if v.severity == 'low') 

642 } 

643 

644 # Count vulnerabilities by analyzer 

645 by_analyzer = {} 

646 for vuln in self.vulnerabilities: 

647 analyzer = vuln.analyzer_type.value 

648 by_analyzer[analyzer] = by_analyzer.get(analyzer, 0) + 1 

649 

650 # Calculate economic metrics 

651 total_max_loss = sum( 

652 v.estimated_loss_max or 0 

653 for v in self.vulnerabilities 

654 ) 

655 

656 risk_scores = [v.risk_score for v in self.vulnerabilities if v.risk_score] 

657 average_risk_score = sum(risk_scores) / len(risk_scores) if risk_scores else 0.0 

658 

659 return ScanReport( 

660 scan_id=f"alprina_{int(time.time())}", 

661 timestamp=datetime.now(), 

662 contract_files=[file_path], 

663 scan_options=options, 

664 vulnerabilities=self.vulnerabilities, 

665 total_vulnerabilities=len(self.vulnerabilities), 

666 vulnerabilities_by_severity=by_severity, 

667 vulnerabilities_by_analyzer=by_analyzer, 

668 total_max_loss=total_max_loss, 

669 average_risk_score=average_risk_score, 

670 total_scan_time=total_time, 

671 analyzer_times=analyzer_times, 

672 success=len(errors) == 0, 

673 errors=errors 

674 ) 

675 

676 def _save_report(self, report: ScanReport, output_file: str, format: str): 

677 """Save report to file""" 

678 if format == "json": 

679 self._save_json_report(report, output_file) 

680 elif format == "html": 

681 self._save_html_report(report, output_file) 

682 elif format == "markdown": 

683 self._save_markdown_report(report, output_file) 

684 else: 

685 self._save_text_report(report, output_file) 

686 

687 def _save_json_report(self, report: ScanReport, output_file: str): 

688 """Save JSON report""" 

689 data = { 

690 'scan_id': report.scan_id, 

691 'timestamp': report.timestamp.isoformat(), 

692 'contract_files': report.contract_files, 

693 'total_vulnerabilities': report.total_vulnerabilities, 

694 'by_severity': report.vulnerabilities_by_severity, 

695 'by_analyzer': report.vulnerabilities_by_analyzer, 

696 'total_max_loss': report.total_max_loss, 

697 'average_risk_score': report.average_risk_score, 

698 'scan_time': report.total_scan_time, 

699 'vulnerabilities': [ 

700 { 

701 'id': v.id, 

702 'title': v.title, 

703 'severity': v.severity, 

704 'description': v.description, 

705 'file_path': v.file_path, 

706 'line_number': v.line_number, 

707 'function_name': v.function_name, 

708 'analyzer': v.analyzer_type.value, 

709 'estimated_loss_min': v.estimated_loss_min, 

710 'estimated_loss_max': v.estimated_loss_max, 

711 'risk_score': v.risk_score, 

712 'code_snippet': v.code_snippet, 

713 'recommendation': v.recommendation, 

714 'references': v.references 

715 } 

716 for v in report.vulnerabilities 

717 ] 

718 } 

719 

720 with open(output_file, 'w') as f: 

721 json.dump(data, f, indent=2) 

722 

723 def _save_markdown_report(self, report: ScanReport, output_file: str): 

724 """Save Markdown report""" 

725 lines = [ 

726 f"# Alprina Security Scan Report", 

727 f"", 

728 f"**Scan ID**: {report.scan_id}", 

729 f"**Timestamp**: {report.timestamp.strftime('%Y-%m-%d %H:%M:%S')}", 

730 f"**Contract**: {', '.join(report.contract_files)}", 

731 f"**Scan Time**: {report.total_scan_time:.2f}s", 

732 f"", 

733 f"## Summary", 

734 f"", 

735 f"- **Total Vulnerabilities**: {report.total_vulnerabilities}", 

736 f"- **Critical**: {report.vulnerabilities_by_severity['critical']}", 

737 f"- **High**: {report.vulnerabilities_by_severity['high']}", 

738 f"- **Medium**: {report.vulnerabilities_by_severity['medium']}", 

739 f"- **Low**: {report.vulnerabilities_by_severity['low']}", 

740 f"", 

741 ] 

742 

743 if report.total_max_loss > 0: 

744 lines.extend([ 

745 f"### Economic Impact", 

746 f"", 

747 f"- **Estimated Max Loss**: ${report.total_max_loss:,.0f}", 

748 f"- **Average Risk Score**: {report.average_risk_score:.1f}/100", 

749 f"", 

750 ]) 

751 

752 lines.append(f"## Vulnerabilities\n") 

753 

754 for severity in ['critical', 'high', 'medium', 'low']: 

755 severity_vulns = [v for v in report.vulnerabilities if v.severity == severity] 

756 

757 if severity_vulns: 

758 lines.append(f"### {severity.upper()} ({len(severity_vulns)})\n") 

759 

760 for vuln in severity_vulns: 

761 lines.extend([ 

762 f"#### {vuln.title}", 

763 f"", 

764 f"- **File**: {vuln.file_path}:{vuln.line_number or '?'}", 

765 f"- **Function**: {vuln.function_name or 'N/A'}", 

766 f"- **Analyzer**: {vuln.analyzer_type.value}", 

767 ]) 

768 

769 if vuln.estimated_loss_max: 

770 lines.append(f"- **Financial Impact**: ${vuln.estimated_loss_min:,.0f} - ${vuln.estimated_loss_max:,.0f}") 

771 

772 lines.extend([ 

773 f"", 

774 f"{vuln.description}", 

775 f"", 

776 f"**Recommendation**: {vuln.recommendation}", 

777 f"", 

778 ]) 

779 

780 with open(output_file, 'w') as f: 

781 f.write('\n'.join(lines)) 

782 

783 def _save_html_report(self, report: ScanReport, output_file: str): 

784 """Save HTML report (placeholder)""" 

785 # TODO: Implement HTML report generation 

786 self._save_markdown_report(report, output_file.replace('.html', '.md')) 

787 

788 def _save_text_report(self, report: ScanReport, output_file: str): 

789 """Save text report""" 

790 lines = [ 

791 "=" * 70, 

792 "ALPRINA SECURITY SCAN REPORT", 

793 "=" * 70, 

794 "", 

795 f"Scan ID: {report.scan_id}", 

796 f"Timestamp: {report.timestamp.strftime('%Y-%m-%d %H:%M:%S')}", 

797 f"Contract: {', '.join(report.contract_files)}", 

798 f"Scan Time: {report.total_scan_time:.2f}s", 

799 "", 

800 "SUMMARY", 

801 "=" * 70, 

802 f"Total Vulnerabilities: {report.total_vulnerabilities}", 

803 f" - Critical: {report.vulnerabilities_by_severity['critical']}", 

804 f" - High: {report.vulnerabilities_by_severity['high']}", 

805 f" - Medium: {report.vulnerabilities_by_severity['medium']}", 

806 f" - Low: {report.vulnerabilities_by_severity['low']}", 

807 "", 

808 ] 

809 

810 if report.total_max_loss > 0: 

811 lines.extend([ 

812 "ECONOMIC IMPACT", 

813 "=" * 70, 

814 f"Estimated Max Loss: ${report.total_max_loss:,.0f}", 

815 f"Average Risk Score: {report.average_risk_score:.1f}/100", 

816 "", 

817 ]) 

818 

819 lines.append("VULNERABILITIES\n" + "=" * 70 + "\n") 

820 

821 for i, vuln in enumerate(report.vulnerabilities, 1): 

822 icon = "🔴" if vuln.severity == "critical" else "🟠" if vuln.severity == "high" else "🟡" if vuln.severity == "medium" else "⚪" 

823 

824 lines.extend([ 

825 f"{i}. {icon} {vuln.title} [{vuln.severity.upper()}]", 

826 f" File: {vuln.file_path}:{vuln.line_number or '?'}", 

827 f" Analyzer: {vuln.analyzer_type.value}", 

828 ]) 

829 

830 if vuln.estimated_loss_max: 

831 lines.append(f" Financial Impact: ${vuln.estimated_loss_min:,.0f} - ${vuln.estimated_loss_max:,.0f}") 

832 

833 lines.append("") 

834 

835 with open(output_file, 'w') as f: 

836 f.write('\n'.join(lines))