Skip to main content

disk_forensic/
normalize.rs

1//! Normalize each scheme's native analysis into the shared
2//! [`forensicnomicon::report`] model, so disk4n6 (and a future GUI) render one
3//! uniform [`Report`] instead of N bespoke `XxxAnalysis` types.
4
5use forensicnomicon::report::{Category, Finding, Location, Provenance, Report, Source};
6
7use crate::DiskReport;
8
9/// Coarse forensic category derived from a finding's stable code. A pragmatic
10/// first pass (keyword-based); refined per-analyzer over time.
11fn classify(code: &str) -> Category {
12    let c = code.to_ascii_uppercase();
13    if c.contains("CRC") || c.contains("INTEGRITY") {
14        Category::Integrity
15    } else if c.contains("OVERLAP")
16        || c.contains("OOB")
17        || c.contains("BOUND")
18        || c.contains("CHS")
19        || c.contains("MAP-COUNT")
20    {
21        Category::Structure
22    } else if c.contains("RESIDUAL")
23        || c.contains("SLACK")
24        || c.contains("GAP")
25        || c.contains("CARVE")
26        || c.contains("UNMAPPED")
27        || c.contains("ZEROLEN")
28    {
29        Category::Residue
30    } else if c.contains("HIDDEN")
31        || c.contains("CONCEAL")
32        || c.contains("WIPED")
33        || c.contains("ERASED")
34        || c.contains("PROTECTIVE")
35    {
36        Category::Concealment
37    } else if c.contains("BOOT") {
38        Category::Threat
39    } else {
40        Category::Structure
41    }
42}
43
44// Since 0.4.0 every analyzer re-exports `forensicnomicon::report::Severity` as
45// its own `Severity`, so an anomaly's severity is already the canonical type —
46// no per-scheme translation is needed.
47
48/// Normalize an MBR analysis. Findings carry their byte offset as evidence.
49#[must_use]
50pub fn mbr_findings(a: &mbr_forensic::MbrAnalysis) -> Vec<Finding> {
51    a.anomalies
52        .iter()
53        .map(|an| {
54            Finding::observation(an.severity, classify(an.code), an.code.to_string())
55                .note(an.note.clone())
56                .source(Source {
57                    analyzer: "mbr-forensic".to_string(),
58                    scope: "MBR".to_string(),
59                    version: None,
60                })
61                .evidence_at(
62                    "offset",
63                    format!("{:#x}", an.offset),
64                    Location::ByteOffset(an.offset),
65                )
66                .build()
67        })
68        .collect()
69}
70
71/// Normalize a GPT analysis.
72#[must_use]
73pub fn gpt_findings(a: &gpt_forensic::GptAnalysis) -> Vec<Finding> {
74    a.anomalies
75        .iter()
76        .map(|an| {
77            Finding::observation(an.severity, classify(an.code), an.code.to_string())
78                .note(an.note.clone())
79                .source(Source {
80                    analyzer: "gpt-forensic".to_string(),
81                    scope: "GPT".to_string(),
82                    version: None,
83                })
84                .build()
85        })
86        .collect()
87}
88
89/// Normalize an Apple Partition Map analysis.
90#[must_use]
91pub fn apm_findings(a: &apm_forensic::ApmAnalysis) -> Vec<Finding> {
92    a.anomalies
93        .iter()
94        .map(|an| {
95            Finding::observation(an.severity, classify(an.code), an.code.to_string())
96                .note(an.note.clone())
97                .source(Source {
98                    analyzer: "apm-forensic".to_string(),
99                    scope: "APM".to_string(),
100                    version: None,
101                })
102                .build()
103        })
104        .collect()
105}
106
107/// Provenance breadcrumbs from an MBR analysis.
108#[must_use]
109pub fn mbr_provenance(a: &mbr_forensic::MbrAnalysis) -> Vec<Provenance> {
110    vec![
111        Provenance {
112            label: "boot code".to_string(),
113            value: format!("{:?}", a.boot_code_id),
114            source: "mbr-forensic".to_string(),
115        },
116        Provenance {
117            label: "partitioning era".to_string(),
118            value: format!("{:?}", a.era),
119            source: "mbr-forensic".to_string(),
120        },
121        Provenance {
122            label: "disk signature".to_string(),
123            value: format!("{:#010x}", a.disk_serial),
124            source: "mbr-forensic".to_string(),
125        },
126    ]
127}
128
129/// Provenance breadcrumbs from a GPT analysis.
130#[must_use]
131pub fn gpt_provenance(a: &gpt_forensic::GptAnalysis) -> Vec<Provenance> {
132    vec![
133        Provenance {
134            label: "disk GUID".to_string(),
135            value: a.disk_guid.to_string(),
136            source: "gpt-forensic".to_string(),
137        },
138        Provenance {
139            label: "sector size".to_string(),
140            value: format!("{} bytes", a.sector_size),
141            source: "gpt-forensic".to_string(),
142        },
143        Provenance {
144            label: "GPT SHA-256".to_string(),
145            value: a.gpt_sha256.clone(),
146            source: "gpt-forensic".to_string(),
147        },
148    ]
149}
150
151/// Provenance breadcrumbs from an APM analysis.
152#[must_use]
153pub fn apm_provenance(a: &apm_forensic::ApmAnalysis) -> Vec<Provenance> {
154    vec![
155        Provenance {
156            label: "block size".to_string(),
157            value: format!("{} bytes", a.block_size),
158            source: "apm-forensic".to_string(),
159        },
160        Provenance {
161            label: "device blocks".to_string(),
162            value: a.device_block_count.to_string(),
163            source: "apm-forensic".to_string(),
164        },
165    ]
166}
167
168/// Build the unified [`Report`] from a [`DiskReport`]. A GPT disk contributes
169/// both its protective-MBR and parsed-GPT findings and provenance.
170#[must_use]
171pub fn report(disk: &DiskReport) -> Report {
172    let (findings, provenance) = match disk {
173        DiskReport::Apm(a) => (apm_findings(a), apm_provenance(a)),
174        DiskReport::Mbr(m) => (mbr_findings(m), mbr_provenance(m)),
175        DiskReport::Gpt(m) => {
176            let mut findings = mbr_findings(m);
177            let mut provenance = mbr_provenance(m);
178            if let Some(gpt) = &m.gpt {
179                findings.extend(gpt_findings(gpt));
180                provenance.extend(gpt_provenance(gpt));
181            }
182            (findings, provenance)
183        }
184    };
185    let mut out = Report::default();
186    out.findings = findings;
187    out.provenance = provenance;
188    out
189}