use crate::{DeviceHistory, Value};
use forensicnomicon::report::Finding;
#[must_use]
pub fn format_epoch(secs: i64) -> String {
let days = secs.div_euclid(86_400);
let tod = secs.rem_euclid(86_400);
let (hour, min, sec) = (tod / 3600, (tod % 3600) / 60, tod % 60);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let year = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = doy - (153 * mp + 2) / 5 + 1; let month = if mp < 10 { mp + 3 } else { mp - 9 }; let year = if month <= 2 { year + 1 } else { year };
format!("{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02} UTC")
}
fn render_value(value: &Value) -> String {
match value {
Value::Timestamp(secs) => format_epoch(*secs),
Value::Text(text) => text.clone(),
}
}
#[must_use]
pub fn render_table(histories: &[DeviceHistory]) -> String {
use std::fmt::Write as _;
let mut out = String::new();
for history in histories {
let _ = writeln!(out, "Device: {}", history.device.0);
for attr in &history.attributes {
let values: Vec<String> = attr
.values
.iter()
.map(|pv| render_value(&pv.value))
.collect();
let _ = writeln!(
out,
" {:<16} {:<13} {}",
format!("{:?}", attr.attribute),
attr.consistency.label(),
values.join(", ")
);
}
}
out
}
#[must_use]
pub fn render_accessed_files(histories: &[DeviceHistory]) -> String {
use crate::model::Attribute;
use std::fmt::Write as _;
let mut out = String::new();
for history in histories {
let files: Vec<&crate::ProvenancedValue> = history
.attributes
.iter()
.filter(|a| a.attribute == Attribute::AccessedFile)
.flat_map(|a| a.values.iter())
.collect();
if files.is_empty() {
continue;
}
let _ = writeln!(out, "Device: {}", history.device.0);
for pv in files {
let _ = writeln!(
out,
" {}\t[{:?} {}]",
render_value(&pv.value),
pv.provenance.source,
pv.provenance.locator
);
}
}
out
}
fn cell(text: &str) -> String {
text.replace('|', "\\|")
}
#[must_use]
pub fn render_report(histories: &[DeviceHistory], findings: &[Finding]) -> String {
use std::fmt::Write as _;
let mut r = String::new();
let _ = writeln!(r, "# USB Device History — Forensic Report\n");
let _ = writeln!(r, "## Executive Summary\n");
let _ = writeln!(
r,
"Reconstructed {} device history record(s) from correlated evidence; \
{} finding(s) surfaced. Every reported value retains its source and locator. \
Findings are observations (\"consistent with\"), not conclusions.\n",
histories.len(),
findings.len()
);
let _ = writeln!(r, "## Devices\n");
for history in histories {
let _ = writeln!(r, "### Device: {}\n", history.device.0);
let _ = writeln!(r, "| Attribute | Consistency | Value | Source | Locator |");
let _ = writeln!(r, "|---|---|---|---|---|");
for attr in &history.attributes {
for pv in &attr.values {
let _ = writeln!(
r,
"| {:?} | {} | {} | {:?} | {} |",
attr.attribute,
attr.consistency.label(),
cell(&render_value(&pv.value)),
pv.provenance.source,
cell(&pv.provenance.locator),
);
}
}
let _ = writeln!(r);
}
let _ = writeln!(r, "## Findings\n");
if findings.is_empty() {
let _ = writeln!(r, "No cross-source conflicts or corroborations surfaced.\n");
} else {
for finding in findings {
let _ = writeln!(
r,
"- **[{:?}] {}** — {}",
finding.severity, finding.code, finding.note
);
}
let _ = writeln!(r);
}
let _ = writeln!(r, "## Methodology & Limitations\n");
let _ = writeln!(
r,
"- Values are graded by tamper-independent storage container: agreement across \
sources sharing one container is not counted as corroboration.\n\
- A conflict is reported as \"not consistent with\", never as proven tampering; \
every value is shown with its source so it can be independently verified.\n\
- The findings are observations of the evidence; the Court may draw its own \
conclusions."
);
r
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Attribute, DeviceKey, Provenance, SourceKind};
use crate::{audit, correlate, Claim};
#[test]
fn format_epoch_matches_the_date_oracle() {
assert_eq!(format_epoch(0), "1970-01-01 00:00:00 UTC"); assert_eq!(format_epoch(1_681_760_520), "2023-04-17 19:42:00 UTC"); assert_eq!(format_epoch(1_600_357_894), "2020-09-17 15:51:34 UTC");
}
#[test]
fn table_renders_a_block_per_device_with_formatted_values() {
let claims = [
Claim {
device: DeviceKey("SN1".into()),
attribute: Attribute::FirstConnected,
value: Value::Timestamp(1_681_760_520),
provenance: Provenance {
source: SourceKind::SetupApi,
locator: "l".into(),
},
},
Claim {
device: DeviceKey("SN1".into()),
attribute: Attribute::VolumeName,
value: Value::Text("KINGSTON".into()),
provenance: Provenance {
source: SourceKind::MountedDevices,
locator: "m".into(),
},
},
];
let table = render_table(&correlate(&claims));
assert!(table.contains("Device: SN1"));
assert!(
table.contains("2023-04-17 19:42:00 UTC"),
"timestamp rendered human"
);
assert!(table.contains("KINGSTON"), "text value rendered");
assert!(table.contains("single-source"), "consistency labelled");
}
fn claim(dev: &str, attr: Attribute, v: Value, src: SourceKind, loc: &str) -> Claim {
Claim {
device: DeviceKey(dev.into()),
attribute: attr,
value: v,
provenance: Provenance {
source: src,
locator: loc.into(),
},
}
}
#[test]
fn accessed_files_lists_opened_files_per_device_and_omits_devices_without_any() {
let claims = [
claim(
"SN1",
Attribute::AccessedFile,
Value::Text("E:\\secret.docx".into()),
SourceKind::Lnk,
"recent\\secret.lnk",
),
claim(
"SN2",
Attribute::FirstConnected,
Value::Timestamp(1),
SourceKind::Usbstor,
"k",
),
];
let report = render_accessed_files(&correlate(&claims));
assert!(report.contains("Device: SN1"));
assert!(report.contains("E:\\secret.docx"));
assert!(report.contains("recent\\secret.lnk"), "locator shown");
assert!(
!report.contains("SN2"),
"device with no file access omitted"
);
}
#[test]
fn accessed_files_is_empty_when_no_files_were_opened() {
let claims = [claim(
"SN1",
Attribute::FirstConnected,
Value::Timestamp(1),
SourceKind::Usbstor,
"k",
)];
assert!(render_accessed_files(&correlate(&claims)).is_empty());
}
#[test]
fn report_has_provenance_table_and_findings_with_hedged_language() {
let claims = [
claim(
"SN1",
Attribute::FirstConnected,
Value::Timestamp(1_681_760_520),
SourceKind::Usbstor,
"k",
),
claim(
"SN1",
Attribute::FirstConnected,
Value::Timestamp(1_600_357_894),
SourceKind::SetupApi,
"setupapi:9",
),
];
let histories = correlate(&claims);
let report = render_report(&histories, &audit(&histories));
assert!(report.contains("# USB Device History — Forensic Report"));
assert!(report.contains("### Device: SN1"));
assert!(report.contains("| Attribute | Consistency | Value | Source | Locator |"));
assert!(
report.contains("setupapi:9"),
"locator appears in the provenance table"
);
assert!(
report.contains("USB-TIMESTAMP-CONFLICT"),
"the conflict finding is listed"
);
assert!(
report.contains("consistent with"),
"hedged, non-conclusive language"
);
assert!(report.contains("Court may draw its own conclusions"));
}
#[test]
fn report_with_no_findings_says_so() {
let report = render_report(&[], &[]);
assert!(report.contains("No cross-source conflicts or corroborations surfaced."));
assert_eq!(cell("a|b"), "a\\|b");
}
}