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
@@ -18,3 +18,4 @@ serde_json = "1"
|
|||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
ctrlc = "3"
|
ctrlc = "3"
|
||||||
|
rpassword = "7"
|
||||||
@@ -31,6 +31,8 @@ enum Cmd {
|
|||||||
path: String,
|
path: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
#[command(flatten)]
|
||||||
|
asm: AsmArgs,
|
||||||
},
|
},
|
||||||
/// Saúde do disco (SMART): estado, temperatura, uso e tabela de atributos
|
/// Saúde do disco (SMART): estado, temperatura, uso e tabela de atributos
|
||||||
Health {
|
Health {
|
||||||
@@ -56,7 +58,36 @@ enum Cmd {
|
|||||||
filter: FilterArgs,
|
filter: FilterArgs,
|
||||||
},
|
},
|
||||||
/// Cria uma imagem do disco (tolerante a setores ruins) para trabalhar na cópia
|
/// Cria uma imagem do disco (tolerante a setores ruins) para trabalhar na cópia
|
||||||
Image { path: String, dest: String },
|
Image {
|
||||||
|
path: String,
|
||||||
|
dest: String,
|
||||||
|
/// Formato da imagem: raw (padrão) ou e01 (Expert Witness, com hash e metadados)
|
||||||
|
#[arg(long, default_value = "raw")]
|
||||||
|
format: String,
|
||||||
|
/// E01: número do caso
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
case: String,
|
||||||
|
/// E01: examinador
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
examiner: String,
|
||||||
|
/// E01: descrição da evidência
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
description: String,
|
||||||
|
/// E01: número da evidência
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
evidence: String,
|
||||||
|
/// E01: observações
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
notes: String,
|
||||||
|
/// E01: tamanho máximo de cada segmento em MiB (0 = um arquivo só)
|
||||||
|
#[arg(long, default_value_t = 1500)]
|
||||||
|
segment_mib: u64,
|
||||||
|
/// E01: compressão 0 (nenhuma) a 9
|
||||||
|
#[arg(long, default_value_t = 1)]
|
||||||
|
compression: u32,
|
||||||
|
},
|
||||||
|
/// Confere uma imagem E01/Ex01: recalcula o MD5 e compara com o gravado na própria imagem
|
||||||
|
Verify { path: String },
|
||||||
/// Exporta os achados de uma sessão para CSV (abre no Excel)
|
/// Exporta os achados de uma sessão para CSV (abre no Excel)
|
||||||
Export {
|
Export {
|
||||||
session: String,
|
session: String,
|
||||||
@@ -140,6 +171,111 @@ struct OutArgs {
|
|||||||
json: bool,
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Montagem do disco: volumes cifrados, RAID e LVM.
|
||||||
|
#[derive(clap::Args, Clone, Default)]
|
||||||
|
struct AsmArgs {
|
||||||
|
/// Senha de volume cifrado (BitLocker, LUKS, FileVault 2). Prefira --ask-password ou GUARA_PASSWORD
|
||||||
|
#[arg(long = "password")]
|
||||||
|
passwords: Vec<String>,
|
||||||
|
/// Pergunta a senha no terminal, sem mostrar o que é digitado
|
||||||
|
#[arg(long)]
|
||||||
|
ask_password: bool,
|
||||||
|
/// Chave de recuperação numérica do BitLocker (8 grupos de 6 dígitos)
|
||||||
|
#[arg(long = "recovery-key")]
|
||||||
|
recovery_keys: Vec<String>,
|
||||||
|
/// Arquivo de chave (.BEK do BitLocker ou keyfile do LUKS)
|
||||||
|
#[arg(long = "key-file")]
|
||||||
|
key_files: Vec<String>,
|
||||||
|
/// Chave do volume em hexadecimal (a que o Guará mostra ao abrir um volume)
|
||||||
|
#[arg(long = "volume-key")]
|
||||||
|
volume_keys: Vec<String>,
|
||||||
|
/// Outro disco ou imagem do mesmo conjunto (RAID/LVM). Pode repetir; "-" marca um disco ausente
|
||||||
|
#[arg(long = "member")]
|
||||||
|
members: Vec<String>,
|
||||||
|
/// Os discos (alvo + --member, nesta ordem) formam um RAID sem metadados: descobre sozinho nível, faixa e layout
|
||||||
|
#[arg(long)]
|
||||||
|
raid: bool,
|
||||||
|
/// Fixa o nível do RAID: 0, 5, 6, 1
|
||||||
|
#[arg(long)]
|
||||||
|
raid_level: Option<String>,
|
||||||
|
/// Fixa o tamanho da faixa do RAID em KiB
|
||||||
|
#[arg(long)]
|
||||||
|
raid_chunk_kb: Option<u64>,
|
||||||
|
/// Fixa o layout: left-symmetric, left-asymmetric, right-symmetric, right-asymmetric
|
||||||
|
#[arg(long)]
|
||||||
|
raid_layout: Option<String>,
|
||||||
|
/// Ordem dos discos no RAID (índices; 0 = alvo)
|
||||||
|
#[arg(long, value_delimiter = ',')]
|
||||||
|
raid_order: Vec<usize>,
|
||||||
|
/// Guarda na sessão as chaves dos volumes abertos (a senha nunca é guardada)
|
||||||
|
#[arg(long)]
|
||||||
|
remember_keys: bool,
|
||||||
|
/// Vídeo íntegro da mesma câmera/drone para refazer o índice de MP4 sem moov (pode repetir)
|
||||||
|
#[arg(long = "reference")]
|
||||||
|
references: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsmArgs {
|
||||||
|
fn assembly(&self) -> Result<guara_core::assemble::Assembly, String> {
|
||||||
|
use guara_core::assemble::{Assembly, RaidSpec, SecretInput};
|
||||||
|
let mut secrets = vec![];
|
||||||
|
for p in &self.passwords {
|
||||||
|
secrets.push(SecretInput::Password { value: p.clone() });
|
||||||
|
}
|
||||||
|
if let Ok(p) = std::env::var("GUARA_PASSWORD") {
|
||||||
|
if !p.is_empty() {
|
||||||
|
secrets.push(SecretInput::Password { value: p });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.ask_password {
|
||||||
|
let p = rpassword::prompt_password("Senha do volume: ").map_err(s)?;
|
||||||
|
secrets.push(SecretInput::Password { value: p });
|
||||||
|
}
|
||||||
|
for r in &self.recovery_keys {
|
||||||
|
secrets.push(SecretInput::Recovery { value: r.clone() });
|
||||||
|
}
|
||||||
|
for k in &self.key_files {
|
||||||
|
secrets.push(SecretInput::Keyfile { path: k.clone() });
|
||||||
|
}
|
||||||
|
for k in &self.volume_keys {
|
||||||
|
secrets.push(SecretInput::Key { hex: k.clone() });
|
||||||
|
}
|
||||||
|
let raid = if self.raid || self.raid_level.is_some() || self.raid_chunk_kb.is_some() || self.raid_layout.is_some() || !self.raid_order.is_empty() {
|
||||||
|
let level = match self.raid_level.as_deref() {
|
||||||
|
None => None,
|
||||||
|
Some("0") => Some(guara_core::raid::Level::R0),
|
||||||
|
Some("1") => Some(guara_core::raid::Level::R1),
|
||||||
|
Some("5") => Some(guara_core::raid::Level::R5),
|
||||||
|
Some("6") => Some(guara_core::raid::Level::R6),
|
||||||
|
Some(x) => return Err(format!("nível de RAID não suportado: {x} (use 0, 1, 5 ou 6)")),
|
||||||
|
};
|
||||||
|
let layout = match self.raid_layout.as_deref() {
|
||||||
|
None => None,
|
||||||
|
Some("left-symmetric") => Some(guara_core::raid::Layout::LeftSymmetric),
|
||||||
|
Some("left-asymmetric") => Some(guara_core::raid::Layout::LeftAsymmetric),
|
||||||
|
Some("right-symmetric") => Some(guara_core::raid::Layout::RightSymmetric),
|
||||||
|
Some("right-asymmetric") => Some(guara_core::raid::Layout::RightAsymmetric),
|
||||||
|
Some(x) => return Err(format!("layout desconhecido: {x}")),
|
||||||
|
};
|
||||||
|
Some(RaidSpec {
|
||||||
|
level,
|
||||||
|
chunk_kb: self.raid_chunk_kb,
|
||||||
|
layout,
|
||||||
|
order: (!self.raid_order.is_empty()).then(|| self.raid_order.clone()),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(Assembly {
|
||||||
|
raid,
|
||||||
|
members: self.members.iter().filter(|m| *m != "-").cloned().collect(),
|
||||||
|
secrets,
|
||||||
|
remember_keys: self.remember_keys,
|
||||||
|
keys: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(clap::Args)]
|
#[derive(clap::Args)]
|
||||||
struct ScanArgs {
|
struct ScanArgs {
|
||||||
/// Dispositivo ou imagem
|
/// Dispositivo ou imagem
|
||||||
@@ -154,6 +290,8 @@ struct ScanArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
recover: Option<String>,
|
recover: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
|
asm: AsmArgs,
|
||||||
|
#[command(flatten)]
|
||||||
filter: FilterArgs,
|
filter: FilterArgs,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
out: OutArgs,
|
out: OutArgs,
|
||||||
@@ -204,8 +342,9 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Cmd::Inspect { path, json } => {
|
Cmd::Inspect { path, json, asm } => {
|
||||||
let v = eng.rpc("inspect", json!({ "path": path })).map_err(s)?;
|
let a = asm.assembly()?;
|
||||||
|
let v = eng.rpc("inspect", json!({ "path": path, "assembly": serde_json::to_value(&a).map_err(s)?})).map_err(s)?;
|
||||||
if json {
|
if json {
|
||||||
println!("{}", serde_json::to_string_pretty(&v).unwrap());
|
println!("{}", serde_json::to_string_pretty(&v).unwrap());
|
||||||
} else {
|
} else {
|
||||||
@@ -231,6 +370,8 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
},
|
},
|
||||||
include_allocated: a.include_allocated,
|
include_allocated: a.include_allocated,
|
||||||
categories: parse_cats(&a.filter.category)?,
|
categories: parse_cats(&a.filter.category)?,
|
||||||
|
assembly: a.asm.assembly()?,
|
||||||
|
references: a.asm.references.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let job = eng.start_scan(&a.path, opts).map_err(s)?;
|
let job = eng.start_scan(&a.path, opts).map_err(s)?;
|
||||||
@@ -272,8 +413,41 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
eng.save_session(&job, &session).map_err(s)?;
|
eng.save_session(&job, &session).map_err(s)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Cmd::Image { path, dest } => {
|
Cmd::Verify { path } => {
|
||||||
let job = eng.start_image(&path, &dest).map_err(s)?;
|
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(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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::Image { path, dest, format, case, examiner, description, evidence, notes, segment_mib, compression } => {
|
||||||
|
let fmt = match format.as_str() {
|
||||||
|
"raw" => guara_core::image::ImageFormat::Raw,
|
||||||
|
"e01" => guara_core::image::ImageFormat::E01 {
|
||||||
|
case_number: case,
|
||||||
|
description,
|
||||||
|
evidence_number: evidence,
|
||||||
|
examiner,
|
||||||
|
notes,
|
||||||
|
segment_mib,
|
||||||
|
compression,
|
||||||
|
},
|
||||||
|
x => return Err(format!("formato de imagem desconhecido: {x} (use raw ou e01)")),
|
||||||
|
};
|
||||||
|
let job = eng.start_image_as(&path, &dest, &fmt).map_err(s)?;
|
||||||
let st = wait(&eng, &job, None)?;
|
let st = wait(&eng, &job, None)?;
|
||||||
if st["status"] == "error" {
|
if st["status"] == "error" {
|
||||||
return Err(st["error"].as_str().unwrap_or("falha").to_string());
|
return Err(st["error"].as_str().unwrap_or("falha").to_string());
|
||||||
@@ -813,3 +987,4 @@ fn print_health(v: &Value) {
|
|||||||
println!("\n{}", adv.as_str().unwrap_or(""));
|
println!("\n{}", adv.as_str().unwrap_or(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||||
pub enum SecretInput {
|
pub enum SecretInput {
|
||||||
Password { value: String },
|
Password { value: String },
|
||||||
@@ -50,8 +50,8 @@ pub struct Assembly {
|
|||||||
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
|
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub members: Vec<String>,
|
pub members: Vec<String>,
|
||||||
/// Senhas e chaves: nunca vão para o disco junto com a sessão.
|
/// Senhas e chaves: o `snapshot` da sessão as descarta antes de gravar.
|
||||||
#[serde(default, skip_serializing)]
|
#[serde(default)]
|
||||||
pub secrets: Vec<SecretInput>,
|
pub secrets: Vec<SecretInput>,
|
||||||
/// Guarda as chaves de volume derivadas na sessão, para reabrir sem pedir a senha de novo.
|
/// Guarda as chaves de volume derivadas na sessão, para reabrir sem pedir a senha de novo.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -24,6 +24,69 @@ pub struct ImageReport {
|
|||||||
const BLOCK: usize = 1 << 20;
|
const BLOCK: usize = 1 << 20;
|
||||||
const SECTOR: usize = 512;
|
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(
|
pub fn create_image(
|
||||||
src: &dyn Source,
|
src: &dyn Source,
|
||||||
dest: &Path,
|
dest: &Path,
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ pub fn analyze(src: &dyn Source, search_lost: bool) -> DiskLayout {
|
|||||||
for p in &layout.partitions {
|
for p in &layout.partitions {
|
||||||
if let Some(e) = &p.encrypted {
|
if let Some(e) = &p.encrypted {
|
||||||
layout.warnings.push(format!(
|
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,
|
p.index + 1,
|
||||||
e
|
e
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -548,6 +548,12 @@ impl Engine {
|
|||||||
// ---------------------------------------------------------------- imagem
|
// ---------------------------------------------------------------- imagem
|
||||||
|
|
||||||
pub fn start_image(&self, path: &str, dest: &str) -> Result<String> {
|
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))?;
|
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||||
recover::check_destination(
|
recover::check_destination(
|
||||||
Some(Path::new(path)),
|
Some(Path::new(path)),
|
||||||
@@ -570,7 +576,7 @@ impl Engine {
|
|||||||
total: t,
|
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();
|
let mut s = j2.st.lock().unwrap();
|
||||||
match res {
|
match res {
|
||||||
Ok(rep) => {
|
Ok(rep) => {
|
||||||
@@ -990,8 +996,35 @@ impl Engine {
|
|||||||
"devices" => json!(device::list_devices()),
|
"devices" => json!(device::list_devices()),
|
||||||
"inspect" => {
|
"inspect" => {
|
||||||
let path = s("path")?;
|
let path = s("path")?;
|
||||||
let src = open_path(Path::new(&path)).map_err(|e| open_error(&path, e))?;
|
let asm: crate::assemble::Assembly = p
|
||||||
json!(partition::analyze(src.as_ref(), false))
|
.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" => {
|
"scan_start" => {
|
||||||
let opts: ScanOptions = p
|
let opts: ScanOptions = p
|
||||||
@@ -1124,7 +1157,15 @@ impl Engine {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
json!({ "job": self.start_recover(&s("job")?, ids, filter, &s("dest")?, layout)? })
|
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" => {
|
"session_save" => {
|
||||||
self.save_session(&s("job")?, &s("path")?)?;
|
self.save_session(&s("job")?, &s("path")?)?;
|
||||||
json!({})
|
json!({})
|
||||||
@@ -1546,10 +1587,16 @@ fn new_session_path(target: &str) -> PathBuf {
|
|||||||
|
|
||||||
fn snapshot(j: &Job) -> SessionFile {
|
fn snapshot(j: &Job) -> SessionFile {
|
||||||
let s = j.st.lock().unwrap();
|
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 {
|
SessionFile {
|
||||||
version: 1,
|
version: 1,
|
||||||
target: j.target.clone(),
|
target: j.target.clone(),
|
||||||
options: j.opts.clone(),
|
options,
|
||||||
found: s.found.clone(),
|
found: s.found.clone(),
|
||||||
checkpoint: s.checkpoint.clone(),
|
checkpoint: s.checkpoint.clone(),
|
||||||
layout: s.layout.clone(),
|
layout: s.layout.clone(),
|
||||||
|
|||||||
Reference in new issue
Block a user