pipa_core/logging/
verify.rs1use chrono::{NaiveDate, Utc};
9use std::path::PathBuf;
10
11use crate::logging::ledger::{compute_sha256, read_ledger_plaintext};
12
13pub struct FileVerification {
15 pub filename: String,
16 pub status: FileStatus,
17 pub stored_hash: Option<String>, pub computed_hash: Option<String>, }
20
21pub enum FileStatus {
23 Verified, Mismatched, Missing, Malformed, Unsealed, }
29
30pub struct VerificationSummary {
32 pub verified: usize,
33 pub mismatched: usize,
34 pub missing: usize,
35 pub malformed: usize,
36 pub unsealed: usize,
37 pub files: Vec<FileVerification>, }
39
40impl VerificationSummary {
41 fn new() -> Self {
42 VerificationSummary {
43 verified: 0,
44 mismatched: 0,
45 missing: 0,
46 malformed: 0,
47 unsealed: 0,
48 files: Vec::new(),
49 }
50 }
51}
52
53pub fn verify_all() -> VerificationSummary {
55 let logs_dir = PathBuf::from("logs");
56 let mut summary = VerificationSummary::new();
57
58 if !logs_dir.exists() {
59 return summary;
60 }
61
62 let ledger_plaintext = read_ledger_plaintext();
64 if ledger_plaintext.is_empty() {
65 return summary;
66 }
67 let ledger_str = String::from_utf8_lossy(&ledger_plaintext);
68
69 for line in ledger_str.lines() {
71 let parts: Vec<&str> = line.split_whitespace().collect();
72 if parts.len() < 3 {
73 summary.malformed += 1;
74 summary.files.push(FileVerification {
75 filename: line.to_string(),
76 status: FileStatus::Malformed,
77 stored_hash: None,
78 computed_hash: None,
79 });
80 continue;
81 }
82
83 let filename = parts[1].to_string();
84 let stored_hash = parts[2].to_string();
85 let log_path = logs_dir.join(&filename);
86
87 if !log_path.exists() {
89 summary.missing += 1;
90 summary.files.push(FileVerification {
91 filename,
92 status: FileStatus::Missing,
93 stored_hash: Some(stored_hash),
94 computed_hash: None,
95 });
96 continue;
97 }
98
99 let computed_hash = compute_sha256(&log_path);
100 if stored_hash == computed_hash {
101 summary.verified += 1;
102 summary.files.push(FileVerification {
103 filename,
104 status: FileStatus::Verified,
105 stored_hash: Some(stored_hash),
106 computed_hash: Some(computed_hash),
107 });
108 } else {
109 summary.mismatched += 1;
110 summary.files.push(FileVerification {
111 filename,
112 status: FileStatus::Mismatched,
113 stored_hash: Some(stored_hash),
114 computed_hash: Some(computed_hash),
115 });
116 }
117 }
118
119 summary
120}
121
122pub fn verify_date(date: Option<&str>) -> VerificationSummary {
124 let logs_dir = PathBuf::from("logs");
125 let mut summary = VerificationSummary::new();
126
127 if !logs_dir.exists() {
128 return summary;
129 }
130
131 let target_date = match date {
133 Some(d) => match NaiveDate::parse_from_str(d, "%Y-%m-%d") {
134 Ok(date) => date.format("%Y-%m-%d").to_string(),
135 Err(_) => return summary,
136 },
137 None => {
138 let yesterday = Utc::now().date_naive() - chrono::Duration::days(1);
139 yesterday.format("%Y-%m-%d").to_string()
140 }
141 };
142
143 let log_filename = format!("audit-{}.jsonl", target_date);
144 let log_path = logs_dir.join(&log_filename);
145
146 let ledger_plaintext = read_ledger_plaintext();
148 if ledger_plaintext.is_empty() {
149 return summary;
150 }
151 let ledger_str = String::from_utf8_lossy(&ledger_plaintext);
152
153 if !log_path.exists() {
155 summary.missing += 1;
156 summary.files.push(FileVerification {
157 filename: log_filename,
158 status: FileStatus::Missing,
159 stored_hash: None,
160 computed_hash: None,
161 });
162 return summary;
163 }
164
165 if let Some(line) = ledger_str.lines().find(|l| l.contains(&log_filename)) {
167 let parts: Vec<&str> = line.split_whitespace().collect();
168 if parts.len() < 3 {
169 summary.malformed += 1;
170 summary.files.push(FileVerification {
171 filename: log_filename,
172 status: FileStatus::Malformed,
173 stored_hash: None,
174 computed_hash: None,
175 });
176 return summary;
177 }
178
179 let stored_hash = parts[2].to_string();
180 let computed_hash = compute_sha256(&log_path);
181
182 if stored_hash == computed_hash {
183 summary.verified += 1;
184 summary.files.push(FileVerification {
185 filename: log_filename,
186 status: FileStatus::Verified,
187 stored_hash: Some(stored_hash),
188 computed_hash: Some(computed_hash),
189 });
190 } else {
191 summary.mismatched += 1;
192 summary.files.push(FileVerification {
193 filename: log_filename,
194 status: FileStatus::Mismatched,
195 stored_hash: Some(stored_hash),
196 computed_hash: Some(computed_hash),
197 });
198 }
199 } else {
200 summary.unsealed += 1;
202 summary.files.push(FileVerification {
203 filename: log_filename,
204 status: FileStatus::Unsealed,
205 stored_hash: None,
206 computed_hash: None,
207 });
208 }
209
210 summary
211}