use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use crate::Severity;
#[derive(Clone, Serialize, Deserialize)]
pub struct RawMatch {
#[serde(with = "serde_arc_str")]
pub detector_id: Arc<str>,
#[serde(with = "serde_arc_str")]
pub detector_name: Arc<str>,
#[serde(with = "serde_arc_str")]
pub service: Arc<str>,
pub severity: Severity,
#[serde(with = "serde_arc_str")]
pub credential: Arc<str>,
pub credential_hash: String,
pub companions: std::collections::HashMap<String, String>,
pub location: MatchLocation,
#[serde(skip_serializing_if = "Option::is_none")]
pub entropy: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
}
impl RawMatch {
pub fn sanitize_floats(mut self) -> Self {
if self.entropy.is_some_and(f64::is_nan) {
self.entropy = None;
}
if self.confidence.is_some_and(f64::is_nan) {
self.confidence = None;
}
self
}
}
impl PartialEq for RawMatch {
fn eq(&self, other: &Self) -> bool {
self.detector_id == other.detector_id
&& self.detector_name == other.detector_name
&& self.service == other.service
&& self.severity == other.severity
&& self.credential == other.credential
&& self.credential_hash == other.credential_hash
&& self.companions == other.companions
&& self.location == other.location
&& opt_f64_total_eq(self.entropy, other.entropy)
&& opt_f64_total_eq(self.confidence, other.confidence)
}
}
impl Eq for RawMatch {}
impl std::fmt::Debug for RawMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawMatch")
.field("detector_id", &self.detector_id)
.field("detector_name", &self.detector_name)
.field("service", &self.service)
.field("severity", &self.severity)
.field(
"credential",
&format_args!("<redacted {} bytes>", self.credential.len()),
)
.field("credential_hash", &self.credential_hash)
.field(
"companions",
&format_args!("<{} redacted companions>", self.companions.len()),
)
.field("location", &self.location)
.field("entropy", &self.entropy)
.field("confidence", &self.confidence)
.finish()
}
}
#[inline]
fn opt_f64_total_eq(a: Option<f64>, b: Option<f64>) -> bool {
match (a, b) {
(None, None) => true,
(Some(x), Some(y)) => x.total_cmp(&y) == std::cmp::Ordering::Equal,
_ => false,
}
}
impl PartialOrd for RawMatch {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RawMatch {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let self_conf = self.confidence.unwrap_or(0.0);
let other_conf = other.confidence.unwrap_or(0.0);
match other_conf.total_cmp(&self_conf) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
match other.severity.cmp(&self.severity) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
match self.detector_id.cmp(&other.detector_id) {
std::cmp::Ordering::Equal => self.credential.cmp(&other.credential),
ord => ord,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MatchLocation {
#[serde(with = "serde_arc_str")]
pub source: Arc<str>,
#[serde(with = "serde_arc_str_opt")]
pub file_path: Option<Arc<str>>,
pub line: Option<usize>,
pub offset: usize,
#[serde(with = "serde_arc_str_opt")]
pub commit: Option<Arc<str>>,
#[serde(with = "serde_arc_str_opt")]
pub author: Option<Arc<str>>,
#[serde(with = "serde_arc_str_opt")]
pub date: Option<Arc<str>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedFinding {
#[serde(with = "serde_arc_str")]
pub detector_id: Arc<str>,
#[serde(with = "serde_arc_str")]
pub detector_name: Arc<str>,
#[serde(with = "serde_arc_str")]
pub service: Arc<str>,
pub severity: Severity,
pub credential_redacted: Cow<'static, str>,
pub credential_hash: String,
pub location: MatchLocation,
pub verification: VerificationResult,
pub metadata: HashMap<String, String>,
pub additional_locations: Vec<MatchLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VerificationResult {
Live,
Revoked,
Dead,
RateLimited,
Error(String),
Unverifiable,
Skipped,
}
impl RawMatch {
pub fn deduplication_key(&self) -> (&str, &str) {
(&self.detector_id, &self.credential)
}
pub fn to_redacted(&self) -> RedactedFinding {
RedactedFinding {
detector_id: self.detector_id.clone(),
detector_name: self.detector_name.clone(),
service: self.service.clone(),
severity: self.severity,
credential_redacted: crate::redact(&self.credential),
credential_hash: self.credential_hash.clone(),
companions_redacted: self
.companions
.iter()
.map(|(k, v)| (k.clone(), crate::redact(v).into_owned()))
.collect(),
location: self.location.clone(),
entropy: self.entropy,
confidence: self.confidence,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedactedFinding {
#[serde(with = "serde_arc_str")]
pub detector_id: Arc<str>,
#[serde(with = "serde_arc_str")]
pub detector_name: Arc<str>,
#[serde(with = "serde_arc_str")]
pub service: Arc<str>,
pub severity: Severity,
pub credential_redacted: Cow<'static, str>,
pub credential_hash: String,
pub companions_redacted: HashMap<String, String>,
pub location: MatchLocation,
#[serde(skip_serializing_if = "Option::is_none")]
pub entropy: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
}
pub mod serde_arc_str {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::Arc;
pub fn serialize<S>(val: &Arc<str>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
val.as_ref().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<str>, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(Arc::from)
}
}
pub mod serde_arc_str_opt {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::Arc;
pub fn serialize<S>(val: &Option<Arc<str>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
val.as_ref().map(|s| s.as_ref()).serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Arc<str>>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).map(|opt| opt.map(Arc::from))
}
}