1use crate::Severity;
32
33pub trait Reportable {
41 fn scanner(&self) -> &str;
43 fn target(&self) -> &str;
45 fn severity(&self) -> Severity;
47 fn title(&self) -> &str;
49 #[allow(clippy::unnecessary_literal_bound)]
51 fn detail(&self) -> &str {
52 ""
53 }
54 fn cwe_ids(&self) -> &[String];
56 fn cve_ids(&self) -> &[String];
58 fn tags(&self) -> &[String];
60 fn confidence(&self) -> Option<f64> {
62 None
63 }
64 fn rule_id(&self) -> String {
66 format!(
67 "{}/{}",
68 self.scanner(),
69 self.title().to_lowercase().replace(' ', "-")
70 )
71 }
72 fn sarif_level(&self) -> &str {
74 self.severity().sarif_level()
75 }
76 fn exploit_hint(&self) -> Option<&str> {
78 None
79 }
80
81 fn evidence(&self) -> &[crate::Evidence] {
83 &[]
84 }
85}
86
87impl Reportable for crate::Finding {
89 fn scanner(&self) -> &str {
90 &self.scanner
91 }
92 fn target(&self) -> &str {
93 &self.target
94 }
95 fn severity(&self) -> Severity {
96 self.severity
97 }
98 fn title(&self) -> &str {
99 &self.title
100 }
101 fn detail(&self) -> &str {
102 &self.detail
103 }
104 fn cwe_ids(&self) -> &[String] {
105 &[]
106 }
107 fn cve_ids(&self) -> &[String] {
108 &self.cve_ids
109 }
110 fn tags(&self) -> &[String] {
111 &self.tags
112 }
113 fn confidence(&self) -> Option<f64> {
114 self.confidence
115 }
116 fn exploit_hint(&self) -> Option<&str> {
117 self.exploit_hint.as_deref()
118 }
119 fn evidence(&self) -> &[crate::Evidence] {
120 &self.evidence
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::{Finding, Severity};
128
129 #[test]
130 fn finding_implements_reportable() {
131 let f = Finding::new("scanner", "target", Severity::High, "Title", "Detail").unwrap();
132 assert_eq!(Reportable::scanner(&f), "scanner");
133 assert_eq!(Reportable::target(&f), "target");
134 assert_eq!(Reportable::severity(&f), Severity::High);
135 assert_eq!(Reportable::title(&f), "Title");
136 assert_eq!(Reportable::detail(&f), "Detail");
137 }
138
139 #[test]
140 fn custom_type_implements_reportable() {
141 struct CustomFinding {
142 name: String,
143 }
144
145 impl Reportable for CustomFinding {
146 fn scanner(&self) -> &str {
147 "custom"
148 }
149 fn target(&self) -> &str {
150 "custom-target"
151 }
152 fn severity(&self) -> Severity {
153 Severity::Critical
154 }
155 fn title(&self) -> &str {
156 &self.name
157 }
158 fn cwe_ids(&self) -> &[String] {
159 &[]
160 }
161 fn cve_ids(&self) -> &[String] {
162 &[]
163 }
164 fn tags(&self) -> &[String] {
165 &[]
166 }
167 }
168
169 let f = CustomFinding { name: "XSS".into() };
170 assert_eq!(f.scanner(), "custom");
171 assert_eq!(f.severity(), Severity::Critical);
172 assert_eq!(f.detail(), ""); assert!(f.tags().is_empty()); assert!(f.rule_id().contains("xss"));
175 }
176
177 #[test]
178 fn reportable_defaults_are_sensible() {
179 struct Minimal;
180 impl Reportable for Minimal {
181 fn scanner(&self) -> &str {
182 "s"
183 }
184 fn target(&self) -> &str {
185 "t"
186 }
187 fn severity(&self) -> Severity {
188 Severity::Info
189 }
190 fn title(&self) -> &str {
191 "minimal"
192 }
193 fn cwe_ids(&self) -> &[String] {
194 &[]
195 }
196 fn cve_ids(&self) -> &[String] {
197 &[]
198 }
199 fn tags(&self) -> &[String] {
200 &[]
201 }
202 }
203
204 let m = Minimal;
205 assert_eq!(m.detail(), "");
206 assert!(m.cwe_ids().is_empty());
207 assert!(m.cve_ids().is_empty());
208 assert!(m.tags().is_empty());
209 assert_eq!(m.confidence(), None);
210 assert_eq!(m.exploit_hint(), None);
211 assert_eq!(m.rule_id(), "s/minimal");
212 }
213
214 #[test]
215 fn reportable_custom_sarif_level() {
216 struct CustomSev;
217 impl Reportable for CustomSev {
218 fn scanner(&self) -> &str {
219 "s"
220 }
221 fn target(&self) -> &str {
222 "t"
223 }
224 fn severity(&self) -> Severity {
225 Severity::Critical
226 }
227 fn title(&self) -> &str {
228 "t"
229 }
230 fn cwe_ids(&self) -> &[String] {
231 &[]
232 }
233 fn cve_ids(&self) -> &[String] {
234 &[]
235 }
236 fn tags(&self) -> &[String] {
237 &[]
238 }
239 }
240 let f = CustomSev;
241 assert_eq!(f.sarif_level(), "error");
242 }
243
244 #[test]
245 fn reportable_custom_rule_id() {
246 struct CustomRuleId;
247 impl Reportable for CustomRuleId {
248 fn scanner(&self) -> &str {
249 "scanner"
250 }
251 fn target(&self) -> &str {
252 "target"
253 }
254 fn severity(&self) -> Severity {
255 Severity::Info
256 }
257 fn title(&self) -> &str {
258 "MY custom TITLE!"
259 }
260 fn rule_id(&self) -> String {
261 "CUSTOM-RULE-ID".to_string()
262 }
263 fn cwe_ids(&self) -> &[String] {
264 &[]
265 }
266 fn cve_ids(&self) -> &[String] {
267 &[]
268 }
269 fn tags(&self) -> &[String] {
270 &[]
271 }
272 }
273 let f = CustomRuleId;
274 assert_eq!(f.rule_id(), "CUSTOM-RULE-ID");
275 }
276
277 #[test]
278 fn reportable_default_rule_id_formatting() {
279 struct Spaces;
280 impl Reportable for Spaces {
281 fn scanner(&self) -> &str {
282 "scan"
283 }
284 fn target(&self) -> &str {
285 "target"
286 }
287 fn severity(&self) -> Severity {
288 Severity::Info
289 }
290 fn title(&self) -> &str {
291 "Some spaces here"
292 }
293 fn cwe_ids(&self) -> &[String] {
294 &[]
295 }
296 fn cve_ids(&self) -> &[String] {
297 &[]
298 }
299 fn tags(&self) -> &[String] {
300 &[]
301 }
302 }
303 let f = Spaces;
304 assert_eq!(f.rule_id(), "scan/some-spaces-here");
305 }
306}