from __future__ import annotations from dataclasses import asdict, dataclass, field from enum import IntEnum import hashlib from pathlib import Path from typing import Any class Severity(IntEnum): INFO = 0 LOW = 1 MEDIUM = 2 HIGH = 3 CRITICAL = 4 @classmethod def parse(cls, value: str) -> "Severity": try: return cls[value.strip().upper()] except KeyError as exc: choices = ", ".join(item.name.lower() for item in cls) raise ValueError(f"unknown severity {value!r}; choose one of: {choices}") from exc def label(self) -> str: return self.name.lower() @dataclass(frozen=True, slots=True) class Artifact: path: Path relative_path: str kind: str media_type: str size_bytes: int sha256: str text: str | None = field(default=None, repr=False) def evidence(self) -> dict[str, Any]: return { "path": self.relative_path, "kind": self.kind, "media_type": self.media_type, "size_bytes": self.size_bytes, "sha256": self.sha256, } @dataclass(frozen=True, slots=True) class Finding: rule_id: str title: str severity: Severity message: str artifact_path: str line: int | None = None column: int | None = None evidence: str | None = None remediation: str | None = None tags: tuple[str, ...] = () def fingerprint(self) -> str: material = "\0".join(( self.rule_id, self.artifact_path, str(self.line or 0), str(self.column or 0), self.message, )).encode("utf-8") return hashlib.sha256(material).hexdigest() def to_dict(self) -> dict[str, Any]: result = asdict(self) result["severity"] = self.severity.label() result["tags"] = list(self.tags) result["fingerprint"] = self.fingerprint() return result @dataclass(slots=True) class ScanReport: schema_version: str tool_version: str started_at: str completed_at: str root: str artifacts: list[dict[str, Any]] findings: list[Finding] errors: list[dict[str, str]] def to_dict(self) -> dict[str, Any]: return { "schema_version": self.schema_version, "tool": {"name": "AAISA", "version": self.tool_version}, "started_at": self.started_at, "completed_at": self.completed_at, "root": self.root, "summary": { "artifact_count": len(self.artifacts), "finding_count": len(self.findings), "error_count": len(self.errors), "by_severity": { level.label(): sum(1 for finding in self.findings if finding.severity == level) for level in Severity }, }, "artifacts": self.artifacts, "findings": [finding.to_dict() for finding in self.findings], "errors": self.errors, }