wallfacer_core/
finding.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4use sha2::{Digest, Sha256};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Finding {
8 pub id: String,
9 pub kind: FindingKind,
10 pub severity: Severity,
11 pub tool: String,
12 pub message: String,
13 pub details: String,
14 pub repro: ReproInfo,
15 pub timestamp: DateTime<Utc>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "type", rename_all = "snake_case")]
20pub enum FindingKind {
21 Crash,
22 Hang { ms: u64 },
23 SchemaViolation,
24 PropertyFailure { invariant: String },
25 ProtocolError,
26 StateLeak,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum Severity {
32 Low,
33 Medium,
34 High,
35 Critical,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ReproInfo {
40 pub seed: u64,
41 pub tool_call: Value,
42 pub transport: String,
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub composition_trail: Vec<String>,
49}
50
51impl Finding {
52 pub fn new(
53 kind: FindingKind,
54 tool: impl Into<String>,
55 message: impl Into<String>,
56 details: impl Into<String>,
57 repro: ReproInfo,
58 ) -> Self {
59 let tool = tool.into();
60 let severity = kind.default_severity();
61 let id = finding_id(&tool, &kind, &repro.tool_call);
62
63 Self {
64 id,
65 kind,
66 severity,
67 tool,
68 message: message.into(),
69 details: details.into(),
70 repro,
71 timestamp: Utc::now(),
72 }
73 }
74}
75
76impl FindingKind {
77 pub fn default_severity(&self) -> Severity {
78 match self {
79 FindingKind::Crash => Severity::Critical,
80 FindingKind::Hang { .. } => Severity::High,
81 FindingKind::ProtocolError => Severity::High,
82 FindingKind::SchemaViolation => Severity::Medium,
83 FindingKind::PropertyFailure { .. } => Severity::Medium,
84 FindingKind::StateLeak => Severity::High,
85 }
86 }
87
88 pub fn keyword(&self) -> &'static str {
91 match self {
92 FindingKind::Crash => "crash",
93 FindingKind::Hang { .. } => "hang",
94 FindingKind::ProtocolError => "protocol_error",
95 FindingKind::SchemaViolation => "schema_violation",
96 FindingKind::PropertyFailure { .. } => "property_failure",
97 FindingKind::StateLeak => "state_leak",
98 }
99 }
100}
101
102impl Finding {
103 #[must_use]
107 pub fn with_severity(mut self, severity: Severity) -> Self {
108 self.severity = severity;
109 self
110 }
111}
112
113pub fn finding_id(tool: &str, kind: &FindingKind, tool_call: &Value) -> String {
114 let canonical = canonical_json(tool_call);
115 let material = format!("{tool}{kind:?}{canonical}");
116 let hash = Sha256::digest(material.as_bytes());
117 hex::encode(hash)[..16].to_string()
118}
119
120pub fn canonical_json(value: &Value) -> String {
121 serde_json::to_string(&canonicalize(value)).unwrap_or_else(|_| "{}".to_string())
125}
126
127fn canonicalize(value: &Value) -> Value {
128 match value {
129 Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()),
130 Value::Object(map) => {
131 let mut entries = map.iter().collect::<Vec<_>>();
132 entries.sort_by_key(|(left, _)| *left);
133
134 let mut ordered = Map::new();
135 for (key, value) in entries {
136 ordered.insert(key.clone(), canonicalize(value));
137 }
138 Value::Object(ordered)
139 }
140 other => other.clone(),
141 }
142}