Trilha de auditoria encadeada por hash e comando guara audit

Cada ação sobre a evidência (inspect, scan, recover, image, hash, search, verify,
exportações e sessões) vira uma linha JSON com usuário, máquina, hora UTC e detalhes,
encadeada por SHA-256. guara audit lista e --verify aponta a entrada onde a cadeia
quebra. Senhas e chaves nunca entram no registro.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-22 16:04:43 -03:00
1 parent 213a5a3df5
commit 26e5ebb592
9 files changed
+706 -45

No files matched your search

+134 -43
View File
@@ -88,6 +88,20 @@ enum Cmd {
},
/// Confere uma imagem E01/Ex01: recalcula o MD5 e compara com o gravado na própria imagem
Verify { path: String },
/// Mostra a trilha de auditoria (quem fez o quê e quando) ou confere o encadeamento por hash
Audit {
/// Arquivo da trilha (padrão: auditoria.jsonl na pasta de configuração ou GUARA_AUDIT_LOG)
#[arg(long)]
log: Option<String>,
/// Só confere a cadeia de hashes; sai com erro se ela estiver quebrada
#[arg(long)]
verify: bool,
/// Mostra só as últimas N entradas
#[arg(long)]
tail: Option<usize>,
#[arg(long)]
json: bool,
},
/// Exporta os achados de uma sessão para CSV (abre no Excel)
Export {
session: String,
@@ -427,52 +441,20 @@ fn run(cli: Cli) -> Result<(), String> {
eng.save_session(&job, &session).map_err(s)?;
Ok(())
}
Cmd::Verify { path } if path.to_ascii_lowercase().ends_with(".aff4") => {
let a = guara_core::aff4::Aff4Source::open(std::path::Path::new(&path)).map_err(s)?;
println!("Formato: AFF4");
for (k, v) in &a.info {
println!("{k}: {v}");
}
eprintln!("Recalculando o hash...");
match a.verify_stream_md5().map_err(s)? {
Some(true) => {
println!("MD5 gravado e recalculado conferem.");
Ok(())
}
Some(false) => Err("O MD5 recalculado difere do gravado na imagem.".into()),
None => {
println!("Esta imagem não traz MD5 para conferir.");
Ok(())
}
}
}
Cmd::Verify { path } => {
let e = guara_core::ewf::EwfSource::open(std::path::Path::new(&path)).map_err(s)?;
println!(
"Formato: {}",
if e.info.version == 2 {
"Ex01 (EnCase 7)"
} else {
"E01 (EnCase)"
}
let r = verify_image(&path);
guara_core::audit::record(
"verify",
json!({ "path": path, "result": match &r { Ok(Some(true)) => "conferem", Ok(Some(false)) => "diferem", Ok(None) => "sem hash gravado", Err(_) => "erro" } }),
);
println!("Segmentos: {}", e.info.segments);
for (k, v) in &e.info.fields {
println!("{k}: {v}");
}
eprintln!("Recalculando o hash...");
match e.verify().map_err(s)? {
Some(true) => {
println!("MD5 gravado e recalculado conferem: {}", e.info.md5.clone().unwrap_or_default());
Ok(())
}
Some(false) => Err("O MD5 recalculado difere do gravado na imagem: ela está corrompida ou foi alterada.".into()),
None => {
println!("Esta imagem não traz hash gravado para conferir.");
Ok(())
}
}
r.map(|_| ())
}
Cmd::Audit {
log,
verify,
tail,
json,
} => audit(log, verify, tail, json),
Cmd::Image {
path,
dest,
@@ -790,6 +772,115 @@ fn recover_job(
Ok(())
}
/// Confere o hash gravado numa imagem E01/Ex01 ou AFF4. `Some(true)` = confere.
fn verify_image(path: &str) -> Result<Option<bool>, String> {
if path.to_ascii_lowercase().ends_with(".aff4") {
let a = guara_core::aff4::Aff4Source::open(std::path::Path::new(path)).map_err(s)?;
println!("Formato: AFF4");
for (k, v) in &a.info {
println!("{k}: {v}");
}
eprintln!("Recalculando o hash...");
return match a.verify_stream_md5().map_err(s)? {
Some(true) => {
println!("MD5 gravado e recalculado conferem.");
Ok(Some(true))
}
Some(false) => Err("O MD5 recalculado difere do gravado na imagem.".into()),
None => {
println!("Esta imagem não traz MD5 para conferir.");
Ok(None)
}
};
}
let e = guara_core::ewf::EwfSource::open(std::path::Path::new(path)).map_err(s)?;
println!(
"Formato: {}",
if e.info.version == 2 {
"Ex01 (EnCase 7)"
} else {
"E01 (EnCase)"
}
);
println!("Segmentos: {}", e.info.segments);
for (k, v) in &e.info.fields {
println!("{k}: {v}");
}
eprintln!("Recalculando o hash...");
match e.verify().map_err(s)? {
Some(true) => {
println!(
"MD5 gravado e recalculado conferem: {}",
e.info.md5.clone().unwrap_or_default()
);
Ok(Some(true))
}
Some(false) => Err(
"O MD5 recalculado difere do gravado na imagem: ela está corrompida ou foi alterada."
.into(),
),
None => {
println!("Esta imagem não traz hash gravado para conferir.");
Ok(None)
}
}
}
/// `guara audit`: lista a trilha ou confere a cadeia de hashes.
fn audit(log: Option<String>, verify: bool, tail: Option<usize>, json: bool) -> Result<(), String> {
let log = match log {
Some(p) => guara_core::audit::Log::at(p),
None => guara_core::audit::Log::open_default(),
};
let v = log.verify().map_err(s)?;
if verify {
if json {
println!("{}", serde_json::to_string_pretty(&v).unwrap());
} else if v.ok {
println!("Cadeia íntegra: {} entradas.", v.entries);
if v.entries > 0 {
println!(
"Hash da última entrada (guarde-o fora desta máquina): {}",
v.head
);
}
}
return match v.broken_at {
None => Ok(()),
Some((n, why)) => Err(format!("cadeia quebrada na entrada {n}: {why}")),
};
}
let mut entries = log.entries().map_err(s)?;
if let Some(n) = tail {
let skip = entries.len().saturating_sub(n);
entries.drain(..skip);
}
if json {
println!("{}", serde_json::to_string_pretty(&entries).unwrap());
} else if entries.is_empty() {
println!("Trilha vazia ({}).", log.path().display());
} else {
for e in &entries {
let who = match &e.examiner {
Some(x) => format!("{}/{x}", e.user),
None => e.user.clone(),
};
println!(
"#{:<4} {} {who}@{} {} {}",
e.seq, e.time, e.host, e.action, e.detail
);
}
}
if !v.ok {
let (n, why) = v.broken_at.unwrap_or_default();
eprintln!(
"{} cadeia quebrada na entrada {n}: {why}",
paint("31", "atenção:")
);
}
Ok(())
}
fn wait(eng: &Engine, job: &str, session: Option<&str>) -> Result<Value, String> {
let interrupted = Arc::new(AtomicBool::new(false));
let flag = interrupted.clone();