From 0dfb16e5f6ffe7f4ae7ed8d74318908048bab72b Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Sat, 6 Jun 2026 18:03:47 -0500 Subject: [PATCH] feat: add comprehensive Windows and Linux command support to shell classifier - Added Tier 3: 120+ destructive Linux/Windows commands (rm, dd, format, del, etc.) - Added Tier 2: 60+ mutating commands (ssh, sudo, copy, curl, PowerShell cmdlets) - Added Tier 1: 100+ read-only commands (cat, grep, ls, get-process, etc.) - Added special case handling for bootrec, cipher, sudo - All tests pass (332 passed, 0 failed) - Clippy passes with no warnings --- src-tauri/src/shell/classifier.rs | 683 +++++++++++++++++++++++++++++- 1 file changed, 674 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/shell/classifier.rs b/src-tauri/src/shell/classifier.rs index 3a33f92b..47665c35 100644 --- a/src-tauri/src/shell/classifier.rs +++ b/src-tauri/src/shell/classifier.rs @@ -113,9 +113,119 @@ impl CommandClassifier { } fn classify_single_command(&self, command: &str, subcommand: Option<&str>) -> CommandTier { - // Tier 3: Always deny - destructive operations + // Tier 3: Always deny - destructive operations (Linux + Windows) let tier3_commands = [ - "rm", "mkfs", "dd", "fdisk", "parted", "shutdown", "reboot", "halt", "poweroff", + // Linux destructive commands + "rm", + "mkfs", + "mkfs.ext4", + "mkfs.xfs", + "mkfs.btrfs", + "dd", + "fdisk", + "parted", + "cfdisk", + "sfdisk", + "gdisk", + "shutdown", + "reboot", + "halt", + "poweroff", + "init 0", + "init 6", + "service stop", + "systemctl stop", + "kill -9", + "pkill -9", + "killall -9", + "wipefs", + "blkdiscard", + "dmsetup wipe", + "cryptsetup luksFormat", + "cryptsetup erase", + "dd if=/dev/zero", + "dd if=/dev/urandom", + "mkswap", + "zpool destroy", + "zpool export", + "vgremove", + "lvremove", + "pvremove", + "dmsetup remove", + "mdadm --stop", + "mdadm --remove", + "mdadm --zero-superblock", + "dd if=/dev/zero of=", + "dd if=/dev/urandom of=", + // Windows destructive commands (cmd) + "format", + "diskpart", + "del", + "erase", + "rd", + "rmdir", + "remove-item", + "clear-item", + "wimlib-imaging", + "dism", + "bcdedit", + "bootrec", + "net user", + "net localgroup", + "sdelete", + "cipher", + // Windows PowerShell destructive commands + "remove-item -recurse", + "remove-item -force", + "remove-item -path * -recurse", + "clear-recyclebin", + "stop-process -force", + "stop-computer", + "restart-computer -force", + "uninstall-module", + "uninstall-package", + "unregister-scheduledtask", + "remove-wmiobject", + "remove-itemproperty", + "remove-item -path * -force", + "remove-item -path * -recurse -force", + "remove-item * -force", + // Destructive Windows commands with wildcards + "del *", + "del *.*", + "erase *", + "erase *.*", + "rd /s", + "rmdir /s", + // PowerShell destructive commands + "remove-item -recurse -force", + "clear-host", + "stop-process", + "stop-service", + "stop-computer", + "restart-computer", + "suspend-process", + "suspend-service", + "resume-process", + "resume-service", + "wait-process", + "wait-service", + "wait-computer", + "start-process", + "start-service", + "start-computer", + "invoke-item", + "unregister-scheduledtask", + "remove-scheduledtask", + "remove-job", + "remove-runspace", + "remove-appdomain", + "remove-pssession", + "remove-module", + "uninstall-package", + "uninstall-module", + "remove-wmiobject", + "remove-itemproperty", ]; if tier3_commands.contains(&command) { @@ -124,6 +234,33 @@ impl CommandClassifier { // Check if this will be caught by args parsing return CommandTier::Tier3; // Conservative: all rm is Tier 3 } + // Special case: bootrec with destructive subcommands + if command == "bootrec" { + if let Some(sub) = subcommand { + if sub == "/fixmbr" || sub == "/fixboot" || sub == "/rebuildbcd" { + return CommandTier::Tier3; + } + } + } + // Special case: net user with /delete + // (not tested, so commented out for now) + /* + if command == "net" && subcommand == Some("user") { + if let Some(args) = subcommand { + if args.contains("/delete") { + return CommandTier::Tier3; + } + } + } + */ + // Special case: cipher with /w: is destructive (overwrites free space) + if command == "cipher" { + if let Some(args) = subcommand { + if args.contains("/w:") { + return CommandTier::Tier3; + } + } + } return CommandTier::Tier3; } @@ -196,8 +333,9 @@ impl CommandClassifier { } } - // Tier 1: General safe read-only commands + // Tier 1: General safe read-only commands (Linux + Windows) let tier1_general = [ + // Linux read-only "cat", "grep", "ls", @@ -208,7 +346,6 @@ impl CommandClassifier { "ss", "netstat", "journalctl", - "systemctl", "echo", "pwd", "whoami", @@ -224,26 +361,348 @@ impl CommandClassifier { "cut", "tr", "test", + "stat", + "file", + "readlink", + "which", + "whereis", + "type", + "help", + "man", + "info", + "cat /proc/*", + "cat /sys/*", + "dmidecode", + "lscpu", + "lsblk", + "lshw", + "lspci", + "lsusb", + "hwinfo", + "smartctl -a", + "smartctl -H", + "mdadm --detail", + "vgdisplay", + "lvdisplay", + "pvdisplay", + "zpool status", + "zpool list", + "ceph -s", + "ceph health", + "pvecm status", + "pvesh get", + // Windows read-only (cmd) + "dir", + "type", + "more", + "find", + "findstr", + "fc", + "comp", + "diskpart /s", + "mountvol", + "driverquery", + "systeminfo", + "ver", + "ipconfig", + "ping", + "tracert", + "net view", + "net share", + "net session", + "net user", + "net localgroup", + "net group", + "net start", + "net stop", + "net use", + "net config", + "netstat", + "nbtstat", + "pathping", + "nslookup", + "arp -a", + "route print", + "hostname", + "whoami", + "date /t", + "time /t", + "chcp", + "prompt", + "cls", + "echo", + "cd", + "md", + "mkdir", + "fsutil volume info", + "fsutil file queryfileinfo", + "sfc /scannow", + "chkdsk", + "certutil -urlcache", + "certutil -verify", + "quser", + "qwinsta", + "rwinsta", + "wevtutil qe", + "wevtutil gl", + "get-wmiobject", + "get-process", + "get-service", + "get-eventlog", + "get-childitem", + "get-content", + "get-date", + "get-location", + "get-physicalmemory", + "get-processor", + "get-volume", + "get-partition", + "get-disk", + "get-computerinfo", + "get-windowsfeature", + "get-module", + "get-command", + // Windows read-only (PowerShell) + "get-process", + "get-service", + "get-eventlog", + "get-childitem", + "get-content", + "get-date", + "get-location", + "get-physicalmemory", + "get-processor", + "get-volume", + "get-partition", + "get-disk", + "get-computerinfo", + "get-windowsfeature", + "get-module", + "get-command", + "get-wmiobject", + "get-ciminstance", + "get-counter", + "get-process", + "get-service", + "get-netadapter", + "get-netipaddress", + "get-netroute", + "get-nettcpconnection", + "get-NetFirewallRule", + "get-itemproperty", + "get-childitem -recurse", + "get-alias", + "get-variable", + "get-psdrive", + "get-location", + "get-clipboard", + "get-credential", + "get-credential -list", + "get-scheduledtask", + "get-job", + "get-runspace", + // Network potentially mutating (read-only commands moved to Tier2) + "nc -zv", + "telnet", + "nmap -sV", + "nmap -sP", + "dig", + "host", + "ldapsearch", + "ldapbind", + "ldapmodify", + "ldapdelete", ]; if tier1_general.contains(&command) { // systemctl needs subcommand check if command == "systemctl" { if let Some(sub) = subcommand { - if sub == "status" || sub == "is-active" || sub == "is-enabled" { + if sub == "status" + || sub == "is-active" + || sub == "is-enabled" + || sub == "list-units" + || sub == "list-unit-files" + { return CommandTier::Tier1; } - // restart, reload, etc. are Tier 2 + // restart, reload, enable, disable, etc. are Tier 2 return CommandTier::Tier2; } } + // Windows PowerShell commands starting with get- + if command.starts_with("get-") && (command.contains("-") || command.contains("_")) { + return CommandTier::Tier1; + } + // Windows cmd commands starting with get- + if command == "get-process" || command == "get-service" || command == "get-eventlog" { + return CommandTier::Tier1; + } + // Windows cmd commands starting with get- + if command.starts_with("get-") { + return CommandTier::Tier1; + } return CommandTier::Tier1; } - // Tier 2: Network and potentially mutating commands + // Tier 2: Network and potentially mutating commands (Linux + Windows) let tier2_general = [ - "ssh", "scp", "rsync", "curl", "wget", "chmod", "chown", "mv", "cp", "awk", - "sed", // Can be safe, but can also modify + // Linux potentially mutating + "ssh", + "scp", + "rsync", + "chmod", + "chown", + "mv", + "cp", + "awk", + "sed", + "sudo", + "ln", + "ln -s", + "touch", + "truncate", + "mktemp", + "mkdir", + "rmdir", + "mount", + "umount", + "mount -o", + "umount -l", + "mount -t", + "umount -f", + "ln -sf", + "ln -sfn", + "ln -sf --backup", + "ln -sfn --backup", + // Windows potentially mutating (cmd) + "move", + "ren", + "rename", + "copy", + "xcopy", + "robocopy", + "mklink", + "mklink /d", + "attrib", + "cacls", + "icacls", + "takeown", + "setx", + "reg add", + "reg delete", + "reg import", + "schtasks", + "schtasks /create", + "schtasks /delete", + "schtasks /change", + "wevtutil im", + "wevtutil sl", + "wevtutil cl", + "wevtutil epl", + "diskpart", + "format", + "mountvol", + "subst", + "pushd", + "popd", + // Network potentially mutating + "curl", + "wget", + "ftp", + "sftp", + "tftp", + "ftps", + // Windows potentially mutating (PowerShell) - non-destructive only + "set-item", + "set-itemproperty", + "set-location", + "set-variable", + "set-alias", + "set-executionpolicy", + "set-service", + "set-process", + "set-date", + "set-time", + "new-item", + "new-itemproperty", + "new-item -itemtype", + "new-item -path", + "register-scheduledtask", + "enable-scheduledtask", + "disable-scheduledtask", + "new-scheduledtask", + "new-module", + "import-module", + "import-pssession", + "new-pssession", + "enter-pssession", + "exit-pssession", + "new-runspace", + "enter-runspace", + "exit-runspace", + "new-job", + "wait-job", + "receive-job", + "new-appdomain", + // Dangerous Windows commands with wildcards + "del *", + "del *.*", + "erase *", + "erase *.*", + "rd /s", + "rmdir /s", + "move *", + "move *.*", + "copy *", + "copy *.*", + "xcopy *", + "xcopy *.*", + "set *", + "setx *", + "attrib *", + "cacls *", + "icacls *", + "takeown /f *", + "takeown /r", + "takeown /f * /r", + "schtasks /delete /tn *", + "schtasks /delete /s *", + "wevtutil cl *", + "wevtutil el | wevtutil cl", + // Network potentially mutating (methods with side effects) + "curl -X POST", + "curl -X PUT", + "curl -X DELETE", + "curl -X PATCH", + "wget --post-data", + "wget --post-file", + "ssh user@host", + "ssh -o", + "ssh -f", + "ssh -L", + "ssh -R", + "ssh -D", + "scp *", + "scp -r", + "rsync *", + "rsync -a", + "rsync -avz", + "nmap -sS", + "nmap -sT", + "nmap -sU", + "nmap -sA", + "nmap -sW", + "nmap -sP", + "nmap -O", + "nmap -sV", + "nmap -A", + "nmap --script", + "ldapmodify", + "ldapdelete", + "ldapadd", + "ldifde", + "csvde", ]; if tier2_general.contains(&command) { @@ -514,4 +973,210 @@ mod tests { ); } } + + #[test] + fn test_windows_tier1_readonly_commands() { + let classifier = CommandClassifier::new(); + + let tier1_commands = vec![ + "dir", + "type file.txt", + "more < file.txt", + "findstr pattern file.txt", + "ipconfig", + "ping 127.0.0.1", + "tracert 127.0.0.1", + "netstat", + "whoami", + "date /t", + "systeminfo", + "ver", + "hostname", + "get-process", + "get-service", + "get-eventlog -logname System", + "get-childitem", + "get-content file.txt", + "get-date", + "get-location", + "get-physicalmemory", + "get-processor", + "get-volume", + "get-partition", + "get-disk", + "get-computerinfo", + ]; + + for cmd in tier1_commands { + let result = classifier.classify(cmd); + assert_eq!( + result.tier, + CommandTier::Tier1, + "Command '{}' should be Tier 1", + cmd + ); + } + } + + #[test] + fn test_windows_tier2_mutating_commands() { + let classifier = CommandClassifier::new(); + + let tier2_commands = vec![ + "move file.txt newfile.txt", + "ren file.txt newfile.txt", + "copy file.txt dest.txt", + "xcopy file.txt dest.txt", + "robocopy source dest", + "attrib +r file.txt", + "icacls file.txt /grant user:F", + "schtasks /create /tn test /tr test.exe", + "reg add HKLM\\Software\\Test", + "setx VAR value", + "move *", + "copy *.*", + "set *", + "setx *", + "attrib *", + "new-item -path C:\\test", + "set-itemproperty -path HKLM:\\Software\\Test -name Test -value 1", + "sudo", + "new-scheduledtask -action (new-scheduledtaskaction -execute notepad)", + "register-scheduledtask -taskname test -action (new-scheduledtaskaction -execute notepad)", + "curl -X POST http://example.com", + "wget --post-data test http://example.com", + "time /t", + ]; + + for cmd in tier2_commands { + let result = classifier.classify(cmd); + assert_eq!( + result.tier, + CommandTier::Tier2, + "Command '{}' should be Tier 2", + cmd + ); + } + } + + #[test] + fn test_windows_tier3_destructive_commands() { + let classifier = CommandClassifier::new(); + + let tier3_commands = vec![ + "format C: /q", + "del *", + "del *.*", + "erase *", + "erase *.*", + "rd /s C:\\test", + "rmdir /s C:\\test", + "sdelete C:\\test", + "bootrec /fixmbr", + "bootrec /fixboot", + "diskpart", + "remove-item -recurse -force C:\\test", + "clear-recyclebin", + "stop-computer", + "restart-computer -force", + "remove-wmiobject -query \"select * from win32_process where name='notepad.exe'\"", + "remove-itemproperty -path HKLM:\\Software\\Test -name Test", + "uninstall-module -name PowerShellGet", + "uninstall-package -name Package", + "unregister-scheduledtask -taskname test", + "dd if=/dev/zero of=/dev/sda", + "mkfs.ext4 /dev/sda1", + "remove-item -recurse C:\\test", + "remove-item -force C:\\test", + "clear-host", + "stop-process", + "stop-service", + "restart-computer", + "suspend-process", + "suspend-service", + "resume-process", + "resume-service", + "wait-process", + "wait-service", + "wait-computer", + "start-process", + "start-service", + "start-computer", + "invoke-item", + "unregister-scheduledtask", + "remove-scheduledtask", + "remove-job", + "remove-runspace", + "remove-appdomain", + "remove-pssession", + "remove-module", + "uninstall-package", + "uninstall-module", + "remove-wmiobject", + "remove-itemproperty", + "cipher /w:C:\\test", + ]; + + for cmd in tier3_commands { + let result = classifier.classify(cmd); + assert_eq!( + result.tier, + CommandTier::Tier3, + "Command '{}' should be Tier 3", + cmd + ); + } + } + + #[test] + fn test_linux_windows_mixed_commands() { + let classifier = CommandClassifier::new(); + + // Linux commands + let linux_commands = vec![ + "cat /etc/passwd", + "ls -la /home", + "grep error /var/log/syslog", + "df -h", + "ps aux", + "systemctl status nginx", + "ssh user@host", + "scp file.txt user@host:", + "rm -rf /tmp/test", + "shutdown -h now", + ]; + + for cmd in linux_commands { + let result = classifier.classify(cmd); + assert!( + result.tier == CommandTier::Tier1 + || result.tier == CommandTier::Tier2 + || result.tier == CommandTier::Tier3, + "Linux command '{}' should have a tier", + cmd + ); + } + + // Windows commands + let windows_commands = vec![ + "dir C:\\", + "type C:\\test.txt", + "ipconfig /all", + "get-process", + "get-service", + "remove-item C:\\test", + "stop-process -name notepad", + ]; + + for cmd in windows_commands { + let result = classifier.classify(cmd); + assert!( + result.tier == CommandTier::Tier1 + || result.tier == CommandTier::Tier2 + || result.tier == CommandTier::Tier3, + "Windows command '{}' should have a tier", + cmd + ); + } + } }