use ewf_forensic::{AnalysisProgress, ComputedHashes, EwfIntegrityAnomaly, EwfIntegrityPath, Severity};
use indicatif::{ProgressBar, ProgressStyle};
use std::path::PathBuf;
use std::process;
const HELP: &str = "\
ewf-check — forensic integrity analysis for EWF / E01 images
USAGE
ewf-check [OPTIONS] <segment>...
ARGUMENTS
<segment>... One or more segment paths (evidence.E01, evidence.E02, …).
When a single .E01 path is given, consecutive siblings are
discovered automatically.
OPTIONS
--min-severity=<level> Only report anomalies at or above this severity.
Levels: info, warning, error, critical [default: info]
--json Emit machine-readable JSON instead of human text.
--hash-md5=<hex> Compare computed MD5 against this hex string (chain-of-custody).
--hash-sha1=<hex> Compare computed SHA-1 against this hex string.
--hash-sha256=<hex> Compare computed SHA-256 against this hex string.
--print-hashes Compute and print MD5, SHA-1, and SHA-256 of all sector data.
Combined with --json: adds a \"hashes\" object to the JSON output.
--progress Show a progress bar on stderr during analysis.
--help Show this help and exit.
--version Print version and exit.
EXIT CODES
0 Clean — no anomalies at or above --min-severity
1 Anomalies found
2 Usage error or I/O failure
";
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.is_empty() {
eprintln!("{HELP}");
process::exit(2);
}
if args.iter().any(|a| a == "--help" || a == "-h") {
print!("{HELP}");
process::exit(0);
}
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("ewf-check {}", env!("CARGO_PKG_VERSION"));
process::exit(0);
}
let mut min_severity = Severity::Info;
let mut json_mode = false;
let mut print_hashes = false;
let mut show_progress = false;
let mut hash_md5: Option<[u8; 16]> = None;
let mut hash_sha1: Option<[u8; 20]> = None;
let mut hash_sha256: Option<[u8; 32]> = None;
let mut paths: Vec<PathBuf> = Vec::new();
for arg in &args {
if let Some(val) = arg.strip_prefix("--min-severity=") {
min_severity = match val {
"info" | "Info" => Severity::Info,
"low" | "Low" => Severity::Low,
"medium" | "Medium" | "warning" | "Warning" => Severity::Medium,
"high" | "High" | "error" | "Error" => Severity::High,
"critical" | "Critical" => Severity::Critical,
other => {
eprintln!("error: unknown severity level '{other}'; expected info/low/medium/high/critical");
process::exit(2);
}
};
} else if arg == "--json" {
json_mode = true;
} else if arg == "--print-hashes" {
print_hashes = true;
} else if arg == "--progress" {
show_progress = true;
} else if let Some(hex) = arg.strip_prefix("--hash-md5=") {
hash_md5 = Some(parse_hex_fixed::<16>(hex, "--hash-md5"));
} else if let Some(hex) = arg.strip_prefix("--hash-sha1=") {
hash_sha1 = Some(parse_hex_fixed::<20>(hex, "--hash-sha1"));
} else if let Some(hex) = arg.strip_prefix("--hash-sha256=") {
hash_sha256 = Some(parse_hex_fixed::<32>(hex, "--hash-sha256"));
} else if arg.starts_with('-') {
eprintln!("error: unknown option '{arg}'");
eprintln!("Run 'ewf-check --help' for usage.");
process::exit(2);
} else {
paths.push(PathBuf::from(arg));
}
}
if paths.is_empty() {
eprintln!("error: no segment paths provided");
eprintln!("Run 'ewf-check --help' for usage.");
process::exit(2);
}
let mut checker = if paths.len() == 1 {
EwfIntegrityPath::from_path(&paths[0])
} else {
EwfIntegrityPath::from_paths(&paths)
};
if let Some(h) = hash_md5 { checker = checker.with_expected_md5(h); }
if let Some(h) = hash_sha1 { checker = checker.with_expected_sha1(h); }
if let Some(h) = hash_sha256 { checker = checker.with_expected_sha256(h); }
let analysis_result: std::io::Result<Vec<_>> = if show_progress {
let pb = ProgressBar::new(0);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} chunks")
.unwrap()
.progress_chars("#>-"),
);
let res = checker.analyse_with_progress(|p: AnalysisProgress| {
if let Some(total) = p.chunks_total {
if pb.length() != Some(total as u64) {
pb.set_length(total as u64);
}
}
pb.set_position(p.chunks_done as u64);
});
pb.finish_and_clear();
res.map(|(anomalies, ())| anomalies)
} else {
checker.analyse()
};
let (findings, computed) = match (analysis_result, print_hashes) {
(Err(e), _) => {
if json_mode {
println!("{{\"error\": \"{}\"}}", json_escape(&e.to_string()));
} else {
eprintln!("error: {e}");
}
process::exit(2);
}
(Ok(f), true) => {
let c = if paths.len() == 1 {
EwfIntegrityPath::from_path(&paths[0]).compute_hashes()
} else {
EwfIntegrityPath::from_paths(&paths).compute_hashes()
};
let hashes = match c {
Err(e) => {
if json_mode {
println!("{{\"error\": \"{}\"}}", json_escape(&e.to_string()));
} else {
eprintln!("error: {e}");
}
process::exit(2);
}
Ok(h) => h,
};
(f, hashes)
}
(Ok(f), false) => (f, None),
};
let visible: Vec<&EwfIntegrityAnomaly> = findings
.iter()
.filter(|a| severity_gte(a.severity(), &min_severity))
.collect();
if json_mode {
print_json(&visible, &min_severity, computed.as_ref());
} else {
print_text(&visible, &min_severity);
if let Some(ref h) = computed {
println!();
println!("MD5: {}", hex_string(&h.md5));
println!("SHA-1: {}", hex_string(&h.sha1));
println!("SHA-256: {}", hex_string(&h.sha256));
}
}
process::exit(if visible.is_empty() { 0 } else { 1 });
}
fn print_text(visible: &[&EwfIntegrityAnomaly], min_severity: &Severity) {
if visible.is_empty() {
println!("clean — 0 anomalies at or above {}", severity_label(min_severity));
return;
}
println!("{} anomaly/anomalies found:\n", visible.len());
for anomaly in visible {
let tag = match anomaly.severity() {
Severity::Critical => "[CRITICAL]",
Severity::High => "[HIGH] ",
Severity::Medium => "[MEDIUM] ",
Severity::Low => "[LOW] ",
Severity::Info => "[INFO] ",
_ => "[UNKNOWN] ",
};
println!("{tag} {anomaly}");
}
}
fn print_json(visible: &[&EwfIntegrityAnomaly], _min_severity: &Severity, hashes: Option<&ComputedHashes>) {
let clean = visible.is_empty();
let count = visible.len();
let mut out = format!(
"{{\n \"clean\": {},\n \"anomaly_count\": {},\n \"anomalies\": [",
clean, count
);
for (i, anomaly) in visible.iter().enumerate() {
let sep = if i == 0 { "\n" } else { ",\n" };
out.push_str(&format!(
"{} {{\"severity\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\"}}",
sep,
severity_label(&anomaly.severity()),
anomaly_kind(anomaly),
json_escape(&anomaly.to_string()),
));
}
if !visible.is_empty() {
out.push_str("\n ");
}
out.push_str("]");
if let Some(h) = hashes {
out.push_str(&format!(
",\n \"hashes\": {{\n \"md5\": \"{}\",\n \"sha1\": \"{}\",\n \"sha256\": \"{}\"\n }}",
hex_string(&h.md5),
hex_string(&h.sha1),
hex_string(&h.sha256),
));
}
out.push_str("\n}");
println!("{out}");
}
fn anomaly_kind(a: &EwfIntegrityAnomaly) -> &'static str {
match a {
EwfIntegrityAnomaly::InvalidSignature => "InvalidSignature",
EwfIntegrityAnomaly::SegmentNumberZero => "SegmentNumberZero",
EwfIntegrityAnomaly::SectionDescriptorCrcMismatch { .. } => "SectionDescriptorCrcMismatch",
EwfIntegrityAnomaly::SectionChainBroken { .. } => "SectionChainBroken",
EwfIntegrityAnomaly::SectionGapNonZero { .. } => "SectionGapNonZero",
EwfIntegrityAnomaly::VolumeSectionMissing => "VolumeSectionMissing",
EwfIntegrityAnomaly::UnknownSectionType { .. } => "UnknownSectionType",
EwfIntegrityAnomaly::DoneSectionMissing => "DoneSectionMissing",
EwfIntegrityAnomaly::SectorsSectionMissing => "SectorsSectionMissing",
EwfIntegrityAnomaly::TableSectionMissing => "TableSectionMissing",
EwfIntegrityAnomaly::ChunkSizeInvalid { .. } => "ChunkSizeInvalid",
EwfIntegrityAnomaly::SectorCountMismatch { .. } => "SectorCountMismatch",
EwfIntegrityAnomaly::BytesPerSectorInvalid { .. } => "BytesPerSectorInvalid",
EwfIntegrityAnomaly::TableChunkCountMismatch { .. } => "TableChunkCountMismatch",
EwfIntegrityAnomaly::TableHeaderAdler32Mismatch { .. } => "TableHeaderAdler32Mismatch",
EwfIntegrityAnomaly::TableEntryOutOfBounds { .. } => "TableEntryOutOfBounds",
EwfIntegrityAnomaly::TableEntryOutsideSectorsRange { .. } => "TableEntryOutsideSectorsRange",
EwfIntegrityAnomaly::SectionGapZero { .. } => "SectionGapZero",
EwfIntegrityAnomaly::HashMismatch { .. } => "HashMismatch",
EwfIntegrityAnomaly::HashSectionMissing => "HashSectionMissing",
EwfIntegrityAnomaly::Table2Mismatch { .. } => "Table2Mismatch",
EwfIntegrityAnomaly::BadSectorsPresent { .. } => "BadSectorsPresent",
EwfIntegrityAnomaly::SegmentOutOfOrder { .. } => "SegmentOutOfOrder",
EwfIntegrityAnomaly::DigestSha1Mismatch { .. } => "DigestSha1Mismatch",
EwfIntegrityAnomaly::DigestSha256Mismatch { .. } => "DigestSha256Mismatch",
EwfIntegrityAnomaly::ExternalMd5Mismatch { .. } => "ExternalMd5Mismatch",
EwfIntegrityAnomaly::ExternalSha1Mismatch { .. } => "ExternalSha1Mismatch",
EwfIntegrityAnomaly::ExternalSha256Mismatch { .. } => "ExternalSha256Mismatch",
EwfIntegrityAnomaly::Ewf2SectionDataHashMismatch { .. } => "Ewf2SectionDataHashMismatch",
EwfIntegrityAnomaly::Ewf2EncryptedSection { .. } => "Ewf2EncryptedSection",
EwfIntegrityAnomaly::Ewf2HashSectionMissing => "Ewf2HashSectionMissing",
EwfIntegrityAnomaly::VolumeBodyCrcMismatch { .. } => "VolumeBodyCrcMismatch",
EwfIntegrityAnomaly::MediaTypeUnknown { .. } => "MediaTypeUnknown",
EwfIntegrityAnomaly::SetIdentifierMismatch { .. } => "SetIdentifierMismatch",
EwfIntegrityAnomaly::Ewf2MediaInfoMissing => "Ewf2MediaInfoMissing",
EwfIntegrityAnomaly::Ewf2ChunkTableChecksumMismatch { .. } => "Ewf2ChunkTableChecksumMismatch",
EwfIntegrityAnomaly::ChunkChecksumMismatch { .. } => "ChunkChecksumMismatch",
EwfIntegrityAnomaly::ChunkDecompressionError { .. } => "ChunkDecompressionError",
EwfIntegrityAnomaly::UnsupportedCompressionAlgorithm { .. } => "UnsupportedCompressionAlgorithm",
EwfIntegrityAnomaly::Ewf2MediaInfoParseFailed => "Ewf2MediaInfoParseFailed",
}
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
fn parse_hex_fixed<const N: usize>(hex: &str, flag: &str) -> [u8; N] {
if hex.len() != N * 2 {
eprintln!(
"error: {flag} expects exactly {} hex characters (got {})",
N * 2,
hex.len()
);
process::exit(2);
}
let mut out = [0u8; N];
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let s = std::str::from_utf8(chunk).unwrap_or("??");
match u8::from_str_radix(s, 16) {
Ok(b) => out[i] = b,
Err(_) => {
eprintln!("error: {flag} contains invalid hex character in '{s}'");
process::exit(2);
}
}
}
out
}
fn hex_string(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn severity_gte(a: Severity, min: &Severity) -> bool {
severity_rank(&a) >= severity_rank(min)
}
fn severity_rank(s: &Severity) -> u8 {
match s {
Severity::Info => 0,
Severity::Low => 1,
Severity::Medium => 2,
Severity::High => 3,
Severity::Critical => 4,
_ => 0,
}
}
fn severity_label(s: &Severity) -> &'static str {
match s {
Severity::Info => "info",
Severity::Low => "low",
Severity::Medium => "medium",
Severity::High => "high",
Severity::Critical => "critical",
_ => "unknown",
}
}