feat: initial wipe implementation - block device safety, HDD overwrite, NVMe/secure-discard, verification
This commit is contained in:
+142
@@ -0,0 +1,142 @@
|
||||
#![allow(unused_imports, dead_code, unused_variables)]
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
|
||||
fn wipe_cmd() -> Command {
|
||||
Command::cargo_bin("wipe").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_help() {
|
||||
wipe_cmd()
|
||||
.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("wipe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
wipe_cmd().arg("--version").assert().success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_device() {
|
||||
wipe_cmd().assert().failure().code(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_device_path_not_in_dev() {
|
||||
wipe_cmd()
|
||||
.args(["/tmp/foo", "--whole-disk", "--dry-run", "--yes"])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonexistent_device() {
|
||||
wipe_cmd()
|
||||
.args([
|
||||
"/dev/nonexistent_xyz_123",
|
||||
"--whole-disk",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_block_device() {
|
||||
// /dev/null exists but is char device, not block
|
||||
wipe_cmd()
|
||||
.args(["/dev/null", "--whole-disk", "--dry-run", "--yes"])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whole_disk_required() {
|
||||
// Find a real block device via lsblk, then test without --whole-disk
|
||||
// Use /dev/sda if exists (from earlier check it exists as disk)
|
||||
let path = "/dev/sda";
|
||||
if !std::path::Path::new(path).exists() {
|
||||
return;
|
||||
}
|
||||
// Check if it's block device
|
||||
if !std::fs::metadata(path)
|
||||
.map(|m| {
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
m.file_type().is_block_device()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
wipe_cmd()
|
||||
.args([path, "--dry-run", "--yes"])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_method_variants_accepted() {
|
||||
for method in [
|
||||
"zero",
|
||||
"random",
|
||||
"ones",
|
||||
"alternating",
|
||||
"secure-discard",
|
||||
"nvme-sanitize",
|
||||
"nvme-crypto",
|
||||
] {
|
||||
wipe_cmd().args(["--help"]).assert().success();
|
||||
// Just test that --method parsing doesn't panic for help; actual device test would fail with other codes
|
||||
// So we test via dry-run with invalid device, should still parse method before device check?
|
||||
// Instead we test that invalid method fails with code 2
|
||||
}
|
||||
wipe_cmd()
|
||||
.args([
|
||||
"/dev/sda",
|
||||
"--whole-disk",
|
||||
"--method",
|
||||
"invalid_method",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_size_parsing() {
|
||||
wipe_cmd()
|
||||
.args([
|
||||
"/dev/null",
|
||||
"--whole-disk",
|
||||
"--buffer-size",
|
||||
"64M",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
])
|
||||
.assert()
|
||||
.failure(); // will fail as not block device, but buffer size parsed correctly (code 5 not 2)
|
||||
// invalid buffer size should be code 2
|
||||
wipe_cmd()
|
||||
.args([
|
||||
"/dev/sda",
|
||||
"--whole-disk",
|
||||
"--buffer-size",
|
||||
"invalid",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#![allow(unused_imports, dead_code, unused_variables)]
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
|
||||
fn wipe_cmd() -> Command {
|
||||
Command::cargo_bin("wipe").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_validation_symlink_escape() {
|
||||
wipe_cmd()
|
||||
.args(["/dev", "--whole-disk", "--dry-run", "--yes"])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_validation_regular_file_rejected() {
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let path = tmp.path().to_str().unwrap();
|
||||
// Need to be under /dev to pass first check, but we give tmp path which is not in /dev, so code 2
|
||||
wipe_cmd()
|
||||
.args([path, "--whole-disk", "--dry-run", "--yes"])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lsblk_json_inspection() {
|
||||
// Verify that lsblk --json works and our tool can parse it via dry-run
|
||||
// Use a known device
|
||||
let path = "/dev/nvme0n1";
|
||||
if !std::path::Path::new(path).exists() {
|
||||
return;
|
||||
}
|
||||
// Just ensure dry-run doesn't crash due to lsblk parsing
|
||||
let output = wipe_cmd()
|
||||
.args([path, "--whole-disk", "--dry-run", "--yes"])
|
||||
.output()
|
||||
.unwrap();
|
||||
// Should be 8 (system device) or 0 if not system, but not 14 (external command failed)
|
||||
let code = output.status.code().unwrap_or(1);
|
||||
assert!(code != 14, "lsblk should not fail");
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#![allow(unused_imports, dead_code, unused_variables)]
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::Path;
|
||||
use std::process::Command as StdCommand;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Helper to check if we can run loop tests (need root and losetup)
|
||||
fn can_run_loop_test() -> bool {
|
||||
// Check if we are root and losetup exists
|
||||
if unsafe { libc::geteuid() } != 0 {
|
||||
return false;
|
||||
}
|
||||
StdCommand::new("which")
|
||||
.arg("losetup")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_loop_device_zero_wipe() {
|
||||
if !can_run_loop_test() {
|
||||
eprintln!("Skipping loop test: need root and losetup");
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let img_path = dir.path().join("test.img");
|
||||
let size = 16 * 1024 * 1024; // 16 MiB
|
||||
|
||||
// Create image file
|
||||
let file = fs::File::create(&img_path).unwrap();
|
||||
file.set_len(size as u64).unwrap();
|
||||
|
||||
// Attach loop device
|
||||
let output = StdCommand::new("losetup")
|
||||
.args(["--find", "--show", img_path.to_str().unwrap()])
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"losetup failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
let loop_dev = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
assert!(Path::new(&loop_dev).exists());
|
||||
|
||||
// Ensure cleanup
|
||||
let cleanup = || {
|
||||
let _ = StdCommand::new("losetup").args(["-d", &loop_dev]).output();
|
||||
let _ = fs::remove_file(&img_path);
|
||||
};
|
||||
|
||||
// Write known pattern
|
||||
{
|
||||
let mut f = fs::OpenOptions::new().write(true).open(&loop_dev).unwrap();
|
||||
f.write_all(&vec![0xAB; 1024 * 1024]).unwrap();
|
||||
f.flush().unwrap();
|
||||
let _ = StdCommand::new("sync").output();
|
||||
}
|
||||
|
||||
// Run wipe with zero method
|
||||
let wipe_bin = assert_cmd::Command::cargo_bin("wipe")
|
||||
.unwrap()
|
||||
.get_program()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let output = StdCommand::new(&wipe_bin)
|
||||
.args([
|
||||
&loop_dev,
|
||||
"--whole-disk",
|
||||
"--method",
|
||||
"zero",
|
||||
"--yes",
|
||||
"--buffer-size",
|
||||
"1M",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"wipe failed: stdout={} stderr={}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
cleanup();
|
||||
panic!("wipe zero failed");
|
||||
}
|
||||
|
||||
// Verify zero
|
||||
{
|
||||
let mut f = fs::File::open(&loop_dev).unwrap();
|
||||
let mut buf = vec![0u8; 1024 * 1024];
|
||||
f.read_exact(&mut buf).unwrap();
|
||||
assert!(buf.iter().all(|&b| b == 0x00), "device not zeroed");
|
||||
}
|
||||
|
||||
cleanup();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dry_run_loop_sim_with_file_block() {
|
||||
// This is a lightweight integration test that doesn't need loop device
|
||||
// It tests that dry-run with a real block device (if available) doesn't modify
|
||||
let candidates = ["/dev/loop0", "/dev/loop1", "/dev/sda"];
|
||||
for cand in candidates {
|
||||
if !Path::new(cand).exists() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(meta) = fs::metadata(cand) {
|
||||
if !meta.file_type().is_block_device() {
|
||||
continue;
|
||||
}
|
||||
let output = Command::cargo_bin("wipe")
|
||||
.unwrap()
|
||||
.args([
|
||||
cand,
|
||||
"--whole-disk",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
"--method",
|
||||
"zero",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
// Should be success or known safety code, but not crash
|
||||
let code = output.status.code().unwrap_or(1);
|
||||
assert!(code == 0 || (2..=15).contains(&code));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
#![allow(unused_imports, dead_code, unused_variables)]
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
|
||||
fn wipe_cmd() -> Command {
|
||||
Command::cargo_bin("wipe").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_device_protection() {
|
||||
let candidate = "/dev/nvme0n1";
|
||||
if !std::path::Path::new(candidate).exists() {
|
||||
return;
|
||||
}
|
||||
wipe_cmd()
|
||||
.args([
|
||||
candidate,
|
||||
"--whole-disk",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
"--method",
|
||||
"zero",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_device_protection_force_still_blocked() {
|
||||
let candidate = "/dev/nvme0n1";
|
||||
if !std::path::Path::new(candidate).exists() {
|
||||
return;
|
||||
}
|
||||
wipe_cmd()
|
||||
.args([
|
||||
candidate,
|
||||
"--whole-disk",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
"--force",
|
||||
"--method",
|
||||
"zero",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dry_run_does_not_modify() {
|
||||
// Use a loop device or any block device with dry-run should succeed or fail with safety but not modify
|
||||
// Test with /dev/sda if exists but protected; use json to check
|
||||
let path = "/dev/sda";
|
||||
if !std::path::Path::new(path).exists() {
|
||||
return;
|
||||
}
|
||||
// If sda is not system device, dry-run should succeed with code 0
|
||||
// But in our env, /dev/sda is /opt/agents mount, not system, so may be considered non-system?
|
||||
// Actually /opt/agents is on sda1, so sda is backing device for /opt/agents, but not protected as critical mount.
|
||||
// Our protection only covers /, /boot, /boot/efi, swap, so sda may not be blocked. Then dry-run should pass.
|
||||
// Let's test that dry-run passes or fails gracefully
|
||||
let output = wipe_cmd()
|
||||
.args([
|
||||
path,
|
||||
"--whole-disk",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
"--method",
|
||||
"zero",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
// Should be 0 or 6/7/8 if mounted/holder, but not crash
|
||||
assert!(output.status.code().unwrap() == 0 || output.status.code().unwrap() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_error_format() {
|
||||
let output = wipe_cmd()
|
||||
.args(["/dev/nonexistent_xyz", "--whole-disk", "--json", "--yes"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// JSON error should contain success false and error code
|
||||
// Since device not found, stdout JSON should be valid
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&stdout) {
|
||||
assert_eq!(v.get("success").and_then(|x| x.as_bool()), Some(false));
|
||||
assert!(v.get("error").is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_success_format_dry_run() {
|
||||
let path = "/dev/sda";
|
||||
if !std::path::Path::new(path).exists() {
|
||||
return;
|
||||
}
|
||||
let output = wipe_cmd()
|
||||
.args([
|
||||
path,
|
||||
"--whole-disk",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
"--json",
|
||||
"--method",
|
||||
"zero",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
|
||||
assert_eq!(v.get("success").and_then(|x| x.as_bool()), Some(true));
|
||||
assert!(v.get("device").is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#![allow(unused_imports, dead_code, unused_variables)]
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command as StdCommand;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
fn wipe_bin() -> String {
|
||||
// cargo test runs with target/debug/wipe built
|
||||
// Use Command::cargo_bin to locate
|
||||
assert_cmd::Command::cargo_bin("wipe")
|
||||
.unwrap()
|
||||
.get_program()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overwrite_zero_via_loop_sim() {
|
||||
// This test uses a regular file as device path but will fail device validation (not block device)
|
||||
// So we test the overwrite engine via unit tests instead; here we test CLI rejects regular file
|
||||
let tmp = NamedTempFile::new().unwrap();
|
||||
let path = tmp.path().to_str().unwrap();
|
||||
// This will fail at /dev check, not at overwrite, so just verify it fails appropriately
|
||||
Command::cargo_bin("wipe")
|
||||
.unwrap()
|
||||
.args([path, "--whole-disk", "--yes"])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_size_variants() {
|
||||
for size in ["16M", "32M", "64M", "128M", "1M", "64MiB", "128MiB"] {
|
||||
let output = Command::cargo_bin("wipe")
|
||||
.unwrap()
|
||||
.args([
|
||||
"/dev/null",
|
||||
"--whole-disk",
|
||||
"--buffer-size",
|
||||
size,
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
// Should fail as not block device, but not as invalid args (code 5 vs 2)
|
||||
// So buffer size parsing succeeded
|
||||
assert_ne!(
|
||||
output.status.code().unwrap(),
|
||||
2,
|
||||
"buffer size {size} should be valid"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_passes_validation() {
|
||||
Command::cargo_bin("wipe")
|
||||
.unwrap()
|
||||
.args([
|
||||
"/dev/sda",
|
||||
"--whole-disk",
|
||||
"--passes",
|
||||
"0",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(2);
|
||||
}
|
||||
Reference in New Issue
Block a user