Skip to main content

forensicnomicon_data/
volatility.rs

1//! Artifact volatility helpers over the assembled global catalog.
2//!
3//! The [`VolatilityClass`] rating type (RFC 3227 Order of Volatility) is defined in
4//! `forensicnomicon-core` and re-exported here (so `crate::volatility::VolatilityClass`
5//! resolves throughout the descriptor dataset). The helpers below resolve against
6//! the global [`crate::catalog::CATALOG`].
7//!
8//! Use [`volatility_for`] for point-lookups and [`acquisition_order`] to get a
9//! live-response collection order (most ephemeral first).
10
11pub use forensicnomicon_core::volatility::VolatilityClass;
12
13/// Returns the descriptor for a given artifact ID if it has been assessed.
14///
15/// Returns `None` when the artifact ID is unknown or has no volatility assessment.
16/// On `Some(d)`, `d.volatility` and `d.volatility_rationale` are guaranteed non-None/non-empty.
17pub fn volatility_for(artifact_id: &str) -> Option<&'static crate::catalog::ArtifactDescriptor> {
18    crate::catalog::CATALOG
19        .by_id(artifact_id)
20        .filter(|d| d.volatility.is_some())
21}
22
23/// Returns all assessed descriptors sorted most-volatile-first (Volatile → Residual).
24/// Ties broken by artifact ID for determinism.
25///
26/// Use this for live-response triage: collect in the returned order to capture
27/// the most ephemeral evidence before it is lost (RFC 3227).
28pub fn acquisition_order() -> Vec<&'static crate::catalog::ArtifactDescriptor> {
29    let mut entries: Vec<&crate::catalog::ArtifactDescriptor> = crate::catalog::CATALOG
30        .list()
31        .iter()
32        .filter(|d| d.volatility.is_some())
33        .collect();
34    entries.sort_by(|a, b| b.volatility.cmp(&a.volatility).then(a.id.cmp(b.id)));
35    entries
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use crate::catalog::CATALOG;
42
43    #[test]
44    fn volatility_for_returns_descriptor() {
45        let result: Option<&crate::catalog::ArtifactDescriptor> = volatility_for("shimcache");
46        assert!(result.is_some());
47    }
48
49    #[test]
50    fn shimcache_is_persistent() {
51        let entry = volatility_for("shimcache").expect("shimcache should be assessed");
52        assert_eq!(entry.volatility, Some(VolatilityClass::Persistent));
53    }
54
55    #[test]
56    fn shimcache_memory_is_volatile() {
57        let entry =
58            volatility_for("shimcache_memory").expect("shimcache_memory should be assessed");
59        assert_eq!(entry.volatility, Some(VolatilityClass::Volatile));
60    }
61
62    #[test]
63    fn mft_is_residual() {
64        let entry = volatility_for("mft_file").expect("mft_file should be assessed");
65        assert_eq!(entry.volatility, Some(VolatilityClass::Residual));
66    }
67
68    #[test]
69    fn evtx_security_is_rotating_buffer() {
70        let entry = volatility_for("evtx_security").expect("evtx_security should be assessed");
71        assert_eq!(entry.volatility, Some(VolatilityClass::RotatingBuffer));
72    }
73
74    #[test]
75    fn acquisition_order_volatile_first() {
76        let order = acquisition_order();
77        assert!(!order.is_empty());
78        assert_eq!(
79            order[0].volatility,
80            Some(VolatilityClass::Volatile),
81            "acquisition_order should start with Volatile class"
82        );
83        assert_eq!(
84            order.last().unwrap().volatility,
85            Some(VolatilityClass::Residual),
86            "acquisition_order should end with Residual class"
87        );
88    }
89
90    #[test]
91    fn unknown_artifact_returns_none() {
92        assert!(volatility_for("this_does_not_exist").is_none());
93    }
94
95    #[test]
96    fn table_covers_critical_triage_artifacts() {
97        let missing: Vec<&str> = CATALOG
98            .for_triage()
99            .into_iter()
100            .filter(|d| d.triage_priority == crate::catalog::TriagePriority::Critical)
101            .filter(|d| volatility_for(d.id).is_none())
102            .map(|d| d.id)
103            .collect();
104        assert!(
105            missing.is_empty(),
106            "Critical-priority artifacts missing from volatility table: {missing:?}"
107        );
108    }
109}