use core::fmt;
use crate::{
Explanation, Scanner, confidence::Confidence, location::Location, remediation::Remediation,
rule::RuleId, severity::Severity,
};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Finding {
rule_id: RuleId,
location: Location,
severity: Severity,
confidence: Confidence,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
remediation: Option<Remediation>,
}
impl Finding {
pub(crate) const fn new(
rule_id: RuleId,
location: Location,
severity: Severity,
confidence: Confidence,
remediation: Option<Remediation>,
) -> Self {
Self {
rule_id,
location,
severity,
confidence,
remediation,
}
}
#[must_use]
pub const fn rule_id(&self) -> &RuleId {
&self.rule_id
}
#[must_use]
pub const fn location(&self) -> &Location {
&self.location
}
#[must_use]
pub const fn severity(&self) -> Severity {
self.severity
}
#[must_use]
pub const fn confidence(&self) -> Confidence {
self.confidence
}
#[must_use]
pub const fn remediation(&self) -> Option<Remediation> {
self.remediation
}
#[must_use]
pub fn explanation(&self, scanner: &Scanner) -> Option<Explanation> {
Explanation::for_finding(scanner, self)
}
#[must_use]
pub const fn is_critical(&self) -> bool {
matches!(self.severity, Severity::Critical)
}
#[must_use]
pub const fn is_high_priority(&self) -> bool {
self.severity.is_high_priority()
}
#[must_use]
pub const fn is_high_confidence(&self) -> bool {
self.confidence.is_high()
}
}
impl fmt::Display for Finding {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{} · {} confidence · {} · {}:{}",
self.severity,
self.confidence,
self.rule_id,
self.location.line(),
self.location.column(),
)?;
if let Some(remediation) = self.remediation {
write!(formatter, " · {}", remediation.label())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DetectionMode, Rule};
#[test]
fn exposes_immutable_finding_metadata() {
let finding = Finding::new(
RuleId::from("example"),
Location::from_span(2, 8),
Severity::High,
Confidence::High,
None,
);
assert_eq!(finding.rule_id().as_str(), "example");
assert_eq!(finding.location().start(), 2);
assert_eq!(finding.location().end(), 8);
assert_eq!(finding.severity(), Severity::High);
assert_eq!(finding.confidence(), Confidence::High);
assert_eq!(finding.remediation(), None);
assert!(!finding.is_critical());
assert!(finding.is_high_priority());
assert!(finding.is_high_confidence());
}
#[test]
fn resolves_explanation_without_storing_duplicate_rule_metadata() {
let scanner = Scanner::builder()
.rule(Rule::literal("example", "secret", Severity::High))
.build()
.expect("scanner should compile");
let results = scanner.scan([("memory", "secret")]);
let finding = &results
.single_report()
.expect("one source was scanned")
.findings()[0];
assert_eq!(
finding.explanation(&scanner),
Some(Explanation::Classified(DetectionMode::MatcherOnly))
);
}
#[test]
fn explanation_fails_closed_against_unrelated_scanner() {
let scanner = Scanner::builder().build().expect("scanner should compile");
let finding = Finding::new(
RuleId::from("example"),
Location::from_span(2, 8),
Severity::High,
Confidence::High,
None,
);
assert_eq!(finding.explanation(&scanner), None);
}
#[test]
fn detects_critical_findings() {
let finding = Finding::new(
RuleId::from("critical"),
Location::from_span(0, 1),
Severity::Critical,
Confidence::Medium,
Some(Remediation::RemoveSensitiveValue),
);
assert!(finding.is_critical());
assert_eq!(
finding.remediation(),
Some(Remediation::RemoveSensitiveValue),
);
assert!(finding.is_high_priority());
assert!(!finding.is_high_confidence());
}
}