1#![allow(missing_docs)]
7
8use serde::{Deserialize, Serialize};
9use std::borrow::Cow;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use crate::Severity;
14
15#[derive(Clone, Serialize, Deserialize)]
26pub struct RawMatch {
27 #[serde(with = "serde_arc_str")]
29 pub detector_id: Arc<str>,
30 #[serde(with = "serde_arc_str")]
32 pub detector_name: Arc<str>,
33 #[serde(with = "serde_arc_str")]
35 pub service: Arc<str>,
36 pub severity: Severity,
38 #[serde(with = "serde_arc_str")]
40 pub credential: Arc<str>,
41 pub credential_hash: String,
43 pub companions: std::collections::HashMap<String, String>,
45 pub location: MatchLocation,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub entropy: Option<f64>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub confidence: Option<f64>,
53}
54
55impl RawMatch {
56 pub fn sanitize_floats(mut self) -> Self {
60 if self.entropy.is_some_and(f64::is_nan) {
61 self.entropy = None;
62 }
63 if self.confidence.is_some_and(f64::is_nan) {
64 self.confidence = None;
65 }
66 self
67 }
68}
69
70impl PartialEq for RawMatch {
71 fn eq(&self, other: &Self) -> bool {
72 self.detector_id == other.detector_id
77 && self.detector_name == other.detector_name
78 && self.service == other.service
79 && self.severity == other.severity
80 && self.credential == other.credential
81 && self.credential_hash == other.credential_hash
82 && self.companions == other.companions
83 && self.location == other.location
84 && opt_f64_total_eq(self.entropy, other.entropy)
85 && opt_f64_total_eq(self.confidence, other.confidence)
86 }
87}
88
89impl Eq for RawMatch {}
90
91impl std::fmt::Debug for RawMatch {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("RawMatch")
97 .field("detector_id", &self.detector_id)
98 .field("detector_name", &self.detector_name)
99 .field("service", &self.service)
100 .field("severity", &self.severity)
101 .field(
102 "credential",
103 &format_args!("<redacted {} bytes>", self.credential.len()),
104 )
105 .field("credential_hash", &self.credential_hash)
106 .field(
107 "companions",
108 &format_args!("<{} redacted companions>", self.companions.len()),
109 )
110 .field("location", &self.location)
111 .field("entropy", &self.entropy)
112 .field("confidence", &self.confidence)
113 .finish()
114 }
115}
116
117#[inline]
118fn opt_f64_total_eq(a: Option<f64>, b: Option<f64>) -> bool {
119 match (a, b) {
120 (None, None) => true,
121 (Some(x), Some(y)) => x.total_cmp(&y) == std::cmp::Ordering::Equal,
122 _ => false,
123 }
124}
125
126impl PartialOrd for RawMatch {
127 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
128 Some(self.cmp(other))
129 }
130}
131
132impl Ord for RawMatch {
133 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
134 let self_conf = self.confidence.unwrap_or(0.0);
136 let other_conf = other.confidence.unwrap_or(0.0);
137
138 match other_conf.total_cmp(&self_conf) {
139 std::cmp::Ordering::Equal => {}
140 ord => return ord,
141 }
142
143 match other.severity.cmp(&self.severity) {
145 std::cmp::Ordering::Equal => {}
146 ord => return ord,
147 }
148
149 match self.detector_id.cmp(&other.detector_id) {
151 std::cmp::Ordering::Equal => self.credential.cmp(&other.credential),
152 ord => ord,
153 }
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
159pub struct MatchLocation {
160 #[serde(with = "serde_arc_str")]
162 pub source: Arc<str>,
163 #[serde(with = "serde_arc_str_opt")]
169 pub file_path: Option<Arc<str>>,
170 pub line: Option<usize>,
172 pub offset: usize,
174 #[serde(with = "serde_arc_str_opt")]
176 pub commit: Option<Arc<str>>,
177 #[serde(with = "serde_arc_str_opt")]
179 pub author: Option<Arc<str>>,
180 #[serde(with = "serde_arc_str_opt")]
182 pub date: Option<Arc<str>>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct VerifiedFinding {
188 #[serde(with = "serde_arc_str")]
190 pub detector_id: Arc<str>,
191 #[serde(with = "serde_arc_str")]
193 pub detector_name: Arc<str>,
194 #[serde(with = "serde_arc_str")]
196 pub service: Arc<str>,
197 pub severity: Severity,
199 pub credential_redacted: Cow<'static, str>,
201 pub credential_hash: String,
203 pub location: MatchLocation,
205 pub verification: VerificationResult,
207 pub metadata: HashMap<String, String>,
209 pub additional_locations: Vec<MatchLocation>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub confidence: Option<f64>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218#[serde(rename_all = "snake_case")]
219pub enum VerificationResult {
220 Live,
222 Revoked,
224 Dead,
226 RateLimited,
228 Error(String),
230 Unverifiable,
232 Skipped,
234}
235
236impl RawMatch {
237 pub fn deduplication_key(&self) -> (&str, &str) {
239 (&self.detector_id, &self.credential)
240 }
241
242 pub fn to_redacted(&self) -> RedactedFinding {
247 RedactedFinding {
248 detector_id: self.detector_id.clone(),
249 detector_name: self.detector_name.clone(),
250 service: self.service.clone(),
251 severity: self.severity,
252 credential_redacted: crate::redact(&self.credential),
253 credential_hash: self.credential_hash.clone(),
254 companions_redacted: self
255 .companions
256 .iter()
257 .map(|(k, v)| (k.clone(), crate::redact(v).into_owned()))
258 .collect(),
259 location: self.location.clone(),
260 entropy: self.entropy,
261 confidence: self.confidence,
262 }
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct RedactedFinding {
271 #[serde(with = "serde_arc_str")]
272 pub detector_id: Arc<str>,
273 #[serde(with = "serde_arc_str")]
274 pub detector_name: Arc<str>,
275 #[serde(with = "serde_arc_str")]
276 pub service: Arc<str>,
277 pub severity: Severity,
278 pub credential_redacted: Cow<'static, str>,
279 pub credential_hash: String,
280 pub companions_redacted: HashMap<String, String>,
281 pub location: MatchLocation,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub entropy: Option<f64>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub confidence: Option<f64>,
286}
287
288pub mod serde_arc_str {
289 use serde::{Deserialize, Deserializer, Serialize, Serializer};
290 use std::sync::Arc;
291
292 pub fn serialize<S>(val: &Arc<str>, serializer: S) -> Result<S::Ok, S::Error>
293 where
294 S: Serializer,
295 {
296 val.as_ref().serialize(serializer)
297 }
298
299 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<str>, D::Error>
300 where
301 D: Deserializer<'de>,
302 {
303 String::deserialize(deserializer).map(Arc::from)
304 }
305}
306
307pub mod serde_arc_str_opt {
308 use serde::{Deserialize, Deserializer, Serialize, Serializer};
309 use std::sync::Arc;
310
311 pub fn serialize<S>(val: &Option<Arc<str>>, serializer: S) -> Result<S::Ok, S::Error>
312 where
313 S: Serializer,
314 {
315 val.as_ref().map(|s| s.as_ref()).serialize(serializer)
316 }
317
318 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Arc<str>>, D::Error>
319 where
320 D: Deserializer<'de>,
321 {
322 Option::<String>::deserialize(deserializer).map(|opt| opt.map(Arc::from))
323 }
324}