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    nested_checks: list[CheckResult] = field(default_factory=list)
 18
 19    def to_dict(self) -> dict:
 20        """Convert to dictionary for JSON serialization."""
 21        result = {
 22            "name": self.name,
 23            "passed": self.passed,
 24            "message": self.message,
 25        }
 26        if self.nested_checks:
 27            result["nested_checks"] = [check.to_dict() for check in self.nested_checks]
 28        return result
 29
 30
 31@dataclass
 32class AuditResult:
 33    """Result of an audit of security checks."""
 34
 35    name: str
 36    detected: bool
 37    checks: list[CheckResult] = field(default_factory=list)
 38    required: bool = True  # Whether this audit is required for all sites
 39    disabled: bool = False  # True if user disabled this audit via --disable
 40    description: str | None = None  # Optional description of what this audit checks
 41
 42    @property
 43    def passed(self) -> bool:
 44        """Audit passes if detected and all checks pass, or if optional and not detected."""
 45        # Disabled audits are excluded from pass/fail logic
 46        if self.disabled:
 47            return True
 48        if not self.detected:
 49            # Optional audits pass even when not detected
 50            return not self.required
 51        return all(check.passed for check in self.checks)
 52
 53    def to_dict(self) -> dict:
 54        """Convert to dictionary for JSON serialization."""
 55        result = {
 56            "name": self.name,
 57            "detected": self.detected,
 58            "required": self.required,
 59            "passed": self.passed,
 60            "checks": [check.to_dict() for check in self.checks],
 61        }
 62        if self.disabled:
 63            result["disabled"] = self.disabled
 64        if self.description:
 65            result["description"] = self.description
 66        return result
 67
 68
 69@dataclass
 70class ScanResult:
 71    """Complete scan results for a URL."""
 72
 73    url: str
 74    audits: list[AuditResult] = field(default_factory=list)
 75    metadata: ScanMetadata | None = None
 76
 77    @property
 78    def passed(self) -> bool:
 79        """Scan passes if all audits pass (including required audits being detected)."""
 80        if not self.audits:
 81            return False
 82        return all(audit.passed for audit in self.audits)
 83
 84    @property
 85    def passed_count(self) -> int:
 86        """Count of audits that passed (excluding disabled audits)."""
 87        return sum(1 for audit in self.audits if audit.passed and not audit.disabled)
 88
 89    @property
 90    def failed_count(self) -> int:
 91        """Count of audits that failed (excluding disabled audits)."""
 92        return sum(
 93            1 for audit in self.audits if not audit.passed and not audit.disabled
 94        )
 95
 96    @property
 97    def total_count(self) -> int:
 98        """Total count of audits (excluding disabled audits)."""
 99        return sum(1 for audit in self.audits if not audit.disabled)
100
101    def to_dict(self) -> dict:
102        """Convert to dictionary for JSON serialization."""
103        result = {
104            "url": self.url,
105            "passed": self.passed,
106            "audits": [audit.to_dict() for audit in self.audits],
107        }
108        if self.metadata:
109            result["metadata"] = self.metadata.to_dict()
110        return result