mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 09:23:27 +00:00
- Add DOMPurify sanitization for AI chat markdown rendering (XSS fix) - Configure DOMPurify to add target=_blank and rel=noopener to links - Update system prompt to align with command approval policy - Clarify safe vs destructive commands in prompt - Improve patrol auto-fix mode guidance with safe operation list - Add verification requirements for auto-fix actions - Update observe-only mode to be clearer about read-only restrictions
261 lines
5.9 KiB
Go
261 lines
5.9 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`,
|
|
},
|
|
|
|
Blocked: []string{
|
|
// Destructive filesystem operations
|
|
`rm\s+-rf\s+/`,
|
|
`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
|
|
}
|