use forensicnomicon::report::{Category, Finding, Severity, Source};
use livedisk::PhysicalDisk;
const ANALYZER: &str = "livedisk-forensic";
fn source(disk: &PhysicalDisk) -> Source {
Source {
analyzer: ANALYZER.to_string(),
scope: disk.name.clone(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}
}
#[must_use]
pub fn analyse(disk: &PhysicalDisk) -> Vec<Finding> {
let mut findings = Vec::new();
let mounted = disk
.partitions
.iter()
.filter(|p| p.mount_point.is_some())
.count();
if mounted > 0 {
let mut builder = Finding::observation(Severity::High, Category::Integrity, "LIVE-MOUNTED")
.source(source(disk))
.note(
"device has mounted volume(s) during acquisition; live writes may alter the \
image — consistent with imaging a running system",
);
for p in &disk.partitions {
if let Some(mount) = &p.mount_point {
builder = builder.evidence(p.name.clone(), mount.clone());
}
}
findings.push(builder.build());
}
if !disk.read_only {
findings.push(
Finding::observation(Severity::Medium, Category::Integrity, "LIVE-WRITABLE")
.source(source(disk))
.note(
"device is writable (no hardware write-blocker detected); acquisition can \
alter the evidence",
)
.build(),
);
}
if disk.removable {
findings.push(
Finding::observation(Severity::Info, Category::Provenance, "LIVE-REMOVABLE")
.source(source(disk))
.note("removable media")
.build(),
);
}
if disk.logical_sector_size > 0 && disk.physical_sector_size != disk.logical_sector_size {
findings.push(
Finding::observation(Severity::Info, Category::Structure, "LIVE-SECTOR-4KN")
.source(source(disk))
.note(
"logical and physical sector sizes differ (512e/4Kn); align imaging to the \
physical sector size",
)
.evidence("logical_sector_size", disk.logical_sector_size.to_string())
.evidence(
"physical_sector_size",
disk.physical_sector_size.to_string(),
)
.build(),
);
}
if disk.synthesized {
findings.push(
Finding::observation(Severity::Info, Category::Provenance, "LIVE-SYNTHESIZED")
.source(source(disk))
.note(
"synthesized device (container overlay), not a backing physical store; image \
the underlying physical disk",
)
.build(),
);
}
findings
}
#[cfg(test)]
mod tests {
use super::*;
use livedisk::Partition;
fn clean_disk() -> PhysicalDisk {
PhysicalDisk {
device_path: "/dev/disk0".into(),
name: "disk0".into(),
size_bytes: 1_000_000_000_000,
logical_sector_size: 512,
physical_sector_size: 512,
model: Some("WRITE BLOCKED".into()),
serial: None,
removable: false,
read_only: true,
synthesized: false,
partitions: vec![],
}
}
fn codes(findings: &[Finding]) -> Vec<&str> {
findings.iter().map(|f| f.code.as_ref()).collect()
}
#[test]
fn clean_write_protected_disk_has_no_findings() {
assert!(analyse(&clean_disk()).is_empty());
}
#[test]
fn writable_disk_flags_live_writable_medium() {
let mut d = clean_disk();
d.read_only = false;
let findings = analyse(&d);
let f = findings.iter().find(|f| f.code == "LIVE-WRITABLE").unwrap();
assert_eq!(f.severity, Some(Severity::Medium));
assert_eq!(f.source.analyzer, "livedisk-forensic");
assert_eq!(f.source.scope, "disk0");
}
#[test]
fn mounted_disk_flags_live_mounted_high_with_evidence() {
let mut d = clean_disk();
d.partitions = vec![Partition {
device_path: "/dev/disk0s1".into(),
name: "disk0s1".into(),
start_offset: 0,
size_bytes: 1,
partition_type: None,
mount_point: Some("/Volumes/Data".into()),
filesystem: None,
label: None,
}];
let findings = analyse(&d);
let f = findings.iter().find(|f| f.code == "LIVE-MOUNTED").unwrap();
assert_eq!(f.severity, Some(Severity::High));
assert!(f.evidence.iter().any(|e| e.value == "/Volumes/Data"));
}
#[test]
fn removable_disk_flags_live_removable_info() {
let mut d = clean_disk();
d.removable = true;
assert!(codes(&analyse(&d)).contains(&"LIVE-REMOVABLE"));
}
#[test]
fn sector_mismatch_flags_4kn_with_both_sizes() {
let mut d = clean_disk();
d.logical_sector_size = 512;
d.physical_sector_size = 4096;
let findings = analyse(&d);
let f = findings
.iter()
.find(|f| f.code == "LIVE-SECTOR-4KN")
.unwrap();
assert!(f.evidence.iter().any(|e| e.value == "4096"));
assert!(f.evidence.iter().any(|e| e.value == "512"));
}
#[test]
fn synthesized_disk_flags_live_synthesized() {
let mut d = clean_disk();
d.synthesized = true;
assert!(codes(&analyse(&d)).contains(&"LIVE-SYNTHESIZED"));
}
}