Files
git.stella-ops.org/tools/slntools/lib/vulnerability_models.py
StellaOps Bot 3acc0ef0cd save progress
2025-12-28 03:09:52 +02:00

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