feat(aa): rewrite the toAccess function to parse, convert and verify the access values.

This commit is contained in:
Alexandre Pujol 2024-05-25 22:14:43 +01:00
parent 05de39d92a
commit 656aa15836
Failed to generate hash of commit
10 changed files with 134 additions and 53 deletions

View file

@ -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"])),
}
}

View file

@ -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 {

View file

@ -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"],
}
}

View file

@ -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"]}

View file

@ -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"],

View file

@ -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"],
}
}

View file

@ -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
}

View file

@ -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"],
}
}

View file

@ -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)
}
}

View file

@ -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"],