1use core::fmt::Write as _;
9
10use forensicnomicon::report::{Report, Severity};
11
12use crate::DiskReport;
13
14const SEVERITY_ORDER: [Severity; 5] = [
16 Severity::Critical,
17 Severity::High,
18 Severity::Medium,
19 Severity::Low,
20 Severity::Info,
21];
22
23#[must_use]
26pub fn render(report: &Report) -> String {
27 let mut s = String::new();
28
29 if report.findings.is_empty() {
31 s.push_str("Findings: none (clean)\n");
32 } else {
33 let _ = writeln!(s, "Forensic findings ({}):", report.findings.len());
34 for sev in SEVERITY_ORDER {
35 let group = report.findings.iter().filter(|f| f.severity == Some(sev));
36 let mut header_written = false;
37 for f in group {
38 if !header_written {
39 let _ = writeln!(s, "\n [{sev}]");
40 header_written = true;
41 }
42 let _ = writeln!(
43 s,
44 " {} ({} / {}): {}",
45 f.code, f.source.analyzer, f.source.scope, f.note
46 );
47 for e in &f.evidence {
48 let _ = writeln!(s, " {} = {}", e.field, e.value);
49 }
50 }
51 }
52 }
53
54 if !report.provenance.is_empty() {
56 s.push_str("\nProvenance:\n");
57 for p in &report.provenance {
58 let _ = writeln!(s, " {}: {} ({})", p.label, p.value, p.source);
59 }
60 }
61
62 if !report.timeline.is_empty() {
64 s.push_str("\nTimeline:\n");
65 for e in &report.timeline {
66 let when = e.when.as_deref().unwrap_or("?");
67 let _ = writeln!(s, " [{when}] {} ({})", e.event, e.source);
68 }
69 }
70
71 s
72}
73
74#[must_use]
79pub fn text_report(report: &DiskReport) -> String {
80 match report {
81 DiskReport::Apm(a) => apm_structure(a),
82 DiskReport::Mbr(m) => mbr_structure(m),
83 DiskReport::Gpt(m) => {
84 let mut s = mbr_structure(m);
85 if let Some(gpt) = &m.gpt {
86 s.push('\n');
87 s.push_str(&gpt_structure(gpt));
88 }
89 s
90 }
91 }
92}
93
94const RULE: usize = 80;
96
97fn mbr_structure(a: &mbr_forensic::MbrAnalysis) -> String {
98 let mut s = String::new();
99 let _ = writeln!(s, "MBR Forensic Analysis");
100 let _ = writeln!(s, " disk signature : {:#010x}", a.disk_serial);
101 let _ = writeln!(s, " boot code : {:?}", a.boot_code_id);
102 let _ = writeln!(s, " partitioning : {:?}", a.era);
103 let _ = writeln!(s, "\nPartition table ({} entries):", a.partitions.len());
104 if a.partitions.is_empty() {
105 let _ = writeln!(s, " (no primary partitions)");
106 }
107 for p in &a.partitions {
108 let fs = match p.detected_fs {
109 Some(fs) => format!("{fs:?}"),
110 None => "-".to_string(),
111 };
112 let _ = writeln!(
113 s,
114 " [{}] {:<24} LBA {:>12}..={:<12} fs={}",
115 p.index,
116 p.declared_type.name(),
117 p.lba_start,
118 p.lba_end,
119 fs,
120 );
121 }
122 if let Some(gpt) = &a.gpt {
123 let _ = writeln!(
124 s,
125 "\nGPT cross-check: {} GPT partition entries",
126 gpt.partitions.len()
127 );
128 }
129 s
130}
131
132fn gpt_structure(a: &gpt_forensic::GptAnalysis) -> String {
133 let mut out = String::new();
134 out.push_str("GPT Forensic Analysis\n");
135 out.push_str(&"=".repeat(RULE));
136 out.push('\n');
137 let rev_hi = a.primary.revision >> 16;
138 let rev_lo = a.primary.revision & 0xFFFF;
139 let _ = writeln!(out, "Disk GUID: {}", a.disk_guid);
140 let _ = writeln!(out, "Revision: {rev_hi}.{rev_lo}");
141 let _ = writeln!(
142 out,
143 "Header CRC: {}",
144 if a.primary.header_crc_valid {
145 "valid"
146 } else {
147 "INVALID"
148 }
149 );
150 let _ = writeln!(
151 out,
152 "Usable LBAs: {}..{}",
153 a.primary.first_usable_lba, a.primary.last_usable_lba
154 );
155 let _ = writeln!(out, "Sector size: {} bytes", a.sector_size);
156 let _ = writeln!(out, "GPT SHA-256: {}", a.gpt_sha256);
157 match &a.backup {
158 Some(b) => {
159 let _ = writeln!(out, "Backup GPT: present (LBA {})", b.my_lba);
160 }
161 None => out.push_str("Backup GPT: MISSING\n"),
162 }
163 out.push('\n');
164 let _ = writeln!(out, "Partitions ({}):", a.partitions.len());
165 let _ = writeln!(
166 out,
167 "{:<3} {:<31} {:<12} {:<11} NAME",
168 "#", "TYPE", "FIRST LBA", "LAST LBA"
169 );
170 for (i, p) in a.partitions.iter().enumerate() {
171 let ty = p
172 .type_name()
173 .map_or_else(|| p.type_guid.to_string(), ToString::to_string);
174 let _ = writeln!(
175 out,
176 "{i:<3} {ty:<31} {:<12} {:<11} {}",
177 p.first_lba, p.last_lba, p.name
178 );
179 }
180 out
181}
182
183fn apm_structure(a: &apm_forensic::ApmAnalysis) -> String {
184 let mut s = String::new();
185 let _ = writeln!(s, "APM Forensic Analysis");
186 let _ = writeln!(s, " block size : {} bytes", a.block_size);
187 let _ = writeln!(s, " device blocks : {}", a.device_block_count);
188 let _ = writeln!(s, "\nPartition map ({} entries):", a.partitions.len());
189 if a.partitions.is_empty() {
190 let _ = writeln!(s, " (no partition entries)");
191 }
192 for (i, p) in a.partitions.iter().enumerate() {
193 let _ = writeln!(
194 s,
195 " [{}] {:<20} {:<24} blocks {:>10}..={:<10}",
196 i,
197 p.name,
198 p.type_name,
199 p.start_block,
200 p.end_block(),
201 );
202 }
203 s
204}