1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
11pub enum Severity {
12 Info,
14 Low,
16 Medium,
18 High,
20 Critical,
22}
23
24impl Severity {
25 pub fn value(self) -> u32 {
27 match self {
28 Self::Info => 0,
29 Self::Low => 1,
30 Self::Medium => 2,
31 Self::High => 3,
32 Self::Critical => 4,
33 }
34 }
35
36 pub fn name(self) -> &'static str {
38 match self {
39 Self::Info => "INFO",
40 Self::Low => "LOW",
41 Self::Medium => "MEDIUM",
42 Self::High => "HIGH",
43 Self::Critical => "CRITICAL",
44 }
45 }
46
47 pub fn ansi_color(self) -> &'static str {
49 match self {
50 Self::Info => "\x1b[36m", Self::Low => "\x1b[34m", Self::Medium => "\x1b[33m", Self::High => "\x1b[1;33m", Self::Critical => "\x1b[1;31m", }
56 }
57}
58
59impl fmt::Display for Severity {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "{}", self.name())
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Finding {
71 pub invariant_id: String,
74
75 pub severity: Severity,
77
78 pub file: String,
80
81 pub line: usize,
83
84 pub col: usize,
86
87 pub message: String,
90
91 pub snippet: String,
94
95 pub source_fragment: Option<String>,
98
99 pub transaction_hash: Option<String>,
101
102 pub metadata: std::collections::BTreeMap<String, String>,
105}
106
107impl Finding {
108 pub fn new(
110 invariant_id: String,
111 severity: Severity,
112 file: String,
113 line: usize,
114 col: usize,
115 message: String,
116 snippet: String,
117 ) -> Self {
118 Self {
119 invariant_id,
120 severity,
121 file,
122 line,
123 col,
124 message,
125 snippet,
126 source_fragment: None,
127 transaction_hash: None,
128 metadata: Default::default(),
129 }
130 }
131
132 pub fn with_metadata(mut self, key: String, value: String) -> Self {
134 self.metadata.insert(key, value);
135 self
136 }
137
138 pub fn with_source_fragment(mut self, fragment: String) -> Self {
140 self.source_fragment = Some(fragment);
141 self
142 }
143
144 pub fn with_transaction_hash(mut self, tx_hash: String) -> Self {
146 self.transaction_hash = Some(tx_hash);
147 self
148 }
149
150 pub fn dedup_key(&self) -> String {
152 format!("{}:{}:{}", self.invariant_id, self.file, self.line)
153 }
154}
155
156impl fmt::Display for Finding {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 write!(
159 f,
160 "[{}] {} at {}:{}:{} - {}",
161 self.severity, self.invariant_id, self.file, self.line, self.col, self.message
162 )
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_severity_ordering() {
172 assert!(Severity::Critical > Severity::High);
173 assert!(Severity::High > Severity::Medium);
174 assert!(Severity::Low < Severity::Medium);
175 }
176
177 #[test]
178 fn test_finding_dedup_key() {
179 let f = Finding::new(
180 "test_invariant".to_string(),
181 Severity::High,
182 "contract.sol".to_string(),
183 42,
184 10,
185 "Test message".to_string(),
186 "code snippet".to_string(),
187 );
188 assert_eq!(f.dedup_key(), "test_invariant:contract.sol:42");
189 }
190
191 #[test]
192 fn test_finding_with_metadata() {
193 let f = Finding::new(
194 "test".to_string(),
195 Severity::Medium,
196 "file.rs".to_string(),
197 1,
198 0,
199 "msg".to_string(),
200 "snippet".to_string(),
201 )
202 .with_metadata("chain".to_string(), "evm".to_string());
203
204 assert_eq!(f.metadata.get("chain"), Some(&"evm".to_string()));
205 }
206}