use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};
#[must_use]
pub fn create_pack() -> Pack {
Pack {
id: "system.disk".to_string(),
name: "Disk Operations",
description: "Protects against destructive disk operations like dd to devices, \
mkfs, partition table modifications, RAID management, \
btrfs/LVM/device-mapper operations, and network block devices",
keywords: &[
"dd",
"fdisk",
"mkfs",
"mkswap",
"parted",
"mount",
"wipefs",
"/dev/",
"mdadm",
"btrfs",
"dmsetup",
"nbd-client",
"pvremove",
"vgremove",
"lvremove",
"vgreduce",
"lvreduce",
"lvresize",
"pvmove",
],
safe_patterns: create_safe_patterns(),
destructive_patterns: create_destructive_patterns(),
keyword_matcher: None,
safe_regex_set: None,
safe_regex_set_is_complete: false,
}
}
fn create_safe_patterns() -> Vec<SafePattern> {
vec![
safe_pattern!("dd-file-out", r#"dd\s+.*of=['"]?[^/\s'"]+\."#),
safe_pattern!(
"dd-discard",
r#"dd\s+.*of=['"]?/dev/(?:null|zero|full)['"]?(?:\s|$)"#
),
safe_pattern!("lsblk", r"\blsblk\b"),
safe_pattern!("fdisk-list", r"fdisk\s+-l"),
safe_pattern!(
"parted-print",
r#"parted\b(?:\s+--?\S+)*\s+(?:['"]?/dev/\S+['"]?\s+)?print(?:\s+(?:devices|free|list|all|\d+))?\s*$"#
),
safe_pattern!("blkid", r"\bblkid\b"),
safe_pattern!("df", r"\bdf\b"),
safe_pattern!("mount-list", r"\bmount\s*$"),
safe_pattern!("mkswap-check", r"mkswap\s+(?:.*\s+)?--check\b"),
safe_pattern!("mdadm-detail", r"mdadm\s+--detail\b"),
safe_pattern!("mdadm-examine", r"mdadm\s+--examine\b"),
safe_pattern!("mdadm-query", r"mdadm\s+--query\b"),
safe_pattern!("mdadm-query-short", r"mdadm\s+-Q\b"),
safe_pattern!("mdadm-scan", r"mdadm\s+--scan\b"),
safe_pattern!(
"btrfs-subvolume-list",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+subvolume\s+list(?=\s|$)"
),
safe_pattern!(
"btrfs-subvolume-show",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+subvolume\s+show(?=\s|$)"
),
safe_pattern!(
"btrfs-filesystem-show",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+filesystem\s+show(?=\s|$)"
),
safe_pattern!(
"btrfs-filesystem-df",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+filesystem\s+df(?=\s|$)"
),
safe_pattern!(
"btrfs-filesystem-usage",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+filesystem\s+usage(?=\s|$)"
),
safe_pattern!(
"btrfs-device-stats",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+device\s+stats(?=\s|$)"
),
safe_pattern!(
"btrfs-property-get",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+property\s+(?:get|list)(?=\s|$)"
),
safe_pattern!(
"btrfs-scrub-status",
r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+scrub\s+status(?=\s|$)"
),
safe_pattern!(
"dmsetup-ls",
r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ls(?=\s|$)"
),
safe_pattern!(
"dmsetup-status",
r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+status(?=\s|$)"
),
safe_pattern!(
"dmsetup-info",
r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+info(?=\s|$)"
),
safe_pattern!(
"dmsetup-table",
r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+table(?=\s|$)"
),
safe_pattern!(
"dmsetup-deps",
r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+deps(?=\s|$)"
),
safe_pattern!("nbd-client-list", r"nbd-client\s+-l\b"),
safe_pattern!("nbd-client-check", r"nbd-client\s+.*-check\b"),
safe_pattern!("lvm-list", r"\b(?:lvs|vgs|pvs)\b"),
safe_pattern!("lvm-display", r"\b(?:lvdisplay|vgdisplay|pvdisplay)\b"),
safe_pattern!("lvm-scan", r"\b(?:lvscan|vgscan|pvscan)\b"),
]
}
fn create_destructive_patterns() -> Vec<DestructivePattern> {
vec![
destructive_pattern!(
"dd-device",
r#"dd\s+.*of=['"]?/dev/"#,
"dd to a block device will OVERWRITE all data on that device. Extremely dangerous!"
),
destructive_pattern!(
"dd-wipe",
r#"dd\s+.*if=['"]?/dev/(?:zero|urandom|random).*of=['"]?/dev/"#,
"dd from /dev/zero or /dev/urandom to a device will WIPE all data!"
),
destructive_pattern!(
"fdisk-edit",
r#"fdisk\s+['"]?/dev/(?!.*-l)"#,
"fdisk can modify partition tables and cause data loss."
),
destructive_pattern!(
"parted-modify",
r#"parted\b[^\n;&|]*?['"]?/dev/\S+['"]?(?:\s+--)?\s+(?:(?!\s*(?:align-check|help|h|print|p|quit|q|select|unit|u)\b)|[^\n;&|]*\b(?:print|p)\b\s+(?:(?:devices|free|list|all|\d+)\s+\S+|(?!devices\b|free\b|list\b|all\b|\d+\b)\S+)|[^\n;&|]*\b(?:disk_set|disk_toggle|mklabel|mktable|mkpart|name|rescue|resizepart|rm|set|toggle|type)\b)"#,
"parted can modify partition tables and cause data loss."
),
destructive_pattern!(
"mkfs",
r"mkfs(?:\.[a-z0-9]+)?\s+",
"mkfs formats a partition/device and ERASES all existing data."
),
destructive_pattern!(
"mkswap",
r"mkswap\s+",
"mkswap formats a partition as a swap area, ERASING any existing data."
),
destructive_pattern!(
"wipefs",
r"wipefs\s+",
"wipefs removes filesystem signatures. Use with extreme caution."
),
destructive_pattern!(
"mount-bind-root",
r#"mount\s+.*--bind\s+.*\s+['"]?/(?:$|[^a-z])"#,
"mount --bind to root directory can have system-wide effects."
),
destructive_pattern!(
"umount-force",
r"umount\s+.*-[a-z]*f",
"umount -f force unmounts which may cause data loss if device is in use."
),
destructive_pattern!(
"losetup-device",
r#"losetup\s+['"]?/dev/loop"#,
"losetup modifies loop device associations. Verify before proceeding."
),
destructive_pattern!(
"mdadm-stop",
r"mdadm\s+(?:.*\s+)?(?:--stop|-S)\b",
"mdadm --stop shuts down a RAID array. Data may become inaccessible."
),
destructive_pattern!(
"mdadm-remove",
r"mdadm\s+(?:.*\s+)?--remove\b",
"mdadm --remove removes a drive from a RAID array. May cause data loss if redundancy is lost."
),
destructive_pattern!(
"mdadm-fail",
r"mdadm\s+(?:.*\s+)?(?:--fail|-f)\b",
"mdadm --fail marks a device as failed. Use only for intentional drive replacement."
),
destructive_pattern!(
"mdadm-zero-superblock",
r"mdadm\s+(?:.*\s+)?--zero-superblock\b",
"mdadm --zero-superblock PERMANENTLY erases RAID metadata. Array cannot be reassembled."
),
destructive_pattern!(
"mdadm-create",
r"mdadm\s+(?:.*\s+)?(?:--create|-C)\b",
"mdadm --create initializes a new RAID array, ERASING existing data on member devices."
),
destructive_pattern!(
"mdadm-grow",
r"mdadm\s+(?:.*\s+)?--grow\b",
"mdadm --grow reshapes a RAID array. Interruption can cause data loss. Backup first."
),
destructive_pattern!(
"btrfs-subvolume-delete",
r"btrfs\b.*?\s+subvolume\s+delete\b",
"btrfs subvolume delete PERMANENTLY removes a subvolume and all its data."
),
destructive_pattern!(
"btrfs-device-remove",
r"btrfs\b.*?\s+device\s+(?:remove|delete)\b",
"btrfs device remove redistributes data off a device. Interruption causes data loss."
),
destructive_pattern!(
"btrfs-device-add",
r"btrfs\b.*?\s+device\s+add\b",
"btrfs device add incorporates a device into the filesystem. Verify the device is correct."
),
destructive_pattern!(
"btrfs-balance",
r"btrfs\b.*?\s+balance\s+start\b",
"btrfs balance redistributes data across devices. Can be slow and disruptive."
),
destructive_pattern!(
"btrfs-check-repair",
r"btrfs\b.*?\s+check\s+(?:.*\s+)?--repair\b",
"btrfs check --repair is DANGEROUS and can cause data loss. Backup first!"
),
destructive_pattern!(
"btrfs-rescue",
r"btrfs\b.*?\s+rescue\b",
"btrfs rescue operations modify filesystem metadata. Use only as last resort."
),
destructive_pattern!(
"btrfs-filesystem-resize",
r"btrfs\b.*?\s+filesystem\s+resize\b",
"btrfs filesystem resize can shrink a filesystem. Data loss if size is too small."
),
destructive_pattern!(
"dmsetup-remove",
r"dmsetup\b.*?\s+remove\b",
"dmsetup remove detaches a device-mapper device. May cause data loss if in use."
),
destructive_pattern!(
"dmsetup-remove-all",
r"dmsetup\b.*?\s+remove_all\b",
"dmsetup remove_all removes ALL device-mapper devices. Extremely dangerous!"
),
destructive_pattern!(
"dmsetup-wipe-table",
r"dmsetup\b.*?\s+wipe_table\b",
"dmsetup wipe_table replaces the device table, causing all I/O to fail."
),
destructive_pattern!(
"dmsetup-clear",
r"dmsetup\b.*?\s+clear\b",
"dmsetup clear removes the mapping table from a device."
),
destructive_pattern!(
"dmsetup-load",
r"dmsetup\b.*?\s+load\b",
"dmsetup load changes device mapping. Verify the new table is correct."
),
destructive_pattern!(
"dmsetup-create",
r"dmsetup\b.*?\s+create\b",
"dmsetup create sets up a new device-mapper device. Verify parameters carefully."
),
destructive_pattern!(
"nbd-client-disconnect",
r"nbd-client\s+(?:.*\s+)?-d\b",
"nbd-client -d disconnects a network block device. Data loss if not properly unmounted."
),
destructive_pattern!(
"nbd-client-connect",
r#"nbd-client\s+\S+\s+\d+\s+['"]?/dev/nbd"#,
"nbd-client connecting a device can expose or overwrite data. Verify server and device."
),
destructive_pattern!(
"pvremove",
r"\bpvremove\b",
"pvremove ERASES LVM metadata from a physical volume. Data becomes inaccessible."
),
destructive_pattern!(
"vgremove",
r"\bvgremove\b",
"vgremove DELETES a volume group and all logical volumes within it."
),
destructive_pattern!(
"lvremove",
r"\blvremove\b",
"lvremove PERMANENTLY deletes a logical volume and ALL its data."
),
destructive_pattern!(
"vgreduce",
r"\bvgreduce\b",
"vgreduce removes a physical volume from a volume group. Data may be lost."
),
destructive_pattern!(
"lvreduce",
r"\blvreduce\b",
"lvreduce SHRINKS a logical volume. Data loss if filesystem isn't resized first!"
),
destructive_pattern!(
"lvresize-shrink",
r"lvresize\s+(?:.*\s+)?(?:-L\s*-|-l\s*-|--size\s+\S*-)",
"lvresize with negative size SHRINKS the volume. Resize filesystem first or lose data!"
),
destructive_pattern!(
"pvmove",
r"\bpvmove\b",
"pvmove migrates data between physical volumes. Do NOT interrupt or data may be lost."
),
destructive_pattern!(
"lvconvert-merge",
r"lvconvert\s+(?:.*\s+)?--merge\b",
"lvconvert --merge reverts LV to snapshot state, discarding changes since snapshot."
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packs::Severity;
use crate::packs::test_helpers::*;
#[test]
fn wipefs_is_reachable_via_keywords() {
let pack = create_pack();
assert!(
pack.might_match("wipefs --all somefile.img"),
"wipefs should be included in pack keywords to prevent false negatives"
);
let matched = pack
.check("wipefs --all somefile.img")
.expect("wipefs should be blocked by disk pack");
assert_eq!(matched.name, Some("wipefs"));
}
#[test]
fn keyword_absent_skips_pack() {
let pack = create_pack();
assert!(!pack.might_match("echo hello"));
assert!(pack.check("echo hello").is_none());
}
#[test]
fn dd_quote_bypass_is_closed() {
let pack = create_pack();
let matched = pack
.check("dd if=/dev/zero of=\"/dev/sda\" bs=1M")
.expect("dd of=\"...\" must still block");
assert_eq!(matched.name, Some("dd-device"));
let matched = pack
.check("dd of='/dev/sdb' if=something.img")
.expect("dd of='...' must still block");
assert_eq!(matched.name, Some("dd-device"));
assert!(
pack.matches_safe("dd if=myfile of=\"/dev/null\""),
"safe /dev/null discard must accept quoted path"
);
}
#[test]
fn btrfs_dmsetup_global_flags_do_not_bypass() {
let pack = create_pack();
let matched = pack
.check("btrfs --format json subvolume delete /mnt/foo")
.expect("btrfs --format subvolume delete should still block");
assert_eq!(matched.name, Some("btrfs-subvolume-delete"));
let matched = pack
.check("btrfs --verbose check --repair /dev/sda1")
.expect("btrfs --verbose check --repair should still block");
assert_eq!(matched.name, Some("btrfs-check-repair"));
let matched = pack
.check("dmsetup -v remove_all")
.expect("dmsetup -v remove_all should still block");
assert_eq!(matched.name, Some("dmsetup-remove-all"));
let matched = pack
.check("dmsetup --noudevsync remove my-dev")
.expect("dmsetup with noudevsync should still block");
assert_eq!(matched.name, Some("dmsetup-remove"));
}
#[test]
fn parted_print_only_forms_remain_allowed() {
let pack = create_pack();
let safe_prints = [
"parted /dev/sda print",
"parted /dev/sda print free",
"parted /dev/sda print all",
"parted -s /dev/sda print 1",
];
for cmd in safe_prints {
assert!(
pack.matches_safe(cmd),
"read-only parted print form should match safe pattern: {cmd}"
);
assert!(
pack.check(cmd).is_none(),
"read-only parted print form should be allowed: {cmd}"
);
}
assert_no_match(&pack, "parted /dev/sda unit s print free");
assert_no_match(&pack, "parted -l");
}
#[test]
fn parted_print_prefix_and_global_flags_do_not_bypass_modifications() {
let pack = create_pack();
let destructive = [
"parted /dev/sda print rm 1",
"parted /dev/sda p rm 1",
"parted /dev/sda print mkla gpt",
"parted /dev/sda print free rm 1",
"parted /dev/sda print mklabel gpt",
"parted /dev/sda print mkpart primary ext4 1MiB 1GiB",
"parted /dev/sda unit s rm 1",
"parted /dev/sda unit s p mkla gpt",
"parted -s /dev/sda mklabel gpt",
"parted --script /dev/sda rm 1",
"parted -s /dev/sdX -- mklabel msdos mkpart primary fat32 64s 4MiB",
];
for cmd in destructive {
let matched = pack
.check(cmd)
.unwrap_or_else(|| panic!("parted mutation must block: {cmd}"));
assert_eq!(matched.name, Some("parted-modify"), "wrong rule for {cmd}");
}
}
#[test]
fn disk_blocks_with_correct_severity() {
let pack = create_pack();
assert_blocks_with_severity(&pack, "dd if=/dev/zero of=/dev/sda bs=1M", Severity::High);
assert_blocks_with_severity(&pack, "fdisk /dev/sda", Severity::High);
assert_blocks_with_severity(&pack, "mkfs.ext4 /dev/sdb1", Severity::High);
assert_blocks_with_severity(&pack, "wipefs --all /dev/sdb", Severity::High);
assert_blocks_with_severity(&pack, "mdadm --stop /dev/md0", Severity::High);
assert_blocks_with_severity(&pack, "btrfs subvolume delete /mnt/foo", Severity::High);
assert_blocks_with_severity(&pack, "dmsetup remove my-dev", Severity::High);
assert_blocks_with_severity(&pack, "pvremove /dev/sda1", Severity::High);
assert_blocks_with_severity(&pack, "vgremove my-vg", Severity::High);
assert_blocks_with_severity(&pack, "lvremove my-vg/my-lv", Severity::High);
}
#[test]
fn disk_unrelated_commands_no_match() {
let pack = create_pack();
assert_no_match(&pack, "git status");
assert_no_match(&pack, "echo hello");
assert_no_match(&pack, "cargo build");
}
#[test]
fn mkswap_blocks_destructive_variants() {
let pack = create_pack();
let cases = [
"mkswap /dev/sdb",
"mkswap /dev/sda1",
"sudo mkswap /dev/sdb",
"mkswap -L swap1 /dev/sdb1",
"mkswap -U random /dev/nvme0n1p2",
];
for cmd in cases {
let matched = pack
.check(cmd)
.unwrap_or_else(|| panic!("mkswap command must block: {cmd}"));
assert_eq!(matched.name, Some("mkswap"), "wrong rule for {cmd}");
assert_eq!(matched.severity, Severity::High);
}
}
#[test]
fn mkswap_check_and_unrelated_text_allowed() {
let pack = create_pack();
assert!(
pack.matches_safe("mkswap --check /dev/sdb"),
"mkswap --check must be safe"
);
assert!(
pack.matches_safe("mkswap -L swap1 --check /dev/sdb1"),
"mkswap with other flags + --check must be safe"
);
assert_no_match(&pack, "cat mkswap-readme.md");
assert_no_match(&pack, "ls /usr/share/doc/mkswap");
}
#[test]
fn mkswap_keyword_reaches_pack() {
let pack = create_pack();
assert!(
pack.might_match("mkswap /dev/sdb"),
"mkswap must be in pack keywords or it will be filtered out before regex eval"
);
}
}