feat: add comprehensive Windows and Linux command support to shell classifier
All checks were successful
Test / frontend-tests (pull_request) Successful in 1m22s
Test / frontend-typecheck (pull_request) Successful in 1m29s
PR Review Automation / review (pull_request) Successful in 3m54s
Test / rust-fmt-check (pull_request) Successful in 10m49s
Test / rust-clippy (pull_request) Successful in 12m33s
Test / rust-tests (pull_request) Successful in 14m18s

- 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
This commit is contained in:
Shaun Arman 2026-06-06 18:03:47 -05:00
parent 4ba0eb1ca9
commit 0dfb16e5f6

View File

@ -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
);
}
}
}