#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum VolatilityClass {
Residual = 0,
Persistent = 1,
ActivityDriven = 2,
RotatingBuffer = 3,
Volatile = 4,
}
pub fn volatility_for(artifact_id: &str) -> Option<&'static crate::catalog::ArtifactDescriptor> {
crate::catalog::CATALOG
.by_id(artifact_id)
.filter(|d| d.volatility.is_some())
}
pub fn acquisition_order() -> Vec<&'static crate::catalog::ArtifactDescriptor> {
let mut entries: Vec<&crate::catalog::ArtifactDescriptor> = crate::catalog::CATALOG
.list()
.iter()
.filter(|d| d.volatility.is_some())
.collect();
entries.sort_by(|a, b| b.volatility.cmp(&a.volatility).then(a.id.cmp(b.id)));
entries
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::CATALOG;
#[test]
fn volatility_for_returns_descriptor() {
let result: Option<&crate::catalog::ArtifactDescriptor> = volatility_for("shimcache");
assert!(result.is_some());
}
#[test]
fn shimcache_is_persistent() {
let entry = volatility_for("shimcache").expect("shimcache should be assessed");
assert_eq!(entry.volatility, Some(VolatilityClass::Persistent));
}
#[test]
fn shimcache_memory_is_volatile() {
let entry =
volatility_for("shimcache_memory").expect("shimcache_memory should be assessed");
assert_eq!(entry.volatility, Some(VolatilityClass::Volatile));
}
#[test]
fn mft_is_residual() {
let entry = volatility_for("mft_file").expect("mft_file should be assessed");
assert_eq!(entry.volatility, Some(VolatilityClass::Residual));
}
#[test]
fn evtx_security_is_rotating_buffer() {
let entry = volatility_for("evtx_security").expect("evtx_security should be assessed");
assert_eq!(entry.volatility, Some(VolatilityClass::RotatingBuffer));
}
#[test]
fn acquisition_order_volatile_first() {
let order = acquisition_order();
assert!(!order.is_empty());
assert_eq!(
order[0].volatility,
Some(VolatilityClass::Volatile),
"acquisition_order should start with Volatile class"
);
assert_eq!(
order.last().unwrap().volatility,
Some(VolatilityClass::Residual),
"acquisition_order should end with Residual class"
);
}
#[test]
fn unknown_artifact_returns_none() {
assert!(volatility_for("this_does_not_exist").is_none());
}
#[test]
fn table_covers_critical_triage_artifacts() {
let missing: Vec<&str> = CATALOG
.for_triage()
.into_iter()
.filter(|d| d.triage_priority == crate::catalog::TriagePriority::Critical)
.filter(|d| volatility_for(d.id).is_none())
.map(|d| d.id)
.collect();
assert!(
missing.is_empty(),
"Critical-priority artifacts missing from volatility table: {missing:?}"
);
}
#[test]
fn volatility_ordering_is_consistent() {
assert!(VolatilityClass::Volatile > VolatilityClass::RotatingBuffer);
assert!(VolatilityClass::RotatingBuffer > VolatilityClass::ActivityDriven);
assert!(VolatilityClass::ActivityDriven > VolatilityClass::Persistent);
assert!(VolatilityClass::Persistent > VolatilityClass::Residual);
}
}