forked from ChelseaKR/ctdl-validate
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfindings.py
More file actions
109 lines (83 loc) · 3.3 KB
/
Copy pathfindings.py
File metadata and controls
109 lines (83 loc) · 3.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
"""Finding model: severities, rule citations, deterministic ordering.
The severity semantics here are the contract of the whole tool:
- ERROR: the payload violates a cited structural rule.
- WARNING: a cited signal that something is very likely wrong, where the rule
is not absolute or Registry enforcement of it is not documented.
- INFO: worth a human look; not a defect on its own.
- UNVERIFIABLE: the answer cannot be determined from the payload alone and the
tool refuses to guess. Never counted as a pass or a fail.
Only ERROR findings make the CLI exit nonzero.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from enum import StrEnum
class Severity(StrEnum):
ERROR = "ERROR"
WARNING = "WARNING"
INFO = "INFO"
UNVERIFIABLE = "UNVERIFIABLE"
@dataclass(frozen=True)
class Rule:
"""Where a rule comes from. Every finding carries one.
``retrieved`` is the date the cited source was downloaded, or ``"-"`` when
the citation is tool policy rather than an external document.
"""
citation: str
url: str
retrieved: str
@dataclass(frozen=True)
class Finding:
code: str
severity: Severity
entity: str
prop: str
value: str
message: str
rule: Rule
def sort_key(self) -> tuple[str, str, str, str, str, str]:
return (self.entity, self.prop, self.code, self.value, self.severity.value, self.message)
def to_dict(self) -> dict[str, object]:
return {
"code": self.code,
"severity": self.severity.value,
"entity": self.entity,
"property": self.prop,
"value": self.value,
"message": self.message,
"rule": {
"citation": self.rule.citation,
"url": self.rule.url,
"retrieved": self.rule.retrieved,
},
}
def render_text(self) -> str:
return (
f"{self.severity.value:12} {self.code} entity={self.entity}\n"
f" {self.prop} = {self.value}\n"
f" {self.message}\n"
f" rule: {self.rule.citation}\n"
f" source: {self.rule.url} (retrieved {self.rule.retrieved})"
)
def finalize(findings: list[Finding]) -> list[Finding]:
"""Deduplicate and order findings deterministically."""
return sorted(set(findings), key=Finding.sort_key)
#: The order severities are counted and printed in, everywhere.
SEVERITY_ORDER = (Severity.ERROR, Severity.WARNING, Severity.INFO, Severity.UNVERIFIABLE)
def counts(findings: list[Finding]) -> dict[str, int]:
return {
severity.value: sum(1 for f in findings if f.severity is severity)
for severity in SEVERITY_ORDER
}
def render_findings_json(findings: list[Finding], version: str) -> str:
payload = {
"tool": {"name": "ctdl-validate", "version": version},
"findings": [f.to_dict() for f in findings],
"summary": counts(findings),
}
return json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False)
def render_findings_text(findings: list[Finding]) -> str:
lines = [f.render_text() + "\n" for f in findings]
summary = ", ".join(f"{counts(findings)[s.value]} {s.value}" for s in SEVERITY_ORDER)
lines.append(f"{len(findings)} finding(s): {summary}")
return "\n".join(lines)