pipa_core/logging/
verify.rs1use chrono::{NaiveDate, Utc};
9use std::path::PathBuf;
10use std::collections::HashMap;
11use std::fs;
12
13use crate::logging::ledger::{compute_sha256, read_ledger_plaintext};
14
15pub struct FileVerification {
17 pub filename: String,
18 pub status: FileStatus,
19 pub stored_hash: Option<String>, pub computed_hash: Option<String>, }
22
23pub enum FileStatus {
25 Verified, Mismatched, Missing, Malformed, Unsealed, }
31
32pub struct VerificationSummary {
34 pub verified: usize,
35 pub mismatched: usize,
36 pub missing: usize,
37 pub malformed: usize,
38 pub unsealed: usize,
39 pub files: Vec<FileVerification>, }
41
42impl VerificationSummary {
43 fn new() -> Self {
44 VerificationSummary {
45 verified: 0,
46 mismatched: 0,
47 missing: 0,
48 malformed: 0,
49 unsealed: 0,
50 files: Vec::new(),
51 }
52 }
53}
54
55pub fn verify_all() -> VerificationSummary {
57 let logs_dir = PathBuf::from("logs");
58 let mut summary = VerificationSummary::new();
59
60 let mut sealed_files: HashMap<String, String> = HashMap::new();
63 let ledger_plaintext = read_ledger_plaintext();
64 if !ledger_plaintext.is_empty() {
65 let ledger_str = String::from_utf8_lossy(&ledger_plaintext);
66 for line in ledger_str.lines() {
67 let parts: Vec<&str> = line.split_whitespace().collect();
68 if parts.len() < 3 {
69 summary.malformed += 1;
70 summary.files.push(FileVerification {
71 filename: line.to_string(),
72 status: FileStatus::Malformed,
73 stored_hash: None,
74 computed_hash: None,
75 });
76 } else {
77 let filename = parts[1].to_string();
78 let stored_hash = parts[2].to_string();
79 sealed_files.insert(filename, stored_hash);
80 }
81 }
82 }
83
84 if !logs_dir.exists() {
87 for (filename, stored_hash) in sealed_files {
88 summary.missing += 1;
89 summary.files.push(FileVerification {
90 filename,
91 status: FileStatus::Missing,
92 stored_hash: Some(stored_hash),
93 computed_hash: None,
94 });
95 }
96 return summary;
97 }
98
99 for entry in fs::read_dir(logs_dir).expect("cannot read logs dir") {
101 let path = entry.expect("bad dir entry").path();
102 if path.is_file() {
103 let filename = path.file_name().unwrap().to_string_lossy().to_string();
104
105 if let Some(stored_hash) = sealed_files.get(&filename) {
107 let computed_hash = compute_sha256(&path);
109 if *stored_hash == computed_hash {
110 summary.verified += 1;
111 summary.files.push(FileVerification {
112 filename: filename.clone(),
113 status: FileStatus::Verified,
114 stored_hash: Some(stored_hash.clone()),
115 computed_hash: Some(computed_hash),
116 });
117 } else {
118 summary.mismatched += 1;
119 summary.files.push(FileVerification {
120 filename: filename.clone(),
121 status: FileStatus::Mismatched,
122 stored_hash: Some(stored_hash.clone()),
123 computed_hash: Some(computed_hash),
124 });
125 }
126 sealed_files.remove(&filename);
128 } else {
129 summary.unsealed += 1;
131 summary.files.push(FileVerification {
132 filename,
133 status: FileStatus::Unsealed,
134 stored_hash: None,
135 computed_hash: Some(compute_sha256(&path)), });
137 }
138 }
139 }
140
141 for (filename, stored_hash) in sealed_files {
144 summary.missing += 1;
145 summary.files.push(FileVerification {
146 filename,
147 status: FileStatus::Missing,
148 stored_hash: Some(stored_hash),
149 computed_hash: None,
150 });
151 }
152
153 summary
154}
155pub fn verify_date(date: Option<&str>) -> VerificationSummary {
157 let logs_dir = PathBuf::from("logs");
158 let mut summary = VerificationSummary::new();
159
160 if !logs_dir.exists() {
161 return summary;
162 }
163
164 let target_date = match date {
166 Some(d) => match NaiveDate::parse_from_str(d, "%Y-%m-%d") {
167 Ok(date) => date.format("%Y-%m-%d").to_string(),
168 Err(_) => return summary,
169 },
170 None => {
171 let yesterday = Utc::now().date_naive() - chrono::Duration::days(1);
172 yesterday.format("%Y-%m-%d").to_string()
173 }
174 };
175
176 let log_filename = format!("audit-{}.jsonl", target_date);
177 let log_path = logs_dir.join(&log_filename);
178
179 let ledger_plaintext = read_ledger_plaintext();
181 if ledger_plaintext.is_empty() {
182 return summary;
183 }
184 let ledger_str = String::from_utf8_lossy(&ledger_plaintext);
185
186 if !log_path.exists() {
188 summary.missing += 1;
189 summary.files.push(FileVerification {
190 filename: log_filename,
191 status: FileStatus::Missing,
192 stored_hash: None,
193 computed_hash: None,
194 });
195 return summary;
196 }
197
198 if let Some(line) = ledger_str.lines().find(|l| l.contains(&log_filename)) {
200 let parts: Vec<&str> = line.split_whitespace().collect();
201 if parts.len() < 3 {
202 summary.malformed += 1;
203 summary.files.push(FileVerification {
204 filename: log_filename,
205 status: FileStatus::Malformed,
206 stored_hash: None,
207 computed_hash: None,
208 });
209 return summary;
210 }
211
212 let stored_hash = parts[2].to_string();
213 let computed_hash = compute_sha256(&log_path);
214
215 if stored_hash == computed_hash {
216 summary.verified += 1;
217 summary.files.push(FileVerification {
218 filename: log_filename,
219 status: FileStatus::Verified,
220 stored_hash: Some(stored_hash),
221 computed_hash: Some(computed_hash),
222 });
223 } else {
224 summary.mismatched += 1;
225 summary.files.push(FileVerification {
226 filename: log_filename,
227 status: FileStatus::Mismatched,
228 stored_hash: Some(stored_hash),
229 computed_hash: Some(computed_hash),
230 });
231 }
232 } else {
233 summary.unsealed += 1;
235 summary.files.push(FileVerification {
236 filename: log_filename,
237 status: FileStatus::Unsealed,
238 stored_hash: None,
239 computed_hash: None,
240 });
241 }
242
243 summary
244}