111 lines
3.0 KiB
Python
111 lines
3.0 KiB
Python
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,
|
|
}
|