// Package elevate implements the elevation broker for running commands // outside the Landlock sandbox with human-in-the-loop approval. package elevate import ( "os" "path/filepath" "sync" "gopkg.in/yaml.v3" ) // Rule represents a single auto-approve rule. type Rule struct { Cmd string `yaml:"cmd,omitempty"` // command pattern (glob) SSH string `yaml:"ssh,omitempty"` // SSH key fingerprint } // Match returns true if the rule matches the given command or fingerprint. func (r Rule) Match(cmd, fingerprint string) bool { if r.Cmd != "" { matched, _ := filepath.Match(r.Cmd, cmd) if matched { return true } // Also try exact match (glob patterns may not cover all cases) if r.Cmd == cmd { return true } } if r.SSH != "" && r.SSH == fingerprint { return true } return false } // Policy is a set of auto-approve rules. type Policy struct { Rules []Rule `yaml:"rules"` } // Matches returns true if any rule matches. func (p *Policy) Matches(cmd, fingerprint string) bool { for _, r := range p.Rules { if r.Match(cmd, fingerprint) { return true } } return false } // Add appends a rule and returns true if it was new. func (p *Policy) Add(r Rule) bool { for _, existing := range p.Rules { if existing == r { return false } } p.Rules = append(p.Rules, r) return true } // Marshal returns the YAML representation. func (p *Policy) Marshal() ([]byte, error) { return yaml.Marshal(p) } // ParsePolicy parses YAML data into a Policy. func ParsePolicy(data []byte, p *Policy) error { return yaml.Unmarshal(data, p) } // PolicyStore manages a disk-backed global policy and in-memory session policies. type PolicyStore struct { mu sync.RWMutex global Policy path string // disk path for global policy } // NewPolicyStore loads (or creates) the global policy from disk. func NewPolicyStore(path string) *PolicyStore { ps := &PolicyStore{path: path} data, err := os.ReadFile(path) if err == nil { yaml.Unmarshal(data, &ps.global) //nolint:errcheck } return ps } // Global returns a copy of the global policy. func (ps *PolicyStore) Global() Policy { ps.mu.RLock() defer ps.mu.RUnlock() cp := Policy{Rules: make([]Rule, len(ps.global.Rules))} copy(cp.Rules, ps.global.Rules) return cp } // SetGlobal replaces the global policy and saves to disk. func (ps *PolicyStore) SetGlobal(p Policy) error { ps.mu.Lock() defer ps.mu.Unlock() ps.global = p return ps.save() } // AddGlobal adds a rule to the global policy and saves. func (ps *PolicyStore) AddGlobal(r Rule) error { ps.mu.Lock() defer ps.mu.Unlock() ps.global.Add(r) return ps.save() } // MatchesGlobal checks if the global policy matches. func (ps *PolicyStore) MatchesGlobal(cmd, fingerprint string) bool { ps.mu.RLock() defer ps.mu.RUnlock() return ps.global.Matches(cmd, fingerprint) } func (ps *PolicyStore) save() error { data, err := yaml.Marshal(&ps.global) if err != nil { return err } dir := filepath.Dir(ps.path) os.MkdirAll(dir, 0700) //nolint:errcheck return os.WriteFile(ps.path, data, 0600) }