diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 5fe214102..cedaef985 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -31,13 +31,11 @@ type Capability struct { Names []string } -} - func newCapabilityFromLog(log map[string]string) Rule { return &Capability{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Names: []string{log["capname"]}, + Names: Must(toValues(tokCAPABILITY, "name", log["capname"])), } } diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 84450afec..1f2190fda 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "slices" "strings" ) @@ -26,6 +27,23 @@ func init() { } } +// cmpFileAccess compares two access strings for file rules. +// It is aimed to be used in slices.SortFunc. +func cmpFileAccess(i, j string) int { + if slices.Contains(requirements[tokFILE]["access"], i) && + slices.Contains(requirements[tokFILE]["access"], j) { + return requirementsWeights[tokFILE]["access"][i] - requirementsWeights[tokFILE]["access"][j] + } + if slices.Contains(requirements[tokFILE]["transition"], i) && + slices.Contains(requirements[tokFILE]["transition"], j) { + return requirementsWeights[tokFILE]["transition"][i] - requirementsWeights[tokFILE]["transition"][j] + } + if slices.Contains(requirements[tokFILE]["access"], i) { + return -1 + } + return 1 +} + type File struct { RuleBase Qualifier @@ -36,12 +54,19 @@ type File struct { } func newFileFromLog(log map[string]string) Rule { + accesses, err := toAccess("file-log", log["requested_mask"]) + if err != nil { + panic(fmt.Errorf("newFileFromLog(%v): %w", log, err)) + } + if slices.Compare(accesses, []string{"l"}) == 0 { + return newLinkFromLog(log) + } return &File{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Owner: isOwner(log), Path: log["name"], - Access: toAccess("file-log", log["requested_mask"]), + Access: accesses, Target: log["target"], } } @@ -104,6 +129,7 @@ func newLinkFromLog(log map[string]string) Rule { Target: log["target"], } } + func (r *Link) Less(other any) bool { o, _ := other.(*Link) if r.Path != o.Path { diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index effc898d5..ce1a8ae67 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -25,7 +25,7 @@ func newIOUringFromLog(log map[string]string) Rule { return &IOUring{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(tokIOURING, log["requested"]), + Access: Must(toAccess(tokIOURING, log["requested"])), Label: log["label"], } } diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 7c3f3866b..88f00637a 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -6,7 +6,6 @@ package aa import ( "slices" - "strings" ) const ( @@ -37,7 +36,7 @@ func newMountConditionsFromLog(log map[string]string) MountConditions { if _, present := log["flags"]; present { return MountConditions{ FsType: log["fstype"], - Options: strings.Split(log["flags"], ", "), + Options: Must(toValues(tokMOUNT, "flags", log["flags"])), } } return MountConditions{FsType: log["fstype"]} diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 08bdc4d0b..52cf7c30d 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -40,7 +40,7 @@ func newMqueueFromLog(log map[string]string) Rule { return &Mqueue{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(tokMQUEUE, log["requested"]), + Access: Must(toAccess(tokMQUEUE, log["requested"])), Type: mqueueType, Label: log["label"], Name: log["name"], diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index 85106b884..1b920a26e 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -4,7 +4,9 @@ package aa -import "slices" +import ( + "slices" +) const tokPTRACE = "ptrace" @@ -27,7 +29,7 @@ func newPtraceFromLog(log map[string]string) Rule { return &Ptrace{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(tokPTRACE, log["requested_mask"]), + Access: Must(toAccess(tokPTRACE, log["requested_mask"])), Peer: log["peer"], } } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 26d90ba35..c42ca020f 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -4,6 +4,12 @@ package aa +import ( + "fmt" + "slices" + "strings" +) + const ( tokALLOW = "allow" tokAUDIT = "audit" @@ -55,3 +61,85 @@ func (r Rules) GetVariables() []*Variable { } return res } + +// Must is a helper that wraps a call to a function returning (any, error) and +// panics if the error is non-nil. +func Must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +// Helper function to convert a string to a slice of rule values according to +// the rule requirements as defined in the requirements map. +func toValues(rule string, key string, input string) ([]string, error) { + var sep string + req, ok := requirements[rule][key] + if !ok { + return nil, fmt.Errorf("unrecognized requirement '%s' for rule %s", key, rule) + } + + switch { + case strings.Contains(input, ","): + sep = "," + case strings.Contains(input, " "): + sep = " " + } + res := strings.Split(input, sep) + for _, access := range res { + if !slices.Contains(req, access) { + return nil, fmt.Errorf("unrecognized %s: %s", key, access) + } + } + slices.SortFunc(res, func(i, j string) int { + return requirementsWeights[rule][key][i] - requirementsWeights[rule][key][j] + }) + return slices.Compact(res), nil +} + +// Helper function to convert an access string to a slice of access according to +// the rule requirements as defined in the requirements map. +func toAccess(rule string, input string) ([]string, error) { + var res []string + + switch rule { + case tokFILE: + raw := strings.Split(input, "") + trans := []string{} + for _, access := range raw { + if slices.Contains(requirements[tokFILE]["access"], access) { + res = append(res, access) + } else { + trans = append(trans, access) + } + } + + transition := strings.Join(trans, "") + if len(transition) > 0 { + if slices.Contains(requirements[tokFILE]["transition"], transition) { + res = append(res, transition) + } else { + return nil, fmt.Errorf("unrecognized transition: %s", transition) + } + } + + case tokFILE + "-log": + raw := strings.Split(input, "") + for _, access := range raw { + if slices.Contains(requirements[tokFILE]["access"], access) { + res = append(res, access) + } else if maskToAccess[access] != "" { + res = append(res, maskToAccess[access]) + } else { + return nil, fmt.Errorf("toAccess: unrecognized file access '%s'", input) + } + } + + default: + return toValues(rule, "access", input) + } + + slices.SortFunc(res, cmpFileAccess) + return slices.Compact(res), nil +} diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index c1dc6803c..33265a6d0 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -4,7 +4,9 @@ package aa -import "slices" +import ( + "slices" +) const tokSIGNAL = "signal" @@ -41,8 +43,8 @@ func newSignalFromLog(log map[string]string) Rule { return &Signal{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(tokSIGNAL, log["requested_mask"]), - Set: toAccess(tokSIGNAL, log["signal"]), + Access: Must(toAccess(tokSIGNAL, log["requested_mask"])), + Set: []string{log["signal"]}, Peer: log["peer"], } } diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 0d81a7294..440be9da3 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -45,9 +45,11 @@ var ( // convert apparmor requested mask to apparmor access mode maskToAccess = map[string]string{ - "a": "w", - "c": "w", - "d": "w", + "a": "w", + "c": "w", + "d": "w", + "wc": "w", + "x": "ix", } // The order the apparmor rules should be sorted @@ -230,39 +232,3 @@ func getLetterIn(alphabet []string, in string) string { } return "" } - -// Helper function to convert a access string to slice of access -func toAccess(constraint string, input string) []string { - var res []string - - switch constraint { - case "file", "file-log": - raw := strings.Split(input, "") - trans := []string{} - for _, access := range raw { - if slices.Contains(fileAccess, access) { - res = append(res, access) - } else if maskToAccess[access] != "" { - res = append(res, maskToAccess[access]) - trans = append(trans, access) - } - } - - if constraint != "file-log" { - transition := strings.Join(trans, "") - if len(transition) > 0 { - if slices.Contains(fileExecTransition, transition) { - res = append(res, transition) - } else { - panic("unrecognized pattern: " + transition) - } - } - } - return res - - default: - res = strings.Fields(input) - slices.Sort(res) - return slices.Compact(res) - } -} diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index 3207f571e..88c37d198 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -36,7 +36,7 @@ func newUnixFromLog(log map[string]string) Rule { return &Unix{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(tokUNIX, log["requested_mask"]), + Access: Must(toAccess(tokUNIX, log["requested_mask"])), Type: log["sock_type"], Protocol: log["protocol"], Address: log["addr"],