1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8#[serde(transparent)]
9pub struct LanguageId(pub String);
10
11impl LanguageId {
12 pub fn new(id: impl Into<String>) -> Self {
13 Self(id.into())
14 }
15
16 pub fn as_str(&self) -> &str {
17 &self.0
18 }
19
20 pub fn rust() -> Self {
21 Self("rust".into())
22 }
23}
24
25impl std::fmt::Display for LanguageId {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.write_str(&self.0)
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum Severity {
34 Info,
35 Low,
36 Medium,
37 High,
38 Critical,
39}
40
41impl Severity {
42 pub fn as_str(self) -> &'static str {
43 match self {
44 Self::Info => "info",
45 Self::Low => "low",
46 Self::Medium => "medium",
47 Self::High => "high",
48 Self::Critical => "critical",
49 }
50 }
51
52 pub fn parse(s: &str) -> Option<Self> {
53 match s.to_ascii_lowercase().as_str() {
54 "info" => Some(Self::Info),
55 "low" => Some(Self::Low),
56 "medium" | "med" => Some(Self::Medium),
57 "high" => Some(Self::High),
58 "critical" | "crit" => Some(Self::Critical),
59 _ => None,
60 }
61 }
62}
63
64impl std::fmt::Display for Severity {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.write_str(self.as_str())
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "lowercase")]
73pub enum Confidence {
74 Low,
75 Medium,
76 High,
77 Definite,
78}
79
80impl Confidence {
81 pub fn as_str(self) -> &'static str {
82 match self {
83 Self::Low => "low",
84 Self::Medium => "medium",
85 Self::High => "high",
86 Self::Definite => "definite",
87 }
88 }
89
90 pub fn is_definite_bug(self) -> bool {
92 matches!(self, Self::Definite)
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct SourceLocation {
98 pub file: PathBuf,
99 pub start_line: u32,
100 pub start_column: u32,
101 pub end_line: u32,
102 pub end_column: u32,
103}
104
105impl SourceLocation {
106 pub fn single_line(file: impl Into<PathBuf>, line: u32, column: u32) -> Self {
107 Self {
108 file: file.into(),
109 start_line: line,
110 start_column: column,
111 end_line: line,
112 end_column: column.saturating_add(1),
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
119pub struct CodeContext {
120 pub before: Vec<String>,
121 pub highlight: String,
122 pub after: Vec<String>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct Diagnostic {
128 pub message: String,
129 pub location: SourceLocation,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub context: Option<CodeContext>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct DetectionEvidence {
137 pub kind: String,
138 pub detail: String,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct Finding {
144 pub id: String,
145 pub rule_id: String,
146 pub severity: Severity,
147 pub confidence: Confidence,
148 pub language: LanguageId,
149 pub location: SourceLocation,
150 pub explanation: String,
151 pub failure_scenario: String,
152 pub remediation: String,
153 pub evidence: Vec<DetectionEvidence>,
154 pub definite_bug: bool,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub context: Option<CodeContext>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub fix_hint: Option<String>,
160}
161
162impl Finding {
163 pub fn builder(rule_id: impl Into<String>, language: LanguageId) -> FindingBuilder {
164 FindingBuilder {
165 rule_id: rule_id.into(),
166 language,
167 severity: Severity::Medium,
168 confidence: Confidence::Medium,
169 location: None,
170 explanation: String::new(),
171 failure_scenario: String::new(),
172 remediation: String::new(),
173 evidence: Vec::new(),
174 context: None,
175 fix_hint: None,
176 id: None,
177 }
178 }
179}
180
181pub struct FindingBuilder {
182 rule_id: String,
183 language: LanguageId,
184 severity: Severity,
185 confidence: Confidence,
186 location: Option<SourceLocation>,
187 explanation: String,
188 failure_scenario: String,
189 remediation: String,
190 evidence: Vec<DetectionEvidence>,
191 context: Option<CodeContext>,
192 fix_hint: Option<String>,
193 id: Option<String>,
194}
195
196impl FindingBuilder {
197 pub fn severity(mut self, s: Severity) -> Self {
198 self.severity = s;
199 self
200 }
201 pub fn confidence(mut self, c: Confidence) -> Self {
202 self.confidence = c;
203 self
204 }
205 pub fn location(mut self, loc: SourceLocation) -> Self {
206 self.location = Some(loc);
207 self
208 }
209 pub fn explanation(mut self, e: impl Into<String>) -> Self {
210 self.explanation = e.into();
211 self
212 }
213 pub fn failure_scenario(mut self, e: impl Into<String>) -> Self {
214 self.failure_scenario = e.into();
215 self
216 }
217 pub fn remediation(mut self, e: impl Into<String>) -> Self {
218 self.remediation = e.into();
219 self
220 }
221 pub fn evidence(mut self, kind: impl Into<String>, detail: impl Into<String>) -> Self {
222 self.evidence.push(DetectionEvidence {
223 kind: kind.into(),
224 detail: detail.into(),
225 });
226 self
227 }
228 pub fn context(mut self, c: CodeContext) -> Self {
229 self.context = Some(c);
230 self
231 }
232 pub fn fix_hint(mut self, h: impl Into<String>) -> Self {
233 self.fix_hint = Some(h.into());
234 self
235 }
236 pub fn id(mut self, id: impl Into<String>) -> Self {
237 self.id = Some(id.into());
238 self
239 }
240 pub fn build(self) -> Finding {
241 let location = self.location.expect("Finding requires location");
242 let definite_bug = self.confidence.is_definite_bug();
243 let id = self.id.unwrap_or_else(|| {
244 format!(
245 "{}:{}:{}:{}",
246 self.rule_id,
247 location.file.display(),
248 location.start_line,
249 location.start_column
250 )
251 });
252 Finding {
253 id,
254 rule_id: self.rule_id,
255 severity: self.severity,
256 confidence: self.confidence,
257 language: self.language,
258 location,
259 explanation: self.explanation,
260 failure_scenario: self.failure_scenario,
261 remediation: self.remediation,
262 evidence: self.evidence,
263 definite_bug,
264 context: self.context,
265 fix_hint: self.fix_hint,
266 }
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct Suppression {
273 pub rule_id: Option<String>,
274 pub file: Option<PathBuf>,
275 pub line: Option<u32>,
276 pub reason: Option<String>,
277 pub pattern: Option<String>,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282pub struct ApprovedPattern {
283 pub rule_id: Option<String>,
284 pub path_glob: Option<String>,
285 pub code_contains: Option<String>,
286 pub reason: String,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct SourceFile {
292 pub path: PathBuf,
293 pub content: String,
294 pub language: LanguageId,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
299pub struct ControlFlow {
300 pub nodes: Vec<String>,
301 pub edges: Vec<(usize, usize)>,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
306pub struct ErrorPath {
307 pub steps: Vec<String>,
308 pub terminates: bool,
309 pub recovers: bool,
310 pub context_lost: bool,
311}