124 lines
3.5 KiB
Python
124 lines
3.5 KiB
Python
"""
|
|
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
|