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:
1 parent
df194edb2f
commit
b02bbcb7d6
6 files changed
+300
-14
No files matched your search
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
));
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user