Files
BoltClam/src/boltclam/models.py
T
TheMajesticMagicianandClaude Code addd00364d BoltClam 1.0.0: ClamAV desktop protection with Bolt sign-in
Adds the PyQt6 Windows client (Homepage, Scan & results, About & updates, Settings), ClamAV scanning and quarantine, browser-download watch with an exclusive scan gate, Bolt loopback/PKCE sign-in with DPAPI key storage, Gitea release feed with checksum verification, opt-in diagnostics, and Windows/portable build tooling.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-25 16:21:14 -06:00

192 lines
5.0 KiB
Python

"""Small typed records shared by the storage, scanner and UI layers."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
@dataclass
class Detection:
path: str
signature: str
action: str = "detected"
quarantine_id: str | None = None
size: int = 0
detected_at: str = ""
id: int | None = None
def __post_init__(self) -> None:
if not self.detected_at:
self.detected_at = now_iso()
@dataclass
class ScanRecord:
id: int | None
started_at: str
finished_at: str | None
scope: str
status: str
paths: list[str]
scanned_files: int = 0
infected_files: int = 0
errors: int = 0
engine_version: str = ""
db_version: str = ""
duration_seconds: float = 0.0
cancelled: bool = False
detections: list[Detection] = field(default_factory=list)
@property
def scope_label(self) -> str:
return "Full disk" if self.scope == "full" else "User homes"
@property
def threat_count(self) -> int:
return max(self.infected_files, len(self.detections))
@dataclass
class QuarantineItem:
id: str
original_path: str
quarantine_path: str
signature: str
sha256: str
size: int
created_at: str
status: str = "quarantined"
scan_id: int | None = None
@dataclass
class RealtimeEvent:
created_at: str
path: str
signature: str
action: str
detail: str = ""
id: int | None = None
@dataclass
class ClamAVInfo:
available: bool
path: Path | None = None
engine_version: str = ""
db_version: str = ""
db_path: Path | None = None
db_date: str = ""
db_age_days: float | None = None
error: str = ""
@property
def db_fresh(self) -> bool:
return self.db_age_days is not None and self.db_age_days <= 7
@property
def summary(self) -> str:
if not self.available:
return self.error or "ClamAV not found"
bits = [self.engine_version or "ClamAV"]
if self.db_version:
bits.append(f"signatures {self.db_version}")
if self.db_age_days is not None:
bits.append(f"{self.db_age_days:.0f}d old")
return " · ".join(bits)
@dataclass
class UpdateAsset:
id: str
name: str
size: int
content_type: str
download_url: str
@dataclass
class UpdateRelease:
id: str
tag: str
name: str
notes: str
prerelease: bool
published_at: str
html_url: str
assets: list[UpdateAsset] = field(default_factory=list)
@dataclass
class UpdateFeed:
channel: str
latest: UpdateRelease | None
releases: list[UpdateRelease]
checked_at: str
@classmethod
def from_api(cls, payload: dict) -> "UpdateFeed":
def release_from(item: dict) -> UpdateRelease:
assets = [
UpdateAsset(
id=str(asset.get("id") or ""),
name=str(asset.get("name") or "download"),
size=int(asset.get("size") or 0),
content_type=str(asset.get("content_type") or ""),
download_url=str(asset.get("download_url") or ""),
)
for asset in (item.get("assets") or [])
if isinstance(asset, dict)
]
return UpdateRelease(
id=str(item.get("id") or ""),
tag=str(item.get("tag") or ""),
name=str(item.get("name") or item.get("tag") or ""),
notes=str(item.get("notes") or ""),
prerelease=bool(item.get("prerelease")),
published_at=str(item.get("published_at") or item.get("created_at") or ""),
html_url=str(item.get("html_url") or ""),
assets=assets,
)
releases = [release_from(item) for item in (payload.get("releases") or [])
if isinstance(item, dict)]
latest_raw = payload.get("latest")
latest = release_from(latest_raw) if isinstance(latest_raw, dict) else (
releases[0] if releases else None)
return cls(
channel=str(payload.get("channel") or "release"),
latest=latest,
releases=releases,
checked_at=str(payload.get("checked_at") or now_iso()),
)
def display_name(path: str) -> str:
"""Basename for a Windows or POSIX path, independent of host OS."""
text = str(path or "")
name = Path(text.replace("\\", "/")).name
return name or text
def human_scope(scope: str) -> str:
return "Full disk" if scope == "full" else "User homes"
def human_action(action: str) -> str:
return {
"detected": "Detected",
"quarantined": "Quarantined",
"deleted": "Deleted",
"failed": "Action failed",
"ignored": "Ignored",
"allowed": "Allowed",
"blocked": "Blocked",
}.get(action, action.replace("_", " ").title())