rule/flags/flags.go (275 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // Package flags provides parsing of audit rules as specified using CLI flags // in accordance to the man page for auditctl (from the auditd userspace tools). package flags import ( "bytes" "errors" "flag" "fmt" "io" "regexp" "strings" "github.com/kballard/go-shellquote" "github.com/elastic/go-libaudit/v2/rule" ) // Parse parses an audit rule specified using flags. It can parse delete all // commands (-D), file watch rules (-w), and syscall rules (-a or -A). func Parse(s string) (rule.Rule, error) { args, err := shellquote.Split(s) if err != nil { return nil, err } // Parse the flags. ruleFlagSet := newRuleFlagSet() if err := ruleFlagSet.flagSet.Parse(args); err != nil { return nil, err } if err := ruleFlagSet.validate(); err != nil { return nil, err } // Build a struct that is specific to the command type. var r rule.Rule switch ruleFlagSet.Type { case rule.DeleteAllRuleType: r = &rule.DeleteAllRule{ Type: rule.DeleteAllRuleType, Keys: ruleFlagSet.Key, } case rule.FileWatchRuleType: r = &rule.FileWatchRule{ Type: rule.FileWatchRuleType, Path: ruleFlagSet.Path, Permissions: ruleFlagSet.Permissions, Keys: ruleFlagSet.Key, } case rule.AppendSyscallRuleType, rule.PrependSyscallRuleType: syscallRule := &rule.SyscallRule{ Type: ruleFlagSet.Type, Filters: ruleFlagSet.Filters, Syscalls: ruleFlagSet.Syscalls, Keys: ruleFlagSet.Key, } r = syscallRule if ruleFlagSet.Type == rule.AppendSyscallRuleType { syscallRule.List = ruleFlagSet.Append.List syscallRule.Action = ruleFlagSet.Append.Action } else if ruleFlagSet.Type == rule.PrependSyscallRuleType { syscallRule.List = ruleFlagSet.Prepend.List syscallRule.Action = ruleFlagSet.Prepend.Action } default: return nil, fmt.Errorf("unknown rule type: %v", ruleFlagSet.Type) } return r, nil } // --- ruleFlagSet --- // ruleFlagSet is a used to parse the flags used in an audit rule. type ruleFlagSet struct { Type rule.Type DeleteAll bool // [-D] Delete all rules. // Audit Rule Prepend addFlag // -A Prepend rule (list,action) or (action,list). Append addFlag // -a Append rule (list,action) or (action,list). Filters filterList // -F [n=v | n!=v | n<v | n>v | n<=v | n>=v | n&v | n&=v] OR -C [n=v | n!=v] Syscalls stringList // -S Syscall name or number or "all". Value can be comma-separated. // Filepath watch (can be done more expressively using syscalls) Path string // -w Path for filesystem watch (no wildcards). Permissions fileAccessTypeFlags // -p [r|w|x|a] Permission filter. Key stringList // -k Key(s) to associate with the rule. flagSet *flag.FlagSet } func newRuleFlagSet() *ruleFlagSet { rule := &ruleFlagSet{ flagSet: flag.NewFlagSet("rule", flag.ContinueOnError), } rule.flagSet.SetOutput(io.Discard) rule.flagSet.BoolVar(&rule.DeleteAll, "D", false, "delete all") rule.flagSet.Var(&rule.Append, "a", "append rule") rule.flagSet.Var(&rule.Prepend, "A", "prepend rule") rule.flagSet.Var((*interFieldFilterList)(&rule.Filters), "C", "comparison filter") rule.flagSet.Var((*valueFilterList)(&rule.Filters), "F", "filter") rule.flagSet.Var(&rule.Syscalls, "S", "syscall name, number, or 'all'") rule.flagSet.Var(&rule.Permissions, "p", "access type - r=read, w=write, x=execute, a=attribute change") rule.flagSet.StringVar(&rule.Path, "w", "", "path to watch, no wildcards") rule.flagSet.Var(&rule.Key, "k", "key") return rule } func (r *ruleFlagSet) Usage() string { buf := new(bytes.Buffer) r.flagSet.SetOutput(buf) r.flagSet.Usage() r.flagSet.SetOutput(io.Discard) return buf.String() } func (r *ruleFlagSet) validate() error { var ( deleteAll uint8 fileWatch uint8 syscall uint8 ) r.flagSet.Visit(func(f *flag.Flag) { switch f.Name { case "D": deleteAll = 1 case "w", "p": fileWatch = 1 case "a", "A", "C", "F", "S": syscall = 1 } }) // Test for mutual exclusivity. switch deleteAll + fileWatch + syscall { case 0: return errors.New("missing an operation flag (add or delete rule)") case 1: switch { case deleteAll > 0: r.Type = rule.DeleteAllRuleType case fileWatch > 0: r.Type = rule.FileWatchRuleType case syscall > 0: r.Type = rule.AppendSyscallRuleType } default: ops := make([]string, 0, 3) if deleteAll > 0 { ops = append(ops, "delete all [-D]") } if fileWatch > 0 { ops = append(ops, "file watch [-w|-p]") } if syscall > 0 { ops = append(ops, "audit rule [-a|-A|-S|-C|-F]") } return fmt.Errorf("mutually exclusive flags uses together (%v)", strings.Join(ops, " and ")) } if syscall > 0 { var zero addFlag if r.Prepend == zero && r.Append == zero { return errors.New("audit rules must specify either [-A] or [-a]") } if r.Prepend != zero && r.Append != zero { return fmt.Errorf("audit rules cannot specify both [-A] and [-a]") } if r.Prepend != zero { r.Type = rule.PrependSyscallRuleType } } return nil } // --- Specialized flag.Value types for parsing the audit rules. // --- filterList ---- type filterList []rule.FilterSpec func (l filterList) String() string { buf := new(bytes.Buffer) buf.WriteString("[") for i, v := range l { buf.WriteString(v.String()) if i > len(l)-1 { buf.WriteString(", ") } } buf.WriteString("]") return buf.String() } // --- interFieldFilter --- type interFieldFilter rule.FilterSpec var comparisonRegexp = regexp.MustCompile(`(\w+)\s*(!?=)(\w+)`) func (f *interFieldFilter) Set(value string) error { values := comparisonRegexp.FindStringSubmatch(value) if len(values) != 4 { return fmt.Errorf("invalid comparison: '%v'", value) } f.Type = rule.InterFieldFilterType f.LHS = values[1] f.Comparator = values[2] f.RHS = values[3] return nil } // --- valueFilterFlag --- type valueFilter rule.FilterSpec var filterRegexp = regexp.MustCompile(`(\w+)\s*(<=|>=|&=|=|!=|<|>|&)(\S+)`) func (f *valueFilter) Set(value string) error { values := filterRegexp.FindStringSubmatch(value) if len(values) != 4 { return fmt.Errorf("invalid filter: '%v'", value) } f.Type = rule.ValueFilterType f.LHS = values[1] f.Comparator = values[2] f.RHS = values[3] return nil } // --- interFieldFilterList ---- type interFieldFilterList filterList func (l interFieldFilterList) String() string { return filterList(l).String() } func (l *interFieldFilterList) Set(value string) error { comparisonFlag := &interFieldFilter{} if err := comparisonFlag.Set(value); err != nil { return err } *l = append(*l, rule.FilterSpec(*comparisonFlag)) return nil } // --- valueFilterList ---- type valueFilterList filterList func (l valueFilterList) String() string { return filterList(l).String() } func (l *valueFilterList) Set(value string) error { filterFlag := &valueFilter{} if err := filterFlag.Set(value); err != nil { return err } *l = append(*l, rule.FilterSpec(*filterFlag)) return nil } // --- stringList --- // StringList is a flag type for usage when the parameter has an arity > 1. type stringList []string func (l *stringList) String() string { return "[" + strings.Join(*l, ", ") + "]" } func (l *stringList) Set(value string) error { words := strings.Split(value, ",") for _, w := range words { *l = append(*l, strings.TrimSpace(w)) } return nil } // --- addFlag --- // addFlag is a flag type for appending or prepending a rule. type addFlag struct { List string Action string } func (f *addFlag) Set(value string) error { parts := strings.Split(value, ",") if len(parts) > 2 { return fmt.Errorf("expected a list type and action but got '%v'", value) } for _, part := range parts { part = strings.TrimSpace(part) switch part { case "task", "exit", "user", "exclude": f.List = part case "never", "always": f.Action = part default: return fmt.Errorf("invalid list type or action: '%v'", part) } } if f.List == "" { return errors.New("missing list type") } if f.Action == "" { return errors.New("missing action") } return nil } func (f *addFlag) String() string { return fmt.Sprintf("%v,%v", f.List, f.Action) } // --- fileAccessTypeFlags --- type fileAccessTypeFlags []rule.AccessType func (f *fileAccessTypeFlags) Set(value string) error { for _, v := range []byte(value) { switch v { case 'r': *f = append(*f, rule.ReadAccessType) case 'w': *f = append(*f, rule.WriteAccessType) case 'x': *f = append(*f, rule.ExecuteAccessType) case 'a': *f = append(*f, rule.AttributeChangeAccessType) default: return fmt.Errorf("invalid file access type: '%v'", string(v)) } } return nil } func (f fileAccessTypeFlags) String() string { flags := make([]string, 0, len(f)) for _, accessType := range f { flags = append(flags, accessType.String()) } return "[" + strings.Join(flags, "|") + "]" }