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
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2Unified Security Scanner - Week 4 Day 1
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
9Author: Alprina Development Team
10Date: 2025-11-13
11"""
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
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 )
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
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
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.
80 # Output options
81 output_file: Optional[str] = None
82 output_format: str = "json" # json, html, markdown, text
83 verbose: bool = False
85 # Performance options
86 parallel: bool = True
87 max_workers: int = 4
88 timeout_per_analyzer: int = 60 # seconds
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
103 # Source analyzer
104 analyzer_type: AnalyzerType = AnalyzerType.STATIC_ANALYSIS
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
111 # Evidence
112 code_snippet: Optional[str] = None
113 proof: Optional[str] = None # Z3 proof, attack scenario, etc.
115 # Remediation
116 recommendation: str = ""
117 references: List[str] = field(default_factory=list)
119 # Metadata
120 detection_time: float = 0.0 # seconds
121 confidence: str = "high" # high, medium, low
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
133 # Results
134 vulnerabilities: List[VulnerabilityReport]
135 total_vulnerabilities: int
136 vulnerabilities_by_severity: Dict[str, int]
137 vulnerabilities_by_analyzer: Dict[str, int]
139 # Economic impact summary
140 total_max_loss: float = 0.0
141 average_risk_score: float = 0.0
143 # Performance metrics
144 total_scan_time: float = 0.0
145 analyzer_times: Dict[str, float] = field(default_factory=dict)
147 # Status
148 success: bool = True
149 errors: List[str] = field(default_factory=list)
152class UnifiedScanner:
153 """
154 Unified Security Scanner
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 """
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
172 self.scan_start_time = 0.0
173 self.vulnerabilities: List[VulnerabilityReport] = []
175 def scan(
176 self,
177 contract_code: str,
178 file_path: str,
179 options: ScanOptions
180 ) -> ScanReport:
181 """
182 Run unified security scan
184 Args:
185 contract_code: Solidity source code
186 file_path: Path to contract file
187 options: Scan options
189 Returns:
190 ScanReport with all findings
191 """
192 self.scan_start_time = time.time()
193 self.vulnerabilities = []
194 errors = []
195 analyzer_times = {}
197 # Determine which analyzers to run
198 analyzers_to_run = self._select_analyzers(options)
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")
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 )
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']}")
238 # Calculate economic impact if requested
239 if options.calculate_economic_impact and options.tvl:
240 self._calculate_economic_impact_for_all(options)
242 # Deduplicate vulnerabilities
243 self.vulnerabilities = self._deduplicate_vulnerabilities(self.vulnerabilities)
245 # Sort by severity and risk score
246 self.vulnerabilities = self._sort_vulnerabilities(self.vulnerabilities)
248 # Generate report
249 total_time = time.time() - self.scan_start_time
251 report = self._generate_report(
252 file_path,
253 options,
254 total_time,
255 analyzer_times,
256 errors
257 )
259 # Save report if output file specified
260 if options.output_file:
261 self._save_report(report, options.output_file, options.output_format)
263 return report
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
274 Args:
275 contracts: Dict of {contract_name: source_code}
276 file_path: Base file path
277 options: Scan options
279 Returns:
280 ScanReport with all findings
281 """
282 self.scan_start_time = time.time()
283 self.vulnerabilities = []
284 errors = []
285 analyzer_times = {}
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 )
298 analyzers = self._select_analyzers(single_options)
300 results = self._run_sequential(
301 contract_code,
302 f"{file_path}:{contract_name}",
303 analyzers,
304 single_options
305 )
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 )
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
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)}")
336 # Calculate economic impact
337 if options.calculate_economic_impact and options.tvl:
338 self._calculate_economic_impact_for_all(options)
340 # Deduplicate and sort
341 self.vulnerabilities = self._deduplicate_vulnerabilities(self.vulnerabilities)
342 self.vulnerabilities = self._sort_vulnerabilities(self.vulnerabilities)
344 # Generate report
345 total_time = time.time() - self.scan_start_time
347 report = self._generate_report(
348 file_path,
349 options,
350 total_time,
351 analyzer_times,
352 errors
353 )
355 if options.output_file:
356 self._save_report(report, options.output_file, options.output_format)
358 return report
360 def _select_analyzers(self, options: ScanOptions) -> List[AnalyzerType]:
361 """Determine which analyzers to run based on options"""
362 analyzers = []
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 ]
374 if options.static_analysis:
375 analyzers.append(AnalyzerType.STATIC_ANALYSIS)
377 if options.symbolic:
378 analyzers.append(AnalyzerType.SYMBOLIC_EXECUTION)
380 if options.mev:
381 analyzers.append(AnalyzerType.MEV_DETECTION)
383 if options.input_validation:
384 analyzers.append(AnalyzerType.INPUT_VALIDATION)
386 if options.oracle:
387 analyzers.append(AnalyzerType.ORACLE_MANIPULATION)
389 if options.gas_optimization:
390 analyzers.append(AnalyzerType.GAS_OPTIMIZATION)
392 return analyzers
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 = {}
404 with ThreadPoolExecutor(max_workers=options.max_workers) as executor:
405 futures = {}
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
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 }
429 return results
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 = {}
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 }
453 return results
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()
464 try:
465 if analyzer_type == AnalyzerType.STATIC_ANALYSIS:
466 vulns = self.static_analyzer.analyze_contract(contract_code, file_path)
468 elif analyzer_type == AnalyzerType.SYMBOLIC_EXECUTION:
469 vulns = self.symbolic_executor.analyze_contract(contract_code, file_path)
471 elif analyzer_type == AnalyzerType.MEV_DETECTION:
472 vulns = self.mev_detector.analyze_contract(contract_code, file_path)
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()]
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()]
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))
503 else:
504 vulns = []
506 elapsed = time.time() - start
508 return {
509 'success': True,
510 'time': elapsed,
511 'vulnerabilities': vulns,
512 'error': None
513 }
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 }
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 )
550 self.vulnerabilities.append(report)
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 }
559 for vuln in self.vulnerabilities:
560 # Map vulnerability type
561 vuln_type = self._map_vulnerability_type(vuln.title)
563 try:
564 impact = self.economic_calculator.calculate_impact(
565 vulnerability_type=vuln_type,
566 severity=vuln.severity,
567 contract_context=contract_context
568 )
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
574 except Exception:
575 # Skip if economic calculation fails
576 pass
578 def _map_vulnerability_type(self, title: str) -> str:
579 """Map vulnerability title to economic impact type"""
580 title_lower = title.lower()
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'
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 = []
603 for vuln in vulnerabilities:
604 key = (vuln.title, vuln.file_path, vuln.line_number)
606 if key not in seen:
607 seen.add(key)
608 unique.append(vuln)
610 return unique
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}
619 return sorted(
620 vulnerabilities,
621 key=lambda v: (
622 severity_order.get(v.severity, 999),
623 -(v.risk_score or 0)
624 )
625 )
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 }
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
650 # Calculate economic metrics
651 total_max_loss = sum(
652 v.estimated_loss_max or 0
653 for v in self.vulnerabilities
654 )
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
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 )
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)
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 }
720 with open(output_file, 'w') as f:
721 json.dump(data, f, indent=2)
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 ]
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 ])
752 lines.append(f"## Vulnerabilities\n")
754 for severity in ['critical', 'high', 'medium', 'low']:
755 severity_vulns = [v for v in report.vulnerabilities if v.severity == severity]
757 if severity_vulns:
758 lines.append(f"### {severity.upper()} ({len(severity_vulns)})\n")
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 ])
769 if vuln.estimated_loss_max:
770 lines.append(f"- **Financial Impact**: ${vuln.estimated_loss_min:,.0f} - ${vuln.estimated_loss_max:,.0f}")
772 lines.extend([
773 f"",
774 f"{vuln.description}",
775 f"",
776 f"**Recommendation**: {vuln.recommendation}",
777 f"",
778 ])
780 with open(output_file, 'w') as f:
781 f.write('\n'.join(lines))
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'))
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 ]
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 ])
819 lines.append("VULNERABILITIES\n" + "=" * 70 + "\n")
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 "⚪"
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 ])
830 if vuln.estimated_loss_max:
831 lines.append(f" Financial Impact: ${vuln.estimated_loss_min:,.0f} - ${vuln.estimated_loss_max:,.0f}")
833 lines.append("")
835 with open(output_file, 'w') as f:
836 f.write('\n'.join(lines))