Skip to main content

forensicnomicon_data/
evidence.rs

1//! Evidence strength helpers over the assembled global catalog.
2//!
3//! The [`EvidenceStrength`] rating type is defined in `forensicnomicon-core` and
4//! re-exported here (so `crate::evidence::EvidenceStrength` resolves throughout the
5//! descriptor dataset). The point-lookup and bulk-query helpers below resolve
6//! against the global [`crate::catalog::CATALOG`].
7//!
8//! Use [`evidence_for`] for point-lookups, [`artifacts_with_strength`] for bulk
9//! queries, and [`crate::catalog::ForensicCatalog::unassessed`] (on the global
10//! [`crate::catalog::CATALOG`]) to find gaps sorted by
11//! triage priority.
12
13pub use forensicnomicon_core::evidence::EvidenceStrength;
14
15/// Returns the descriptor for a given artifact ID if it has been assessed.
16///
17/// Returns `None` when the artifact ID is unknown or has no evidence assessment yet.
18/// On `Some(d)`, callers may access `d.evidence_strength`, `d.evidence_caveats`,
19/// `d.volatility`, and `d.volatility_rationale` directly — all are guaranteed `Some`.
20pub fn evidence_for(artifact_id: &str) -> Option<&'static crate::catalog::ArtifactDescriptor> {
21    crate::catalog::CATALOG
22        .by_id(artifact_id)
23        .filter(|d| d.evidence_strength.is_some())
24}
25
26/// Returns all assessed descriptors with evidence strength at or above `min_strength`.
27pub fn artifacts_with_strength(
28    min_strength: EvidenceStrength,
29) -> Vec<&'static crate::catalog::ArtifactDescriptor> {
30    crate::catalog::CATALOG
31        .list()
32        .iter()
33        .filter(|d| d.evidence_strength.is_some_and(|s| s >= min_strength))
34        .collect()
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::catalog::{TriagePriority, CATALOG};
41
42    #[test]
43    fn evidence_for_returns_descriptor() {
44        let result: Option<&crate::catalog::ArtifactDescriptor> = evidence_for("shimcache");
45        assert!(result.is_some());
46    }
47
48    #[test]
49    fn prefetch_is_definitive() {
50        let e = evidence_for("prefetch_file").expect("prefetch_file must be assessed");
51        assert_eq!(e.evidence_strength, Some(EvidenceStrength::Definitive));
52    }
53
54    #[test]
55    fn bash_history_is_circumstantial() {
56        let e = evidence_for("linux_bash_history").expect("linux_bash_history must be assessed");
57        assert_eq!(e.evidence_strength, Some(EvidenceStrength::Circumstantial));
58    }
59
60    #[test]
61    fn definitive_entries_have_caveats() {
62        for d in CATALOG.list() {
63            if d.evidence_strength == Some(EvidenceStrength::Definitive) {
64                assert!(
65                    !d.evidence_caveats.is_empty(),
66                    "{} is Definitive but has no caveats documented",
67                    d.id
68                );
69            }
70        }
71    }
72
73    #[test]
74    fn filter_by_strength_returns_subset() {
75        let definitive = artifacts_with_strength(EvidenceStrength::Definitive);
76        let all = artifacts_with_strength(EvidenceStrength::Unreliable);
77        assert!(definitive.len() < all.len());
78        assert!(definitive
79            .iter()
80            .all(|d| d.evidence_strength == Some(EvidenceStrength::Definitive)));
81    }
82
83    #[test]
84    fn unknown_artifact_returns_none() {
85        assert!(evidence_for("this_does_not_exist").is_none());
86    }
87
88    #[test]
89    fn critical_triage_artifacts_have_evidence_entries() {
90        let missing: Vec<&str> = CATALOG
91            .for_triage()
92            .into_iter()
93            .filter(|d| d.triage_priority == TriagePriority::Critical)
94            .filter(|d| evidence_for(d.id).is_none())
95            .map(|d| d.id)
96            .collect();
97        assert!(
98            missing.is_empty(),
99            "Critical-priority artifacts missing from evidence table: {missing:?}"
100        );
101    }
102}