CLI: senhas, chaves, membros de RAID, referências de vídeo, imagem E01 e verify

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-19 00:08:47 -03:00
1 parent df194edb2f
commit b02bbcb7d6
6 files changed
+300 -14

No files matched your search

+3 -3
View File
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum SecretInput {
Password { value: String },
@@ -50,8 +50,8 @@ pub struct Assembly {
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
#[serde(default)]
pub members: Vec<String>,
/// Senhas e chaves: nunca vão para o disco junto com a sessão.
#[serde(default, skip_serializing)]
/// Senhas e chaves: o `snapshot` da sessão as descarta antes de gravar.
#[serde(default)]
pub secrets: Vec<SecretInput>,
/// Guarda as chaves de volume derivadas na sessão, para reabrir sem pedir a senha de novo.
#[serde(default)]
+63
View File
@@ -24,6 +24,69 @@ pub struct ImageReport {
const BLOCK: usize = 1 << 20;
const SECTOR: usize = 512;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum ImageFormat {
#[default]
Raw,
/// Expert Witness (EnCase 6): `.E01`, dividido em segmentos.
E01 {
#[serde(default)]
case_number: String,
#[serde(default)]
description: String,
#[serde(default)]
evidence_number: String,
#[serde(default)]
examiner: String,
#[serde(default)]
notes: String,
/// Tamanho máximo de cada segmento em MiB (0 = um arquivo só).
#[serde(default = "default_segment_mib")]
segment_mib: u64,
/// 0 = sem compressão, 1 a 9 = deflate.
#[serde(default = "default_compression")]
compression: u32,
},
}
fn default_segment_mib() -> u64 {
1500
}
fn default_compression() -> u32 {
1
}
pub fn create_image_as(
src: &dyn Source,
dest: &Path,
format: &ImageFormat,
cancelled: &(dyn Fn() -> bool + Sync),
progress: &mut dyn FnMut(u64, u64),
) -> Result<ImageReport> {
match format {
ImageFormat::Raw => create_image(src, dest, cancelled, progress),
ImageFormat::E01 { case_number, description, evidence_number, examiner, notes, segment_mib, compression } => {
let opts = crate::ewf::E01Options {
case_number: case_number.clone(),
description: description.clone(),
evidence_number: evidence_number.clone(),
examiner: examiner.clone(),
notes: notes.clone(),
segment_size: segment_mib * 1024 * 1024,
compression: *compression,
..Default::default()
};
// O destino pode vir com ou sem a extensão .E01.
let base = match dest.extension().and_then(|e| e.to_str()) {
Some(e) if e.eq_ignore_ascii_case("e01") => dest.with_extension(""),
_ => dest.to_path_buf(),
};
crate::ewf::write_e01(src, &base, &opts, cancelled, progress)
}
}
}
pub fn create_image(
src: &dyn Source,
dest: &Path,
+1 -1
View File
@@ -248,7 +248,7 @@ pub fn analyze(src: &dyn Source, search_lost: bool) -> DiskLayout {
for p in &layout.partitions {
if let Some(e) = &p.encrypted {
layout.warnings.push(format!(
"Partição {} está cifrada com {}: o Guará só detecta cifragem na v1 e não varre esse volume sem a chave.",
"Partição {} está cifrada com {}: informe a senha, a chave de recuperação ou o arquivo de chave para abrir e varrer o conteúdo.",
p.index + 1,
e
));
+52 -5
View File
@@ -548,6 +548,12 @@ impl Engine {
// ---------------------------------------------------------------- imagem
pub fn start_image(&self, path: &str, dest: &str) -> Result<String> {
self.start_image_as(path, dest, &image::ImageFormat::Raw)
}
/// Cria a imagem em bruto ou em outro formato forense (E01 ou AFF4).
pub fn start_image_as(&self, path: &str, dest: &str, format: &image::ImageFormat) -> Result<String> {
let format = format.clone();
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
recover::check_destination(
Some(Path::new(path)),
@@ -570,7 +576,7 @@ impl Engine {
total: t,
};
};
let res = image::create_image(src.as_ref(), &dest, &cancelled, &mut prog);
let res = image::create_image_as(src.as_ref(), &dest, &format, &cancelled, &mut prog);
let mut s = j2.st.lock().unwrap();
match res {
Ok(rep) => {
@@ -990,8 +996,35 @@ impl Engine {
"devices" => json!(device::list_devices()),
"inspect" => {
let path = s("path")?;
let src = open_path(Path::new(&path)).map_err(|e| open_error(&path, e))?;
json!(partition::analyze(src.as_ref(), false))
let asm: crate::assemble::Assembly = p
.get("assembly")
.cloned()
.map(serde_json::from_value)
.transpose()?
.unwrap_or_default();
let raw = open_path(Path::new(&path)).map_err(|e| open_error(&path, e))?;
let a = crate::assemble::assemble_source(raw, &asm)?;
let mut layout = partition::analyze(a.source.as_ref(), false);
if a.skip_primary {
layout.partitions.clear();
layout.gaps.clear();
layout.table = "membro de RAID".into();
}
for pi in &a.extra_parts {
if !layout.partitions.iter().any(|q| q.start == pi.start) {
let mut pi = pi.clone();
pi.index = layout.partitions.len();
layout.partitions.push(pi);
}
}
layout.warnings.extend(a.notes.iter().cloned());
for u in &a.unlocked {
layout.warnings.push(format!("Volume aberto em {}: {}.", u.start, u.description));
}
for l in &a.locked {
layout.warnings.push(format!("Volume cifrado em {} continua fechado: {}.", l.start, l.reason));
}
json!(layout)
}
"scan_start" => {
let opts: ScanOptions = p
@@ -1124,7 +1157,15 @@ impl Engine {
.unwrap_or_default();
json!({ "job": self.start_recover(&s("job")?, ids, filter, &s("dest")?, layout)? })
}
"image_start" => json!({ "job": self.start_image(&s("path")?, &s("dest")?)? }),
"image_start" => {
let format: image::ImageFormat = p
.get("format")
.cloned()
.map(serde_json::from_value)
.transpose()?
.unwrap_or(image::ImageFormat::Raw);
json!({ "job": self.start_image_as(&s("path")?, &s("dest")?, &format)? })
}
"session_save" => {
self.save_session(&s("job")?, &s("path")?)?;
json!({})
@@ -1546,10 +1587,16 @@ fn new_session_path(target: &str) -> PathBuf {
fn snapshot(j: &Job) -> SessionFile {
let s = j.st.lock().unwrap();
// Senhas e chaves nunca vão para o arquivo de sessão (só as chaves de volume, se o usuário pediu).
let mut options = j.opts.clone();
options.assembly.secrets.clear();
if !options.assembly.remember_keys {
options.assembly.keys.clear();
}
SessionFile {
version: 1,
target: j.target.clone(),
options: j.opts.clone(),
options,
found: s.found.clone(),
checkpoint: s.checkpoint.clone(),
layout: s.layout.clone(),