use chrono::{NaiveDate, Utc};
use std::path::PathBuf;
use std::collections::HashMap;
use std::fs;
use crate::logging::ledger::{compute_sha256, read_ledger_plaintext};
pub struct FileVerification {
pub filename: String,
pub status: FileStatus,
pub stored_hash: Option<String>, pub computed_hash: Option<String>, }
pub enum FileStatus {
Verified, Mismatched, Missing, Malformed, Unsealed, }
pub struct VerificationSummary {
pub verified: usize,
pub mismatched: usize,
pub missing: usize,
pub malformed: usize,
pub unsealed: usize,
pub files: Vec<FileVerification>, }
impl VerificationSummary {
fn new() -> Self {
VerificationSummary {
verified: 0,
mismatched: 0,
missing: 0,
malformed: 0,
unsealed: 0,
files: Vec::new(),
}
}
}
pub fn verify_all() -> VerificationSummary {
let logs_dir = PathBuf::from("logs");
let mut summary = VerificationSummary::new();
let mut sealed_files: HashMap<String, String> = HashMap::new();
let ledger_plaintext = read_ledger_plaintext();
if !ledger_plaintext.is_empty() {
let ledger_str = String::from_utf8_lossy(&ledger_plaintext);
for line in ledger_str.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
summary.malformed += 1;
summary.files.push(FileVerification {
filename: line.to_string(),
status: FileStatus::Malformed,
stored_hash: None,
computed_hash: None,
});
} else {
let filename = parts[1].to_string();
let stored_hash = parts[2].to_string();
sealed_files.insert(filename, stored_hash);
}
}
}
if !logs_dir.exists() {
for (filename, stored_hash) in sealed_files {
summary.missing += 1;
summary.files.push(FileVerification {
filename,
status: FileStatus::Missing,
stored_hash: Some(stored_hash),
computed_hash: None,
});
}
return summary;
}
for entry in fs::read_dir(logs_dir).expect("cannot read logs dir") {
let path = entry.expect("bad dir entry").path();
if path.is_file() {
let filename = path.file_name().unwrap().to_string_lossy().to_string();
if let Some(stored_hash) = sealed_files.get(&filename) {
let computed_hash = compute_sha256(&path);
if *stored_hash == computed_hash {
summary.verified += 1;
summary.files.push(FileVerification {
filename: filename.clone(),
status: FileStatus::Verified,
stored_hash: Some(stored_hash.clone()),
computed_hash: Some(computed_hash),
});
} else {
summary.mismatched += 1;
summary.files.push(FileVerification {
filename: filename.clone(),
status: FileStatus::Mismatched,
stored_hash: Some(stored_hash.clone()),
computed_hash: Some(computed_hash),
});
}
sealed_files.remove(&filename);
} else {
summary.unsealed += 1;
summary.files.push(FileVerification {
filename,
status: FileStatus::Unsealed,
stored_hash: None,
computed_hash: Some(compute_sha256(&path)), });
}
}
}
for (filename, stored_hash) in sealed_files {
summary.missing += 1;
summary.files.push(FileVerification {
filename,
status: FileStatus::Missing,
stored_hash: Some(stored_hash),
computed_hash: None,
});
}
summary
}
pub fn verify_date(date: Option<&str>) -> VerificationSummary {
let logs_dir = PathBuf::from("logs");
let mut summary = VerificationSummary::new();
if !logs_dir.exists() {
return summary;
}
let target_date = match date {
Some(d) => match NaiveDate::parse_from_str(d, "%Y-%m-%d") {
Ok(date) => date.format("%Y-%m-%d").to_string(),
Err(_) => return summary,
},
None => {
let yesterday = Utc::now().date_naive() - chrono::Duration::days(1);
yesterday.format("%Y-%m-%d").to_string()
}
};
let log_filename = format!("audit-{}.jsonl", target_date);
let log_path = logs_dir.join(&log_filename);
let ledger_plaintext = read_ledger_plaintext();
if ledger_plaintext.is_empty() {
return summary;
}
let ledger_str = String::from_utf8_lossy(&ledger_plaintext);
if !log_path.exists() {
summary.missing += 1;
summary.files.push(FileVerification {
filename: log_filename,
status: FileStatus::Missing,
stored_hash: None,
computed_hash: None,
});
return summary;
}
if let Some(line) = ledger_str.lines().find(|l| l.contains(&log_filename)) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
summary.malformed += 1;
summary.files.push(FileVerification {
filename: log_filename,
status: FileStatus::Malformed,
stored_hash: None,
computed_hash: None,
});
return summary;
}
let stored_hash = parts[2].to_string();
let computed_hash = compute_sha256(&log_path);
if stored_hash == computed_hash {
summary.verified += 1;
summary.files.push(FileVerification {
filename: log_filename,
status: FileStatus::Verified,
stored_hash: Some(stored_hash),
computed_hash: Some(computed_hash),
});
} else {
summary.mismatched += 1;
summary.files.push(FileVerification {
filename: log_filename,
status: FileStatus::Mismatched,
stored_hash: Some(stored_hash),
computed_hash: Some(computed_hash),
});
}
} else {
summary.unsealed += 1;
summary.files.push(FileVerification {
filename: log_filename,
status: FileStatus::Unsealed,
stored_hash: None,
computed_hash: None,
});
}
summary
}