forensicnomicon 0.2.2

The ForensicNomicon — comprehensive DFIR artifact catalog: UserAssist, Shimcache, Amcache, Prefetch, $MFT, ShellBags, EVTX, NTDS.dit, SAM, SRUM, LNK, Jump Lists + KAPE/Velociraptor/Sigma/MITRE. Zero deps.
Documentation
//! Evidence strength / confidence model for forensic artifacts.
//!
//! Ratings live directly in [`crate::catalog::ArtifactDescriptor::evidence_strength`]
//! (populated for assessed entries; `None` for generated entries awaiting review).
//!
//! Use [`evidence_for`] for point-lookups, [`artifacts_with_strength`] for bulk queries,
//! and [`crate::catalog::CATALOG::unassessed`] to find gaps sorted by triage priority.

/// How strongly an artifact proves a fact in isolation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EvidenceStrength {
    /// Known false-positive generator; use only with strong corroboration.
    Unreliable = 0,
    /// Suggestive but easily explained by benign activity.
    Circumstantial = 1,
    /// Useful with other evidence; not standalone proof.
    Corroborative = 2,
    /// Strong evidence; edge-case alternative explanations exist.
    Strong = 3,
    /// Definitive proof of the claimed activity (e.g., Prefetch = execution occurred).
    Definitive = 4,
}

/// Returns the descriptor for a given artifact ID if it has been assessed.
///
/// Returns `None` when the artifact ID is unknown or has no evidence assessment yet.
/// On `Some(d)`, callers may access `d.evidence_strength`, `d.evidence_caveats`,
/// `d.volatility`, and `d.volatility_rationale` directly — all are guaranteed `Some`.
pub fn evidence_for(artifact_id: &str) -> Option<&'static crate::catalog::ArtifactDescriptor> {
    crate::catalog::CATALOG
        .by_id(artifact_id)
        .filter(|d| d.evidence_strength.is_some())
}

/// Returns all assessed descriptors with evidence strength at or above `min_strength`.
pub fn artifacts_with_strength(
    min_strength: EvidenceStrength,
) -> Vec<&'static crate::catalog::ArtifactDescriptor> {
    crate::catalog::CATALOG
        .list()
        .iter()
        .filter(|d| d.evidence_strength.map_or(false, |s| s >= min_strength))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::catalog::{TriagePriority, CATALOG};

    #[test]
    fn evidence_for_returns_descriptor() {
        let result: Option<&crate::catalog::ArtifactDescriptor> = evidence_for("shimcache");
        assert!(result.is_some());
    }

    #[test]
    fn prefetch_is_definitive() {
        let e = evidence_for("prefetch_file").expect("prefetch_file must be assessed");
        assert_eq!(e.evidence_strength, Some(EvidenceStrength::Definitive));
    }

    #[test]
    fn bash_history_is_circumstantial() {
        let e = evidence_for("linux_bash_history").expect("linux_bash_history must be assessed");
        assert_eq!(e.evidence_strength, Some(EvidenceStrength::Circumstantial));
    }

    #[test]
    fn definitive_entries_have_caveats() {
        for d in CATALOG.list() {
            if d.evidence_strength == Some(EvidenceStrength::Definitive) {
                assert!(
                    !d.evidence_caveats.is_empty(),
                    "{} is Definitive but has no caveats documented",
                    d.id
                );
            }
        }
    }

    #[test]
    fn strength_ordering_is_consistent() {
        assert!(EvidenceStrength::Definitive > EvidenceStrength::Strong);
        assert!(EvidenceStrength::Strong > EvidenceStrength::Corroborative);
        assert!(EvidenceStrength::Corroborative > EvidenceStrength::Circumstantial);
        assert!(EvidenceStrength::Circumstantial > EvidenceStrength::Unreliable);
    }

    #[test]
    fn filter_by_strength_returns_subset() {
        let definitive = artifacts_with_strength(EvidenceStrength::Definitive);
        let all = artifacts_with_strength(EvidenceStrength::Unreliable);
        assert!(definitive.len() < all.len());
        assert!(definitive
            .iter()
            .all(|d| d.evidence_strength == Some(EvidenceStrength::Definitive)));
    }

    #[test]
    fn unknown_artifact_returns_none() {
        assert!(evidence_for("this_does_not_exist").is_none());
    }

    #[test]
    fn critical_triage_artifacts_have_evidence_entries() {
        let missing: Vec<&str> = CATALOG
            .for_triage()
            .into_iter()
            .filter(|d| d.triage_priority == TriagePriority::Critical)
            .filter(|d| evidence_for(d.id).is_none())
            .map(|d| d.id)
            .collect();
        assert!(
            missing.is_empty(),
            "Critical-priority artifacts missing from evidence table: {missing:?}"
        );
    }
}