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