from __future__ import annotations from datetime import datetime, timezone from typing import Optional from sqlmodel import Field, SQLModel def _now() -> datetime: return datetime.now(timezone.utc) #: Worst first. The order is load-bearing: findings are sorted and truncated by #: it, so what survives a cap is always the part that matters. SEVERITIES = ["critical", "high", "medium", "low", "unknown"] class ImageScan(SQLModel, table=True): """The result of scanning one image tag for known vulnerabilities. One row per image reference, replaced on each scan. ``digest`` is the local image's digest when it was scanned: if the image is pulled again the digest moves and the result is stale, which the UI says rather than quietly presenting yesterday's counts for today's image. """ id: Optional[int] = Field(default=None, primary_key=True) image: str = Field(index=True, unique=True) digest: Optional[str] = None scanner: str = "trivy" critical: int = 0 high: int = 0 medium: int = 0 low: int = 0 unknown: int = 0 #: How many findings have a fixed version available. The actionable number — #: a hundred unfixable CVEs are noise, three fixable ones are a to-do. fixable: int = 0 #: JSON list of the findings, worst first and capped (see scan_service). findings: str = "[]" scanned_at: datetime = Field(default_factory=_now) duration_ms: int = 0 error: Optional[str] = None # --- API schemas --- class ScanRequest(SQLModel): image: str class ScanSummary(SQLModel): image: str digest: Optional[str] scanner: str critical: int high: int medium: int low: int unknown: int fixable: int total: int scanned_at: datetime duration_ms: int error: Optional[str] #: True when the image has been pulled since this scan ran. stale: bool = False class ScanDetail(ScanSummary): findings: list[dict] = []