""" Data models for NuGet vulnerability checking. """ from dataclasses import dataclass, field from pathlib import Path @dataclass class VulnerabilityDetail: """Details about a specific vulnerability.""" severity: str # low, moderate, high, critical advisory_url: str @dataclass class VulnerablePackage: """A package with known vulnerabilities.""" package_id: str resolved_version: str requested_version: str vulnerabilities: list[VulnerabilityDetail] = field(default_factory=list) affected_projects: list[Path] = field(default_factory=list) suggested_version: str | None = None fix_risk: str = "unknown" # low, medium, high @property def highest_severity(self) -> str: """Get the highest severity among all vulnerabilities.""" severity_order = {"low": 1, "moderate": 2, "high": 3, "critical": 4} if not self.vulnerabilities: return "unknown" return max( self.vulnerabilities, key=lambda v: severity_order.get(v.severity.lower(), 0), ).severity @property def advisory_urls(self) -> list[str]: """Get all advisory URLs.""" return [v.advisory_url for v in self.vulnerabilities] @dataclass class SuggestedFix: """Suggested fix for a vulnerable package.""" version: str is_major_upgrade: bool is_minor_upgrade: bool is_patch_upgrade: bool breaking_change_risk: str # low, medium, high @classmethod def from_versions( cls, current: str, suggested: str, current_parsed: tuple, suggested_parsed: tuple ) -> "SuggestedFix": """Create a SuggestedFix from version tuples.""" is_major = suggested_parsed[0] > current_parsed[0] is_minor = not is_major and suggested_parsed[1] > current_parsed[1] is_patch = not is_major and not is_minor and suggested_parsed[2] > current_parsed[2] # Estimate breaking change risk if is_major: risk = "high" elif is_minor: risk = "medium" else: risk = "low" return cls( version=suggested, is_major_upgrade=is_major, is_minor_upgrade=is_minor, is_patch_upgrade=is_patch, breaking_change_risk=risk, ) @dataclass class VulnerabilityReport: """Complete vulnerability scan report.""" solution: Path min_severity: str total_packages: int vulnerabilities: list[VulnerablePackage] = field(default_factory=list) unfixable: list[tuple[str, str]] = field(default_factory=list) # (package, reason) @property def vulnerable_count(self) -> int: """Count of vulnerable packages.""" return len(self.vulnerabilities) @property def fixable_count(self) -> int: """Count of packages with suggested fixes.""" return sum(1 for v in self.vulnerabilities if v.suggested_version) @property def unfixable_count(self) -> int: """Count of packages without fixes.""" return len(self.unfixable) + sum( 1 for v in self.vulnerabilities if not v.suggested_version ) # Severity level mapping for comparisons SEVERITY_LEVELS = { "low": 1, "moderate": 2, "high": 3, "critical": 4, } def meets_severity_threshold(vuln_severity: str, min_severity: str) -> bool: """Check if vulnerability meets minimum severity threshold.""" vuln_level = SEVERITY_LEVELS.get(vuln_severity.lower(), 0) min_level = SEVERITY_LEVELS.get(min_severity.lower(), 0) return vuln_level >= min_level