use std::collections::HashMap;
use std::hash::BuildHasher;
use crate::finding::{
DialTier, Finding, FindingBody, OriginStage, Presentation as FindingPresentation, Provenance,
Severity, cluster_key_of,
};
use crate::scan::{MatchKind, ScanReport};
const CATALOG_MATCH_SOURCE: &str = "scan:catalog-match";
#[must_use]
pub fn catalog_match_findings_with_source<S: BuildHasher>(
report: &ScanReport,
provenance_by_class: &HashMap<String, Provenance, S>,
source: &str,
) -> Vec<Finding> {
report
.presentations
.iter()
.filter(|p| p.match_kind == MatchKind::FingerprintMatch)
.filter_map(|p| {
let class_provenance = *provenance_by_class.get(&p.antigen_type)?;
Some((p, class_provenance))
})
.enumerate()
.map(|(i, (p, class_provenance))| Finding {
schema_version: crate::finding::FINDING_SCHEMA_VERSION,
file: p.file.display().to_string(),
line: p.line,
structural_digest: p.structural_fingerprint.clone(),
shape_digest: String::new(),
cluster_key: cluster_key_of(&p.structural_fingerprint, &p.antigen_type),
severity: severity_for(class_provenance),
source: source.to_string(),
class_provenance,
presentation: FindingPresentation::Passive,
timestamp: i as u64,
origin_stage: OriginStage::Scan,
body: FindingBody::FingerprintMatch {
class: p.antigen_type.clone(),
tier: DialTier::Suspected,
},
})
.collect()
}
#[must_use]
pub fn catalog_match_findings<S: BuildHasher>(
report: &ScanReport,
provenance_by_class: &HashMap<String, Provenance, S>,
) -> Vec<Finding> {
catalog_match_findings_with_source(report, provenance_by_class, CATALOG_MATCH_SOURCE)
}
const fn severity_for(provenance: Provenance) -> Severity {
match provenance {
Provenance::Constructable | Provenance::Encountered => Severity::High,
Provenance::Heuristic | Provenance::Imagined => Severity::Medium,
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::scan::{ItemTarget, Presentation};
fn fp_presentation(class: &str, file: &str, line: usize, digest: &str) -> Presentation {
Presentation {
antigen_type: class.to_string(),
file: PathBuf::from(file),
line,
item_kind: "fn".to_string(),
item_target: ItemTarget::Fn(format!("f{line}")),
match_kind: MatchKind::FingerprintMatch,
canonical_path: None,
inherited_from: None,
structural_fingerprint: digest.to_string(),
requires_predicate: None,
proof: None,
}
}
#[test]
fn projects_only_classes_in_the_provenance_map() {
let mut report = ScanReport::default();
report
.presentations
.push(fp_presentation("known-class", "a.rs", 1, "d1"));
report
.presentations
.push(fp_presentation("unknown-class", "b.rs", 2, "d2"));
let mut prov = HashMap::new();
prov.insert("known-class".to_string(), Provenance::Constructable);
let findings = catalog_match_findings(&report, &prov);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].class_provenance, Provenance::Constructable);
assert!(matches!(
&findings[0].body,
FindingBody::FingerprintMatch { class, .. } if class == "known-class"
));
}
#[test]
fn never_emits_an_audited_verdict_body() {
let mut report = ScanReport::default();
report
.presentations
.push(fp_presentation("c", "a.rs", 1, "d"));
let mut prov = HashMap::new();
prov.insert("c".to_string(), Provenance::Encountered);
let findings = catalog_match_findings(&report, &prov);
for f in &findings {
assert!(!matches!(f.body, FindingBody::DialVerdict { .. }));
assert_eq!(f.origin_stage, OriginStage::Scan);
}
}
#[test]
fn explicit_marker_presentations_are_not_projected() {
let mut report = ScanReport::default();
let mut explicit = fp_presentation("c", "a.rs", 1, "d");
explicit.match_kind = MatchKind::ExplicitMarker;
report.presentations.push(explicit);
let mut prov = HashMap::new();
prov.insert("c".to_string(), Provenance::Constructable);
let findings = catalog_match_findings(&report, &prov);
assert!(
findings.is_empty(),
"explicit markers are not catalog matches"
);
}
#[test]
fn cluster_key_is_class_at_digest() {
let mut report = ScanReport::default();
report
.presentations
.push(fp_presentation("panic-in-drop", "a.rs", 9, "fnv:abc"));
let mut prov = HashMap::new();
prov.insert("panic-in-drop".to_string(), Provenance::Constructable);
let findings = catalog_match_findings(&report, &prov);
assert_eq!(findings[0].cluster_key, "panic-in-drop@fnv:abc");
}
}