use std::sync::Arc;
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;
use crate::evidence::Evidence;
use crate::kind::FindingKind;
use crate::location::Location;
use crate::severity::Severity;
use crate::status::FindingStatus;
pub const FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
#[allow(clippy::struct_field_names)]
pub struct FindingConfig {
pub max_scanner_len: usize,
pub max_target_len: usize,
pub max_title_len: usize,
pub max_detail_len: usize,
pub max_evidence_count: usize,
pub max_tags_count: usize,
pub max_cve_count: usize,
pub max_cwe_count: usize,
pub max_references_count: usize,
pub max_matched_values_count: usize,
}
impl Default for FindingConfig {
fn default() -> Self {
Self {
max_scanner_len: 1024,
max_target_len: 65_536,
max_title_len: 10_240,
max_detail_len: 1_048_576,
max_evidence_count: 10_000,
max_tags_count: 10_000,
max_cve_count: 100,
max_cwe_count: 100,
max_references_count: 1_000,
max_matched_values_count: 10_000,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Finding {
pub version: u32,
pub(crate) id: Uuid,
pub(crate) scanner: Arc<str>,
pub(crate) target: Arc<str>,
pub(crate) severity: Severity,
pub(crate) title: Arc<str>,
pub(crate) detail: Arc<str>,
#[serde(rename = "type")]
pub(crate) kind: FindingKind,
pub(crate) status: FindingStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) evidence: Vec<Evidence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) location: Option<Location>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) tags: Vec<Arc<str>>,
pub(crate) timestamp: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) cve_ids: Vec<Arc<str>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) cwe_ids: Vec<Arc<str>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) references: Vec<Arc<str>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) confidence: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) cvss_score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) scan_id: Option<Arc<str>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) exploit_hint: Option<Arc<str>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) remediation: Option<Arc<str>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) matched_values: Vec<Arc<str>>,
}
impl Finding {
pub fn version(&self) -> u32 {
self.version
}
pub fn id(&self) -> Uuid {
self.id
}
pub fn scanner(&self) -> &str {
&self.scanner
}
pub fn target(&self) -> &str {
&self.target
}
pub fn severity(&self) -> Severity {
self.severity
}
pub fn title(&self) -> &str {
&self.title
}
pub fn detail(&self) -> &str {
&self.detail
}
pub fn kind(&self) -> FindingKind {
self.kind
}
pub fn status(&self) -> FindingStatus {
self.status
}
pub fn evidence(&self) -> &[Evidence] {
&self.evidence
}
pub fn location(&self) -> Option<&Location> {
self.location.as_ref()
}
pub fn tags(&self) -> &[Arc<str>] {
&self.tags
}
pub fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
pub fn cve_ids(&self) -> &[Arc<str>] {
&self.cve_ids
}
pub fn cwe_ids(&self) -> &[Arc<str>] {
&self.cwe_ids
}
pub fn references(&self) -> &[Arc<str>] {
&self.references
}
pub fn confidence(&self) -> Option<f64> {
self.confidence
}
pub fn cvss_score(&self) -> Option<f64> {
self.cvss_score
}
pub fn scan_id(&self) -> Option<&str> {
self.scan_id.as_deref()
}
pub fn exploit_hint(&self) -> Option<&str> {
self.exploit_hint.as_deref()
}
pub fn remediation(&self) -> Option<&str> {
self.remediation.as_deref()
}
pub fn matched_values(&self) -> &[Arc<str>] {
&self.matched_values
}
}
impl std::hash::Hash for Finding {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.version.hash(state);
self.id.hash(state);
self.scanner.hash(state);
self.target.hash(state);
self.severity.hash(state);
self.title.hash(state);
self.detail.hash(state);
self.kind.hash(state);
self.status.hash(state);
self.evidence.hash(state);
self.location.hash(state);
self.tags.hash(state);
self.timestamp.hash(state);
self.cve_ids.hash(state);
self.cwe_ids.hash(state);
self.references.hash(state);
if let Some(c) = self.confidence {
let bits = if c == 0.0 { 0.0_f64.to_bits() } else { c.to_bits() };
state.write_u64(bits);
}
if let Some(s) = self.cvss_score {
let bits = if s == 0.0 { 0.0_f64.to_bits() } else { s.to_bits() };
state.write_u64(bits);
}
self.scan_id.hash(state);
self.exploit_hint.hash(state);
self.remediation.hash(state);
self.matched_values.hash(state);
}
}
impl Eq for Finding {}
impl PartialOrd for Finding {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Finding {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.severity
.cmp(&other.severity)
.then_with(|| self.scanner.cmp(&other.scanner))
.then_with(|| self.target.cmp(&other.target))
.then_with(|| self.title.cmp(&other.title))
.then_with(|| self.id.cmp(&other.id))
}
}
const MAX_DISPLAY_LEN: usize = 200;
pub(crate) fn redact_for_display(s: &str) -> String {
let mut result = s.to_string();
if let Some(pos) = result.find("://") {
if let Some(at_pos) = result[pos + 3..].find('@') {
let at_absolute = pos + 3 + at_pos;
if result[pos + 3..at_absolute].contains(':') {
result.replace_range(pos + 3..at_absolute, "***");
}
}
}
if result.len() > MAX_DISPLAY_LEN {
format!("{}...[truncated]", &result[..MAX_DISPLAY_LEN])
} else {
result
}
}
impl std::fmt::Display for Finding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let target_safe = redact_for_display(&self.target);
let title_safe = redact_for_display(&self.title);
write!(
f,
"[{}] [{}] {} {} {}",
self.severity.label(),
self.status.label(),
self.kind,
target_safe,
title_safe
)?;
if let Some(loc) = &self.location {
write!(f, " at {loc}")?;
}
Ok(())
}
}