1from __future__ import annotations
  2
  3from dataclasses import dataclass, field
  4from typing import TYPE_CHECKING
  5
  6if TYPE_CHECKING:
  7    from .metadata import ScanMetadata
  8
  9
 10@dataclass
 11class CheckResult:
 12    """Result of a single security check."""
 13
 14    name: str
 15    passed: bool
 16    message: str
 17
 18    @classmethod
 19    def from_dict(cls, data: dict) -> CheckResult:
 20        """Reconstruct CheckResult from dictionary."""
 21        return cls(
 22            name=data["name"],
 23            passed=data["passed"],
 24            message=data["message"],
 25        )
 26
 27    def to_dict(self) -> dict:
 28        """Convert to dictionary for JSON serialization."""
 29        return {
 30            "name": self.name,
 31            "passed": self.passed,
 32            "message": self.message,
 33        }
 34
 35
 36@dataclass
 37class AuditResult:
 38    """Result of an audit of security checks."""
 39
 40    name: str
 41    detected: bool
 42    checks: list[CheckResult] = field(default_factory=list)
 43    required: bool = True  # Whether this audit is required for all sites
 44    disabled: bool = False  # True if user disabled this audit via --disable
 45    description: str | None = None  # Optional description of what this audit checks
 46
 47    @property
 48    def passed(self) -> bool:
 49        """Audit passes if detected and all checks pass, or if optional and not detected."""
 50        # Disabled audits are excluded from pass/fail logic
 51        if self.disabled:
 52            return True
 53        if not self.detected:
 54            # Optional audits pass even when not detected
 55            return not self.required
 56        return all(check.passed for check in self.checks)
 57
 58    @classmethod
 59    def from_dict(cls, data: dict) -> AuditResult:
 60        """Reconstruct AuditResult from dictionary."""
 61        checks = [CheckResult.from_dict(c) for c in data.get("checks", [])]
 62        return cls(
 63            name=data["name"],
 64            detected=data["detected"],
 65            checks=checks,
 66            required=data.get("required", True),
 67            disabled=data.get("disabled", False),
 68            description=data.get("description"),
 69        )
 70
 71    def to_dict(self) -> dict:
 72        """Convert to dictionary for JSON serialization."""
 73        result = {
 74            "name": self.name,
 75            "detected": self.detected,
 76            "required": self.required,
 77            "passed": self.passed,
 78            "checks": [check.to_dict() for check in self.checks],
 79        }
 80        if self.disabled:
 81            result["disabled"] = self.disabled
 82        if self.description:
 83            result["description"] = self.description
 84        return result
 85
 86
 87@dataclass
 88class ScanResult:
 89    """Complete scan results for a URL."""
 90
 91    url: str
 92    audits: list[AuditResult] = field(default_factory=list)
 93    metadata: ScanMetadata | None = None
 94
 95    @property
 96    def passed(self) -> bool:
 97        """Scan passes if all audits pass (including required audits being detected)."""
 98        if not self.audits:
 99            return False
100        return all(audit.passed for audit in self.audits)
101
102    @property
103    def passed_count(self) -> int:
104        """Count of audits that passed (excluding disabled audits)."""
105        return sum(1 for audit in self.audits if audit.passed and not audit.disabled)
106
107    @property
108    def failed_count(self) -> int:
109        """Count of audits that failed (excluding disabled audits)."""
110        return sum(
111            1 for audit in self.audits if not audit.passed and not audit.disabled
112        )
113
114    @property
115    def total_count(self) -> int:
116        """Total count of audits (excluding disabled audits)."""
117        return sum(1 for audit in self.audits if not audit.disabled)
118
119    @classmethod
120    def from_dict(cls, data: dict) -> ScanResult:
121        """Reconstruct ScanResult from dictionary."""
122        from .metadata import ScanMetadata
123
124        audits = [AuditResult.from_dict(a) for a in data.get("audits", [])]
125        metadata = None
126        if "metadata" in data:
127            metadata = ScanMetadata.from_dict(data["metadata"])
128        return cls(
129            url=data["url"],
130            audits=audits,
131            metadata=metadata,
132        )
133
134    def to_dict(self) -> dict:
135        """Convert to dictionary for JSON serialization."""
136        result = {
137            "url": self.url,
138            "passed": self.passed,
139            "audits": [audit.to_dict() for audit in self.audits],
140        }
141        if self.metadata:
142            result["metadata"] = self.metadata.to_dict()
143        return result