diff --git a/internal/policy/command.go b/internal/policy/command.go index 4cd8fac..1d50bcd 100644 --- a/internal/policy/command.go +++ b/internal/policy/command.go @@ -72,6 +72,7 @@ var readOnlyLeadPattern = regexp.MustCompile( `grep|egrep|fgrep|rg|wc|sort|uniq|cut|tr|tee|find|tree|locate|` + `dpkg\s+(-l|-s|--list|--status)\b|apt\s+(list|search|show)\b|` + `systemctl\s+(status|is-active|is-enabled|is-failed|list-units|list-unit-files|list-timers|show)\b|` + + `ethtool|lsmod|lspci|modinfo|dkms|` + `timedatectl|hostnamectl|systemd-analyze|` + `docker\s+(ps|images|inspect|logs|version|info|stats)|` + `docker\s+compose\s+(logs|ps|top|config|images|port|cp)\b|` + diff --git a/internal/policy/command_test.go b/internal/policy/command_test.go index 770d911..f0d9325 100644 --- a/internal/policy/command_test.go +++ b/internal/policy/command_test.go @@ -46,6 +46,12 @@ func TestClassifyCommand_ReadOnly(t *testing.T) { "sudo pct exec 121 -- systemctl status caddy", // qm guest exec on a VM, read-only inner. "qm guest exec 100 -- systemctl status caddy", + // Hardware/driver diagnostic commands (F2 fix — 2026-08-12). + "ethtool -i eno1", + "lsmod", + "lspci", + "modinfo r8125", + "dkms status", } for _, c := range cases { if got := ClassifyCommand(c, ""); got != RiskReadOnly { @@ -171,6 +177,9 @@ func TestClassifyCommand_CompoundReadOnly(t *testing.T) { // P4: the exact compound from session d0d562e0 — find + ls + tail + // echo + journalctl, all read-only segments. "ls -lt /var/log/rclone-backup/ | head -20 && tail -3 /var/log/rclone-backup/runs.jsonl || echo \"not found\" && find /var/log/rclone-backup/ -name 'runs.jsonl'", + // F2 fix: the exact compound diagnostic that was misclassified as + // config_mutation (2026-08-12 hubris NIC driver cutover session). + "uname -r && echo '---' && ethtool -i eno1 && echo '---' && lsmod | grep r8169 && echo '---' && ip link show eno1 && echo '---' && cat /etc/network/interfaces | head -30", } for _, c := range cases { if got := ClassifyCommand(c, ""); got != RiskReadOnly {