Updated command policy to be more nuanced: BLOCKED (hard block, never allowed): - rm -rf / (root) - rm -rf /* (root wildcard) - rm -rf /home, /etc, /usr, /var/lib, /boot, /root, /bin, /sbin, /lib, /opt REQUIRE APPROVAL (user must click 'Run'): - rm -rf /var/tmp/* (Proxmox vzdump temp files) - rm -rf /tmp/* This allows AI to suggest cleaning up vzdump temp files while still protecting against destructive operations on critical paths.
278 lines
6.7 KiB
Go
278 lines
6.7 KiB
Go
package agentexec
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// CommandPolicy defines what commands are allowed, blocked, or require approval
|
|
type CommandPolicy struct {
|
|
// AutoApprove patterns - commands matching these are automatically allowed
|
|
AutoApprove []string
|
|
|
|
// RequireApproval patterns - commands matching these need user approval
|
|
RequireApproval []string
|
|
|
|
// Blocked patterns - commands matching these are never allowed
|
|
Blocked []string
|
|
|
|
// compiled regex patterns
|
|
autoApproveRe []*regexp.Regexp
|
|
requireApprovalRe []*regexp.Regexp
|
|
blockedRe []*regexp.Regexp
|
|
}
|
|
|
|
// DefaultPolicy returns a sensible default command policy
|
|
func DefaultPolicy() *CommandPolicy {
|
|
p := &CommandPolicy{
|
|
AutoApprove: []string{
|
|
// System inspection
|
|
`^ps(\s|$)`,
|
|
`^top\s+-bn`,
|
|
`^df(\s|$)`,
|
|
`^free(\s|$)`,
|
|
`^uptime$`,
|
|
`^hostname$`,
|
|
`^uname(\s|$)`,
|
|
`^cat\s+/proc/`,
|
|
`^cat\s+/etc/os-release`,
|
|
`^lsof(\s|$)`,
|
|
`^netstat(\s|$)`,
|
|
`^ss(\s|$)`,
|
|
`^ip\s+(addr|route|link)`,
|
|
`^ifconfig`,
|
|
`^w$`,
|
|
`^who$`,
|
|
`^last(\s|$)`,
|
|
|
|
// Log reading (read-only)
|
|
`^cat\s+/var/log/`,
|
|
`^tail\s+.*(/var/log/|/log/)`,
|
|
`^head\s+.*(/var/log/|/log/)`,
|
|
`^grep\s+.*(/var/log/|/log/)`,
|
|
`^journalctl\s`,
|
|
|
|
// Service status (read-only)
|
|
`^systemctl\s+status\s`,
|
|
`^systemctl\s+is-active\s`,
|
|
`^systemctl\s+is-enabled\s`,
|
|
`^systemctl\s+list-units`,
|
|
`^service\s+\S+\s+status`,
|
|
|
|
// Docker inspection (read-only)
|
|
`^docker\s+ps`,
|
|
`^docker\s+logs\s`,
|
|
`^docker\s+inspect\s`,
|
|
`^docker\s+stats\s+--no-stream`,
|
|
`^docker\s+top\s`,
|
|
`^docker\s+images`,
|
|
`^docker\s+network\s+ls`,
|
|
`^docker\s+volume\s+ls`,
|
|
|
|
// Proxmox inspection (read-only)
|
|
`^pct\s+list`,
|
|
`^pct\s+config\s`,
|
|
`^pct\s+status\s`,
|
|
`^qm\s+list`,
|
|
`^qm\s+config\s`,
|
|
`^qm\s+status\s`,
|
|
`^pvesh\s+get\s`,
|
|
|
|
// Disk/storage info
|
|
`^lsblk`,
|
|
`^blkid`,
|
|
`^fdisk\s+-l`,
|
|
`^du(\s|$)`,
|
|
`^ls(\s|$)`,
|
|
`^stat(\s|$)`,
|
|
`^file(\s|$)`,
|
|
`^find\s+/.*-size`, // Find large files
|
|
`^find\s+/.*-mtime`, // Find by modification time
|
|
`^find\s+/.*-type`, // Find by type
|
|
|
|
// Memory inspection
|
|
`^vmstat`,
|
|
`^sar\s`,
|
|
`^iostat`,
|
|
`^mpstat`,
|
|
|
|
// Docker inspection (read-only)
|
|
`^docker\s+system\s+df`,
|
|
|
|
// APT inspection (read-only)
|
|
`^apt\s+list`,
|
|
`^apt-cache\s`,
|
|
`^dpkg\s+-l`,
|
|
`^dpkg\s+--list`,
|
|
},
|
|
|
|
RequireApproval: []string{
|
|
// Service control
|
|
`^systemctl\s+(restart|stop|start|reload)\s`,
|
|
`^service\s+\S+\s+(restart|stop|start|reload)`,
|
|
|
|
// Docker control
|
|
`^docker\s+(restart|stop|start|kill)\s`,
|
|
`^docker\s+exec\s`,
|
|
`^docker\s+rm\s`,
|
|
|
|
// Process control
|
|
`^kill\s`,
|
|
`^pkill\s`,
|
|
`^killall\s`,
|
|
|
|
// Package management
|
|
`^apt\s`,
|
|
`^apt-get\s`,
|
|
`^yum\s`,
|
|
`^dnf\s`,
|
|
`^pacman\s`,
|
|
|
|
// Proxmox control
|
|
`^pct\s+(start|stop|shutdown|reboot|resize|set)\s`,
|
|
`^qm\s+(start|stop|shutdown|reboot|reset|resize|set)\s`,
|
|
|
|
// journalctl maintenance (modifies logs / persistent state)
|
|
`^journalctl\s+--vacuum`,
|
|
`^journalctl\s+--rotate`,
|
|
|
|
// Temp file cleanup (safe with approval)
|
|
`^rm\s+(-rf?|-fr?)\s+/var/tmp/`,
|
|
`^rm\s+(-rf?|-fr?)\s+/tmp/`,
|
|
},
|
|
|
|
Blocked: []string{
|
|
// Destructive filesystem operations - block root and critical paths
|
|
// Note: /var/tmp and /tmp cleanup is allowed with approval (see RequireApproval)
|
|
`rm\s+-rf\s+/$`, // rm -rf / (root)
|
|
`rm\s+-rf\s+/\*`, // rm -rf /* (root wildcard)
|
|
`rm\s+-rf\s+/home($|\s|/)`, // rm -rf /home
|
|
`rm\s+-rf\s+/etc($|\s|/)`, // rm -rf /etc
|
|
`rm\s+-rf\s+/usr($|\s|/)`, // rm -rf /usr
|
|
`rm\s+-rf\s+/var/lib($|\s|/)`, // rm -rf /var/lib
|
|
`rm\s+-rf\s+/boot($|\s|/)`, // rm -rf /boot
|
|
`rm\s+-rf\s+/root($|\s|/)`, // rm -rf /root (root home)
|
|
`rm\s+-rf\s+/bin($|\s|/)`, // rm -rf /bin
|
|
`rm\s+-rf\s+/sbin($|\s|/)`, // rm -rf /sbin
|
|
`rm\s+-rf\s+/lib($|\s|/)`, // rm -rf /lib
|
|
`rm\s+-rf\s+/opt($|\s|/)`, // rm -rf /opt
|
|
`rm\s+--no-preserve-root`,
|
|
`mkfs`,
|
|
`dd\s+.*of=/dev/`,
|
|
`>\s*/dev/sd`,
|
|
`>\s*/dev/nvme`,
|
|
|
|
|
|
// System destruction
|
|
`shutdown`,
|
|
`reboot`,
|
|
`init\s+0`,
|
|
`poweroff`,
|
|
`halt`,
|
|
|
|
// Dangerous permissions
|
|
`chmod\s+777`,
|
|
`chmod\s+-R\s+777`,
|
|
`chown\s+-R\s+.*:.*\s+/`,
|
|
|
|
// Remote code execution
|
|
`curl.*\|\s*(ba)?sh`,
|
|
`wget.*\|\s*(ba)?sh`,
|
|
`bash\s+-c\s+.*curl`,
|
|
`bash\s+-c\s+.*wget`,
|
|
|
|
// Crypto mining indicators
|
|
`xmrig`,
|
|
`minerd`,
|
|
`cpuminer`,
|
|
|
|
// Fork bomb patterns
|
|
`:\(\)\s*{\s*:\s*\|\s*:`,
|
|
|
|
// Clear system logs
|
|
`>\s*/var/log/`,
|
|
`truncate.*--size.*0.*/var/log/`,
|
|
},
|
|
}
|
|
|
|
p.compile()
|
|
return p
|
|
}
|
|
|
|
func (p *CommandPolicy) compile() {
|
|
p.autoApproveRe = compilePatterns(p.AutoApprove)
|
|
p.requireApprovalRe = compilePatterns(p.RequireApproval)
|
|
p.blockedRe = compilePatterns(p.Blocked)
|
|
}
|
|
|
|
func compilePatterns(patterns []string) []*regexp.Regexp {
|
|
result := make([]*regexp.Regexp, 0, len(patterns))
|
|
for _, pattern := range patterns {
|
|
re, err := regexp.Compile(pattern)
|
|
if err == nil {
|
|
result = append(result, re)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// PolicyDecision represents the policy decision for a command
|
|
type PolicyDecision string
|
|
|
|
const (
|
|
PolicyAllow PolicyDecision = "allow"
|
|
PolicyRequireApproval PolicyDecision = "require_approval"
|
|
PolicyBlock PolicyDecision = "block"
|
|
)
|
|
|
|
// Evaluate checks a command against the policy
|
|
func (p *CommandPolicy) Evaluate(command string) PolicyDecision {
|
|
command = strings.TrimSpace(command)
|
|
// Normalize simple sudo prefix so policy applies consistently.
|
|
// For complex sudo invocations (sudo flags), keep the command as-is (conservative).
|
|
if strings.HasPrefix(command, "sudo ") {
|
|
parts := strings.Fields(command)
|
|
if len(parts) >= 2 && !strings.HasPrefix(parts[1], "-") {
|
|
command = strings.Join(parts[1:], " ")
|
|
}
|
|
}
|
|
|
|
// Check blocked first (highest priority)
|
|
for _, re := range p.blockedRe {
|
|
if re.MatchString(command) {
|
|
return PolicyBlock
|
|
}
|
|
}
|
|
|
|
// Check require approval
|
|
for _, re := range p.requireApprovalRe {
|
|
if re.MatchString(command) {
|
|
return PolicyRequireApproval
|
|
}
|
|
}
|
|
|
|
// Check auto-approve
|
|
for _, re := range p.autoApproveRe {
|
|
if re.MatchString(command) {
|
|
return PolicyAllow
|
|
}
|
|
}
|
|
|
|
// Default: require approval for unknown commands
|
|
return PolicyRequireApproval
|
|
}
|
|
|
|
// IsBlocked returns true if a command is blocked
|
|
func (p *CommandPolicy) IsBlocked(command string) bool {
|
|
return p.Evaluate(command) == PolicyBlock
|
|
}
|
|
|
|
// NeedsApproval returns true if a command needs user approval
|
|
func (p *CommandPolicy) NeedsApproval(command string) bool {
|
|
return p.Evaluate(command) == PolicyRequireApproval
|
|
}
|
|
|
|
// IsAutoApproved returns true if a command can run without approval
|
|
func (p *CommandPolicy) IsAutoApproved(command string) bool {
|
|
return p.Evaluate(command) == PolicyAllow
|
|
}
|