From ea1736083acfc2f6ff22e535f0d1fb1e9e9b4caa Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Fri, 12 Apr 2024 20:07:05 +0100 Subject: [PATCH 01/62] chore: use slices from standard library. --- cmd/aa-log/main.go | 2 +- pkg/aa/mount.go | 4 ++-- pkg/aa/profile.go | 2 +- pkg/aa/variables.go | 3 +-- pkg/logs/logs.go | 2 +- pkg/prebuild/builder/complain.go | 2 +- pkg/prebuild/builder/core_test.go | 3 +-- pkg/prebuild/builder/enforce.go | 2 +- pkg/prebuild/cfg/os.go | 2 +- pkg/prebuild/directive/exec.go | 2 +- pkg/prebuild/directive/filter.go | 2 +- pkg/prebuild/prepare/core_test.go | 2 +- 12 files changed, 13 insertions(+), 15 deletions(-) diff --git a/cmd/aa-log/main.go b/cmd/aa-log/main.go index 89ca1d0b..cbc59c9e 100644 --- a/cmd/aa-log/main.go +++ b/cmd/aa-log/main.go @@ -9,10 +9,10 @@ import ( "fmt" "io" "os" + "slices" "strings" "github.com/roddhjav/apparmor.d/pkg/logs" - "golang.org/x/exp/slices" ) const usage = `aa-log [-h] [--systemd] [--file file] [--rules | --raw] [profile] diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 0dac82ff..35875c4a 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -5,9 +5,9 @@ package aa import ( + "slices" "strings" - - "golang.org/x/exp/slices" +) ) type MountConditions struct { diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index b5aa2d7b..2e97b9d4 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -7,11 +7,11 @@ package aa import ( "bytes" "reflect" + "slices" "sort" "strings" "github.com/arduino/go-paths-helper" - "golang.org/x/exp/slices" ) // Default Apparmor magic directory: /etc/apparmor.d/. diff --git a/pkg/aa/variables.go b/pkg/aa/variables.go index 6105cc30..ddd2e3d1 100644 --- a/pkg/aa/variables.go +++ b/pkg/aa/variables.go @@ -9,9 +9,8 @@ package aa import ( "regexp" + "slices" "strings" - - "golang.org/x/exp/slices" ) var ( diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go index 28c14ffb..913f1172 100644 --- a/pkg/logs/logs.go +++ b/pkg/logs/logs.go @@ -7,11 +7,11 @@ package logs import ( "io" "regexp" + "slices" "strings" "github.com/roddhjav/apparmor.d/pkg/aa" "github.com/roddhjav/apparmor.d/pkg/util" - "golang.org/x/exp/slices" ) // Colors diff --git a/pkg/prebuild/builder/complain.go b/pkg/prebuild/builder/complain.go index 3dc04166..3970e6df 100644 --- a/pkg/prebuild/builder/complain.go +++ b/pkg/prebuild/builder/complain.go @@ -6,10 +6,10 @@ package builder import ( "regexp" + "slices" "strings" "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" - "golang.org/x/exp/slices" ) var ( diff --git a/pkg/prebuild/builder/core_test.go b/pkg/prebuild/builder/core_test.go index 3b620e6b..b0c59e77 100644 --- a/pkg/prebuild/builder/core_test.go +++ b/pkg/prebuild/builder/core_test.go @@ -5,9 +5,8 @@ package builder import ( + "slices" "testing" - - "golang.org/x/exp/slices" ) func TestBuilder_Apply(t *testing.T) { diff --git a/pkg/prebuild/builder/enforce.go b/pkg/prebuild/builder/enforce.go index 8c4aaccf..a3bd2c1d 100644 --- a/pkg/prebuild/builder/enforce.go +++ b/pkg/prebuild/builder/enforce.go @@ -5,10 +5,10 @@ package builder import ( + "slices" "strings" "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" - "golang.org/x/exp/slices" ) type Enforce struct { diff --git a/pkg/prebuild/cfg/os.go b/pkg/prebuild/cfg/os.go index aa995200..d792c490 100644 --- a/pkg/prebuild/cfg/os.go +++ b/pkg/prebuild/cfg/os.go @@ -6,10 +6,10 @@ package cfg import ( "os" + "slices" "strings" "github.com/arduino/go-paths-helper" - "golang.org/x/exp/slices" ) var ( diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index e0b1e2e1..db08ba6e 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -5,12 +5,12 @@ package directive import ( + "slices" "strings" "github.com/roddhjav/apparmor.d/pkg/aa" "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" "github.com/roddhjav/apparmor.d/pkg/util" - "golang.org/x/exp/slices" ) type Exec struct { diff --git a/pkg/prebuild/directive/filter.go b/pkg/prebuild/directive/filter.go index 8b5502ca..b4cc54af 100644 --- a/pkg/prebuild/directive/filter.go +++ b/pkg/prebuild/directive/filter.go @@ -6,10 +6,10 @@ package directive import ( "regexp" + "slices" "strings" "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" - "golang.org/x/exp/slices" ) type FilterOnly struct { diff --git a/pkg/prebuild/prepare/core_test.go b/pkg/prebuild/prepare/core_test.go index 0e8f913d..591bf838 100644 --- a/pkg/prebuild/prepare/core_test.go +++ b/pkg/prebuild/prepare/core_test.go @@ -7,11 +7,11 @@ package prepare import ( "os" "os/exec" + "slices" "testing" "github.com/arduino/go-paths-helper" "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" - "golang.org/x/exp/slices" ) func chdirGitRoot() { From ab4feda5baa4d7de82f9ca65dd4df30ca65e309a Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 14 Apr 2024 23:58:34 +0100 Subject: [PATCH 02/62] feat(aa): improve apparmor struct. --- pkg/aa/capability.go | 12 ++-- pkg/aa/change_profile.go | 22 +++--- pkg/aa/data_test.go | 33 ++++----- pkg/aa/dbus.go | 65 ++++++++++------- pkg/aa/file.go | 44 ++++++++---- pkg/aa/include.go | 28 -------- pkg/aa/io_uring.go | 18 +++-- pkg/aa/mount.go | 62 ++++++++-------- pkg/aa/mqueue.go | 27 ++++--- pkg/aa/network.go | 65 +++++++++-------- pkg/aa/pivot_root.go | 20 +++--- pkg/aa/preamble.go | 87 ++++++++++++++++++++++ pkg/aa/profile.go | 50 ++++++------- pkg/aa/profile_test.go | 30 ++++---- pkg/aa/ptrace.go | 14 ++-- pkg/aa/rlimit.go | 19 +++-- pkg/aa/rules.go | 108 +++++++++------------------- pkg/aa/rules_test.go | 128 +++++++++++++++++++-------------- pkg/aa/signal.go | 20 +++--- pkg/aa/templates/include.j2 | 1 + pkg/aa/templates/profile.j2 | 32 ++++++--- pkg/aa/templates/qualifier.j2 | 3 - pkg/aa/unix.go | 81 +++++++++++---------- pkg/aa/userns.go | 8 ++- pkg/aa/variables.go | 35 +++------ pkg/aa/variables_test.go | 88 +++++++++++++---------- pkg/logs/logs_test.go | 20 +++--- pkg/prebuild/directive/dbus.go | 14 ++-- 28 files changed, 638 insertions(+), 496 deletions(-) delete mode 100644 pkg/aa/include.go create mode 100644 pkg/aa/preamble.go diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index b65e8bd2..292e3814 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -5,23 +5,25 @@ package aa type Capability struct { + Rule Qualifier Name string } -func CapabilityFromLog(log map[string]string) ApparmorRule { +func newCapabilityFromLog(log map[string]string) *Capability { return &Capability{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), Name: log["capname"], } } func (r *Capability) Less(other any) bool { o, _ := other.(*Capability) - if r.Name == o.Name { - return r.Qualifier.Less(o.Qualifier) + if r.Name != o.Name { + return r.Name < o.Name } - return r.Name < o.Name + return r.Qualifier.Less(o.Qualifier) } func (r *Capability) Equals(other any) bool { diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index 610376eb..eeb5f973 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -5,15 +5,17 @@ package aa type ChangeProfile struct { + Rule Qualifier ExecMode string Exec string ProfileName string } -func ChangeProfileFromLog(log map[string]string) ApparmorRule { +func newChangeProfileFromLog(log map[string]string) *ChangeProfile { return &ChangeProfile{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), ExecMode: log["mode"], Exec: log["exec"], ProfileName: log["target"], @@ -22,16 +24,20 @@ func ChangeProfileFromLog(log map[string]string) ApparmorRule { func (r *ChangeProfile) Less(other any) bool { o, _ := other.(*ChangeProfile) - if r.ExecMode == o.ExecMode { - if r.Exec == o.Exec { - return r.ProfileName < o.ProfileName - } + if r.ExecMode != o.ExecMode { + return r.ExecMode < o.ExecMode + } + if r.Exec != o.Exec { return r.Exec < o.Exec } - return r.ExecMode < o.ExecMode + if r.ProfileName != o.ProfileName { + return r.ProfileName < o.ProfileName + } + return r.Qualifier.Less(o.Qualifier) } func (r *ChangeProfile) Equals(other any) bool { o, _ := other.(*ChangeProfile) - return r.ExecMode == o.ExecMode && r.Exec == o.Exec && r.ProfileName == o.ProfileName + return r.ExecMode == o.ExecMode && r.Exec == o.Exec && + r.ProfileName == o.ProfileName && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go index b3d01a70..63aef6bb 100644 --- a/pkg/aa/data_test.go +++ b/pkg/aa/data_test.go @@ -71,13 +71,13 @@ var ( "flags": "rw, rbind", } mount1 = &Mount{ - Qualifier: Qualifier{Comment: "failed perms check"}, + Rule: Rule{Comment: "failed perms check"}, MountConditions: MountConditions{FsType: "overlay"}, Source: "overlay", MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/", } mount2 = &Mount{ - Qualifier: Qualifier{Comment: "failed perms check"}, + Rule: Rule{Comment: "failed perms check"}, MountConditions: MountConditions{Options: []string{"rw", "rbind"}}, Source: "/oldroot/dev/tty", MountPoint: "/newroot/dev/tty", @@ -197,17 +197,17 @@ var ( "protocol": "0", } unix1 = &Unix{ - Access: "send receive", - Type: "stream", - Protocol: "0", - Address: "none", - Peer: "dbus-daemon", - PeerAddr: "@/tmp/dbus-AaKMpxzC4k", + Access: "send receive", + Type: "stream", + Protocol: "0", + Address: "none", + PeerAddr: "@/tmp/dbus-AaKMpxzC4k", + PeerLabel: "dbus-daemon", } unix2 = &Unix{ - Qualifier: Qualifier{FileInherit: true}, - Access: "receive", - Type: "stream", + Rule: Rule{FileInherit: true}, + Access: "receive", + Type: "stream", } // Dbus @@ -236,11 +236,11 @@ var ( dbus1 = &Dbus{ Access: "receive", Bus: "session", - Name: ":1.15", Path: "/org/gtk/vfs/metadata", Interface: "org.gtk.vfs.Metadata", Member: "Remove", - Label: "tracker-extract", + PeerName: ":1.15", + PeerLabel: "tracker-extract", } dbus2 = &Dbus{ Access: "bind", @@ -285,8 +285,9 @@ var ( } file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"} file2 = &File{ - Qualifier: Qualifier{Owner: true, NoNewPrivs: true}, - Path: "@{PROC}/4163/cgroup", - Access: "r", + Rule: Rule{NoNewPrivs: true}, + Owner: true, + Path: "@{PROC}/4163/cgroup", + Access: "r", } ) diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index b2e32ba2..1c43df88 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -5,6 +5,7 @@ package aa type Dbus struct { + Rule Qualifier Access string Bus string @@ -12,45 +13,58 @@ type Dbus struct { Path string Interface string Member string - Label string + PeerName string + PeerLabel string } -func DbusFromLog(log map[string]string) ApparmorRule { +func newDbusFromLog(log map[string]string) *Dbus { + name := "" + peerName := "" + if log["mask"] == "bind" { + name = log["name"] + } else { + peerName = log["name"] + } return &Dbus{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), Access: log["mask"], Bus: log["bus"], - Name: log["name"], + Name: name, Path: log["path"], Interface: log["interface"], Member: log["member"], - Label: log["peer_label"], + PeerName: peerName, + PeerLabel: log["peer_label"], } } func (r *Dbus) Less(other any) bool { o, _ := other.(*Dbus) - if r.Qualifier.Equals(o.Qualifier) { - if r.Access == o.Access { - if r.Bus == o.Bus { - if r.Name == o.Name { - if r.Path == o.Path { - if r.Interface == o.Interface { - if r.Member == o.Member { - return r.Label < o.Label - } - return r.Member < o.Member - } - return r.Interface < o.Interface - } - return r.Path < o.Path - } - return r.Name < o.Name - } - return r.Bus < o.Bus - } + if r.Access != o.Access { return r.Access < o.Access } + if r.Bus != o.Bus { + return r.Bus < o.Bus + } + if r.Name != o.Name { + return r.Name < o.Name + } + if r.Path != o.Path { + return r.Path < o.Path + } + if r.Interface != o.Interface { + return r.Interface < o.Interface + } + if r.Member != o.Member { + return r.Member < o.Member + } + if r.PeerName != o.PeerName { + return r.PeerName < o.PeerName + } + if r.PeerLabel != o.PeerLabel { + return r.PeerLabel < o.PeerLabel + } return r.Qualifier.Less(o.Qualifier) } @@ -58,5 +72,6 @@ func (r *Dbus) Equals(other any) bool { o, _ := other.(*Dbus) return r.Access == o.Access && r.Bus == o.Bus && r.Name == o.Name && r.Path == o.Path && r.Interface == o.Interface && - r.Member == o.Member && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) + r.Member == o.Member && r.PeerName == o.PeerName && + r.PeerLabel == o.PeerLabel && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/file.go b/pkg/aa/file.go index c83322e8..ec16e54c 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -5,15 +5,26 @@ package aa type File struct { + Rule Qualifier + Owner bool Path string Access string Target string } -func FileFromLog(log map[string]string) ApparmorRule { +func newFileFromLog(log map[string]string) *File { + owner := false + fsuid, hasFsUID := log["fsuid"] + ouid, hasOuUID := log["ouid"] + isDbus := strings.Contains(log["operation"], "dbus") + if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus { + owner = true + } return &File{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), + Owner: owner, Path: log["name"], Access: toAccess(log["requested_mask"]), Target: log["target"], @@ -24,23 +35,26 @@ func (r *File) Less(other any) bool { o, _ := other.(*File) letterR := getLetterIn(fileAlphabet, r.Path) letterO := getLetterIn(fileAlphabet, o.Path) - if fileWeights[letterR] == fileWeights[letterO] || letterR == "" || letterO == "" { - if r.Qualifier.Equals(o.Qualifier) { - if r.Path == o.Path { - if r.Access == o.Access { - return r.Target < o.Target - } - return r.Access < o.Access - } - return r.Path < o.Path - } - return r.Qualifier.Less(o.Qualifier) + if fileWeights[letterR] != fileWeights[letterO] && letterR != "" && letterO != "" { + return fileWeights[letterR] < fileWeights[letterO] } - return fileWeights[letterR] < fileWeights[letterO] + if r.Path != o.Path { + return r.Path < o.Path + } + if r.Access != o.Access { + return r.Access < o.Access + } + if r.Target != o.Target { + return r.Target < o.Target + } + if o.Owner != r.Owner { + return r.Owner + } + return r.Qualifier.Less(o.Qualifier) } func (r *File) Equals(other any) bool { o, _ := other.(*File) - return r.Path == o.Path && r.Access == o.Access && + return r.Path == o.Path && r.Access == o.Access && r.Owner == o.Owner && r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/include.go b/pkg/aa/include.go deleted file mode 100644 index b3aec7a9..00000000 --- a/pkg/aa/include.go +++ /dev/null @@ -1,28 +0,0 @@ -// apparmor.d - Full set of apparmor profiles -// Copyright (C) 2021-2024 Alexandre Pujol -// SPDX-License-Identifier: GPL-2.0-only - -package aa - -type Include struct { - IfExists bool - Path string - IsMagic bool -} - -func (r *Include) Less(other any) bool { - o, _ := other.(*Include) - if r.Path == o.Path { - if r.IsMagic == o.IsMagic { - return r.IfExists - } - return r.IsMagic - } - return r.Path < o.Path -} - -func (r *Include) Equals(other any) bool { - o, _ := other.(*Include) - return r.Path == o.Path && r.IsMagic == o.IsMagic && - r.IfExists == o.IfExists -} diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index 62507bf7..08370564 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -5,19 +5,29 @@ package aa type IOUring struct { + Rule Qualifier Access string Label string } +func newIOUringFromLog(log map[string]string) *IOUring { + return &IOUring{ + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), + Access: toAccess(log["requested"]), + Label: log["label"], + } +} + func (r *IOUring) Less(other any) bool { o, _ := other.(*IOUring) - if r.Qualifier.Equals(o.Qualifier) { - if r.Access == o.Access { - return r.Label < o.Label - } + if r.Access != o.Access { return r.Access < o.Access } + if r.Label != o.Label { + return r.Label < o.Label + } return r.Qualifier.Less(o.Qualifier) } diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 35875c4a..81938097 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -15,7 +15,7 @@ type MountConditions struct { Options []string } -func MountConditionsFromLog(log map[string]string) MountConditions { +func newMountConditionsFromLog(log map[string]string) MountConditions { if _, present := log["flags"]; present { return MountConditions{ FsType: log["fstype"], @@ -26,10 +26,10 @@ func MountConditionsFromLog(log map[string]string) MountConditions { } func (m MountConditions) Less(other MountConditions) bool { - if m.FsType == other.FsType { - return len(m.Options) < len(other.Options) + if m.FsType != other.FsType { + return m.FsType < other.FsType } - return m.FsType < other.FsType + return len(m.Options) < len(other.Options) } func (m MountConditions) Equals(other MountConditions) bool { @@ -37,16 +37,18 @@ func (m MountConditions) Equals(other MountConditions) bool { } type Mount struct { + Rule Qualifier MountConditions Source string MountPoint string } -func MountFromLog(log map[string]string) ApparmorRule { +func newMountFromLog(log map[string]string) *Mount { return &Mount{ - Qualifier: NewQualifierFromLog(log), - MountConditions: MountConditionsFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), + MountConditions: newMountConditionsFromLog(log), Source: log["srcname"], MountPoint: log["name"], } @@ -54,15 +56,15 @@ func MountFromLog(log map[string]string) ApparmorRule { func (r *Mount) Less(other any) bool { o, _ := other.(*Mount) - if r.Qualifier.Equals(o.Qualifier) { - if r.Source == o.Source { - if r.MountPoint == o.MountPoint { - return r.MountConditions.Less(o.MountConditions) - } - return r.MountPoint < o.MountPoint - } + if r.Source != o.Source { return r.Source < o.Source } + if r.MountPoint != o.MountPoint { + return r.MountPoint < o.MountPoint + } + if r.MountConditions.Equals(o.MountConditions) { + return r.MountConditions.Less(o.MountConditions) + } return r.Qualifier.Less(o.Qualifier) } @@ -74,27 +76,29 @@ func (r *Mount) Equals(other any) bool { } type Umount struct { + Rule Qualifier MountConditions MountPoint string } -func UmountFromLog(log map[string]string) ApparmorRule { +func newUmountFromLog(log map[string]string) *Umount { return &Umount{ - Qualifier: NewQualifierFromLog(log), - MountConditions: MountConditionsFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), + MountConditions: newMountConditionsFromLog(log), MountPoint: log["name"], } } func (r *Umount) Less(other any) bool { o, _ := other.(*Umount) - if r.Qualifier.Equals(o.Qualifier) { - if r.MountPoint == o.MountPoint { - return r.MountConditions.Less(o.MountConditions) - } + if r.MountPoint != o.MountPoint { return r.MountPoint < o.MountPoint } + if r.MountConditions.Equals(o.MountConditions) { + return r.MountConditions.Less(o.MountConditions) + } return r.Qualifier.Less(o.Qualifier) } @@ -106,27 +110,29 @@ func (r *Umount) Equals(other any) bool { } type Remount struct { + Rule Qualifier MountConditions MountPoint string } -func RemountFromLog(log map[string]string) ApparmorRule { +func newRemountFromLog(log map[string]string) *Remount { return &Remount{ - Qualifier: NewQualifierFromLog(log), - MountConditions: MountConditionsFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), + MountConditions: newMountConditionsFromLog(log), MountPoint: log["name"], } } func (r *Remount) Less(other any) bool { o, _ := other.(*Remount) - if r.Qualifier.Equals(o.Qualifier) { - if r.MountPoint == o.MountPoint { - return r.MountConditions.Less(o.MountConditions) - } + if r.MountPoint != o.MountPoint { return r.MountPoint < o.MountPoint } + if r.MountConditions.Equals(o.MountConditions) { + return r.MountConditions.Less(o.MountConditions) + } return r.Qualifier.Less(o.Qualifier) } diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 03a52bdf..6afba37f 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -4,9 +4,12 @@ package aa -import "strings" +import ( + "strings" +) type Mqueue struct { + Rule Qualifier Access string Type string @@ -14,7 +17,7 @@ type Mqueue struct { Name string } -func MqueueFromLog(log map[string]string) ApparmorRule { +func newMqueueFromLog(log map[string]string) *Mqueue { mqueueType := "posix" if strings.Contains(log["class"], "posix") { mqueueType = "posix" @@ -22,7 +25,8 @@ func MqueueFromLog(log map[string]string) ApparmorRule { mqueueType = "sysv" } return &Mqueue{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested"]), Type: mqueueType, Label: log["label"], @@ -32,19 +36,20 @@ func MqueueFromLog(log map[string]string) ApparmorRule { func (r *Mqueue) Less(other any) bool { o, _ := other.(*Mqueue) - if r.Qualifier.Equals(o.Qualifier) { - if r.Access == o.Access { - if r.Type == o.Type { - return r.Label < o.Label - } - return r.Type < o.Type - } + if r.Access != o.Access { return r.Access < o.Access } + if r.Type != o.Type { + return r.Type < o.Type + } + if r.Label != o.Label { + return r.Label < o.Label + } return r.Qualifier.Less(o.Qualifier) } func (r *Mqueue) Equals(other any) bool { o, _ := other.(*Mqueue) - return r.Access == o.Access && r.Type == o.Type && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) + return r.Access == o.Access && r.Type == o.Type && r.Label == o.Label && + r.Name == o.Name && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/network.go b/pkg/aa/network.go index 74b0c763..b23bdf71 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -10,54 +10,63 @@ type AddressExpr struct { Port string } +func newAddressExprFromLog(log map[string]string) AddressExpr { + return AddressExpr{ + Source: log["laddr"], + Destination: log["faddr"], + Port: log["lport"], + } +} + +func (r AddressExpr) Less(other AddressExpr) bool { + if r.Source != other.Source { + return r.Source < other.Source + } + if r.Destination != other.Destination { + return r.Destination < other.Destination + } + return r.Port < other.Port +} + func (r AddressExpr) Equals(other AddressExpr) bool { return r.Source == other.Source && r.Destination == other.Destination && r.Port == other.Port } -func (r AddressExpr) Less(other AddressExpr) bool { - if r.Source == other.Source { - if r.Destination == other.Destination { - return r.Port < other.Port - } - return r.Destination < other.Destination - } - return r.Source < other.Source -} - type Network struct { + Rule Qualifier + AddressExpr Domain string Type string Protocol string - AddressExpr } -func NetworkFromLog(log map[string]string) ApparmorRule { +func newNetworkFromLog(log map[string]string) *Network { return &Network{ - Qualifier: NewQualifierFromLog(log), - AddressExpr: AddressExpr{ - Source: log["laddr"], - Destination: log["faddr"], - Port: log["lport"], - }, - Domain: log["family"], - Type: log["sock_type"], - Protocol: log["protocol"], + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), + AddressExpr: newAddressExprFromLog(log), + Domain: log["family"], + Type: log["sock_type"], + Protocol: log["protocol"], } } func (r *Network) Less(other any) bool { o, _ := other.(*Network) - if r.Qualifier.Equals(o.Qualifier) { - if r.Domain == o.Domain { - if r.Type == o.Type { - return r.Protocol < o.Protocol - } - return r.Type < o.Type - } + if r.Domain != o.Domain { return r.Domain < o.Domain } + if r.Type != o.Type { + return r.Type < o.Type + } + if r.Protocol != o.Protocol { + return r.Protocol < o.Protocol + } + if r.AddressExpr.Less(o.AddressExpr) { + return r.AddressExpr.Less(o.AddressExpr) + } return r.Qualifier.Less(o.Qualifier) } diff --git a/pkg/aa/pivot_root.go b/pkg/aa/pivot_root.go index 05e3d0a5..13979ca3 100644 --- a/pkg/aa/pivot_root.go +++ b/pkg/aa/pivot_root.go @@ -5,15 +5,17 @@ package aa type PivotRoot struct { + Rule Qualifier OldRoot string NewRoot string TargetProfile string } -func PivotRootFromLog(log map[string]string) ApparmorRule { +func newPivotRootFromLog(log map[string]string) *PivotRoot { return &PivotRoot{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), OldRoot: log["srcname"], NewRoot: log["name"], TargetProfile: "", @@ -22,15 +24,15 @@ func PivotRootFromLog(log map[string]string) ApparmorRule { func (r *PivotRoot) Less(other any) bool { o, _ := other.(*PivotRoot) - if r.Qualifier.Equals(o.Qualifier) { - if r.OldRoot == o.OldRoot { - if r.NewRoot == o.NewRoot { - return r.TargetProfile < o.TargetProfile - } - return r.NewRoot < o.NewRoot - } + if r.OldRoot != o.OldRoot { return r.OldRoot < o.OldRoot } + if r.NewRoot != o.NewRoot { + return r.NewRoot < o.NewRoot + } + if r.TargetProfile != o.TargetProfile { + return r.TargetProfile < o.TargetProfile + } return r.Qualifier.Less(o.Qualifier) } diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go new file mode 100644 index 00000000..00f5042f --- /dev/null +++ b/pkg/aa/preamble.go @@ -0,0 +1,87 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "slices" +) + +type Abi struct { + Rule + Path string + IsMagic bool +} + +func (r *Abi) Less(other any) bool { + o, _ := other.(*Abi) + if r.Path != o.Path { + return r.Path < o.Path + } + return r.IsMagic == o.IsMagic +} + +func (r *Abi) Equals(other any) bool { + o, _ := other.(*Abi) + return r.Path == o.Path && r.IsMagic == o.IsMagic +} + +type Alias struct { + Rule + Path string + RewrittenPath string +} + +func (r Alias) Less(other any) bool { + o, _ := other.(*Alias) + if r.Path != o.Path { + return r.Path < o.Path + } + return r.RewrittenPath < o.RewrittenPath +} + +func (r Alias) Equals(other any) bool { + o, _ := other.(*Alias) + return r.Path == o.Path && r.RewrittenPath == o.RewrittenPath +} + +type Include struct { + Rule + IfExists bool + Path string + IsMagic bool +} + +func (r *Include) Less(other any) bool { + o, _ := other.(*Include) + if r.Path == o.Path { + return r.Path < o.Path + } + if r.IsMagic != o.IsMagic { + return r.IsMagic + } + return r.IfExists +} + +func (r *Include) Equals(other any) bool { + o, _ := other.(*Include) + return r.Path == o.Path && r.IsMagic == o.IsMagic && r.IfExists == o.IfExists +} + +type Variable struct { + Rule + Name string + Values []string +} + +func (r *Variable) Less(other Variable) bool { + if r.Name != other.Name { + return r.Name < other.Name + } + return len(r.Values) < len(other.Values) +} + +func (r *Variable) Equals(other Variable) bool { + return r.Name == other.Name && slices.Equal(r.Values, other.Values) +} diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 2e97b9d4..48596aca 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -31,10 +31,10 @@ type AppArmorProfile struct { // Preamble section of a profile type Preamble struct { - Abi []Abi - Includes []Include - Aliases []Alias - Variables []Variable + Abi []*Abi + Includes []*Include + Aliases []*Alias + Variables []*Variable } // Profile section of a profile @@ -78,7 +78,7 @@ func (p *AppArmorProfile) AddRule(log map[string]string) { } case "-13": if strings.Contains(log["info"], "namespace creation restricted") { - p.Rules = append(p.Rules, UsernsFromLog(log)) + p.Rules = append(p.Rules, newUsernsFromLog(log)) } else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") { p.Flags = append(p.Flags, "attach_disconnected") } @@ -87,49 +87,51 @@ func (p *AppArmorProfile) AddRule(log map[string]string) { switch log["class"] { case "cap": - p.Rules = append(p.Rules, CapabilityFromLog(log)) + p.Rules = append(p.Rules, newCapabilityFromLog(log)) case "net": if log["family"] == "unix" { - p.Rules = append(p.Rules, UnixFromLog(log)) + p.Rules = append(p.Rules, newUnixFromLog(log)) } else { - p.Rules = append(p.Rules, NetworkFromLog(log)) + p.Rules = append(p.Rules, newNetworkFromLog(log)) } case "mount": if strings.Contains(log["flags"], "remount") { - p.Rules = append(p.Rules, RemountFromLog(log)) + p.Rules = append(p.Rules, newRemountFromLog(log)) } else { switch log["operation"] { case "mount": - p.Rules = append(p.Rules, MountFromLog(log)) + p.Rules = append(p.Rules, newMountFromLog(log)) case "umount": - p.Rules = append(p.Rules, UmountFromLog(log)) + p.Rules = append(p.Rules, newUmountFromLog(log)) case "remount": - p.Rules = append(p.Rules, RemountFromLog(log)) + p.Rules = append(p.Rules, newRemountFromLog(log)) case "pivotroot": - p.Rules = append(p.Rules, PivotRootFromLog(log)) + p.Rules = append(p.Rules, newPivotRootFromLog(log)) } } case "posix_mqueue", "sysv_mqueue": - p.Rules = append(p.Rules, MqueueFromLog(log)) + p.Rules = append(p.Rules, newMqueueFromLog(log)) case "signal": - p.Rules = append(p.Rules, SignalFromLog(log)) + p.Rules = append(p.Rules, newSignalFromLog(log)) case "ptrace": - p.Rules = append(p.Rules, PtraceFromLog(log)) + p.Rules = append(p.Rules, newPtraceFromLog(log)) case "namespace": - p.Rules = append(p.Rules, UsernsFromLog(log)) + p.Rules = append(p.Rules, newUsernsFromLog(log)) case "unix": - p.Rules = append(p.Rules, UnixFromLog(log)) + p.Rules = append(p.Rules, newUnixFromLog(log)) + case "dbus": + p.Rules = append(p.Rules, newDbusFromLog(log)) case "file": if log["operation"] == "change_onexec" { - p.Rules = append(p.Rules, ChangeProfileFromLog(log)) + p.Rules = append(p.Rules, newChangeProfileFromLog(log)) } else { - p.Rules = append(p.Rules, FileFromLog(log)) + p.Rules = append(p.Rules, newFileFromLog(log)) } default: if strings.Contains(log["operation"], "dbus") { - p.Rules = append(p.Rules, DbusFromLog(log)) + p.Rules = append(p.Rules, newDbusFromLog(log)) } else if log["family"] == "unix" { - p.Rules = append(p.Rules, UnixFromLog(log)) + p.Rules = append(p.Rules, newUnixFromLog(log)) } } } @@ -155,7 +157,7 @@ func (p *AppArmorProfile) Sort() { }) } -// MergeRules merge similar rules together +// MergeRules merge similar rules together. // Steps: // - Remove identical rules // - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw' @@ -179,7 +181,7 @@ func (p *AppArmorProfile) MergeRules() { } } -// Format the profile for better readability before printing it +// Format the profile for better readability before printing it. // Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block func (p *AppArmorProfile) Format() { const prefixOwner = " " diff --git a/pkg/aa/profile_test.go b/pkg/aa/profile_test.go index 78206b26..14403e47 100644 --- a/pkg/aa/profile_test.go +++ b/pkg/aa/profile_test.go @@ -43,10 +43,10 @@ func TestAppArmorProfile_String(t *testing.T) { name: "foo", p: &AppArmorProfile{ Preamble: Preamble{ - Abi: []Abi{{IsMagic: true, Path: "abi/4.0"}}, - Includes: []Include{{IsMagic: true, Path: "tunables/global"}}, - Aliases: []Alias{{Path: "/mnt/usr", RewrittenPath: "/usr"}}, - Variables: []Variable{{ + Abi: []*Abi{{IsMagic: true, Path: "abi/4.0"}}, + Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, + Aliases: []*Alias{{Path: "/mnt/usr", RewrittenPath: "/usr"}}, + Variables: []*Variable{{ Name: "exec_path", Values: []string{"@{bin}/foo", "@{lib}/foo"}, }}, @@ -83,11 +83,11 @@ func TestAppArmorProfile_String(t *testing.T) { }, &Ptrace{Access: "read", Peer: "nautilus"}, &Unix{ - Access: "send receive", - Type: "stream", - Address: "@/tmp/.ICE-unix/1995", - Peer: "gnome-shell", - PeerAddr: "none", + Access: "send receive", + Type: "stream", + Address: "@/tmp/.ICE-unix/1995", + PeerLabel: "gnome-shell", + PeerAddr: "none", }, &Dbus{ Access: "bind", @@ -97,11 +97,11 @@ func TestAppArmorProfile_String(t *testing.T) { &Dbus{ Access: "receive", Bus: "system", - Name: ":1.3", Path: "/org/freedesktop/DBus", Interface: "org.freedesktop.DBus", Member: "AddMatch", - Label: "power-profiles-daemon", + PeerName: ":1.3", + PeerLabel: "power-profiles-daemon", }, &File{Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", Access: "rm"}, &File{Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: "rw"}, @@ -290,9 +290,9 @@ func TestAppArmorProfile_Integration(t *testing.T) { name: "aa-status", p: &AppArmorProfile{ Preamble: Preamble{ - Abi: []Abi{{IsMagic: true, Path: "abi/3.0"}}, - Includes: []Include{{IsMagic: true, Path: "tunables/global"}}, - Variables: []Variable{{ + Abi: []*Abi{{IsMagic: true, Path: "abi/3.0"}}, + Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, + Variables: []*Variable{{ Name: "exec_path", Values: []string{"@{bin}/aa-status", "@{bin}/apparmor_status"}, }}, @@ -310,7 +310,7 @@ func TestAppArmorProfile_Integration(t *testing.T) { &File{Path: "@{sys}/kernel/security/apparmor/profiles", Access: "r"}, &File{Path: "@{PROC}/@{pids}/attr/current", Access: "r"}, &Include{IsMagic: true, Path: "abstractions/consoles"}, - &File{Qualifier: Qualifier{Owner: true}, Path: "@{PROC}/@{pid}/mounts", Access: "r"}, + &File{Owner: true, Path: "@{PROC}/@{pid}/mounts", Access: "r"}, &Include{IsMagic: true, Path: "abstractions/base"}, &File{Path: "/dev/tty@{int}", Access: "rw"}, &Capability{Name: "sys_ptrace"}, diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index 5603a24b..6c444e22 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -5,14 +5,16 @@ package aa type Ptrace struct { + Rule Qualifier Access string Peer string } -func PtraceFromLog(log map[string]string) ApparmorRule { +func newPtraceFromLog(log map[string]string) *Ptrace { return &Ptrace{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested_mask"]), Peer: log["peer"], } @@ -20,12 +22,12 @@ func PtraceFromLog(log map[string]string) ApparmorRule { func (r *Ptrace) Less(other any) bool { o, _ := other.(*Ptrace) - if r.Qualifier.Equals(o.Qualifier) { - if r.Access == o.Access { - return r.Peer == o.Peer - } + if r.Access != o.Access { return r.Access < o.Access } + if r.Peer != o.Peer { + return r.Peer == o.Peer + } return r.Qualifier.Less(o.Qualifier) } diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index 7c2d1231..b3d0e782 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -10,15 +10,24 @@ type Rlimit struct { Value string } +func newRlimitFromLog(log map[string]string) *Rlimit { + return &Rlimit{ + Rule: newRuleFromLog(log), + Key: log["key"], + Op: log["op"], + Value: log["value"], + } +} + func (r *Rlimit) Less(other any) bool { o, _ := other.(*Rlimit) - if r.Key == o.Key { - if r.Op == o.Op { - return r.Value < o.Value - } + if r.Key != o.Key { + return r.Key < o.Key + } + if r.Op != o.Op { return r.Op < o.Op } - return r.Key < o.Key + return r.Value < o.Value } func (r *Rlimit) Equals(other any) bool { diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index c2702148..a09aac40 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "strings" ) @@ -12,43 +13,12 @@ type Rule struct { Comment string NoNewPrivs bool FileInherit bool -} - -func (r *Rule) Less(other any) bool { - return false -} - -func (r *Rule) Equals(other any) bool { - return false -} - -// Qualifier to apply extra settings to a rule -type Qualifier struct { - Audit bool - AccessType string - Owner bool - NoNewPrivs bool - FileInherit bool - Optional bool - Comment string Prefix string Padding string + Optional bool } -func NewQualifierFromLog(log map[string]string) Qualifier { - owner := false - fsuid, hasFsUID := log["fsuid"] - ouid, hasOuUID := log["ouid"] - isDbus := strings.Contains(log["operation"], "dbus") - if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus { - owner = true - } - - audit := false - if log["apparmor"] == "AUDIT" { - audit = true - } - +func newRuleFromLog(log map[string]string) Rule { fileInherit := false if log["operation"] == "file_inherit" { fileInherit = true @@ -76,62 +46,54 @@ func NewQualifierFromLog(log map[string]string) Qualifier { default: } - return Qualifier{ - Audit: audit, - Owner: owner, + return Rule{ + Comment: msg, NoNewPrivs: noNewPrivs, FileInherit: fileInherit, Optional: optional, - Comment: msg, } } +func (r Rule) Less(other any) bool { + return false +} + +func (r Rule) Equals(other any) bool { + return false +} + +type Qualifier struct { + Audit bool + AccessType string +} + +func newQualifierFromLog(log map[string]string) Qualifier { + audit := false + if log["apparmor"] == "AUDIT" { + audit = true + } + return Qualifier{Audit: audit} +} + func (r Qualifier) Less(other Qualifier) bool { - if r.Owner == other.Owner { - if r.Audit == other.Audit { - return r.AccessType < other.AccessType - } + if r.Audit != other.Audit { return r.Audit } - return other.Owner + return r.AccessType < other.AccessType } func (r Qualifier) Equals(other Qualifier) bool { - return r.Audit == other.Audit && r.AccessType == other.AccessType && - r.Owner == other.Owner && r.NoNewPrivs == other.NoNewPrivs && - r.FileInherit == other.FileInherit + return r.Audit == other.Audit && r.AccessType == other.AccessType } -// Preamble specific rules - -type Abi struct { - Path string - IsMagic bool +type All struct { + Rule } -func (r Abi) Less(other Abi) bool { - if r.Path == other.Path { - return r.IsMagic == other.IsMagic - } - return r.Path < other.Path +func (r *All) Less(other any) bool { + return false } -func (r Abi) Equals(other Abi) bool { - return r.Path == other.Path && r.IsMagic == other.IsMagic -} - -type Alias struct { - Path string - RewrittenPath string -} - -func (r Alias) Less(other Alias) bool { - if r.Path == other.Path { - return r.RewrittenPath < other.RewrittenPath - } - return r.Path < other.Path -} - -func (r Alias) Equals(other Alias) bool { - return r.Path == other.Path && r.RewrittenPath == other.RewrittenPath +func (r *All) Equals(other any) bool { + return false } diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 0699f123..48f10726 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -17,76 +17,100 @@ func TestRule_FromLog(t *testing.T) { want ApparmorRule }{ { - name: "capbability", - fromLog: CapabilityFromLog, - log: capability1Log, - want: capability1, + name: "capbability", + fromLog: func(m map[string]string) ApparmorRule { + return newCapabilityFromLog(m) + }, + log: capability1Log, + want: capability1, }, { - name: "network", - fromLog: NetworkFromLog, - log: network1Log, - want: network1, + name: "network", + fromLog: func(m map[string]string) ApparmorRule { + return newNetworkFromLog(m) + }, + log: network1Log, + want: network1, }, { - name: "mount", - fromLog: MountFromLog, - log: mount1Log, - want: mount1, + name: "mount", + fromLog: func(m map[string]string) ApparmorRule { + return newMountFromLog(m) + }, + log: mount1Log, + want: mount1, }, { - name: "umount", - fromLog: UmountFromLog, - log: umount1Log, - want: umount1, + name: "umount", + fromLog: func(m map[string]string) ApparmorRule { + return newUmountFromLog(m) + }, + log: umount1Log, + want: umount1, }, { - name: "pivotroot", - fromLog: PivotRootFromLog, - log: pivotroot1Log, - want: pivotroot1, + name: "pivotroot", + fromLog: func(m map[string]string) ApparmorRule { + return newPivotRootFromLog(m) + }, + log: pivotroot1Log, + want: pivotroot1, }, { - name: "changeprofile", - fromLog: ChangeProfileFromLog, - log: changeprofile1Log, - want: changeprofile1, + name: "changeprofile", + fromLog: func(m map[string]string) ApparmorRule { + return newChangeProfileFromLog(m) + }, + log: changeprofile1Log, + want: changeprofile1, }, { - name: "signal", - fromLog: SignalFromLog, - log: signal1Log, - want: signal1, + name: "signal", + fromLog: func(m map[string]string) ApparmorRule { + return newSignalFromLog(m) + }, + log: signal1Log, + want: signal1, }, { - name: "ptrace/xdg-document-portal", - fromLog: PtraceFromLog, - log: ptrace1Log, - want: ptrace1, + name: "ptrace/xdg-document-portal", + fromLog: func(m map[string]string) ApparmorRule { + return newPtraceFromLog(m) + }, + log: ptrace1Log, + want: ptrace1, }, { - name: "ptrace/snap-update-ns.firefox", - fromLog: PtraceFromLog, - log: ptrace2Log, - want: ptrace2, + name: "ptrace/snap-update-ns.firefox", + fromLog: func(m map[string]string) ApparmorRule { + return newPtraceFromLog(m) + }, + log: ptrace2Log, + want: ptrace2, }, { - name: "unix", - fromLog: UnixFromLog, - log: unix1Log, - want: unix1, + name: "unix", + fromLog: func(m map[string]string) ApparmorRule { + return newUnixFromLog(m) + }, + log: unix1Log, + want: unix1, }, { - name: "dbus", - fromLog: DbusFromLog, - log: dbus1Log, - want: dbus1, + name: "dbus", + fromLog: func(m map[string]string) ApparmorRule { + return newDbusFromLog(m) + }, + log: dbus1Log, + want: dbus1, }, { - name: "file", - fromLog: FileFromLog, - log: file1Log, - want: file1, + name: "file", + fromLog: func(m map[string]string) ApparmorRule { + return newFileFromLog(m) + }, + log: file1Log, + want: file1, }, } for _, tt := range tests { @@ -109,13 +133,13 @@ func TestRule_Less(t *testing.T) { name: "include1", rule: include1, other: includeLocal1, - want: true, + want: false, }, { name: "include2", rule: include1, other: include2, - want: true, + want: false, }, { name: "include3", @@ -245,9 +269,9 @@ func TestRule_Less(t *testing.T) { }, { name: "file/owner", - rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Qualifier: Qualifier{Owner: true}}, + rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Owner: true}, other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, - want: false, + want: true, }, { name: "file/access", diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 3dbf9e16..9589f508 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -5,15 +5,17 @@ package aa type Signal struct { + Rule Qualifier Access string Set string Peer string } -func SignalFromLog(log map[string]string) ApparmorRule { +func newSignalFromLog(log map[string]string) *Signal { return &Signal{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested_mask"]), Set: log["signal"], Peer: log["peer"], @@ -22,15 +24,15 @@ func SignalFromLog(log map[string]string) ApparmorRule { func (r *Signal) Less(other any) bool { o, _ := other.(*Signal) - if r.Qualifier.Equals(o.Qualifier) { - if r.Access == o.Access { - if r.Set == o.Set { - return r.Peer < o.Peer - } - return r.Set < o.Set - } + if r.Access != o.Access { return r.Access < o.Access } + if r.Set != o.Set { + return r.Set < o.Set + } + if r.Peer != o.Peer { + return r.Peer < o.Peer + } return r.Qualifier.Less(o.Qualifier) } diff --git a/pkg/aa/templates/include.j2 b/pkg/aa/templates/include.j2 index 8a39a8c3..fad5e9ca 100644 --- a/pkg/aa/templates/include.j2 +++ b/pkg/aa/templates/include.j2 @@ -8,4 +8,5 @@ {{- else -}} {{ " \"" }}{{ .Path }}{{ "\"" }} {{- end -}} + {{- template "comment" . -}} {{- end -}} diff --git a/pkg/aa/templates/profile.j2 b/pkg/aa/templates/profile.j2 index da406e86..8c9587e1 100644 --- a/pkg/aa/templates/profile.j2 +++ b/pkg/aa/templates/profile.j2 @@ -56,7 +56,7 @@ {{- end -}} {{- if eq $type "Rlimit" -}} - {{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }} + {{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }}{{ template "comment" . }} {{- end -}} {{- if eq $type "Capability" -}} @@ -191,15 +191,24 @@ {{- with .Type -}} {{ " type=" }}{{ . }} {{- end -}} + {{- with .Protocol -}} + {{ " protocol=" }}{{ . }} + {{- end -}} {{- with .Address -}} {{ " addr=" }}{{ . }} {{- end -}} - {{- if .Peer -}} - {{ " peer=(label=" }}{{ .Peer }} - {{- with .PeerAddr -}} - {{ ", addr="}}{{ . }} + {{- with .Label -}} + {{ " label=" }}{{ . }} + {{- end -}} + {{- if and .PeerLabel .PeerAddr -}} + {{ " peer=(label=" }}{{ .PeerLabel }}{{ ", addr="}}{{ .PeerAddr }}{{ ")" }} + {{- else -}} + {{- with .PeerLabel -}} + {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .PeerAddr -}} + {{ overindent "peer=(addr=" }}{{ . }}{{ ")" }} {{- end -}} - {{- ")" -}} {{- end -}} {{- "," -}} {{- template "comment" . -}} @@ -256,13 +265,13 @@ {{- with .Member -}} {{ overindent "member=" }}{{ . }}{{ "\n" }} {{- end -}} - {{- if and .Name .Label -}} - {{ overindent "peer=(name=" }}{{ .Name }}{{ ", label="}}{{ .Label }}{{ ")" }} + {{- if and .PeerName .PeerLabel -}} + {{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }} {{- else -}} - {{- with .Name -}} + {{- with .PeerName -}} {{ overindent "peer=(name=" }}{{ . }}{{ ")" }} {{- end -}} - {{- with .Label -}} + {{- with .PeerLabel -}} {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} {{- end -}} {{- end -}} @@ -273,6 +282,9 @@ {{- if eq $type "File" -}} {{- template "qualifier" . -}} + {{- if .Owner -}} + {{- "owner " -}} + {{- end -}} {{- .Path -}} {{- " " -}} {{- with .Padding -}} diff --git a/pkg/aa/templates/qualifier.j2 b/pkg/aa/templates/qualifier.j2 index 929cc8ed..51373549 100644 --- a/pkg/aa/templates/qualifier.j2 +++ b/pkg/aa/templates/qualifier.j2 @@ -2,9 +2,6 @@ {{- with .Prefix -}} {{ . }} {{- end -}} - {{- if .Owner -}} - {{- "owner " -}} - {{- end -}} {{- if .Audit -}} {{- "audit " -}} {{- end -}} diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index 3c353579..0372d467 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -5,62 +5,64 @@ package aa type Unix struct { + Rule Qualifier - Access string - Type string - Protocol string - Address string - Label string - Attr string - Opt string - Peer string - PeerAddr string + Access string + Type string + Protocol string + Address string + Label string + Attr string + Opt string + PeerLabel string + PeerAddr string } -func UnixFromLog(log map[string]string) ApparmorRule { +func newUnixFromLog(log map[string]string) *Unix { return &Unix{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested_mask"]), Type: log["sock_type"], Protocol: log["protocol"], Address: log["addr"], - Label: log["peer_label"], + Label: log["label"], Attr: log["attr"], Opt: log["opt"], - Peer: log["peer"], + PeerLabel: log["peer"], PeerAddr: log["peer_addr"], } } func (r *Unix) Less(other any) bool { o, _ := other.(*Unix) - if r.Qualifier.Equals(o.Qualifier) { - if r.Access == o.Access { - if r.Type == o.Type { - if r.Protocol == o.Protocol { - if r.Address == o.Address { - if r.Label == o.Label { - if r.Attr == o.Attr { - if r.Opt == o.Opt { - if r.Peer == o.Peer { - return r.PeerAddr < o.PeerAddr - } - return r.Peer < o.Peer - } - return r.Opt < o.Opt - } - return r.Attr < o.Attr - } - return r.Label < o.Label - } - return r.Address < o.Address - } - return r.Protocol < o.Protocol - } - return r.Type < o.Type - } + if r.Access != o.Access { return r.Access < o.Access } + if r.Type != o.Type { + return r.Type < o.Type + } + if r.Protocol != o.Protocol { + return r.Protocol < o.Protocol + } + if r.Address != o.Address { + return r.Address < o.Address + } + if r.Label != o.Label { + return r.Label < o.Label + } + if r.Attr != o.Attr { + return r.Attr < o.Attr + } + if r.Opt != o.Opt { + return r.Opt < o.Opt + } + if r.PeerLabel != o.PeerLabel { + return r.PeerLabel < o.PeerLabel + } + if r.PeerAddr != o.PeerAddr { + return r.PeerAddr < o.PeerAddr + } return r.Qualifier.Less(o.Qualifier) } @@ -69,5 +71,6 @@ func (r *Unix) Equals(other any) bool { return r.Access == o.Access && r.Type == o.Type && r.Protocol == o.Protocol && r.Address == o.Address && r.Label == o.Label && r.Attr == o.Attr && r.Opt == o.Opt && - r.Peer == o.Peer && r.PeerAddr == o.PeerAddr && r.Qualifier.Equals(o.Qualifier) + r.PeerLabel == o.PeerLabel && r.PeerAddr == o.PeerAddr && + r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/userns.go b/pkg/aa/userns.go index 446b130d..9c582400 100644 --- a/pkg/aa/userns.go +++ b/pkg/aa/userns.go @@ -5,20 +5,22 @@ package aa type Userns struct { + Rule Qualifier Create bool } -func UsernsFromLog(log map[string]string) ApparmorRule { +func newUsernsFromLog(log map[string]string) *Userns { return &Userns{ - Qualifier: NewQualifierFromLog(log), + Rule: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), Create: true, } } func (r *Userns) Less(other any) bool { o, _ := other.(*Userns) - if r.Qualifier.Equals(o.Qualifier) { + if r.Create != o.Create { return r.Create } return r.Qualifier.Less(o.Qualifier) diff --git a/pkg/aa/variables.go b/pkg/aa/variables.go index ddd2e3d1..5081b5ba 100644 --- a/pkg/aa/variables.go +++ b/pkg/aa/variables.go @@ -9,7 +9,6 @@ package aa import ( "regexp" - "slices" "strings" ) @@ -18,35 +17,19 @@ var ( regVariablesRef = regexp.MustCompile(`@{([^{}]+)}`) ) -type Variable struct { - Name string - Values []string -} - -func (r Variable) Less(other Variable) bool { - if r.Name == other.Name { - return len(r.Values) < len(other.Values) - } - return r.Name < other.Name -} - -func (r Variable) Equals(other Variable) bool { - return r.Name == other.Name && slices.Equal(r.Values, other.Values) -} - // DefaultTunables return a minimal working profile to build the profile // It should not be used when loading file from /etc/apparmor.d func DefaultTunables() *AppArmorProfile { return &AppArmorProfile{ Preamble: Preamble{ - Variables: []Variable{ - {"bin", []string{"/{,usr/}{,s}bin"}}, - {"lib", []string{"/{,usr/}lib{,exec,32,64}"}}, - {"multiarch", []string{"*-linux-gnu*"}}, - {"HOME", []string{"/home/*"}}, - {"user_share_dirs", []string{"/home/*/.local/share"}}, - {"etc_ro", []string{"/{,usr/}etc/"}}, - {"int", []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, + Variables: []*Variable{ + {Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, + {Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}}, + {Name: "multiarch", Values: []string{"*-linux-gnu*"}}, + {Name: "HOME", Values: []string{"/home/*"}}, + {Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}}, + {Name: "etc_ro", Values: []string{"/{,usr/}etc/"}}, + {Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, }, }, } @@ -68,7 +51,7 @@ func (p *AppArmorProfile) ParseVariables(content string) { } } if !found { - variable := Variable{Name: key, Values: values} + variable := &Variable{Name: key, Values: values} p.Variables = append(p.Variables, variable) } } diff --git a/pkg/aa/variables_test.go b/pkg/aa/variables_test.go index 3d91ec7b..2660c5b9 100644 --- a/pkg/aa/variables_test.go +++ b/pkg/aa/variables_test.go @@ -9,6 +9,10 @@ import ( "testing" ) +// TODO: space in variable need to be tested. +// @{name} = "Mullvad VPN" +// profile mullvad-gui /{opt/"Mullvad/mullvad-gui,opt/VPN"/mullvad-gui,mullvad-gui} flags=(attach_disconnected,complain) { + func TestDefaultTunables(t *testing.T) { tests := []struct { name string @@ -18,14 +22,14 @@ func TestDefaultTunables(t *testing.T) { name: "aa", want: &AppArmorProfile{ Preamble: Preamble{ - Variables: []Variable{ - {"bin", []string{"/{,usr/}{,s}bin"}}, - {"lib", []string{"/{,usr/}lib{,exec,32,64}"}}, - {"multiarch", []string{"*-linux-gnu*"}}, - {"HOME", []string{"/home/*"}}, - {"user_share_dirs", []string{"/home/*/.local/share"}}, - {"etc_ro", []string{"/{,usr/}etc/"}}, - {"int", []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, + Variables: []*Variable{ + {Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, + {Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}}, + {Name: "multiarch", Values: []string{"*-linux-gnu*"}}, + {Name: "HOME", Values: []string{"/home/*"}}, + {Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}}, + {Name: "etc_ro", Values: []string{"/{,usr/}etc/"}}, + {Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, }, }, }, @@ -44,7 +48,7 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { tests := []struct { name string content string - want []Variable + want []*Variable }{ { name: "firefox", @@ -54,12 +58,12 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { @{firefox_cache_dirs} = @{user_cache_dirs}/mozilla/ @{exec_path} = /{usr/,}bin/@{firefox_name} @{firefox_lib_dirs}/@{firefox_name} `, - want: []Variable{ - {"firefox_name", []string{"firefox{,-esr,-bin}"}}, - {"firefox_lib_dirs", []string{"/{usr/,}lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, - {"firefox_config_dirs", []string{"@{HOME}/.mozilla/"}}, - {"firefox_cache_dirs", []string{"@{user_cache_dirs}/mozilla/"}}, - {"exec_path", []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, + want: []*Variable{ + {Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}}, + {Name: "firefox_lib_dirs", Values: []string{"/{usr/,}lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, + {Name: "firefox_config_dirs", Values: []string{"@{HOME}/.mozilla/"}}, + {Name: "firefox_cache_dirs", Values: []string{"@{user_cache_dirs}/mozilla/"}}, + {Name: "exec_path", Values: []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, }, }, { @@ -68,8 +72,8 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { @{exec_path} += /{usr/,}bin/Xorg{,.bin} @{exec_path} += /{usr/,}lib/Xorg{,.wrap} @{exec_path} += /{usr/,}lib/xorg/Xorg{,.wrap}`, - want: []Variable{ - {"exec_path", []string{ + want: []*Variable{ + {Name: "exec_path", Values: []string{ "/{usr/,}bin/X", "/{usr/,}bin/Xorg{,.bin}", "/{usr/,}lib/Xorg{,.wrap}", @@ -81,9 +85,9 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { name: "snapd", content: `@{lib_dirs} = @{lib}/ /snap/snapd/@{int}@{lib} @{exec_path} = @{lib_dirs}/snapd/snapd`, - want: []Variable{ - {"lib_dirs", []string{"@{lib}/", "/snap/snapd/@{int}@{lib}"}}, - {"exec_path", []string{"@{lib_dirs}/snapd/snapd"}}, + want: []*Variable{ + {Name: "lib_dirs", Values: []string{"@{lib}/", "/snap/snapd/@{int}@{lib}"}}, + {Name: "exec_path", Values: []string{"@{lib_dirs}/snapd/snapd"}}, }, }, } @@ -104,11 +108,21 @@ func TestAppArmorProfile_resolve(t *testing.T) { input string want []string }{ + { + name: "default", + input: "@{etc_ro}", + want: []string{"/{,usr/}etc/"}, + }, { name: "empty", input: "@{}", want: []string{"@{}"}, }, + { + name: "nil", + input: "@{foo}", + want: []string{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -123,15 +137,15 @@ func TestAppArmorProfile_resolve(t *testing.T) { func TestAppArmorProfile_ResolveAttachments(t *testing.T) { tests := []struct { name string - variables []Variable + variables []*Variable want []string }{ { name: "firefox", - variables: []Variable{ - {"firefox_name", []string{"firefox{,-esr,-bin}"}}, - {"firefox_lib_dirs", []string{"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, - {"exec_path", []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, + variables: []*Variable{ + {Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}}, + {Name: "firefox_lib_dirs", Values: []string{"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, + {Name: "exec_path", Values: []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, }, want: []string{ "/{usr/,}bin/firefox{,-esr,-bin}", @@ -141,10 +155,10 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { }, { name: "chromium", - variables: []Variable{ - {"name", []string{"chromium"}}, - {"lib_dirs", []string{"/{usr/,}lib/@{name}"}}, - {"exec_path", []string{"@{lib_dirs}/@{name}"}}, + variables: []*Variable{ + {Name: "name", Values: []string{"chromium"}}, + {Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{name}"}}, + {Name: "exec_path", Values: []string{"@{lib_dirs}/@{name}"}}, }, want: []string{ "/{usr/,}lib/chromium/chromium", @@ -152,9 +166,9 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { }, { name: "geoclue", - variables: []Variable{ - {"libexec", []string{"/{usr/,}libexec"}}, - {"exec_path", []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}}, + variables: []*Variable{ + {Name: "libexec", Values: []string{"/{usr/,}libexec"}}, + {Name: "exec_path", Values: []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}}, }, want: []string{ "/{usr/,}libexec/geoclue", @@ -163,11 +177,11 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { }, { name: "opera", - variables: []Variable{ - {"multiarch", []string{"*-linux-gnu*"}}, - {"name", []string{"opera{,-beta,-developer}"}}, - {"lib_dirs", []string{"/{usr/,}lib/@{multiarch}/@{name}"}}, - {"exec_path", []string{"@{lib_dirs}/@{name}"}}, + variables: []*Variable{ + {Name: "multiarch", Values: []string{"*-linux-gnu*"}}, + {Name: "name", Values: []string{"opera{,-beta,-developer}"}}, + {Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{multiarch}/@{name}"}}, + {Name: "exec_path", Values: []string{"@{lib_dirs}/@{name}"}}, }, want: []string{ "/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}", diff --git a/pkg/logs/logs_test.go b/pkg/logs/logs_test.go index 4bfd5d90..d0aa0f5b 100644 --- a/pkg/logs/logs_test.go +++ b/pkg/logs/logs_test.go @@ -303,16 +303,16 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { Name: "kmod", Rules: aa.Rules{ &aa.Unix{ - Qualifier: aa.Qualifier{FileInherit: true}, - Access: "send receive", - Type: "stream", - Protocol: "0", + Rule: aa.Rule{FileInherit: true}, + Access: "send receive", + Type: "stream", + Protocol: "0", }, &aa.Unix{ - Qualifier: aa.Qualifier{FileInherit: true}, - Access: "send receive", - Type: "stream", - Protocol: "0", + Rule: aa.Rule{FileInherit: true}, + Access: "send receive", + Type: "stream", + Protocol: "0", }, }, }, @@ -324,11 +324,11 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { &aa.Dbus{ Access: "send", Bus: "system", - Name: "org.freedesktop.DBus", Path: "/org/freedesktop/DBus", Interface: "org.freedesktop.DBus", Member: "AddMatch", - Label: "dbus-daemon", + PeerName: "org.freedesktop.DBus", + PeerLabel: "dbus-daemon", }, }, }, diff --git a/pkg/prebuild/directive/dbus.go b/pkg/prebuild/directive/dbus.go index 2c171624..f0922c55 100644 --- a/pkg/prebuild/directive/dbus.go +++ b/pkg/prebuild/directive/dbus.go @@ -107,7 +107,7 @@ func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile { Bus: rules["bus"], Path: rules["path"], Interface: iface, - Name: `":1.@{int}"`, + PeerName: `":1.@{int}"`, }) } for _, iface := range interfaces { @@ -116,7 +116,7 @@ func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile { Bus: rules["bus"], Path: rules["path"], Interface: iface, - Name: `"{:1.@{int},org.freedesktop.DBus}"`, + PeerName: `"{:1.@{int},org.freedesktop.DBus}"`, }) } p.Rules = append(p.Rules, &aa.Dbus{ @@ -125,7 +125,7 @@ func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile { Path: rules["path"], Interface: "org.freedesktop.DBus.Introspectable", Member: "Introspect", - Name: `":1.@{int}"`, + PeerName: `":1.@{int}"`, }) return p } @@ -139,8 +139,8 @@ func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile { Bus: rules["bus"], Path: rules["path"], Interface: iface, - Name: `"{:1.@{int},` + rules["name"] + `}"`, - Label: rules["label"], + PeerName: `"{:1.@{int},` + rules["name"] + `}"`, + PeerLabel: rules["label"], }) } for _, iface := range interfaces { @@ -149,8 +149,8 @@ func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile { Bus: rules["bus"], Path: rules["path"], Interface: iface, - Name: `"{:1.@{int},` + rules["name"] + `}"`, - Label: rules["label"], + PeerName: `"{:1.@{int},` + rules["name"] + `}"`, + PeerLabel: rules["label"], }) } return p From 507002c66020f2a18c53a08bbddbb2a3292b34cd Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Mon, 15 Apr 2024 13:32:20 +0100 Subject: [PATCH 03/62] feat(aa): rename the main file template. --- pkg/aa/template.go | 2 +- pkg/aa/templates/{profile.j2 => file.j2} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/aa/templates/{profile.j2 => file.j2} (100%) diff --git a/pkg/aa/template.go b/pkg/aa/template.go index b5600286..3c7e7084 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -96,7 +96,7 @@ var ( ) func generateTemplate() *template.Template { - res := template.New("profile.j2").Funcs(tmplFunctionMap) + res := template.New("file.j2").Funcs(tmplFunctionMap) res = template.Must(res.ParseFS(tmplFiles, "templates/*.j2")) return res } diff --git a/pkg/aa/templates/profile.j2 b/pkg/aa/templates/file.j2 similarity index 100% rename from pkg/aa/templates/profile.j2 rename to pkg/aa/templates/file.j2 From 4b753210e72daeeb3e84ca1a9059d6c03f916c0b Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Mon, 15 Apr 2024 14:09:04 +0100 Subject: [PATCH 04/62] feat(aa): modify the apparmor struct to support multiple profiles and subprofile. --- pkg/aa/profile.go | 143 ++++++++++------ pkg/aa/profile_test.go | 72 ++++---- pkg/aa/rlimit.go | 1 + pkg/aa/template.go | 1 + pkg/aa/templates/file.j2 | 284 +----------------------------- pkg/aa/templates/profile.j2 | 303 +++++++++++++++++++++++++++++++++ pkg/aa/variables.go | 11 +- pkg/aa/variables_test.go | 8 +- pkg/logs/logs.go | 5 +- pkg/logs/logs_test.go | 12 +- pkg/prebuild/directive/dbus.go | 10 +- pkg/prebuild/directive/exec.go | 11 +- 12 files changed, 467 insertions(+), 394 deletions(-) create mode 100644 pkg/aa/templates/profile.j2 diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 48596aca..54f288dd 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -6,6 +6,7 @@ package aa import ( "bytes" + "maps" "reflect" "slices" "sort" @@ -20,16 +21,16 @@ var MagicRoot = paths.New("/etc/apparmor.d") // AppArmorProfiles represents a full set of apparmor profiles type AppArmorProfiles map[string]*AppArmorProfile -// ApparmorProfile represents a full apparmor profile. +// ApparmorProfile represents a full apparmor profile file. // Warning: close to the BNF grammar of apparmor profile but not exactly the same (yet): // - Some rules are not supported yet (subprofile, hat...) // - The structure is simplified as it only aims at writing profile, not parsing it. type AppArmorProfile struct { Preamble - Profile + Profiles []*Profile } -// Preamble section of a profile +// Preamble section of a profile file, type Preamble struct { Abi []*Abi Includes []*Include @@ -37,13 +38,29 @@ type Preamble struct { Variables []*Variable } -// Profile section of a profile +// Profile represent a single AppArmor profile. type Profile struct { + Rule + Header + Rules Rules +} + +type Header struct { Name string Attachments []string Attributes map[string]string Flags []string - Rules Rules +} + +func (r *Profile) Less(other any) bool { + return false // TBD +} + +func (r *Profile) Equals(other any) bool { + o, _ := other.(*Profile) + return r.Name == o.Name && slices.Equal(r.Attachments, o.Attachments) && + maps.Equal(r.Attributes, o.Attributes) && + slices.Equal(r.Flags, o.Flags) } // ApparmorRule generic interface @@ -68,8 +85,20 @@ func (p *AppArmorProfile) String() string { return res.String() } +// GetDefaultProfile ensure a profile is always present in the profile file and +// return it, as a default profile. +func (p *AppArmorProfile) GetDefaultProfile() *Profile { + if len(p.Profiles) == 0 { + p.Profiles = append(p.Profiles, &Profile{}) + } + return p.Profiles[0] +} + // AddRule adds a new rule to the profile from a log map -func (p *AppArmorProfile) AddRule(log map[string]string) { +// See utils/apparmor/logparser.py for the format of the log map +func (profile *AppArmorProfile) AddRule(log map[string]string) { + p := profile.GetDefaultProfile() + // Generate profile flags and extra rules switch log["error"] { case "-2": @@ -138,23 +167,25 @@ func (p *AppArmorProfile) AddRule(log map[string]string) { // Sort the rules in the profile // Follow: https://apparmor.pujol.io/development/guidelines/#guidelines -func (p *AppArmorProfile) Sort() { - sort.Slice(p.Rules, func(i, j int) bool { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - valueOfI := typeToValue(typeOfI) - valueOfJ := typeToValue(typeOfJ) - if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists { - valueOfI = "include_if_exists" +func (profile *AppArmorProfile) Sort() { + for _, p := range profile.Profiles { + sort.Slice(p.Rules, func(i, j int) bool { + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + if typeOfI != typeOfJ { + valueOfI := typeToValue(typeOfI) + valueOfJ := typeToValue(typeOfJ) + if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists { + valueOfI = "include_if_exists" + } + if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists { + valueOfJ = "include_if_exists" + } + return ruleWeights[valueOfI] < ruleWeights[valueOfJ] } - if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists { - valueOfJ = "include_if_exists" - } - return ruleWeights[valueOfI] < ruleWeights[valueOfJ] - } - return p.Rules[i].Less(p.Rules[j]) - }) + return p.Rules[i].Less(p.Rules[j]) + }) + } } // MergeRules merge similar rules together. @@ -163,19 +194,21 @@ func (p *AppArmorProfile) Sort() { // - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw' // // Note: logs.regCleanLogs helps a lot to do a first cleaning -func (p *AppArmorProfile) MergeRules() { - for i := 0; i < len(p.Rules); i++ { - for j := i + 1; j < len(p.Rules); j++ { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - continue - } +func (profile *AppArmorProfile) MergeRules() { + for _, p := range profile.Profiles { + for i := 0; i < len(p.Rules); i++ { + for j := i + 1; j < len(p.Rules); j++ { + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + if typeOfI != typeOfJ { + continue + } - // If rules are identical, merge them - if p.Rules[i].Equals(p.Rules[j]) { - p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) - j-- + // If rules are identical, merge them + if p.Rules[i].Equals(p.Rules[j]) { + p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) + j-- + } } } } @@ -183,30 +216,32 @@ func (p *AppArmorProfile) MergeRules() { // Format the profile for better readability before printing it. // Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block -func (p *AppArmorProfile) Format() { +func (profile *AppArmorProfile) Format() { const prefixOwner = " " - hasOwnerRule := false - for i := len(p.Rules) - 1; i > 0; i-- { - j := i - 1 - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) + for _, p := range profile.Profiles { + hasOwnerRule := false + for i := len(p.Rules) - 1; i > 0; i-- { + j := i - 1 + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) - // File rule - if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) { - letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) - letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) + // File rule + if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) { + letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) + letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) - // Add prefix before rule path to align with other rule - if p.Rules[i].(*File).Owner { - hasOwnerRule = true - } else if hasOwnerRule { - p.Rules[i].(*File).Prefix = prefixOwner - } + // Add prefix before rule path to align with other rule + if p.Rules[i].(*File).Owner { + hasOwnerRule = true + } else if hasOwnerRule { + p.Rules[i].(*File).Prefix = prefixOwner + } - if letterI != letterJ { - // Add a new empty line between Files rule of different type - hasOwnerRule = false - p.Rules = append(p.Rules[:i], append([]ApparmorRule{&Rule{}}, p.Rules[i:]...)...) + if letterI != letterJ { + // Add a new empty line between Files rule of different type + hasOwnerRule = false + p.Rules = append(p.Rules[:i], append([]ApparmorRule{&Rule{}}, p.Rules[i:]...)...) + } } } } diff --git a/pkg/aa/profile_test.go b/pkg/aa/profile_test.go index 14403e47..9866cf89 100644 --- a/pkg/aa/profile_test.go +++ b/pkg/aa/profile_test.go @@ -51,11 +51,13 @@ func TestAppArmorProfile_String(t *testing.T) { Values: []string{"@{bin}/foo", "@{lib}/foo"}, }}, }, - Profile: Profile{ - Name: "foo", - Attachments: []string{"@{exec_path}"}, - Attributes: map[string]string{"security.tagged": "allowed"}, - Flags: []string{"complain", "attach_disconnected"}, + Profiles: []*Profile{{ + Header: Header{ + Name: "foo", + Attachments: []string{"@{exec_path}"}, + Attributes: map[string]string{"security.tagged": "allowed"}, + Flags: []string{"complain", "attach_disconnected"}, + }, Rules: []ApparmorRule{ &Include{IsMagic: true, Path: "abstractions/base"}, &Include{IsMagic: true, Path: "abstractions/nameservice-strict"}, @@ -108,7 +110,7 @@ func TestAppArmorProfile_String(t *testing.T) { &File{Path: "@{sys}/devices/@{pci}/class", Access: "r"}, includeLocal1, }, - }, + }}, }, want: readprofile("tests/string.aa"), }, @@ -132,72 +134,72 @@ func TestAppArmorProfile_AddRule(t *testing.T) { name: "capability", log: capability1Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{capability1}, - }, + }}, }, }, { name: "network", log: network1Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{network1}, - }, + }}, }, }, { name: "mount", log: mount2Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{mount2}, - }, + }}, }, }, { name: "signal", log: signal1Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{signal1}, - }, + }}, }, }, { name: "ptrace", log: ptrace2Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{ptrace2}, - }, + }}, }, }, { name: "unix", log: unix1Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{unix1}, - }, + }}, }, }, { name: "dbus", log: dbus2Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{dbus2}, - }, + }}, }, }, { name: "file", log: file2Log, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{file2}, - }, + }}, }, }, } @@ -221,20 +223,20 @@ func TestAppArmorProfile_Sort(t *testing.T) { { name: "all", origin: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{ file2, network1, includeLocal1, dbus2, signal1, ptrace1, capability2, file1, dbus1, unix2, signal2, mount2, }, - }, + }}, }, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{ capability2, network1, mount2, signal1, signal2, ptrace1, unix2, dbus2, dbus1, file1, file2, includeLocal1, }, - }, + }}, }, }, } @@ -258,14 +260,14 @@ func TestAppArmorProfile_MergeRules(t *testing.T) { { name: "all", origin: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{capability1, capability1, network1, network1, file1, file1}, - }, + }}, }, want: &AppArmorProfile{ - Profile: Profile{ + Profiles: []*Profile{{ Rules: []ApparmorRule{capability1, network1, file1}, - }, + }}, }, }, } @@ -297,9 +299,11 @@ func TestAppArmorProfile_Integration(t *testing.T) { Values: []string{"@{bin}/aa-status", "@{bin}/apparmor_status"}, }}, }, - Profile: Profile{ - Name: "aa-status", - Attachments: []string{"@{exec_path}"}, + Profiles: []*Profile{{ + Header: Header{ + Name: "aa-status", + Attachments: []string{"@{exec_path}"}, + }, Rules: Rules{ &Include{IfExists: true, IsMagic: true, Path: "local/aa-status"}, &Capability{Name: "dac_read_search"}, @@ -316,7 +320,7 @@ func TestAppArmorProfile_Integration(t *testing.T) { &Capability{Name: "sys_ptrace"}, &Ptrace{Access: "read"}, }, - }, + }}, }, want: readprofile("apparmor.d/profiles-a-f/aa-status"), }, diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index b3d0e782..d484b352 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -5,6 +5,7 @@ package aa type Rlimit struct { + Rule Key string Op string Value string diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 3c7e7084..88235e8c 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -64,6 +64,7 @@ var ( "iouring", "dbus", "file", + "profile", "include_if_exists", } ruleWeights = map[string]int{} diff --git a/pkg/aa/templates/file.j2 b/pkg/aa/templates/file.j2 index 8c9587e1..821341b5 100644 --- a/pkg/aa/templates/file.j2 +++ b/pkg/aa/templates/file.j2 @@ -22,286 +22,6 @@ {{ "@{" }}{{ .Name }}{{ "} = " }}{{ join .Values }} {{ end -}} -{{- if or .Name .Attachments .Attributes .Flags -}} - {{- "profile" -}} - {{- with .Name -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Attachments -}} - {{ " " }}{{ join . }} - {{- end -}} - {{- with .Attributes -}} - {{ " xattrs=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .Flags -}} - {{ " flags=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{ " {\n" }} -{{- end -}} - -{{- $oldtype := "" -}} -{{- range .Rules -}} - {{- $type := typeof . -}} - {{- if eq $type "Rule" -}} - {{- "\n" -}} - {{- continue -}} - {{- end -}} - {{- if and (ne $type $oldtype) (ne $oldtype "") -}} - {{- "\n" -}} - {{- end -}} - {{- indent "" -}} - - {{- if eq $type "Include" -}} - {{ template "include" . }} - {{- end -}} - - {{- if eq $type "Rlimit" -}} - {{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }}{{ template "comment" . }} - {{- end -}} - - {{- if eq $type "Capability" -}} - {{ template "qualifier" . }}{{ "capability " }}{{ .Name }}{{ "," }}{{ template "comment" . }} - {{- end -}} - - {{- if eq $type "Network" -}} - {{- template "qualifier" . -}} - {{ "network" }} - {{- with .Domain -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Type -}} - {{ " " }}{{ . }} - {{- else -}} - {{- with .Protocol -}} - {{ " " }}{{ . }} - {{- end -}} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Mount" -}} - {{- template "qualifier" . -}} - {{- "mount" -}} - {{- with .FsType -}} - {{ " fstype=" }}{{ . }} - {{- end -}} - {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .Source -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .MountPoint -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Umount" -}} - {{- template "qualifier" . -}} - {{- "umount" -}} - {{- with .FsType -}} - {{ " fstype=" }}{{ . }} - {{- end -}} - {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .MountPoint -}} - {{ " " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Remount" -}} - {{- template "qualifier" . -}} - {{- "remount" -}} - {{- with .FsType -}} - {{ " fstype=" }}{{ . }} - {{- end -}} - {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .MountPoint -}} - {{ " " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "PivotRoot" -}} - {{- template "qualifier" . -}} - {{- "pivot_root" -}} - {{- with .OldRoot -}} - {{ " oldroot=" }}{{ . }} - {{- end -}} - {{- with .NewRoot -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .TargetProfile -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "ChangeProfile" -}} - {{- template "qualifier" . -}} - {{- "change_profile" -}} - {{- with .ExecMode -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Exec -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .ProfileName -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Mqueue" -}} - {{- template "qualifier" . -}} - {{- "mqueue" -}} - {{- with .Access -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Type -}} - {{ " type=" }}{{ . }} - {{- end -}} - {{- with .Label -}} - {{ " label=" }}{{ . }} - {{- end -}} - {{- with .Name -}} - {{ " " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Unix" -}} - {{- template "qualifier" . -}} - {{- "unix" -}} - {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Type -}} - {{ " type=" }}{{ . }} - {{- end -}} - {{- with .Protocol -}} - {{ " protocol=" }}{{ . }} - {{- end -}} - {{- with .Address -}} - {{ " addr=" }}{{ . }} - {{- end -}} - {{- with .Label -}} - {{ " label=" }}{{ . }} - {{- end -}} - {{- if and .PeerLabel .PeerAddr -}} - {{ " peer=(label=" }}{{ .PeerLabel }}{{ ", addr="}}{{ .PeerAddr }}{{ ")" }} - {{- else -}} - {{- with .PeerLabel -}} - {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .PeerAddr -}} - {{ overindent "peer=(addr=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Ptrace" -}} - {{- template "qualifier" . -}} - {{- "ptrace" -}} - {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Peer -}} - {{ " peer=" }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Signal" -}} - {{- template "qualifier" . -}} - {{- "signal" -}} - {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Set -}} - {{ " set=(" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Peer -}} - {{ " peer=" }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Dbus" -}} - {{- template "qualifier" . -}} - {{- "dbus" -}} - {{- if eq .Access "bind" -}} - {{ " bind bus=" }}{{ .Bus }}{{ " name=" }}{{ .Name }} - {{- else -}} - {{- with .Access -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Bus -}} - {{ " bus=" }}{{ . }} - {{- end -}} - {{- with .Path -}} - {{ " path=" }}{{ . }} - {{- end -}} - {{ "\n" }} - {{- with .Interface -}} - {{ overindent "interface=" }}{{ . }}{{ "\n" }} - {{- end -}} - {{- with .Member -}} - {{ overindent "member=" }}{{ . }}{{ "\n" }} - {{- end -}} - {{- if and .PeerName .PeerLabel -}} - {{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }} - {{- else -}} - {{- with .PeerName -}} - {{ overindent "peer=(name=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .PeerLabel -}} - {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- end -}} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "File" -}} - {{- template "qualifier" . -}} - {{- if .Owner -}} - {{- "owner " -}} - {{- end -}} - {{- .Path -}} - {{- " " -}} - {{- with .Padding -}} - {{ . }} - {{- end -}} - {{- .Access -}} - {{- with .Target -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- "\n" -}} - {{- $oldtype = $type -}} -{{- end -}} - -{{- if or .Name .Attachments .Attributes .Flags -}} - {{- "}\n" -}} +{{- range .Profiles -}} + {{ template "profile" . }} {{- end -}} diff --git a/pkg/aa/templates/profile.j2 b/pkg/aa/templates/profile.j2 new file mode 100644 index 00000000..9875587c --- /dev/null +++ b/pkg/aa/templates/profile.j2 @@ -0,0 +1,303 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "profile" -}} + + {{- if or .Name .Attachments .Attributes .Flags -}} + {{- "profile" -}} + {{- with .Name -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Attachments -}} + {{ " " }}{{ join . }} + {{- end -}} + {{- with .Attributes -}} + {{ " xattrs=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .Flags -}} + {{ " flags=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{ " {" }} + {{- template "comment" . -}} + {{- "\n" -}} + {{- end -}} + + {{- $oldtype := "" -}} + {{- range .Rules -}} + {{- $type := typeof . -}} + {{- if eq $type "Rule" -}} + {{- "\n" -}} + {{- continue -}} + {{- end -}} + {{- if and (ne $type $oldtype) (ne $oldtype "") -}} + {{- "\n" -}} + {{- end -}} + {{- indent "" -}} + + {{- if eq $type "Include" -}} + {{ template "include" . }} + {{- end -}} + + {{- if eq $type "Rlimit" -}} + {{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }}{{ template "comment" . }} + {{- end -}} + + {{- if eq $type "Userns" -}} + {{- if .Create -}} + {{ template "qualifier" . }}{{ "userns," }}{{ template "comment" . }} + {{- end -}} + {{- end -}} + + {{- if eq $type "Capability" -}} + {{ template "qualifier" . }}{{ "capability " }}{{ .Name }}{{ "," }}{{ template "comment" . }} + {{- end -}} + + {{- if eq $type "Network" -}} + {{- template "qualifier" . -}} + {{ "network" }} + {{- with .Domain -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Type -}} + {{ " " }}{{ . }} + {{- else -}} + {{- with .Protocol -}} + {{ " " }}{{ . }} + {{- end -}} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Mount" -}} + {{- template "qualifier" . -}} + {{- "mount" -}} + {{- with .FsType -}} + {{ " fstype=" }}{{ . }} + {{- end -}} + {{- with .Options -}} + {{ " options=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .Source -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .MountPoint -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Umount" -}} + {{- template "qualifier" . -}} + {{- "umount" -}} + {{- with .FsType -}} + {{ " fstype=" }}{{ . }} + {{- end -}} + {{- with .Options -}} + {{ " options=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .MountPoint -}} + {{ " " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Remount" -}} + {{- template "qualifier" . -}} + {{- "remount" -}} + {{- with .FsType -}} + {{ " fstype=" }}{{ . }} + {{- end -}} + {{- with .Options -}} + {{ " options=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .MountPoint -}} + {{ " " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "PivotRoot" -}} + {{- template "qualifier" . -}} + {{- "pivot_root" -}} + {{- with .OldRoot -}} + {{ " oldroot=" }}{{ . }} + {{- end -}} + {{- with .NewRoot -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .TargetProfile -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "ChangeProfile" -}} + {{- template "qualifier" . -}} + {{- "change_profile" -}} + {{- with .ExecMode -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Exec -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .ProfileName -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Mqueue" -}} + {{- template "qualifier" . -}} + {{- "mqueue" -}} + {{- with .Access -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Type -}} + {{ " type=" }}{{ . }} + {{- end -}} + {{- with .Label -}} + {{ " label=" }}{{ . }} + {{- end -}} + {{- with .Name -}} + {{ " " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Unix" -}} + {{- template "qualifier" . -}} + {{- "unix" -}} + {{- with .Access -}} + {{ " (" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Type -}} + {{ " type=" }}{{ . }} + {{- end -}} + {{- with .Protocol -}} + {{ " protocol=" }}{{ . }} + {{- end -}} + {{- with .Address -}} + {{ " addr=" }}{{ . }} + {{- end -}} + {{- with .Label -}} + {{ " label=" }}{{ . }} + {{- end -}} + {{- if and .PeerLabel .PeerAddr -}} + {{ " peer=(label=" }}{{ .PeerLabel }}{{ ", addr="}}{{ .PeerAddr }}{{ ")" }} + {{- else -}} + {{- with .PeerLabel -}} + {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .PeerAddr -}} + {{ overindent "peer=(addr=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Ptrace" -}} + {{- template "qualifier" . -}} + {{- "ptrace" -}} + {{- with .Access -}} + {{ " (" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Peer -}} + {{ " peer=" }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Signal" -}} + {{- template "qualifier" . -}} + {{- "signal" -}} + {{- with .Access -}} + {{ " (" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Set -}} + {{ " set=(" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Peer -}} + {{ " peer=" }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Dbus" -}} + {{- template "qualifier" . -}} + {{- "dbus" -}} + {{- if eq .Access "bind" -}} + {{ " bind bus=" }}{{ .Bus }}{{ " name=" }}{{ .Name }} + {{- else -}} + {{- with .Access -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Bus -}} + {{ " bus=" }}{{ . }} + {{- end -}} + {{- with .Path -}} + {{ " path=" }}{{ . }} + {{- end -}} + {{ "\n" }} + {{- with .Interface -}} + {{ overindent "interface=" }}{{ . }}{{ "\n" }} + {{- end -}} + {{- with .Member -}} + {{ overindent "member=" }}{{ . }}{{ "\n" }} + {{- end -}} + {{- if and .PeerName .PeerLabel -}} + {{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }} + {{- else -}} + {{- with .PeerName -}} + {{ overindent "peer=(name=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .PeerLabel -}} + {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- end -}} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "File" -}} + {{- template "qualifier" . -}} + {{- if .Owner -}} + {{- "owner " -}} + {{- end -}} + {{- .Path -}} + {{- " " -}} + {{- with .Padding -}} + {{ . }} + {{- end -}} + {{- .Access -}} + {{- with .Target -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} + {{- end -}} + + {{- if eq $type "Profile" -}} + {{ template "profile" . }} + {{- end -}} + + {{- "\n" -}} + {{- $oldtype = $type -}} + {{- end -}} + + {{- if or .Name .Attachments .Attributes .Flags -}} + {{- "}\n" -}} + {{- end -}} + +{{- end -}} diff --git a/pkg/aa/variables.go b/pkg/aa/variables.go index 5081b5ba..cd34bd1a 100644 --- a/pkg/aa/variables.go +++ b/pkg/aa/variables.go @@ -83,11 +83,13 @@ func (p *AppArmorProfile) resolve(str string) []string { } // ResolveAttachments resolve profile attachments defined in exec_path -func (p *AppArmorProfile) ResolveAttachments() { - for _, variable := range p.Variables { +func (profile *AppArmorProfile) ResolveAttachments() { + p := profile.GetDefaultProfile() + + for _, variable := range profile.Variables { if variable.Name == "exec_path" { for _, value := range variable.Values { - attachments := p.resolve(value) + attachments := profile.resolve(value) if len(attachments) == 0 { panic("Variable not defined in: " + value) } @@ -98,7 +100,8 @@ func (p *AppArmorProfile) ResolveAttachments() { } // NestAttachments return a nested attachment string -func (p *AppArmorProfile) NestAttachments() string { +func (profile *AppArmorProfile) NestAttachments() string { + p := profile.GetDefaultProfile() if len(p.Attachments) == 0 { return "" } else if len(p.Attachments) == 1 { diff --git a/pkg/aa/variables_test.go b/pkg/aa/variables_test.go index 2660c5b9..2232e65a 100644 --- a/pkg/aa/variables_test.go +++ b/pkg/aa/variables_test.go @@ -193,8 +193,9 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { p := NewAppArmorProfile() p.Variables = tt.variables p.ResolveAttachments() - if !reflect.DeepEqual(p.Attachments, tt.want) { - t.Errorf("AppArmorProfile.ResolveAttachments() = %v, want %v", p.Attachments, tt.want) + profile := p.GetDefaultProfile() + if !reflect.DeepEqual(profile.Attachments, tt.want) { + t.Errorf("AppArmorProfile.ResolveAttachments() = %v, want %v", profile.Attachments, tt.want) } }) } @@ -242,7 +243,8 @@ func TestAppArmorProfile_NestAttachments(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := NewAppArmorProfile() - p.Attachments = tt.Attachments + profile := p.GetDefaultProfile() + profile.Attachments = tt.Attachments if got := p.NestAttachments(); got != tt.want { t.Errorf("AppArmorProfile.NestAttachments() = %v, want %v", got, tt.want) } diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go index 913f1172..5b2ea712 100644 --- a/pkg/logs/logs.go +++ b/pkg/logs/logs.go @@ -208,8 +208,9 @@ func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles { } if _, ok := profiles[name]; !ok { - profile := &aa.AppArmorProfile{} - profile.Name = name + profile := &aa.AppArmorProfile{ + Profiles: []*aa.Profile{{Header: aa.Header{Name: name}}}, + } profile.AddRule(log) profiles[name] = profile } else { diff --git a/pkg/logs/logs_test.go b/pkg/logs/logs_test.go index d0aa0f5b..e970da5f 100644 --- a/pkg/logs/logs_test.go +++ b/pkg/logs/logs_test.go @@ -299,8 +299,8 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...), want: aa.AppArmorProfiles{ "kmod": &aa.AppArmorProfile{ - Profile: aa.Profile{ - Name: "kmod", + Profiles: []*aa.Profile{{ + Header: aa.Header{Name: "kmod"}, Rules: aa.Rules{ &aa.Unix{ Rule: aa.Rule{FileInherit: true}, @@ -315,11 +315,11 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { Protocol: "0", }, }, - }, + }}, }, "power-profiles-daemon": &aa.AppArmorProfile{ - Profile: aa.Profile{ - Name: "power-profiles-daemon", + Profiles: []*aa.Profile{{ + Header: aa.Header{Name: "power-profiles-daemon"}, Rules: aa.Rules{ &aa.Dbus{ Access: "send", @@ -331,7 +331,7 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { PeerLabel: "dbus-daemon", }, }, - }, + }}, }, }, }, diff --git a/pkg/prebuild/directive/dbus.go b/pkg/prebuild/directive/dbus.go index f0922c55..98c2306f 100644 --- a/pkg/prebuild/directive/dbus.go +++ b/pkg/prebuild/directive/dbus.go @@ -97,7 +97,8 @@ func (d Dbus) sanityCheck(opt *Option) string { func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile { interfaces := setInterfaces(rules) - p := &aa.AppArmorProfile{} + profile := &aa.AppArmorProfile{} + p := profile.GetDefaultProfile() p.Rules = append(p.Rules, &aa.Dbus{ Access: "bind", Bus: rules["bus"], Name: rules["name"], }) @@ -127,12 +128,13 @@ func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile { Member: "Introspect", PeerName: `":1.@{int}"`, }) - return p + return profile } func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile { interfaces := setInterfaces(rules) - p := &aa.AppArmorProfile{} + profile := &aa.AppArmorProfile{} + p := profile.GetDefaultProfile() for _, iface := range interfaces { p.Rules = append(p.Rules, &aa.Dbus{ Access: "send", @@ -153,5 +155,5 @@ func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile { PeerLabel: rules["label"], }) } - return p + return profile } diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index db08ba6e..e622d7af 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -27,7 +27,7 @@ func init() { }) } -func (d Exec) Apply(opt *Option, profile string) string { +func (d Exec) Apply(opt *Option, profileRaw string) string { transition := "Px" transitions := []string{"P", "U", "p", "u", "PU", "pu"} t := opt.ArgList[0] @@ -36,7 +36,8 @@ func (d Exec) Apply(opt *Option, profile string) string { delete(opt.ArgMap, t) } - p := &aa.AppArmorProfile{} + profile := &aa.AppArmorProfile{} + p := profile.GetDefaultProfile() for name := range opt.ArgMap { profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name)) dstProfile := aa.DefaultTunables() @@ -53,9 +54,9 @@ func (d Exec) Apply(opt *Option, profile string) string { } } } - p.Sort() - rules := p.String() + profile.Sort() + rules := profile.String() lenRules := len(rules) rules = rules[:lenRules-1] - return strings.Replace(profile, opt.Raw, rules, -1) + return strings.Replace(profileRaw, opt.Raw, rules, -1) } From 890275fb22f82efc6edd38edbe518abb04fdd1b0 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 16 Apr 2024 21:51:56 +0100 Subject: [PATCH 05/62] feat(aa): rename the main profile struct. --- pkg/aa/apparmor.go | 214 ++++++++++++++++++ pkg/aa/{profile_test.go => apparmor_test.go} | 54 ++--- pkg/aa/profile.go | 221 +------------------ pkg/aa/rules.go | 8 + pkg/aa/templates/profile.j2 | 8 +- pkg/aa/variables.go | 26 +-- pkg/aa/variables_test.go | 4 +- pkg/logs/logs.go | 6 +- pkg/logs/logs_test.go | 8 +- pkg/prebuild/directive/dbus.go | 10 +- pkg/prebuild/directive/exec.go | 2 +- 11 files changed, 287 insertions(+), 274 deletions(-) create mode 100644 pkg/aa/apparmor.go rename pkg/aa/{profile_test.go => apparmor_test.go} (90%) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go new file mode 100644 index 00000000..db0ca8b9 --- /dev/null +++ b/pkg/aa/apparmor.go @@ -0,0 +1,214 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "bytes" + "reflect" + "slices" + "sort" + "strings" + + "github.com/arduino/go-paths-helper" +) + +// Default Apparmor magic directory: /etc/apparmor.d/. +var MagicRoot = paths.New("/etc/apparmor.d") + +// AppArmorProfileFiles represents a full set of apparmor profiles +type AppArmorProfileFiles map[string]*AppArmorProfileFile + +// AppArmorProfileFile represents a full apparmor profile file. +// Warning: close to the BNF grammar of apparmor profile but not exactly the same (yet): +// - Some rules are not supported yet (subprofile, hat...) +// - The structure is simplified as it only aims at writing profile, not parsing it. +type AppArmorProfileFile struct { + Preamble + Profiles []*Profile +} + +// Preamble section of a profile file, +type Preamble struct { + Abi []*Abi + Includes []*Include + Aliases []*Alias + Variables []*Variable +} + +func NewAppArmorProfile() *AppArmorProfileFile { + return &AppArmorProfileFile{} +} + +// String returns the formatted representation of a profile as a string +func (f *AppArmorProfileFile) String() string { + var res bytes.Buffer + err := tmplAppArmorProfile.Execute(&res, f) + if err != nil { + return err.Error() + } + return res.String() +} + +// GetDefaultProfile ensure a profile is always present in the profile file and +// return it, as a default profile. +func (f *AppArmorProfileFile) GetDefaultProfile() *Profile { + if len(f.Profiles) == 0 { + f.Profiles = append(f.Profiles, &Profile{}) + } + return f.Profiles[0] +} + +// AddRule adds a new rule to the profile from a log map +// See utils/apparmor/logparser.py for the format of the log map +func (f *AppArmorProfileFile) AddRule(log map[string]string) { + p := f.GetDefaultProfile() + + // Generate profile flags and extra rules + switch log["error"] { + case "-2": + if !slices.Contains(p.Flags, "mediate_deleted") { + p.Flags = append(p.Flags, "mediate_deleted") + } + case "-13": + if strings.Contains(log["info"], "namespace creation restricted") { + p.Rules = append(p.Rules, newUsernsFromLog(log)) + } else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") { + p.Flags = append(p.Flags, "attach_disconnected") + } + default: + } + + switch log["class"] { + case "cap": + p.Rules = append(p.Rules, newCapabilityFromLog(log)) + case "net": + if log["family"] == "unix" { + p.Rules = append(p.Rules, newUnixFromLog(log)) + } else { + p.Rules = append(p.Rules, newNetworkFromLog(log)) + } + case "mount": + if strings.Contains(log["flags"], "remount") { + p.Rules = append(p.Rules, newRemountFromLog(log)) + } else { + switch log["operation"] { + case "mount": + p.Rules = append(p.Rules, newMountFromLog(log)) + case "umount": + p.Rules = append(p.Rules, newUmountFromLog(log)) + case "remount": + p.Rules = append(p.Rules, newRemountFromLog(log)) + case "pivotroot": + p.Rules = append(p.Rules, newPivotRootFromLog(log)) + } + } + case "posix_mqueue", "sysv_mqueue": + p.Rules = append(p.Rules, newMqueueFromLog(log)) + case "signal": + p.Rules = append(p.Rules, newSignalFromLog(log)) + case "ptrace": + p.Rules = append(p.Rules, newPtraceFromLog(log)) + case "namespace": + p.Rules = append(p.Rules, newUsernsFromLog(log)) + case "unix": + p.Rules = append(p.Rules, newUnixFromLog(log)) + case "dbus": + p.Rules = append(p.Rules, newDbusFromLog(log)) + case "file": + if log["operation"] == "change_onexec" { + p.Rules = append(p.Rules, newChangeProfileFromLog(log)) + } else { + p.Rules = append(p.Rules, newFileFromLog(log)) + } + default: + if strings.Contains(log["operation"], "dbus") { + p.Rules = append(p.Rules, newDbusFromLog(log)) + } else if log["family"] == "unix" { + p.Rules = append(p.Rules, newUnixFromLog(log)) + } + } +} + +// Sort the rules in the profile +// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines +func (f *AppArmorProfileFile) Sort() { + for _, p := range f.Profiles { + sort.Slice(p.Rules, func(i, j int) bool { + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + if typeOfI != typeOfJ { + valueOfI := typeToValue(typeOfI) + valueOfJ := typeToValue(typeOfJ) + if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists { + valueOfI = "include_if_exists" + } + if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists { + valueOfJ = "include_if_exists" + } + return ruleWeights[valueOfI] < ruleWeights[valueOfJ] + } + return p.Rules[i].Less(p.Rules[j]) + }) + } +} + +// MergeRules merge similar rules together. +// Steps: +// - Remove identical rules +// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw' +// +// Note: logs.regCleanLogs helps a lot to do a first cleaning +func (f *AppArmorProfileFile) MergeRules() { + for _, p := range f.Profiles { + for i := 0; i < len(p.Rules); i++ { + for j := i + 1; j < len(p.Rules); j++ { + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + if typeOfI != typeOfJ { + continue + } + + // If rules are identical, merge them + if p.Rules[i].Equals(p.Rules[j]) { + p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) + j-- + } + } + } + } +} + +// Format the profile for better readability before printing it. +// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block +func (f *AppArmorProfileFile) Format() { + const prefixOwner = " " + for _, p := range f.Profiles { + hasOwnerRule := false + for i := len(p.Rules) - 1; i > 0; i-- { + j := i - 1 + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + + // File rule + if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) { + letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) + letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) + + // Add prefix before rule path to align with other rule + if p.Rules[i].(*File).Owner { + hasOwnerRule = true + } else if hasOwnerRule { + p.Rules[i].(*File).Prefix = prefixOwner + } + + if letterI != letterJ { + // Add a new empty line between Files rule of different type + hasOwnerRule = false + p.Rules = append(p.Rules[:i], append([]ApparmorRule{&Rule{}}, p.Rules[i:]...)...) + } + } + } + } +} diff --git a/pkg/aa/profile_test.go b/pkg/aa/apparmor_test.go similarity index 90% rename from pkg/aa/profile_test.go rename to pkg/aa/apparmor_test.go index 9866cf89..2f89737b 100644 --- a/pkg/aa/profile_test.go +++ b/pkg/aa/apparmor_test.go @@ -31,17 +31,17 @@ func readprofile(path string) string { func TestAppArmorProfile_String(t *testing.T) { tests := []struct { name string - p *AppArmorProfile + f *AppArmorProfileFile want string }{ { name: "empty", - p: &AppArmorProfile{}, + f: &AppArmorProfileFile{}, want: ``, }, { name: "foo", - p: &AppArmorProfile{ + f: &AppArmorProfileFile{ Preamble: Preamble{ Abi: []*Abi{{IsMagic: true, Path: "abi/4.0"}}, Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, @@ -117,7 +117,7 @@ func TestAppArmorProfile_String(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.p.String(); got != tt.want { + if got := tt.f.String(); got != tt.want { t.Errorf("AppArmorProfile.String() = |%v|, want |%v|", got, tt.want) } }) @@ -128,12 +128,12 @@ func TestAppArmorProfile_AddRule(t *testing.T) { tests := []struct { name string log map[string]string - want *AppArmorProfile + want *AppArmorProfileFile }{ { name: "capability", log: capability1Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{capability1}, }}, @@ -142,7 +142,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { { name: "network", log: network1Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{network1}, }}, @@ -151,7 +151,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { { name: "mount", log: mount2Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{mount2}, }}, @@ -160,7 +160,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { { name: "signal", log: signal1Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{signal1}, }}, @@ -169,7 +169,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { { name: "ptrace", log: ptrace2Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{ptrace2}, }}, @@ -178,7 +178,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { { name: "unix", log: unix1Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{unix1}, }}, @@ -187,7 +187,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { { name: "dbus", log: dbus2Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{dbus2}, }}, @@ -196,7 +196,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { { name: "file", log: file2Log, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{file2}, }}, @@ -217,12 +217,12 @@ func TestAppArmorProfile_AddRule(t *testing.T) { func TestAppArmorProfile_Sort(t *testing.T) { tests := []struct { name string - origin *AppArmorProfile - want *AppArmorProfile + origin *AppArmorProfileFile + want *AppArmorProfileFile }{ { name: "all", - origin: &AppArmorProfile{ + origin: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{ file2, network1, includeLocal1, dbus2, signal1, ptrace1, @@ -230,7 +230,7 @@ func TestAppArmorProfile_Sort(t *testing.T) { }, }}, }, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{ capability2, network1, mount2, signal1, signal2, ptrace1, @@ -254,17 +254,17 @@ func TestAppArmorProfile_Sort(t *testing.T) { func TestAppArmorProfile_MergeRules(t *testing.T) { tests := []struct { name string - origin *AppArmorProfile - want *AppArmorProfile + origin *AppArmorProfileFile + want *AppArmorProfileFile }{ { name: "all", - origin: &AppArmorProfile{ + origin: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{capability1, capability1, network1, network1, file1, file1}, }}, }, - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []ApparmorRule{capability1, network1, file1}, }}, @@ -285,12 +285,12 @@ func TestAppArmorProfile_MergeRules(t *testing.T) { func TestAppArmorProfile_Integration(t *testing.T) { tests := []struct { name string - p *AppArmorProfile + f *AppArmorProfileFile want string }{ { name: "aa-status", - p: &AppArmorProfile{ + f: &AppArmorProfileFile{ Preamble: Preamble{ Abi: []*Abi{{IsMagic: true, Path: "abi/3.0"}}, Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, @@ -327,10 +327,10 @@ func TestAppArmorProfile_Integration(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.p.Sort() - tt.p.MergeRules() - tt.p.Format() - if got := tt.p.String(); "\n"+got != tt.want { + tt.f.Sort() + tt.f.MergeRules() + tt.f.Format() + if got := tt.f.String(); "\n"+got != tt.want { t.Errorf("AppArmorProfile = |%v|, want |%v|", "\n"+got, tt.want) } }) diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 54f288dd..4091dc5e 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -5,46 +5,19 @@ package aa import ( - "bytes" "maps" - "reflect" "slices" - "sort" "strings" - - "github.com/arduino/go-paths-helper" ) -// Default Apparmor magic directory: /etc/apparmor.d/. -var MagicRoot = paths.New("/etc/apparmor.d") - -// AppArmorProfiles represents a full set of apparmor profiles -type AppArmorProfiles map[string]*AppArmorProfile - -// ApparmorProfile represents a full apparmor profile file. -// Warning: close to the BNF grammar of apparmor profile but not exactly the same (yet): -// - Some rules are not supported yet (subprofile, hat...) -// - The structure is simplified as it only aims at writing profile, not parsing it. -type AppArmorProfile struct { - Preamble - Profiles []*Profile -} - -// Preamble section of a profile file, -type Preamble struct { - Abi []*Abi - Includes []*Include - Aliases []*Alias - Variables []*Variable -} - -// Profile represent a single AppArmor profile. +// Profile represents a single AppArmor profile. type Profile struct { Rule Header Rules Rules } +// Header represents the header of a profile. type Header struct { Name string Attachments []string @@ -53,7 +26,11 @@ type Header struct { } func (r *Profile) Less(other any) bool { - return false // TBD + o, _ := other.(*Profile) + if r.Name != o.Name { + return r.Name < o.Name + } + return len(r.Attachments) < len(o.Attachments) } func (r *Profile) Equals(other any) bool { @@ -62,187 +39,3 @@ func (r *Profile) Equals(other any) bool { maps.Equal(r.Attributes, o.Attributes) && slices.Equal(r.Flags, o.Flags) } - -// ApparmorRule generic interface -type ApparmorRule interface { - Less(other any) bool - Equals(other any) bool -} - -type Rules []ApparmorRule - -func NewAppArmorProfile() *AppArmorProfile { - return &AppArmorProfile{} -} - -// String returns the formatted representation of a profile as a string -func (p *AppArmorProfile) String() string { - var res bytes.Buffer - err := tmplAppArmorProfile.Execute(&res, p) - if err != nil { - return err.Error() - } - return res.String() -} - -// GetDefaultProfile ensure a profile is always present in the profile file and -// return it, as a default profile. -func (p *AppArmorProfile) GetDefaultProfile() *Profile { - if len(p.Profiles) == 0 { - p.Profiles = append(p.Profiles, &Profile{}) - } - return p.Profiles[0] -} - -// AddRule adds a new rule to the profile from a log map -// See utils/apparmor/logparser.py for the format of the log map -func (profile *AppArmorProfile) AddRule(log map[string]string) { - p := profile.GetDefaultProfile() - - // Generate profile flags and extra rules - switch log["error"] { - case "-2": - if !slices.Contains(p.Flags, "mediate_deleted") { - p.Flags = append(p.Flags, "mediate_deleted") - } - case "-13": - if strings.Contains(log["info"], "namespace creation restricted") { - p.Rules = append(p.Rules, newUsernsFromLog(log)) - } else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") { - p.Flags = append(p.Flags, "attach_disconnected") - } - default: - } - - switch log["class"] { - case "cap": - p.Rules = append(p.Rules, newCapabilityFromLog(log)) - case "net": - if log["family"] == "unix" { - p.Rules = append(p.Rules, newUnixFromLog(log)) - } else { - p.Rules = append(p.Rules, newNetworkFromLog(log)) - } - case "mount": - if strings.Contains(log["flags"], "remount") { - p.Rules = append(p.Rules, newRemountFromLog(log)) - } else { - switch log["operation"] { - case "mount": - p.Rules = append(p.Rules, newMountFromLog(log)) - case "umount": - p.Rules = append(p.Rules, newUmountFromLog(log)) - case "remount": - p.Rules = append(p.Rules, newRemountFromLog(log)) - case "pivotroot": - p.Rules = append(p.Rules, newPivotRootFromLog(log)) - } - } - case "posix_mqueue", "sysv_mqueue": - p.Rules = append(p.Rules, newMqueueFromLog(log)) - case "signal": - p.Rules = append(p.Rules, newSignalFromLog(log)) - case "ptrace": - p.Rules = append(p.Rules, newPtraceFromLog(log)) - case "namespace": - p.Rules = append(p.Rules, newUsernsFromLog(log)) - case "unix": - p.Rules = append(p.Rules, newUnixFromLog(log)) - case "dbus": - p.Rules = append(p.Rules, newDbusFromLog(log)) - case "file": - if log["operation"] == "change_onexec" { - p.Rules = append(p.Rules, newChangeProfileFromLog(log)) - } else { - p.Rules = append(p.Rules, newFileFromLog(log)) - } - default: - if strings.Contains(log["operation"], "dbus") { - p.Rules = append(p.Rules, newDbusFromLog(log)) - } else if log["family"] == "unix" { - p.Rules = append(p.Rules, newUnixFromLog(log)) - } - } -} - -// Sort the rules in the profile -// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines -func (profile *AppArmorProfile) Sort() { - for _, p := range profile.Profiles { - sort.Slice(p.Rules, func(i, j int) bool { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - valueOfI := typeToValue(typeOfI) - valueOfJ := typeToValue(typeOfJ) - if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists { - valueOfI = "include_if_exists" - } - if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists { - valueOfJ = "include_if_exists" - } - return ruleWeights[valueOfI] < ruleWeights[valueOfJ] - } - return p.Rules[i].Less(p.Rules[j]) - }) - } -} - -// MergeRules merge similar rules together. -// Steps: -// - Remove identical rules -// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw' -// -// Note: logs.regCleanLogs helps a lot to do a first cleaning -func (profile *AppArmorProfile) MergeRules() { - for _, p := range profile.Profiles { - for i := 0; i < len(p.Rules); i++ { - for j := i + 1; j < len(p.Rules); j++ { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - continue - } - - // If rules are identical, merge them - if p.Rules[i].Equals(p.Rules[j]) { - p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) - j-- - } - } - } - } -} - -// Format the profile for better readability before printing it. -// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block -func (profile *AppArmorProfile) Format() { - const prefixOwner = " " - for _, p := range profile.Profiles { - hasOwnerRule := false - for i := len(p.Rules) - 1; i > 0; i-- { - j := i - 1 - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - - // File rule - if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) { - letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) - letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) - - // Add prefix before rule path to align with other rule - if p.Rules[i].(*File).Owner { - hasOwnerRule = true - } else if hasOwnerRule { - p.Rules[i].(*File).Prefix = prefixOwner - } - - if letterI != letterJ { - // Add a new empty line between Files rule of different type - hasOwnerRule = false - p.Rules = append(p.Rules[:i], append([]ApparmorRule{&Rule{}}, p.Rules[i:]...)...) - } - } - } - } -} diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index a09aac40..dad738ba 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -9,6 +9,14 @@ import ( "strings" ) +// ApparmorRule generic interface +type ApparmorRule interface { + Less(other any) bool + Equals(other any) bool +} + +type Rules []ApparmorRule + type Rule struct { Comment string NoNewPrivs bool diff --git a/pkg/aa/templates/profile.j2 b/pkg/aa/templates/profile.j2 index 9875587c..1c4cb287 100644 --- a/pkg/aa/templates/profile.j2 +++ b/pkg/aa/templates/profile.j2 @@ -4,7 +4,7 @@ {{- define "profile" -}} - {{- if or .Name .Attachments .Attributes .Flags -}} + {{- with .Header -}} {{- "profile" -}} {{- with .Name -}} {{ " " }}{{ . }} @@ -18,9 +18,7 @@ {{- with .Flags -}} {{ " flags=(" }}{{ join . }}{{ ")" }} {{- end -}} - {{ " {" }} - {{- template "comment" . -}} - {{- "\n" -}} + {{- "{\n" -}} {{- end -}} {{- $oldtype := "" -}} @@ -296,7 +294,7 @@ {{- $oldtype = $type -}} {{- end -}} - {{- if or .Name .Attachments .Attributes .Flags -}} + {{- with .Header -}} {{- "}\n" -}} {{- end -}} diff --git a/pkg/aa/variables.go b/pkg/aa/variables.go index cd34bd1a..684564a2 100644 --- a/pkg/aa/variables.go +++ b/pkg/aa/variables.go @@ -19,8 +19,8 @@ var ( // DefaultTunables return a minimal working profile to build the profile // It should not be used when loading file from /etc/apparmor.d -func DefaultTunables() *AppArmorProfile { - return &AppArmorProfile{ +func DefaultTunables() *AppArmorProfileFile { + return &AppArmorProfileFile{ Preamble: Preamble{ Variables: []*Variable{ {Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, @@ -36,41 +36,41 @@ func DefaultTunables() *AppArmorProfile { } // ParseVariables extract all variables from the profile -func (p *AppArmorProfile) ParseVariables(content string) { +func (f *AppArmorProfileFile) ParseVariables(content string) { matches := regVariablesDef.FindAllStringSubmatch(content, -1) for _, match := range matches { if len(match) > 2 { key := match[1] values := strings.Split(match[2], " ") found := false - for idx, variable := range p.Variables { + for idx, variable := range f.Variables { if variable.Name == key { - p.Variables[idx].Values = append(p.Variables[idx].Values, values...) + f.Variables[idx].Values = append(f.Variables[idx].Values, values...) found = true break } } if !found { variable := &Variable{Name: key, Values: values} - p.Variables = append(p.Variables, variable) + f.Variables = append(f.Variables, variable) } } } } // resolve recursively resolves all variables references -func (p *AppArmorProfile) resolve(str string) []string { +func (f *AppArmorProfileFile) resolve(str string) []string { if strings.Contains(str, "@{") { vars := []string{} match := regVariablesRef.FindStringSubmatch(str) if len(match) > 1 { variable := match[0] varname := match[1] - for _, vrbl := range p.Variables { + for _, vrbl := range f.Variables { if vrbl.Name == varname { for _, value := range vrbl.Values { newVar := strings.ReplaceAll(str, variable, value) - vars = append(vars, p.resolve(newVar)...) + vars = append(vars, f.resolve(newVar)...) } } } @@ -83,8 +83,8 @@ func (p *AppArmorProfile) resolve(str string) []string { } // ResolveAttachments resolve profile attachments defined in exec_path -func (profile *AppArmorProfile) ResolveAttachments() { - p := profile.GetDefaultProfile() +func (f *AppArmorProfileFile) ResolveAttachments() { + p := f.GetDefaultProfile() for _, variable := range profile.Variables { if variable.Name == "exec_path" { @@ -100,8 +100,8 @@ func (profile *AppArmorProfile) ResolveAttachments() { } // NestAttachments return a nested attachment string -func (profile *AppArmorProfile) NestAttachments() string { - p := profile.GetDefaultProfile() +func (f *AppArmorProfileFile) NestAttachments() string { + p := f.GetDefaultProfile() if len(p.Attachments) == 0 { return "" } else if len(p.Attachments) == 1 { diff --git a/pkg/aa/variables_test.go b/pkg/aa/variables_test.go index 2232e65a..778b3380 100644 --- a/pkg/aa/variables_test.go +++ b/pkg/aa/variables_test.go @@ -16,11 +16,11 @@ import ( func TestDefaultTunables(t *testing.T) { tests := []struct { name string - want *AppArmorProfile + want *AppArmorProfileFile }{ { name: "aa", - want: &AppArmorProfile{ + want: &AppArmorProfileFile{ Preamble: Preamble{ Variables: []*Variable{ {Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go index 5b2ea712..f14aefd4 100644 --- a/pkg/logs/logs.go +++ b/pkg/logs/logs.go @@ -197,8 +197,8 @@ func (aaLogs AppArmorLogs) String() string { } // ParseToProfiles convert the log data into a new AppArmorProfiles -func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles { - profiles := make(aa.AppArmorProfiles, 0) +func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfileFiles { + profiles := make(aa.AppArmorProfileFiles, 0) for _, log := range aaLogs { name := "" if strings.Contains(log["operation"], "dbus") { @@ -208,7 +208,7 @@ func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles { } if _, ok := profiles[name]; !ok { - profile := &aa.AppArmorProfile{ + profile := &aa.AppArmorProfileFile{ Profiles: []*aa.Profile{{Header: aa.Header{Name: name}}}, } profile.AddRule(log) diff --git a/pkg/logs/logs_test.go b/pkg/logs/logs_test.go index e970da5f..462e8417 100644 --- a/pkg/logs/logs_test.go +++ b/pkg/logs/logs_test.go @@ -292,13 +292,13 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { tests := []struct { name string aaLogs AppArmorLogs - want aa.AppArmorProfiles + want aa.AppArmorProfileFiles }{ { name: "", aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...), - want: aa.AppArmorProfiles{ - "kmod": &aa.AppArmorProfile{ + want: aa.AppArmorProfileFiles{ + "kmod": &aa.AppArmorProfileFile{ Profiles: []*aa.Profile{{ Header: aa.Header{Name: "kmod"}, Rules: aa.Rules{ @@ -317,7 +317,7 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { }, }}, }, - "power-profiles-daemon": &aa.AppArmorProfile{ + "power-profiles-daemon": &aa.AppArmorProfileFile{ Profiles: []*aa.Profile{{ Header: aa.Header{Name: "power-profiles-daemon"}, Rules: aa.Rules{ diff --git a/pkg/prebuild/directive/dbus.go b/pkg/prebuild/directive/dbus.go index 98c2306f..9b93ec8e 100644 --- a/pkg/prebuild/directive/dbus.go +++ b/pkg/prebuild/directive/dbus.go @@ -51,7 +51,7 @@ func setInterfaces(rules map[string]string) []string { } func (d Dbus) Apply(opt *Option, profile string) string { - var p *aa.AppArmorProfile + var p *aa.AppArmorProfileFile action := d.sanityCheck(opt) switch action { @@ -95,9 +95,9 @@ func (d Dbus) sanityCheck(opt *Option) string { return action } -func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile { +func (d Dbus) own(rules map[string]string) *aa.AppArmorProfileFile { interfaces := setInterfaces(rules) - profile := &aa.AppArmorProfile{} + profile := &aa.AppArmorProfileFile{} p := profile.GetDefaultProfile() p.Rules = append(p.Rules, &aa.Dbus{ Access: "bind", Bus: rules["bus"], Name: rules["name"], @@ -131,9 +131,9 @@ func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile { return profile } -func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile { +func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfileFile { interfaces := setInterfaces(rules) - profile := &aa.AppArmorProfile{} + profile := &aa.AppArmorProfileFile{} p := profile.GetDefaultProfile() for _, iface := range interfaces { p.Rules = append(p.Rules, &aa.Dbus{ diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index e622d7af..9296ca96 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -36,7 +36,7 @@ func (d Exec) Apply(opt *Option, profileRaw string) string { delete(opt.ArgMap, t) } - profile := &aa.AppArmorProfile{} + profile := &aa.AppArmorProfileFile{} p := profile.GetDefaultProfile() for name := range opt.ArgMap { profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name)) From 8ef858ad352c2d81990023cd593489ced3b63670 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Wed, 17 Apr 2024 18:02:41 +0100 Subject: [PATCH 06/62] feat(aa): refractor template to allow multiple templates. --- pkg/aa/apparmor.go | 2 +- pkg/aa/template.go | 31 ++- pkg/aa/templates/{file.j2 => apparmor.j2} | 0 pkg/aa/templates/profile.j2 | 228 ++-------------------- pkg/aa/templates/rule/capability.j2 | 7 + pkg/aa/templates/rule/change_profile.j2 | 19 ++ pkg/aa/templates/{ => rule}/comment.j2 | 4 + pkg/aa/templates/rule/dbus.j2 | 40 ++++ pkg/aa/templates/rule/file.j2 | 21 ++ pkg/aa/templates/{ => rule}/include.j2 | 4 + pkg/aa/templates/rule/mount.j2 | 54 +++++ pkg/aa/templates/rule/mqueue.j2 | 22 +++ pkg/aa/templates/rule/network.j2 | 20 ++ pkg/aa/templates/rule/pivot_root.j2 | 19 ++ pkg/aa/templates/rule/ptrace.j2 | 16 ++ pkg/aa/templates/{ => rule}/qualifier.j2 | 4 + pkg/aa/templates/rule/rlimit.j2 | 7 + pkg/aa/templates/rule/signal.j2 | 19 ++ pkg/aa/templates/rule/unix.j2 | 35 ++++ pkg/aa/templates/rule/userns.j2 | 9 + 20 files changed, 347 insertions(+), 214 deletions(-) rename pkg/aa/templates/{file.j2 => apparmor.j2} (100%) create mode 100644 pkg/aa/templates/rule/capability.j2 create mode 100644 pkg/aa/templates/rule/change_profile.j2 rename pkg/aa/templates/{ => rule}/comment.j2 (69%) create mode 100644 pkg/aa/templates/rule/dbus.j2 create mode 100644 pkg/aa/templates/rule/file.j2 rename pkg/aa/templates/{ => rule}/include.j2 (61%) create mode 100644 pkg/aa/templates/rule/mount.j2 create mode 100644 pkg/aa/templates/rule/mqueue.j2 create mode 100644 pkg/aa/templates/rule/network.j2 create mode 100644 pkg/aa/templates/rule/pivot_root.j2 create mode 100644 pkg/aa/templates/rule/ptrace.j2 rename pkg/aa/templates/{ => rule}/qualifier.j2 (56%) create mode 100644 pkg/aa/templates/rule/rlimit.j2 create mode 100644 pkg/aa/templates/rule/signal.j2 create mode 100644 pkg/aa/templates/rule/unix.j2 create mode 100644 pkg/aa/templates/rule/userns.j2 diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index db0ca8b9..b3a14ccf 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -44,7 +44,7 @@ func NewAppArmorProfile() *AppArmorProfileFile { // String returns the formatted representation of a profile as a string func (f *AppArmorProfileFile) String() string { var res bytes.Buffer - err := tmplAppArmorProfile.Execute(&res, f) + err := tmpl["apparmor"].Execute(&res, f) if err != nil { return err.Error() } diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 88235e8c..2a4d0e0a 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -6,6 +6,7 @@ package aa import ( "embed" + "fmt" "reflect" "strings" "text/template" @@ -26,8 +27,10 @@ var ( "overindent": indentDbus, } - // The apparmor profile template - tmplAppArmorProfile = generateTemplate() + // The apparmor templates + tmpl = map[string]*template.Template{ + "apparmor": generateTemplate("apparmor.j2"), + } // convert apparmor requested mask to apparmor access mode requestedMaskToAccess = map[string]string{ @@ -96,9 +99,27 @@ var ( fileWeights = map[string]int{} ) -func generateTemplate() *template.Template { - res := template.New("file.j2").Funcs(tmplFunctionMap) - res = template.Must(res.ParseFS(tmplFiles, "templates/*.j2")) +func generateTemplate(name string) *template.Template { + res := template.New(name).Funcs(tmplFunctionMap) + switch name { + case "apparmor.j2": + res = template.Must(res.ParseFS(tmplFiles, + "templates/*.j2", "templates/rule/*.j2", + )) + case "profile.j2": + res = template.Must(res.Parse("{{ template \"profile\" . }}")) + res = template.Must(res.ParseFS(tmplFiles, + "templates/profile.j2", "templates/rule/*.j2", + )) + default: + res = template.Must(res.Parse( + fmt.Sprintf("{{ template \"%s\" . }}", name), + )) + res = template.Must(res.ParseFS(tmplFiles, + fmt.Sprintf("templates/rule/%s.j2", name), + "templates/rule/qualifier.j2", "templates/rule/comment.j2", + )) + } return res } diff --git a/pkg/aa/templates/file.j2 b/pkg/aa/templates/apparmor.j2 similarity index 100% rename from pkg/aa/templates/file.j2 rename to pkg/aa/templates/apparmor.j2 diff --git a/pkg/aa/templates/profile.j2 b/pkg/aa/templates/profile.j2 index 1c4cb287..394f18a1 100644 --- a/pkg/aa/templates/profile.j2 +++ b/pkg/aa/templates/profile.j2 @@ -18,13 +18,14 @@ {{- with .Flags -}} {{ " flags=(" }}{{ join . }}{{ ")" }} {{- end -}} - {{- "{\n" -}} + {{- " {\n" -}} {{- end -}} {{- $oldtype := "" -}} {{- range .Rules -}} {{- $type := typeof . -}} {{- if eq $type "Rule" -}} + {{- template "comment" . -}} {{- "\n" -}} {{- continue -}} {{- end -}} @@ -38,252 +39,63 @@ {{- end -}} {{- if eq $type "Rlimit" -}} - {{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }}{{ template "comment" . }} + {{- template "rlimit" . -}} {{- end -}} {{- if eq $type "Userns" -}} - {{- if .Create -}} - {{ template "qualifier" . }}{{ "userns," }}{{ template "comment" . }} - {{- end -}} + {{- template "userns" . -}} {{- end -}} {{- if eq $type "Capability" -}} - {{ template "qualifier" . }}{{ "capability " }}{{ .Name }}{{ "," }}{{ template "comment" . }} + {{- template "capability" . -}} {{- end -}} {{- if eq $type "Network" -}} - {{- template "qualifier" . -}} - {{ "network" }} - {{- with .Domain -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Type -}} - {{ " " }}{{ . }} - {{- else -}} - {{- with .Protocol -}} - {{ " " }}{{ . }} - {{- end -}} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "network" . -}} {{- end -}} {{- if eq $type "Mount" -}} - {{- template "qualifier" . -}} - {{- "mount" -}} - {{- with .FsType -}} - {{ " fstype=" }}{{ . }} - {{- end -}} - {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .Source -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .MountPoint -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} - {{- end -}} - - {{- if eq $type "Umount" -}} - {{- template "qualifier" . -}} - {{- "umount" -}} - {{- with .FsType -}} - {{ " fstype=" }}{{ . }} - {{- end -}} - {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .MountPoint -}} - {{ " " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "mount" . -}} {{- end -}} {{- if eq $type "Remount" -}} - {{- template "qualifier" . -}} - {{- "remount" -}} - {{- with .FsType -}} - {{ " fstype=" }}{{ . }} - {{- end -}} - {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .MountPoint -}} - {{ " " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "remount" . -}} + {{- end -}} + + {{- if eq $type "Umount" -}} + {{- template "umount" . -}} {{- end -}} {{- if eq $type "PivotRoot" -}} - {{- template "qualifier" . -}} - {{- "pivot_root" -}} - {{- with .OldRoot -}} - {{ " oldroot=" }}{{ . }} - {{- end -}} - {{- with .NewRoot -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .TargetProfile -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "pivot_root" . -}} {{- end -}} {{- if eq $type "ChangeProfile" -}} - {{- template "qualifier" . -}} - {{- "change_profile" -}} - {{- with .ExecMode -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Exec -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .ProfileName -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "change_profile" . -}} {{- end -}} {{- if eq $type "Mqueue" -}} - {{- template "qualifier" . -}} - {{- "mqueue" -}} - {{- with .Access -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Type -}} - {{ " type=" }}{{ . }} - {{- end -}} - {{- with .Label -}} - {{ " label=" }}{{ . }} - {{- end -}} - {{- with .Name -}} - {{ " " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "mqueue" . -}} {{- end -}} {{- if eq $type "Unix" -}} - {{- template "qualifier" . -}} - {{- "unix" -}} - {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Type -}} - {{ " type=" }}{{ . }} - {{- end -}} - {{- with .Protocol -}} - {{ " protocol=" }}{{ . }} - {{- end -}} - {{- with .Address -}} - {{ " addr=" }}{{ . }} - {{- end -}} - {{- with .Label -}} - {{ " label=" }}{{ . }} - {{- end -}} - {{- if and .PeerLabel .PeerAddr -}} - {{ " peer=(label=" }}{{ .PeerLabel }}{{ ", addr="}}{{ .PeerAddr }}{{ ")" }} - {{- else -}} - {{- with .PeerLabel -}} - {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .PeerAddr -}} - {{ overindent "peer=(addr=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "unix" . -}} {{- end -}} {{- if eq $type "Ptrace" -}} - {{- template "qualifier" . -}} - {{- "ptrace" -}} - {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Peer -}} - {{ " peer=" }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "ptrace" . -}} {{- end -}} {{- if eq $type "Signal" -}} - {{- template "qualifier" . -}} - {{- "signal" -}} - {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Set -}} - {{ " set=(" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .Peer -}} - {{ " peer=" }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "signal" . -}} {{- end -}} {{- if eq $type "Dbus" -}} - {{- template "qualifier" . -}} - {{- "dbus" -}} - {{- if eq .Access "bind" -}} - {{ " bind bus=" }}{{ .Bus }}{{ " name=" }}{{ .Name }} - {{- else -}} - {{- with .Access -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Bus -}} - {{ " bus=" }}{{ . }} - {{- end -}} - {{- with .Path -}} - {{ " path=" }}{{ . }} - {{- end -}} - {{ "\n" }} - {{- with .Interface -}} - {{ overindent "interface=" }}{{ . }}{{ "\n" }} - {{- end -}} - {{- with .Member -}} - {{ overindent "member=" }}{{ . }}{{ "\n" }} - {{- end -}} - {{- if and .PeerName .PeerLabel -}} - {{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }} - {{- else -}} - {{- with .PeerName -}} - {{ overindent "peer=(name=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- with .PeerLabel -}} - {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} - {{- end -}} - {{- end -}} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "dbus" . -}} {{- end -}} {{- if eq $type "File" -}} - {{- template "qualifier" . -}} - {{- if .Owner -}} - {{- "owner " -}} - {{- end -}} - {{- .Path -}} - {{- " " -}} - {{- with .Padding -}} - {{ . }} - {{- end -}} - {{- .Access -}} - {{- with .Target -}} - {{ " -> " }}{{ . }} - {{- end -}} - {{- "," -}} - {{- template "comment" . -}} + {{- template "file" . -}} {{- end -}} {{- if eq $type "Profile" -}} diff --git a/pkg/aa/templates/rule/capability.j2 b/pkg/aa/templates/rule/capability.j2 new file mode 100644 index 00000000..4041ab11 --- /dev/null +++ b/pkg/aa/templates/rule/capability.j2 @@ -0,0 +1,7 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "capability" -}} + {{ template "qualifier" . }}{{ "capability " }}{{ .Name }}{{ "," }}{{ template "comment" . }} +{{- end -}} diff --git a/pkg/aa/templates/rule/change_profile.j2 b/pkg/aa/templates/rule/change_profile.j2 new file mode 100644 index 00000000..a5e4e75f --- /dev/null +++ b/pkg/aa/templates/rule/change_profile.j2 @@ -0,0 +1,19 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "change_profile" -}} + {{- template "qualifier" . -}} + {{- "change_profile" -}} + {{- with .ExecMode -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Exec -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .ProfileName -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/comment.j2 b/pkg/aa/templates/rule/comment.j2 similarity index 69% rename from pkg/aa/templates/comment.j2 rename to pkg/aa/templates/rule/comment.j2 index ce7c30b9..68fc20a8 100644 --- a/pkg/aa/templates/comment.j2 +++ b/pkg/aa/templates/rule/comment.j2 @@ -1,3 +1,7 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + {{- define "comment" -}} {{- if or .FileInherit .NoNewPrivs .Optional .Comment -}} {{- " #" -}} diff --git a/pkg/aa/templates/rule/dbus.j2 b/pkg/aa/templates/rule/dbus.j2 new file mode 100644 index 00000000..a25b87ef --- /dev/null +++ b/pkg/aa/templates/rule/dbus.j2 @@ -0,0 +1,40 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "dbus" -}} + {{- template "qualifier" . -}} + {{- "dbus" -}} + {{- if eq .Access "bind" -}} + {{ " bind bus=" }}{{ .Bus }}{{ " name=" }}{{ .Name }} + {{- else -}} + {{- with .Access -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Bus -}} + {{ " bus=" }}{{ . }} + {{- end -}} + {{- with .Path -}} + {{ " path=" }}{{ . }} + {{- end -}} + {{ "\n" }} + {{- with .Interface -}} + {{ overindent "interface=" }}{{ . }}{{ "\n" }} + {{- end -}} + {{- with .Member -}} + {{ overindent "member=" }}{{ . }}{{ "\n" }} + {{- end -}} + {{- if and .PeerName .PeerLabel -}} + {{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }} + {{- else -}} + {{- with .PeerName -}} + {{ overindent "peer=(name=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .PeerLabel -}} + {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- end -}} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rule/file.j2 b/pkg/aa/templates/rule/file.j2 new file mode 100644 index 00000000..ea016e77 --- /dev/null +++ b/pkg/aa/templates/rule/file.j2 @@ -0,0 +1,21 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "file" -}} + {{- template "qualifier" . -}} + {{- if .Owner -}} + {{- "owner " -}} + {{- end -}} + {{- .Path -}} + {{- " " -}} + {{- with .Padding -}} + {{ . }} + {{- end -}} + {{- .Access -}} + {{- with .Target -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/include.j2 b/pkg/aa/templates/rule/include.j2 similarity index 61% rename from pkg/aa/templates/include.j2 rename to pkg/aa/templates/rule/include.j2 index fad5e9ca..b2dcb110 100644 --- a/pkg/aa/templates/include.j2 +++ b/pkg/aa/templates/rule/include.j2 @@ -1,3 +1,7 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + {{- define "include" -}} {{- "include" -}} {{- if .IfExists -}} diff --git a/pkg/aa/templates/rule/mount.j2 b/pkg/aa/templates/rule/mount.j2 new file mode 100644 index 00000000..19d29b13 --- /dev/null +++ b/pkg/aa/templates/rule/mount.j2 @@ -0,0 +1,54 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "mount" -}} + {{- template "qualifier" . -}} + {{- "mount" -}} + {{- with .FsType -}} + {{ " fstype=" }}{{ . }} + {{- end -}} + {{- with .Options -}} + {{ " options=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .Source -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .MountPoint -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} + +{{- define "remount" -}} + {{- template "qualifier" . -}} + {{- "remount" -}} + {{- with .FsType -}} + {{ " fstype=" }}{{ . }} + {{- end -}} + {{- with .Options -}} + {{ " options=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .MountPoint -}} + {{ " " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} + +{{- define "umount" -}} + {{- template "qualifier" . -}} + {{- "umount" -}} + {{- with .FsType -}} + {{ " fstype=" }}{{ . }} + {{- end -}} + {{- with .Options -}} + {{ " options=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .MountPoint -}} + {{ " " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rule/mqueue.j2 b/pkg/aa/templates/rule/mqueue.j2 new file mode 100644 index 00000000..48b764aa --- /dev/null +++ b/pkg/aa/templates/rule/mqueue.j2 @@ -0,0 +1,22 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "mqueue" -}} + {{- template "qualifier" . -}} + {{- "mqueue" -}} + {{- with .Access -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Type -}} + {{ " type=" }}{{ . }} + {{- end -}} + {{- with .Label -}} + {{ " label=" }}{{ . }} + {{- end -}} + {{- with .Name -}} + {{ " " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rule/network.j2 b/pkg/aa/templates/rule/network.j2 new file mode 100644 index 00000000..6f2503a8 --- /dev/null +++ b/pkg/aa/templates/rule/network.j2 @@ -0,0 +1,20 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "network" -}} + {{- template "qualifier" . -}} + {{ "network" }} + {{- with .Domain -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Type -}} + {{ " " }}{{ . }} + {{- else -}} + {{- with .Protocol -}} + {{ " " }}{{ . }} + {{- end -}} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/rule/pivot_root.j2 b/pkg/aa/templates/rule/pivot_root.j2 new file mode 100644 index 00000000..d779e2c1 --- /dev/null +++ b/pkg/aa/templates/rule/pivot_root.j2 @@ -0,0 +1,19 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "pivot_root" -}} + {{- template "qualifier" . -}} + {{- "pivot_root" -}} + {{- with .OldRoot -}} + {{ " oldroot=" }}{{ . }} + {{- end -}} + {{- with .NewRoot -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .TargetProfile -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/rule/ptrace.j2 b/pkg/aa/templates/rule/ptrace.j2 new file mode 100644 index 00000000..95318a28 --- /dev/null +++ b/pkg/aa/templates/rule/ptrace.j2 @@ -0,0 +1,16 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "ptrace" -}} + {{- template "qualifier" . -}} + {{- "ptrace" -}} + {{- with .Access -}} + {{ " (" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Peer -}} + {{ " peer=" }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/qualifier.j2 b/pkg/aa/templates/rule/qualifier.j2 similarity index 56% rename from pkg/aa/templates/qualifier.j2 rename to pkg/aa/templates/rule/qualifier.j2 index 51373549..ed89f63e 100644 --- a/pkg/aa/templates/qualifier.j2 +++ b/pkg/aa/templates/rule/qualifier.j2 @@ -1,3 +1,7 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + {{- define "qualifier" -}} {{- with .Prefix -}} {{ . }} diff --git a/pkg/aa/templates/rule/rlimit.j2 b/pkg/aa/templates/rule/rlimit.j2 new file mode 100644 index 00000000..5061c1c4 --- /dev/null +++ b/pkg/aa/templates/rule/rlimit.j2 @@ -0,0 +1,7 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "rlimit" -}} + {{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }}{{ template "comment" . }} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/rule/signal.j2 b/pkg/aa/templates/rule/signal.j2 new file mode 100644 index 00000000..b0fdbc35 --- /dev/null +++ b/pkg/aa/templates/rule/signal.j2 @@ -0,0 +1,19 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "signal" -}} + {{- template "qualifier" . -}} + {{- "signal" -}} + {{- with .Access -}} + {{ " (" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Set -}} + {{ " set=(" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Peer -}} + {{ " peer=" }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/rule/unix.j2 b/pkg/aa/templates/rule/unix.j2 new file mode 100644 index 00000000..fe1a6c7a --- /dev/null +++ b/pkg/aa/templates/rule/unix.j2 @@ -0,0 +1,35 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "unix" -}} + {{- template "qualifier" . -}} + {{- "unix" -}} + {{- with .Access -}} + {{ " (" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .Type -}} + {{ " type=" }}{{ . }} + {{- end -}} + {{- with .Protocol -}} + {{ " protocol=" }}{{ . }} + {{- end -}} + {{- with .Address -}} + {{ " addr=" }}{{ . }} + {{- end -}} + {{- with .Label -}} + {{ " label=" }}{{ . }} + {{- end -}} + {{- if and .PeerLabel .PeerAddr -}} + {{ " peer=(label=" }}{{ .PeerLabel }}{{ ", addr="}}{{ .PeerAddr }}{{ ")" }} + {{- else -}} + {{- with .PeerLabel -}} + {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- with .PeerAddr -}} + {{ overindent "peer=(addr=" }}{{ . }}{{ ")" }} + {{- end -}} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/aa/templates/rule/userns.j2 b/pkg/aa/templates/rule/userns.j2 new file mode 100644 index 00000000..771a5e2f --- /dev/null +++ b/pkg/aa/templates/rule/userns.j2 @@ -0,0 +1,9 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "userns" -}} + {{- if .Create -}} + {{ template "qualifier" . }}{{ "userns," }}{{ template "comment" . }} + {{- end -}} +{{- end -}} \ No newline at end of file From c97886d9606e3aa52ed2e84c28c1f78ed0f0088a Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Fri, 19 Apr 2024 22:43:02 +0100 Subject: [PATCH 07/62] feat(aa): continue refractoring the aa structure. --- pkg/aa/apparmor.go | 1 + pkg/aa/apparmor_test.go | 26 ++++---- pkg/aa/capability.go | 6 +- pkg/aa/change_profile.go | 6 +- pkg/aa/data_test.go | 18 +++--- pkg/aa/dbus.go | 6 +- pkg/aa/file.go | 6 +- pkg/aa/io_uring.go | 6 +- pkg/aa/mount.go | 18 +++--- pkg/aa/mqueue.go | 6 +- pkg/aa/network.go | 6 +- pkg/aa/pivot_root.go | 6 +- pkg/aa/preamble.go | 22 ++++--- pkg/aa/profile.go | 18 +++--- pkg/aa/ptrace.go | 6 +- pkg/aa/rlimit.go | 12 ++-- pkg/aa/rules.go | 19 +++--- pkg/aa/rules_test.go | 132 ++++++++++++++++----------------------- pkg/aa/signal.go | 6 +- pkg/aa/unix.go | 6 +- pkg/aa/userns.go | 6 +- pkg/logs/logs_test.go | 4 +- 22 files changed, 160 insertions(+), 182 deletions(-) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index b3a14ccf..6c598e3f 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -35,6 +35,7 @@ type Preamble struct { Includes []*Include Aliases []*Alias Variables []*Variable + Comments []*RuleBase } func NewAppArmorProfile() *AppArmorProfileFile { diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index 2f89737b..fb8cd687 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -58,7 +58,7 @@ func TestAppArmorProfile_String(t *testing.T) { Attributes: map[string]string{"security.tagged": "allowed"}, Flags: []string{"complain", "attach_disconnected"}, }, - Rules: []ApparmorRule{ + Rules: []Rule{ &Include{IsMagic: true, Path: "abstractions/base"}, &Include{IsMagic: true, Path: "abstractions/nameservice-strict"}, rlimit1, @@ -135,7 +135,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: capability1Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{capability1}, + Rules: []Rule{capability1}, }}, }, }, @@ -144,7 +144,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: network1Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{network1}, + Rules: []Rule{network1}, }}, }, }, @@ -153,7 +153,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: mount2Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{mount2}, + Rules: []Rule{mount2}, }}, }, }, @@ -162,7 +162,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: signal1Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{signal1}, + Rules: []Rule{signal1}, }}, }, }, @@ -171,7 +171,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: ptrace2Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{ptrace2}, + Rules: []Rule{ptrace2}, }}, }, }, @@ -180,7 +180,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: unix1Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{unix1}, + Rules: []Rule{unix1}, }}, }, }, @@ -189,7 +189,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: dbus2Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{dbus2}, + Rules: []Rule{dbus2}, }}, }, }, @@ -198,7 +198,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) { log: file2Log, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{file2}, + Rules: []Rule{file2}, }}, }, }, @@ -224,7 +224,7 @@ func TestAppArmorProfile_Sort(t *testing.T) { name: "all", origin: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{ + Rules: []Rule{ file2, network1, includeLocal1, dbus2, signal1, ptrace1, capability2, file1, dbus1, unix2, signal2, mount2, }, @@ -232,7 +232,7 @@ func TestAppArmorProfile_Sort(t *testing.T) { }, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{ + Rules: []Rule{ capability2, network1, mount2, signal1, signal2, ptrace1, unix2, dbus2, dbus1, file1, file2, includeLocal1, }, @@ -261,12 +261,12 @@ func TestAppArmorProfile_MergeRules(t *testing.T) { name: "all", origin: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{capability1, capability1, network1, network1, file1, file1}, + Rules: []Rule{capability1, capability1, network1, network1, file1, file1}, }}, }, want: &AppArmorProfileFile{ Profiles: []*Profile{{ - Rules: []ApparmorRule{capability1, network1, file1}, + Rules: []Rule{capability1, network1, file1}, }}, }, }, diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 292e3814..eb6c8b36 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -5,14 +5,14 @@ package aa type Capability struct { - Rule + RuleBase Qualifier Name string } -func newCapabilityFromLog(log map[string]string) *Capability { +func newCapabilityFromLog(log map[string]string) Rule { return &Capability{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Name: log["capname"], } diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index eeb5f973..c8114653 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -5,16 +5,16 @@ package aa type ChangeProfile struct { - Rule + RuleBase Qualifier ExecMode string Exec string ProfileName string } -func newChangeProfileFromLog(log map[string]string) *ChangeProfile { +func newChangeProfileFromLog(log map[string]string) Rule { return &ChangeProfile{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), ExecMode: log["mode"], Exec: log["exec"], diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go index 63aef6bb..49c6da54 100644 --- a/pkg/aa/data_test.go +++ b/pkg/aa/data_test.go @@ -71,13 +71,13 @@ var ( "flags": "rw, rbind", } mount1 = &Mount{ - Rule: Rule{Comment: "failed perms check"}, + RuleBase: RuleBase{Comment: "failed perms check"}, MountConditions: MountConditions{FsType: "overlay"}, Source: "overlay", MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/", } mount2 = &Mount{ - Rule: Rule{Comment: "failed perms check"}, + RuleBase: RuleBase{Comment: "failed perms check"}, MountConditions: MountConditions{Options: []string{"rw", "rbind"}}, Source: "/oldroot/dev/tty", MountPoint: "/newroot/dev/tty", @@ -205,9 +205,9 @@ var ( PeerLabel: "dbus-daemon", } unix2 = &Unix{ - Rule: Rule{FileInherit: true}, - Access: "receive", - Type: "stream", + RuleBase: RuleBase{FileInherit: true}, + Access: "receive", + Type: "stream", } // Dbus @@ -285,9 +285,9 @@ var ( } file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"} file2 = &File{ - Rule: Rule{NoNewPrivs: true}, - Owner: true, - Path: "@{PROC}/4163/cgroup", - Access: "r", + RuleBase: RuleBase{NoNewPrivs: true}, + Owner: true, + Path: "@{PROC}/4163/cgroup", + Access: "r", } ) diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index 1c43df88..6c8a0dc7 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -5,7 +5,7 @@ package aa type Dbus struct { - Rule + RuleBase Qualifier Access string Bus string @@ -17,7 +17,7 @@ type Dbus struct { PeerLabel string } -func newDbusFromLog(log map[string]string) *Dbus { +func newDbusFromLog(log map[string]string) Rule { name := "" peerName := "" if log["mask"] == "bind" { @@ -26,7 +26,7 @@ func newDbusFromLog(log map[string]string) *Dbus { peerName = log["name"] } return &Dbus{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Access: log["mask"], Bus: log["bus"], diff --git a/pkg/aa/file.go b/pkg/aa/file.go index ec16e54c..cba3fbaa 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -5,7 +5,7 @@ package aa type File struct { - Rule + RuleBase Qualifier Owner bool Path string @@ -13,7 +13,7 @@ type File struct { Target string } -func newFileFromLog(log map[string]string) *File { +func newFileFromLog(log map[string]string) Rule { owner := false fsuid, hasFsUID := log["fsuid"] ouid, hasOuUID := log["ouid"] @@ -22,7 +22,7 @@ func newFileFromLog(log map[string]string) *File { owner = true } return &File{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Owner: owner, Path: log["name"], diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index 08370564..e2f91c59 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -5,15 +5,15 @@ package aa type IOUring struct { - Rule + RuleBase Qualifier Access string Label string } -func newIOUringFromLog(log map[string]string) *IOUring { +func newIOUringFromLog(log map[string]string) Rule { return &IOUring{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested"]), Label: log["label"], diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 81938097..103a871d 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -37,16 +37,16 @@ func (m MountConditions) Equals(other MountConditions) bool { } type Mount struct { - Rule + RuleBase Qualifier MountConditions Source string MountPoint string } -func newMountFromLog(log map[string]string) *Mount { +func newMountFromLog(log map[string]string) Rule { return &Mount{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), MountConditions: newMountConditionsFromLog(log), Source: log["srcname"], @@ -76,15 +76,15 @@ func (r *Mount) Equals(other any) bool { } type Umount struct { - Rule + RuleBase Qualifier MountConditions MountPoint string } -func newUmountFromLog(log map[string]string) *Umount { +func newUmountFromLog(log map[string]string) Rule { return &Umount{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), MountConditions: newMountConditionsFromLog(log), MountPoint: log["name"], @@ -110,15 +110,15 @@ func (r *Umount) Equals(other any) bool { } type Remount struct { - Rule + RuleBase Qualifier MountConditions MountPoint string } -func newRemountFromLog(log map[string]string) *Remount { +func newRemountFromLog(log map[string]string) Rule { return &Remount{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), MountConditions: newMountConditionsFromLog(log), MountPoint: log["name"], diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 6afba37f..b7a3edb5 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -9,7 +9,7 @@ import ( ) type Mqueue struct { - Rule + RuleBase Qualifier Access string Type string @@ -17,7 +17,7 @@ type Mqueue struct { Name string } -func newMqueueFromLog(log map[string]string) *Mqueue { +func newMqueueFromLog(log map[string]string) Rule { mqueueType := "posix" if strings.Contains(log["class"], "posix") { mqueueType = "posix" @@ -25,7 +25,7 @@ func newMqueueFromLog(log map[string]string) *Mqueue { mqueueType = "sysv" } return &Mqueue{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested"]), Type: mqueueType, diff --git a/pkg/aa/network.go b/pkg/aa/network.go index b23bdf71..ec8188ec 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -34,7 +34,7 @@ func (r AddressExpr) Equals(other AddressExpr) bool { } type Network struct { - Rule + RuleBase Qualifier AddressExpr Domain string @@ -42,9 +42,9 @@ type Network struct { Protocol string } -func newNetworkFromLog(log map[string]string) *Network { +func newNetworkFromLog(log map[string]string) Rule { return &Network{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), AddressExpr: newAddressExprFromLog(log), Domain: log["family"], diff --git a/pkg/aa/pivot_root.go b/pkg/aa/pivot_root.go index 13979ca3..94f289c5 100644 --- a/pkg/aa/pivot_root.go +++ b/pkg/aa/pivot_root.go @@ -5,16 +5,16 @@ package aa type PivotRoot struct { - Rule + RuleBase Qualifier OldRoot string NewRoot string TargetProfile string } -func newPivotRootFromLog(log map[string]string) *PivotRoot { +func newPivotRootFromLog(log map[string]string) Rule { return &PivotRoot{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), OldRoot: log["srcname"], NewRoot: log["name"], diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 00f5042f..8b650629 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -9,7 +9,7 @@ import ( ) type Abi struct { - Rule + RuleBase Path string IsMagic bool } @@ -28,7 +28,7 @@ func (r *Abi) Equals(other any) bool { } type Alias struct { - Rule + RuleBase Path string RewrittenPath string } @@ -47,7 +47,7 @@ func (r Alias) Equals(other any) bool { } type Include struct { - Rule + RuleBase IfExists bool Path string IsMagic bool @@ -70,18 +70,20 @@ func (r *Include) Equals(other any) bool { } type Variable struct { - Rule + RuleBase Name string Values []string } -func (r *Variable) Less(other Variable) bool { - if r.Name != other.Name { - return r.Name < other.Name +func (r *Variable) Less(other any) bool { + o, _ := other.(*Variable) + if r.Name != o.Name { + return r.Name < o.Name } - return len(r.Values) < len(other.Values) + return len(r.Values) < len(o.Values) } -func (r *Variable) Equals(other Variable) bool { - return r.Name == other.Name && slices.Equal(r.Values, other.Values) +func (r *Variable) Equals(other any) bool { + o, _ := other.(*Variable) + return r.Name == o.Name && slices.Equal(r.Values, o.Values) } diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 4091dc5e..a7d7a6fd 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -12,7 +12,7 @@ import ( // Profile represents a single AppArmor profile. type Profile struct { - Rule + RuleBase Header Rules Rules } @@ -25,17 +25,17 @@ type Header struct { Flags []string } -func (r *Profile) Less(other any) bool { +func (p *Profile) Less(other any) bool { o, _ := other.(*Profile) - if r.Name != o.Name { - return r.Name < o.Name + if p.Name != o.Name { + return p.Name < o.Name } - return len(r.Attachments) < len(o.Attachments) + return len(p.Attachments) < len(o.Attachments) } -func (r *Profile) Equals(other any) bool { +func (p *Profile) Equals(other any) bool { o, _ := other.(*Profile) - return r.Name == o.Name && slices.Equal(r.Attachments, o.Attachments) && - maps.Equal(r.Attributes, o.Attributes) && - slices.Equal(r.Flags, o.Flags) + return p.Name == o.Name && slices.Equal(p.Attachments, o.Attachments) && + maps.Equal(p.Attributes, o.Attributes) && + slices.Equal(p.Flags, o.Flags) } diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index 6c444e22..ff6be070 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -5,15 +5,15 @@ package aa type Ptrace struct { - Rule + RuleBase Qualifier Access string Peer string } -func newPtraceFromLog(log map[string]string) *Ptrace { +func newPtraceFromLog(log map[string]string) Rule { return &Ptrace{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested_mask"]), Peer: log["peer"], diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index d484b352..66ed3c5e 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -5,18 +5,18 @@ package aa type Rlimit struct { - Rule + RuleBase Key string Op string Value string } -func newRlimitFromLog(log map[string]string) *Rlimit { +func newRlimitFromLog(log map[string]string) Rule { return &Rlimit{ - Rule: newRuleFromLog(log), - Key: log["key"], - Op: log["op"], - Value: log["value"], + RuleBase: newRuleFromLog(log), + Key: log["key"], + Op: log["op"], + Value: log["value"], } } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index dad738ba..16b06503 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -5,19 +5,18 @@ package aa import ( - "fmt" "strings" ) -// ApparmorRule generic interface -type ApparmorRule interface { +// Rule generic interface for all AppArmor rules +type Rule interface { Less(other any) bool Equals(other any) bool } -type Rules []ApparmorRule +type Rules []Rule -type Rule struct { +type RuleBase struct { Comment string NoNewPrivs bool FileInherit bool @@ -26,7 +25,7 @@ type Rule struct { Optional bool } -func newRuleFromLog(log map[string]string) Rule { +func newRuleFromLog(log map[string]string) RuleBase { fileInherit := false if log["operation"] == "file_inherit" { fileInherit = true @@ -54,7 +53,7 @@ func newRuleFromLog(log map[string]string) Rule { default: } - return Rule{ + return RuleBase{ Comment: msg, NoNewPrivs: noNewPrivs, FileInherit: fileInherit, @@ -62,11 +61,11 @@ func newRuleFromLog(log map[string]string) Rule { } } -func (r Rule) Less(other any) bool { +func (r RuleBase) Less(other any) bool { return false } -func (r Rule) Equals(other any) bool { +func (r RuleBase) Equals(other any) bool { return false } @@ -95,7 +94,7 @@ func (r Qualifier) Equals(other Qualifier) bool { } type All struct { - Rule + RuleBase } func (r *All) Less(other any) bool { diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 48f10726..4e9f94af 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -12,105 +12,81 @@ import ( func TestRule_FromLog(t *testing.T) { tests := []struct { name string - fromLog func(map[string]string) ApparmorRule + fromLog func(map[string]string) Rule log map[string]string - want ApparmorRule + want Rule }{ { - name: "capbability", - fromLog: func(m map[string]string) ApparmorRule { - return newCapabilityFromLog(m) - }, - log: capability1Log, - want: capability1, + name: "capbability", + fromLog: newCapabilityFromLog, + log: capability1Log, + want: capability1, }, { - name: "network", - fromLog: func(m map[string]string) ApparmorRule { - return newNetworkFromLog(m) - }, - log: network1Log, - want: network1, + name: "network", + fromLog: newNetworkFromLog, + log: network1Log, + want: network1, }, { - name: "mount", - fromLog: func(m map[string]string) ApparmorRule { - return newMountFromLog(m) - }, - log: mount1Log, - want: mount1, + name: "mount", + fromLog: newMountFromLog, + log: mount1Log, + want: mount1, }, { - name: "umount", - fromLog: func(m map[string]string) ApparmorRule { - return newUmountFromLog(m) - }, - log: umount1Log, - want: umount1, + name: "umount", + fromLog: newUmountFromLog, + log: umount1Log, + want: umount1, }, { - name: "pivotroot", - fromLog: func(m map[string]string) ApparmorRule { - return newPivotRootFromLog(m) - }, - log: pivotroot1Log, - want: pivotroot1, + name: "pivotroot", + fromLog: newPivotRootFromLog, + log: pivotroot1Log, + want: pivotroot1, }, { - name: "changeprofile", - fromLog: func(m map[string]string) ApparmorRule { - return newChangeProfileFromLog(m) - }, - log: changeprofile1Log, - want: changeprofile1, + name: "changeprofile", + fromLog: newChangeProfileFromLog, + log: changeprofile1Log, + want: changeprofile1, }, { - name: "signal", - fromLog: func(m map[string]string) ApparmorRule { - return newSignalFromLog(m) - }, - log: signal1Log, - want: signal1, + name: "signal", + fromLog: newSignalFromLog, + log: signal1Log, + want: signal1, }, { - name: "ptrace/xdg-document-portal", - fromLog: func(m map[string]string) ApparmorRule { - return newPtraceFromLog(m) - }, - log: ptrace1Log, - want: ptrace1, + name: "ptrace/xdg-document-portal", + fromLog: newPtraceFromLog, + log: ptrace1Log, + want: ptrace1, }, { - name: "ptrace/snap-update-ns.firefox", - fromLog: func(m map[string]string) ApparmorRule { - return newPtraceFromLog(m) - }, - log: ptrace2Log, - want: ptrace2, + name: "ptrace/snap-update-ns.firefox", + fromLog: newPtraceFromLog, + log: ptrace2Log, + want: ptrace2, }, { - name: "unix", - fromLog: func(m map[string]string) ApparmorRule { - return newUnixFromLog(m) - }, - log: unix1Log, - want: unix1, + name: "unix", + fromLog: newUnixFromLog, + log: unix1Log, + want: unix1, }, { - name: "dbus", - fromLog: func(m map[string]string) ApparmorRule { - return newDbusFromLog(m) - }, - log: dbus1Log, - want: dbus1, + name: "dbus", + fromLog: newDbusFromLog, + log: dbus1Log, + want: dbus1, }, { - name: "file", - fromLog: func(m map[string]string) ApparmorRule { - return newFileFromLog(m) - }, - log: file1Log, - want: file1, + name: "file", + fromLog: newFileFromLog, + log: file1Log, + want: file1, }, } for _, tt := range tests { @@ -125,8 +101,8 @@ func TestRule_FromLog(t *testing.T) { func TestRule_Less(t *testing.T) { tests := []struct { name string - rule ApparmorRule - other ApparmorRule + rule Rule + other Rule want bool }{ { @@ -299,8 +275,8 @@ func TestRule_Less(t *testing.T) { func TestRule_Equals(t *testing.T) { tests := []struct { name string - rule ApparmorRule - other ApparmorRule + rule Rule + other Rule want bool }{ { diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 9589f508..ca8706bc 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -5,16 +5,16 @@ package aa type Signal struct { - Rule + RuleBase Qualifier Access string Set string Peer string } -func newSignalFromLog(log map[string]string) *Signal { +func newSignalFromLog(log map[string]string) Rule { return &Signal{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested_mask"]), Set: log["signal"], diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index 0372d467..ca9415be 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -5,7 +5,7 @@ package aa type Unix struct { - Rule + RuleBase Qualifier Access string Type string @@ -18,9 +18,9 @@ type Unix struct { PeerAddr string } -func newUnixFromLog(log map[string]string) *Unix { +func newUnixFromLog(log map[string]string) Rule { return &Unix{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Access: toAccess(log["requested_mask"]), Type: log["sock_type"], diff --git a/pkg/aa/userns.go b/pkg/aa/userns.go index 9c582400..e6c41bc0 100644 --- a/pkg/aa/userns.go +++ b/pkg/aa/userns.go @@ -5,14 +5,14 @@ package aa type Userns struct { - Rule + RuleBase Qualifier Create bool } -func newUsernsFromLog(log map[string]string) *Userns { +func newUsernsFromLog(log map[string]string) Rule { return &Userns{ - Rule: newRuleFromLog(log), + RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), Create: true, } diff --git a/pkg/logs/logs_test.go b/pkg/logs/logs_test.go index 462e8417..cd2d2c52 100644 --- a/pkg/logs/logs_test.go +++ b/pkg/logs/logs_test.go @@ -303,13 +303,13 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { Header: aa.Header{Name: "kmod"}, Rules: aa.Rules{ &aa.Unix{ - Rule: aa.Rule{FileInherit: true}, + RuleBase: aa.RuleBase{FileInherit: true}, Access: "send receive", Type: "stream", Protocol: "0", }, &aa.Unix{ - Rule: aa.Rule{FileInherit: true}, + RuleBase: aa.RuleBase{FileInherit: true}, Access: "send receive", Type: "stream", Protocol: "0", From a2910122d206122777d641ebdd969a5d826294f8 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 19:18:42 +0100 Subject: [PATCH 08/62] fix: do not use the wrong profile. --- pkg/aa/variables.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/aa/variables.go b/pkg/aa/variables.go index 684564a2..ea7ce499 100644 --- a/pkg/aa/variables.go +++ b/pkg/aa/variables.go @@ -86,10 +86,10 @@ func (f *AppArmorProfileFile) resolve(str string) []string { func (f *AppArmorProfileFile) ResolveAttachments() { p := f.GetDefaultProfile() - for _, variable := range profile.Variables { + for _, variable := range f.Variables { if variable.Name == "exec_path" { for _, value := range variable.Values { - attachments := profile.resolve(value) + attachments := f.resolve(value) if len(attachments) == 0 { panic("Variable not defined in: " + value) } From c719a0a109df521b7f551b4d470ea2a4fd13ac1a Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:17:25 +0100 Subject: [PATCH 09/62] feat(aa): ensure accesses are slice of string. --- pkg/aa/apparmor.go | 71 --------------------------------------- pkg/aa/apparmor_test.go | 44 ++++++++++++------------- pkg/aa/capability.go | 17 +++++++--- pkg/aa/data_test.go | 30 ++++++++--------- pkg/aa/dbus.go | 12 ++++--- pkg/aa/file.go | 13 +++++--- pkg/aa/io_uring.go | 12 ++++--- pkg/aa/mqueue.go | 10 +++--- pkg/aa/profile.go | 73 +++++++++++++++++++++++++++++++++++++++++ pkg/aa/ptrace.go | 10 +++--- pkg/aa/rules_test.go | 6 ++-- pkg/aa/signal.go | 18 +++++----- pkg/aa/template.go | 54 ++++++++++++++++++++---------- pkg/aa/unix.go | 10 +++--- pkg/logs/logs.go | 8 ++--- pkg/logs/logs_test.go | 62 ++++++++++++++++------------------ 16 files changed, 240 insertions(+), 210 deletions(-) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index 6c598e3f..a7f6cc26 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -61,77 +61,6 @@ func (f *AppArmorProfileFile) GetDefaultProfile() *Profile { return f.Profiles[0] } -// AddRule adds a new rule to the profile from a log map -// See utils/apparmor/logparser.py for the format of the log map -func (f *AppArmorProfileFile) AddRule(log map[string]string) { - p := f.GetDefaultProfile() - - // Generate profile flags and extra rules - switch log["error"] { - case "-2": - if !slices.Contains(p.Flags, "mediate_deleted") { - p.Flags = append(p.Flags, "mediate_deleted") - } - case "-13": - if strings.Contains(log["info"], "namespace creation restricted") { - p.Rules = append(p.Rules, newUsernsFromLog(log)) - } else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") { - p.Flags = append(p.Flags, "attach_disconnected") - } - default: - } - - switch log["class"] { - case "cap": - p.Rules = append(p.Rules, newCapabilityFromLog(log)) - case "net": - if log["family"] == "unix" { - p.Rules = append(p.Rules, newUnixFromLog(log)) - } else { - p.Rules = append(p.Rules, newNetworkFromLog(log)) - } - case "mount": - if strings.Contains(log["flags"], "remount") { - p.Rules = append(p.Rules, newRemountFromLog(log)) - } else { - switch log["operation"] { - case "mount": - p.Rules = append(p.Rules, newMountFromLog(log)) - case "umount": - p.Rules = append(p.Rules, newUmountFromLog(log)) - case "remount": - p.Rules = append(p.Rules, newRemountFromLog(log)) - case "pivotroot": - p.Rules = append(p.Rules, newPivotRootFromLog(log)) - } - } - case "posix_mqueue", "sysv_mqueue": - p.Rules = append(p.Rules, newMqueueFromLog(log)) - case "signal": - p.Rules = append(p.Rules, newSignalFromLog(log)) - case "ptrace": - p.Rules = append(p.Rules, newPtraceFromLog(log)) - case "namespace": - p.Rules = append(p.Rules, newUsernsFromLog(log)) - case "unix": - p.Rules = append(p.Rules, newUnixFromLog(log)) - case "dbus": - p.Rules = append(p.Rules, newDbusFromLog(log)) - case "file": - if log["operation"] == "change_onexec" { - p.Rules = append(p.Rules, newChangeProfileFromLog(log)) - } else { - p.Rules = append(p.Rules, newFileFromLog(log)) - } - default: - if strings.Contains(log["operation"], "dbus") { - p.Rules = append(p.Rules, newDbusFromLog(log)) - } else if log["family"] == "unix" { - p.Rules = append(p.Rules, newUnixFromLog(log)) - } - } -} - // Sort the rules in the profile // Follow: https://apparmor.pujol.io/development/guidelines/#guidelines func (f *AppArmorProfileFile) Sort() { diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index fb8cd687..3c1bcaa1 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -62,8 +62,8 @@ func TestAppArmorProfile_String(t *testing.T) { &Include{IsMagic: true, Path: "abstractions/base"}, &Include{IsMagic: true, Path: "abstractions/nameservice-strict"}, rlimit1, - &Capability{Name: "dac_read_search"}, - &Capability{Name: "dac_override"}, + &Capability{Names: []string{"dac_read_search"}}, + &Capability{Names: []string{"dac_override"}}, &Network{Domain: "inet", Type: "stream"}, &Network{Domain: "inet6", Type: "stream"}, &Mount{ @@ -79,25 +79,25 @@ func TestAppArmorProfile_String(t *testing.T) { MountPoint: "@{run}/user/@{uid}/", }, &Signal{ - Access: "receive", - Set: "term", + Access: []string{"receive"}, + Set: []string{"term"}, Peer: "at-spi-bus-launcher", }, - &Ptrace{Access: "read", Peer: "nautilus"}, + &Ptrace{Access: []string{"read"}, Peer: "nautilus"}, &Unix{ - Access: "send receive", + Access: []string{"send", "receive"}, Type: "stream", Address: "@/tmp/.ICE-unix/1995", PeerLabel: "gnome-shell", PeerAddr: "none", }, &Dbus{ - Access: "bind", + Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*", }, &Dbus{ - Access: "receive", + Access: []string{"receive"}, Bus: "system", Path: "/org/freedesktop/DBus", Interface: "org.freedesktop.DBus", @@ -105,9 +105,9 @@ func TestAppArmorProfile_String(t *testing.T) { PeerName: ":1.3", PeerLabel: "power-profiles-daemon", }, - &File{Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", Access: "rm"}, - &File{Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: "rw"}, - &File{Path: "@{sys}/devices/@{pci}/class", Access: "r"}, + &File{Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", Access: []string{"r", "m"}}, + &File{Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: []string{"r", "w"}}, + &File{Path: "@{sys}/devices/@{pci}/class", Access: []string{"r"}}, includeLocal1, }, }}, @@ -306,19 +306,19 @@ func TestAppArmorProfile_Integration(t *testing.T) { }, Rules: Rules{ &Include{IfExists: true, IsMagic: true, Path: "local/aa-status"}, - &Capability{Name: "dac_read_search"}, - &File{Path: "@{exec_path}", Access: "mr"}, - &File{Path: "@{PROC}/@{pids}/attr/apparmor/current", Access: "r"}, - &File{Path: "@{PROC}/", Access: "r"}, - &File{Path: "@{sys}/module/apparmor/parameters/enabled", Access: "r"}, - &File{Path: "@{sys}/kernel/security/apparmor/profiles", Access: "r"}, - &File{Path: "@{PROC}/@{pids}/attr/current", Access: "r"}, + &Capability{Names: []string{"dac_read_search"}}, + &File{Path: "@{exec_path}", Access: []string{"m", "r"}}, + &File{Path: "@{PROC}/@{pids}/attr/apparmor/current", Access: []string{"r"}}, + &File{Path: "@{PROC}/", Access: []string{"r"}}, + &File{Path: "@{sys}/module/apparmor/parameters/enabled", Access: []string{"r"}}, + &File{Path: "@{sys}/kernel/security/apparmor/profiles", Access: []string{"r"}}, + &File{Path: "@{PROC}/@{pids}/attr/current", Access: []string{"r"}}, &Include{IsMagic: true, Path: "abstractions/consoles"}, - &File{Owner: true, Path: "@{PROC}/@{pid}/mounts", Access: "r"}, + &File{Owner: true, Path: "@{PROC}/@{pid}/mounts", Access: []string{"r"}}, &Include{IsMagic: true, Path: "abstractions/base"}, - &File{Path: "/dev/tty@{int}", Access: "rw"}, - &Capability{Name: "sys_ptrace"}, - &Ptrace{Access: "read"}, + &File{Path: "/dev/tty@{int}", Access: []string{"r", "w"}}, + &Capability{Names: []string{"sys_ptrace"}}, + &Ptrace{Access: []string{"read"}}, }, }}, }, diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index eb6c8b36..0e4918fa 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -4,29 +4,36 @@ package aa + type Capability struct { RuleBase Qualifier - Name string + Names []string +} + } func newCapabilityFromLog(log map[string]string) Rule { return &Capability{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Name: log["capname"], + Names: []string{log["capname"]}, } } func (r *Capability) Less(other any) bool { o, _ := other.(*Capability) - if r.Name != o.Name { - return r.Name < o.Name + for i := 0; i < len(r.Names) && i < len(o.Names); i++ { + if r.Names[i] != o.Names[i] { + return r.Names[i] < o.Names[i] + } } return r.Qualifier.Less(o.Qualifier) } func (r *Capability) Equals(other any) bool { o, _ := other.(*Capability) - return r.Name == o.Name && r.Qualifier.Equals(o.Qualifier) + return slices.Equal(r.Names, o.Names) && r.Qualifier.Equals(o.Qualifier) +} + } diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go index 49c6da54..5787154b 100644 --- a/pkg/aa/data_test.go +++ b/pkg/aa/data_test.go @@ -26,8 +26,8 @@ var ( "profile": "pkexec", "comm": "pkexec", } - capability1 = &Capability{Name: "net_admin"} - capability2 = &Capability{Name: "sys_ptrace"} + capability1 = &Capability{Names: []string{"net_admin"}} + capability2 = &Capability{Names: []string{"sys_ptrace"}} // Network network1Log = map[string]string{ @@ -147,13 +147,13 @@ var ( "peer": "firefox//&firejail-default", } signal1 = &Signal{ - Access: "receive", - Set: "kill", + Access: []string{"receive"}, + Set: []string{"kill"}, Peer: "firefox//&firejail-default", } signal2 = &Signal{ - Access: "receive", - Set: "up", + Access: []string{"receive"}, + Set: []string{"up"}, Peer: "firefox//&firejail-default", } @@ -177,8 +177,8 @@ var ( "denied_mask": "readby", "peer": "systemd-journald", } - ptrace1 = &Ptrace{Access: "read", Peer: "nautilus"} - ptrace2 = &Ptrace{Access: "readby", Peer: "systemd-journald"} + ptrace1 = &Ptrace{Access: []string{"read"}, Peer: "nautilus"} + ptrace2 = &Ptrace{Access: []string{"readby"}, Peer: "systemd-journald"} // Unix unix1Log = map[string]string{ @@ -197,7 +197,7 @@ var ( "protocol": "0", } unix1 = &Unix{ - Access: "send receive", + Access: []string{"receive", "send"}, Type: "stream", Protocol: "0", Address: "none", @@ -206,7 +206,7 @@ var ( } unix2 = &Unix{ RuleBase: RuleBase{FileInherit: true}, - Access: "receive", + Access: []string{"receive"}, Type: "stream", } @@ -234,7 +234,7 @@ var ( "label": "evolution-source-registry", } dbus1 = &Dbus{ - Access: "receive", + Access: []string{"receive"}, Bus: "session", Path: "/org/gtk/vfs/metadata", Interface: "org.gtk.vfs.Metadata", @@ -243,12 +243,12 @@ var ( PeerLabel: "tracker-extract", } dbus2 = &Dbus{ - Access: "bind", + Access: []string{"bind"}, Bus: "session", Name: "org.gnome.evolution.dataserver.Sources5", } dbus3 = &Dbus{ - Access: "bind", + Access: []string{"bind"}, Bus: "session", Name: "org.gnome.evolution.dataserver", } @@ -283,11 +283,11 @@ var ( "OUID": "user", "error": "-1", } - file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"} + file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}} file2 = &File{ RuleBase: RuleBase{NoNewPrivs: true}, Owner: true, Path: "@{PROC}/4163/cgroup", - Access: "r", + Access: []string{"r"}, } ) diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index 6c8a0dc7..45a5dabf 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -7,7 +7,7 @@ package aa type Dbus struct { RuleBase Qualifier - Access string + Access []string Bus string Name string Path string @@ -28,7 +28,7 @@ func newDbusFromLog(log map[string]string) Rule { return &Dbus{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: log["mask"], + Access: []string{log["mask"]}, Bus: log["bus"], Name: name, Path: log["path"], @@ -41,8 +41,10 @@ func newDbusFromLog(log map[string]string) Rule { func (r *Dbus) Less(other any) bool { o, _ := other.(*Dbus) - if r.Access != o.Access { - return r.Access < o.Access + for i := 0; i < len(r.Access) && i < len(o.Access); i++ { + if r.Access[i] != o.Access[i] { + return r.Access[i] < o.Access[i] + } } if r.Bus != o.Bus { return r.Bus < o.Bus @@ -70,7 +72,7 @@ func (r *Dbus) Less(other any) bool { func (r *Dbus) Equals(other any) bool { o, _ := other.(*Dbus) - return r.Access == o.Access && r.Bus == o.Bus && r.Name == o.Name && + return slices.Equal(r.Access, o.Access) && r.Bus == o.Bus && r.Name == o.Name && r.Path == o.Path && r.Interface == o.Interface && r.Member == o.Member && r.PeerName == o.PeerName && r.PeerLabel == o.PeerLabel && r.Qualifier.Equals(o.Qualifier) diff --git a/pkg/aa/file.go b/pkg/aa/file.go index cba3fbaa..266989d8 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -9,7 +9,7 @@ type File struct { Qualifier Owner bool Path string - Access string + Access []string Target string } @@ -26,7 +26,7 @@ func newFileFromLog(log map[string]string) Rule { Qualifier: newQualifierFromLog(log), Owner: owner, Path: log["name"], - Access: toAccess(log["requested_mask"]), + Access: toAccess("file-log", log["requested_mask"]), Target: log["target"], } } @@ -41,8 +41,8 @@ func (r *File) Less(other any) bool { if r.Path != o.Path { return r.Path < o.Path } - if r.Access != o.Access { - return r.Access < o.Access + if len(r.Access) != len(o.Access) { + return len(r.Access) < len(o.Access) } if r.Target != o.Target { return r.Target < o.Target @@ -55,6 +55,9 @@ func (r *File) Less(other any) bool { func (r *File) Equals(other any) bool { o, _ := other.(*File) - return r.Path == o.Path && r.Access == o.Access && r.Owner == o.Owner && + return r.Path == o.Path && slices.Equal(r.Access, o.Access) && r.Owner == o.Owner && + r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) +} + r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index e2f91c59..90aae249 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -7,7 +7,7 @@ package aa type IOUring struct { RuleBase Qualifier - Access string + Access []string Label string } @@ -15,15 +15,15 @@ func newIOUringFromLog(log map[string]string) Rule { return &IOUring{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(log["requested"]), + Access: toAccess(tokIOURING, log["requested"]), Label: log["label"], } } func (r *IOUring) Less(other any) bool { o, _ := other.(*IOUring) - if r.Access != o.Access { - return r.Access < o.Access + if len(r.Access) != len(o.Access) { + return len(r.Access) < len(o.Access) } if r.Label != o.Label { return r.Label < o.Label @@ -33,5 +33,7 @@ func (r *IOUring) Less(other any) bool { func (r *IOUring) Equals(other any) bool { o, _ := other.(*IOUring) - return r.Access == o.Access && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) + return slices.Equal(r.Access, o.Access) && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) +} + } diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index b7a3edb5..42b674b5 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -11,7 +11,7 @@ import ( type Mqueue struct { RuleBase Qualifier - Access string + Access []string Type string Label string Name string @@ -27,7 +27,7 @@ func newMqueueFromLog(log map[string]string) Rule { return &Mqueue{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(log["requested"]), + Access: toAccess(tokMQUEUE, log["requested"]), Type: mqueueType, Label: log["label"], Name: log["name"], @@ -36,8 +36,8 @@ func newMqueueFromLog(log map[string]string) Rule { func (r *Mqueue) Less(other any) bool { o, _ := other.(*Mqueue) - if r.Access != o.Access { - return r.Access < o.Access + if len(r.Access) != len(o.Access) { + return len(r.Access) < len(o.Access) } if r.Type != o.Type { return r.Type < o.Type @@ -50,6 +50,6 @@ func (r *Mqueue) Less(other any) bool { func (r *Mqueue) Equals(other any) bool { o, _ := other.(*Mqueue) - return r.Access == o.Access && r.Type == o.Type && r.Label == o.Label && + return slices.Equal(r.Access, o.Access) && r.Type == o.Type && r.Label == o.Label && r.Name == o.Name && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index a7d7a6fd..24576e20 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -39,3 +39,76 @@ func (p *Profile) Equals(other any) bool { maps.Equal(p.Attributes, o.Attributes) && slices.Equal(p.Flags, o.Flags) } + +// AddRule adds a new rule to the profile from a log map. +func (p *Profile) AddRule(log map[string]string) { + + // Generate profile flags and extra rules + switch log["error"] { + case "-2": + if !slices.Contains(p.Flags, "mediate_deleted") { + p.Flags = append(p.Flags, "mediate_deleted") + } + case "-13": + if strings.Contains(log["info"], "namespace creation restricted") { + p.Rules = append(p.Rules, newUsernsFromLog(log)) + } else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") { + p.Flags = append(p.Flags, "attach_disconnected") + } + default: + } + + switch log["class"] { + case "rlimits": + p.Rules = append(p.Rules, newRlimitFromLog(log)) + case "cap": + p.Rules = append(p.Rules, newCapabilityFromLog(log)) + case "net": + if log["family"] == "unix" { + p.Rules = append(p.Rules, newUnixFromLog(log)) + } else { + p.Rules = append(p.Rules, newNetworkFromLog(log)) + } + case "io_uring": + p.Rules = append(p.Rules, newIOUringFromLog(log)) + case "mount": + if strings.Contains(log["flags"], "remount") { + p.Rules = append(p.Rules, newRemountFromLog(log)) + } else { + switch log["operation"] { + case "mount": + p.Rules = append(p.Rules, newMountFromLog(log)) + case "umount": + p.Rules = append(p.Rules, newUmountFromLog(log)) + case "remount": + p.Rules = append(p.Rules, newRemountFromLog(log)) + case "pivotroot": + p.Rules = append(p.Rules, newPivotRootFromLog(log)) + } + } + case "posix_mqueue", "sysv_mqueue": + p.Rules = append(p.Rules, newMqueueFromLog(log)) + case "signal": + p.Rules = append(p.Rules, newSignalFromLog(log)) + case "ptrace": + p.Rules = append(p.Rules, newPtraceFromLog(log)) + case "namespace": + p.Rules = append(p.Rules, newUsernsFromLog(log)) + case "unix": + p.Rules = append(p.Rules, newUnixFromLog(log)) + case "dbus": + p.Rules = append(p.Rules, newDbusFromLog(log)) + case "file": + if log["operation"] == "change_onexec" { + p.Rules = append(p.Rules, newChangeProfileFromLog(log)) + } else { + p.Rules = append(p.Rules, newFileFromLog(log)) + } + default: + if strings.Contains(log["operation"], "dbus") { + p.Rules = append(p.Rules, newDbusFromLog(log)) + } else if log["family"] == "unix" { + p.Rules = append(p.Rules, newUnixFromLog(log)) + } + } +} diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index ff6be070..17b04cb9 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -7,7 +7,7 @@ package aa type Ptrace struct { RuleBase Qualifier - Access string + Access []string Peer string } @@ -15,15 +15,15 @@ func newPtraceFromLog(log map[string]string) Rule { return &Ptrace{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(log["requested_mask"]), + Access: toAccess(tokPTRACE, log["requested_mask"]), Peer: log["peer"], } } func (r *Ptrace) Less(other any) bool { o, _ := other.(*Ptrace) - if r.Access != o.Access { - return r.Access < o.Access + if len(r.Access) != len(o.Access) { + return len(r.Access) < len(o.Access) } if r.Peer != o.Peer { return r.Peer == o.Peer @@ -33,6 +33,6 @@ func (r *Ptrace) Less(other any) bool { func (r *Ptrace) Equals(other any) bool { o, _ := other.(*Ptrace) - return r.Access == o.Access && r.Peer == o.Peer && + return slices.Equal(r.Access, o.Access) && r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 4e9f94af..96d7a5df 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -251,9 +251,9 @@ func TestRule_Less(t *testing.T) { }, { name: "file/access", - rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"}, - other: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "w"}, - want: true, + rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}}, + other: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"w"}}, + want: false, }, { name: "file/close", diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index ca8706bc..13deb74d 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -7,8 +7,8 @@ package aa type Signal struct { RuleBase Qualifier - Access string - Set string + Access []string + Set []string Peer string } @@ -16,19 +16,19 @@ func newSignalFromLog(log map[string]string) Rule { return &Signal{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(log["requested_mask"]), - Set: log["signal"], + Access: toAccess(tokSIGNAL, log["requested_mask"]), + Set: toAccess(tokSIGNAL, log["signal"]), Peer: log["peer"], } } func (r *Signal) Less(other any) bool { o, _ := other.(*Signal) - if r.Access != o.Access { - return r.Access < o.Access + if len(r.Access) != len(o.Access) { + return len(r.Access) < len(o.Access) } - if r.Set != o.Set { - return r.Set < o.Set + if len(r.Set) != len(o.Set) { + return len(r.Set) < len(o.Set) } if r.Peer != o.Peer { return r.Peer < o.Peer @@ -38,6 +38,6 @@ func (r *Signal) Less(other any) bool { func (r *Signal) Equals(other any) bool { o, _ := other.(*Signal) - return r.Access == o.Access && r.Set == o.Set && + return slices.Equal(r.Access, o.Access) && slices.Equal(r.Set, o.Set) && r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 2a4d0e0a..2e94480b 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -33,19 +33,10 @@ var ( } // convert apparmor requested mask to apparmor access mode - requestedMaskToAccess = map[string]string{ - "a": "w", - "ac": "w", - "c": "w", - "d": "w", - "m": "rm", - "ra": "rw", - "wc": "w", - "wd": "w", - "wr": "rw", - "wrc": "rw", - "wrd": "rw", - "x": "rix", + maskToAccess = map[string]string{ + "a": "w", + "c": "w", + "d": "w", } // The order the apparmor rules should be sorted @@ -172,9 +163,38 @@ func getLetterIn(alphabet []string, in string) string { return "" } -func toAccess(mask string) string { - if requestedMaskToAccess[mask] != "" { - return requestedMaskToAccess[mask] +// 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) } - return mask } diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index ca9415be..f734f327 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -7,7 +7,7 @@ package aa type Unix struct { RuleBase Qualifier - Access string + Access []string Type string Protocol string Address string @@ -22,7 +22,7 @@ func newUnixFromLog(log map[string]string) Rule { return &Unix{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: toAccess(log["requested_mask"]), + Access: toAccess(tokUNIX, log["requested_mask"]), Type: log["sock_type"], Protocol: log["protocol"], Address: log["addr"], @@ -36,8 +36,8 @@ func newUnixFromLog(log map[string]string) Rule { func (r *Unix) Less(other any) bool { o, _ := other.(*Unix) - if r.Access != o.Access { - return r.Access < o.Access + if len(r.Access) != len(o.Access) { + return len(r.Access) < len(o.Access) } if r.Type != o.Type { return r.Type < o.Type @@ -68,7 +68,7 @@ func (r *Unix) Less(other any) bool { func (r *Unix) Equals(other any) bool { o, _ := other.(*Unix) - return r.Access == o.Access && r.Type == o.Type && + return slices.Equal(r.Access, o.Access) && r.Type == o.Type && r.Protocol == o.Protocol && r.Address == o.Address && r.Label == o.Label && r.Attr == o.Attr && r.Opt == o.Opt && r.PeerLabel == o.PeerLabel && r.PeerAddr == o.PeerAddr && diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go index f14aefd4..bc95dd03 100644 --- a/pkg/logs/logs.go +++ b/pkg/logs/logs.go @@ -197,8 +197,8 @@ func (aaLogs AppArmorLogs) String() string { } // ParseToProfiles convert the log data into a new AppArmorProfiles -func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfileFiles { - profiles := make(aa.AppArmorProfileFiles, 0) +func (aaLogs AppArmorLogs) ParseToProfiles() map[string]*aa.Profile { + profiles := make(map[string]*aa.Profile, 0) for _, log := range aaLogs { name := "" if strings.Contains(log["operation"], "dbus") { @@ -208,9 +208,7 @@ func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfileFiles { } if _, ok := profiles[name]; !ok { - profile := &aa.AppArmorProfileFile{ - Profiles: []*aa.Profile{{Header: aa.Header{Name: name}}}, - } + profile := &aa.Profile{Header: aa.Header{Name: name}} profile.AddRule(log) profiles[name] = profile } else { diff --git a/pkg/logs/logs_test.go b/pkg/logs/logs_test.go index cd2d2c52..eb92f4ed 100644 --- a/pkg/logs/logs_test.go +++ b/pkg/logs/logs_test.go @@ -292,46 +292,42 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { tests := []struct { name string aaLogs AppArmorLogs - want aa.AppArmorProfileFiles + want map[string]*aa.Profile }{ { name: "", aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...), - want: aa.AppArmorProfileFiles{ - "kmod": &aa.AppArmorProfileFile{ - Profiles: []*aa.Profile{{ - Header: aa.Header{Name: "kmod"}, - Rules: aa.Rules{ - &aa.Unix{ - RuleBase: aa.RuleBase{FileInherit: true}, - Access: "send receive", - Type: "stream", - Protocol: "0", - }, - &aa.Unix{ - RuleBase: aa.RuleBase{FileInherit: true}, - Access: "send receive", - Type: "stream", - Protocol: "0", - }, + want: map[string]*aa.Profile{ + "kmod": { + Header: aa.Header{Name: "kmod"}, + Rules: aa.Rules{ + &aa.Unix{ + RuleBase: aa.RuleBase{FileInherit: true}, + Access: []string{"receive", "send"}, + Type: "stream", + Protocol: "0", }, - }}, + &aa.Unix{ + RuleBase: aa.RuleBase{FileInherit: true}, + Access: []string{"receive", "send"}, + Type: "stream", + Protocol: "0", + }, + }, }, - "power-profiles-daemon": &aa.AppArmorProfileFile{ - Profiles: []*aa.Profile{{ - Header: aa.Header{Name: "power-profiles-daemon"}, - Rules: aa.Rules{ - &aa.Dbus{ - Access: "send", - Bus: "system", - Path: "/org/freedesktop/DBus", - Interface: "org.freedesktop.DBus", - Member: "AddMatch", - PeerName: "org.freedesktop.DBus", - PeerLabel: "dbus-daemon", - }, + "power-profiles-daemon": { + Header: aa.Header{Name: "power-profiles-daemon"}, + Rules: aa.Rules{ + &aa.Dbus{ + Access: []string{"send"}, + Bus: "system", + Path: "/org/freedesktop/DBus", + Interface: "org.freedesktop.DBus", + Member: "AddMatch", + PeerName: "org.freedesktop.DBus", + PeerLabel: "dbus-daemon", }, - }}, + }, }, }, }, From e9fa0660f8663e1701782ef6961edeb2d71e6bf6 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:18:44 +0100 Subject: [PATCH 10/62] feat(aa): add define parameter for variables. --- pkg/aa/apparmor_test.go | 2 +- pkg/aa/preamble.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index 3c1bcaa1..ae39caef 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -47,7 +47,7 @@ func TestAppArmorProfile_String(t *testing.T) { Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, Aliases: []*Alias{{Path: "/mnt/usr", RewrittenPath: "/usr"}}, Variables: []*Variable{{ - Name: "exec_path", + Name: "exec_path", Define: true, Values: []string{"@{bin}/foo", "@{lib}/foo"}, }}, }, diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 8b650629..d10b4dfb 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -73,6 +73,9 @@ type Variable struct { RuleBase Name string Values []string + Define bool +} + } func (r *Variable) Less(other any) bool { From 54836685744db13efff1d13654b408a0d9a25557 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:26:09 +0100 Subject: [PATCH 11/62] feat(aa): add a string method to all rule struct. --- pkg/aa/apparmor.go | 9 +--- pkg/aa/capability.go | 4 ++ pkg/aa/change_profile.go | 6 +++ pkg/aa/dbus.go | 6 +++ pkg/aa/file.go | 4 ++ pkg/aa/io_uring.go | 5 ++ pkg/aa/mount.go | 19 +++++++ pkg/aa/mqueue.go | 7 +++ pkg/aa/network.go | 7 +++ pkg/aa/pivot_root.go | 6 +++ pkg/aa/preamble.go | 22 ++++++++ pkg/aa/profile.go | 11 ++++ pkg/aa/ptrace.go | 6 +++ pkg/aa/rlimit.go | 10 ++++ pkg/aa/rules.go | 21 ++++++++ pkg/aa/rules_test.go | 110 +++++++++++++++++++++++++++++++++++++++ pkg/aa/signal.go | 7 +++ pkg/aa/template.go | 99 +++++++++++++++++++++++++---------- pkg/aa/unix.go | 6 +++ pkg/aa/userns.go | 6 +++ 20 files changed, 337 insertions(+), 34 deletions(-) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index a7f6cc26..7a10c085 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -42,14 +42,9 @@ func NewAppArmorProfile() *AppArmorProfileFile { return &AppArmorProfileFile{} } -// String returns the formatted representation of a profile as a string +// String returns the formatted representation of a profile file as a string func (f *AppArmorProfileFile) String() string { - var res bytes.Buffer - err := tmpl["apparmor"].Execute(&res, f) - if err != nil { - return err.Error() - } - return res.String() + return renderTemplate("apparmor", f) } // GetDefaultProfile ensure a profile is always present in the profile file and diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 0e4918fa..14b27209 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -5,6 +5,8 @@ package aa +const tokCAPABILITY = "capability" + type Capability struct { RuleBase Qualifier @@ -36,4 +38,6 @@ func (r *Capability) Equals(other any) bool { return slices.Equal(r.Names, o.Names) && r.Qualifier.Equals(o.Qualifier) } +func (r *Capability) String() string { + return renderTemplate(tokCAPABILITY, r) } diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index c8114653..32106f9b 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -4,6 +4,8 @@ package aa +const tokCHANGEPROFILE = "change_profile" + type ChangeProfile struct { RuleBase Qualifier @@ -41,3 +43,7 @@ func (r *ChangeProfile) Equals(other any) bool { return r.ExecMode == o.ExecMode && r.Exec == o.Exec && r.ProfileName == o.ProfileName && r.Qualifier.Equals(o.Qualifier) } + +func (r *ChangeProfile) String() string { + return renderTemplate(tokCHANGEPROFILE, r) +} diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index 45a5dabf..3ab30ae7 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -4,6 +4,8 @@ package aa +const tokDBUS = "dbus" + type Dbus struct { RuleBase Qualifier @@ -77,3 +79,7 @@ func (r *Dbus) Equals(other any) bool { r.Member == o.Member && r.PeerName == o.PeerName && r.PeerLabel == o.PeerLabel && r.Qualifier.Equals(o.Qualifier) } + +func (r *Dbus) String() string { + return renderTemplate(tokDBUS, r) +} diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 266989d8..9390afbe 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -59,5 +59,9 @@ func (r *File) Equals(other any) bool { r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) } +func (r *File) String() string { + return renderTemplate("file", r) +} + r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) } diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index 90aae249..eedee845 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -4,6 +4,9 @@ package aa +const tokIOURING = "io_uring" + + type IOUring struct { RuleBase Qualifier @@ -36,4 +39,6 @@ func (r *IOUring) Equals(other any) bool { return slices.Equal(r.Access, o.Access) && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) } +func (r *IOUring) String() string { + return renderTemplate(tokIOURING, r) } diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 103a871d..7f0f5621 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -8,6 +8,13 @@ import ( "slices" "strings" ) + +const ( + tokMOUNT = "mount" + tokREMOUNT = "remount" + tokUMOUNT = "umount" +) + ) type MountConditions struct { @@ -75,6 +82,10 @@ func (r *Mount) Equals(other any) bool { r.Qualifier.Equals(o.Qualifier) } +func (r *Mount) String() string { + return renderTemplate(tokMOUNT, r) +} + type Umount struct { RuleBase Qualifier @@ -109,6 +120,10 @@ func (r *Umount) Equals(other any) bool { r.Qualifier.Equals(o.Qualifier) } +func (r *Umount) String() string { + return renderTemplate(tokUMOUNT, r) +} + type Remount struct { RuleBase Qualifier @@ -142,3 +157,7 @@ func (r *Remount) Equals(other any) bool { r.MountConditions.Equals(o.MountConditions) && r.Qualifier.Equals(o.Qualifier) } + +func (r *Remount) String() string { + return renderTemplate(tokREMOUNT, r) +} diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 42b674b5..0035f2cd 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -8,6 +8,9 @@ import ( "strings" ) +const tokMQUEUE = "mqueue" + + type Mqueue struct { RuleBase Qualifier @@ -53,3 +56,7 @@ func (r *Mqueue) Equals(other any) bool { return slices.Equal(r.Access, o.Access) && r.Type == o.Type && r.Label == o.Label && r.Name == o.Name && r.Qualifier.Equals(o.Qualifier) } + +func (r *Mqueue) String() string { + return renderTemplate(tokMQUEUE, r) +} diff --git a/pkg/aa/network.go b/pkg/aa/network.go index ec8188ec..d4fd9669 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -4,6 +4,9 @@ package aa +const tokNETWORK = "network" + + type AddressExpr struct { Source string Destination string @@ -76,3 +79,7 @@ func (r *Network) Equals(other any) bool { r.Protocol == o.Protocol && r.AddressExpr.Equals(o.AddressExpr) && r.Qualifier.Equals(o.Qualifier) } + +func (r *Network) String() string { + return renderTemplate(tokNETWORK, r) +} diff --git a/pkg/aa/pivot_root.go b/pkg/aa/pivot_root.go index 94f289c5..66829f56 100644 --- a/pkg/aa/pivot_root.go +++ b/pkg/aa/pivot_root.go @@ -4,6 +4,8 @@ package aa +const tokPIVOTROOT = "pivot_root" + type PivotRoot struct { RuleBase Qualifier @@ -42,3 +44,7 @@ func (r *PivotRoot) Equals(other any) bool { r.TargetProfile == o.TargetProfile && r.Qualifier.Equals(o.Qualifier) } + +func (r *PivotRoot) String() string { + return renderTemplate(tokPIVOTROOT, r) +} diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index d10b4dfb..6970bee5 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -6,6 +6,12 @@ package aa import ( "slices" + +const ( + tokABI = "abi" + tokALIAS = "alias" + tokINCLUDE = "include" + tokIFEXISTS = "if exists" ) type Abi struct { @@ -27,6 +33,10 @@ func (r *Abi) Equals(other any) bool { return r.Path == o.Path && r.IsMagic == o.IsMagic } +func (r *Abi) String() string { + return renderTemplate(tokABI, r) +} + type Alias struct { RuleBase Path string @@ -46,6 +56,10 @@ func (r Alias) Equals(other any) bool { return r.Path == o.Path && r.RewrittenPath == o.RewrittenPath } +func (r *Alias) String() string { + return renderTemplate(tokALIAS, r) +} + type Include struct { RuleBase IfExists bool @@ -69,6 +83,10 @@ func (r *Include) Equals(other any) bool { return r.Path == o.Path && r.IsMagic == o.IsMagic && r.IfExists == o.IfExists } +func (r *Include) String() string { + return renderTemplate(tokINCLUDE, r) +} + type Variable struct { RuleBase Name string @@ -90,3 +108,7 @@ func (r *Variable) Equals(other any) bool { o, _ := other.(*Variable) return r.Name == o.Name && slices.Equal(r.Values, o.Values) } + +func (r *Variable) String() string { + return renderTemplate("variable", r) +} diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 24576e20..c47f33f2 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -10,6 +10,12 @@ import ( "strings" ) +const ( + tokATTRIBUTES = "xattrs" + tokFLAGS = "flags" + tokPROFILE = "profile" +) + // Profile represents a single AppArmor profile. type Profile struct { RuleBase @@ -40,6 +46,11 @@ func (p *Profile) Equals(other any) bool { slices.Equal(p.Flags, o.Flags) } +func (p *Profile) String() string { + return renderTemplate(tokPROFILE, p) +} + + // AddRule adds a new rule to the profile from a log map. func (p *Profile) AddRule(log map[string]string) { diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index 17b04cb9..ffe69dfc 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -4,6 +4,8 @@ package aa +const tokPTRACE = "ptrace" + type Ptrace struct { RuleBase Qualifier @@ -36,3 +38,7 @@ func (r *Ptrace) Equals(other any) bool { return slices.Equal(r.Access, o.Access) && r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier) } + +func (r *Ptrace) String() string { + return renderTemplate(tokPTRACE, r) +} diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index 66ed3c5e..585211e7 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -4,6 +4,12 @@ package aa +const ( + tokRLIMIT = "rlimit" + tokSET = "set" +) + + type Rlimit struct { RuleBase Key string @@ -35,3 +41,7 @@ func (r *Rlimit) Equals(other any) bool { o, _ := other.(*Rlimit) return r.Key == o.Key && r.Op == o.Op && r.Value == o.Value } + +func (r *Rlimit) String() string { + return renderTemplate(tokRLIMIT, r) +} diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 16b06503..ed3594e6 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -8,14 +8,27 @@ import ( "strings" ) +const ( + tokALL = "all" + tokALLOW = "allow" + tokAUDIT = "audit" + tokDENY = "deny" +) + // Rule generic interface for all AppArmor rules type Rule interface { Less(other any) bool Equals(other any) bool + String() string } type Rules []Rule +func (r Rules) String() string { + return renderTemplate("rules", r) +} + + type RuleBase struct { Comment string NoNewPrivs bool @@ -69,6 +82,10 @@ func (r RuleBase) Equals(other any) bool { return false } +func (r RuleBase) String() string { + return renderTemplate("comment", r) +} + type Qualifier struct { Audit bool AccessType string @@ -104,3 +121,7 @@ func (r *All) Less(other any) bool { func (r *All) Equals(other any) bool { return false } + +func (r *All) String() string { + return renderTemplate(tokALL, r) +} diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 96d7a5df..1f035278 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -367,3 +367,113 @@ func TestRule_Equals(t *testing.T) { }) } } + +func TestRule_String(t *testing.T) { + tests := []struct { + name string + rule Rule + want string + }{ + { + name: "include1", + rule: include1, + want: "include ", + }, + { + name: "include-local", + rule: includeLocal1, + want: "include if exists ", + }, + { + name: "include-abs", + rule: &Include{Path: "/usr/share/apparmor.d/", IsMagic: false}, + want: `include "/usr/share/apparmor.d/"`, + }, + { + name: "rlimit", + rule: rlimit1, + want: "set rlimit nproc <= 200,", + }, + { + name: "capability", + rule: capability1, + want: "capability net_admin,", + }, + { + name: "capability/multi", + rule: &Capability{Names: []string{"dac_override", "dac_read_search"}}, + want: "capability dac_override dac_read_search,", + }, + { + name: "capability/all", + rule: &Capability{}, + want: "capability,", + }, + { + name: "network", + rule: network1, + want: "network netlink raw,", + }, + { + name: "mount", + rule: mount1, + want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check", + }, + { + name: "pivot_root", + rule: pivotroot1, + want: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,", + }, + { + name: "change_profile", + rule: changeprofile1, + want: "change_profile -> systemd-user,", + }, + { + name: "signal", + rule: signal1, + want: "signal receive set=kill peer=firefox//&firejail-default,", + }, + { + name: "ptrace", + rule: ptrace1, + want: "ptrace read peer=nautilus,", + }, + { + name: "unix", + rule: unix1, + want: "unix (receive send) type=stream protocol=0 addr=none peer=(label=dbus-daemon, addr=@/tmp/dbus-AaKMpxzC4k),", + }, + { + name: "dbus", + rule: dbus1, + want: `dbus receive bus=session path=/org/gtk/vfs/metadata + interface=org.gtk.vfs.Metadata + member=Remove + peer=(name=:1.15, label=tracker-extract),`, + }, + { + name: "dbus-bind", + rule: &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"}, + want: `dbus bind bus=session name=org.gnome.*,`, + }, + { + name: "dbus-full", + rule: &Dbus{Bus: "accessibility"}, + want: `dbus bus=accessibility,`, + }, + { + name: "file", + rule: file1, + want: "/usr/share/poppler/cMap/Identity-H r,", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.rule + if got := r.String(); got != tt.want { + t.Errorf("Rule.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 13deb74d..237607d2 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -4,6 +4,9 @@ package aa + +const tokSIGNAL = "signal" + type Signal struct { RuleBase Qualifier @@ -41,3 +44,7 @@ func (r *Signal) Equals(other any) bool { return slices.Equal(r.Access, o.Access) && slices.Equal(r.Set, o.Set) && r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier) } + +func (r *Signal) String() string { + return renderTemplate(tokSIGNAL, r) +} diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 2e94480b..ab976469 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -8,29 +8,40 @@ import ( "embed" "fmt" "reflect" + "slices" "strings" "text/template" ) -// Default indentation for apparmor profile (2 spaces) -const indentation = " " - var ( + // Default indentation for apparmor profile (2 spaces) + TemplateIndentation = " " + + // The current indentation level + TemplateIndentationLevel = 0 + //go:embed templates/*.j2 + //go:embed templates/rule/*.j2 tmplFiles embed.FS // The functions available in the template tmplFunctionMap = template.FuncMap{ "typeof": typeOf, "join": join, + "cjoin": cjoin, "indent": indent, "overindent": indentDbus, + "setindent": setindent, } // The apparmor templates - tmpl = map[string]*template.Template{ - "apparmor": generateTemplate("apparmor.j2"), - } + tmpl = generateTemplates([]string{ + "apparmor", tokPROFILE, "rules", // Global templates + tokINCLUDE, tokRLIMIT, tokCAPABILITY, tokNETWORK, + tokMOUNT, tokPIVOTROOT, tokCHANGEPROFILE, tokSIGNAL, + tokPTRACE, tokUNIX, tokUSERNS, tokIOURING, + tokDBUS, "file", + }) // convert apparmor requested mask to apparmor access mode maskToAccess = map[string]string{ @@ -90,30 +101,35 @@ var ( fileWeights = map[string]int{} ) -func generateTemplate(name string) *template.Template { - res := template.New(name).Funcs(tmplFunctionMap) - switch name { - case "apparmor.j2": - res = template.Must(res.ParseFS(tmplFiles, - "templates/*.j2", "templates/rule/*.j2", - )) - case "profile.j2": - res = template.Must(res.Parse("{{ template \"profile\" . }}")) - res = template.Must(res.ParseFS(tmplFiles, - "templates/profile.j2", "templates/rule/*.j2", - )) - default: - res = template.Must(res.Parse( - fmt.Sprintf("{{ template \"%s\" . }}", name), - )) - res = template.Must(res.ParseFS(tmplFiles, - fmt.Sprintf("templates/rule/%s.j2", name), - "templates/rule/qualifier.j2", "templates/rule/comment.j2", +func generateTemplates(names []string) map[string]*template.Template { + res := make(map[string]*template.Template, len(names)) + base := template.New("").Funcs(tmplFunctionMap) + base = template.Must(base.ParseFS(tmplFiles, + "templates/*.j2", "templates/rule/*.j2", + )) + for _, name := range names { + t := template.Must(base.Clone()) + t = template.Must(t.Parse( + fmt.Sprintf(`{{- template "%s" . -}}`, name), )) + res[name] = t } return res } +func renderTemplate(name string, data any) string { + var res strings.Builder + template, ok := tmpl[name] + if !ok { + panic("template not found") + } + err := template.Execute(&res, data) + if err != nil { + panic(err) + } + return res.String() +} + func init() { for i, r := range fileAlphabet { fileWeights[r] = i @@ -138,6 +154,25 @@ func join(i any) string { } } +func cjoin(i any) string { + switch reflect.TypeOf(i).Kind() { + case reflect.Slice: + s := i.([]string) + if len(s) == 1 { + return s[0] + } + return "(" + strings.Join(s, " ") + ")" + case reflect.Map: + res := []string{} + for k, v := range i.(map[string]string) { + res = append(res, k+"="+v) + } + return "(" + strings.Join(res, " ") + ")" + default: + return i.(string) + } +} + func typeOf(i any) string { return strings.TrimPrefix(reflect.TypeOf(i).String(), "*aa.") } @@ -146,12 +181,22 @@ func typeToValue(i reflect.Type) string { return strings.ToLower(strings.TrimPrefix(i.String(), "*aa.")) } +func setindent(i string) string { + switch i { + case "++": + TemplateIndentationLevel++ + case "--": + TemplateIndentationLevel-- + } + return "" +} + func indent(s string) string { - return indentation + s + return strings.Repeat(TemplateIndentation, TemplateIndentationLevel) + s } func indentDbus(s string) string { - return indentation + " " + s + return strings.Join([]string{TemplateIndentation, s}, " ") } func getLetterIn(alphabet []string, in string) string { diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index f734f327..ee92fe38 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -4,6 +4,8 @@ package aa +const tokUNIX = "unix" + type Unix struct { RuleBase Qualifier @@ -74,3 +76,7 @@ func (r *Unix) Equals(other any) bool { r.PeerLabel == o.PeerLabel && r.PeerAddr == o.PeerAddr && r.Qualifier.Equals(o.Qualifier) } + +func (r *Unix) String() string { + return renderTemplate(tokUNIX, r) +} diff --git a/pkg/aa/userns.go b/pkg/aa/userns.go index e6c41bc0..24087e11 100644 --- a/pkg/aa/userns.go +++ b/pkg/aa/userns.go @@ -4,6 +4,8 @@ package aa +const tokUSERNS = "userns" + type Userns struct { RuleBase Qualifier @@ -30,3 +32,7 @@ func (r *Userns) Equals(other any) bool { o, _ := other.(*Userns) return r.Create == o.Create && r.Qualifier.Equals(o.Qualifier) } + +func (r *Userns) String() string { + return renderTemplate(tokUSERNS, r) +} From 120db93396a00fc673032bab247b5e7547d9fbe6 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:27:35 +0100 Subject: [PATCH 12/62] feat(aa): refractor apparmor templates to the last changes. --- pkg/aa/templates/apparmor.j2 | 58 +++++++++----- pkg/aa/templates/profile.j2 | 120 ++++------------------------ pkg/aa/templates/rule/abi.j2 | 14 ++++ pkg/aa/templates/rule/alias.j2 | 12 +++ pkg/aa/templates/rule/capability.j2 | 8 +- pkg/aa/templates/rule/comment.j2 | 30 ++++--- pkg/aa/templates/rule/dbus.j2 | 19 +++-- pkg/aa/templates/rule/file.j2 | 4 +- pkg/aa/templates/rule/mount.j2 | 6 +- pkg/aa/templates/rule/mqueue.j2 | 2 +- pkg/aa/templates/rule/ptrace.j2 | 2 +- pkg/aa/templates/rule/signal.j2 | 4 +- pkg/aa/templates/rule/unix.j2 | 2 +- pkg/aa/templates/rule/variable.j2 | 14 ++++ pkg/aa/templates/rules.j2 | 93 +++++++++++++++++++++ 15 files changed, 236 insertions(+), 152 deletions(-) create mode 100644 pkg/aa/templates/rule/abi.j2 create mode 100644 pkg/aa/templates/rule/alias.j2 create mode 100644 pkg/aa/templates/rule/variable.j2 create mode 100644 pkg/aa/templates/rules.j2 diff --git a/pkg/aa/templates/apparmor.j2 b/pkg/aa/templates/apparmor.j2 index 821341b5..1686a7de 100644 --- a/pkg/aa/templates/apparmor.j2 +++ b/pkg/aa/templates/apparmor.j2 @@ -2,26 +2,48 @@ {{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} {{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} -{{- range .Abi -}} - {{- if .IsMagic -}} - {{ "abi <" }}{{ .Path }}{{ ">,\n" }} - {{- else -}} - {{ "abi \"" }}{{ .Path }}{{ "\",\n" }} - {{- end }} -{{ end -}} +{{- define "apparmor" -}} -{{- range .Aliases -}} - {{ "alias " }}{{ .Path }}{{ " -> " }}{{ .RewrittenPath }}{{ ",\n" }} -{{ end -}} + {{- with .Comments -}} + {{- range . -}} + {{- template "comment" . -}} + {{- "\n" -}} + {{- end -}} + {{- "\n" -}} + {{- end -}} -{{- range .Includes -}} - {{ template "include" . }}{{ "\n" }} -{{ end -}} + {{- with .Abi -}} + {{- range . -}} + {{- template "abi" . -}} + {{- "\n" -}} + {{- end -}} + {{- "\n" -}} + {{- end -}} -{{- range .Variables -}} - {{ "@{" }}{{ .Name }}{{ "} = " }}{{ join .Values }} -{{ end -}} + {{- with .Aliases -}} + {{- range . -}} + {{- template "alias" . -}} + {{- "\n" -}} + {{- end -}} + {{- "\n" -}} + {{- end -}} + + {{- with .Includes -}} + {{- range . -}} + {{- template "include" . -}} + {{- "\n" -}} + {{- end -}} + {{- "\n" -}} + {{- end -}} + + {{- range .Variables -}} + {{- template "variable" . -}} + {{- "\n" -}} + {{- end -}} + + {{- range .Profiles -}} + {{- template "profile" . -}} + {{- "\n" -}} + {{- end -}} -{{- range .Profiles -}} - {{ template "profile" . }} {{- end -}} diff --git a/pkg/aa/templates/profile.j2 b/pkg/aa/templates/profile.j2 index 394f18a1..f2df9069 100644 --- a/pkg/aa/templates/profile.j2 +++ b/pkg/aa/templates/profile.j2 @@ -4,110 +4,24 @@ {{- define "profile" -}} - {{- with .Header -}} - {{- "profile" -}} - {{- with .Name -}} - {{ " " }}{{ . }} - {{- end -}} - {{- with .Attachments -}} - {{ " " }}{{ join . }} - {{- end -}} - {{- with .Attributes -}} - {{ " xattrs=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- with .Flags -}} - {{ " flags=(" }}{{ join . }}{{ ")" }} - {{- end -}} - {{- " {\n" -}} + {{- "profile" -}} + {{- with .Name -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Attachments -}} + {{ " " }}{{ join . }} + {{- end -}} + {{- with .Attributes -}} + {{ " xattrs=(" }}{{ join . }}{{ ")" }} + {{- end -}} + {{- with .Flags -}} + {{ " flags=(" }}{{ join . }}{{ ")" }} {{- end -}} - {{- $oldtype := "" -}} - {{- range .Rules -}} - {{- $type := typeof . -}} - {{- if eq $type "Rule" -}} - {{- template "comment" . -}} - {{- "\n" -}} - {{- continue -}} - {{- end -}} - {{- if and (ne $type $oldtype) (ne $oldtype "") -}} - {{- "\n" -}} - {{- end -}} - {{- indent "" -}} - - {{- if eq $type "Include" -}} - {{ template "include" . }} - {{- end -}} - - {{- if eq $type "Rlimit" -}} - {{- template "rlimit" . -}} - {{- end -}} - - {{- if eq $type "Userns" -}} - {{- template "userns" . -}} - {{- end -}} - - {{- if eq $type "Capability" -}} - {{- template "capability" . -}} - {{- end -}} - - {{- if eq $type "Network" -}} - {{- template "network" . -}} - {{- end -}} - - {{- if eq $type "Mount" -}} - {{- template "mount" . -}} - {{- end -}} - - {{- if eq $type "Remount" -}} - {{- template "remount" . -}} - {{- end -}} - - {{- if eq $type "Umount" -}} - {{- template "umount" . -}} - {{- end -}} - - {{- if eq $type "PivotRoot" -}} - {{- template "pivot_root" . -}} - {{- end -}} - - {{- if eq $type "ChangeProfile" -}} - {{- template "change_profile" . -}} - {{- end -}} - - {{- if eq $type "Mqueue" -}} - {{- template "mqueue" . -}} - {{- end -}} - - {{- if eq $type "Unix" -}} - {{- template "unix" . -}} - {{- end -}} - - {{- if eq $type "Ptrace" -}} - {{- template "ptrace" . -}} - {{- end -}} - - {{- if eq $type "Signal" -}} - {{- template "signal" . -}} - {{- end -}} - - {{- if eq $type "Dbus" -}} - {{- template "dbus" . -}} - {{- end -}} - - {{- if eq $type "File" -}} - {{- template "file" . -}} - {{- end -}} - - {{- if eq $type "Profile" -}} - {{ template "profile" . }} - {{- end -}} - - {{- "\n" -}} - {{- $oldtype = $type -}} - {{- end -}} - - {{- with .Header -}} - {{- "}\n" -}} - {{- end -}} + {{- " {\n" -}} + {{- setindent "++" -}} + {{- template "rules" .Rules -}} + {{- setindent "--" -}} + {{- indent "}" -}} {{- end -}} diff --git a/pkg/aa/templates/rule/abi.j2 b/pkg/aa/templates/rule/abi.j2 new file mode 100644 index 00000000..09840a8d --- /dev/null +++ b/pkg/aa/templates/rule/abi.j2 @@ -0,0 +1,14 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "abi" -}} + {{- "abi" -}} + {{- if .IsMagic -}} + {{ " <" }}{{ .Path }}{{ ">" }} + {{- else -}} + {{ " \"" }}{{ .Path }}{{ "\"" }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rule/alias.j2 b/pkg/aa/templates/rule/alias.j2 new file mode 100644 index 00000000..2912d334 --- /dev/null +++ b/pkg/aa/templates/rule/alias.j2 @@ -0,0 +1,12 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "alias" -}} + {{- "alias " -}} + {{- .Path -}} + {{- " -> " -}} + {{- .RewrittenPath -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rule/capability.j2 b/pkg/aa/templates/rule/capability.j2 index 4041ab11..5b46e73a 100644 --- a/pkg/aa/templates/rule/capability.j2 +++ b/pkg/aa/templates/rule/capability.j2 @@ -3,5 +3,11 @@ {{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} {{- define "capability" -}} - {{ template "qualifier" . }}{{ "capability " }}{{ .Name }}{{ "," }}{{ template "comment" . }} + {{- template "qualifier" . -}} + {{- "capability" -}} + {{- range .Names -}} + {{ " " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} {{- end -}} diff --git a/pkg/aa/templates/rule/comment.j2 b/pkg/aa/templates/rule/comment.j2 index 68fc20a8..2a752288 100644 --- a/pkg/aa/templates/rule/comment.j2 +++ b/pkg/aa/templates/rule/comment.j2 @@ -4,18 +4,22 @@ {{- define "comment" -}} {{- if or .FileInherit .NoNewPrivs .Optional .Comment -}} - {{- " #" -}} - {{- end -}} - {{- if .FileInherit -}} - {{- " file_inherit" -}} - {{- end -}} - {{- if .NoNewPrivs -}} - {{- " no new privs" -}} - {{- end -}} - {{- if .Optional -}} - {{- " optional:" -}} - {{- end -}} - {{- with .Comment -}} - {{ " " }}{{ . }} + {{- if .IsLineRule }} + {{- "#" -}} + {{- else -}} + {{- " #" -}} + {{- end -}} + {{- if .FileInherit -}} + {{- " file_inherit" -}} + {{- end -}} + {{- if .NoNewPrivs -}} + {{- " no new privs" -}} + {{- end -}} + {{- if .Optional -}} + {{- " optional:" -}} + {{- end -}} + {{- with .Comment -}} + {{ " " }}{{ . }} + {{- end -}} {{- end -}} {{- end -}} diff --git a/pkg/aa/templates/rule/dbus.j2 b/pkg/aa/templates/rule/dbus.j2 index a25b87ef..f3227ad7 100644 --- a/pkg/aa/templates/rule/dbus.j2 +++ b/pkg/aa/templates/rule/dbus.j2 @@ -5,11 +5,15 @@ {{- define "dbus" -}} {{- template "qualifier" . -}} {{- "dbus" -}} - {{- if eq .Access "bind" -}} + {{- $access := "" -}} + {{- if .Access -}} + {{- $access = index .Access 0 -}} + {{- end -}} + {{- if eq $access "bind" -}} {{ " bind bus=" }}{{ .Bus }}{{ " name=" }}{{ .Name }} {{- else -}} {{- with .Access -}} - {{ " " }}{{ . }} + {{ " " }}{{ cjoin . }} {{- end -}} {{- with .Bus -}} {{ " bus=" }}{{ . }} @@ -17,21 +21,20 @@ {{- with .Path -}} {{ " path=" }}{{ . }} {{- end -}} - {{ "\n" }} {{- with .Interface -}} - {{ overindent "interface=" }}{{ . }}{{ "\n" }} + {{ "\n" }}{{ overindent "interface=" }}{{ . }} {{- end -}} {{- with .Member -}} - {{ overindent "member=" }}{{ . }}{{ "\n" }} + {{ "\n" }}{{ overindent "member=" }}{{ . }} {{- end -}} {{- if and .PeerName .PeerLabel -}} - {{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }} + {{ "\n" }}{{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }} {{- else -}} {{- with .PeerName -}} - {{ overindent "peer=(name=" }}{{ . }}{{ ")" }} + {{ "\n" }}{{ overindent "peer=(name=" }}{{ . }}{{ ")" }} {{- end -}} {{- with .PeerLabel -}} - {{ overindent "peer=(label=" }}{{ . }}{{ ")" }} + {{ "\n" }}{{ overindent "peer=(label=" }}{{ . }}{{ ")" }} {{- end -}} {{- end -}} {{- end -}} diff --git a/pkg/aa/templates/rule/file.j2 b/pkg/aa/templates/rule/file.j2 index ea016e77..0021a874 100644 --- a/pkg/aa/templates/rule/file.j2 +++ b/pkg/aa/templates/rule/file.j2 @@ -12,7 +12,9 @@ {{- with .Padding -}} {{ . }} {{- end -}} - {{- .Access -}} + {{- range .Access -}} + {{- . -}} + {{- end -}} {{- with .Target -}} {{ " -> " }}{{ . }} {{- end -}} diff --git a/pkg/aa/templates/rule/mount.j2 b/pkg/aa/templates/rule/mount.j2 index 19d29b13..c97ead10 100644 --- a/pkg/aa/templates/rule/mount.j2 +++ b/pkg/aa/templates/rule/mount.j2 @@ -9,7 +9,7 @@ {{ " fstype=" }}{{ . }} {{- end -}} {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} + {{ " options=" }}{{ cjoin . }} {{- end -}} {{- with .Source -}} {{ " " }}{{ . }} @@ -28,7 +28,7 @@ {{ " fstype=" }}{{ . }} {{- end -}} {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} + {{ " options=" }}{{ cjoin . }} {{- end -}} {{- with .MountPoint -}} {{ " " }}{{ . }} @@ -44,7 +44,7 @@ {{ " fstype=" }}{{ . }} {{- end -}} {{- with .Options -}} - {{ " options=(" }}{{ join . }}{{ ")" }} + {{ " options=" }}{{ cjoin . }} {{- end -}} {{- with .MountPoint -}} {{ " " }}{{ . }} diff --git a/pkg/aa/templates/rule/mqueue.j2 b/pkg/aa/templates/rule/mqueue.j2 index 48b764aa..e2df2756 100644 --- a/pkg/aa/templates/rule/mqueue.j2 +++ b/pkg/aa/templates/rule/mqueue.j2 @@ -6,7 +6,7 @@ {{- template "qualifier" . -}} {{- "mqueue" -}} {{- with .Access -}} - {{ " " }}{{ . }} + {{ " " }}{{ cjoin . }} {{- end -}} {{- with .Type -}} {{ " type=" }}{{ . }} diff --git a/pkg/aa/templates/rule/ptrace.j2 b/pkg/aa/templates/rule/ptrace.j2 index 95318a28..c499890b 100644 --- a/pkg/aa/templates/rule/ptrace.j2 +++ b/pkg/aa/templates/rule/ptrace.j2 @@ -6,7 +6,7 @@ {{- template "qualifier" . -}} {{- "ptrace" -}} {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} + {{ " " }}{{ cjoin . }} {{- end -}} {{- with .Peer -}} {{ " peer=" }}{{ . }} diff --git a/pkg/aa/templates/rule/signal.j2 b/pkg/aa/templates/rule/signal.j2 index b0fdbc35..b56085d8 100644 --- a/pkg/aa/templates/rule/signal.j2 +++ b/pkg/aa/templates/rule/signal.j2 @@ -6,10 +6,10 @@ {{- template "qualifier" . -}} {{- "signal" -}} {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} + {{ " " }}{{ cjoin . }} {{- end -}} {{- with .Set -}} - {{ " set=(" }}{{ . }}{{ ")" }} + {{ " set=" }}{{ cjoin . }} {{- end -}} {{- with .Peer -}} {{ " peer=" }}{{ . }} diff --git a/pkg/aa/templates/rule/unix.j2 b/pkg/aa/templates/rule/unix.j2 index fe1a6c7a..531eaaf9 100644 --- a/pkg/aa/templates/rule/unix.j2 +++ b/pkg/aa/templates/rule/unix.j2 @@ -6,7 +6,7 @@ {{- template "qualifier" . -}} {{- "unix" -}} {{- with .Access -}} - {{ " (" }}{{ . }}{{ ")" }} + {{ " " }}{{ cjoin . }} {{- end -}} {{- with .Type -}} {{ " type=" }}{{ . }} diff --git a/pkg/aa/templates/rule/variable.j2 b/pkg/aa/templates/rule/variable.j2 new file mode 100644 index 00000000..f27e01cc --- /dev/null +++ b/pkg/aa/templates/rule/variable.j2 @@ -0,0 +1,14 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "variable" -}} + {{- "@{" -}}{{- .Name -}}{{- "}" -}} + {{- if .Define }} + {{- " = " -}} + {{- else -}} + {{- " += " -}} + {{- end -}} + {{- join .Values -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rules.j2 b/pkg/aa/templates/rules.j2 new file mode 100644 index 00000000..4ce59626 --- /dev/null +++ b/pkg/aa/templates/rules.j2 @@ -0,0 +1,93 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "rules" -}} + + {{- $oldtype := "" -}} + {{- range . -}} + {{- $type := typeof . -}} + {{- if eq $type "RuleBase" -}} + {{- template "comment" . -}} + {{- "\n" -}} + {{- continue -}} + {{- end -}} + + {{- if and (ne $type $oldtype) (ne $oldtype "") -}} + {{- "\n" -}} + {{- end -}} + {{- indent "" -}} + + {{- if eq $type "Include" -}} + {{- template "include" . -}} + {{- end -}} + + {{- if eq $type "Rlimit" -}} + {{- template "rlimit" . -}} + {{- end -}} + + {{- if eq $type "Userns" -}} + {{- template "userns" . -}} + {{- end -}} + + {{- if eq $type "Capability" -}} + {{- template "capability" . -}} + {{- end -}} + + {{- if eq $type "Network" -}} + {{- template "network" . -}} + {{- end -}} + + {{- if eq $type "Mount" -}} + {{- template "mount" . -}} + {{- end -}} + + {{- if eq $type "Remount" -}} + {{- template "remount" . -}} + {{- end -}} + + {{- if eq $type "Umount" -}} + {{- template "umount" . -}} + {{- end -}} + + {{- if eq $type "PivotRoot" -}} + {{- template "pivot_root" . -}} + {{- end -}} + + {{- if eq $type "ChangeProfile" -}} + {{- template "change_profile" . -}} + {{- end -}} + + {{- if eq $type "Mqueue" -}} + {{- template "mqueue" . -}} + {{- end -}} + + {{- if eq $type "Unix" -}} + {{- template "unix" . -}} + {{- end -}} + + {{- if eq $type "Ptrace" -}} + {{- template "ptrace" . -}} + {{- end -}} + + {{- if eq $type "Signal" -}} + {{- template "signal" . -}} + {{- end -}} + + {{- if eq $type "Dbus" -}} + {{- template "dbus" . -}} + {{- end -}} + + {{- if eq $type "File" -}} + {{- template "file" . -}} + {{- end -}} + + {{- if eq $type "Profile" -}} + {{- template "profile" . -}} + {{- end -}} + + {{- "\n" -}} + {{- $oldtype = $type -}} + {{- end -}} + +{{- end -}} From 2923df2a73fd1b23c73bb14e32c1ab24fbf6361e Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:32:58 +0100 Subject: [PATCH 13/62] refractor(aa): move profile specific method to the profile struct. --- pkg/aa/apparmor.go | 66 +++------------------------------------------- pkg/aa/profile.go | 62 +++++++++++++++++++++++++++++++++++++++++++ pkg/aa/rules.go | 24 +++++++++++++++++ 3 files changed, 89 insertions(+), 63 deletions(-) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index 7a10c085..36ed44c8 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -5,12 +5,6 @@ package aa import ( - "bytes" - "reflect" - "slices" - "sort" - "strings" - "github.com/arduino/go-paths-helper" ) @@ -60,22 +54,7 @@ func (f *AppArmorProfileFile) GetDefaultProfile() *Profile { // Follow: https://apparmor.pujol.io/development/guidelines/#guidelines func (f *AppArmorProfileFile) Sort() { for _, p := range f.Profiles { - sort.Slice(p.Rules, func(i, j int) bool { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - valueOfI := typeToValue(typeOfI) - valueOfJ := typeToValue(typeOfJ) - if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists { - valueOfI = "include_if_exists" - } - if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists { - valueOfJ = "include_if_exists" - } - return ruleWeights[valueOfI] < ruleWeights[valueOfJ] - } - return p.Rules[i].Less(p.Rules[j]) - }) + p.Sort() } } @@ -87,53 +66,14 @@ func (f *AppArmorProfileFile) Sort() { // Note: logs.regCleanLogs helps a lot to do a first cleaning func (f *AppArmorProfileFile) MergeRules() { for _, p := range f.Profiles { - for i := 0; i < len(p.Rules); i++ { - for j := i + 1; j < len(p.Rules); j++ { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - continue - } - - // If rules are identical, merge them - if p.Rules[i].Equals(p.Rules[j]) { - p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) - j-- - } - } - } + p.Merge() } } // Format the profile for better readability before printing it. // Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block func (f *AppArmorProfileFile) Format() { - const prefixOwner = " " for _, p := range f.Profiles { - hasOwnerRule := false - for i := len(p.Rules) - 1; i > 0; i-- { - j := i - 1 - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - - // File rule - if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) { - letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) - letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) - - // Add prefix before rule path to align with other rule - if p.Rules[i].(*File).Owner { - hasOwnerRule = true - } else if hasOwnerRule { - p.Rules[i].(*File).Prefix = prefixOwner - } - - if letterI != letterJ { - // Add a new empty line between Files rule of different type - hasOwnerRule = false - p.Rules = append(p.Rules[:i], append([]ApparmorRule{&Rule{}}, p.Rules[i:]...)...) - } - } - } + p.Format() } } diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index c47f33f2..8fff8110 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -6,6 +6,7 @@ package aa import ( "maps" + "reflect" "slices" "strings" ) @@ -50,6 +51,67 @@ func (p *Profile) String() string { return renderTemplate(tokPROFILE, p) } +// Merge merge similar rules together. +// Steps: +// - Remove identical rules +// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw' +// +// Note: logs.regCleanLogs helps a lot to do a first cleaning +func (p *Profile) Merge() { + for i := 0; i < len(p.Rules); i++ { + for j := i + 1; j < len(p.Rules); j++ { + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + if typeOfI != typeOfJ { + continue + } + + // If rules are identical, merge them + if p.Rules[i].Equals(p.Rules[j]) { + p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) + j-- + } + } + } +} + +// Sort the rules in a profile. +// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines +func (p *Profile) Sort() { + p.Rules.Sort() +} + +// Format the profile for better readability before printing it. +// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block +func (p *Profile) Format() { + const prefixOwner = " " + + hasOwnerRule := false + for i := len(p.Rules) - 1; i > 0; i-- { + j := i - 1 + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + + // File rule + if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) { + letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) + letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) + + // Add prefix before rule path to align with other rule + if p.Rules[i].(*File).Owner { + hasOwnerRule = true + } else if hasOwnerRule { + p.Rules[i].(*File).Prefix = prefixOwner + } + + if letterI != letterJ { + // Add a new empty line between Files rule of different type + hasOwnerRule = false + p.Rules = append(p.Rules[:i], append([]Rule{&RuleBase{}}, p.Rules[i:]...)...) + } + } + } +} // AddRule adds a new rule to the profile from a log map. func (p *Profile) AddRule(log map[string]string) { diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index ed3594e6..0b818038 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -5,6 +5,8 @@ package aa import ( + "reflect" + "sort" "strings" ) @@ -28,8 +30,29 @@ func (r Rules) String() string { return renderTemplate("rules", r) } +// Sort the rules in a profile. +// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines +func (r Rules) Sort() { + sort.Slice(r, func(i, j int) bool { + typeOfI := reflect.TypeOf(r[i]) + typeOfJ := reflect.TypeOf(r[j]) + if typeOfI != typeOfJ { + valueOfI := typeToValue(typeOfI) + valueOfJ := typeToValue(typeOfJ) + if typeOfI == reflect.TypeOf((*Include)(nil)) && r[i].(*Include).IfExists { + valueOfI = "include_if_exists" + } + if typeOfJ == reflect.TypeOf((*Include)(nil)) && r[j].(*Include).IfExists { + valueOfJ = "include_if_exists" + } + return ruleWeights[valueOfI] < ruleWeights[valueOfJ] + } + return r[i].Less(r[j]) + }) +} type RuleBase struct { + IsLineRule bool Comment string NoNewPrivs bool FileInherit bool @@ -67,6 +90,7 @@ func newRuleFromLog(log map[string]string) RuleBase { } return RuleBase{ + IsLineRule: false, Comment: msg, NoNewPrivs: noNewPrivs, FileInherit: fileInherit, From a0b5362589df8193b4e0357e0382e4cdfc367fe3 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:35:23 +0100 Subject: [PATCH 14/62] refractor(aa): update test structure. --- pkg/aa/apparmor_test.go | 98 ++--------------------------------------- pkg/aa/profile_test.go | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 94 deletions(-) create mode 100644 pkg/aa/profile_test.go diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index ae39caef..f052e27a 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -28,7 +28,7 @@ func readprofile(path string) string { return res[:len(res)-1] } -func TestAppArmorProfile_String(t *testing.T) { +func TestAppArmorProfileFile_String(t *testing.T) { tests := []struct { name string f *AppArmorProfileFile @@ -124,97 +124,7 @@ func TestAppArmorProfile_String(t *testing.T) { } } -func TestAppArmorProfile_AddRule(t *testing.T) { - tests := []struct { - name string - log map[string]string - want *AppArmorProfileFile - }{ - { - name: "capability", - log: capability1Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{capability1}, - }}, - }, - }, - { - name: "network", - log: network1Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{network1}, - }}, - }, - }, - { - name: "mount", - log: mount2Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{mount2}, - }}, - }, - }, - { - name: "signal", - log: signal1Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{signal1}, - }}, - }, - }, - { - name: "ptrace", - log: ptrace2Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{ptrace2}, - }}, - }, - }, - { - name: "unix", - log: unix1Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{unix1}, - }}, - }, - }, - { - name: "dbus", - log: dbus2Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{dbus2}, - }}, - }, - }, - { - name: "file", - log: file2Log, - want: &AppArmorProfileFile{ - Profiles: []*Profile{{ - Rules: []Rule{file2}, - }}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := NewAppArmorProfile() - got.AddRule(tt.log) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("AppArmorProfile.AddRule() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAppArmorProfile_Sort(t *testing.T) { +func TestAppArmorProfileFile_Sort(t *testing.T) { tests := []struct { name string origin *AppArmorProfileFile @@ -251,7 +161,7 @@ func TestAppArmorProfile_Sort(t *testing.T) { } } -func TestAppArmorProfile_MergeRules(t *testing.T) { +func TestAppArmorProfileFile_MergeRules(t *testing.T) { tests := []struct { name string origin *AppArmorProfileFile @@ -282,7 +192,7 @@ func TestAppArmorProfile_MergeRules(t *testing.T) { } } -func TestAppArmorProfile_Integration(t *testing.T) { +func TestAppArmorProfileFile_Integration(t *testing.T) { tests := []struct { name string f *AppArmorProfileFile diff --git a/pkg/aa/profile_test.go b/pkg/aa/profile_test.go new file mode 100644 index 00000000..26ea6316 --- /dev/null +++ b/pkg/aa/profile_test.go @@ -0,0 +1,84 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "reflect" + "testing" +) + +func TestProfile_AddRule(t *testing.T) { + tests := []struct { + name string + log map[string]string + want *Profile + }{ + { + name: "capability", + log: capability1Log, + want: &Profile{ + Rules: Rules{capability1}, + }, + }, + { + name: "network", + log: network1Log, + want: &Profile{ + Rules: Rules{network1}, + }, + }, + { + name: "mount", + log: mount2Log, + want: &Profile{ + Rules: Rules{mount2}, + }, + }, + { + name: "signal", + log: signal1Log, + want: &Profile{ + Rules: Rules{signal1}, + }, + }, + { + name: "ptrace", + log: ptrace2Log, + want: &Profile{ + Rules: Rules{ptrace2}, + }, + }, + { + name: "unix", + log: unix1Log, + want: &Profile{ + Rules: Rules{unix1}, + }, + }, + { + name: "dbus", + log: dbus2Log, + want: &Profile{ + Rules: Rules{dbus2}, + }, + }, + { + name: "file", + log: file2Log, + want: &Profile{ + Rules: Rules{file2}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &Profile{} + got.AddRule(tt.log) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Profile.AddRule() = |%v|, want |%v|", got, tt.want) + } + }) + } +} From de73c9b706a7669c5f1fef9859d6b1d04d0422d0 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:38:52 +0100 Subject: [PATCH 15/62] test(aa): improve some internal unit test. Thanks to the last changes... --- pkg/aa/apparmor_test.go | 40 ++++++++++++++++++---------------------- pkg/aa/rules_test.go | 4 ++-- tests/string.aa | 9 +++++---- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index f052e27a..00d99d4f 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -6,27 +6,16 @@ package aa import ( "reflect" - "strings" "testing" "github.com/arduino/go-paths-helper" + "github.com/roddhjav/apparmor.d/pkg/util" ) -func readprofile(path string) string { - file := paths.New("../../").Join(path) - lines, err := file.ReadFileAsLines() - if err != nil { - panic(err) - } - res := "" - for _, line := range lines { - if strings.HasPrefix(line, "#") { - continue - } - res += line + "\n" - } - return res[:len(res)-1] -} +var ( + testData = paths.New("../../").Join("tests") + intData = paths.New("../../").Join("apparmor.d") +) func TestAppArmorProfileFile_String(t *testing.T) { tests := []struct { @@ -50,6 +39,7 @@ func TestAppArmorProfileFile_String(t *testing.T) { Name: "exec_path", Define: true, Values: []string{"@{bin}/foo", "@{lib}/foo"}, }}, + Comments: []*RuleBase{{Comment: "Simple test profile for the AppArmorProfileFile.String() method", IsLineRule: true}}, }, Profiles: []*Profile{{ Header: Header{ @@ -67,11 +57,12 @@ func TestAppArmorProfileFile_String(t *testing.T) { &Network{Domain: "inet", Type: "stream"}, &Network{Domain: "inet6", Type: "stream"}, &Mount{ + RuleBase: RuleBase{Comment: "failed perms check"}, MountConditions: MountConditions{ FsType: "fuse.portal", Options: []string{"rw", "rbind"}, }, - Source: "@{run}/user/@{uid}/ ", + Source: "@{run}/user/@{uid}/", MountPoint: "/", }, &Umount{ @@ -112,7 +103,7 @@ func TestAppArmorProfileFile_String(t *testing.T) { }, }}, }, - want: readprofile("tests/string.aa"), + want: util.MustReadFile(testData.Join("string.aa")), }, } for _, tt := range tests { @@ -205,9 +196,14 @@ func TestAppArmorProfileFile_Integration(t *testing.T) { Abi: []*Abi{{IsMagic: true, Path: "abi/3.0"}}, Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, Variables: []*Variable{{ - Name: "exec_path", + Name: "exec_path", Define: true, Values: []string{"@{bin}/aa-status", "@{bin}/apparmor_status"}, }}, + Comments: []*RuleBase{ + {Comment: "apparmor.d - Full set of apparmor profiles", IsLineRule: true}, + {Comment: "Copyright (C) 2021-2024 Alexandre Pujol ", IsLineRule: true}, + {Comment: "SPDX-License-Identifier: GPL-2.0-only", IsLineRule: true}, + }, }, Profiles: []*Profile{{ Header: Header{ @@ -232,7 +228,7 @@ func TestAppArmorProfileFile_Integration(t *testing.T) { }, }}, }, - want: readprofile("apparmor.d/profiles-a-f/aa-status"), + want: util.MustReadFile(intData.Join("profiles-a-f/aa-status")), }, } for _, tt := range tests { @@ -240,8 +236,8 @@ func TestAppArmorProfileFile_Integration(t *testing.T) { tt.f.Sort() tt.f.MergeRules() tt.f.Format() - if got := tt.f.String(); "\n"+got != tt.want { - t.Errorf("AppArmorProfile = |%v|, want |%v|", "\n"+got, tt.want) + if got := tt.f.String(); got != tt.want { + t.Errorf("AppArmorProfile = |%v|, want |%v|", got, tt.want) } }) } diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 1f035278..7bbd119b 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -193,13 +193,13 @@ func TestRule_Less(t *testing.T) { name: "signal", rule: signal1, other: signal2, - want: true, + want: false, }, { name: "ptrace/less", rule: ptrace1, other: ptrace2, - want: true, + want: false, }, { name: "ptrace/more", diff --git a/tests/string.aa b/tests/string.aa index 896ac0b0..2ec5d3a6 100644 --- a/tests/string.aa +++ b/tests/string.aa @@ -1,4 +1,5 @@ -# Simple test profile for the AppArmorProfile.String() method +# Simple test profile for the AppArmorProfileFile.String() method + abi , alias /mnt/usr -> /usr, @@ -18,13 +19,13 @@ profile foo @{exec_path} xattrs=(security.tagged=allowed) flags=(complain attach network inet stream, network inet6 stream, - mount fstype=fuse.portal options=(rw rbind) @{run}/user/@{uid}/ -> /, + mount fstype=fuse.portal options=(rw rbind) @{run}/user/@{uid}/ -> /, # failed perms check umount @{run}/user/@{uid}/, - signal (receive) set=(term) peer=at-spi-bus-launcher, + signal receive set=term peer=at-spi-bus-launcher, - ptrace (read) peer=nautilus, + ptrace read peer=nautilus, unix (send receive) type=stream addr=@/tmp/.ICE-unix/1995 peer=(label=gnome-shell, addr=none), From 8bb6f079501421c3418eb3e153144fdef627addf Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:43:22 +0100 Subject: [PATCH 16/62] feat(prebuilt): update aa usage to the last changes. --- pkg/prebuild/directive/dbus.go | 49 +++++++++++++++-------------- pkg/prebuild/directive/exec.go | 20 ++++++------ pkg/prebuild/directive/exec_test.go | 2 +- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/pkg/prebuild/directive/dbus.go b/pkg/prebuild/directive/dbus.go index 9b93ec8e..e1e64a64 100644 --- a/pkg/prebuild/directive/dbus.go +++ b/pkg/prebuild/directive/dbus.go @@ -51,17 +51,20 @@ func setInterfaces(rules map[string]string) []string { } func (d Dbus) Apply(opt *Option, profile string) string { - var p *aa.AppArmorProfileFile + var r aa.Rules action := d.sanityCheck(opt) switch action { case "own": - p = d.own(opt.ArgMap) + r = d.own(opt.ArgMap) case "talk": - p = d.talk(opt.ArgMap) + r = d.talk(opt.ArgMap) } - generatedDbus := p.String() + aa.TemplateIndentationLevel = strings.Count( + strings.SplitN(opt.Raw, Keyword, 1)[0], aa.TemplateIndentation, + ) + generatedDbus := r.String() lenDbus := len(generatedDbus) generatedDbus = generatedDbus[:lenDbus-1] profile = strings.Replace(profile, opt.Raw, generatedDbus, -1) @@ -95,16 +98,15 @@ func (d Dbus) sanityCheck(opt *Option) string { return action } -func (d Dbus) own(rules map[string]string) *aa.AppArmorProfileFile { +func (d Dbus) own(rules map[string]string) aa.Rules { interfaces := setInterfaces(rules) - profile := &aa.AppArmorProfileFile{} - p := profile.GetDefaultProfile() - p.Rules = append(p.Rules, &aa.Dbus{ - Access: "bind", Bus: rules["bus"], Name: rules["name"], + res := aa.Rules{} + res = append(res, &aa.Dbus{ + Access: []string{"bind"}, Bus: rules["bus"], Name: rules["name"], }) for _, iface := range interfaces { - p.Rules = append(p.Rules, &aa.Dbus{ - Access: "receive", + res = append(res, &aa.Dbus{ + Access: []string{"receive"}, Bus: rules["bus"], Path: rules["path"], Interface: iface, @@ -112,32 +114,31 @@ func (d Dbus) own(rules map[string]string) *aa.AppArmorProfileFile { }) } for _, iface := range interfaces { - p.Rules = append(p.Rules, &aa.Dbus{ - Access: "send", + res = append(res, &aa.Dbus{ + Access: []string{"send"}, Bus: rules["bus"], Path: rules["path"], Interface: iface, PeerName: `"{:1.@{int},org.freedesktop.DBus}"`, }) } - p.Rules = append(p.Rules, &aa.Dbus{ - Access: "receive", + res = append(res, &aa.Dbus{ + Access: []string{"receive"}, Bus: rules["bus"], Path: rules["path"], Interface: "org.freedesktop.DBus.Introspectable", Member: "Introspect", PeerName: `":1.@{int}"`, }) - return profile + return res } -func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfileFile { +func (d Dbus) talk(rules map[string]string) aa.Rules { interfaces := setInterfaces(rules) - profile := &aa.AppArmorProfileFile{} - p := profile.GetDefaultProfile() + res := aa.Rules{} for _, iface := range interfaces { - p.Rules = append(p.Rules, &aa.Dbus{ - Access: "send", + res = append(res, &aa.Dbus{ + Access: []string{"send"}, Bus: rules["bus"], Path: rules["path"], Interface: iface, @@ -146,8 +147,8 @@ func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfileFile { }) } for _, iface := range interfaces { - p.Rules = append(p.Rules, &aa.Dbus{ - Access: "receive", + res = append(res, &aa.Dbus{ + Access: []string{"receive"}, Bus: rules["bus"], Path: rules["path"], Interface: iface, @@ -155,5 +156,5 @@ func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfileFile { PeerLabel: rules["label"], }) } - return profile + return res } diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index 9296ca96..b2899baa 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -36,8 +36,7 @@ func (d Exec) Apply(opt *Option, profileRaw string) string { delete(opt.ArgMap, t) } - profile := &aa.AppArmorProfileFile{} - p := profile.GetDefaultProfile() + rules := aa.Rules{} for name := range opt.ArgMap { profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name)) dstProfile := aa.DefaultTunables() @@ -45,18 +44,21 @@ func (d Exec) Apply(opt *Option, profileRaw string) string { for _, variable := range dstProfile.Variables { if variable.Name == "exec_path" { for _, v := range variable.Values { - p.Rules = append(p.Rules, &aa.File{ + rules = append(rules, &aa.File{ Path: v, - Access: transition, + Access: []string{transition}, }) } break } } } - profile.Sort() - rules := profile.String() - lenRules := len(rules) - rules = rules[:lenRules-1] - return strings.Replace(profileRaw, opt.Raw, rules, -1) + + aa.TemplateIndentationLevel = strings.Count( + strings.SplitN(opt.Raw, Keyword, 1)[0], aa.TemplateIndentation, + ) + rules.Sort() + new := rules.String() + new = new[:len(new)-1] + return strings.Replace(profileRaw, opt.Raw, new, -1) } diff --git a/pkg/prebuild/directive/exec_test.go b/pkg/prebuild/directive/exec_test.go index a2c8a6f1..de675033 100644 --- a/pkg/prebuild/directive/exec_test.go +++ b/pkg/prebuild/directive/exec_test.go @@ -52,7 +52,7 @@ func TestExec_Apply(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cfg.RootApparmord = tt.rootApparmord if got := Directives["exec"].Apply(tt.opt, tt.profile); got != tt.want { - t.Errorf("Exec.Apply() = %v, want %v", got, tt.want) + t.Errorf("Exec.Apply() = |%v|, want |%v|", got, tt.want) } }) } From 8a8808194bb37c4335bcd3134747257c5aaa1f19 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Wed, 24 Apr 2024 13:31:22 +0100 Subject: [PATCH 17/62] refractor(aa): move base rule & qualifier to their own file. --- pkg/aa/base.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ pkg/aa/rules.go | 100 -------------------------------------------- 2 files changed, 108 insertions(+), 100 deletions(-) create mode 100644 pkg/aa/base.go diff --git a/pkg/aa/base.go b/pkg/aa/base.go new file mode 100644 index 00000000..f9cdac6d --- /dev/null +++ b/pkg/aa/base.go @@ -0,0 +1,108 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import "strings" + +type RuleBase struct { + IsLineRule bool + Comment string + NoNewPrivs bool + FileInherit bool + Prefix string + Padding string + Optional bool +} + + +func newRuleFromLog(log map[string]string) RuleBase { + fileInherit := false + if log["operation"] == "file_inherit" { + fileInherit = true + } + + noNewPrivs := false + optional := false + msg := "" + switch log["error"] { + case "-1": + if strings.Contains(log["info"], "optional:") { + optional = true + msg = strings.Replace(log["info"], "optional: ", "", 1) + } else { + noNewPrivs = true + } + case "-13": + ignoreProfileInfo := []string{"namespace", "disconnected path"} + for _, info := range ignoreProfileInfo { + if strings.Contains(log["info"], info) { + break + } + } + msg = log["info"] + default: + } + + return RuleBase{ + IsLineRule: false, + Comment: msg, + NoNewPrivs: noNewPrivs, + FileInherit: fileInherit, + Optional: optional, + } +} + +func (r RuleBase) Less(other any) bool { + return false +} + +func (r RuleBase) Equals(other any) bool { + return false +} + +func (r RuleBase) String() string { + return renderTemplate("comment", r) +} + +type Qualifier struct { + Audit bool + AccessType string +} + +func newQualifierFromLog(log map[string]string) Qualifier { + audit := false + if log["apparmor"] == "AUDIT" { + audit = true + } + return Qualifier{Audit: audit} +} + +func (r Qualifier) Less(other Qualifier) bool { + if r.Audit != other.Audit { + return r.Audit + } + return r.AccessType < other.AccessType +} + +func (r Qualifier) Equals(other Qualifier) bool { + return r.Audit == other.Audit && r.AccessType == other.AccessType +} + +type All struct { + RuleBase +} + + +func (r *All) Less(other any) bool { + return false +} + +func (r *All) Equals(other any) bool { + return false +} + +func (r *All) String() string { + return renderTemplate(tokALL, r) +} diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 0b818038..ba9cc223 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -7,7 +7,6 @@ package aa import ( "reflect" "sort" - "strings" ) const ( @@ -50,102 +49,3 @@ func (r Rules) Sort() { return r[i].Less(r[j]) }) } - -type RuleBase struct { - IsLineRule bool - Comment string - NoNewPrivs bool - FileInherit bool - Prefix string - Padding string - Optional bool -} - -func newRuleFromLog(log map[string]string) RuleBase { - fileInherit := false - if log["operation"] == "file_inherit" { - fileInherit = true - } - - noNewPrivs := false - optional := false - msg := "" - switch log["error"] { - case "-1": - if strings.Contains(log["info"], "optional:") { - optional = true - msg = strings.Replace(log["info"], "optional: ", "", 1) - } else { - noNewPrivs = true - } - case "-13": - ignoreProfileInfo := []string{"namespace", "disconnected path"} - for _, info := range ignoreProfileInfo { - if strings.Contains(log["info"], info) { - break - } - } - msg = log["info"] - default: - } - - return RuleBase{ - IsLineRule: false, - Comment: msg, - NoNewPrivs: noNewPrivs, - FileInherit: fileInherit, - Optional: optional, - } -} - -func (r RuleBase) Less(other any) bool { - return false -} - -func (r RuleBase) Equals(other any) bool { - return false -} - -func (r RuleBase) String() string { - return renderTemplate("comment", r) -} - -type Qualifier struct { - Audit bool - AccessType string -} - -func newQualifierFromLog(log map[string]string) Qualifier { - audit := false - if log["apparmor"] == "AUDIT" { - audit = true - } - return Qualifier{Audit: audit} -} - -func (r Qualifier) Less(other Qualifier) bool { - if r.Audit != other.Audit { - return r.Audit - } - return r.AccessType < other.AccessType -} - -func (r Qualifier) Equals(other Qualifier) bool { - return r.Audit == other.Audit && r.AccessType == other.AccessType -} - -type All struct { - RuleBase -} - -func (r *All) Less(other any) bool { - return false -} - -func (r *All) Equals(other any) bool { - return false -} - -func (r *All) String() string { - return renderTemplate(tokALL, r) -} From 978daa446b900e1d78a8610d57b610a1b35f9e77 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Wed, 24 Apr 2024 21:58:15 +0100 Subject: [PATCH 18/62] feat(aa-log): update aa module to last changes. --- cmd/aa-log/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/aa-log/main.go b/cmd/aa-log/main.go index cbc59c9e..dde4e2ca 100644 --- a/cmd/aa-log/main.go +++ b/cmd/aa-log/main.go @@ -67,11 +67,11 @@ func aaLog(logger string, path string, profile string) error { aaLogs := logs.NewApparmorLogs(file, profile) if rules { profiles := aaLogs.ParseToProfiles() - for _, profile := range profiles { - profile.MergeRules() - profile.Sort() - profile.Format() - fmt.Print(profile.String() + "\n") + for _, p := range profiles { + p.Merge() + p.Sort() + p.Format() + fmt.Print(p.String() + "\n\n") } } else { fmt.Print(aaLogs.String()) From 068373405fc94a17ecc32025a485fbd5b40ec1b0 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 25 Apr 2024 14:01:04 +0100 Subject: [PATCH 19/62] feat(aa): add some missing rule template. --- pkg/aa/template.go | 5 +++-- pkg/aa/templates/rule/all.j2 | 9 +++++++++ pkg/aa/templates/rule/io_uring.j2 | 16 ++++++++++++++++ pkg/aa/templates/rules.j2 | 8 ++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 pkg/aa/templates/rule/all.j2 create mode 100644 pkg/aa/templates/rule/io_uring.j2 diff --git a/pkg/aa/template.go b/pkg/aa/template.go index ab976469..7cd78d53 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -53,7 +53,9 @@ var ( // The order the apparmor rules should be sorted ruleAlphabet = []string{ "include", + "all", "rlimit", + "userns", "capability", "network", "mount", @@ -62,11 +64,10 @@ var ( "pivotroot", "changeprofile", "mqueue", + "iouring", "signal", "ptrace", "unix", - "userns", - "iouring", "dbus", "file", "profile", diff --git a/pkg/aa/templates/rule/all.j2 b/pkg/aa/templates/rule/all.j2 new file mode 100644 index 00000000..645d0ff5 --- /dev/null +++ b/pkg/aa/templates/rule/all.j2 @@ -0,0 +1,9 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "all" -}} + {{- "all" -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rule/io_uring.j2 b/pkg/aa/templates/rule/io_uring.j2 new file mode 100644 index 00000000..78e1aa17 --- /dev/null +++ b/pkg/aa/templates/rule/io_uring.j2 @@ -0,0 +1,16 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "io_uring" -}} + {{- template "qualifier" . -}} + {{- "io_uring" -}} + {{- range .Access -}} + {{ " " }}{{ . }} + {{- end -}} + {{- with .Label -}} + {{ " label=" }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rules.j2 b/pkg/aa/templates/rules.j2 index 4ce59626..8ab9bfb3 100644 --- a/pkg/aa/templates/rules.j2 +++ b/pkg/aa/templates/rules.j2 @@ -22,6 +22,10 @@ {{- template "include" . -}} {{- end -}} + {{- if eq $type "All" -}} + {{- template "all" . -}} + {{- end -}} + {{- if eq $type "Rlimit" -}} {{- template "rlimit" . -}} {{- end -}} @@ -62,6 +66,10 @@ {{- template "mqueue" . -}} {{- end -}} + {{- if eq $type "IOUring" -}} + {{- template "io_uring" . -}} + {{- end -}} + {{- if eq $type "Unix" -}} {{- template "unix" . -}} {{- end -}} From a5c4eab0cf6820ec8cee8a526ca2c462c32c7316 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 4 May 2024 23:25:55 +0100 Subject: [PATCH 20/62] feat(aa): make preamble rule classic aa rules. --- pkg/aa/apparmor.go | 11 +--------- pkg/aa/apparmor_test.go | 32 +++++++++++++-------------- pkg/aa/preamble.go | 34 +++++++++++++++++++++++++++++ pkg/aa/template.go | 3 +++ pkg/aa/templates/apparmor.j2 | 37 +------------------------------- pkg/aa/templates/rule/comment.j2 | 2 +- pkg/aa/templates/rules.j2 | 18 +++++++++++++++- pkg/aa/variables_test.go | 18 +++++++--------- 8 files changed, 81 insertions(+), 74 deletions(-) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index 36ed44c8..139ff234 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -19,19 +19,10 @@ type AppArmorProfileFiles map[string]*AppArmorProfileFile // - Some rules are not supported yet (subprofile, hat...) // - The structure is simplified as it only aims at writing profile, not parsing it. type AppArmorProfileFile struct { - Preamble + Preamble Rules Profiles []*Profile } -// Preamble section of a profile file, -type Preamble struct { - Abi []*Abi - Includes []*Include - Aliases []*Alias - Variables []*Variable - Comments []*RuleBase -} - func NewAppArmorProfile() *AppArmorProfileFile { return &AppArmorProfileFile{} } diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index 00d99d4f..dd0a9fe3 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -31,15 +31,16 @@ func TestAppArmorProfileFile_String(t *testing.T) { { name: "foo", f: &AppArmorProfileFile{ - Preamble: Preamble{ - Abi: []*Abi{{IsMagic: true, Path: "abi/4.0"}}, - Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, - Aliases: []*Alias{{Path: "/mnt/usr", RewrittenPath: "/usr"}}, - Variables: []*Variable{{ + Preamble: Rules{ + &Comment{RuleBase: RuleBase{Comment: " Simple test profile for the AppArmorProfileFile.String() method", IsLineRule: true}}, + nil, + &Abi{IsMagic: true, Path: "abi/4.0"}, + &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"}, + &Include{IsMagic: true, Path: "tunables/global"}, + &Variable{ Name: "exec_path", Define: true, Values: []string{"@{bin}/foo", "@{lib}/foo"}, - }}, - Comments: []*RuleBase{{Comment: "Simple test profile for the AppArmorProfileFile.String() method", IsLineRule: true}}, + }, }, Profiles: []*Profile{{ Header: Header{ @@ -192,17 +193,16 @@ func TestAppArmorProfileFile_Integration(t *testing.T) { { name: "aa-status", f: &AppArmorProfileFile{ - Preamble: Preamble{ - Abi: []*Abi{{IsMagic: true, Path: "abi/3.0"}}, - Includes: []*Include{{IsMagic: true, Path: "tunables/global"}}, - Variables: []*Variable{{ + Preamble: Rules{ + &Comment{RuleBase: RuleBase{Comment: " apparmor.d - Full set of apparmor profiles", IsLineRule: true}}, + &Comment{RuleBase: RuleBase{Comment: " Copyright (C) 2021-2024 Alexandre Pujol ", IsLineRule: true}}, + &Comment{RuleBase: RuleBase{Comment: " SPDX-License-Identifier: GPL-2.0-only", IsLineRule: true}}, + nil, + &Abi{IsMagic: true, Path: "abi/3.0"}, + &Include{IsMagic: true, Path: "tunables/global"}, + &Variable{ Name: "exec_path", Define: true, Values: []string{"@{bin}/aa-status", "@{bin}/apparmor_status"}, - }}, - Comments: []*RuleBase{ - {Comment: "apparmor.d - Full set of apparmor profiles", IsLineRule: true}, - {Comment: "Copyright (C) 2021-2024 Alexandre Pujol ", IsLineRule: true}, - {Comment: "SPDX-License-Identifier: GPL-2.0-only", IsLineRule: true}, }, }, Profiles: []*Profile{{ diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 6970bee5..8459099a 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -14,6 +14,40 @@ const ( tokIFEXISTS = "if exists" ) +type Comment struct { + RuleBase +} + +func newCommentFromRule(rule rule) (Rule, error) { + base := newRuleFromRule(rule) + base.IsLineRule = true + return &Comment{RuleBase: base}, nil +} + +func (r *Comment) Less(other any) bool { + return false +} + +func (r *Comment) Equals(other any) bool { + return false +} + +func (r *Comment) String() string { + return renderTemplate("comment", r) +} + +func (r *Comment) IsPreamble() bool { + return true +} + +func (r *Comment) Constraint() RuleConstraint { + return anyKind +} + +func (r *Comment) Kind() string { + return tokCOMMENT +} + type Abi struct { RuleBase Path string diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 7cd78d53..8ee6d756 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -175,6 +175,9 @@ func cjoin(i any) string { } func typeOf(i any) string { + if i == nil { + return "" + } return strings.TrimPrefix(reflect.TypeOf(i).String(), "*aa.") } diff --git a/pkg/aa/templates/apparmor.j2 b/pkg/aa/templates/apparmor.j2 index 1686a7de..75a0026f 100644 --- a/pkg/aa/templates/apparmor.j2 +++ b/pkg/aa/templates/apparmor.j2 @@ -4,42 +4,7 @@ {{- define "apparmor" -}} - {{- with .Comments -}} - {{- range . -}} - {{- template "comment" . -}} - {{- "\n" -}} - {{- end -}} - {{- "\n" -}} - {{- end -}} - - {{- with .Abi -}} - {{- range . -}} - {{- template "abi" . -}} - {{- "\n" -}} - {{- end -}} - {{- "\n" -}} - {{- end -}} - - {{- with .Aliases -}} - {{- range . -}} - {{- template "alias" . -}} - {{- "\n" -}} - {{- end -}} - {{- "\n" -}} - {{- end -}} - - {{- with .Includes -}} - {{- range . -}} - {{- template "include" . -}} - {{- "\n" -}} - {{- end -}} - {{- "\n" -}} - {{- end -}} - - {{- range .Variables -}} - {{- template "variable" . -}} - {{- "\n" -}} - {{- end -}} + {{- template "rules" .Preamble -}} {{- range .Profiles -}} {{- template "profile" . -}} diff --git a/pkg/aa/templates/rule/comment.j2 b/pkg/aa/templates/rule/comment.j2 index 2a752288..abe41963 100644 --- a/pkg/aa/templates/rule/comment.j2 +++ b/pkg/aa/templates/rule/comment.j2 @@ -19,7 +19,7 @@ {{- " optional:" -}} {{- end -}} {{- with .Comment -}} - {{ " " }}{{ . }} + {{ . }} {{- end -}} {{- end -}} {{- end -}} diff --git a/pkg/aa/templates/rules.j2 b/pkg/aa/templates/rules.j2 index 8ab9bfb3..f2099334 100644 --- a/pkg/aa/templates/rules.j2 +++ b/pkg/aa/templates/rules.j2 @@ -7,7 +7,11 @@ {{- $oldtype := "" -}} {{- range . -}} {{- $type := typeof . -}} - {{- if eq $type "RuleBase" -}} + {{- if eq $type "" -}} + {{- "\n" -}} + {{- continue -}} + {{- end -}} + {{- if eq $type "Comment" -}} {{- template "comment" . -}} {{- "\n" -}} {{- continue -}} @@ -18,10 +22,22 @@ {{- end -}} {{- indent "" -}} + {{- if eq $type "Abi" -}} + {{- template "abi" . -}} + {{- end -}} + + {{- if eq $type "Alias" -}} + {{- template "alias" . -}} + {{- end -}} + {{- if eq $type "Include" -}} {{- template "include" . -}} {{- end -}} + {{- if eq $type "Variable" -}} + {{- template "variable" . -}} + {{- end -}} + {{- if eq $type "All" -}} {{- template "all" . -}} {{- end -}} diff --git a/pkg/aa/variables_test.go b/pkg/aa/variables_test.go index 778b3380..8f8f55fa 100644 --- a/pkg/aa/variables_test.go +++ b/pkg/aa/variables_test.go @@ -21,16 +21,14 @@ func TestDefaultTunables(t *testing.T) { { name: "aa", want: &AppArmorProfileFile{ - Preamble: Preamble{ - Variables: []*Variable{ - {Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, - {Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}}, - {Name: "multiarch", Values: []string{"*-linux-gnu*"}}, - {Name: "HOME", Values: []string{"/home/*"}}, - {Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}}, - {Name: "etc_ro", Values: []string{"/{,usr/}etc/"}}, - {Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, - }, + Preamble: Rules{ + &Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, + &Variable{Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}}, + &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}}, + &Variable{Name: "HOME", Values: []string{"/home/*"}}, + &Variable{Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}}, + &Variable{Name: "etc_ro", Values: []string{"/{,usr/}etc/"}}, + &Variable{Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, }, }, }, From f763d31a07ca6a9a8824beca56ae6e8bb2bf0117 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 4 May 2024 23:41:47 +0100 Subject: [PATCH 21/62] feat(aa): a Constraint and Kind method to the Rule interface. --- pkg/aa/base.go | 18 +++++++++++++++++- pkg/aa/capability.go | 10 +++++++++- pkg/aa/change_profile.go | 8 ++++++++ pkg/aa/dbus.go | 10 +++++++++- pkg/aa/file.go | 9 +++++++-- pkg/aa/io_uring.go | 10 +++++++++- pkg/aa/mount.go | 30 +++++++++++++++++++++++++++--- pkg/aa/mqueue.go | 10 +++++++++- pkg/aa/network.go | 10 +++++++++- pkg/aa/pivot_root.go | 10 +++++++++- pkg/aa/preamble.go | 34 +++++++++++++++++++++++++++++++++- pkg/aa/profile.go | 10 +++++++++- pkg/aa/ptrace.go | 10 +++++++++- pkg/aa/rlimit.go | 10 +++++++++- pkg/aa/rules.go | 10 ++++++++++ pkg/aa/signal.go | 10 +++++++++- pkg/aa/unix.go | 10 +++++++++- pkg/aa/userns.go | 10 +++++++++- 18 files changed, 210 insertions(+), 19 deletions(-) diff --git a/pkg/aa/base.go b/pkg/aa/base.go index f9cdac6d..f0806746 100644 --- a/pkg/aa/base.go +++ b/pkg/aa/base.go @@ -66,6 +66,14 @@ func (r RuleBase) String() string { return renderTemplate("comment", r) } +func (r RuleBase) Constraint() constraint { + return anyKind +} + +func (r RuleBase) Kind() string { + return "base" +} + type Qualifier struct { Audit bool AccessType string @@ -104,5 +112,13 @@ func (r *All) Equals(other any) bool { } func (r *All) String() string { - return renderTemplate(tokALL, r) + return renderTemplate(r.Kind(), r) +} + +func (r *All) Constraint() constraint { + return blockKind +} + +func (r *All) Kind() string { + return tokALL } diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 14b27209..f458350a 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -39,5 +39,13 @@ func (r *Capability) Equals(other any) bool { } func (r *Capability) String() string { - return renderTemplate(tokCAPABILITY, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Capability) Constraint() constraint { + return blockKind +} + +func (r *Capability) Kind() string { + return tokCAPABILITY } diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index 32106f9b..4d5ded15 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -47,3 +47,11 @@ func (r *ChangeProfile) Equals(other any) bool { func (r *ChangeProfile) String() string { return renderTemplate(tokCHANGEPROFILE, r) } + +func (r *ChangeProfile) Constraint() constraint { + return blockKind +} + +func (r *ChangeProfile) Kind() string { + return tokCHANGEPROFILE +} diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index 3ab30ae7..aa88266c 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -81,5 +81,13 @@ func (r *Dbus) Equals(other any) bool { } func (r *Dbus) String() string { - return renderTemplate(tokDBUS, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Dbus) Constraint() constraint { + return blockKind +} + +func (r *Dbus) Kind() string { + return tokDBUS } diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 9390afbe..8aabd577 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -60,8 +60,13 @@ func (r *File) Equals(other any) bool { } func (r *File) String() string { - return renderTemplate("file", r) + return renderTemplate(r.Kind(), r) } - r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) +func (r *File) Constraint() constraint { + return blockKind +} + +func (r *File) Kind() string { + return "file" } diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index eedee845..4f76354c 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -40,5 +40,13 @@ func (r *IOUring) Equals(other any) bool { } func (r *IOUring) String() string { - return renderTemplate(tokIOURING, r) + return renderTemplate(r.Kind(), r) +} + +func (r *IOUring) Constraint() constraint { + return blockKind +} + +func (r *IOUring) Kind() string { + return tokIOURING } diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 7f0f5621..7d7fef3a 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -83,7 +83,15 @@ func (r *Mount) Equals(other any) bool { } func (r *Mount) String() string { - return renderTemplate(tokMOUNT, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Mount) Constraint() constraint { + return blockKind +} + +func (r *Mount) Kind() string { + return tokMOUNT } type Umount struct { @@ -121,7 +129,15 @@ func (r *Umount) Equals(other any) bool { } func (r *Umount) String() string { - return renderTemplate(tokUMOUNT, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Umount) Constraint() constraint { + return blockKind +} + +func (r *Umount) Kind() string { + return tokUMOUNT } type Remount struct { @@ -159,5 +175,13 @@ func (r *Remount) Equals(other any) bool { } func (r *Remount) String() string { - return renderTemplate(tokREMOUNT, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Remount) Constraint() constraint { + return blockKind +} + +func (r *Remount) Kind() string { + return tokREMOUNT } diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 0035f2cd..92a2252c 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -58,5 +58,13 @@ func (r *Mqueue) Equals(other any) bool { } func (r *Mqueue) String() string { - return renderTemplate(tokMQUEUE, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Mqueue) Constraint() constraint { + return blockKind +} + +func (r *Mqueue) Kind() string { + return tokMQUEUE } diff --git a/pkg/aa/network.go b/pkg/aa/network.go index d4fd9669..36ef3ac0 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -81,5 +81,13 @@ func (r *Network) Equals(other any) bool { } func (r *Network) String() string { - return renderTemplate(tokNETWORK, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Network) Constraint() constraint { + return blockKind +} + +func (r *Network) Kind() string { + return tokNETWORK } diff --git a/pkg/aa/pivot_root.go b/pkg/aa/pivot_root.go index 66829f56..3c421adf 100644 --- a/pkg/aa/pivot_root.go +++ b/pkg/aa/pivot_root.go @@ -46,5 +46,13 @@ func (r *PivotRoot) Equals(other any) bool { } func (r *PivotRoot) String() string { - return renderTemplate(tokPIVOTROOT, r) + return renderTemplate(r.Kind(), r) +} + +func (r *PivotRoot) Constraint() constraint { + return blockKind +} + +func (r *PivotRoot) Kind() string { + return tokPIVOTROOT } diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 8459099a..344ccaa1 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -40,7 +40,7 @@ func (r *Comment) IsPreamble() bool { return true } -func (r *Comment) Constraint() RuleConstraint { +func (r *Comment) Constraint() constraint { return anyKind } @@ -71,6 +71,14 @@ func (r *Abi) String() string { return renderTemplate(tokABI, r) } +func (r *Abi) Constraint() constraint { + return preambleKind +} + +func (r *Abi) Kind() string { + return tokABI +} + type Alias struct { RuleBase Path string @@ -94,6 +102,14 @@ func (r *Alias) String() string { return renderTemplate(tokALIAS, r) } +func (r *Alias) Constraint() constraint { + return preambleKind +} + +func (r *Alias) Kind() string { + return tokALIAS +} + type Include struct { RuleBase IfExists bool @@ -121,6 +137,14 @@ func (r *Include) String() string { return renderTemplate(tokINCLUDE, r) } +func (r *Include) Constraint() constraint { + return anyKind +} + +func (r *Include) Kind() string { + return tokINCLUDE +} + type Variable struct { RuleBase Name string @@ -146,3 +170,11 @@ func (r *Variable) Equals(other any) bool { func (r *Variable) String() string { return renderTemplate("variable", r) } + +func (r *Variable) Constraint() constraint { + return preambleKind +} + +func (r *Variable) Kind() string { + return tokVARIABLE +} diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 8fff8110..974a9b2c 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -48,7 +48,15 @@ func (p *Profile) Equals(other any) bool { } func (p *Profile) String() string { - return renderTemplate(tokPROFILE, p) + return renderTemplate(p.Kind(), p) +} + +func (p *Profile) Constraint() constraint { + return blockKind +} + +func (p *Profile) Kind() string { + return tokPROFILE } // Merge merge similar rules together. diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index ffe69dfc..5a014bc7 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -40,5 +40,13 @@ func (r *Ptrace) Equals(other any) bool { } func (r *Ptrace) String() string { - return renderTemplate(tokPTRACE, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Ptrace) Constraint() constraint { + return blockKind +} + +func (r *Ptrace) Kind() string { + return tokPTRACE } diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index 585211e7..4005cd22 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -43,5 +43,13 @@ func (r *Rlimit) Equals(other any) bool { } func (r *Rlimit) String() string { - return renderTemplate(tokRLIMIT, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Rlimit) Constraint() constraint { + return blockKind +} + +func (r *Rlimit) Kind() string { + return tokRLIMIT } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index ba9cc223..dc2eeebe 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -16,11 +16,21 @@ const ( tokDENY = "deny" ) +type constraint uint + +const ( + anyKind constraint = iota // The rule can be found in either preamble or profile + preambleKind // The rule can only be found in the preamble + blockKind // The rule can only be found in a profile +) + // Rule generic interface for all AppArmor rules type Rule interface { Less(other any) bool Equals(other any) bool String() string + Constraint() constraint + Kind() string } type Rules []Rule diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 237607d2..9a6da935 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -46,5 +46,13 @@ func (r *Signal) Equals(other any) bool { } func (r *Signal) String() string { - return renderTemplate(tokSIGNAL, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Signal) Constraint() constraint { + return blockKind +} + +func (r *Signal) Kind() string { + return tokSIGNAL } diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index ee92fe38..3c53dc84 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -78,5 +78,13 @@ func (r *Unix) Equals(other any) bool { } func (r *Unix) String() string { - return renderTemplate(tokUNIX, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Unix) Constraint() constraint { + return blockKind +} + +func (r *Unix) Kind() string { + return tokUNIX } diff --git a/pkg/aa/userns.go b/pkg/aa/userns.go index 24087e11..5e9437fa 100644 --- a/pkg/aa/userns.go +++ b/pkg/aa/userns.go @@ -34,5 +34,13 @@ func (r *Userns) Equals(other any) bool { } func (r *Userns) String() string { - return renderTemplate(tokUSERNS, r) + return renderTemplate(r.Kind(), r) +} + +func (r *Userns) Constraint() constraint { + return blockKind +} + +func (r *Userns) Kind() string { + return tokUSERNS } From 5943e9a24d1c2e20939bc5c1245bd061c5e2ed34 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 4 May 2024 23:45:36 +0100 Subject: [PATCH 22/62] test(aa): cleanup unit tests. --- pkg/aa/apparmor_test.go | 10 +++------- pkg/aa/rules_test.go | 10 +++++----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index dd0a9fe3..faef895d 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -49,7 +49,7 @@ func TestAppArmorProfileFile_String(t *testing.T) { Attributes: map[string]string{"security.tagged": "allowed"}, Flags: []string{"complain", "attach_disconnected"}, }, - Rules: []Rule{ + Rules: Rules{ &Include{IsMagic: true, Path: "abstractions/base"}, &Include{IsMagic: true, Path: "abstractions/nameservice-strict"}, rlimit1, @@ -58,7 +58,7 @@ func TestAppArmorProfileFile_String(t *testing.T) { &Network{Domain: "inet", Type: "stream"}, &Network{Domain: "inet6", Type: "stream"}, &Mount{ - RuleBase: RuleBase{Comment: "failed perms check"}, + RuleBase: RuleBase{Comment: " failed perms check"}, MountConditions: MountConditions{ FsType: "fuse.portal", Options: []string{"rw", "rbind"}, @@ -83,11 +83,7 @@ func TestAppArmorProfileFile_String(t *testing.T) { PeerLabel: "gnome-shell", PeerAddr: "none", }, - &Dbus{ - Access: []string{"bind"}, - Bus: "session", - Name: "org.gnome.*", - }, + &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"}, &Dbus{ Access: []string{"receive"}, Bus: "system", diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 7bbd119b..f44d2e70 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -func TestRule_FromLog(t *testing.T) { +func TestRules_FromLog(t *testing.T) { tests := []struct { name string fromLog func(map[string]string) Rule @@ -98,7 +98,7 @@ func TestRule_FromLog(t *testing.T) { } } -func TestRule_Less(t *testing.T) { +func TestRules_Less(t *testing.T) { tests := []struct { name string rule Rule @@ -272,7 +272,7 @@ func TestRule_Less(t *testing.T) { } } -func TestRule_Equals(t *testing.T) { +func TestRules_Equals(t *testing.T) { tests := []struct { name string rule Rule @@ -368,7 +368,7 @@ func TestRule_Equals(t *testing.T) { } } -func TestRule_String(t *testing.T) { +func TestRules_String(t *testing.T) { tests := []struct { name string rule Rule @@ -417,7 +417,7 @@ func TestRule_String(t *testing.T) { { name: "mount", rule: mount1, - want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check", + want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, #failed perms check", }, { name: "pivot_root", From 1e79d272328557f6810d515059fcaad6e66b1833 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 4 May 2024 23:54:39 +0100 Subject: [PATCH 23/62] feat(aa): rename identation variables. --- pkg/aa/template.go | 14 +++++++------- pkg/prebuild/directive/dbus.go | 4 ++-- pkg/prebuild/directive/exec.go | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 8ee6d756..4f433c09 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -15,10 +15,10 @@ import ( var ( // Default indentation for apparmor profile (2 spaces) - TemplateIndentation = " " + Indentation = " " // The current indentation level - TemplateIndentationLevel = 0 + IndentationLevel = 0 //go:embed templates/*.j2 //go:embed templates/rule/*.j2 @@ -122,7 +122,7 @@ func renderTemplate(name string, data any) string { var res strings.Builder template, ok := tmpl[name] if !ok { - panic("template not found") + panic("template '" + name + "' not found") } err := template.Execute(&res, data) if err != nil { @@ -188,19 +188,19 @@ func typeToValue(i reflect.Type) string { func setindent(i string) string { switch i { case "++": - TemplateIndentationLevel++ + IndentationLevel++ case "--": - TemplateIndentationLevel-- + IndentationLevel-- } return "" } func indent(s string) string { - return strings.Repeat(TemplateIndentation, TemplateIndentationLevel) + s + return strings.Repeat(Indentation, IndentationLevel) + s } func indentDbus(s string) string { - return strings.Join([]string{TemplateIndentation, s}, " ") + return strings.Join([]string{Indentation, s}, " ") } func getLetterIn(alphabet []string, in string) string { diff --git a/pkg/prebuild/directive/dbus.go b/pkg/prebuild/directive/dbus.go index e1e64a64..f98105b5 100644 --- a/pkg/prebuild/directive/dbus.go +++ b/pkg/prebuild/directive/dbus.go @@ -61,8 +61,8 @@ func (d Dbus) Apply(opt *Option, profile string) string { r = d.talk(opt.ArgMap) } - aa.TemplateIndentationLevel = strings.Count( - strings.SplitN(opt.Raw, Keyword, 1)[0], aa.TemplateIndentation, + aa.IndentationLevel = strings.Count( + strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation, ) generatedDbus := r.String() lenDbus := len(generatedDbus) diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index b2899baa..a7a8c736 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -54,8 +54,8 @@ func (d Exec) Apply(opt *Option, profileRaw string) string { } } - aa.TemplateIndentationLevel = strings.Count( - strings.SplitN(opt.Raw, Keyword, 1)[0], aa.TemplateIndentation, + aa.IndentationLevel = strings.Count( + strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation, ) rules.Sort() new := rules.String() From 28f4294774099c9aacaa1c62b03a7fefdcec832b Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 5 May 2024 13:57:15 +0100 Subject: [PATCH 24/62] feat(aa): move the all rule to its own file. --- pkg/aa/all.go | 33 +++++++++++++++++++++++++++++++++ pkg/aa/base.go | 25 ------------------------- 2 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 pkg/aa/all.go diff --git a/pkg/aa/all.go b/pkg/aa/all.go new file mode 100644 index 00000000..8c0527be --- /dev/null +++ b/pkg/aa/all.go @@ -0,0 +1,33 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +const ( + tokALL = "all" +) + +type All struct { + RuleBase +} + +func (r *All) Less(other any) bool { + return false +} + +func (r *All) Equals(other any) bool { + return false +} + +func (r *All) String() string { + return renderTemplate(r.Kind(), r) +} + +func (r *All) Constraint() constraint { + return blockKind +} + +func (r *All) Kind() string { + return tokALL +} diff --git a/pkg/aa/base.go b/pkg/aa/base.go index f0806746..ed12a1d7 100644 --- a/pkg/aa/base.go +++ b/pkg/aa/base.go @@ -97,28 +97,3 @@ func (r Qualifier) Less(other Qualifier) bool { func (r Qualifier) Equals(other Qualifier) bool { return r.Audit == other.Audit && r.AccessType == other.AccessType } - -type All struct { - RuleBase -} - - -func (r *All) Less(other any) bool { - return false -} - -func (r *All) Equals(other any) bool { - return false -} - -func (r *All) String() string { - return renderTemplate(r.Kind(), r) -} - -func (r *All) Constraint() constraint { - return blockKind -} - -func (r *All) Kind() string { - return tokALL -} From 305d06dbe04b25506d55717ee4648f352c427bc4 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 5 May 2024 14:09:00 +0100 Subject: [PATCH 25/62] feat(aa): rewrite variable handling. --- pkg/aa/apparmor.go | 17 +++ pkg/aa/profile.go | 133 +++++++++++++-------- pkg/aa/resolve.go | 71 +++++++++++ pkg/aa/resolve_test.go | 197 ++++++++++++++++++++++++++++++ pkg/aa/rules.go | 1 - pkg/aa/variables.go | 120 ------------------- pkg/aa/variables_test.go | 251 --------------------------------------- 7 files changed, 368 insertions(+), 422 deletions(-) create mode 100644 pkg/aa/resolve.go create mode 100644 pkg/aa/resolve_test.go delete mode 100644 pkg/aa/variables.go delete mode 100644 pkg/aa/variables_test.go diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index 139ff234..6ddcd7a4 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -27,6 +27,23 @@ func NewAppArmorProfile() *AppArmorProfileFile { return &AppArmorProfileFile{} } +// DefaultTunables return a minimal working profile to build the profile +// It should not be used when loading file from /etc/apparmor.d +func DefaultTunables() *AppArmorProfileFile { + return &AppArmorProfileFile{ + Preamble: Rules{ + &Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}, Define: true}, + &Variable{Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}, Define: true}, + &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, + &Variable{Name: "HOME", Values: []string{"/home/*"}, Define: true}, + &Variable{Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}, Define: true}, + &Variable{Name: "etc_ro", Values: []string{"/{,usr/}etc/"}, Define: true}, + &Variable{Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}, Define: true}, + &Variable{Name: "user_cache_dirs", Values: []string{"/home/*/.cache"}, Define: true}, + }, + } +} + // String returns the formatted representation of a profile file as a string func (f *AppArmorProfileFile) String() string { return renderTemplate("apparmor", f) diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 974a9b2c..956a7922 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -8,6 +8,7 @@ import ( "maps" "reflect" "slices" + "sort" "strings" ) @@ -86,7 +87,22 @@ func (p *Profile) Merge() { // Sort the rules in a profile. // Follow: https://apparmor.pujol.io/development/guidelines/#guidelines func (p *Profile) Sort() { - p.Rules.Sort() + sort.Slice(p.Rules, func(i, j int) bool { + typeOfI := reflect.TypeOf(p.Rules[i]) + typeOfJ := reflect.TypeOf(p.Rules[j]) + if typeOfI != typeOfJ { + valueOfI := typeToValue(typeOfI) + valueOfJ := typeToValue(typeOfJ) + if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists { + valueOfI = "include_if_exists" + } + if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists { + valueOfJ = "include_if_exists" + } + return ruleWeights[valueOfI] < ruleWeights[valueOfJ] + } + return p.Rules[i].Less(p.Rules[j]) + }) } // Format the profile for better readability before printing it. @@ -121,9 +137,68 @@ func (p *Profile) Format() { } } -// AddRule adds a new rule to the profile from a log map. -func (p *Profile) AddRule(log map[string]string) { +// GetAttachments return a nested attachment string +func (p *Profile) GetAttachments() string { + if len(p.Attachments) == 0 { + return "" + } else if len(p.Attachments) == 1 { + return p.Attachments[0] + } else { + res := []string{} + for _, attachment := range p.Attachments { + if strings.HasPrefix(attachment, "/") { + res = append(res, attachment[1:]) + } else { + res = append(res, attachment) + } + } + return "/{" + strings.Join(res, ",") + "}" + } +} +var ( + newLogMap = map[string]func(log map[string]string) Rule{ + "rlimits": newRlimitFromLog, + "cap": newCapabilityFromLog, + "io_uring": newIOUringFromLog, + "signal": newSignalFromLog, + "ptrace": newPtraceFromLog, + "namespace": newUsernsFromLog, + "unix": newUnixFromLog, + "dbus": newDbusFromLog, + "posix_mqueue": newMqueueFromLog, + "sysv_mqueue": newMqueueFromLog, + "mount": func(log map[string]string) Rule { + if strings.Contains(log["flags"], "remount") { + return newRemountFromLog(log) + } + newRule := newLogMountMap[log["operation"]] + return newRule(log) + }, + "net": func(log map[string]string) Rule { + if log["family"] == "unix" { + return newUnixFromLog(log) + } else { + return newNetworkFromLog(log) + } + }, + "file": func(log map[string]string) Rule { + if log["operation"] == "change_onexec" { + return newChangeProfileFromLog(log) + } else { + return newFileFromLog(log) + } + }, + } + newLogMountMap = map[string]func(log map[string]string) Rule{ + "mount": newMountFromLog, + "umount": newUmountFromLog, + "remount": newRemountFromLog, + "pivotroot": newPivotRootFromLog, + } +) + +func (p *Profile) AddRule(log map[string]string) { // Generate profile flags and extra rules switch log["error"] { case "-2": @@ -139,57 +214,15 @@ func (p *Profile) AddRule(log map[string]string) { default: } - switch log["class"] { - case "rlimits": - p.Rules = append(p.Rules, newRlimitFromLog(log)) - case "cap": - p.Rules = append(p.Rules, newCapabilityFromLog(log)) - case "net": - if log["family"] == "unix" { - p.Rules = append(p.Rules, newUnixFromLog(log)) - } else { - p.Rules = append(p.Rules, newNetworkFromLog(log)) - } - case "io_uring": - p.Rules = append(p.Rules, newIOUringFromLog(log)) - case "mount": - if strings.Contains(log["flags"], "remount") { - p.Rules = append(p.Rules, newRemountFromLog(log)) - } else { - switch log["operation"] { - case "mount": - p.Rules = append(p.Rules, newMountFromLog(log)) - case "umount": - p.Rules = append(p.Rules, newUmountFromLog(log)) - case "remount": - p.Rules = append(p.Rules, newRemountFromLog(log)) - case "pivotroot": - p.Rules = append(p.Rules, newPivotRootFromLog(log)) - } - } - case "posix_mqueue", "sysv_mqueue": - p.Rules = append(p.Rules, newMqueueFromLog(log)) - case "signal": - p.Rules = append(p.Rules, newSignalFromLog(log)) - case "ptrace": - p.Rules = append(p.Rules, newPtraceFromLog(log)) - case "namespace": - p.Rules = append(p.Rules, newUsernsFromLog(log)) - case "unix": - p.Rules = append(p.Rules, newUnixFromLog(log)) - case "dbus": - p.Rules = append(p.Rules, newDbusFromLog(log)) - case "file": - if log["operation"] == "change_onexec" { - p.Rules = append(p.Rules, newChangeProfileFromLog(log)) - } else { - p.Rules = append(p.Rules, newFileFromLog(log)) - } - default: + if newRule, ok := newLogMap[log["class"]]; ok { + p.Rules = append(p.Rules, newRule(log)) + } else { if strings.Contains(log["operation"], "dbus") { p.Rules = append(p.Rules, newDbusFromLog(log)) } else if log["family"] == "unix" { p.Rules = append(p.Rules, newUnixFromLog(log)) + } else { + panic("unknown class: " + log["class"]) } } } diff --git a/pkg/aa/resolve.go b/pkg/aa/resolve.go new file mode 100644 index 00000000..308c22f1 --- /dev/null +++ b/pkg/aa/resolve.go @@ -0,0 +1,71 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "fmt" + "regexp" + "strings" +) + +var ( + regVariableReference = regexp.MustCompile(`@{([^{}]+)}`) +) + +// Resolve resolves all variables and includes in the profile and merge the rules in the profile +func (f *AppArmorProfileFile) Resolve() error { + // Resolve variables + for _, variable := range f.Preamble.GetVariables() { + newValues := []string{} + for _, value := range variable.Values { + vars := f.resolveVariable(value) + if len(vars) == 0 { + return fmt.Errorf("Variable not defined in: %s", value) + } + newValues = append(newValues, vars...) + } + variable.Values = newValues + } + + // Resolve variables in attachements + for _, profile := range f.Profiles { + attachments := []string{} + for _, att := range profile.Attachments { + vars := f.resolveVariable(att) + if len(vars) == 0 { + return fmt.Errorf("Variable not defined in: %s", att) + } + attachments = append(attachments, vars...) + } + profile.Attachments = attachments + } + + return nil +} + +func (f *AppArmorProfileFile) resolveVariable(input string) []string { + if !strings.Contains(input, tokVARIABLE) { + return []string{input} + } + + vars := []string{} + match := regVariableReference.FindStringSubmatch(input) + if len(match) > 1 { + variable := match[0] + varname := match[1] + for _, vrbl := range f.Preamble.GetVariables() { + if vrbl.Name == varname { + for _, v := range vrbl.Values { + newVar := strings.ReplaceAll(input, variable, v) + res := f.resolveVariable(newVar) + vars = append(vars, res...) + } + } + } + } else { + vars = append(vars, input) + } + return vars +} diff --git a/pkg/aa/resolve_test.go b/pkg/aa/resolve_test.go new file mode 100644 index 00000000..3c6f20c6 --- /dev/null +++ b/pkg/aa/resolve_test.go @@ -0,0 +1,197 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "reflect" + "testing" +) + +func TestAppArmorProfileFile_resolveVariable(t *testing.T) { + tests := []struct { + name string + f AppArmorProfileFile + input string + want []string + }{ + { + name: "nil", + input: "@{newvar}", + want: []string{}, + }, + { + name: "empty", + input: "@{}", + want: []string{"@{}"}, + }, + { + name: "default", + input: "@{etc_ro}", + want: []string{"/{,usr/}etc/"}, + }, + { + name: "simple", + input: "@{bin}/foo", + want: []string{"/{,usr/}{,s}bin/foo"}, + }, + { + name: "double", + input: "@{lib}/@{multiarch}", + want: []string{"/{,usr/}lib{,exec,32,64}/*-linux-gnu*"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := DefaultTunables() + got := f.resolveVariable(tt.input) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AppArmorProfileFile.resolveVariable() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAppArmorProfileFile_Resolve(t *testing.T) { + tests := []struct { + name string + variables Rules + attachements []string + want *AppArmorProfileFile + wantErr bool + }{ + { + name: "firefox", + variables: Rules{ + &Variable{Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}, Define: true}, + &Variable{Name: "firefox_lib_dirs", Values: []string{"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}, Define: true}, + }, + attachements: []string{"@{exec_path}"}, + want: &AppArmorProfileFile{ + Preamble: Rules{ + &Variable{Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}, Define: true}, + &Variable{ + Name: "firefox_lib_dirs", Define: true, + Values: []string{ + "/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}", + "/opt/firefox{,-esr,-bin}", + }, + }, + &Variable{ + Name: "exec_path", Define: true, + Values: []string{ + "/{usr/,}bin/firefox{,-esr,-bin}", + "/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + }, + }, + }, + Profiles: []*Profile{ + {Header: Header{ + Attachments: []string{ + "/{usr/,}bin/firefox{,-esr,-bin}", + "/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + }, + }}, + }, + }, + wantErr: false, + }, + { + name: "chromium", + variables: Rules{ + &Variable{Name: "name", Values: []string{"chromium"}, Define: true}, + &Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{name}"}, Define: true}, + &Variable{Name: "path", Values: []string{"@{lib_dirs}/@{name}"}, Define: true}, + }, + attachements: []string{"@{path}/pass"}, + want: &AppArmorProfileFile{ + Preamble: Rules{ + &Variable{Name: "name", Values: []string{"chromium"}, Define: true}, + &Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/chromium"}, Define: true}, + &Variable{Name: "path", Values: []string{"/{usr/,}lib/chromium/chromium"}, Define: true}, + }, + Profiles: []*Profile{ + {Header: Header{ + Attachments: []string{"/{usr/,}lib/chromium/chromium/pass"}, + }}, + }, + }, + wantErr: false, + }, + { + name: "geoclue", + variables: Rules{ + &Variable{Name: "libexec", Values: []string{"/{usr/,}libexec"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}, Define: true}, + }, + attachements: []string{"@{exec_path}"}, + want: &AppArmorProfileFile{ + Preamble: Rules{ + &Variable{Name: "libexec", Values: []string{"/{usr/,}libexec"}, Define: true}, + &Variable{ + Name: "exec_path", Define: true, + Values: []string{ + "/{usr/,}libexec/geoclue", + "/{usr/,}libexec/geoclue-2.0/demos/agent", + }, + }, + }, + Profiles: []*Profile{ + {Header: Header{ + Attachments: []string{ + "/{usr/,}libexec/geoclue", + "/{usr/,}libexec/geoclue-2.0/demos/agent", + }, + }}, + }, + }, + wantErr: false, + }, + { + name: "opera", + variables: Rules{ + &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, + &Variable{Name: "name", Values: []string{"opera{,-beta,-developer}"}, Define: true}, + &Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{multiarch}/@{name}"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"@{lib_dirs}/@{name}"}, Define: true}, + }, + attachements: []string{"@{exec_path}"}, + want: &AppArmorProfileFile{ + Preamble: Rules{ + &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, + &Variable{Name: "name", Values: []string{"opera{,-beta,-developer}"}, Define: true}, + &Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}"}, Define: true}, + }, + Profiles: []*Profile{ + {Header: Header{ + Attachments: []string{ + "/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}", + }, + }}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &AppArmorProfileFile{ + Profiles: []*Profile{{ + Header: Header{Attachments: tt.attachements}, + }}, + } + got.Preamble = tt.variables + if err := got.Resolve(); (err != nil) != tt.wantErr { + t.Errorf("AppArmorProfileFile.Resolve() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AppArmorProfile.Resolve() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index dc2eeebe..c6c5446b 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -10,7 +10,6 @@ import ( ) const ( - tokALL = "all" tokALLOW = "allow" tokAUDIT = "audit" tokDENY = "deny" diff --git a/pkg/aa/variables.go b/pkg/aa/variables.go deleted file mode 100644 index ea7ce499..00000000 --- a/pkg/aa/variables.go +++ /dev/null @@ -1,120 +0,0 @@ -// apparmor.d - Full set of apparmor profiles -// Copyright (C) 2021-2024 Alexandre Pujol -// SPDX-License-Identifier: GPL-2.0-only - -// Warning: this is purposely not using a Yacc parser. Its only aim is to -// extract variables and attachments for apparmor.d profile - -package aa - -import ( - "regexp" - "strings" -) - -var ( - regVariablesDef = regexp.MustCompile(`@{(.*)}\s*[+=]+\s*(.*)`) - regVariablesRef = regexp.MustCompile(`@{([^{}]+)}`) -) - -// DefaultTunables return a minimal working profile to build the profile -// It should not be used when loading file from /etc/apparmor.d -func DefaultTunables() *AppArmorProfileFile { - return &AppArmorProfileFile{ - Preamble: Preamble{ - Variables: []*Variable{ - {Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, - {Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}}, - {Name: "multiarch", Values: []string{"*-linux-gnu*"}}, - {Name: "HOME", Values: []string{"/home/*"}}, - {Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}}, - {Name: "etc_ro", Values: []string{"/{,usr/}etc/"}}, - {Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, - }, - }, - } -} - -// ParseVariables extract all variables from the profile -func (f *AppArmorProfileFile) ParseVariables(content string) { - matches := regVariablesDef.FindAllStringSubmatch(content, -1) - for _, match := range matches { - if len(match) > 2 { - key := match[1] - values := strings.Split(match[2], " ") - found := false - for idx, variable := range f.Variables { - if variable.Name == key { - f.Variables[idx].Values = append(f.Variables[idx].Values, values...) - found = true - break - } - } - if !found { - variable := &Variable{Name: key, Values: values} - f.Variables = append(f.Variables, variable) - } - } - } -} - -// resolve recursively resolves all variables references -func (f *AppArmorProfileFile) resolve(str string) []string { - if strings.Contains(str, "@{") { - vars := []string{} - match := regVariablesRef.FindStringSubmatch(str) - if len(match) > 1 { - variable := match[0] - varname := match[1] - for _, vrbl := range f.Variables { - if vrbl.Name == varname { - for _, value := range vrbl.Values { - newVar := strings.ReplaceAll(str, variable, value) - vars = append(vars, f.resolve(newVar)...) - } - } - } - } else { - vars = append(vars, str) - } - return vars - } - return []string{str} -} - -// ResolveAttachments resolve profile attachments defined in exec_path -func (f *AppArmorProfileFile) ResolveAttachments() { - p := f.GetDefaultProfile() - - for _, variable := range f.Variables { - if variable.Name == "exec_path" { - for _, value := range variable.Values { - attachments := f.resolve(value) - if len(attachments) == 0 { - panic("Variable not defined in: " + value) - } - p.Attachments = append(p.Attachments, attachments...) - } - } - } -} - -// NestAttachments return a nested attachment string -func (f *AppArmorProfileFile) NestAttachments() string { - p := f.GetDefaultProfile() - if len(p.Attachments) == 0 { - return "" - } else if len(p.Attachments) == 1 { - return p.Attachments[0] - } else { - res := []string{} - for _, attachment := range p.Attachments { - if strings.HasPrefix(attachment, "/") { - res = append(res, attachment[1:]) - } else { - res = append(res, attachment) - } - } - return "/{" + strings.Join(res, ",") + "}" - } -} diff --git a/pkg/aa/variables_test.go b/pkg/aa/variables_test.go deleted file mode 100644 index 8f8f55fa..00000000 --- a/pkg/aa/variables_test.go +++ /dev/null @@ -1,251 +0,0 @@ -// apparmor.d - Full set of apparmor profiles -// Copyright (C) 2023-2024 Alexandre Pujol -// SPDX-License-Identifier: GPL-2.0-only - -package aa - -import ( - "reflect" - "testing" -) - -// TODO: space in variable need to be tested. -// @{name} = "Mullvad VPN" -// profile mullvad-gui /{opt/"Mullvad/mullvad-gui,opt/VPN"/mullvad-gui,mullvad-gui} flags=(attach_disconnected,complain) { - -func TestDefaultTunables(t *testing.T) { - tests := []struct { - name string - want *AppArmorProfileFile - }{ - { - name: "aa", - want: &AppArmorProfileFile{ - Preamble: Rules{ - &Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}}, - &Variable{Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}}, - &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}}, - &Variable{Name: "HOME", Values: []string{"/home/*"}}, - &Variable{Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}}, - &Variable{Name: "etc_ro", Values: []string{"/{,usr/}etc/"}}, - &Variable{Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := DefaultTunables(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("DefaultTunables() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAppArmorProfile_ParseVariables(t *testing.T) { - tests := []struct { - name string - content string - want []*Variable - }{ - { - name: "firefox", - content: `@{firefox_name} = firefox{,-esr,-bin} - @{firefox_lib_dirs} = /{usr/,}lib{,32,64}/@{firefox_name} /opt/@{firefox_name} - @{firefox_config_dirs} = @{HOME}/.mozilla/ - @{firefox_cache_dirs} = @{user_cache_dirs}/mozilla/ - @{exec_path} = /{usr/,}bin/@{firefox_name} @{firefox_lib_dirs}/@{firefox_name} - `, - want: []*Variable{ - {Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}}, - {Name: "firefox_lib_dirs", Values: []string{"/{usr/,}lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, - {Name: "firefox_config_dirs", Values: []string{"@{HOME}/.mozilla/"}}, - {Name: "firefox_cache_dirs", Values: []string{"@{user_cache_dirs}/mozilla/"}}, - {Name: "exec_path", Values: []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, - }, - }, - { - name: "xorg", - content: `@{exec_path} = /{usr/,}bin/X - @{exec_path} += /{usr/,}bin/Xorg{,.bin} - @{exec_path} += /{usr/,}lib/Xorg{,.wrap} - @{exec_path} += /{usr/,}lib/xorg/Xorg{,.wrap}`, - want: []*Variable{ - {Name: "exec_path", Values: []string{ - "/{usr/,}bin/X", - "/{usr/,}bin/Xorg{,.bin}", - "/{usr/,}lib/Xorg{,.wrap}", - "/{usr/,}lib/xorg/Xorg{,.wrap}"}, - }, - }, - }, - { - name: "snapd", - content: `@{lib_dirs} = @{lib}/ /snap/snapd/@{int}@{lib} - @{exec_path} = @{lib_dirs}/snapd/snapd`, - want: []*Variable{ - {Name: "lib_dirs", Values: []string{"@{lib}/", "/snap/snapd/@{int}@{lib}"}}, - {Name: "exec_path", Values: []string{"@{lib_dirs}/snapd/snapd"}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := NewAppArmorProfile() - p.ParseVariables(tt.content) - if !reflect.DeepEqual(p.Variables, tt.want) { - t.Errorf("AppArmorProfile.ParseVariables() = %v, want %v", p.Variables, tt.want) - } - }) - } -} - -func TestAppArmorProfile_resolve(t *testing.T) { - tests := []struct { - name string - input string - want []string - }{ - { - name: "default", - input: "@{etc_ro}", - want: []string{"/{,usr/}etc/"}, - }, - { - name: "empty", - input: "@{}", - want: []string{"@{}"}, - }, - { - name: "nil", - input: "@{foo}", - want: []string{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := DefaultTunables() - if got := p.resolve(tt.input); !reflect.DeepEqual(got, tt.want) { - t.Errorf("AppArmorProfile.resolve() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAppArmorProfile_ResolveAttachments(t *testing.T) { - tests := []struct { - name string - variables []*Variable - want []string - }{ - { - name: "firefox", - variables: []*Variable{ - {Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}}, - {Name: "firefox_lib_dirs", Values: []string{"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, - {Name: "exec_path", Values: []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, - }, - want: []string{ - "/{usr/,}bin/firefox{,-esr,-bin}", - "/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}", - "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}", - }, - }, - { - name: "chromium", - variables: []*Variable{ - {Name: "name", Values: []string{"chromium"}}, - {Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{name}"}}, - {Name: "exec_path", Values: []string{"@{lib_dirs}/@{name}"}}, - }, - want: []string{ - "/{usr/,}lib/chromium/chromium", - }, - }, - { - name: "geoclue", - variables: []*Variable{ - {Name: "libexec", Values: []string{"/{usr/,}libexec"}}, - {Name: "exec_path", Values: []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}}, - }, - want: []string{ - "/{usr/,}libexec/geoclue", - "/{usr/,}libexec/geoclue-2.0/demos/agent", - }, - }, - { - name: "opera", - variables: []*Variable{ - {Name: "multiarch", Values: []string{"*-linux-gnu*"}}, - {Name: "name", Values: []string{"opera{,-beta,-developer}"}}, - {Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{multiarch}/@{name}"}}, - {Name: "exec_path", Values: []string{"@{lib_dirs}/@{name}"}}, - }, - want: []string{ - "/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := NewAppArmorProfile() - p.Variables = tt.variables - p.ResolveAttachments() - profile := p.GetDefaultProfile() - if !reflect.DeepEqual(profile.Attachments, tt.want) { - t.Errorf("AppArmorProfile.ResolveAttachments() = %v, want %v", profile.Attachments, tt.want) - } - }) - } -} - -func TestAppArmorProfile_NestAttachments(t *testing.T) { - tests := []struct { - name string - Attachments []string - want string - }{ - { - name: "firefox", - Attachments: []string{ - "/{usr/,}bin/firefox{,-esr,-bin}", - "/{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}", - "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}", - }, - want: "/{{usr/,}bin/firefox{,-esr,-bin},{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin},opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}}", - }, - { - name: "geoclue", - Attachments: []string{ - "/{usr/,}libexec/geoclue", - "/{usr/,}libexec/geoclue-2.0/demos/agent", - }, - want: "/{{usr/,}libexec/geoclue,{usr/,}libexec/geoclue-2.0/demos/agent}", - }, - { - name: "null", - Attachments: []string{}, - want: "", - }, - { - name: "empty", - Attachments: []string{""}, - want: "", - }, - { - name: "not valid aare", - Attachments: []string{"/file", "relative"}, - want: "/{file,relative}", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := NewAppArmorProfile() - profile := p.GetDefaultProfile() - profile.Attachments = tt.Attachments - if got := p.NestAttachments(); got != tt.want { - t.Errorf("AppArmorProfile.NestAttachments() = %v, want %v", got, tt.want) - } - }) - } -} From ad81c39e31be631f55bdb7cfaacb2e97f7f7fc1e Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 5 May 2024 14:10:14 +0100 Subject: [PATCH 26/62] feat(aa): remove now unsused rule.Sort method. --- pkg/aa/rules.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index c6c5446b..4cfdda3b 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -4,11 +4,6 @@ package aa -import ( - "reflect" - "sort" -) - const ( tokALLOW = "allow" tokAUDIT = "audit" @@ -37,24 +32,3 @@ type Rules []Rule func (r Rules) String() string { return renderTemplate("rules", r) } - -// Sort the rules in a profile. -// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines -func (r Rules) Sort() { - sort.Slice(r, func(i, j int) bool { - typeOfI := reflect.TypeOf(r[i]) - typeOfJ := reflect.TypeOf(r[j]) - if typeOfI != typeOfJ { - valueOfI := typeToValue(typeOfI) - valueOfJ := typeToValue(typeOfJ) - if typeOfI == reflect.TypeOf((*Include)(nil)) && r[i].(*Include).IfExists { - valueOfI = "include_if_exists" - } - if typeOfJ == reflect.TypeOf((*Include)(nil)) && r[j].(*Include).IfExists { - valueOfJ = "include_if_exists" - } - return ruleWeights[valueOfI] < ruleWeights[valueOfJ] - } - return r[i].Less(r[j]) - }) -} From 3ad55927bf8e99bc1c227a7e3e7be4096678a4fe Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 5 May 2024 14:11:00 +0100 Subject: [PATCH 27/62] feat(aa): add basic rules getter --- pkg/aa/rules.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 4cfdda3b..7cb6dc1a 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -32,3 +32,24 @@ type Rules []Rule func (r Rules) String() string { return renderTemplate("rules", r) } + +func (r Rules) Get(filter string) Rules { + res := make(Rules, 0) + for _, rule := range r { + if rule.Kind() == filter { + res = append(res, rule) + } + } + return res +} + +func (r Rules) GetVariables() []*Variable { + res := make([]*Variable, 0) + for _, rule := range r { + switch rule.(type) { + case *Variable: + res = append(res, rule.(*Variable)) + } + } + return res +} From 81f0163086e9a8b0c0015318e383d68df6bebf4b Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 5 May 2024 14:19:25 +0100 Subject: [PATCH 28/62] feat(aa): cleanup, fix import and add some unit tests. --- pkg/aa/base.go | 4 +++- pkg/aa/capability.go | 3 +++ pkg/aa/dbus.go | 4 ++++ pkg/aa/file.go | 11 ++++++++++ pkg/aa/io_uring.go | 2 ++ pkg/aa/mqueue.go | 1 + pkg/aa/network.go | 2 ++ pkg/aa/profile.go | 2 +- pkg/aa/profile_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++ pkg/aa/ptrace.go | 2 ++ pkg/aa/signal.go | 1 + pkg/aa/template.go | 7 +++--- pkg/aa/unix.go | 2 ++ 13 files changed, 86 insertions(+), 5 deletions(-) diff --git a/pkg/aa/base.go b/pkg/aa/base.go index ed12a1d7..7b2bb127 100644 --- a/pkg/aa/base.go +++ b/pkg/aa/base.go @@ -4,7 +4,9 @@ package aa -import "strings" +import ( + "strings" +) type RuleBase struct { IsLineRule bool diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index f458350a..46450e45 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -4,6 +4,9 @@ package aa +import ( + "slices" +) const tokCAPABILITY = "capability" diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index aa88266c..ea88e538 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -4,6 +4,10 @@ package aa +import ( + "slices" +) + const tokDBUS = "dbus" type Dbus struct { diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 8aabd577..4facc57a 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -4,6 +4,17 @@ package aa +import ( + "slices" + "strings" +) + +const ( + tokLINK = "link" + tokOWNER = "owner" +) + + type File struct { RuleBase Qualifier diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index 4f76354c..9ad2829a 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -4,6 +4,8 @@ package aa +import "slices" + const tokIOURING = "io_uring" diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 92a2252c..e00aedb7 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -5,6 +5,7 @@ package aa import ( + "slices" "strings" ) diff --git a/pkg/aa/network.go b/pkg/aa/network.go index 36ef3ac0..f8a286e3 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -4,6 +4,8 @@ package aa +import "slices" + const tokNETWORK = "network" diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 956a7922..8ad9e2a1 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -131,7 +131,7 @@ func (p *Profile) Format() { if letterI != letterJ { // Add a new empty line between Files rule of different type hasOwnerRule = false - p.Rules = append(p.Rules[:i], append([]Rule{&RuleBase{}}, p.Rules[i:]...)...) + p.Rules = append(p.Rules[:i], append(Rules{nil}, p.Rules[i:]...)...) } } } diff --git a/pkg/aa/profile_test.go b/pkg/aa/profile_test.go index 26ea6316..c2edd52c 100644 --- a/pkg/aa/profile_test.go +++ b/pkg/aa/profile_test.go @@ -82,3 +82,53 @@ func TestProfile_AddRule(t *testing.T) { }) } } + +func TestProfile_GetAttachments(t *testing.T) { + tests := []struct { + name string + Attachments []string + want string + }{ + { + name: "firefox", + Attachments: []string{ + "/{usr/,}bin/firefox{,-esr,-bin}", + "/{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + }, + want: "/{{usr/,}bin/firefox{,-esr,-bin},{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin},opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}}", + }, + { + name: "geoclue", + Attachments: []string{ + "/{usr/,}libexec/geoclue", + "/{usr/,}libexec/geoclue-2.0/demos/agent", + }, + want: "/{{usr/,}libexec/geoclue,{usr/,}libexec/geoclue-2.0/demos/agent}", + }, + { + name: "null", + Attachments: []string{}, + want: "", + }, + { + name: "empty", + Attachments: []string{""}, + want: "", + }, + { + name: "not valid aare", + Attachments: []string{"/file", "relative"}, + want: "/{file,relative}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Profile{} + p.Attachments = tt.Attachments + if got := p.GetAttachments(); got != tt.want { + t.Errorf("Profile.GetAttachments() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index 5a014bc7..a8ac55fc 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -4,6 +4,8 @@ package aa +import "slices" + const tokPTRACE = "ptrace" type Ptrace struct { diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 9a6da935..7daaa9a8 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -4,6 +4,7 @@ package aa +import "slices" const tokSIGNAL = "signal" diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 4f433c09..c0b74148 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -40,7 +40,7 @@ var ( tokINCLUDE, tokRLIMIT, tokCAPABILITY, tokNETWORK, tokMOUNT, tokPIVOTROOT, tokCHANGEPROFILE, tokSIGNAL, tokPTRACE, tokUNIX, tokUSERNS, tokIOURING, - tokDBUS, "file", + tokDBUS, "file", "variable", }) // convert apparmor requested mask to apparmor access mode @@ -73,7 +73,7 @@ var ( "profile", "include_if_exists", } - ruleWeights = map[string]int{} + ruleWeights = make(map[string]int, len(ruleAlphabet)) // The order the apparmor file rules should be sorted fileAlphabet = []string{ @@ -98,8 +98,9 @@ var ( "@{PROC}", // 10. Proc files "/dev", // 11. Dev files "deny", // 12. Deny rules + "profile", // 13. Subprofiles } - fileWeights = map[string]int{} + fileWeights = make(map[string]int, len(fileAlphabet)) ) func generateTemplates(names []string) map[string]*template.Template { diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index 3c53dc84..f5915f01 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -4,6 +4,8 @@ package aa +import "slices" + const tokUNIX = "unix" type Unix struct { From e38f5b46374028143af70c27e120ee8b0f33c22d Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 21:56:28 +0100 Subject: [PATCH 29/62] feat(aa): add the link rule. --- pkg/aa/file.go | 81 +++++++++++++++++++++++++++++------ pkg/aa/templates/rule/file.j2 | 20 ++++++++- pkg/aa/templates/rules.j2 | 4 ++ 3 files changed, 92 insertions(+), 13 deletions(-) diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 4facc57a..330ec6a5 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -10,11 +10,12 @@ import ( ) const ( - tokLINK = "link" - tokOWNER = "owner" + tokLINK = "link" + tokFILE = "file" + tokOWNER = "owner" + tokSUBSET = "subset" ) - type File struct { RuleBase Qualifier @@ -25,17 +26,10 @@ type File struct { } func newFileFromLog(log map[string]string) Rule { - owner := false - fsuid, hasFsUID := log["fsuid"] - ouid, hasOuUID := log["ouid"] - isDbus := strings.Contains(log["operation"], "dbus") - if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus { - owner = true - } return &File{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Owner: owner, + Owner: isOwner(log), Path: log["name"], Access: toAccess("file-log", log["requested_mask"]), Target: log["target"], @@ -79,5 +73,68 @@ func (r *File) Constraint() constraint { } func (r *File) Kind() string { - return "file" + return tokFILE +} + +type Link struct { + RuleBase + Qualifier + Owner bool + Subset bool + Path string + Target string +} + +func newLinkFromLog(log map[string]string) Rule { + return &Link{ + RuleBase: newRuleFromLog(log), + Qualifier: newQualifierFromLog(log), + Owner: isOwner(log), + Path: log["name"], + Target: log["target"], + } +} +func (r *Link) Less(other any) bool { + o, _ := other.(*Link) + if r.Path != o.Path { + return r.Path < o.Path + } + if r.Target != o.Target { + return r.Target < o.Target + } + if o.Owner != r.Owner { + return r.Owner + } + if r.Subset != o.Subset { + return r.Subset + } + return r.Qualifier.Less(o.Qualifier) +} + +func (r *Link) Equals(other any) bool { + o, _ := other.(*Link) + return r.Subset == o.Subset && r.Owner == o.Owner && r.Path == o.Path && + r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) +} + +func (r *Link) String() string { + return renderTemplate(r.Kind(), r) +} + +func (r *Link) Constraint() constraint { + return blockKind +} + +func (r *Link) Kind() string { + return tokLINK +} + +func isOwner(log map[string]string) bool { + fsuid, hasFsUID := log["fsuid"] + ouid, hasOuUID := log["ouid"] + isDbus := strings.Contains(log["operation"], "dbus") + if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus { + return true + } + return false } diff --git a/pkg/aa/templates/rule/file.j2 b/pkg/aa/templates/rule/file.j2 index 0021a874..57536d8d 100644 --- a/pkg/aa/templates/rule/file.j2 +++ b/pkg/aa/templates/rule/file.j2 @@ -20,4 +20,22 @@ {{- end -}} {{- "," -}} {{- template "comment" . -}} -{{- end -}} \ No newline at end of file +{{- end -}} + +{{- define "link" -}} + {{- template "qualifier" . -}} + {{- if .Owner -}} + {{- "owner " -}} + {{- end -}} + {{- "link " -}} + {{- if .Subset -}} + {{- "subset " -}} + {{- end -}} + {{- .Path -}} + {{- " " -}} + {{- with .Target -}} + {{ " -> " }}{{ . }} + {{- end -}} + {{- "," -}} + {{- template "comment" . -}} +{{- end -}} diff --git a/pkg/aa/templates/rules.j2 b/pkg/aa/templates/rules.j2 index f2099334..4b66ab38 100644 --- a/pkg/aa/templates/rules.j2 +++ b/pkg/aa/templates/rules.j2 @@ -106,6 +106,10 @@ {{- template "file" . -}} {{- end -}} + {{- if eq $type "Link" -}} + {{- template "link" . -}} + {{- end -}} + {{- if eq $type "Profile" -}} {{- template "profile" . -}} {{- end -}} From 744c745394084842106b0a727c28aa4c40c85b34 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:01:29 +0100 Subject: [PATCH 30/62] feat(aa): add requirements map. --- pkg/aa/capability.go | 15 +++++++++++++++ pkg/aa/change_profile.go | 6 ++++++ pkg/aa/dbus.go | 10 ++++++++++ pkg/aa/file.go | 10 ++++++++++ pkg/aa/io_uring.go | 5 +++++ pkg/aa/mount.go | 13 ++++++++++++- pkg/aa/mqueue.go | 9 +++++++++ pkg/aa/network.go | 23 +++++++++++++++++++++-- pkg/aa/ptrace.go | 8 ++++++++ pkg/aa/rlimit.go | 9 +++++++++ pkg/aa/rules.go | 2 ++ pkg/aa/signal.go | 21 +++++++++++++++++++++ pkg/aa/template.go | 32 +++++++++++++++++++++++++------- pkg/aa/unix.go | 10 ++++++++++ 14 files changed, 163 insertions(+), 10 deletions(-) diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 46450e45..5fe21410 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -10,6 +10,21 @@ import ( const tokCAPABILITY = "capability" +func init() { + requirements[tokCAPABILITY] = requirement{ + "name": { + "audit_control", "audit_read", "audit_write", "block_suspend", "bpf", + "checkpoint_restore", "chown", "dac_override", "dac_read_search", + "fowner", "fsetid", "ipc_lock", "ipc_owner", "kill", "lease", + "linux_immutable", "mac_admin", "mac_override", "mknod", "net_admin", + "net_bind_service", "net_broadcast", "net_raw", "perfmon", "setfcap", + "setgid", "setpcap", "setuid", "sys_admin", "sys_boot", "sys_chroot", + "sys_module", "sys_nice", "sys_pacct", "sys_ptrace", "sys_rawio", + "sys_resource", "sys_time", "sys_tty_config", "syslog", "wake_alarm", + }, + } +} + type Capability struct { RuleBase Qualifier diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index 4d5ded15..7ba53d0d 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -6,6 +6,12 @@ package aa const tokCHANGEPROFILE = "change_profile" +func init() { + requirements[tokCHANGEPROFILE] = requirement{ + "mode": []string{"safe", "unsafe"}, + } +} + type ChangeProfile struct { RuleBase Qualifier diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index ea88e538..41863da9 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -10,6 +10,16 @@ import ( const tokDBUS = "dbus" +func init() { + requirements[tokDBUS] = requirement{ + "access": []string{ + "send", "receive", "bind", "eavesdrop", "r", "read", + "w", "write", "rw", + }, + "bus": []string{"system", "session", "accessibility"}, + } +} + type Dbus struct { RuleBase Qualifier diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 330ec6a5..84450afe 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -16,6 +16,16 @@ const ( tokSUBSET = "subset" ) +func init() { + requirements[tokFILE] = requirement{ + "access": {"m", "r", "w", "l", "k"}, + "transition": { + "ix", "ux", "Ux", "px", "Px", "cx", "Cx", "pix", "Pix", "cix", + "Cix", "pux", "PUx", "cux", "CUx", "x", + }, + } +} + type File struct { RuleBase Qualifier diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index 9ad2829a..effc898d 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -8,6 +8,11 @@ import "slices" const tokIOURING = "io_uring" +func init() { + requirements[tokIOURING] = requirement{ + "access": []string{"sqpoll", "override_creds"}, + } +} type IOUring struct { RuleBase diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 7d7fef3a..7c3f3866 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -15,7 +15,18 @@ const ( tokUMOUNT = "umount" ) -) +func init() { + requirements[tokMOUNT] = requirement{ + "flags": { + "acl", "async", "atime", "bind", "dev", "diratime", "dirsync", "exec", + "iversion", "loud", "mand", "move", "noacl", "noatime", "nodev", + "nodiratime", "noexec", "noiversion", "nomand", "norelatime", "nosuid", + "nouser", "private", "rbind", "relatime", "remount", "ro", "rprivate", + "rshared", "rslave", "runbindable", "rw", "shared", "silent", "slave", + "strictatime", "suid", "sync", "unbindable", "user", "verbose", + }, + } +} type MountConditions struct { FsType string diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index e00aedb7..08bdc4d0 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -11,6 +11,15 @@ import ( const tokMQUEUE = "mqueue" +func init() { + requirements[tokMQUEUE] = requirement{ + "access": []string{ + "r", "w", "rw", "read", "write", "create", "open", + "delete", "getattr", "setattr", + }, + "type": []string{"posix", "sysv"}, + } +} type Mqueue struct { RuleBase diff --git a/pkg/aa/network.go b/pkg/aa/network.go index f8a286e3..554b77ab 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -4,10 +4,29 @@ package aa -import "slices" - const tokNETWORK = "network" +func init() { + requirements[tokNETWORK] = requirement{ + "access": []string{ + "create", "bind", "listen", "accept", "connect", "shutdown", + "getattr", "setattr", "getopt", "setopt", "send", "receive", + "r", "w", "rw", + }, + "domains": []string{ + "unix", "inet", "ax25", "ipx", "appletalk", "netrom", "bridge", + "atmpvc", "x25", "inet6", "rose", "netbeui", "security", "key", + "netlink", "packet", "ash", "econet", "atmsvc", "rds", "sna", "irda", + "pppox", "wanpipe", "llc", "ib", "mpls", "can", "tipc", "bluetooth", + "iucv", "rxrpc", "isdn", "phonet", "ieee802154", "caif", "alg", + "nfc", "vsock", "kcm", "qipcrtr", "smc", "xdp", "mctp", + }, + "type": []string{ + "stream", "dgram", "seqpacket", "rdm", "raw", "packet", + }, + "protocol": []string{"tcp", "udp", "icmp"}, + } +} type AddressExpr struct { Source string diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index a8ac55fc..85106b88 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -8,6 +8,14 @@ import "slices" const tokPTRACE = "ptrace" +func init() { + requirements[tokPTRACE] = requirement{ + "access": []string{ + "r", "w", "rw", "read", "readby", "trace", "tracedby", + }, + } +} + type Ptrace struct { RuleBase Qualifier diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index 4005cd22..fc9ebf28 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -9,6 +9,15 @@ const ( tokSET = "set" ) +func init() { + requirements[tokRLIMIT] = requirement{ + "keys": { + "cpu", "fsize", "data", "stack", "core", "rss", "nofile", "ofile", + "as", "nproc", "memlock", "locks", "sigpending", "msgqueue", "nice", + "rtprio", "rttime", + }, + } +} type Rlimit struct { RuleBase diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 7cb6dc1a..26d90ba3 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -10,6 +10,8 @@ const ( tokDENY = "deny" ) +type requirement map[string][]string + type constraint uint const ( diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 7daaa9a8..c1dc6803 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -8,6 +8,27 @@ import "slices" const tokSIGNAL = "signal" +func init() { + requirements[tokSIGNAL] = requirement{ + "access": { + "r", "w", "rw", "read", "write", "send", "receive", + }, + "set": { + "hup", "int", "quit", "ill", "trap", "abrt", "bus", "fpe", + "kill", "usr1", "segv", "usr2", "pipe", "alrm", "term", "stkflt", + "chld", "cont", "stop", "stp", "ttin", "ttou", "urg", "xcpu", + "xfsz", "vtalrm", "prof", "winch", "io", "pwr", "sys", "emt", + "exists", "rtmin+0", "rtmin+1", "rtmin+2", "rtmin+3", "rtmin+4", + "rtmin+5", "rtmin+6", "rtmin+7", "rtmin+8", "rtmin+9", "rtmin+10", + "rtmin+11", "rtmin+12", "rtmin+13", "rtmin+14", "rtmin+15", + "rtmin+16", "rtmin+17", "rtmin+18", "rtmin+19", "rtmin+20", + "rtmin+21", "rtmin+22", "rtmin+23", "rtmin+24", "rtmin+25", + "rtmin+26", "rtmin+27", "rtmin+28", "rtmin+29", "rtmin+30", + "rtmin+31", "rtmin+32", + }, + } +} + type Signal struct { RuleBase Qualifier diff --git a/pkg/aa/template.go b/pkg/aa/template.go index c0b74148..0d81a729 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -73,7 +73,7 @@ var ( "profile", "include_if_exists", } - ruleWeights = make(map[string]int, len(ruleAlphabet)) + ruleWeights = generateWeights(ruleAlphabet) // The order the apparmor file rules should be sorted fileAlphabet = []string{ @@ -100,9 +100,17 @@ var ( "deny", // 12. Deny rules "profile", // 13. Subprofiles } - fileWeights = make(map[string]int, len(fileAlphabet)) + fileWeights = generateWeights(fileAlphabet) + + // The order the rule values (access, type, domains, etc) should be sorted + requirements = map[string]requirement{} + requirementsWeights map[string]map[string]map[string]int ) +func init() { + requirementsWeights = generateRequirementsWeights(requirements) +} + func generateTemplates(names []string) map[string]*template.Template { res := make(map[string]*template.Template, len(names)) base := template.New("").Funcs(tmplFunctionMap) @@ -132,13 +140,23 @@ func renderTemplate(name string, data any) string { return res.String() } -func init() { - for i, r := range fileAlphabet { - fileWeights[r] = i +func generateWeights(alphabet []string) map[string]int { + res := make(map[string]int, len(alphabet)) + for i, r := range alphabet { + res[r] = i } - for i, r := range ruleAlphabet { - ruleWeights[r] = i + return res +} + +func generateRequirementsWeights(requirements map[string]requirement) map[string]map[string]map[string]int { + res := make(map[string]map[string]map[string]int, len(requirements)) + for rule, req := range requirements { + res[rule] = make(map[string]map[string]int, len(req)) + for key, values := range req { + res[rule][key] = generateWeights(values) + } } + return res } func join(i any) string { diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index f5915f01..3207f571 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -8,6 +8,16 @@ import "slices" const tokUNIX = "unix" +func init() { + requirements[tokUNIX] = requirement{ + "access": []string{ + "create", "bind", "listen", "accept", "connect", "shutdown", + "getattr", "setattr", "getopt", "setopt", "send", "receive", + "r", "w", "rw", + }, + } +} + type Unix struct { RuleBase Qualifier From 05de39d92a221751fc6e6becd0a4064bdd18ee34 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:03:16 +0100 Subject: [PATCH 31/62] feat(aa): improve comment generation from log map. --- pkg/aa/base.go | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/pkg/aa/base.go b/pkg/aa/base.go index 7b2bb127..0e04bc2c 100644 --- a/pkg/aa/base.go +++ b/pkg/aa/base.go @@ -18,38 +18,27 @@ type RuleBase struct { Optional bool } - func newRuleFromLog(log map[string]string) RuleBase { - fileInherit := false + comment := "" + fileInherit, noNewPrivs, optional := false, false, false + if log["operation"] == "file_inherit" { fileInherit = true } - - noNewPrivs := false - optional := false - msg := "" - switch log["error"] { - case "-1": + if log["error"] == "-1" { if strings.Contains(log["info"], "optional:") { optional = true - msg = strings.Replace(log["info"], "optional: ", "", 1) + comment = strings.Replace(log["info"], "optional: ", "", 1) } else { noNewPrivs = true } - case "-13": - ignoreProfileInfo := []string{"namespace", "disconnected path"} - for _, info := range ignoreProfileInfo { - if strings.Contains(log["info"], info) { - break - } - } - msg = log["info"] - default: } - + if log["info"] != "" { + comment += " " + log["info"] + } return RuleBase{ IsLineRule: false, - Comment: msg, + Comment: comment, NoNewPrivs: noNewPrivs, FileInherit: fileInherit, Optional: optional, From 656aa15836395a6ee615e3a131c10d5e4d31f8ee Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:14:43 +0100 Subject: [PATCH 32/62] feat(aa): rewrite the toAccess function to parse, convert and verify the access values. --- pkg/aa/capability.go | 4 +- pkg/aa/file.go | 28 +++++++++++++- pkg/aa/io_uring.go | 2 +- pkg/aa/mount.go | 3 +- pkg/aa/mqueue.go | 2 +- pkg/aa/ptrace.go | 6 ++- pkg/aa/rules.go | 88 ++++++++++++++++++++++++++++++++++++++++++++ pkg/aa/signal.go | 8 ++-- pkg/aa/template.go | 44 +++------------------- pkg/aa/unix.go | 2 +- 10 files changed, 134 insertions(+), 53 deletions(-) diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 5fe21410..cedaef98 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 84450afe..1f2190fd 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 effc898d..ce1a8ae6 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 7c3f3866..88f00637 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 08bdc4d0..52cf7c30 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 85106b88..1b920a26 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 26d90ba3..c42ca020 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 c1dc6803..33265a6d 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 0d81a729..440be9da 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 3207f571..88c37d19 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"], From 474481f1d3de938d0818b884926511034f3b870e Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:16:31 +0100 Subject: [PATCH 33/62] test(aa): update unit tests. --- pkg/aa/data_test.go | 32 +++++++++++++++++++++++++++++--- pkg/aa/rules_test.go | 10 ++++++++-- pkg/aa/templates/rule/file.j2 | 2 +- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go index 5787154b..36058820 100644 --- a/pkg/aa/data_test.go +++ b/pkg/aa/data_test.go @@ -71,13 +71,13 @@ var ( "flags": "rw, rbind", } mount1 = &Mount{ - RuleBase: RuleBase{Comment: "failed perms check"}, + RuleBase: RuleBase{Comment: " failed perms check"}, MountConditions: MountConditions{FsType: "overlay"}, Source: "overlay", MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/", } mount2 = &Mount{ - RuleBase: RuleBase{Comment: "failed perms check"}, + RuleBase: RuleBase{Comment: " failed perms check"}, MountConditions: MountConditions{Options: []string{"rw", "rbind"}}, Source: "/oldroot/dev/tty", MountPoint: "/newroot/dev/tty", @@ -197,7 +197,7 @@ var ( "protocol": "0", } unix1 = &Unix{ - Access: []string{"receive", "send"}, + Access: []string{"send", "receive"}, Type: "stream", Protocol: "0", Address: "none", @@ -290,4 +290,30 @@ var ( Path: "@{PROC}/4163/cgroup", Access: []string{"r"}, } + + // Link + link1Log = map[string]string{ + "apparmor": "ALLOWED", + "operation": "link", + "class": "file", + "profile": "mkinitcpio", + "name": "/tmp/mkinitcpio.QDWtza/early@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst", + "comm": "cp", + "requested_mask": "l", + "denied_mask": "l", + "fsuid": "0", + "ouid": "0", + "target": "/tmp/mkinitcpio.QDWtza/root@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst", + "FSUID": "root", + "OUID": "root", + } + link1 = &Link{ + Path: "/tmp/mkinitcpio.QDWtza/early@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst", + Target: "/tmp/mkinitcpio.QDWtza/root@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst", + } + link2 = &File{ + Owner: true, + Path: "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", + Target: "@{user_config_dirs}/#@{int}", + } ) diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index f44d2e70..d2d9bf87 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -88,6 +88,12 @@ func TestRules_FromLog(t *testing.T) { log: file1Log, want: file1, }, + { + name: "link", + fromLog: newLinkFromLog, + log: link1Log, + want: link1, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -417,7 +423,7 @@ func TestRules_String(t *testing.T) { { name: "mount", rule: mount1, - want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, #failed perms check", + want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check", }, { name: "pivot_root", @@ -442,7 +448,7 @@ func TestRules_String(t *testing.T) { { name: "unix", rule: unix1, - want: "unix (receive send) type=stream protocol=0 addr=none peer=(label=dbus-daemon, addr=@/tmp/dbus-AaKMpxzC4k),", + want: "unix (send receive) type=stream protocol=0 addr=none peer=(label=dbus-daemon, addr=@/tmp/dbus-AaKMpxzC4k),", }, { name: "dbus", diff --git a/pkg/aa/templates/rule/file.j2 b/pkg/aa/templates/rule/file.j2 index 57536d8d..566e7442 100644 --- a/pkg/aa/templates/rule/file.j2 +++ b/pkg/aa/templates/rule/file.j2 @@ -34,7 +34,7 @@ {{- .Path -}} {{- " " -}} {{- with .Target -}} - {{ " -> " }}{{ . }} + {{ "-> " }}{{ . }} {{- end -}} {{- "," -}} {{- template "comment" . -}} From 72107dcfff4c8be8340b6f92767905e88030d8ea Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:17:32 +0100 Subject: [PATCH 34/62] feat(aa): add the hat struct. --- pkg/aa/blocks.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pkg/aa/blocks.go diff --git a/pkg/aa/blocks.go b/pkg/aa/blocks.go new file mode 100644 index 00000000..544106db --- /dev/null +++ b/pkg/aa/blocks.go @@ -0,0 +1,39 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +const ( + tokHAT = "hat" + tokCARET = "^" +) + +// Hat represents a single AppArmor hat. +type Hat struct { + RuleBase + Name string + Rules Rules +} + +func (p *Hat) Less(other any) bool { + o, _ := other.(*Hat) + return p.Name < o.Name +} + +func (p *Hat) Equals(other any) bool { + o, _ := other.(*Profile) + return p.Name == o.Name +} + +func (p *Hat) String() string { + return renderTemplate(p.Kind(), p) +} + +func (p *Hat) Constraint() constraint { + return blockKind +} + +func (p *Hat) Kind() string { + return tokHAT +} From 54fdf388615cc61c5877b446696bf174136300bf Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:21:59 +0100 Subject: [PATCH 35/62] chore: cosmetic & fix. --- pkg/aa/preamble.go | 11 +++-------- pkg/aa/rules.go | 11 +++++++++++ pkg/aa/template.go | 24 ++++++++++++++++++------ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 344ccaa1..2197612f 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -6,24 +6,21 @@ package aa import ( "slices" +) const ( tokABI = "abi" tokALIAS = "alias" tokINCLUDE = "include" tokIFEXISTS = "if exists" + tokVARIABLE = "@{" + tokCOMMENT = "#" ) type Comment struct { RuleBase } -func newCommentFromRule(rule rule) (Rule, error) { - base := newRuleFromRule(rule) - base.IsLineRule = true - return &Comment{RuleBase: base}, nil -} - func (r *Comment) Less(other any) bool { return false } @@ -152,8 +149,6 @@ type Variable struct { Define bool } -} - func (r *Variable) Less(other any) bool { o, _ := other.(*Variable) if r.Name != o.Name { diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index c42ca020..e6ab8ec1 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -62,6 +62,17 @@ func (r Rules) GetVariables() []*Variable { return res } +func (r Rules) GetIncludes() []*Include { + res := make([]*Include, 0) + for _, rule := range r { + switch rule.(type) { + case *Include: + res = append(res, rule.(*Include)) + } + } + 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 { diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 440be9da..10d1c161 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -8,7 +8,6 @@ import ( "embed" "fmt" "reflect" - "slices" "strings" "text/template" ) @@ -36,11 +35,23 @@ var ( // The apparmor templates tmpl = generateTemplates([]string{ - "apparmor", tokPROFILE, "rules", // Global templates - tokINCLUDE, tokRLIMIT, tokCAPABILITY, tokNETWORK, - tokMOUNT, tokPIVOTROOT, tokCHANGEPROFILE, tokSIGNAL, - tokPTRACE, tokUNIX, tokUSERNS, tokIOURING, - tokDBUS, "file", "variable", + // Global templates + "apparmor", + tokPROFILE, + "rules", + + // Preamble templates + tokABI, + tokALIAS, + tokINCLUDE, + "variable", + "comment", + + // Rules templates + tokALL, tokRLIMIT, tokUSERNS, tokCAPABILITY, tokNETWORK, + tokMOUNT, tokREMOUNT, tokUMOUNT, tokPIVOTROOT, tokCHANGEPROFILE, + tokMQUEUE, tokIOURING, tokUNIX, tokPTRACE, tokSIGNAL, tokDBUS, + tokFILE, tokLINK, }) // convert apparmor requested mask to apparmor access mode @@ -72,6 +83,7 @@ var ( "unix", "dbus", "file", + "link", "profile", "include_if_exists", } From 019b6f81973b394612d12e7efaabc955c8d8f230 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:22:32 +0100 Subject: [PATCH 36/62] feat(aa): format now merge access list. --- pkg/aa/profile.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 8ad9e2a1..fdcc912b 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -79,6 +79,21 @@ func (p *Profile) Merge() { if p.Rules[i].Equals(p.Rules[j]) { p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) j-- + continue + } + + // File rule + if typeOfI == reflect.TypeFor[*File]() && typeOfJ == reflect.TypeFor[*File]() { + // Merge access + fileI := p.Rules[i].(*File) + fileJ := p.Rules[j].(*File) + if fileI.Path == fileJ.Path { + fileI.Access = append(fileI.Access, fileJ.Access...) + slices.SortFunc(fileI.Access, cmpFileAccess) + fileI.Access = slices.Compact(fileI.Access) + p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) + j-- + } } } } @@ -93,10 +108,10 @@ func (p *Profile) Sort() { if typeOfI != typeOfJ { valueOfI := typeToValue(typeOfI) valueOfJ := typeToValue(typeOfJ) - if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists { + if typeOfI == reflect.TypeFor[*Include]() && p.Rules[i].(*Include).IfExists { valueOfI = "include_if_exists" } - if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists { + if typeOfJ == reflect.TypeFor[*Include]() && p.Rules[j].(*Include).IfExists { valueOfJ = "include_if_exists" } return ruleWeights[valueOfI] < ruleWeights[valueOfJ] @@ -117,7 +132,7 @@ func (p *Profile) Format() { typeOfJ := reflect.TypeOf(p.Rules[j]) // File rule - if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) { + if typeOfI == reflect.TypeFor[*File]() && typeOfJ == reflect.TypeFor[*File]() { letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) From 9812c38b838969f2981ad1b21ef84ca8c03d81a8 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:22:57 +0100 Subject: [PATCH 37/62] test(aa): add unit tests for the link rule. --- pkg/aa/data_test.go | 21 ++++++++++++++++++++- pkg/aa/rules_test.go | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go index 36058820..d034788d 100644 --- a/pkg/aa/data_test.go +++ b/pkg/aa/data_test.go @@ -292,6 +292,7 @@ var ( } // Link + link3LogStr = `apparmor="ALLOWED" operation="link" class="file" profile="dolphin" name="@{user_config_dirs}/kiorc" comm="dolphin" requested_mask="l" denied_mask="l" fsuid=1000 ouid=1000 target="@{user_config_dirs}/#3954"` link1Log = map[string]string{ "apparmor": "ALLOWED", "operation": "link", @@ -307,13 +308,31 @@ var ( "FSUID": "root", "OUID": "root", } + link3Log = map[string]string{ + "apparmor": "ALLOWED", + "operation": "link", + "class": "file", + "profile": "dolphin", + "name": "@{user_config_dirs}/kiorc", + "comm": "dolphin", + "requested_mask": "l", + "denied_mask": "l", + "fsuid": "1000", + "ouid": "1000", + "target": "@{user_config_dirs}/#3954", + } link1 = &Link{ Path: "/tmp/mkinitcpio.QDWtza/early@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst", Target: "/tmp/mkinitcpio.QDWtza/root@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst", } - link2 = &File{ + link2 = &Link{ Owner: true, Path: "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", Target: "@{user_config_dirs}/#@{int}", } + link3 = &Link{ + Owner: true, + Path: "@{user_config_dirs}/kiorc", + Target: "@{user_config_dirs}/#3954", + } ) diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index d2d9bf87..67c8397e 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -94,6 +94,12 @@ func TestRules_FromLog(t *testing.T) { log: link1Log, want: link1, }, + { + name: "link", + fromLog: newFileFromLog, + log: link3Log, + want: link3, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -267,6 +273,12 @@ func TestRules_Less(t *testing.T) { other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, want: true, }, + { + name: "link", + rule: link1, + other: link2, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -363,6 +375,12 @@ func TestRules_Equals(t *testing.T) { other: file2, want: true, }, + { + name: "link", + rule: link1, + other: link3, + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -473,6 +491,11 @@ func TestRules_String(t *testing.T) { rule: file1, want: "/usr/share/poppler/cMap/Identity-H r,", }, + { + name: "link", + rule: link3, + want: "owner link @{user_config_dirs}/kiorc -> @{user_config_dirs}/#3954,", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 865ce4c66b060a893c38602247273eccf17181db Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:26:51 +0100 Subject: [PATCH 38/62] chore: cosmetic --- pkg/aa/change_profile.go | 2 +- pkg/aa/file.go | 20 ++++++++++---------- pkg/aa/preamble.go | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index 7ba53d0d..f0966bc5 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -51,7 +51,7 @@ func (r *ChangeProfile) Equals(other any) bool { } func (r *ChangeProfile) String() string { - return renderTemplate(tokCHANGEPROFILE, r) + return renderTemplate(r.Kind(), r) } func (r *ChangeProfile) Constraint() constraint { diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 1f2190fd..12d8b96b 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -27,6 +27,16 @@ func init() { } } +func isOwner(log map[string]string) bool { + fsuid, hasFsUID := log["fsuid"] + ouid, hasOuUID := log["ouid"] + isDbus := strings.Contains(log["operation"], "dbus") + if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus { + return true + } + return false +} + // cmpFileAccess compares two access strings for file rules. // It is aimed to be used in slices.SortFunc. func cmpFileAccess(i, j string) int { @@ -164,13 +174,3 @@ func (r *Link) Constraint() constraint { func (r *Link) Kind() string { return tokLINK } - -func isOwner(log map[string]string) bool { - fsuid, hasFsUID := log["fsuid"] - ouid, hasOuUID := log["ouid"] - isDbus := strings.Contains(log["operation"], "dbus") - if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus { - return true - } - return false -} diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 2197612f..539e719c 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -65,7 +65,7 @@ func (r *Abi) Equals(other any) bool { } func (r *Abi) String() string { - return renderTemplate(tokABI, r) + return renderTemplate(r.Kind(), r) } func (r *Abi) Constraint() constraint { @@ -96,7 +96,7 @@ func (r Alias) Equals(other any) bool { } func (r *Alias) String() string { - return renderTemplate(tokALIAS, r) + return renderTemplate(r.Kind(), r) } func (r *Alias) Constraint() constraint { @@ -131,7 +131,7 @@ func (r *Include) Equals(other any) bool { } func (r *Include) String() string { - return renderTemplate(tokINCLUDE, r) + return renderTemplate(r.Kind(), r) } func (r *Include) Constraint() constraint { From 02e33349492367e1c9fcfca2bc5cd9716a6512be Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:30:20 +0100 Subject: [PATCH 39/62] feat(prebuild): add err reporting to builder & directive tasks. --- pkg/prebuild/builder/abi.go | 4 ++-- pkg/prebuild/builder/complain.go | 6 +++--- pkg/prebuild/builder/core.go | 2 +- pkg/prebuild/builder/core_test.go | 8 +++++++- pkg/prebuild/builder/dev.go | 4 ++-- pkg/prebuild/builder/enforce.go | 8 ++++---- pkg/prebuild/builder/fsp.go | 4 ++-- pkg/prebuild/builder/userspace.go | 6 +++--- pkg/prebuild/directive/core.go | 14 +++++++++----- pkg/prebuild/directive/core_test.go | 8 +++++++- pkg/prebuild/directive/dbus.go | 23 +++++++++++++---------- pkg/prebuild/directive/dbus_test.go | 8 +++++++- pkg/prebuild/directive/exec.go | 4 ++-- pkg/prebuild/directive/exec_test.go | 8 +++++++- pkg/prebuild/directive/filter.go | 12 ++++++------ pkg/prebuild/directive/filter_test.go | 16 ++++++++++++++-- pkg/prebuild/directive/stack.go | 8 ++++---- pkg/prebuild/directive/stack_test.go | 8 +++++++- pkg/prebuild/prebuild.go | 10 ++++++++-- 19 files changed, 108 insertions(+), 53 deletions(-) diff --git a/pkg/prebuild/builder/abi.go b/pkg/prebuild/builder/abi.go index 4790ba4c..3b5183a6 100644 --- a/pkg/prebuild/builder/abi.go +++ b/pkg/prebuild/builder/abi.go @@ -30,6 +30,6 @@ func init() { }) } -func (b ABI3) Apply(profile string) string { - return regAbi4To3.Replace(profile) +func (b ABI3) Apply(profile string) (string, error) { + return regAbi4To3.Replace(profile), nil } diff --git a/pkg/prebuild/builder/complain.go b/pkg/prebuild/builder/complain.go index 3970e6df..ad1249c8 100644 --- a/pkg/prebuild/builder/complain.go +++ b/pkg/prebuild/builder/complain.go @@ -30,13 +30,13 @@ func init() { }) } -func (b Complain) Apply(profile string) string { +func (b Complain) Apply(profile string) (string, error) { flags := []string{} matches := regFlags.FindStringSubmatch(profile) if len(matches) != 0 { flags = strings.Split(matches[1], ",") if slices.Contains(flags, "complain") { - return profile + return profile, nil } } flags = append(flags, "complain") @@ -44,5 +44,5 @@ func (b Complain) Apply(profile string) string { // Remove all flags definition, then set manifest' flags profile = regFlags.ReplaceAllLiteralString(profile, "") - return regProfileHeader.ReplaceAllLiteralString(profile, strFlags) + return regProfileHeader.ReplaceAllLiteralString(profile, strFlags), nil } diff --git a/pkg/prebuild/builder/core.go b/pkg/prebuild/builder/core.go index b8dbcbc8..91f07c88 100644 --- a/pkg/prebuild/builder/core.go +++ b/pkg/prebuild/builder/core.go @@ -21,7 +21,7 @@ var ( // Main directive interface type Builder interface { cfg.BaseInterface - Apply(profile string) string + Apply(profile string) (string, error) } func Register(names ...string) { diff --git a/pkg/prebuild/builder/core_test.go b/pkg/prebuild/builder/core_test.go index b0c59e77..3d76e888 100644 --- a/pkg/prebuild/builder/core_test.go +++ b/pkg/prebuild/builder/core_test.go @@ -15,6 +15,7 @@ func TestBuilder_Apply(t *testing.T) { b Builder profile string want string + wantErr bool }{ { name: "abi3", @@ -237,7 +238,12 @@ func TestBuilder_Apply(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.b.Apply(tt.profile); got != tt.want { + got, err := tt.b.Apply(tt.profile) + if (err != nil) != tt.wantErr { + t.Errorf("Builder.Apply() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("Builder.Apply() = %v, want %v", got, tt.want) } }) diff --git a/pkg/prebuild/builder/dev.go b/pkg/prebuild/builder/dev.go index e555e5d9..73020e3b 100644 --- a/pkg/prebuild/builder/dev.go +++ b/pkg/prebuild/builder/dev.go @@ -31,6 +31,6 @@ func init() { }) } -func (b Dev) Apply(profile string) string { - return regDev.Replace(profile) +func (b Dev) Apply(profile string) (string, error) { + return regDev.Replace(profile), nil } diff --git a/pkg/prebuild/builder/enforce.go b/pkg/prebuild/builder/enforce.go index a3bd2c1d..676ac1ef 100644 --- a/pkg/prebuild/builder/enforce.go +++ b/pkg/prebuild/builder/enforce.go @@ -24,16 +24,16 @@ func init() { }) } -func (b Enforce) Apply(profile string) string { +func (b Enforce) Apply(profile string) (string, error) { matches := regFlags.FindStringSubmatch(profile) if len(matches) == 0 { - return profile + return profile, nil } flags := strings.Split(matches[1], ",") idx := slices.Index(flags, "complain") if idx == -1 { - return profile + return profile, nil } flags = slices.Delete(flags, idx, idx+1) strFlags := "{" @@ -43,5 +43,5 @@ func (b Enforce) Apply(profile string) string { // Remove all flags definition, then set new flags profile = regFlags.ReplaceAllLiteralString(profile, "") - return regProfileHeader.ReplaceAllLiteralString(profile, strFlags) + return regProfileHeader.ReplaceAllLiteralString(profile, strFlags), nil } diff --git a/pkg/prebuild/builder/fsp.go b/pkg/prebuild/builder/fsp.go index 07bbbb8a..f118e149 100644 --- a/pkg/prebuild/builder/fsp.go +++ b/pkg/prebuild/builder/fsp.go @@ -28,6 +28,6 @@ func init() { }) } -func (b FullSystemPolicy) Apply(profile string) string { - return regFullSystemPolicy.Replace(profile) +func (b FullSystemPolicy) Apply(profile string) (string, error) { + return regFullSystemPolicy.Replace(profile), nil } diff --git a/pkg/prebuild/builder/userspace.go b/pkg/prebuild/builder/userspace.go index 702fd56d..8a95de7f 100644 --- a/pkg/prebuild/builder/userspace.go +++ b/pkg/prebuild/builder/userspace.go @@ -29,7 +29,7 @@ func init() { }) } -func (b Userspace) Apply(profile string) string { +func (b Userspace) Apply(profile string) (string, error) { p := aa.DefaultTunables() p.ParseVariables(profile) p.ResolveAttachments() @@ -37,7 +37,7 @@ func (b Userspace) Apply(profile string) string { matches := regAttachments.FindAllString(profile, -1) if len(matches) > 0 { strheader := strings.Replace(matches[0], "@{exec_path}", att, -1) - return regAttachments.ReplaceAllLiteralString(profile, strheader) + return regAttachments.ReplaceAllLiteralString(profile, strheader), nil } - return profile + return profile, nil } diff --git a/pkg/prebuild/directive/core.go b/pkg/prebuild/directive/core.go index e6f97e02..8c068981 100644 --- a/pkg/prebuild/directive/core.go +++ b/pkg/prebuild/directive/core.go @@ -26,7 +26,7 @@ var ( // Main directive interface type Directive interface { cfg.BaseInterface - Apply(opt *Option, profile string) string + Apply(opt *Option, profile string) (string, error) } // Directive options @@ -65,14 +65,18 @@ func RegisterDirective(d Directive) { Directives[d.Name()] = d } -func Run(file *paths.Path, profile string) string { +func Run(file *paths.Path, profile string) (string, error) { + var err error for _, match := range regDirective.FindAllStringSubmatch(profile, -1) { opt := NewOption(file, match) drtv, ok := Directives[opt.Name] if !ok { - panic(fmt.Sprintf("Unknown directive: %s", opt.Name)) + return "", fmt.Errorf("Unknown directive: %s", opt.Name) + } + profile, err = drtv.Apply(opt, profile) + if err != nil { + return "", err } - profile = drtv.Apply(opt, profile) } - return profile + return profile, nil } diff --git a/pkg/prebuild/directive/core_test.go b/pkg/prebuild/directive/core_test.go index c74192ff..676520d9 100644 --- a/pkg/prebuild/directive/core_test.go +++ b/pkg/prebuild/directive/core_test.go @@ -70,6 +70,7 @@ func TestRun(t *testing.T) { file *paths.Path profile string want string + wantErr bool }{ { name: "none", @@ -86,7 +87,12 @@ func TestRun(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := Run(tt.file, tt.profile); got != tt.want { + got, err := Run(tt.file, tt.profile) + if (err != nil) != tt.wantErr { + t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("Run() = %v, want %v", got, tt.want) } }) diff --git a/pkg/prebuild/directive/dbus.go b/pkg/prebuild/directive/dbus.go index f98105b5..dc7ac16d 100644 --- a/pkg/prebuild/directive/dbus.go +++ b/pkg/prebuild/directive/dbus.go @@ -50,10 +50,13 @@ func setInterfaces(rules map[string]string) []string { return interfaces } -func (d Dbus) Apply(opt *Option, profile string) string { +func (d Dbus) Apply(opt *Option, profile string) (string, error) { var r aa.Rules - action := d.sanityCheck(opt) + action, err := d.sanityCheck(opt) + if err != nil { + return "", err + } switch action { case "own": r = d.own(opt.ArgMap) @@ -68,26 +71,26 @@ func (d Dbus) Apply(opt *Option, profile string) string { lenDbus := len(generatedDbus) generatedDbus = generatedDbus[:lenDbus-1] profile = strings.Replace(profile, opt.Raw, generatedDbus, -1) - return profile + return profile, nil } -func (d Dbus) sanityCheck(opt *Option) string { +func (d Dbus) sanityCheck(opt *Option) (string, error) { if len(opt.ArgList) < 1 { - panic(fmt.Sprintf("Unknown dbus action: %s in %s", opt.Name, opt.File)) + return "", fmt.Errorf("Unknown dbus action: %s in %s", opt.Name, opt.File) } action := opt.ArgList[0] if action != "own" && action != "talk" { - panic(fmt.Sprintf("Unknown dbus action: %s in %s", opt.Name, opt.File)) + return "", fmt.Errorf("Unknown dbus action: %s in %s", opt.Name, opt.File) } if _, present := opt.ArgMap["name"]; !present { - panic(fmt.Sprintf("Missing name for 'dbus: %s' in %s", action, opt.File)) + return "", fmt.Errorf("Missing name for 'dbus: %s' in %s", action, opt.File) } if _, present := opt.ArgMap["bus"]; !present { - panic(fmt.Sprintf("Missing bus for '%s' in %s", opt.ArgMap["name"], opt.File)) + return "", fmt.Errorf("Missing bus for '%s' in %s", opt.ArgMap["name"], opt.File) } if _, present := opt.ArgMap["label"]; !present && action == "talk" { - panic(fmt.Sprintf("Missing label for '%s' in %s", opt.ArgMap["name"], opt.File)) + return "", fmt.Errorf("Missing label for '%s' in %s", opt.ArgMap["name"], opt.File) } // Set default values @@ -95,7 +98,7 @@ func (d Dbus) sanityCheck(opt *Option) string { opt.ArgMap["path"] = "/" + strings.Replace(opt.ArgMap["name"], ".", "/", -1) + "{,/**}" } opt.ArgMap["name"] += "{,.*}" - return action + return action, nil } func (d Dbus) own(rules map[string]string) aa.Rules { diff --git a/pkg/prebuild/directive/dbus_test.go b/pkg/prebuild/directive/dbus_test.go index 6d7c0594..65e55e78 100644 --- a/pkg/prebuild/directive/dbus_test.go +++ b/pkg/prebuild/directive/dbus_test.go @@ -38,6 +38,7 @@ func TestDbus_Apply(t *testing.T) { opt *Option profile string want string + wantErr bool }{ { name: "own", @@ -137,7 +138,12 @@ func TestDbus_Apply(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := Directives["dbus"].Apply(tt.opt, tt.profile); got != tt.want { + got, err := Directives["dbus"].Apply(tt.opt, tt.profile) + if (err != nil) != tt.wantErr { + t.Errorf("Dbus.Apply() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("Dbus.Apply() = %v, want %v", got, tt.want) } }) diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index a7a8c736..0dc1aec6 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -27,7 +27,7 @@ func init() { }) } -func (d Exec) Apply(opt *Option, profileRaw string) string { +func (d Exec) Apply(opt *Option, profileRaw string) (string, error) { transition := "Px" transitions := []string{"P", "U", "p", "u", "PU", "pu"} t := opt.ArgList[0] @@ -60,5 +60,5 @@ func (d Exec) Apply(opt *Option, profileRaw string) string { rules.Sort() new := rules.String() new = new[:len(new)-1] - return strings.Replace(profileRaw, opt.Raw, new, -1) + return strings.Replace(profileRaw, opt.Raw, new, -1), nil } diff --git a/pkg/prebuild/directive/exec_test.go b/pkg/prebuild/directive/exec_test.go index de675033..f21544c0 100644 --- a/pkg/prebuild/directive/exec_test.go +++ b/pkg/prebuild/directive/exec_test.go @@ -18,6 +18,7 @@ func TestExec_Apply(t *testing.T) { opt *Option profile string want string + wantErr bool }{ { name: "exec", @@ -51,7 +52,12 @@ func TestExec_Apply(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg.RootApparmord = tt.rootApparmord - if got := Directives["exec"].Apply(tt.opt, tt.profile); got != tt.want { + got, err := Directives["exec"].Apply(tt.opt, tt.profile) + if (err != nil) != tt.wantErr { + t.Errorf("Exec.Apply() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("Exec.Apply() = |%v|, want |%v|", got, tt.want) } }) diff --git a/pkg/prebuild/directive/filter.go b/pkg/prebuild/directive/filter.go index b4cc54af..256b0660 100644 --- a/pkg/prebuild/directive/filter.go +++ b/pkg/prebuild/directive/filter.go @@ -41,12 +41,12 @@ func filterRuleForUs(opt *Option) bool { return slices.Contains(opt.ArgList, cfg.Distribution) || slices.Contains(opt.ArgList, cfg.Family) } -func filter(only bool, opt *Option, profile string) string { +func filter(only bool, opt *Option, profile string) (string, error) { if only && filterRuleForUs(opt) { - return profile + return profile, nil } if !only && !filterRuleForUs(opt) { - return profile + return profile, nil } inline := true @@ -64,13 +64,13 @@ func filter(only bool, opt *Option, profile string) string { regRemoveParagraph := regexp.MustCompile(`(?s)` + opt.Raw + `\n.*?\n\n`) profile = regRemoveParagraph.ReplaceAllString(profile, "") } - return profile + return profile, nil } -func (d FilterOnly) Apply(opt *Option, profile string) string { +func (d FilterOnly) Apply(opt *Option, profile string) (string, error) { return filter(true, opt, profile) } -func (d FilterExclude) Apply(opt *Option, profile string) string { +func (d FilterExclude) Apply(opt *Option, profile string) (string, error) { return filter(false, opt, profile) } diff --git a/pkg/prebuild/directive/filter_test.go b/pkg/prebuild/directive/filter_test.go index 4dbeca91..69e1bff9 100644 --- a/pkg/prebuild/directive/filter_test.go +++ b/pkg/prebuild/directive/filter_test.go @@ -18,6 +18,7 @@ func TestFilterOnly_Apply(t *testing.T) { opt *Option profile string want string + wantErr bool }{ { name: "inline", @@ -79,7 +80,12 @@ func TestFilterOnly_Apply(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cfg.Distribution = tt.dist cfg.Family = tt.family - if got := Directives["only"].Apply(tt.opt, tt.profile); got != tt.want { + got, err := Directives["only"].Apply(tt.opt, tt.profile) + if (err != nil) != tt.wantErr { + t.Errorf("FilterOnly.Apply() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("FilterOnly.Apply() = %v, want %v", got, tt.want) } }) @@ -94,6 +100,7 @@ func TestFilterExclude_Apply(t *testing.T) { opt *Option profile string want string + wantErr bool }{ { name: "inline", @@ -128,7 +135,12 @@ func TestFilterExclude_Apply(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cfg.Distribution = tt.dist cfg.Family = tt.family - if got := Directives["exclude"].Apply(tt.opt, tt.profile); got != tt.want { + got, err := Directives["exclude"].Apply(tt.opt, tt.profile) + if (err != nil) != tt.wantErr { + t.Errorf("FilterExclude.Apply() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("FilterExclude.Apply() = %v, want %v", got, tt.want) } }) diff --git a/pkg/prebuild/directive/stack.go b/pkg/prebuild/directive/stack.go index cb891acc..e0ab9d84 100644 --- a/pkg/prebuild/directive/stack.go +++ b/pkg/prebuild/directive/stack.go @@ -38,13 +38,13 @@ func init() { }) } -func (s Stack) Apply(opt *Option, profile string) string { +func (s Stack) Apply(opt *Option, profile string) (string, error) { res := "" for name := range opt.ArgMap { stackedProfile := util.MustReadFile(cfg.RootApparmord.Join(name)) m := regRules.FindStringSubmatch(stackedProfile) if len(m) < 2 { - panic(fmt.Sprintf("No profile found in %s", name)) + return "", fmt.Errorf("No profile found in %s", name) } stackedRules := m[1] stackedRules = regCleanStakedRules.Replace(stackedRules) @@ -54,9 +54,9 @@ func (s Stack) Apply(opt *Option, profile string) string { // Insert the stacked profile at the end of the current profile, remove the stack directive m := regEndOfRules.FindStringSubmatch(profile) if len(m) <= 1 { - panic(fmt.Sprintf("No end of rules found in %s", opt.File)) + return "", fmt.Errorf("No end of rules found in %s", opt.File) } profile = strings.Replace(profile, m[0], res+m[0], -1) profile = strings.Replace(profile, opt.Raw, "", -1) - return profile + return profile, nil } diff --git a/pkg/prebuild/directive/stack_test.go b/pkg/prebuild/directive/stack_test.go index 4d5a284a..47f1cb28 100644 --- a/pkg/prebuild/directive/stack_test.go +++ b/pkg/prebuild/directive/stack_test.go @@ -18,6 +18,7 @@ func TestStack_Apply(t *testing.T) { opt *Option profile string want string + wantErr bool }{ { name: "stack", @@ -68,7 +69,12 @@ profile parent @{exec_path} { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg.RootApparmord = tt.rootApparmord - if got := Directives["stack"].Apply(tt.opt, tt.profile); got != tt.want { + got, err := Directives["stack"].Apply(tt.opt, tt.profile) + if (err != nil) != tt.wantErr { + t.Errorf("Stack.Apply() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("Stack.Apply() = %v, want %v", got, tt.want) } }) diff --git a/pkg/prebuild/prebuild.go b/pkg/prebuild/prebuild.go index 4553156c..2dd57183 100644 --- a/pkg/prebuild/prebuild.go +++ b/pkg/prebuild/prebuild.go @@ -84,9 +84,15 @@ func Build() error { return err } for _, b := range builder.Builds { - profile = b.Apply(profile) + profile, err = b.Apply(profile) + if err != nil { + return err + } + } + profile, err = directive.Run(file, profile) + if err != nil { + return err } - profile = directive.Run(file, profile) if err := file.WriteFile([]byte(profile)); err != nil { return err } From 2dd6046697c2b0c468714bba83f3b17063193cee Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:32:10 +0100 Subject: [PATCH 40/62] feat(prebuild): add builder opt to build tasks. --- pkg/prebuild/builder/abi.go | 2 +- pkg/prebuild/builder/complain.go | 2 +- pkg/prebuild/builder/core.go | 28 +++++++++++++++++++++++++++- pkg/prebuild/builder/core_test.go | 3 ++- pkg/prebuild/builder/dev.go | 2 +- pkg/prebuild/builder/enforce.go | 2 +- pkg/prebuild/builder/fsp.go | 2 +- pkg/prebuild/builder/userspace.go | 2 +- pkg/prebuild/prebuild.go | 8 +++----- 9 files changed, 38 insertions(+), 13 deletions(-) diff --git a/pkg/prebuild/builder/abi.go b/pkg/prebuild/builder/abi.go index 3b5183a6..72b3943d 100644 --- a/pkg/prebuild/builder/abi.go +++ b/pkg/prebuild/builder/abi.go @@ -30,6 +30,6 @@ func init() { }) } -func (b ABI3) Apply(profile string) (string, error) { +func (b ABI3) Apply(opt *Option, profile string) (string, error) { return regAbi4To3.Replace(profile), nil } diff --git a/pkg/prebuild/builder/complain.go b/pkg/prebuild/builder/complain.go index ad1249c8..e0f9f26b 100644 --- a/pkg/prebuild/builder/complain.go +++ b/pkg/prebuild/builder/complain.go @@ -30,7 +30,7 @@ func init() { }) } -func (b Complain) Apply(profile string) (string, error) { +func (b Complain) Apply(opt *Option, profile string) (string, error) { flags := []string{} matches := regFlags.FindStringSubmatch(profile) if len(matches) != 0 { diff --git a/pkg/prebuild/builder/core.go b/pkg/prebuild/builder/core.go index 91f07c88..e6512820 100644 --- a/pkg/prebuild/builder/core.go +++ b/pkg/prebuild/builder/core.go @@ -7,6 +7,7 @@ package builder import ( "fmt" + "github.com/arduino/go-paths-helper" "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" ) @@ -21,7 +22,20 @@ var ( // Main directive interface type Builder interface { cfg.BaseInterface - Apply(profile string) (string, error) + Apply(opt *Option, profile string) (string, error) +} + +// Builder options +type Option struct { + Name string + File *paths.Path +} + +func NewOption(file *paths.Path) *Option { + return &Option{ + Name: file.Base(), + File: file, + } } func Register(names ...string) { @@ -37,3 +51,15 @@ func Register(names ...string) { func RegisterBuilder(d Builder) { Builders[d.Name()] = d } + +func Run(file *paths.Path, profile string) (string, error) { + var err error + opt := NewOption(file) + for _, b := range Builds { + profile, err = b.Apply(opt, profile) + if err != nil { + return "", err + } + } + return profile, nil +} diff --git a/pkg/prebuild/builder/core_test.go b/pkg/prebuild/builder/core_test.go index 3d76e888..838d63df 100644 --- a/pkg/prebuild/builder/core_test.go +++ b/pkg/prebuild/builder/core_test.go @@ -238,7 +238,8 @@ func TestBuilder_Apply(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.b.Apply(tt.profile) + opt := &Option{} + got, err := tt.b.Apply(opt, tt.profile) if (err != nil) != tt.wantErr { t.Errorf("Builder.Apply() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/prebuild/builder/dev.go b/pkg/prebuild/builder/dev.go index 73020e3b..f8ebdff0 100644 --- a/pkg/prebuild/builder/dev.go +++ b/pkg/prebuild/builder/dev.go @@ -31,6 +31,6 @@ func init() { }) } -func (b Dev) Apply(profile string) (string, error) { +func (b Dev) Apply(opt *Option, profile string) (string, error) { return regDev.Replace(profile), nil } diff --git a/pkg/prebuild/builder/enforce.go b/pkg/prebuild/builder/enforce.go index 676ac1ef..bc25e03d 100644 --- a/pkg/prebuild/builder/enforce.go +++ b/pkg/prebuild/builder/enforce.go @@ -24,7 +24,7 @@ func init() { }) } -func (b Enforce) Apply(profile string) (string, error) { +func (b Enforce) Apply(opt *Option, profile string) (string, error) { matches := regFlags.FindStringSubmatch(profile) if len(matches) == 0 { return profile, nil diff --git a/pkg/prebuild/builder/fsp.go b/pkg/prebuild/builder/fsp.go index f118e149..003f7952 100644 --- a/pkg/prebuild/builder/fsp.go +++ b/pkg/prebuild/builder/fsp.go @@ -28,6 +28,6 @@ func init() { }) } -func (b FullSystemPolicy) Apply(profile string) (string, error) { +func (b FullSystemPolicy) Apply(opt *Option, profile string) (string, error) { return regFullSystemPolicy.Replace(profile), nil } diff --git a/pkg/prebuild/builder/userspace.go b/pkg/prebuild/builder/userspace.go index 8a95de7f..a8bbbf6e 100644 --- a/pkg/prebuild/builder/userspace.go +++ b/pkg/prebuild/builder/userspace.go @@ -29,7 +29,7 @@ func init() { }) } -func (b Userspace) Apply(profile string) (string, error) { +func (b Userspace) Apply(opt *Option, profile string) (string, error) { p := aa.DefaultTunables() p.ParseVariables(profile) p.ResolveAttachments() diff --git a/pkg/prebuild/prebuild.go b/pkg/prebuild/prebuild.go index 2dd57183..aafeb7b4 100644 --- a/pkg/prebuild/prebuild.go +++ b/pkg/prebuild/prebuild.go @@ -83,11 +83,9 @@ func Build() error { if err != nil { return err } - for _, b := range builder.Builds { - profile, err = b.Apply(profile) - if err != nil { - return err - } + profile, err = builder.Run(file, profile) + if err != nil { + return err } profile, err = directive.Run(file, profile) if err != nil { From 92641e7e2873f94e1e2102f89f25719f5e36f5c9 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sat, 25 May 2024 22:36:39 +0100 Subject: [PATCH 41/62] feat(aa): add initial profile validation structure. --- pkg/aa/all.go | 4 ++++ pkg/aa/apparmor.go | 13 +++++++++++++ pkg/aa/blocks.go | 4 ++++ pkg/aa/capability.go | 8 ++++++++ pkg/aa/change_profile.go | 7 +++++++ pkg/aa/dbus.go | 8 ++++++++ pkg/aa/file.go | 8 ++++++++ pkg/aa/io_uring.go | 12 +++++++++++- pkg/aa/mount.go | 25 +++++++++++++++++++++++++ pkg/aa/mqueue.go | 11 +++++++++++ pkg/aa/network.go | 17 +++++++++++++++++ pkg/aa/pivot_root.go | 4 ++++ pkg/aa/preamble.go | 20 ++++++++++++++++++++ pkg/aa/profile.go | 19 +++++++++++++++++++ pkg/aa/ptrace.go | 8 ++++++++ pkg/aa/rlimit.go | 7 +++++++ pkg/aa/rules.go | 22 ++++++++++++++++++++++ pkg/aa/signal.go | 11 +++++++++++ pkg/aa/unix.go | 12 +++++++++++- pkg/aa/userns.go | 4 ++++ 20 files changed, 222 insertions(+), 2 deletions(-) diff --git a/pkg/aa/all.go b/pkg/aa/all.go index 8c0527be..608bfd9a 100644 --- a/pkg/aa/all.go +++ b/pkg/aa/all.go @@ -12,6 +12,10 @@ type All struct { RuleBase } +func (r *All) Validate() error { + return nil +} + func (r *All) Less(other any) bool { return false } diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index 6ddcd7a4..13f0b6b1 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -49,6 +49,19 @@ func (f *AppArmorProfileFile) String() string { return renderTemplate("apparmor", f) } +// Validate the profile file +func (f *AppArmorProfileFile) Validate() error { + if err := f.Preamble.Validate(); err != nil { + return err + } + for _, p := range f.Profiles { + if err := p.Validate(); err != nil { + return err + } + } + return nil +} + // GetDefaultProfile ensure a profile is always present in the profile file and // return it, as a default profile. func (f *AppArmorProfileFile) GetDefaultProfile() *Profile { diff --git a/pkg/aa/blocks.go b/pkg/aa/blocks.go index 544106db..8b4f3ab5 100644 --- a/pkg/aa/blocks.go +++ b/pkg/aa/blocks.go @@ -16,6 +16,10 @@ type Hat struct { Rules Rules } +func (r *Hat) Validate() error { + return nil +} + func (p *Hat) Less(other any) bool { o, _ := other.(*Hat) return p.Name < o.Name diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index cedaef98..33a0a6a8 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "slices" ) @@ -39,6 +40,13 @@ func newCapabilityFromLog(log map[string]string) Rule { } } +func (r *Capability) Validate() error { + if err := validateValues(r.Kind(), "name", r.Names); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Capability) Less(other any) bool { o, _ := other.(*Capability) for i := 0; i < len(r.Names) && i < len(o.Names); i++ { diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index f0966bc5..cc21322a 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -30,6 +30,13 @@ func newChangeProfileFromLog(log map[string]string) Rule { } } +func (r *ChangeProfile) Validate() error { + if err := validateValues(r.Kind(), "mode", []string{r.ExecMode}); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *ChangeProfile) Less(other any) bool { o, _ := other.(*ChangeProfile) if r.ExecMode != o.ExecMode { diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index 41863da9..ded2b3cb 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "slices" ) @@ -55,6 +56,13 @@ func newDbusFromLog(log map[string]string) Rule { } } +func (r *Dbus) Validate() error { + if err := validateValues(r.Kind(), "access", r.Access); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return validateValues(r.Kind(), "bus", []string{r.Bus}) +} + func (r *Dbus) Less(other any) bool { o, _ := other.(*Dbus) for i := 0; i < len(r.Access) && i < len(o.Access); i++ { diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 12d8b96b..18e2c5cb 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -81,6 +81,10 @@ func newFileFromLog(log map[string]string) Rule { } } +func (r *File) Validate() error { + return nil +} + func (r *File) Less(other any) bool { o, _ := other.(*File) letterR := getLetterIn(fileAlphabet, r.Path) @@ -140,6 +144,10 @@ func newLinkFromLog(log map[string]string) Rule { } } +func (r *Link) Validate() error { + return nil +} + 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 ce1a8ae6..13c1031b 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -4,7 +4,10 @@ package aa -import "slices" +import ( + "fmt" + "slices" +) const tokIOURING = "io_uring" @@ -30,6 +33,13 @@ func newIOUringFromLog(log map[string]string) Rule { } } +func (r *IOUring) Validate() error { + if err := validateValues(r.Kind(), "access", r.Access); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *IOUring) Less(other any) bool { o, _ := other.(*IOUring) if len(r.Access) != len(o.Access) { diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 88f00637..4b177442 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -42,6 +42,10 @@ func newMountConditionsFromLog(log map[string]string) MountConditions { return MountConditions{FsType: log["fstype"]} } +func (m MountConditions) Validate() error { + return validateValues(tokMOUNT, "flags", m.Options) +} + func (m MountConditions) Less(other MountConditions) bool { if m.FsType != other.FsType { return m.FsType < other.FsType @@ -71,6 +75,13 @@ func newMountFromLog(log map[string]string) Rule { } } +func (r *Mount) Validate() error { + if err := r.MountConditions.Validate(); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Mount) Less(other any) bool { o, _ := other.(*Mount) if r.Source != o.Source { @@ -120,6 +131,13 @@ func newUmountFromLog(log map[string]string) Rule { } } +func (r *Umount) Validate() error { + if err := r.MountConditions.Validate(); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Umount) Less(other any) bool { o, _ := other.(*Umount) if r.MountPoint != o.MountPoint { @@ -166,6 +184,13 @@ func newRemountFromLog(log map[string]string) Rule { } } +func (r *Remount) Validate() error { + if err := r.MountConditions.Validate(); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Remount) Less(other any) bool { o, _ := other.(*Remount) if r.MountPoint != o.MountPoint { diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 52cf7c30..7afb1179 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "slices" "strings" ) @@ -47,6 +48,16 @@ func newMqueueFromLog(log map[string]string) Rule { } } +func (r *Mqueue) Validate() error { + if err := validateValues(r.Kind(), "access", r.Access); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + if err := validateValues(r.Kind(), "type", []string{r.Type}); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Mqueue) Less(other any) bool { o, _ := other.(*Mqueue) if len(r.Access) != len(o.Access) { diff --git a/pkg/aa/network.go b/pkg/aa/network.go index 554b77ab..edc78100 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -4,6 +4,10 @@ package aa +import ( + "fmt" +) + const tokNETWORK = "network" func init() { @@ -77,6 +81,19 @@ func newNetworkFromLog(log map[string]string) Rule { } } +func (r *Network) Validate() error { + if err := validateValues(r.Kind(), "domains", []string{r.Domain}); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + if err := validateValues(r.Kind(), "type", []string{r.Type}); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + if err := validateValues(r.Kind(), "protocol", []string{r.Protocol}); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Network) Less(other any) bool { o, _ := other.(*Network) if r.Domain != o.Domain { diff --git a/pkg/aa/pivot_root.go b/pkg/aa/pivot_root.go index 3c421adf..7c3f5ab8 100644 --- a/pkg/aa/pivot_root.go +++ b/pkg/aa/pivot_root.go @@ -24,6 +24,10 @@ func newPivotRootFromLog(log map[string]string) Rule { } } +func (r *PivotRoot) Validate() error { + return nil +} + func (r *PivotRoot) Less(other any) bool { o, _ := other.(*PivotRoot) if r.OldRoot != o.OldRoot { diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 539e719c..8d28612b 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -21,6 +21,10 @@ type Comment struct { RuleBase } +func (r *Comment) Validate() error { + return nil +} + func (r *Comment) Less(other any) bool { return false } @@ -51,6 +55,10 @@ type Abi struct { IsMagic bool } +func (r *Abi) Validate() error { + return nil +} + func (r *Abi) Less(other any) bool { o, _ := other.(*Abi) if r.Path != o.Path { @@ -82,6 +90,10 @@ type Alias struct { RewrittenPath string } +func (r *Alias) Validate() error { + return nil +} + func (r Alias) Less(other any) bool { o, _ := other.(*Alias) if r.Path != o.Path { @@ -114,6 +126,10 @@ type Include struct { IsMagic bool } +func (r *Include) Validate() error { + return nil +} + func (r *Include) Less(other any) bool { o, _ := other.(*Include) if r.Path == o.Path { @@ -149,6 +165,10 @@ type Variable struct { Define bool } +func (r *Variable) Validate() error { + return nil +} + func (r *Variable) Less(other any) bool { o, _ := other.(*Variable) if r.Name != o.Name { diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index fdcc912b..ec646ea2 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "maps" "reflect" "slices" @@ -18,6 +19,17 @@ const ( tokPROFILE = "profile" ) +func init() { + requirements[tokPROFILE] = requirement{ + tokFLAGS: { + "enforce", "complain", "kill", "default_allow", "unconfined", + "prompt", "audit", "mediate_deleted", "attach_disconnected", + "attach_disconneced.path=", "chroot_relative", "debug", + "interruptible", "kill", "kill.signal=", + }, + } +} + // Profile represents a single AppArmor profile. type Profile struct { RuleBase @@ -33,6 +45,13 @@ type Header struct { Flags []string } +func (r *Profile) Validate() error { + if err := validateValues(r.Kind(), tokFLAGS, r.Flags); err != nil { + return fmt.Errorf("profile %s: %w", r.Name, err) + } + return r.Rules.Validate() +} + func (p *Profile) Less(other any) bool { o, _ := other.(*Profile) if p.Name != o.Name { diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index 1b920a26..d73060ed 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "slices" ) @@ -34,6 +35,13 @@ func newPtraceFromLog(log map[string]string) Rule { } } +func (r *Ptrace) Validate() error { + if err := validateValues(r.Kind(), "access", r.Access); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Ptrace) Less(other any) bool { o, _ := other.(*Ptrace) if len(r.Access) != len(o.Access) { diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index fc9ebf28..1f2c484f 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -35,6 +35,13 @@ func newRlimitFromLog(log map[string]string) Rule { } } +func (r *Rlimit) Validate() error { + if err := validateValues(r.Kind(), "keys", []string{r.Key}); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Rlimit) Less(other any) bool { o, _ := other.(*Rlimit) if r.Key != o.Key { diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index e6ab8ec1..0a971f5d 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -28,6 +28,7 @@ const ( // Rule generic interface for all AppArmor rules type Rule interface { + Validate() error Less(other any) bool Equals(other any) bool String() string @@ -37,6 +38,15 @@ type Rule interface { type Rules []Rule +func (r Rules) Validate() error { + for _, rule := range r { + if err := rule.Validate(); err != nil { + return err + } + } + return nil +} + func (r Rules) String() string { return renderTemplate("rules", r) } @@ -82,6 +92,18 @@ func Must[T any](v T, err error) T { return v } +func validateValues(rule string, key string, values []string) error { + for _, v := range values { + if v == "" { + continue + } + if !slices.Contains(requirements[rule][key], v) { + return fmt.Errorf("invalid mode '%s'", v) + } + } + return nil +} + // 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) { diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 33265a6d..ace95d79 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "slices" ) @@ -49,6 +50,16 @@ func newSignalFromLog(log map[string]string) Rule { } } +func (r *Signal) Validate() error { + if err := validateValues(r.Kind(), "access", r.Access); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + if err := validateValues(r.Kind(), "set", r.Set); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Signal) Less(other any) bool { o, _ := other.(*Signal) if len(r.Access) != len(o.Access) { diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index 88c37d19..eefe049f 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -4,7 +4,10 @@ package aa -import "slices" +import ( + "fmt" + "slices" +) const tokUNIX = "unix" @@ -48,6 +51,13 @@ func newUnixFromLog(log map[string]string) Rule { } } +func (r *Unix) Validate() error { + if err := validateValues(r.Kind(), "access", r.Access); err != nil { + return fmt.Errorf("%s: %w", r, err) + } + return nil +} + func (r *Unix) Less(other any) bool { o, _ := other.(*Unix) if len(r.Access) != len(o.Access) { diff --git a/pkg/aa/userns.go b/pkg/aa/userns.go index 5e9437fa..e8409313 100644 --- a/pkg/aa/userns.go +++ b/pkg/aa/userns.go @@ -20,6 +20,10 @@ func newUsernsFromLog(log map[string]string) Rule { } } +func (r *Userns) Validate() error { + return nil +} + func (r *Userns) Less(other any) bool { o, _ := other.(*Userns) if r.Create != o.Create { From 2e043d4ec89fd329198113cc0d1da71cc6faea6d Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 26 May 2024 18:05:15 +0100 Subject: [PATCH 42/62] feat(aa): add some rules methods. --- pkg/aa/change_profile.go | 2 ++ pkg/aa/mount.go | 1 + pkg/aa/rlimit.go | 2 ++ pkg/aa/rules.go | 57 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index cc21322a..be08692f 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -4,6 +4,8 @@ package aa +import "fmt" + const tokCHANGEPROFILE = "change_profile" func init() { diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 4b177442..7d2e0388 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -5,6 +5,7 @@ package aa import ( + "fmt" "slices" ) diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index 1f2c484f..d3a29049 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -4,6 +4,8 @@ package aa +import "fmt" + const ( tokRLIMIT = "rlimit" tokSET = "set" diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 0a971f5d..6eec8167 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -51,10 +51,56 @@ func (r Rules) String() string { return renderTemplate("rules", r) } -func (r Rules) Get(filter string) Rules { +func (r Rules) IndexOf(rule Rule) int { + for idx, rr := range r { + if rr.Kind() == rule.Kind() && rr.Equals(rule) { + return idx + } + } + return -1 +} + +func (r Rules) Contains(rule Rule) bool { + return r.IndexOf(rule) != -1 +} + +func (r Rules) Add(rule Rule) Rules { + if r.Contains(rule) { + return r + } + return append(r, rule) +} + +func (r Rules) Remove(rule Rule) Rules { + idx := r.IndexOf(rule) + if idx == -1 { + return r + } + return append(r[:idx], r[idx+1:]...) +} + +func (r Rules) Insert(idx int, rules ...Rule) Rules { + return append(r[:idx], append(rules, r[idx:]...)...) +} + +func (r Rules) Sort() Rules { + return r +} + +func (r Rules) DeleteKind(kind string) Rules { res := make(Rules, 0) for _, rule := range r { - if rule.Kind() == filter { + if rule.Kind() != kind { + res = append(res, rule) + } + } + return res +} + +func (r Rules) Filter(filter string) Rules { + res := make(Rules, 0) + for _, rule := range r { + if rule.Kind() != filter { res = append(res, rule) } } @@ -120,9 +166,10 @@ func toValues(rule string, key string, input string) ([]string, error) { sep = " " } res := strings.Split(input, sep) - for _, access := range res { - if !slices.Contains(req, access) { - return nil, fmt.Errorf("unrecognized %s: %s", key, access) + for idx := range res { + res[idx] = strings.Trim(res[idx], `" `) + if !slices.Contains(req, res[idx]) { + return nil, fmt.Errorf("unrecognized %s: %s", key, res[idx]) } } slices.SortFunc(res, func(i, j string) int { From a99387c323348703b30240f3e711b60eddbc82d7 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Mon, 27 May 2024 18:55:21 +0100 Subject: [PATCH 43/62] feat(aa): parse apparmor preamble files. --- pkg/aa/base.go | 31 +++++ pkg/aa/parse.go | 238 ++++++++++++++++++++++++++++++++++++ pkg/aa/parse_test.go | 281 +++++++++++++++++++++++++++++++++++++++++++ pkg/aa/preamble.go | 114 ++++++++++++++++++ pkg/aa/profile.go | 45 +++++++ pkg/aa/rules.go | 9 +- 6 files changed, 710 insertions(+), 8 deletions(-) create mode 100644 pkg/aa/parse.go create mode 100644 pkg/aa/parse_test.go diff --git a/pkg/aa/base.go b/pkg/aa/base.go index 0e04bc2c..13a67527 100644 --- a/pkg/aa/base.go +++ b/pkg/aa/base.go @@ -18,6 +18,37 @@ type RuleBase struct { Optional bool } +func newRule(rule []string) RuleBase { + comment := "" + fileInherit, noNewPrivs, optional := false, false, false + + idx := 0 + for idx < len(rule) { + if rule[idx] == tokCOMMENT { + comment = " " + strings.Join(rule[idx+1:], " ") + break + } + idx++ + } + switch { + case strings.Contains(comment, "file_inherit"): + fileInherit = true + comment = strings.Replace(comment, "file_inherit ", "", 1) + case strings.HasPrefix(comment, "no new privs"): + noNewPrivs = true + comment = strings.Replace(comment, "no new privs ", "", 1) + case strings.Contains(comment, "optional:"): + optional = true + comment = strings.Replace(comment, "optional: ", "", 1) + } + return RuleBase{ + Comment: comment, + NoNewPrivs: noNewPrivs, + FileInherit: fileInherit, + Optional: optional, + } +} + func newRuleFromLog(log map[string]string) RuleBase { comment := "" fileInherit, noNewPrivs, optional := false, false, false diff --git a/pkg/aa/parse.go b/pkg/aa/parse.go new file mode 100644 index 00000000..b9d65eb9 --- /dev/null +++ b/pkg/aa/parse.go @@ -0,0 +1,238 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "fmt" + "slices" + "strings" +) + +const ( + tokARROW = "->" + tokEQUAL = "=" + tokLESS = "<" + tokPLUS = "+" + tokCLOSEBRACE = '}' + tokCLOSEBRACKET = ']' + tokCLOSEPAREN = ')' + tokCOLON = ',' + tokOPENBRACE = '{' + tokOPENBRACKET = '[' + tokOPENPAREN = '(' +) + +var ( + newRuleMap = map[string]func([]string) (Rule, error){ + tokCOMMENT: newComment, + tokABI: newAbi, + tokALIAS: newAlias, + tokINCLUDE: newInclude, + } + + openBlocks = []rune{tokOPENPAREN, tokOPENBRACE, tokOPENBRACKET} + closeBlocks = []rune{tokCLOSEPAREN, tokCLOSEBRACE, tokCLOSEBRACKET} +) + +// Split a raw input rule string into tokens by space or =, but ignore spaces +// within quotes, brakets, or parentheses. +// +// Example: +// +// `owner @{user_config_dirs}/powerdevilrc{,.@{rand6}} rwl -> @{user_config_dirs}/#@{int}` +// +// Returns: +// +// []string{"owner", "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", "rwl", "->", "@{user_config_dirs}/#@{int}"} +func tokenize(str string) []string { + var currentToken strings.Builder + var isVariable bool + var quoted bool + + blockStack := []rune{} + tokens := make([]string, 0, len(str)/2) + if len(str) > 2 && str[0:2] == tokVARIABLE { + isVariable = true + } + for _, r := range str { + switch { + case (r == ' ' || r == '\t') && len(blockStack) == 0 && !quoted: + // Split on space/tab if not in a block or quoted + if currentToken.Len() != 0 { + tokens = append(tokens, currentToken.String()) + currentToken.Reset() + } + + case (r == '=' || r == '+') && len(blockStack) == 0 && !quoted && isVariable: + // Handle variable assignment + if currentToken.Len() != 0 { + tokens = append(tokens, currentToken.String()) + currentToken.Reset() + } + tokens = append(tokens, string(r)) + + case r == '"' && len(blockStack) == 0: + quoted = !quoted + currentToken.WriteRune(r) + + case slices.Contains(openBlocks, r): + blockStack = append(blockStack, r) + currentToken.WriteRune(r) + + case slices.Contains(closeBlocks, r): + if len(blockStack) > 0 { + blockStack = blockStack[:len(blockStack)-1] + } else { + panic(fmt.Sprintf("Unbalanced block, missing '{' or '}' on: %s\n", str)) + } + currentToken.WriteRune(r) + + default: + currentToken.WriteRune(r) + } + } + if currentToken.Len() != 0 { + tokens = append(tokens, currentToken.String()) + } + return tokens +} + +func tokenToSlice(token string) []string { + res := []string{} + token = strings.Trim(token, "()\n") + if strings.ContainsAny(token, ", ") { + var sep string + switch { + case strings.Contains(token, ","): + sep = "," + case strings.Contains(token, " "): + sep = " " + } + for _, v := range strings.Split(token, sep) { + res = append(res, strings.Trim(v, " ")) + } + } else { + res = append(res, token) + } + return res +} + +func tokensStripComment(tokens []string) []string { + res := []string{} + for _, v := range tokens { + if v == tokCOMMENT { + break + } + res = append(res, v) + } + return res +} + +// Convert a slice of internal rules to a slice of ApparmorRule. +func newRules(rules [][]string) (Rules, error) { + var err error + var r Rule + res := make(Rules, 0, len(rules)) + + for _, rule := range rules { + if len(rule) == 0 { + return nil, fmt.Errorf("Empty rule") + } + + if newRule, ok := newRuleMap[rule[0]]; ok { + r, err = newRule(rule) + if err != nil { + return nil, err + } + res = append(res, r) + } else if strings.HasPrefix(rule[0], tokVARIABLE) { + r, err = newVariable(rule) + if err != nil { + return nil, err + } + res = append(res, r) + } else { + return nil, fmt.Errorf("Unrecognized rule: %s", rule) + } + } + return res, nil +} + +func (f *AppArmorProfileFile) parsePreamble(input []string) error { + var err error + var r Rule + var rules Rules + + tokenizedRules := [][]string{} + for _, line := range input { + if strings.HasPrefix(line, tokCOMMENT) { + r, err = newComment(strings.Split(line, " ")) + if err != nil { + return err + } + rules = append(rules, r) + } else { + tokens := tokenize(line) + tokenizedRules = append(tokenizedRules, tokens) + } + } + + rr, err := newRules(tokenizedRules) + if err != nil { + return err + } + f.Preamble = append(f.Preamble, rules...) + f.Preamble = append(f.Preamble, rr...) + return nil +} + +// Parse an apparmor profile file. +// +// Only supports parsing of apparmor file preamble and profile headers. +// +// Warning: It is purposelly an uncomplete basic parser for apparmor profile, +// it is only aimed for internal tooling purpose. For "simplicity", it is not +// using antlr / participle. It is only used for experimental feature in the +// apparmor.d project. +// +// Stop at the first profile header. Does not support multiline coma rules. +// +// Current use case: +// +// - Parse include and tunables +// - Parse variable in profile preamble and in tunable files +// - Parse (sub) profiles header to edit flags +func (f *AppArmorProfileFile) Parse(input string) error { + rawHeader := "" + rawPreamble := []string{} + +done: + for _, line := range strings.Split(input, "\n") { + tmp := strings.TrimLeft(line, "\t ") + tmp = strings.TrimRight(tmp, ",") + switch { + case tmp == "": + continue + case strings.HasPrefix(tmp, tokPROFILE): + rawHeader = tmp + break done + default: + rawPreamble = append(rawPreamble, tmp) + } + } + + if err := f.parsePreamble(rawPreamble); err != nil { + return err + } + if rawHeader != "" { + header, err := newHeader(tokenize(rawHeader)) + if err != nil { + return err + } + profile := &Profile{Header: header} + f.Profiles = append(f.Profiles, profile) + } + return nil +} diff --git a/pkg/aa/parse_test.go b/pkg/aa/parse_test.go new file mode 100644 index 00000000..c5f0f084 --- /dev/null +++ b/pkg/aa/parse_test.go @@ -0,0 +1,281 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "reflect" + "testing" + + "github.com/roddhjav/apparmor.d/pkg/util" +) + +func Test_tokenizeRule(t *testing.T) { + for _, tt := range testRules { + t.Run(tt.name, func(t *testing.T) { + if got := tokenize(tt.raw); !reflect.DeepEqual(got, tt.tokens) { + t.Errorf("tokenize() = %v, want %v", got, tt.tokens) + } + }) + } +} + +func Test_AppArmorProfileFile_Parse(t *testing.T) { + for _, tt := range testBlocks { + t.Run(tt.name, func(t *testing.T) { + got := &AppArmorProfileFile{} + if err := got.Parse(tt.raw); (err != nil) != tt.wParseErr { + t.Errorf("AppArmorProfileFile.Parse() error = %v, wantErr %v", err, tt.wParseErr) + } + if !reflect.DeepEqual(got, tt.apparmor) { + t.Errorf("AppArmorProfileFile.Parse() = |%v|, want |%v|", got, tt.apparmor) + } + }) + } +} + +var ( + // Test cases for tokenize + testRules = []struct { + name string + raw string + tokens []string + }{ + { + name: "empty", + raw: "", + tokens: []string{}, + }, + { + name: "abi", + raw: `abi `, + tokens: []string{"abi", ""}, + }, + { + name: "alias", + raw: `alias /mnt/usr -> /usr`, + tokens: []string{"alias", "/mnt/usr", "->", "/usr"}, + }, + { + name: "variable", + raw: `@{name} = torbrowser "tor browser"`, + tokens: []string{"@{name}", "=", "torbrowser", `"tor browser"`}, + }, + { + name: "variable-2", + raw: `@{exec_path} += @{bin}/@{name}`, + tokens: []string{"@{exec_path}", "+", "=", "@{bin}/@{name}"}, + }, + { + name: "variable-3", + raw: `@{empty}="dummy"`, + tokens: []string{"@{empty}", "=", `"dummy"`}, + }, + { + name: "variable-4", + raw: `@{XDG_PROJECTS_DIR}+="Git"`, + tokens: []string{"@{XDG_PROJECTS_DIR}", "+", "=", `"Git"`}, + }, + { + name: "header", + raw: `profile foo @{exec_path} xattrs=(security.tagged=allowed) flags=(complain attach_disconnected)`, + tokens: []string{"profile", "foo", "@{exec_path}", "xattrs=(security.tagged=allowed)", "flags=(complain attach_disconnected)"}, + }, + { + name: "include", + raw: `include `, + tokens: []string{"include", ""}, + }, + { + name: "include-if-exists", + raw: `include if exists "/etc/apparmor.d/dummy"`, + tokens: []string{"include", "if", "exists", `"/etc/apparmor.d/dummy"`}, + }, + { + name: "rlimit", + raw: `set rlimit nproc <= 200`, + tokens: []string{"set", "rlimit", "nproc", "<=", "200"}, + }, + { + name: "userns", + raw: `userns`, + tokens: []string{"userns"}, + }, + { + name: "capability", + raw: `capability dac_read_search`, + tokens: []string{"capability", "dac_read_search"}, + }, + { + name: "network", + raw: `network netlink raw`, + tokens: []string{"network", "netlink", "raw"}, + }, + { + name: "mount", + raw: `mount /{,**}`, + tokens: []string{"mount", "/{,**}"}, + }, + { + name: "mount-2", + raw: `mount options=(rw rbind) /tmp/newroot/ -> /tmp/newroot/`, + tokens: []string{"mount", "options=(rw rbind)", "/tmp/newroot/", "->", "/tmp/newroot/"}, + }, + { + name: "mount-3", + raw: `mount options=(rw silent rprivate) -> /oldroot/`, + tokens: []string{"mount", "options=(rw silent rprivate)", "->", "/oldroot/"}, + }, + { + name: "mount-4", + raw: `mount fstype=devpts options=(rw nosuid noexec) devpts -> /newroot/dev/pts/`, + tokens: []string{"mount", "fstype=devpts", "options=(rw nosuid noexec)", "devpts", "->", "/newroot/dev/pts/"}, + }, + { + name: "signal", + raw: `signal (receive) set=(cont, term,winch) peer=at-spi-bus-launcher`, + tokens: []string{"signal", "(receive)", "set=(cont, term,winch)", "peer=at-spi-bus-launcher"}, + }, + { + name: "unix", + raw: `unix (send receive) type=stream addr="@/tmp/.ICE[0-9]*-unix/19 5" peer=(label="@{p_systemd}", addr=none)`, + tokens: []string{"unix", "(send receive)", "type=stream", "addr=\"@/tmp/.ICE[0-9]*-unix/19 5\"", "peer=(label=\"@{p_systemd}\", addr=none)"}, + }, + { + name: "unix-2", + raw: ` unix (connect, receive, send) + type=stream + peer=(addr="@/tmp/ibus/dbus-????????")`, + tokens: []string{"unix", "(connect, receive, send)\n", "type=stream\n", `peer=(addr="@/tmp/ibus/dbus-????????")`}, + }, + { + name: "dbus", + raw: `dbus receive bus=system path=/org/freedesktop/DBus interface=org.freedesktop.DBus member=AddMatch peer=(name=:1.3, label=power-profiles-daemon)`, + tokens: []string{ + "dbus", "receive", "bus=system", + "path=/org/freedesktop/DBus", "interface=org.freedesktop.DBus", + "member=AddMatch", "peer=(name=:1.3, label=power-profiles-daemon)", + }, + }, + { + name: "file-1", + raw: `owner @{user_config_dirs}/powerdevilrc{,.@{rand6}} rwl -> @{user_config_dirs}/#@{int}`, + tokens: []string{"owner", "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", "rwl", "->", "@{user_config_dirs}/#@{int}"}, + }, + { + name: "file-2", + raw: `@{sys}/devices/@{pci}/class r`, + tokens: []string{"@{sys}/devices/@{pci}/class", "r"}, + }, + { + name: "file-3", + raw: `owner @{PROC}/@{pid}/task/@{tid}/comm rw`, + tokens: []string{"owner", "@{PROC}/@{pid}/task/@{tid}/comm", "rw"}, + }, + { + name: "file-4", + raw: `owner /{var/,}tmp/#@{int} rw`, + tokens: []string{"owner", "/{var/,}tmp/#@{int}", "rw"}, + }, + } + + // Test cases for Parse + testBlocks = []struct { + name string + raw string + apparmor *AppArmorProfileFile + wParseErr bool + }{ + { + name: "empty", + raw: "", + apparmor: &AppArmorProfileFile{}, + wParseErr: false, + }, + { + name: "comment", + raw: ` + # IsLineRule comment + include # comment included + @{lib_dirs} = @{lib}/@{name} /opt/@{name} # comment in variable`, + apparmor: &AppArmorProfileFile{ + Preamble: Rules{ + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " IsLineRule comment"}}, + &Include{ + RuleBase: RuleBase{Comment: " comment included"}, + Path: "tunables/global", IsMagic: true, + }, + &Variable{ + RuleBase: RuleBase{Comment: " comment in variable"}, + Name: "lib_dirs", Define: true, + Values: []string{"@{lib}/@{name}", "/opt/@{name}"}, + }, + }, + }, + wParseErr: false, + }, + { + name: "cornercases", + raw: `# Simple test + include + + # { commented block } + @{name} = {D,d}ummy + @{exec_path} = @{bin}/@{name} + alias /mnt/{,usr.sbin.}mount.cifs -> /sbin/mount.cifs, + @{coreutils} += gawk {,e,f}grep head + profile @{exec_path} { + `, + apparmor: &AppArmorProfileFile{ + Preamble: Rules{ + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " Simple test"}}, + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " { commented block }"}}, + &Include{IsMagic: true, Path: "tunables/global"}, + &Variable{Name: "name", Values: []string{"{D,d}ummy"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"@{bin}/@{name}"}, Define: true}, + &Alias{Path: "/mnt/{,usr.sbin.}mount.cifs", RewrittenPath: "/sbin/mount.cifs"}, + &Variable{Name: "coreutils", Values: []string{"gawk", "{,e,f}grep", "head"}, Define: false}, + }, + Profiles: []*Profile{ + { + Header: Header{ + Name: "@{exec_path}", + Attachments: []string{}, + Attributes: map[string]string{}, + Flags: []string{}, + }, + }, + }, + }, + wParseErr: false, + }, + { + name: "string.aa", + raw: util.MustReadFile(testData.Join("string.aa")), + apparmor: &AppArmorProfileFile{ + Preamble: Rules{ + &Comment{RuleBase: RuleBase{Comment: " Simple test profile for the AppArmorProfileFile.String() method", IsLineRule: true}}, + &Abi{IsMagic: true, Path: "abi/4.0"}, + &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"}, + &Include{IsMagic: true, Path: "tunables/global"}, + &Variable{ + Name: "exec_path", Define: true, + Values: []string{"@{bin}/foo", "@{lib}/foo"}, + }, + }, + Profiles: []*Profile{ + { + Header: Header{ + Name: "foo", + Attachments: []string{"@{exec_path}"}, + Attributes: map[string]string{"security.tagged": "allowed"}, + Flags: []string{"complain", "attach_disconnected"}, + }, + }, + }, + }, + wParseErr: false, + }, + } +) diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index 8d28612b..a2b68099 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -5,7 +5,9 @@ package aa import ( + "fmt" "slices" + "strings" ) const ( @@ -21,6 +23,12 @@ type Comment struct { RuleBase } +func newComment(rule []string) (Rule, error) { + base := newRule(rule) + base.IsLineRule = true + return &Comment{RuleBase: base}, nil +} + func (r *Comment) Validate() error { return nil } @@ -55,6 +63,31 @@ type Abi struct { IsMagic bool } +func newAbi(rule []string) (Rule, error) { + var magic bool + if len(rule) > 0 && rule[0] == tokABI { + rule = rule[1:] + } + if len(rule) != 1 { + return nil, fmt.Errorf("invalid abi format: %s", rule) + } + + path := rule[0] + switch { + case path[0] == '"': + magic = false + case path[0] == '<': + magic = true + default: + return nil, fmt.Errorf("invalid path %s in rule: %s", path, rule) + } + return &Abi{ + RuleBase: newRule(rule), + Path: strings.Trim(path, "\"<>"), + IsMagic: magic, + }, nil +} + func (r *Abi) Validate() error { return nil } @@ -90,6 +123,23 @@ type Alias struct { RewrittenPath string } +func newAlias(rule []string) (Rule, error) { + if len(rule) > 0 && rule[0] == tokALIAS { + rule = rule[1:] + } + if len(rule) != 3 { + return nil, fmt.Errorf("invalid alias format: %s", rule) + } + if rule[1] != tokARROW { + return nil, fmt.Errorf("invalid alias format, missing %s in: %s", tokARROW, rule) + } + return &Alias{ + RuleBase: newRule(rule), + Path: rule[0], + RewrittenPath: rule[2], + }, nil +} + func (r *Alias) Validate() error { return nil } @@ -126,6 +176,41 @@ type Include struct { IsMagic bool } +func newInclude(rule []string) (Rule, error) { + var magic bool + var ifexists bool + + if len(rule) > 0 && rule[0] == tokINCLUDE { + rule = rule[1:] + } + + size := len(rule) + if size == 0 { + return nil, fmt.Errorf("invalid include format: %v", rule) + } + + if size >= 3 && strings.Join(rule[:2], " ") == tokIFEXISTS { + ifexists = true + rule = rule[2:] + } + + path := rule[0] + switch { + case path[0] == '"': + magic = false + case path[0] == '<': + magic = true + default: + return nil, fmt.Errorf("invalid path format: %v", path) + } + return &Include{ + RuleBase: newRule(rule), + IfExists: ifexists, + Path: strings.Trim(path, "\"<>"), + IsMagic: magic, + }, nil +} + func (r *Include) Validate() error { return nil } @@ -165,6 +250,35 @@ type Variable struct { Define bool } +func newVariable(rule []string) (Rule, error) { + var define bool + var values []string + if len(rule) < 3 { + return nil, fmt.Errorf("invalid variable format: %v", rule) + } + + name := strings.Trim(rule[0], tokVARIABLE+"}") + switch rule[1] { + case tokEQUAL: + define = true + values = tokensStripComment(rule[2:]) + case tokPLUS: + if rule[2] != tokEQUAL { + return nil, fmt.Errorf("invalid operator in variable: %v", rule) + } + define = false + values = tokensStripComment(rule[3:]) + default: + return nil, fmt.Errorf("invalid operator in variable: %v", rule) + } + return &Variable{ + RuleBase: newRule(rule), + Name: name, + Values: values, + Define: define, + }, nil +} + func (r *Variable) Validate() error { return nil } diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index ec646ea2..8936bbef 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -45,6 +45,51 @@ type Header struct { Flags []string } +func newHeader(rule []string) (Header, error) { + if len(rule) == 0 { + return Header{}, nil + } + if rule[len(rule)-1] == "{" { + rule = rule[:len(rule)-1] + } + if rule[0] == tokPROFILE { + rule = rule[1:] + } + + delete := []int{} + flags := []string{} + attributes := make(map[string]string) + for idx, token := range rule { + if item, ok := strings.CutPrefix(token, tokFLAGS+"="); ok { + flags = tokenToSlice(item) + delete = append(delete, idx) + } else if item, ok := strings.CutPrefix(token, tokATTRIBUTES+"="); ok { + for _, m := range tokenToSlice(item) { + kv := strings.SplitN(m, "=", 2) + attributes[kv[0]] = kv[1] + } + delete = append(delete, idx) + } + } + for i := len(delete) - 1; i >= 0; i-- { + rule = slices.Delete(rule, delete[i], delete[i]+1) + } + + name, attachments := "", []string{} + if len(rule) >= 1 { + name = rule[0] + if len(rule) > 1 { + attachments = rule[1:] + } + } + return Header{ + Name: name, + Attachments: attachments, + Attributes: attributes, + Flags: flags, + }, nil +} + func (r *Profile) Validate() error { if err := validateValues(r.Kind(), tokFLAGS, r.Flags); err != nil { return fmt.Errorf("profile %s: %w", r.Name, err) diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 6eec8167..ec28779f 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -153,19 +153,12 @@ func validateValues(rule string, key string, values []string) error { // 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) + res := tokenToSlice(input) for idx := range res { res[idx] = strings.Trim(res[idx], `" `) if !slices.Contains(req, res[idx]) { From 0aa317d8e42907bd797ade08e2d80af5cd01f3c8 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Mon, 27 May 2024 23:44:03 +0100 Subject: [PATCH 44/62] feat(aa): update default tunable selection. --- pkg/aa/apparmor.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index 13f0b6b1..4858a14f 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -33,13 +33,18 @@ func DefaultTunables() *AppArmorProfileFile { return &AppArmorProfileFile{ Preamble: Rules{ &Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}, Define: true}, - &Variable{Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}, Define: true}, - &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, - &Variable{Name: "HOME", Values: []string{"/home/*"}, Define: true}, - &Variable{Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}, Define: true}, &Variable{Name: "etc_ro", Values: []string{"/{,usr/}etc/"}, Define: true}, + &Variable{Name: "HOME", Values: []string{"/home/*"}, Define: true}, &Variable{Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}, Define: true}, + &Variable{Name: "int2", Values: []string{"[0-9][0-9]"}, Define: true}, + &Variable{Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}, Define: true}, + &Variable{Name: "MOUNTS", Values: []string{"/media/*/", "/run/media/*/*/", "/mnt/*/"}, Define: true}, + &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, + &Variable{Name: "run", Values: []string{"/run/", "/var/run/"}, Define: true}, + &Variable{Name: "uid", Values: []string{"{[0-9],[1-9][0-9],[1-9][0-9][0-9],[1-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9],[1-4][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]}"}, Define: true}, &Variable{Name: "user_cache_dirs", Values: []string{"/home/*/.cache"}, Define: true}, + &Variable{Name: "user_config_dirs", Values: []string{"/home/*/.config"}, Define: true}, + &Variable{Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}, Define: true}, }, } } From 04a91bbd9bf08ad9c843a253ea0e61748ca62ff2 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Mon, 27 May 2024 23:44:23 +0100 Subject: [PATCH 45/62] feat(aa): updaqte mount flags order. --- pkg/aa/mount.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 7d2e0388..03b758cc 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -18,12 +18,13 @@ const ( func init() { requirements[tokMOUNT] = requirement{ "flags": { - "acl", "async", "atime", "bind", "dev", "diratime", "dirsync", "exec", - "iversion", "loud", "mand", "move", "noacl", "noatime", "nodev", - "nodiratime", "noexec", "noiversion", "nomand", "norelatime", "nosuid", - "nouser", "private", "rbind", "relatime", "remount", "ro", "rprivate", - "rshared", "rslave", "runbindable", "rw", "shared", "silent", "slave", - "strictatime", "suid", "sync", "unbindable", "user", "verbose", + "acl", "async", "atime", "ro", "rw", "bind", "rbind", "dev", + "diratime", "dirsync", "exec", "iversion", "loud", "mand", "move", + "noacl", "noatime", "nodev", "nodiratime", "noexec", "noiversion", + "nomand", "norelatime", "nosuid", "nouser", "private", "relatime", + "remount", "rprivate", "rshared", "rslave", "runbindable", "shared", + "silent", "slave", "strictatime", "suid", "sync", "unbindable", + "user", "verbose", }, } } From dc0e0084a05c769d88aff587beaa1c67d0852cc3 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 11:53:32 +0100 Subject: [PATCH 46/62] feat(aa): add function to resolve include preamble. --- pkg/aa/resolve.go | 138 ++++++++++++++++++++++---- pkg/aa/resolve_test.go | 80 ++++++++++++--- pkg/aa/rules.go | 2 +- tests/testdata/tunables/dir.d/aliases | 2 + tests/testdata/tunables/dir.d/vars | 2 + tests/testdata/tunables/global | 3 + 6 files changed, 191 insertions(+), 36 deletions(-) create mode 100644 tests/testdata/tunables/dir.d/aliases create mode 100644 tests/testdata/tunables/dir.d/vars create mode 100644 tests/testdata/tunables/global diff --git a/pkg/aa/resolve.go b/pkg/aa/resolve.go index 308c22f1..85426340 100644 --- a/pkg/aa/resolve.go +++ b/pkg/aa/resolve.go @@ -8,21 +8,34 @@ import ( "fmt" "regexp" "strings" + + "github.com/arduino/go-paths-helper" + "github.com/roddhjav/apparmor.d/pkg/util" ) var ( + includeCache map[*Include]*AppArmorProfileFile = make(map[*Include]*AppArmorProfileFile) + regVariableReference = regexp.MustCompile(`@{([^{}]+)}`) ) -// Resolve resolves all variables and includes in the profile and merge the rules in the profile +// Resolve resolves variables and includes definied in the profile preamble func (f *AppArmorProfileFile) Resolve() error { + // Resolve preamble includes + for _, include := range f.Preamble.GetIncludes() { + err := f.resolveInclude(include) + if err != nil { + return err + } + } + // Resolve variables for _, variable := range f.Preamble.GetVariables() { newValues := []string{} for _, value := range variable.Values { - vars := f.resolveVariable(value) - if len(vars) == 0 { - return fmt.Errorf("Variable not defined in: %s", value) + vars, err := f.resolveValues(value) + if err != nil { + return err } newValues = append(newValues, vars...) } @@ -33,9 +46,9 @@ func (f *AppArmorProfileFile) Resolve() error { for _, profile := range f.Profiles { attachments := []string{} for _, att := range profile.Attachments { - vars := f.resolveVariable(att) - if len(vars) == 0 { - return fmt.Errorf("Variable not defined in: %s", att) + vars, err := f.resolveValues(att) + if err != nil { + return err } attachments = append(attachments, vars...) } @@ -45,27 +58,108 @@ func (f *AppArmorProfileFile) Resolve() error { return nil } -func (f *AppArmorProfileFile) resolveVariable(input string) []string { +func (f *AppArmorProfileFile) resolveValues(input string) ([]string, error) { if !strings.Contains(input, tokVARIABLE) { - return []string{input} + return []string{input}, nil } - vars := []string{} + values := []string{} match := regVariableReference.FindStringSubmatch(input) - if len(match) > 1 { - variable := match[0] - varname := match[1] - for _, vrbl := range f.Preamble.GetVariables() { - if vrbl.Name == varname { - for _, v := range vrbl.Values { - newVar := strings.ReplaceAll(input, variable, v) - res := f.resolveVariable(newVar) - vars = append(vars, res...) + if len(match) == 0 { + return nil, fmt.Errorf("Invalid variable reference: %s", input) + } + + variable := match[0] + varname := match[1] + found := false + for _, vrbl := range f.Preamble.GetVariables() { + if vrbl.Name == varname { + found = true + for _, v := range vrbl.Values { + if strings.Contains(v, tokVARIABLE+varname+"}") { + return nil, fmt.Errorf("recursive variable found in: %s", varname) } + newValues := strings.ReplaceAll(input, variable, v) + newValues = strings.ReplaceAll(newValues, "//", "/") + res, err := f.resolveValues(newValues) + if err != nil { + return nil, err + } + values = append(values, res...) } } - } else { - vars = append(vars, input) } - return vars + + if !found { + return nil, fmt.Errorf("Variable %s not defined", varname) + } + return values, nil +} + +// resolveInclude resolves all includes defined in the profile preamble +func (f *AppArmorProfileFile) resolveInclude(include *Include) error { + if include == nil || include.Path == "" { + return fmt.Errorf("Invalid include: %v", include) + } + + _, isCached := includeCache[include] + if !isCached { + var files paths.PathList + var err error + + path := MagicRoot.Join(include.Path) + if !include.IsMagic { + path = paths.New(include.Path) + } + + if path.IsDir() { + files, err = path.ReadDir(paths.FilterOutDirectories()) + if err != nil { + if include.IfExists { + return nil + } + return fmt.Errorf("File %s not found: %v", path, err) + } + + } else if path.Exist() { + files = append(files, path) + + } else { + if include.IfExists { + return nil + } + return fmt.Errorf("File %s not found", path) + + } + + iFile := &AppArmorProfileFile{} + for _, file := range files { + raw, err := util.ReadFile(file) + if err != nil { + return err + } + if err := iFile.Parse(raw); err != nil { + return err + } + } + if err := iFile.Validate(); err != nil { + return err + } + for _, inc := range iFile.Preamble.GetIncludes() { + if err := iFile.resolveInclude(inc); err != nil { + return err + } + } + + // Remove all includes in iFile + iFile.Preamble = iFile.Preamble.DeleteKind(tokINCLUDE) + + // Cache the included file + includeCache[include] = iFile + } + + // Insert iFile in the place of include in the current file + index := f.Preamble.IndexOf(include) + f.Preamble = f.Preamble.Insert(index, includeCache[include].Preamble...) + return nil } diff --git a/pkg/aa/resolve_test.go b/pkg/aa/resolve_test.go index 3c6f20c6..8e4ff6b8 100644 --- a/pkg/aa/resolve_test.go +++ b/pkg/aa/resolve_test.go @@ -7,24 +7,75 @@ package aa import ( "reflect" "testing" + + "github.com/arduino/go-paths-helper" ) -func TestAppArmorProfileFile_resolveVariable(t *testing.T) { +func TestAppArmorProfileFile_resolveInclude(t *testing.T) { tests := []struct { - name string - f AppArmorProfileFile - input string - want []string + name string + include *Include + want *AppArmorProfileFile + wantErr bool }{ { - name: "nil", - input: "@{newvar}", - want: []string{}, + name: "empty", + include: &Include{Path: "", IsMagic: true}, + want: &AppArmorProfileFile{Preamble: Rules{&Include{Path: "", IsMagic: true}}}, + wantErr: true, }, { - name: "empty", - input: "@{}", - want: []string{"@{}"}, + name: "tunables", + include: &Include{Path: "tunables/global", IsMagic: true}, + want: &AppArmorProfileFile{ + Preamble: Rules{ + &Alias{Path: "/usr/", RewrittenPath: "/User/"}, + &Alias{Path: "/lib/", RewrittenPath: "/Libraries/"}, + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " variable declarations for inclusion"}}, + &Variable{ + Name: "FOO", Define: true, + Values: []string{ + "/foo", "/bar", "/baz", "/biff", "/lib", "/tmp", + }, + }, + }, + }, + wantErr: false, + }, + } + MagicRoot = paths.New("../../tests/testdata") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &AppArmorProfileFile{} + got.Preamble = append(got.Preamble, tt.include) + if err := got.resolveInclude(tt.include); (err != nil) != tt.wantErr { + t.Errorf("AppArmorProfileFile.resolveInclude() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AppArmorProfileFile.resolveValues() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAppArmorProfileFile_resolveValues(t *testing.T) { + tests := []struct { + name string + input string + want []string + wantErr bool + }{ + { + name: "not-defined", + input: "@{newvar}", + want: nil, + wantErr: true, + }, + { + name: "no-name", + input: "@{}", + want: nil, + wantErr: true, }, { name: "default", @@ -45,9 +96,12 @@ func TestAppArmorProfileFile_resolveVariable(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := DefaultTunables() - got := f.resolveVariable(tt.input) + got, err := f.resolveValues(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("AppArmorProfileFile.resolveValues() error = %v, wantErr %v", err, tt.wantErr) + } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("AppArmorProfileFile.resolveVariable() = %v, want %v", got, tt.want) + t.Errorf("AppArmorProfileFile.resolveValues() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index ec28779f..895d5f28 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -80,7 +80,7 @@ func (r Rules) Remove(rule Rule) Rules { } func (r Rules) Insert(idx int, rules ...Rule) Rules { - return append(r[:idx], append(rules, r[idx:]...)...) + return append(r[:idx], append(rules, r[idx+1:]...)...) } func (r Rules) Sort() Rules { diff --git a/tests/testdata/tunables/dir.d/aliases b/tests/testdata/tunables/dir.d/aliases new file mode 100644 index 00000000..61db9635 --- /dev/null +++ b/tests/testdata/tunables/dir.d/aliases @@ -0,0 +1,2 @@ +alias /usr/ -> /User/, +alias /lib/ -> /Libraries/, diff --git a/tests/testdata/tunables/dir.d/vars b/tests/testdata/tunables/dir.d/vars new file mode 100644 index 00000000..505ca6d3 --- /dev/null +++ b/tests/testdata/tunables/dir.d/vars @@ -0,0 +1,2 @@ +# variable declarations for inclusion +@{FOO} = /foo /bar /baz /biff /lib /tmp diff --git a/tests/testdata/tunables/global b/tests/testdata/tunables/global new file mode 100644 index 00000000..145ba3b7 --- /dev/null +++ b/tests/testdata/tunables/global @@ -0,0 +1,3 @@ + +include + From 1333ec202502f93bfe67c1416f0975c4301cf3ca Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 18:07:32 +0100 Subject: [PATCH 47/62] feat(aa): cleanup rules methods. --- pkg/aa/resolve.go | 4 ++-- pkg/aa/rules.go | 34 ++++++++++++---------------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/pkg/aa/resolve.go b/pkg/aa/resolve.go index 85426340..9a246746 100644 --- a/pkg/aa/resolve.go +++ b/pkg/aa/resolve.go @@ -159,7 +159,7 @@ func (f *AppArmorProfileFile) resolveInclude(include *Include) error { } // Insert iFile in the place of include in the current file - index := f.Preamble.IndexOf(include) - f.Preamble = f.Preamble.Insert(index, includeCache[include].Preamble...) + index := f.Preamble.Index(include) + f.Preamble = f.Preamble.Replace(index, includeCache[include].Preamble...) return nil } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 895d5f28..4bd1b61e 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -51,7 +51,8 @@ func (r Rules) String() string { return renderTemplate("rules", r) } -func (r Rules) IndexOf(rule Rule) int { +// Index returns the index of the first occurrence of rule rin r, or -1 if not present. +func (r Rules) Index(rule Rule) int { for idx, rr := range r { if rr.Kind() == rule.Kind() && rr.Equals(rule) { return idx @@ -60,31 +61,20 @@ func (r Rules) IndexOf(rule Rule) int { return -1 } -func (r Rules) Contains(rule Rule) bool { - return r.IndexOf(rule) != -1 +// Replace replaces the elements r[i] by the given rules, and returns the +// modified slice. +func (r Rules) Replace(i int, rules ...Rule) Rules { + return append(r[:i], append(rules, r[i+1:]...)...) } -func (r Rules) Add(rule Rule) Rules { - if r.Contains(rule) { - return r - } - return append(r, rule) +// Insert inserts the rules into r at index i, returning the modified slice. +func (r Rules) Insert(i int, rules ...Rule) Rules { + return append(r[:i], append(rules, r[i:]...)...) } -func (r Rules) Remove(rule Rule) Rules { - idx := r.IndexOf(rule) - if idx == -1 { - return r - } - return append(r[:idx], r[idx+1:]...) -} - -func (r Rules) Insert(idx int, rules ...Rule) Rules { - return append(r[:idx], append(rules, r[idx+1:]...)...) -} - -func (r Rules) Sort() Rules { - return r +// Delete removes the elements r[i] from r, returning the modified slice. +func (r Rules) Delete(i int) Rules { + return append(r[:i], r[i+1:]...) } func (r Rules) DeleteKind(kind string) Rules { From 3b0944c615f785327a24b4e382515c6f36e27558 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 18:15:22 +0100 Subject: [PATCH 48/62] feat(aa): add the Kind struct to manage aa rules. --- pkg/aa/base.go | 8 ++-- pkg/aa/blocks.go | 9 ++-- pkg/aa/capability.go | 10 ++-- pkg/aa/change_profile.go | 8 ++-- pkg/aa/dbus.go | 8 ++-- pkg/aa/file.go | 32 ++++++------- pkg/aa/io_uring.go | 10 ++-- pkg/aa/mount.go | 24 +++++----- pkg/aa/mqueue.go | 10 ++-- pkg/aa/network.go | 8 ++-- pkg/aa/parse.go | 22 +++++---- pkg/aa/pivot_root.go | 6 +-- pkg/aa/preamble.go | 43 +++++++++--------- pkg/aa/profile.go | 11 +++-- pkg/aa/ptrace.go | 10 ++-- pkg/aa/resolve.go | 6 +-- pkg/aa/rlimit.go | 9 ++-- pkg/aa/rules.go | 48 +++++++++++++------- pkg/aa/signal.go | 10 ++-- pkg/aa/template.go | 96 +++++++++++++++++++-------------------- pkg/aa/templates/rules.j2 | 58 +++++++++++------------ pkg/aa/unix.go | 10 ++-- pkg/aa/userns.go | 6 +-- 23 files changed, 239 insertions(+), 223 deletions(-) diff --git a/pkg/aa/base.go b/pkg/aa/base.go index 13a67527..8fdae72c 100644 --- a/pkg/aa/base.go +++ b/pkg/aa/base.go @@ -24,7 +24,7 @@ func newRule(rule []string) RuleBase { idx := 0 for idx < len(rule) { - if rule[idx] == tokCOMMENT { + if rule[idx] == COMMENT.Tok() { comment = " " + strings.Join(rule[idx+1:], " ") break } @@ -85,15 +85,15 @@ func (r RuleBase) Equals(other any) bool { } func (r RuleBase) String() string { - return renderTemplate("comment", r) + return renderTemplate(r.Kind(), r) } func (r RuleBase) Constraint() constraint { return anyKind } -func (r RuleBase) Kind() string { - return "base" +func (r RuleBase) Kind() Kind { + return COMMENT } type Qualifier struct { diff --git a/pkg/aa/blocks.go b/pkg/aa/blocks.go index 8b4f3ab5..6d1079ac 100644 --- a/pkg/aa/blocks.go +++ b/pkg/aa/blocks.go @@ -5,8 +5,7 @@ package aa const ( - tokHAT = "hat" - tokCARET = "^" + HAT Kind = "hat" ) // Hat represents a single AppArmor hat. @@ -26,7 +25,7 @@ func (p *Hat) Less(other any) bool { } func (p *Hat) Equals(other any) bool { - o, _ := other.(*Profile) + o, _ := other.(*Hat) return p.Name == o.Name } @@ -38,6 +37,6 @@ func (p *Hat) Constraint() constraint { return blockKind } -func (p *Hat) Kind() string { - return tokHAT +func (p *Hat) Kind() Kind { + return HAT } diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 33a0a6a8..44508e0e 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -9,10 +9,10 @@ import ( "slices" ) -const tokCAPABILITY = "capability" +const CAPABILITY Kind = "capability" func init() { - requirements[tokCAPABILITY] = requirement{ + requirements[CAPABILITY] = requirement{ "name": { "audit_control", "audit_read", "audit_write", "block_suspend", "bpf", "checkpoint_restore", "chown", "dac_override", "dac_read_search", @@ -36,7 +36,7 @@ func newCapabilityFromLog(log map[string]string) Rule { return &Capability{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Names: Must(toValues(tokCAPABILITY, "name", log["capname"])), + Names: Must(toValues(CAPABILITY, "name", log["capname"])), } } @@ -70,6 +70,6 @@ func (r *Capability) Constraint() constraint { return blockKind } -func (r *Capability) Kind() string { - return tokCAPABILITY +func (r *Capability) Kind() Kind { + return CAPABILITY } diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index be08692f..d5cc618c 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -6,10 +6,10 @@ package aa import "fmt" -const tokCHANGEPROFILE = "change_profile" +const CHANGEPROFILE Kind = "change_profile" func init() { - requirements[tokCHANGEPROFILE] = requirement{ + requirements[CHANGEPROFILE] = requirement{ "mode": []string{"safe", "unsafe"}, } } @@ -67,6 +67,6 @@ func (r *ChangeProfile) Constraint() constraint { return blockKind } -func (r *ChangeProfile) Kind() string { - return tokCHANGEPROFILE +func (r *ChangeProfile) Kind() Kind { + return CHANGEPROFILE } diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index ded2b3cb..56edd797 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -9,10 +9,10 @@ import ( "slices" ) -const tokDBUS = "dbus" +const DBUS Kind = "dbus" func init() { - requirements[tokDBUS] = requirement{ + requirements[DBUS] = requirement{ "access": []string{ "send", "receive", "bind", "eavesdrop", "r", "read", "w", "write", "rw", @@ -110,6 +110,6 @@ func (r *Dbus) Constraint() constraint { return blockKind } -func (r *Dbus) Kind() string { - return tokDBUS +func (r *Dbus) Kind() Kind { + return DBUS } diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 18e2c5cb..07ab71d8 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -11,14 +11,14 @@ import ( ) const ( - tokLINK = "link" - tokFILE = "file" - tokOWNER = "owner" - tokSUBSET = "subset" + LINK Kind = "link" + FILE Kind = "file" + tokOWNER = "owner" + tokSUBSET = "subset" ) func init() { - requirements[tokFILE] = requirement{ + requirements[FILE] = requirement{ "access": {"m", "r", "w", "l", "k"}, "transition": { "ix", "ux", "Ux", "px", "Px", "cx", "Cx", "pix", "Pix", "cix", @@ -40,15 +40,15 @@ func isOwner(log map[string]string) bool { // 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[FILE]["access"], i) && + slices.Contains(requirements[FILE]["access"], j) { + return requirementsWeights[FILE]["access"][i] - requirementsWeights[FILE]["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[FILE]["transition"], i) && + slices.Contains(requirements[FILE]["transition"], j) { + return requirementsWeights[FILE]["transition"][i] - requirementsWeights[FILE]["transition"][j] } - if slices.Contains(requirements[tokFILE]["access"], i) { + if slices.Contains(requirements[FILE]["access"], i) { return -1 } return 1 @@ -121,8 +121,8 @@ func (r *File) Constraint() constraint { return blockKind } -func (r *File) Kind() string { - return tokFILE +func (r *File) Kind() Kind { + return FILE } type Link struct { @@ -179,6 +179,6 @@ func (r *Link) Constraint() constraint { return blockKind } -func (r *Link) Kind() string { - return tokLINK +func (r *Link) Kind() Kind { + return LINK } diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index 13c1031b..42297a1f 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -9,10 +9,10 @@ import ( "slices" ) -const tokIOURING = "io_uring" +const IOURING Kind = "io_uring" func init() { - requirements[tokIOURING] = requirement{ + requirements[IOURING] = requirement{ "access": []string{"sqpoll", "override_creds"}, } } @@ -28,7 +28,7 @@ func newIOUringFromLog(log map[string]string) Rule { return &IOUring{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: Must(toAccess(tokIOURING, log["requested"])), + Access: Must(toAccess(IOURING, log["requested"])), Label: log["label"], } } @@ -64,6 +64,6 @@ func (r *IOUring) Constraint() constraint { return blockKind } -func (r *IOUring) Kind() string { - return tokIOURING +func (r *IOUring) Kind() Kind { + return IOURING } diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 03b758cc..e131e54c 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -10,13 +10,13 @@ import ( ) const ( - tokMOUNT = "mount" - tokREMOUNT = "remount" - tokUMOUNT = "umount" + MOUNT Kind = "mount" + REMOUNT Kind = "remount" + UMOUNT Kind = "umount" ) func init() { - requirements[tokMOUNT] = requirement{ + requirements[MOUNT] = requirement{ "flags": { "acl", "async", "atime", "ro", "rw", "bind", "rbind", "dev", "diratime", "dirsync", "exec", "iversion", "loud", "mand", "move", @@ -38,14 +38,14 @@ func newMountConditionsFromLog(log map[string]string) MountConditions { if _, present := log["flags"]; present { return MountConditions{ FsType: log["fstype"], - Options: Must(toValues(tokMOUNT, "flags", log["flags"])), + Options: Must(toValues(MOUNT, "flags", log["flags"])), } } return MountConditions{FsType: log["fstype"]} } func (m MountConditions) Validate() error { - return validateValues(tokMOUNT, "flags", m.Options) + return validateValues(MOUNT, "flags", m.Options) } func (m MountConditions) Less(other MountConditions) bool { @@ -113,8 +113,8 @@ func (r *Mount) Constraint() constraint { return blockKind } -func (r *Mount) Kind() string { - return tokMOUNT +func (r *Mount) Kind() Kind { + return MOUNT } type Umount struct { @@ -166,8 +166,8 @@ func (r *Umount) Constraint() constraint { return blockKind } -func (r *Umount) Kind() string { - return tokUMOUNT +func (r *Umount) Kind() Kind { + return UMOUNT } type Remount struct { @@ -219,6 +219,6 @@ func (r *Remount) Constraint() constraint { return blockKind } -func (r *Remount) Kind() string { - return tokREMOUNT +func (r *Remount) Kind() Kind { + return REMOUNT } diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 7afb1179..9fc5f260 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -10,10 +10,10 @@ import ( "strings" ) -const tokMQUEUE = "mqueue" +const MQUEUE Kind = "mqueue" func init() { - requirements[tokMQUEUE] = requirement{ + requirements[MQUEUE] = requirement{ "access": []string{ "r", "w", "rw", "read", "write", "create", "open", "delete", "getattr", "setattr", @@ -41,7 +41,7 @@ func newMqueueFromLog(log map[string]string) Rule { return &Mqueue{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: Must(toAccess(tokMQUEUE, log["requested"])), + Access: Must(toAccess(MQUEUE, log["requested"])), Type: mqueueType, Label: log["label"], Name: log["name"], @@ -86,6 +86,6 @@ func (r *Mqueue) Constraint() constraint { return blockKind } -func (r *Mqueue) Kind() string { - return tokMQUEUE +func (r *Mqueue) Kind() Kind { + return MQUEUE } diff --git a/pkg/aa/network.go b/pkg/aa/network.go index edc78100..8d01b0ba 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -8,10 +8,10 @@ import ( "fmt" ) -const tokNETWORK = "network" +const NETWORK Kind = "network" func init() { - requirements[tokNETWORK] = requirement{ + requirements[NETWORK] = requirement{ "access": []string{ "create", "bind", "listen", "accept", "connect", "shutdown", "getattr", "setattr", "getopt", "setopt", "send", "receive", @@ -126,6 +126,6 @@ func (r *Network) Constraint() constraint { return blockKind } -func (r *Network) Kind() string { - return tokNETWORK +func (r *Network) Kind() Kind { + return NETWORK } diff --git a/pkg/aa/parse.go b/pkg/aa/parse.go index b9d65eb9..7d949e15 100644 --- a/pkg/aa/parse.go +++ b/pkg/aa/parse.go @@ -26,12 +26,16 @@ const ( var ( newRuleMap = map[string]func([]string) (Rule, error){ - tokCOMMENT: newComment, - tokABI: newAbi, - tokALIAS: newAlias, - tokINCLUDE: newInclude, + COMMENT.Tok(): newComment, + ABI.Tok(): newAbi, + ALIAS.Tok(): newAlias, + INCLUDE.Tok(): newInclude, } + tok = map[Kind]string{ + COMMENT: "#", + VARIABLE: "@{", + } openBlocks = []rune{tokOPENPAREN, tokOPENBRACE, tokOPENBRACKET} closeBlocks = []rune{tokCLOSEPAREN, tokCLOSEBRACE, tokCLOSEBRACKET} ) @@ -53,7 +57,7 @@ func tokenize(str string) []string { blockStack := []rune{} tokens := make([]string, 0, len(str)/2) - if len(str) > 2 && str[0:2] == tokVARIABLE { + if len(str) > 2 && str[0:2] == VARIABLE.Tok() { isVariable = true } for _, r := range str { @@ -122,7 +126,7 @@ func tokenToSlice(token string) []string { func tokensStripComment(tokens []string) []string { res := []string{} for _, v := range tokens { - if v == tokCOMMENT { + if v == COMMENT.Tok() { break } res = append(res, v) @@ -147,7 +151,7 @@ func newRules(rules [][]string) (Rules, error) { return nil, err } res = append(res, r) - } else if strings.HasPrefix(rule[0], tokVARIABLE) { + } else if strings.HasPrefix(rule[0], VARIABLE.Tok()) { r, err = newVariable(rule) if err != nil { return nil, err @@ -167,7 +171,7 @@ func (f *AppArmorProfileFile) parsePreamble(input []string) error { tokenizedRules := [][]string{} for _, line := range input { - if strings.HasPrefix(line, tokCOMMENT) { + if strings.HasPrefix(line, COMMENT.Tok()) { r, err = newComment(strings.Split(line, " ")) if err != nil { return err @@ -215,7 +219,7 @@ done: switch { case tmp == "": continue - case strings.HasPrefix(tmp, tokPROFILE): + case strings.HasPrefix(tmp, PROFILE.Tok()): rawHeader = tmp break done default: diff --git a/pkg/aa/pivot_root.go b/pkg/aa/pivot_root.go index 7c3f5ab8..178b848d 100644 --- a/pkg/aa/pivot_root.go +++ b/pkg/aa/pivot_root.go @@ -4,7 +4,7 @@ package aa -const tokPIVOTROOT = "pivot_root" +const PIVOTROOT = "pivot_root" type PivotRoot struct { RuleBase @@ -57,6 +57,6 @@ func (r *PivotRoot) Constraint() constraint { return blockKind } -func (r *PivotRoot) Kind() string { - return tokPIVOTROOT +func (r *PivotRoot) Kind() Kind { + return PIVOTROOT } diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index a2b68099..d8cb5813 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -11,12 +11,13 @@ import ( ) const ( - tokABI = "abi" - tokALIAS = "alias" - tokINCLUDE = "include" + ABI Kind = "abi" + ALIAS Kind = "alias" + INCLUDE Kind = "include" + VARIABLE Kind = "variable" + COMMENT Kind = "comment" + tokIFEXISTS = "if exists" - tokVARIABLE = "@{" - tokCOMMENT = "#" ) type Comment struct { @@ -42,7 +43,7 @@ func (r *Comment) Equals(other any) bool { } func (r *Comment) String() string { - return renderTemplate("comment", r) + return renderTemplate(r.Kind(), r) } func (r *Comment) IsPreamble() bool { @@ -53,8 +54,8 @@ func (r *Comment) Constraint() constraint { return anyKind } -func (r *Comment) Kind() string { - return tokCOMMENT +func (r *Comment) Kind() Kind { + return COMMENT } type Abi struct { @@ -65,7 +66,7 @@ type Abi struct { func newAbi(rule []string) (Rule, error) { var magic bool - if len(rule) > 0 && rule[0] == tokABI { + if len(rule) > 0 && rule[0] == ABI.Tok() { rule = rule[1:] } if len(rule) != 1 { @@ -113,8 +114,8 @@ func (r *Abi) Constraint() constraint { return preambleKind } -func (r *Abi) Kind() string { - return tokABI +func (r *Abi) Kind() Kind { + return ABI } type Alias struct { @@ -124,7 +125,7 @@ type Alias struct { } func newAlias(rule []string) (Rule, error) { - if len(rule) > 0 && rule[0] == tokALIAS { + if len(rule) > 0 && rule[0] == ALIAS.Tok() { rule = rule[1:] } if len(rule) != 3 { @@ -165,8 +166,8 @@ func (r *Alias) Constraint() constraint { return preambleKind } -func (r *Alias) Kind() string { - return tokALIAS +func (r *Alias) Kind() Kind { + return ALIAS } type Include struct { @@ -180,7 +181,7 @@ func newInclude(rule []string) (Rule, error) { var magic bool var ifexists bool - if len(rule) > 0 && rule[0] == tokINCLUDE { + if len(rule) > 0 && rule[0] == INCLUDE.Tok() { rule = rule[1:] } @@ -239,8 +240,8 @@ func (r *Include) Constraint() constraint { return anyKind } -func (r *Include) Kind() string { - return tokINCLUDE +func (r *Include) Kind() Kind { + return INCLUDE } type Variable struct { @@ -257,7 +258,7 @@ func newVariable(rule []string) (Rule, error) { return nil, fmt.Errorf("invalid variable format: %v", rule) } - name := strings.Trim(rule[0], tokVARIABLE+"}") + name := strings.Trim(rule[0], VARIABLE.Tok()+"}") switch rule[1] { case tokEQUAL: define = true @@ -297,13 +298,13 @@ func (r *Variable) Equals(other any) bool { } func (r *Variable) String() string { - return renderTemplate("variable", r) + return renderTemplate(r.Kind(), r) } func (r *Variable) Constraint() constraint { return preambleKind } -func (r *Variable) Kind() string { - return tokVARIABLE +func (r *Variable) Kind() Kind { + return VARIABLE } diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 8936bbef..55488bfb 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -14,13 +14,14 @@ import ( ) const ( + PROFILE Kind = "profile" + tokATTRIBUTES = "xattrs" tokFLAGS = "flags" - tokPROFILE = "profile" ) func init() { - requirements[tokPROFILE] = requirement{ + requirements[PROFILE] = requirement{ tokFLAGS: { "enforce", "complain", "kill", "default_allow", "unconfined", "prompt", "audit", "mediate_deleted", "attach_disconnected", @@ -52,7 +53,7 @@ func newHeader(rule []string) (Header, error) { if rule[len(rule)-1] == "{" { rule = rule[:len(rule)-1] } - if rule[0] == tokPROFILE { + if rule[0] == PROFILE.Tok() { rule = rule[1:] } @@ -120,8 +121,8 @@ func (p *Profile) Constraint() constraint { return blockKind } -func (p *Profile) Kind() string { - return tokPROFILE +func (p *Profile) Kind() Kind { + return PROFILE } // Merge merge similar rules together. diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index d73060ed..00eca588 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -9,10 +9,10 @@ import ( "slices" ) -const tokPTRACE = "ptrace" +const PTRACE Kind = "ptrace" func init() { - requirements[tokPTRACE] = requirement{ + requirements[PTRACE] = requirement{ "access": []string{ "r", "w", "rw", "read", "readby", "trace", "tracedby", }, @@ -30,7 +30,7 @@ func newPtraceFromLog(log map[string]string) Rule { return &Ptrace{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: Must(toAccess(tokPTRACE, log["requested_mask"])), + Access: Must(toAccess(PTRACE, log["requested_mask"])), Peer: log["peer"], } } @@ -67,6 +67,6 @@ func (r *Ptrace) Constraint() constraint { return blockKind } -func (r *Ptrace) Kind() string { - return tokPTRACE +func (r *Ptrace) Kind() Kind { + return PTRACE } diff --git a/pkg/aa/resolve.go b/pkg/aa/resolve.go index 9a246746..df97c2aa 100644 --- a/pkg/aa/resolve.go +++ b/pkg/aa/resolve.go @@ -59,7 +59,7 @@ func (f *AppArmorProfileFile) Resolve() error { } func (f *AppArmorProfileFile) resolveValues(input string) ([]string, error) { - if !strings.Contains(input, tokVARIABLE) { + if !strings.Contains(input, VARIABLE.Tok()) { return []string{input}, nil } @@ -76,7 +76,7 @@ func (f *AppArmorProfileFile) resolveValues(input string) ([]string, error) { if vrbl.Name == varname { found = true for _, v := range vrbl.Values { - if strings.Contains(v, tokVARIABLE+varname+"}") { + if strings.Contains(v, VARIABLE.Tok()+varname+"}") { return nil, fmt.Errorf("recursive variable found in: %s", varname) } newValues := strings.ReplaceAll(input, variable, v) @@ -152,7 +152,7 @@ func (f *AppArmorProfileFile) resolveInclude(include *Include) error { } // Remove all includes in iFile - iFile.Preamble = iFile.Preamble.DeleteKind(tokINCLUDE) + iFile.Preamble = iFile.Preamble.DeleteKind(INCLUDE) // Cache the included file includeCache[include] = iFile diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index d3a29049..ddb70710 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -7,12 +7,11 @@ package aa import "fmt" const ( - tokRLIMIT = "rlimit" - tokSET = "set" + RLIMIT Kind = "rlimit" ) func init() { - requirements[tokRLIMIT] = requirement{ + requirements[RLIMIT] = requirement{ "keys": { "cpu", "fsize", "data", "stack", "core", "rss", "nofile", "ofile", "as", "nproc", "memlock", "locks", "sigpending", "msgqueue", "nice", @@ -68,6 +67,6 @@ func (r *Rlimit) Constraint() constraint { return blockKind } -func (r *Rlimit) Kind() string { - return tokRLIMIT +func (r *Rlimit) Kind() Kind { + return RLIMIT } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 4bd1b61e..79db32e0 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -26,6 +26,20 @@ const ( blockKind // The rule can only be found in a profile ) +// Kind represents an AppArmor rule kind. +type Kind string + +func (k Kind) String() string { + return string(k) +} + +func (k Kind) Tok() string { + if t, ok := tok[k]; ok { + return t + } + return string(k) +} + // Rule generic interface for all AppArmor rules type Rule interface { Validate() error @@ -33,7 +47,7 @@ type Rule interface { Equals(other any) bool String() string Constraint() constraint - Kind() string + Kind() Kind } type Rules []Rule @@ -77,7 +91,7 @@ func (r Rules) Delete(i int) Rules { return append(r[:i], r[i+1:]...) } -func (r Rules) DeleteKind(kind string) Rules { +func (r Rules) DeleteKind(kind Kind) Rules { res := make(Rules, 0) for _, rule := range r { if rule.Kind() != kind { @@ -87,7 +101,7 @@ func (r Rules) DeleteKind(kind string) Rules { return res } -func (r Rules) Filter(filter string) Rules { +func (r Rules) Filter(filter Kind) Rules { res := make(Rules, 0) for _, rule := range r { if rule.Kind() != filter { @@ -128,12 +142,12 @@ func Must[T any](v T, err error) T { return v } -func validateValues(rule string, key string, values []string) error { +func validateValues(kind Kind, key string, values []string) error { for _, v := range values { if v == "" { continue } - if !slices.Contains(requirements[rule][key], v) { + if !slices.Contains(requirements[kind][key], v) { return fmt.Errorf("invalid mode '%s'", v) } } @@ -142,10 +156,10 @@ func validateValues(rule string, key string, values []string) error { // 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) { - req, ok := requirements[rule][key] +func toValues(kind Kind, key string, input string) ([]string, error) { + req, ok := requirements[kind][key] if !ok { - return nil, fmt.Errorf("unrecognized requirement '%s' for rule %s", key, rule) + return nil, fmt.Errorf("unrecognized requirement '%s' for rule %s", key, kind) } res := tokenToSlice(input) @@ -156,22 +170,22 @@ func toValues(rule string, key string, input string) ([]string, error) { } } slices.SortFunc(res, func(i, j string) int { - return requirementsWeights[rule][key][i] - requirementsWeights[rule][key][j] + return requirementsWeights[kind][key][i] - requirementsWeights[kind][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) { +func toAccess(kind Kind, input string) ([]string, error) { var res []string - switch rule { - case tokFILE: + switch kind { + case FILE: raw := strings.Split(input, "") trans := []string{} for _, access := range raw { - if slices.Contains(requirements[tokFILE]["access"], access) { + if slices.Contains(requirements[FILE]["access"], access) { res = append(res, access) } else { trans = append(trans, access) @@ -180,17 +194,17 @@ func toAccess(rule string, input string) ([]string, error) { transition := strings.Join(trans, "") if len(transition) > 0 { - if slices.Contains(requirements[tokFILE]["transition"], transition) { + if slices.Contains(requirements[FILE]["transition"], transition) { res = append(res, transition) } else { return nil, fmt.Errorf("unrecognized transition: %s", transition) } } - case tokFILE + "-log": + case FILE + "-log": raw := strings.Split(input, "") for _, access := range raw { - if slices.Contains(requirements[tokFILE]["access"], access) { + if slices.Contains(requirements[FILE]["access"], access) { res = append(res, access) } else if maskToAccess[access] != "" { res = append(res, maskToAccess[access]) @@ -200,7 +214,7 @@ func toAccess(rule string, input string) ([]string, error) { } default: - return toValues(rule, "access", input) + return toValues(kind, "access", input) } slices.SortFunc(res, cmpFileAccess) diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index ace95d79..4e7ce91c 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -9,10 +9,10 @@ import ( "slices" ) -const tokSIGNAL = "signal" +const SIGNAL Kind = "signal" func init() { - requirements[tokSIGNAL] = requirement{ + requirements[SIGNAL] = requirement{ "access": { "r", "w", "rw", "read", "write", "send", "receive", }, @@ -44,7 +44,7 @@ func newSignalFromLog(log map[string]string) Rule { return &Signal{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: Must(toAccess(tokSIGNAL, log["requested_mask"])), + Access: Must(toAccess(SIGNAL, log["requested_mask"])), Set: []string{log["signal"]}, Peer: log["peer"], } @@ -88,6 +88,6 @@ func (r *Signal) Constraint() constraint { return blockKind } -func (r *Signal) Kind() string { - return tokSIGNAL +func (r *Signal) Kind() Kind { + return SIGNAL } diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 10d1c161..aed36f37 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -25,7 +25,7 @@ var ( // The functions available in the template tmplFunctionMap = template.FuncMap{ - "typeof": typeOf, + "kindof": kindOf, "join": join, "cjoin": cjoin, "indent": indent, @@ -34,24 +34,25 @@ var ( } // The apparmor templates - tmpl = generateTemplates([]string{ + tmpl = generateTemplates([]Kind{ // Global templates "apparmor", - tokPROFILE, + PROFILE, + HAT, "rules", // Preamble templates - tokABI, - tokALIAS, - tokINCLUDE, - "variable", - "comment", + ABI, + ALIAS, + INCLUDE, + VARIABLE, + COMMENT, // Rules templates - tokALL, tokRLIMIT, tokUSERNS, tokCAPABILITY, tokNETWORK, - tokMOUNT, tokREMOUNT, tokUMOUNT, tokPIVOTROOT, tokCHANGEPROFILE, - tokMQUEUE, tokIOURING, tokUNIX, tokPTRACE, tokSIGNAL, tokDBUS, - tokFILE, tokLINK, + ALL, RLIMIT, USERNS, CAPABILITY, NETWORK, + MOUNT, REMOUNT, UMOUNT, PIVOTROOT, CHANGEPROFILE, + MQUEUE, IOURING, UNIX, PTRACE, SIGNAL, DBUS, + FILE, LINK, }) // convert apparmor requested mask to apparmor access mode @@ -64,27 +65,28 @@ var ( } // The order the apparmor rules should be sorted - ruleAlphabet = []string{ - "include", - "all", - "rlimit", - "userns", - "capability", - "network", - "mount", - "remount", - "umount", - "pivotroot", - "changeprofile", - "mqueue", - "iouring", - "signal", - "ptrace", - "unix", - "dbus", - "file", - "link", - "profile", + ruleAlphabet = []Kind{ + INCLUDE, + ALL, + RLIMIT, + USERNS, + CAPABILITY, + NETWORK, + MOUNT, + REMOUNT, + UMOUNT, + PIVOTROOT, + CHANGEPROFILE, + MQUEUE, + IOURING, + SIGNAL, + PTRACE, + UNIX, + DBUS, + FILE, + LINK, + PROFILE, + HAT, "include_if_exists", } ruleWeights = generateWeights(ruleAlphabet) @@ -117,16 +119,16 @@ var ( fileWeights = generateWeights(fileAlphabet) // The order the rule values (access, type, domains, etc) should be sorted - requirements = map[string]requirement{} - requirementsWeights map[string]map[string]map[string]int + requirements = map[Kind]requirement{} + requirementsWeights map[Kind]map[string]map[string]int ) func init() { requirementsWeights = generateRequirementsWeights(requirements) } -func generateTemplates(names []string) map[string]*template.Template { - res := make(map[string]*template.Template, len(names)) +func generateTemplates(names []Kind) map[Kind]*template.Template { + res := make(map[Kind]*template.Template, len(names)) base := template.New("").Funcs(tmplFunctionMap) base = template.Must(base.ParseFS(tmplFiles, "templates/*.j2", "templates/rule/*.j2", @@ -141,11 +143,11 @@ func generateTemplates(names []string) map[string]*template.Template { return res } -func renderTemplate(name string, data any) string { +func renderTemplate(name Kind, data any) string { var res strings.Builder template, ok := tmpl[name] if !ok { - panic("template '" + name + "' not found") + panic("template '" + name.String() + "' not found") } err := template.Execute(&res, data) if err != nil { @@ -154,16 +156,16 @@ func renderTemplate(name string, data any) string { return res.String() } -func generateWeights(alphabet []string) map[string]int { - res := make(map[string]int, len(alphabet)) +func generateWeights[T Kind | string](alphabet []T) map[T]int { + res := make(map[T]int, len(alphabet)) for i, r := range alphabet { res[r] = i } return res } -func generateRequirementsWeights(requirements map[string]requirement) map[string]map[string]map[string]int { - res := make(map[string]map[string]map[string]int, len(requirements)) +func generateRequirementsWeights(requirements map[Kind]requirement) map[Kind]map[string]map[string]int { + res := make(map[Kind]map[string]map[string]int, len(requirements)) for rule, req := range requirements { res[rule] = make(map[string]map[string]int, len(req)) for key, values := range req { @@ -207,15 +209,11 @@ func cjoin(i any) string { } } -func typeOf(i any) string { +func kindOf(i any) string { if i == nil { return "" } - return strings.TrimPrefix(reflect.TypeOf(i).String(), "*aa.") -} - -func typeToValue(i reflect.Type) string { - return strings.ToLower(strings.TrimPrefix(i.String(), "*aa.")) + return i.(Rule).Kind().String() } func setindent(i string) string { diff --git a/pkg/aa/templates/rules.j2 b/pkg/aa/templates/rules.j2 index 4b66ab38..9a611332 100644 --- a/pkg/aa/templates/rules.j2 +++ b/pkg/aa/templates/rules.j2 @@ -4,118 +4,118 @@ {{- define "rules" -}} - {{- $oldtype := "" -}} + {{- $oldkind := "" -}} {{- range . -}} - {{- $type := typeof . -}} - {{- if eq $type "" -}} + {{- $kind := kindof . -}} + {{- if eq $kind "" -}} {{- "\n" -}} {{- continue -}} {{- end -}} - {{- if eq $type "Comment" -}} + {{- if eq $kind "comment" -}} {{- template "comment" . -}} {{- "\n" -}} {{- continue -}} {{- end -}} - {{- if and (ne $type $oldtype) (ne $oldtype "") -}} + {{- if and (ne $kind $oldkind) (ne $oldkind "") -}} {{- "\n" -}} {{- end -}} {{- indent "" -}} - {{- if eq $type "Abi" -}} + {{- if eq $kind "abi" -}} {{- template "abi" . -}} {{- end -}} - {{- if eq $type "Alias" -}} + {{- if eq $kind "alias" -}} {{- template "alias" . -}} {{- end -}} - {{- if eq $type "Include" -}} + {{- if eq $kind "include" -}} {{- template "include" . -}} {{- end -}} - {{- if eq $type "Variable" -}} + {{- if eq $kind "variable" -}} {{- template "variable" . -}} {{- end -}} - {{- if eq $type "All" -}} + {{- if eq $kind "all" -}} {{- template "all" . -}} {{- end -}} - {{- if eq $type "Rlimit" -}} + {{- if eq $kind "rlimit" -}} {{- template "rlimit" . -}} {{- end -}} - {{- if eq $type "Userns" -}} + {{- if eq $kind "userns" -}} {{- template "userns" . -}} {{- end -}} - {{- if eq $type "Capability" -}} + {{- if eq $kind "capability" -}} {{- template "capability" . -}} {{- end -}} - {{- if eq $type "Network" -}} + {{- if eq $kind "network" -}} {{- template "network" . -}} {{- end -}} - {{- if eq $type "Mount" -}} + {{- if eq $kind "mount" -}} {{- template "mount" . -}} {{- end -}} - {{- if eq $type "Remount" -}} + {{- if eq $kind "remount" -}} {{- template "remount" . -}} {{- end -}} - {{- if eq $type "Umount" -}} + {{- if eq $kind "umount" -}} {{- template "umount" . -}} {{- end -}} - {{- if eq $type "PivotRoot" -}} + {{- if eq $kind "pivot_root" -}} {{- template "pivot_root" . -}} {{- end -}} - {{- if eq $type "ChangeProfile" -}} + {{- if eq $kind "change_profile" -}} {{- template "change_profile" . -}} {{- end -}} - {{- if eq $type "Mqueue" -}} + {{- if eq $kind "mqueue" -}} {{- template "mqueue" . -}} {{- end -}} - {{- if eq $type "IOUring" -}} + {{- if eq $kind "io_uring" -}} {{- template "io_uring" . -}} {{- end -}} - {{- if eq $type "Unix" -}} + {{- if eq $kind "unix" -}} {{- template "unix" . -}} {{- end -}} - {{- if eq $type "Ptrace" -}} + {{- if eq $kind "ptrace" -}} {{- template "ptrace" . -}} {{- end -}} - {{- if eq $type "Signal" -}} + {{- if eq $kind "signal" -}} {{- template "signal" . -}} {{- end -}} - {{- if eq $type "Dbus" -}} + {{- if eq $kind "dbus" -}} {{- template "dbus" . -}} {{- end -}} - {{- if eq $type "File" -}} + {{- if eq $kind "file" -}} {{- template "file" . -}} {{- end -}} - {{- if eq $type "Link" -}} + {{- if eq $kind "link" -}} {{- template "link" . -}} {{- end -}} - {{- if eq $type "Profile" -}} + {{- if eq $kind "profile" -}} {{- template "profile" . -}} {{- end -}} {{- "\n" -}} - {{- $oldtype = $type -}} + {{- $oldkind = $kind -}} {{- end -}} {{- end -}} diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index eefe049f..b868459b 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -9,10 +9,10 @@ import ( "slices" ) -const tokUNIX = "unix" +const UNIX Kind = "unix" func init() { - requirements[tokUNIX] = requirement{ + requirements[UNIX] = requirement{ "access": []string{ "create", "bind", "listen", "accept", "connect", "shutdown", "getattr", "setattr", "getopt", "setopt", "send", "receive", @@ -39,7 +39,7 @@ func newUnixFromLog(log map[string]string) Rule { return &Unix{ RuleBase: newRuleFromLog(log), Qualifier: newQualifierFromLog(log), - Access: Must(toAccess(tokUNIX, log["requested_mask"])), + Access: Must(toAccess(UNIX, log["requested_mask"])), Type: log["sock_type"], Protocol: log["protocol"], Address: log["addr"], @@ -107,6 +107,6 @@ func (r *Unix) Constraint() constraint { return blockKind } -func (r *Unix) Kind() string { - return tokUNIX +func (r *Unix) Kind() Kind { + return UNIX } diff --git a/pkg/aa/userns.go b/pkg/aa/userns.go index e8409313..4c678f3d 100644 --- a/pkg/aa/userns.go +++ b/pkg/aa/userns.go @@ -4,7 +4,7 @@ package aa -const tokUSERNS = "userns" +const USERNS Kind = "userns" type Userns struct { RuleBase @@ -45,6 +45,6 @@ func (r *Userns) Constraint() constraint { return blockKind } -func (r *Userns) Kind() string { - return tokUSERNS +func (r *Userns) Kind() Kind { + return USERNS } From 0761a6c4664f4785afbc5c4ca3dfe9531a9b9053 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 18:16:21 +0100 Subject: [PATCH 49/62] feat(aa): add the hat template. --- pkg/aa/templates/hat.j2 | 18 ++++++++++++++++++ pkg/aa/templates/rules.j2 | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 pkg/aa/templates/hat.j2 diff --git a/pkg/aa/templates/hat.j2 b/pkg/aa/templates/hat.j2 new file mode 100644 index 00000000..694c3acc --- /dev/null +++ b/pkg/aa/templates/hat.j2 @@ -0,0 +1,18 @@ +{{- /* apparmor.d - Full set of apparmor profiles */ -}} +{{- /* Copyright (C) 2021-2024 Alexandre Pujol */ -}} +{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} + +{{- define "hat" -}} + + {{- "hat" -}} + {{- with .Name -}} + {{ " " }}{{ . }} + {{- end -}} + + {{- " {\n" -}} + {{- setindent "++" -}} + {{- template "rules" .Rules -}} + {{- setindent "--" -}} + {{- indent "}" -}} + +{{- end -}} diff --git a/pkg/aa/templates/rules.j2 b/pkg/aa/templates/rules.j2 index 9a611332..efc057e0 100644 --- a/pkg/aa/templates/rules.j2 +++ b/pkg/aa/templates/rules.j2 @@ -114,6 +114,10 @@ {{- template "profile" . -}} {{- end -}} + {{- if eq $kind "hat" -}} + {{- template "hat" . -}} + {{- end -}} + {{- "\n" -}} {{- $oldkind = $kind -}} {{- end -}} From 90087be509346dbf0a8abc21243f673deca9d395 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 18:20:52 +0100 Subject: [PATCH 50/62] feat(aa): Move sort, merge and format methods to the rules interface. - Use the new Kind struct in favor of reflect - Update sort function to slices.SortFunc --- pkg/aa/profile.go | 90 +++--------------------------------------- pkg/aa/rules.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 85 deletions(-) diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 55488bfb..5296d022 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -7,9 +7,7 @@ package aa import ( "fmt" "maps" - "reflect" "slices" - "sort" "strings" ) @@ -125,96 +123,18 @@ func (p *Profile) Kind() Kind { return PROFILE } -// Merge merge similar rules together. -// Steps: -// - Remove identical rules -// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw' -// -// Note: logs.regCleanLogs helps a lot to do a first cleaning func (p *Profile) Merge() { - for i := 0; i < len(p.Rules); i++ { - for j := i + 1; j < len(p.Rules); j++ { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - continue - } - - // If rules are identical, merge them - if p.Rules[i].Equals(p.Rules[j]) { - p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) - j-- - continue - } - - // File rule - if typeOfI == reflect.TypeFor[*File]() && typeOfJ == reflect.TypeFor[*File]() { - // Merge access - fileI := p.Rules[i].(*File) - fileJ := p.Rules[j].(*File) - if fileI.Path == fileJ.Path { - fileI.Access = append(fileI.Access, fileJ.Access...) - slices.SortFunc(fileI.Access, cmpFileAccess) - fileI.Access = slices.Compact(fileI.Access) - p.Rules = append(p.Rules[:j], p.Rules[j+1:]...) - j-- - } - } - } - } + slices.Sort(p.Flags) + p.Flags = slices.Compact(p.Flags) + p.Rules = p.Rules.Merge() } -// Sort the rules in a profile. -// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines func (p *Profile) Sort() { - sort.Slice(p.Rules, func(i, j int) bool { - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - if typeOfI != typeOfJ { - valueOfI := typeToValue(typeOfI) - valueOfJ := typeToValue(typeOfJ) - if typeOfI == reflect.TypeFor[*Include]() && p.Rules[i].(*Include).IfExists { - valueOfI = "include_if_exists" - } - if typeOfJ == reflect.TypeFor[*Include]() && p.Rules[j].(*Include).IfExists { - valueOfJ = "include_if_exists" - } - return ruleWeights[valueOfI] < ruleWeights[valueOfJ] - } - return p.Rules[i].Less(p.Rules[j]) - }) + p.Rules = p.Rules.Sort() } -// Format the profile for better readability before printing it. -// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block func (p *Profile) Format() { - const prefixOwner = " " - - hasOwnerRule := false - for i := len(p.Rules) - 1; i > 0; i-- { - j := i - 1 - typeOfI := reflect.TypeOf(p.Rules[i]) - typeOfJ := reflect.TypeOf(p.Rules[j]) - - // File rule - if typeOfI == reflect.TypeFor[*File]() && typeOfJ == reflect.TypeFor[*File]() { - letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path) - letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path) - - // Add prefix before rule path to align with other rule - if p.Rules[i].(*File).Owner { - hasOwnerRule = true - } else if hasOwnerRule { - p.Rules[i].(*File).Prefix = prefixOwner - } - - if letterI != letterJ { - // Add a new empty line between Files rule of different type - hasOwnerRule = false - p.Rules = append(p.Rules[:i], append(Rules{nil}, p.Rules[i:]...)...) - } - } - } + p.Rules = p.Rules.Format() } // GetAttachments return a nested attachment string diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 79db32e0..bc0f847c 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -133,6 +133,105 @@ func (r Rules) GetIncludes() []*Include { return res } +// Merge merge similar rules together. +// Steps: +// - Remove identical rules +// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw' +// +// Note: logs.regCleanLogs helps a lot to do a first cleaning +func (r Rules) Merge() Rules { + for i := 0; i < len(r); i++ { + for j := i + 1; j < len(r); j++ { + typeOfI := r[i].Kind() + typeOfJ := r[j].Kind() + if typeOfI != typeOfJ { + continue + } + + // If rules are identical, merge them + if r[i].Equals(r[j]) { + r = r.Delete(j) + j-- + continue + } + + // File rule + if typeOfI == FILE && typeOfJ == FILE { + // Merge access + fileI := r[i].(*File) + fileJ := r[j].(*File) + if fileI.Path == fileJ.Path { + fileI.Access = append(fileI.Access, fileJ.Access...) + slices.SortFunc(fileI.Access, cmpFileAccess) + fileI.Access = slices.Compact(fileI.Access) + r = r.Delete(j) + j-- + } + } + } + } + return r +} + +// Sort the rules according to the guidelines: +// https://apparmor.pujol.io/development/guidelines/#guidelines +func (r Rules) Sort() Rules { + slices.SortFunc(r, func(a, b Rule) int { + kindOfA := a.Kind() + kindOfB := b.Kind() + if kindOfA != kindOfB { + if kindOfA == INCLUDE && a.(*Include).IfExists { + kindOfA = "include_if_exists" + } + if kindOfB == INCLUDE && b.(*Include).IfExists { + kindOfB = "include_if_exists" + } + return ruleWeights[kindOfA] - ruleWeights[kindOfB] + } + if a.Equals(b) { + return 0 + } + if a.Less(b) { + return -1 + } + return 1 + }) + return r +} + +// Format the rules for better readability before printing it. +// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block +func (r Rules) Format() Rules { + const prefixOwner = " " + + hasOwnerRule := false + for i := len(r) - 1; i > 0; i-- { + j := i - 1 + typeOfI := r[i].Kind() + typeOfJ := r[j].Kind() + + // File rule + if typeOfI == FILE && typeOfJ == FILE { + letterI := getLetterIn(fileAlphabet, r[i].(*File).Path) + letterJ := getLetterIn(fileAlphabet, r[j].(*File).Path) + + // Add prefix before rule path to align with other rule + if r[i].(*File).Owner { + hasOwnerRule = true + } else if hasOwnerRule { + r[i].(*File).Prefix = prefixOwner + } + + if letterI != letterJ { + // Add a new empty line between Files rule of different type + hasOwnerRule = false + r = r.Insert(i, nil) + } + } + } + return r +} + // 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 { From e33c1243cc93316f75a361b1a74f02bd9042ebec Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 18:22:14 +0100 Subject: [PATCH 51/62] chore(aa): cleanup. --- pkg/aa/all.go | 6 +++--- pkg/aa/file.go | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/aa/all.go b/pkg/aa/all.go index 608bfd9a..ba23aa10 100644 --- a/pkg/aa/all.go +++ b/pkg/aa/all.go @@ -5,7 +5,7 @@ package aa const ( - tokALL = "all" + ALL Kind = "all" ) type All struct { @@ -32,6 +32,6 @@ func (r *All) Constraint() constraint { return blockKind } -func (r *All) Kind() string { - return tokALL +func (r *All) Kind() Kind { + return ALL } diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 07ab71d8..dd828951 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -95,15 +95,15 @@ func (r *File) Less(other any) bool { if r.Path != o.Path { return r.Path < o.Path } + if o.Owner != r.Owner { + return r.Owner + } if len(r.Access) != len(o.Access) { return len(r.Access) < len(o.Access) } if r.Target != o.Target { return r.Target < o.Target } - if o.Owner != r.Owner { - return r.Owner - } return r.Qualifier.Less(o.Qualifier) } @@ -153,12 +153,12 @@ func (r *Link) Less(other any) bool { if r.Path != o.Path { return r.Path < o.Path } - if r.Target != o.Target { - return r.Target < o.Target - } if o.Owner != r.Owner { return r.Owner } + if r.Target != o.Target { + return r.Target < o.Target + } if r.Subset != o.Subset { return r.Subset } From fe4c86a245ff3f415a6e307ef79070db8140ec6a Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 18:23:37 +0100 Subject: [PATCH 52/62] feat(aa): add more unit tests. --- pkg/aa/data_test.go | 62 +++++++++++++- pkg/aa/rules_test.go | 193 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 249 insertions(+), 6 deletions(-) diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go index d034788d..629010f2 100644 --- a/pkg/aa/data_test.go +++ b/pkg/aa/data_test.go @@ -5,17 +5,41 @@ package aa var ( + // Comment + comment1 = &Comment{RuleBase: RuleBase{Comment: "comment", IsLineRule: true}} + comment2 = &Comment{RuleBase: RuleBase{Comment: "another comment", IsLineRule: true}} + + // Abi + abi1 = &Abi{IsMagic: true, Path: "abi/4.0"} + abi2 = &Abi{IsMagic: true, Path: "abi/3.0"} + + // Alias + alias1 = &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"} + alias2 = &Alias{Path: "/mnt/var", RewrittenPath: "/var"} + // Include include1 = &Include{IsMagic: true, Path: "abstraction/base"} include2 = &Include{IsMagic: false, Path: "abstraction/base"} include3 = &Include{IfExists: true, IsMagic: true, Path: "abstraction/base"} includeLocal1 = &Include{IfExists: true, IsMagic: true, Path: "local/foo"} + // Variable + variable1 = &Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}, Define: true} + variable2 = &Variable{Name: "exec_path", Values: []string{"@{bin}/foo", "@{lib}/foo"}, Define: true} + + // All + all1 = &All{} + all2 = &All{RuleBase: RuleBase{Comment: "comment"}} + // Rlimit rlimit1 = &Rlimit{Key: "nproc", Op: "<=", Value: "200"} rlimit2 = &Rlimit{Key: "cpu", Op: "<=", Value: "2"} rlimit3 = &Rlimit{Key: "nproc", Op: "<", Value: "2"} + // Userns + userns1 = &Userns{Create: true} + userns2 = &Userns{} + // Capability capability1Log = map[string]string{ "apparmor": "ALLOWED", @@ -83,8 +107,12 @@ var ( MountPoint: "/newroot/dev/tty", } + // Remount + remount1 = &Remount{MountPoint: "/"} + remount2 = &Remount{MountPoint: "/{,**}/"} + // Umount - umount1Log = map[string]string{ + umount1Log = map[string]string{ "apparmor": "ALLOWED", "class": "mount", "operation": "umount", @@ -96,7 +124,6 @@ var ( umount2 = &Umount{MountPoint: "/oldroot/"} // PivotRoot - // pivotroot1LogStr = `apparmor="ALLOWED" operation="pivotroot" class="mount" profile="systemd" name="@{run}/systemd/mount-rootfs/" comm="(ostnamed)" srcname="@{run}/systemd/mount-rootfs/"` pivotroot1Log = map[string]string{ "apparmor": "ALLOWED", "class": "mount", @@ -120,7 +147,6 @@ var ( } // Change Profile - // changeprofile1LogStr = `apparmor="ALLOWED" operation="change_onexec" class="file" profile="systemd" name="systemd-user" comm="(systemd)" target="systemd-user"` changeprofile1Log = map[string]string{ "apparmor": "ALLOWED", "class": "file", @@ -134,6 +160,14 @@ var ( changeprofile2 = &ChangeProfile{ProfileName: "brwap"} changeprofile3 = &ChangeProfile{ExecMode: "safe", Exec: "/bin/bash", ProfileName: "brwap//default"} + // Mqueue + mqueue1 = &Mqueue{Access: []string{"r"}, Type: "posix", Name: "/"} + mqueue2 = &Mqueue{Access: []string{"r"}, Type: "sysv", Name: "/"} + + // IO Uring + iouring1 = &IOUring{Access: []string{"sqpoll"}, Label: "foo"} + iouring2 = &IOUring{Access: []string{"override_creds"}} + // Signal signal1Log = map[string]string{ "apparmor": "ALLOWED", @@ -335,4 +369,26 @@ var ( Path: "@{user_config_dirs}/kiorc", Target: "@{user_config_dirs}/#3954", } + + // Profile + profile1 = &Profile{ + Header: Header{ + Name: "sudo", + Attachments: []string{}, + Attributes: map[string]string{}, + Flags: []string{}, + }, + } + profile2 = &Profile{ + Header: Header{ + Name: "systemctl", + Attachments: []string{}, + Attributes: map[string]string{}, + Flags: []string{}, + }, + } + + // Hat + hat1 = &Hat{Name: "user"} + hat2 = &Hat{Name: "root"} ) diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 67c8397e..4278da8f 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -117,6 +117,18 @@ func TestRules_Less(t *testing.T) { other Rule want bool }{ + { + name: "comment", + rule: comment1, + other: comment2, + want: false, + }, + { + name: "abi", + rule: abi1, + other: abi2, + want: false, + }, { name: "include1", rule: include1, @@ -135,6 +147,18 @@ func TestRules_Less(t *testing.T) { other: include3, want: false, }, + { + name: "variable", + rule: variable2, + other: variable1, + want: false, + }, + { + name: "all", + rule: all1, + other: all2, + want: false, + }, { name: "rlimit", rule: rlimit1, @@ -153,6 +177,12 @@ func TestRules_Less(t *testing.T) { other: rlimit3, want: false, }, + { + name: "userns", + rule: userns1, + other: userns2, + want: true, + }, { name: "capability", rule: capability1, @@ -171,6 +201,12 @@ func TestRules_Less(t *testing.T) { other: mount2, want: false, }, + { + name: "remount", + rule: remount1, + other: remount2, + want: true, + }, { name: "umount", rule: umount1, @@ -201,6 +237,18 @@ func TestRules_Less(t *testing.T) { other: changeprofile3, want: true, }, + { + name: "mqueue", + rule: mqueue1, + other: mqueue2, + want: true, + }, + { + name: "iouring", + rule: iouring1, + other: iouring2, + want: false, + }, { name: "signal", rule: signal1, @@ -279,6 +327,18 @@ func TestRules_Less(t *testing.T) { other: link2, want: true, }, + { + name: "profile", + rule: profile1, + other: profile2, + want: true, + }, + { + name: "hat", + rule: hat1, + other: hat2, + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -298,17 +358,53 @@ func TestRules_Equals(t *testing.T) { want bool }{ { - name: "include1", + name: "comment", + rule: comment1, + other: comment2, + want: false, + }, + { + name: "abi", + rule: abi1, + other: abi1, + want: true, + }, + { + name: "alias", + rule: alias1, + other: alias2, + want: false, + }, + { + name: "include", rule: include1, other: includeLocal1, want: false, }, + { + name: "variable", + rule: variable1, + other: variable2, + want: false, + }, + { + name: "all", + rule: all1, + other: all2, + want: false, + }, { name: "rlimit", rule: rlimit1, other: rlimit1, want: true, }, + { + name: "userns", + rule: userns1, + other: userns1, + want: true, + }, { name: "capability/equal", rule: capability1, @@ -324,7 +420,19 @@ func TestRules_Equals(t *testing.T) { { name: "mount", rule: mount1, - other: mount1, + other: mount2, + want: false, + }, + { + name: "remount", + rule: remount2, + other: remount2, + want: true, + }, + { + name: "umount", + rule: umount1, + other: umount1, want: true, }, { @@ -339,6 +447,18 @@ func TestRules_Equals(t *testing.T) { other: changeprofile2, want: false, }, + { + name: "mqueue", + rule: mqueue1, + other: mqueue1, + want: true, + }, + { + name: "iouring", + rule: iouring1, + other: iouring2, + want: false, + }, { name: "signal1/equal", rule: signal1, @@ -381,6 +501,18 @@ func TestRules_Equals(t *testing.T) { other: link3, want: false, }, + { + name: "profile", + rule: profile1, + other: profile1, + want: true, + }, + { + name: "hat", + rule: hat1, + other: hat1, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -399,7 +531,22 @@ func TestRules_String(t *testing.T) { want string }{ { - name: "include1", + name: "comment", + rule: comment1, + want: "#comment", + }, + { + name: "abi", + rule: abi1, + want: "abi ,", + }, + { + name: "alias", + rule: alias1, + want: "alias /mnt/usr -> /usr,", + }, + { + name: "include", rule: include1, want: "include ", }, @@ -413,11 +560,26 @@ func TestRules_String(t *testing.T) { rule: &Include{Path: "/usr/share/apparmor.d/", IsMagic: false}, want: `include "/usr/share/apparmor.d/"`, }, + { + name: "variable", + rule: variable1, + want: "@{bin} = /{,usr/}{,s}bin", + }, + { + name: "all", + rule: all1, + want: "all,", + }, { name: "rlimit", rule: rlimit1, want: "set rlimit nproc <= 200,", }, + { + name: "userns", + rule: userns1, + want: "userns,", + }, { name: "capability", rule: capability1, @@ -443,6 +605,16 @@ func TestRules_String(t *testing.T) { rule: mount1, want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check", }, + { + name: "remount", + rule: remount1, + want: "remount /,", + }, + { + name: "umount", + rule: umount1, + want: "umount /,", + }, { name: "pivot_root", rule: pivotroot1, @@ -453,6 +625,16 @@ func TestRules_String(t *testing.T) { rule: changeprofile1, want: "change_profile -> systemd-user,", }, + { + name: "mqeue", + rule: mqueue1, + want: "mqueue r type=posix /,", + }, + { + name: "iouring", + rule: iouring1, + want: "io_uring sqpoll label=foo,", + }, { name: "signal", rule: signal1, @@ -496,6 +678,11 @@ func TestRules_String(t *testing.T) { rule: link3, want: "owner link @{user_config_dirs}/kiorc -> @{user_config_dirs}/#3954,", }, + { + name: "hat", + rule: hat1, + want: "hat user {\n}", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 23eaa20fb77d4dda4696fefde8c363b49bafcc74 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Wed, 29 May 2024 21:12:54 +0100 Subject: [PATCH 53/62] feat(aa): ensure the prebuild jobs are working. --- pkg/aa/resolve.go | 12 ++++++------ pkg/prebuild/builder/core.go | 2 +- pkg/prebuild/builder/userspace.go | 19 +++++++++++++++---- pkg/prebuild/directive/core.go | 4 ++-- pkg/prebuild/directive/exec.go | 8 +++++--- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/pkg/aa/resolve.go b/pkg/aa/resolve.go index df97c2aa..ec640de3 100644 --- a/pkg/aa/resolve.go +++ b/pkg/aa/resolve.go @@ -22,12 +22,12 @@ var ( // Resolve resolves variables and includes definied in the profile preamble func (f *AppArmorProfileFile) Resolve() error { // Resolve preamble includes - for _, include := range f.Preamble.GetIncludes() { - err := f.resolveInclude(include) - if err != nil { - return err - } - } + // for _, include := range f.Preamble.GetIncludes() { + // err := f.resolveInclude(include) + // if err != nil { + // return err + // } + // } // Resolve variables for _, variable := range f.Preamble.GetVariables() { diff --git a/pkg/prebuild/builder/core.go b/pkg/prebuild/builder/core.go index e6512820..40321bb1 100644 --- a/pkg/prebuild/builder/core.go +++ b/pkg/prebuild/builder/core.go @@ -58,7 +58,7 @@ func Run(file *paths.Path, profile string) (string, error) { for _, b := range Builds { profile, err = b.Apply(opt, profile) if err != nil { - return "", err + return "", fmt.Errorf("%s %s: %w", b.Name(), opt.File, err) } } return profile, nil diff --git a/pkg/prebuild/builder/userspace.go b/pkg/prebuild/builder/userspace.go index a8bbbf6e..7060d2b1 100644 --- a/pkg/prebuild/builder/userspace.go +++ b/pkg/prebuild/builder/userspace.go @@ -30,10 +30,21 @@ func init() { } func (b Userspace) Apply(opt *Option, profile string) (string, error) { - p := aa.DefaultTunables() - p.ParseVariables(profile) - p.ResolveAttachments() - att := p.NestAttachments() + if ok, _ := opt.File.IsInsideDir(cfg.RootApparmord.Join("abstractions")); ok { + return profile, nil + } + if ok, _ := opt.File.IsInsideDir(cfg.RootApparmord.Join("tunables")); ok { + return profile, nil + } + + f := aa.DefaultTunables() + if err := f.Parse(profile); err != nil { + return "", err + } + if err := f.Resolve(); err != nil { + return "", err + } + att := f.GetDefaultProfile().GetAttachments() matches := regAttachments.FindAllString(profile, -1) if len(matches) > 0 { strheader := strings.Replace(matches[0], "@{exec_path}", att, -1) diff --git a/pkg/prebuild/directive/core.go b/pkg/prebuild/directive/core.go index 8c068981..b94e2fdd 100644 --- a/pkg/prebuild/directive/core.go +++ b/pkg/prebuild/directive/core.go @@ -71,11 +71,11 @@ func Run(file *paths.Path, profile string) (string, error) { opt := NewOption(file, match) drtv, ok := Directives[opt.Name] if !ok { - return "", fmt.Errorf("Unknown directive: %s", opt.Name) + return "", fmt.Errorf("Unknown directive '%s' in %s", opt.Name, opt.File) } profile, err = drtv.Apply(opt, profile) if err != nil { - return "", err + return "", fmt.Errorf("%s %s: %w", drtv.Name(), opt.File, err) } } return profile, nil diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index 0dc1aec6..0a8caf2b 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -2,6 +2,8 @@ // Copyright (C) 2021-2024 Alexandre Pujol // SPDX-License-Identifier: GPL-2.0-only +// TODO: Local variables in profile header need to be resolved + package directive import ( @@ -40,8 +42,8 @@ func (d Exec) Apply(opt *Option, profileRaw string) (string, error) { for name := range opt.ArgMap { profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name)) dstProfile := aa.DefaultTunables() - dstProfile.ParseVariables(profiletoTransition) - for _, variable := range dstProfile.Variables { + dstProfile.Parse(profiletoTransition) + for _, variable := range dstProfile.Preamble.GetVariables() { if variable.Name == "exec_path" { for _, v := range variable.Values { rules = append(rules, &aa.File{ @@ -57,7 +59,7 @@ func (d Exec) Apply(opt *Option, profileRaw string) (string, error) { aa.IndentationLevel = strings.Count( strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation, ) - rules.Sort() + rules = rules.Sort() new := rules.String() new = new[:len(new)-1] return strings.Replace(profileRaw, opt.Raw, new, -1), nil From 5f64bb4e0cfc6444ec584a48165533ec58bbc2ba Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Wed, 29 May 2024 21:17:21 +0100 Subject: [PATCH 54/62] tests(aa): improve rules unit tests. --- pkg/aa/parse_test.go | 4 +- pkg/aa/rules.go | 18 +- pkg/aa/rules_test.go | 1080 +++++++++++++++++------------------------- 3 files changed, 441 insertions(+), 661 deletions(-) diff --git a/pkg/aa/parse_test.go b/pkg/aa/parse_test.go index c5f0f084..eae45a06 100644 --- a/pkg/aa/parse_test.go +++ b/pkg/aa/parse_test.go @@ -12,7 +12,7 @@ import ( ) func Test_tokenizeRule(t *testing.T) { - for _, tt := range testRules { + for _, tt := range testTokenRules { t.Run(tt.name, func(t *testing.T) { if got := tokenize(tt.raw); !reflect.DeepEqual(got, tt.tokens) { t.Errorf("tokenize() = %v, want %v", got, tt.tokens) @@ -37,7 +37,7 @@ func Test_AppArmorProfileFile_Parse(t *testing.T) { var ( // Test cases for tokenize - testRules = []struct { + testTokenRules = []struct { name string raw string tokens []string diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index bc0f847c..8f432635 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -54,6 +54,9 @@ type Rules []Rule func (r Rules) Validate() error { for _, rule := range r { + if rule == nil { + continue + } if err := rule.Validate(); err != nil { return err } @@ -66,9 +69,12 @@ func (r Rules) String() string { } // Index returns the index of the first occurrence of rule rin r, or -1 if not present. -func (r Rules) Index(rule Rule) int { - for idx, rr := range r { - if rr.Kind() == rule.Kind() && rr.Equals(rule) { +func (r Rules) Index(item Rule) int { + for idx, rule := range r { + if rule == nil { + continue + } + if rule.Kind() == item.Kind() && rule.Equals(item) { return idx } } @@ -94,6 +100,9 @@ func (r Rules) Delete(i int) Rules { func (r Rules) DeleteKind(kind Kind) Rules { res := make(Rules, 0) for _, rule := range r { + if rule == nil { + continue + } if rule.Kind() != kind { res = append(res, rule) } @@ -104,6 +113,9 @@ func (r Rules) DeleteKind(kind Kind) Rules { func (r Rules) Filter(filter Kind) Rules { res := make(Rules, 0) for _, rule := range r { + if rule == nil { + continue + } if rule.Kind() != filter { res = append(res, rule) } diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 4278da8f..2b944005 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -10,686 +10,454 @@ import ( ) func TestRules_FromLog(t *testing.T) { - tests := []struct { - name string - fromLog func(map[string]string) Rule - log map[string]string - want Rule - }{ - { - name: "capbability", - fromLog: newCapabilityFromLog, - log: capability1Log, - want: capability1, - }, - { - name: "network", - fromLog: newNetworkFromLog, - log: network1Log, - want: network1, - }, - { - name: "mount", - fromLog: newMountFromLog, - log: mount1Log, - want: mount1, - }, - { - name: "umount", - fromLog: newUmountFromLog, - log: umount1Log, - want: umount1, - }, - { - name: "pivotroot", - fromLog: newPivotRootFromLog, - log: pivotroot1Log, - want: pivotroot1, - }, - { - name: "changeprofile", - fromLog: newChangeProfileFromLog, - log: changeprofile1Log, - want: changeprofile1, - }, - { - name: "signal", - fromLog: newSignalFromLog, - log: signal1Log, - want: signal1, - }, - { - name: "ptrace/xdg-document-portal", - fromLog: newPtraceFromLog, - log: ptrace1Log, - want: ptrace1, - }, - { - name: "ptrace/snap-update-ns.firefox", - fromLog: newPtraceFromLog, - log: ptrace2Log, - want: ptrace2, - }, - { - name: "unix", - fromLog: newUnixFromLog, - log: unix1Log, - want: unix1, - }, - { - name: "dbus", - fromLog: newDbusFromLog, - log: dbus1Log, - want: dbus1, - }, - { - name: "file", - fromLog: newFileFromLog, - log: file1Log, - want: file1, - }, - { - name: "link", - fromLog: newLinkFromLog, - log: link1Log, - want: link1, - }, - { - name: "link", - fromLog: newFileFromLog, - log: link3Log, - want: link3, - }, - } - for _, tt := range tests { + for _, tt := range testRule { + if tt.fromLog == nil { + continue + } t.Run(tt.name, func(t *testing.T) { - if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.want) { - t.Errorf("RuleFromLog() = %v, want %v", got, tt.want) + if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.rule) { + t.Errorf("RuleFromLog() = %v, want %v", got, tt.rule) + } + }) + } +} + +func TestRules_Validate(t *testing.T) { + for _, tt := range testRule { + t.Run(tt.name, func(t *testing.T) { + if err := tt.rule.Validate(); (err != nil) != tt.wValidErr { + t.Errorf("Rules.Validate() error = %v, wantErr %v", err, tt.wValidErr) } }) } } func TestRules_Less(t *testing.T) { - tests := []struct { - name string - rule Rule - other Rule - want bool - }{ - { - name: "comment", - rule: comment1, - other: comment2, - want: false, - }, - { - name: "abi", - rule: abi1, - other: abi2, - want: false, - }, - { - name: "include1", - rule: include1, - other: includeLocal1, - want: false, - }, - { - name: "include2", - rule: include1, - other: include2, - want: false, - }, - { - name: "include3", - rule: include1, - other: include3, - want: false, - }, - { - name: "variable", - rule: variable2, - other: variable1, - want: false, - }, - { - name: "all", - rule: all1, - other: all2, - want: false, - }, - { - name: "rlimit", - rule: rlimit1, - other: rlimit2, - want: false, - }, - { - name: "rlimit2", - rule: rlimit2, - other: rlimit2, - want: false, - }, - { - name: "rlimit3", - rule: rlimit1, - other: rlimit3, - want: false, - }, - { - name: "userns", - rule: userns1, - other: userns2, - want: true, - }, - { - name: "capability", - rule: capability1, - other: capability2, - want: true, - }, - { - name: "network", - rule: network1, - other: network2, - want: false, - }, - { - name: "mount", - rule: mount1, - other: mount2, - want: false, - }, - { - name: "remount", - rule: remount1, - other: remount2, - want: true, - }, - { - name: "umount", - rule: umount1, - other: umount2, - want: true, - }, - { - name: "pivot_root1", - rule: pivotroot2, - other: pivotroot1, - want: true, - }, - { - name: "pivot_root2", - rule: pivotroot1, - other: pivotroot3, - want: false, - }, - { - name: "change_profile1", - rule: changeprofile1, - other: changeprofile2, - want: false, - }, - { - name: "change_profile2", - rule: changeprofile1, - other: changeprofile3, - want: true, - }, - { - name: "mqueue", - rule: mqueue1, - other: mqueue2, - want: true, - }, - { - name: "iouring", - rule: iouring1, - other: iouring2, - want: false, - }, - { - name: "signal", - rule: signal1, - other: signal2, - want: false, - }, - { - name: "ptrace/less", - rule: ptrace1, - other: ptrace2, - want: false, - }, - { - name: "ptrace/more", - rule: ptrace2, - other: ptrace1, - want: false, - }, - { - name: "unix", - rule: unix1, - other: unix1, - want: false, - }, - { - name: "dbus", - rule: dbus1, - other: dbus1, - want: false, - }, - { - name: "dbus2", - rule: dbus2, - other: dbus3, - want: false, - }, - { - name: "file", - rule: file1, - other: file2, - want: true, - }, - { - name: "file/empty", - rule: &File{}, - other: &File{}, - want: false, - }, - { - name: "file/equal", - rule: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, - other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, - want: false, - }, - { - name: "file/owner", - rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Owner: true}, - other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, - want: true, - }, - { - name: "file/access", - rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}}, - other: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"w"}}, - want: false, - }, - { - name: "file/close", - rule: &File{Path: "/usr/share/poppler/cMap/"}, - other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, - want: true, - }, - { - name: "link", - rule: link1, - other: link2, - want: true, - }, - { - name: "profile", - rule: profile1, - other: profile2, - want: true, - }, - { - name: "hat", - rule: hat1, - other: hat2, - want: false, - }, - } - for _, tt := range tests { + for _, tt := range testRule { + if tt.oLess == nil { + continue + } t.Run(tt.name, func(t *testing.T) { - r := tt.rule - if got := r.Less(tt.other); got != tt.want { - t.Errorf("Rule.Less() = %v, want %v", got, tt.want) + if got := tt.rule.Less(tt.oLess); got != tt.wLessErr { + t.Errorf("Rule.Less() = %v, want %v", got, tt.wLessErr) } }) } } func TestRules_Equals(t *testing.T) { - tests := []struct { - name string - rule Rule - other Rule - want bool - }{ - { - name: "comment", - rule: comment1, - other: comment2, - want: false, - }, - { - name: "abi", - rule: abi1, - other: abi1, - want: true, - }, - { - name: "alias", - rule: alias1, - other: alias2, - want: false, - }, - { - name: "include", - rule: include1, - other: includeLocal1, - want: false, - }, - { - name: "variable", - rule: variable1, - other: variable2, - want: false, - }, - { - name: "all", - rule: all1, - other: all2, - want: false, - }, - { - name: "rlimit", - rule: rlimit1, - other: rlimit1, - want: true, - }, - { - name: "userns", - rule: userns1, - other: userns1, - want: true, - }, - { - name: "capability/equal", - rule: capability1, - other: capability1, - want: true, - }, - { - name: "network/equal", - rule: network1, - other: network1, - want: true, - }, - { - name: "mount", - rule: mount1, - other: mount2, - want: false, - }, - { - name: "remount", - rule: remount2, - other: remount2, - want: true, - }, - { - name: "umount", - rule: umount1, - other: umount1, - want: true, - }, - { - name: "pivot_root", - rule: pivotroot1, - other: pivotroot2, - want: false, - }, - { - name: "change_profile", - rule: changeprofile1, - other: changeprofile2, - want: false, - }, - { - name: "mqueue", - rule: mqueue1, - other: mqueue1, - want: true, - }, - { - name: "iouring", - rule: iouring1, - other: iouring2, - want: false, - }, - { - name: "signal1/equal", - rule: signal1, - other: signal1, - want: true, - }, - { - name: "ptrace/equal", - rule: ptrace1, - other: ptrace1, - want: true, - }, - { - name: "ptrace/not_equal", - rule: ptrace1, - other: ptrace2, - want: false, - }, - { - name: "unix", - rule: unix1, - other: unix1, - want: true, - }, - { - name: "dbus", - rule: dbus1, - other: dbus2, - want: false, - }, - { - name: "file", - rule: file2, - other: file2, - want: true, - }, - { - name: "link", - rule: link1, - other: link3, - want: false, - }, - { - name: "profile", - rule: profile1, - other: profile1, - want: true, - }, - { - name: "hat", - rule: hat1, - other: hat1, - want: true, - }, - } - for _, tt := range tests { + for _, tt := range testRule { + if tt.oEqual == nil { + continue + } t.Run(tt.name, func(t *testing.T) { r := tt.rule - if got := r.Equals(tt.other); got != tt.want { - t.Errorf("Rule.Equals() = %v, want %v", got, tt.want) + if got := r.Equals(tt.oEqual); got != tt.wEqualErr { + t.Errorf("Rule.Equals() = %v, want %v", got, tt.wEqualErr) } }) } } func TestRules_String(t *testing.T) { - tests := []struct { - name string - rule Rule - want string - }{ - { - name: "comment", - rule: comment1, - want: "#comment", - }, - { - name: "abi", - rule: abi1, - want: "abi ,", - }, - { - name: "alias", - rule: alias1, - want: "alias /mnt/usr -> /usr,", - }, - { - name: "include", - rule: include1, - want: "include ", - }, - { - name: "include-local", - rule: includeLocal1, - want: "include if exists ", - }, - { - name: "include-abs", - rule: &Include{Path: "/usr/share/apparmor.d/", IsMagic: false}, - want: `include "/usr/share/apparmor.d/"`, - }, - { - name: "variable", - rule: variable1, - want: "@{bin} = /{,usr/}{,s}bin", - }, - { - name: "all", - rule: all1, - want: "all,", - }, - { - name: "rlimit", - rule: rlimit1, - want: "set rlimit nproc <= 200,", - }, - { - name: "userns", - rule: userns1, - want: "userns,", - }, - { - name: "capability", - rule: capability1, - want: "capability net_admin,", - }, - { - name: "capability/multi", - rule: &Capability{Names: []string{"dac_override", "dac_read_search"}}, - want: "capability dac_override dac_read_search,", - }, - { - name: "capability/all", - rule: &Capability{}, - want: "capability,", - }, - { - name: "network", - rule: network1, - want: "network netlink raw,", - }, - { - name: "mount", - rule: mount1, - want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check", - }, - { - name: "remount", - rule: remount1, - want: "remount /,", - }, - { - name: "umount", - rule: umount1, - want: "umount /,", - }, - { - name: "pivot_root", - rule: pivotroot1, - want: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,", - }, - { - name: "change_profile", - rule: changeprofile1, - want: "change_profile -> systemd-user,", - }, - { - name: "mqeue", - rule: mqueue1, - want: "mqueue r type=posix /,", - }, - { - name: "iouring", - rule: iouring1, - want: "io_uring sqpoll label=foo,", - }, - { - name: "signal", - rule: signal1, - want: "signal receive set=kill peer=firefox//&firejail-default,", - }, - { - name: "ptrace", - rule: ptrace1, - want: "ptrace read peer=nautilus,", - }, - { - name: "unix", - rule: unix1, - want: "unix (send receive) type=stream protocol=0 addr=none peer=(label=dbus-daemon, addr=@/tmp/dbus-AaKMpxzC4k),", - }, - { - name: "dbus", - rule: dbus1, - want: `dbus receive bus=session path=/org/gtk/vfs/metadata - interface=org.gtk.vfs.Metadata - member=Remove - peer=(name=:1.15, label=tracker-extract),`, - }, - { - name: "dbus-bind", - rule: &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"}, - want: `dbus bind bus=session name=org.gnome.*,`, - }, - { - name: "dbus-full", - rule: &Dbus{Bus: "accessibility"}, - want: `dbus bus=accessibility,`, - }, - { - name: "file", - rule: file1, - want: "/usr/share/poppler/cMap/Identity-H r,", - }, - { - name: "link", - rule: link3, - want: "owner link @{user_config_dirs}/kiorc -> @{user_config_dirs}/#3954,", - }, - { - name: "hat", - rule: hat1, - want: "hat user {\n}", - }, - } - for _, tt := range tests { + for _, tt := range testRule { t.Run(tt.name, func(t *testing.T) { - r := tt.rule - if got := r.String(); got != tt.want { - t.Errorf("Rule.String() = %v, want %v", got, tt.want) + if got := tt.rule.String(); got != tt.wString { + t.Errorf("Rule.String() = %v, want %v", got, tt.wString) } }) } } + +var ( + // Test cases for the Rule interface + testRule = []struct { + name string + fromLog func(map[string]string) Rule + log map[string]string + rule Rule + wValidErr bool + oLess Rule + wLessErr bool + oEqual Rule + wEqualErr bool + wString string + }{ + { + name: "comment", + rule: comment1, + oLess: comment2, + wLessErr: false, + oEqual: comment2, + wEqualErr: false, + wString: "#comment", + }, + { + name: "abi", + rule: abi1, + oLess: abi2, + wLessErr: false, + oEqual: abi1, + wEqualErr: true, + wString: "abi ,", + }, + { + name: "alias", + rule: alias1, + oLess: alias2, + wLessErr: true, + oEqual: alias2, + wEqualErr: false, + wString: "alias /mnt/usr -> /usr,", + }, + { + name: "include1", + rule: include1, + oLess: includeLocal1, + wLessErr: false, + oEqual: includeLocal1, + wEqualErr: false, + wString: "include ", + }, + { + name: "include2", + rule: include1, + oLess: include2, + wLessErr: false, + wString: "include ", + }, + { + name: "include-local", + rule: includeLocal1, + oLess: include1, + wLessErr: true, + wString: "include if exists ", + }, + { + name: "include/abs", + rule: &Include{Path: "/usr/share/apparmor.d/", IsMagic: false}, + wString: `include "/usr/share/apparmor.d/"`, + }, + { + name: "variable", + rule: variable1, + oLess: variable2, + wLessErr: true, + oEqual: variable1, + wEqualErr: true, + wString: "@{bin} = /{,usr/}{,s}bin", + }, + { + name: "all", + rule: all1, + oLess: all2, + wLessErr: false, + oEqual: all2, + wEqualErr: false, + wString: "all,", + }, + { + name: "rlimit", + rule: rlimit1, + oLess: rlimit2, + wLessErr: false, + oEqual: rlimit1, + wEqualErr: true, + wString: "set rlimit nproc <= 200,", + }, + { + name: "rlimit2", + rule: rlimit2, + oLess: rlimit2, + wLessErr: false, + wString: "set rlimit cpu <= 2,", + }, + { + name: "rlimit3", + rule: rlimit3, + oLess: rlimit1, + wLessErr: true, + + wString: "set rlimit nproc < 2,", + }, + { + name: "userns", + rule: userns1, + oLess: userns2, + wLessErr: true, + oEqual: userns1, + wEqualErr: true, + wString: "userns,", + }, + { + name: "capbability", + fromLog: newCapabilityFromLog, + log: capability1Log, + rule: capability1, + oLess: capability2, + wLessErr: true, + oEqual: capability1, + wEqualErr: true, + wString: "capability net_admin,", + }, + { + name: "capability/multi", + rule: &Capability{Names: []string{"dac_override", "dac_read_search"}}, + wString: "capability dac_override dac_read_search,", + }, + { + name: "capability/all", + rule: &Capability{}, + wString: "capability,", + }, + { + name: "network", + fromLog: newNetworkFromLog, + log: network1Log, + rule: network1, + wValidErr: true, + oLess: network2, + wLessErr: false, + oEqual: network1, + wEqualErr: true, + wString: "network netlink raw,", + }, + { + name: "mount", + fromLog: newMountFromLog, + log: mount1Log, + rule: mount1, + oEqual: mount2, + wEqualErr: false, + wString: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check", + }, + { + name: "remount", + rule: remount1, + oLess: remount2, + wLessErr: true, + oEqual: remount1, + wEqualErr: true, + wString: "remount /,", + }, + { + name: "umount", + fromLog: newUmountFromLog, + log: umount1Log, + rule: umount1, + oLess: umount2, + wLessErr: true, + oEqual: umount1, + wEqualErr: true, + wString: "umount /,", + }, + { + name: "pivot_root1", + fromLog: newPivotRootFromLog, + log: pivotroot1Log, + rule: pivotroot1, + oLess: pivotroot2, + wLessErr: false, + oEqual: pivotroot2, + wEqualErr: false, + wString: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,", + }, + { + name: "pivot_root2", + rule: pivotroot1, + oLess: pivotroot3, + wLessErr: false, + wString: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,", + }, + { + name: "change_profile1", + fromLog: newChangeProfileFromLog, + log: changeprofile1Log, + rule: changeprofile1, + oLess: changeprofile2, + wLessErr: false, + wString: "change_profile -> systemd-user,", + }, + { + name: "change_profile2", + rule: changeprofile2, + oLess: changeprofile3, + wLessErr: true, + oEqual: changeprofile1, + wEqualErr: false, + wString: "change_profile -> brwap,", + }, + { + name: "mqueue", + rule: mqueue1, + oLess: mqueue2, + wLessErr: true, + oEqual: mqueue1, + wEqualErr: true, + wString: "mqueue r type=posix /,", + }, + { + name: "iouring", + rule: iouring1, + oLess: iouring2, + wLessErr: false, + oEqual: iouring2, + wEqualErr: false, + wString: "io_uring sqpoll label=foo,", + }, + { + name: "signal", + fromLog: newSignalFromLog, + log: signal1Log, + rule: signal1, + oLess: signal2, + wLessErr: false, + oEqual: signal1, + wEqualErr: true, + wString: "signal receive set=kill peer=firefox//&firejail-default,", + }, + { + name: "ptrace/xdg-document-portal", + fromLog: newPtraceFromLog, + log: ptrace1Log, + rule: ptrace1, + oLess: ptrace2, + wLessErr: false, + oEqual: ptrace1, + wEqualErr: true, + wString: "ptrace read peer=nautilus,", + }, + { + name: "ptrace/snap-update-ns.firefox", + fromLog: newPtraceFromLog, + log: ptrace2Log, + rule: ptrace2, + oLess: ptrace1, + wLessErr: false, + oEqual: ptrace1, + wEqualErr: false, + wString: "ptrace readby peer=systemd-journald,", + }, + { + name: "unix", + fromLog: newUnixFromLog, + log: unix1Log, + rule: unix1, + oLess: unix1, + wLessErr: false, + oEqual: unix1, + wEqualErr: true, + wString: "unix (send receive) type=stream protocol=0 addr=none peer=(label=dbus-daemon, addr=@/tmp/dbus-AaKMpxzC4k),", + }, + { + name: "dbus", + fromLog: newDbusFromLog, + log: dbus1Log, + rule: dbus1, + oLess: dbus1, + wLessErr: false, + oEqual: dbus2, + wEqualErr: false, + wString: "dbus receive bus=session path=/org/gtk/vfs/metadata\n interface=org.gtk.vfs.Metadata\n member=Remove\n peer=(name=:1.15, label=tracker-extract),", + }, + { + name: "dbus2", + rule: dbus2, + oLess: dbus3, + wLessErr: false, + wString: "dbus bind bus=session name=org.gnome.evolution.dataserver.Sources5,", + }, + { + name: "dbus/bind", + rule: &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"}, + wString: `dbus bind bus=session name=org.gnome.*,`, + }, + { + name: "dbus/full", + rule: &Dbus{Bus: "accessibility"}, + wString: `dbus bus=accessibility,`, + }, + { + name: "file", + fromLog: newFileFromLog, + log: file1Log, + rule: file1, + oLess: file2, + wLessErr: true, + oEqual: file2, + wEqualErr: false, + wString: "/usr/share/poppler/cMap/Identity-H r,", + }, + { + name: "file/empty", + rule: &File{}, + oLess: &File{}, + wLessErr: false, + wString: " ,", + }, + { + name: "file/equal", + rule: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, + oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, + wLessErr: false, + wString: "/usr/share/poppler/cMap/Identity-H ,", + }, + { + name: "file/owner", + rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Owner: true}, + oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, + wLessErr: true, + wString: "owner /usr/share/poppler/cMap/Identity-H ,", + }, + { + name: "file/access", + rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}}, + oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"w"}}, + wLessErr: false, + wString: "/usr/share/poppler/cMap/Identity-H r,", + }, + { + name: "file/close", + rule: &File{Path: "/usr/share/poppler/cMap/"}, + oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, + wLessErr: true, + wString: "/usr/share/poppler/cMap/ ,", + }, + { + name: "link", + fromLog: newLinkFromLog, + log: link1Log, + rule: link1, + oLess: link2, + wLessErr: true, + oEqual: link3, + wEqualErr: false, + wString: "link /tmp/mkinitcpio.QDWtza/early@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst -> /tmp/mkinitcpio.QDWtza/root@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst,", + }, + { + name: "link", + fromLog: newFileFromLog, + log: link3Log, + rule: link3, + wString: "owner link @{user_config_dirs}/kiorc -> @{user_config_dirs}/#3954,", + }, + { + name: "profile", + rule: profile1, + oLess: profile2, + wLessErr: true, + oEqual: profile1, + wEqualErr: true, + wString: "profile sudo {\n}", + }, + { + name: "hat", + rule: hat1, + oLess: hat2, + wLessErr: false, + oEqual: hat1, + wEqualErr: true, + wString: "hat user {\n}", + }, + } +) From 0f382a4d5d5bb81d952091361a67442b8f4ce013 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Wed, 29 May 2024 21:18:30 +0100 Subject: [PATCH 55/62] tests(aa): improve aa unit tests. --- pkg/aa/apparmor_test.go | 18 ++++++++++++++---- pkg/aa/parse.go | 3 +++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pkg/aa/apparmor_test.go b/pkg/aa/apparmor_test.go index faef895d..85bb1fc3 100644 --- a/pkg/aa/apparmor_test.go +++ b/pkg/aa/apparmor_test.go @@ -123,16 +123,22 @@ func TestAppArmorProfileFile_Sort(t *testing.T) { origin: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []Rule{ - file2, network1, includeLocal1, dbus2, signal1, ptrace1, - capability2, file1, dbus1, unix2, signal2, mount2, + file2, network1, userns1, include1, dbus2, signal1, + ptrace1, includeLocal1, rlimit3, capability1, network2, + mqueue2, iouring2, dbus1, link2, capability2, file1, + unix2, signal2, mount2, all1, umount2, mount1, remount2, + pivotroot1, changeprofile2, }, }}, }, want: &AppArmorProfileFile{ Profiles: []*Profile{{ Rules: []Rule{ - capability2, network1, mount2, signal1, signal2, ptrace1, - unix2, dbus2, dbus1, file1, file2, includeLocal1, + include1, all1, rlimit3, userns1, capability1, capability2, + network2, network1, mount2, mount1, remount2, umount2, + pivotroot1, changeprofile2, mqueue2, iouring2, signal2, + signal1, ptrace1, unix2, dbus2, dbus1, file1, file2, + link2, includeLocal1, }, }}, }, @@ -232,6 +238,10 @@ func TestAppArmorProfileFile_Integration(t *testing.T) { tt.f.Sort() tt.f.MergeRules() tt.f.Format() + err := tt.f.Validate() + if err != nil { + t.Errorf("AppArmorProfile.Validate() = %v", err) + } if got := tt.f.String(); got != tt.want { t.Errorf("AppArmorProfile = |%v|, want |%v|", got, tt.want) } diff --git a/pkg/aa/parse.go b/pkg/aa/parse.go index 7d949e15..0e07ebef 100644 --- a/pkg/aa/parse.go +++ b/pkg/aa/parse.go @@ -35,6 +35,7 @@ var ( tok = map[Kind]string{ COMMENT: "#", VARIABLE: "@{", + HAT: "^", } openBlocks = []rune{tokOPENPAREN, tokOPENBRACE, tokOPENBRACKET} closeBlocks = []rune{tokCLOSEPAREN, tokCLOSEBRACE, tokCLOSEBRACKET} @@ -222,6 +223,8 @@ done: case strings.HasPrefix(tmp, PROFILE.Tok()): rawHeader = tmp break done + case strings.HasPrefix(tmp, HAT.String()), strings.HasPrefix(tmp, HAT.Tok()): + break done default: rawPreamble = append(rawPreamble, tmp) } From bc216176a3d49dbf71d07de1b6263faaf0fe4336 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 30 May 2024 12:28:12 +0100 Subject: [PATCH 56/62] fix: go linter issue & not defined variables. --- apparmor.d/groups/_full/default-sudo | 2 +- apparmor.d/groups/_full/systemd-service | 2 +- apparmor.d/profiles-a-f/aa-status | 2 +- pkg/aa/data_test.go | 4 +--- pkg/aa/rules.go | 14 ++++---------- pkg/logs/logs_test.go | 4 ++-- pkg/prebuild/directive/exec.go | 5 ++++- 7 files changed, 14 insertions(+), 19 deletions(-) diff --git a/apparmor.d/groups/_full/default-sudo b/apparmor.d/groups/_full/default-sudo index f5289fae..45391b5c 100644 --- a/apparmor.d/groups/_full/default-sudo +++ b/apparmor.d/groups/_full/default-sudo @@ -6,7 +6,7 @@ abi , include -profile default-sudo @{exec_path} { +profile default-sudo { include include diff --git a/apparmor.d/groups/_full/systemd-service b/apparmor.d/groups/_full/systemd-service index f475039e..6a6357ce 100644 --- a/apparmor.d/groups/_full/systemd-service +++ b/apparmor.d/groups/_full/systemd-service @@ -12,7 +12,7 @@ abi , include -profile systemd-service @{exec_path} flags=(attach_disconnected) { +profile systemd-service flags=(attach_disconnected) { include include include diff --git a/apparmor.d/profiles-a-f/aa-status b/apparmor.d/profiles-a-f/aa-status index 19886bd2..7b94ce35 100644 --- a/apparmor.d/profiles-a-f/aa-status +++ b/apparmor.d/profiles-a-f/aa-status @@ -14,7 +14,7 @@ profile aa-status @{exec_path} { capability dac_read_search, capability sys_ptrace, - ptrace (read), + ptrace read, @{exec_path} mr, diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go index 629010f2..b4e24786 100644 --- a/pkg/aa/data_test.go +++ b/pkg/aa/data_test.go @@ -20,7 +20,6 @@ var ( // Include include1 = &Include{IsMagic: true, Path: "abstraction/base"} include2 = &Include{IsMagic: false, Path: "abstraction/base"} - include3 = &Include{IfExists: true, IsMagic: true, Path: "abstraction/base"} includeLocal1 = &Include{IfExists: true, IsMagic: true, Path: "local/foo"} // Variable @@ -326,8 +325,7 @@ var ( } // Link - link3LogStr = `apparmor="ALLOWED" operation="link" class="file" profile="dolphin" name="@{user_config_dirs}/kiorc" comm="dolphin" requested_mask="l" denied_mask="l" fsuid=1000 ouid=1000 target="@{user_config_dirs}/#3954"` - link1Log = map[string]string{ + link1Log = map[string]string{ "apparmor": "ALLOWED", "operation": "link", "class": "file", diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 8f432635..5d37ef32 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -10,12 +10,6 @@ import ( "strings" ) -const ( - tokALLOW = "allow" - tokAUDIT = "audit" - tokDENY = "deny" -) - type requirement map[string][]string type constraint uint @@ -126,9 +120,9 @@ func (r Rules) Filter(filter Kind) Rules { func (r Rules) GetVariables() []*Variable { res := make([]*Variable, 0) for _, rule := range r { - switch rule.(type) { + switch rule := rule.(type) { case *Variable: - res = append(res, rule.(*Variable)) + res = append(res, rule) } } return res @@ -137,9 +131,9 @@ func (r Rules) GetVariables() []*Variable { func (r Rules) GetIncludes() []*Include { res := make([]*Include, 0) for _, rule := range r { - switch rule.(type) { + switch rule := rule.(type) { case *Include: - res = append(res, rule.(*Include)) + res = append(res, rule) } } return res diff --git a/pkg/logs/logs_test.go b/pkg/logs/logs_test.go index eb92f4ed..44dc565f 100644 --- a/pkg/logs/logs_test.go +++ b/pkg/logs/logs_test.go @@ -303,13 +303,13 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) { Rules: aa.Rules{ &aa.Unix{ RuleBase: aa.RuleBase{FileInherit: true}, - Access: []string{"receive", "send"}, + Access: []string{"send", "receive"}, Type: "stream", Protocol: "0", }, &aa.Unix{ RuleBase: aa.RuleBase{FileInherit: true}, - Access: []string{"receive", "send"}, + Access: []string{"send", "receive"}, Type: "stream", Protocol: "0", }, diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index 0a8caf2b..214c51b2 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -42,7 +42,10 @@ func (d Exec) Apply(opt *Option, profileRaw string) (string, error) { for name := range opt.ArgMap { profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name)) dstProfile := aa.DefaultTunables() - dstProfile.Parse(profiletoTransition) + err := dstProfile.Parse(profiletoTransition) + if err != nil { + return "", err + } for _, variable := range dstProfile.Preamble.GetVariables() { if variable.Name == "exec_path" { for _, v := range variable.Values { From 992cab1fa49c4a14219393a138d083e92c5833f2 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 30 May 2024 12:32:30 +0100 Subject: [PATCH 57/62] feat(aa): move conversion function to its own file & add unit tests. --- pkg/aa/convert.go | 120 +++++++++++++++++++++++++++++++++++++++++ pkg/aa/convert_test.go | 92 +++++++++++++++++++++++++++++++ pkg/aa/file.go | 17 ------ pkg/aa/rules.go | 90 ------------------------------- 4 files changed, 212 insertions(+), 107 deletions(-) create mode 100644 pkg/aa/convert.go create mode 100644 pkg/aa/convert_test.go diff --git a/pkg/aa/convert.go b/pkg/aa/convert.go new file mode 100644 index 00000000..b78dc00b --- /dev/null +++ b/pkg/aa/convert.go @@ -0,0 +1,120 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "fmt" + "slices" + "strings" +) + +// 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 +} + +// 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[FILE]["access"], i) && + slices.Contains(requirements[FILE]["access"], j) { + return requirementsWeights[FILE]["access"][i] - requirementsWeights[FILE]["access"][j] + } + if slices.Contains(requirements[FILE]["transition"], i) && + slices.Contains(requirements[FILE]["transition"], j) { + return requirementsWeights[FILE]["transition"][i] - requirementsWeights[FILE]["transition"][j] + } + if slices.Contains(requirements[FILE]["access"], i) { + return -1 + } + return 1 +} + +func validateValues(kind Kind, key string, values []string) error { + for _, v := range values { + if v == "" { + continue + } + if !slices.Contains(requirements[kind][key], v) { + return fmt.Errorf("invalid mode '%s'", v) + } + } + return nil +} + +// 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(kind Kind, key string, input string) ([]string, error) { + req, ok := requirements[kind][key] + if !ok { + return nil, fmt.Errorf("unrecognized requirement '%s' for rule %s", key, kind) + } + + res := tokenToSlice(input) + for idx := range res { + res[idx] = strings.Trim(res[idx], `" `) + if res[idx] == "" { + res = slices.Delete(res, idx, idx+1) + continue + } + if !slices.Contains(req, res[idx]) { + return nil, fmt.Errorf("unrecognized %s: %s", key, res[idx]) + } + } + slices.SortFunc(res, func(i, j string) int { + return requirementsWeights[kind][key][i] - requirementsWeights[kind][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(kind Kind, input string) ([]string, error) { + var res []string + + switch kind { + case FILE: + raw := strings.Split(input, "") + trans := []string{} + for _, access := range raw { + if slices.Contains(requirements[FILE]["access"], access) { + res = append(res, access) + } else { + trans = append(trans, access) + } + } + + transition := strings.Join(trans, "") + if len(transition) > 0 { + if slices.Contains(requirements[FILE]["transition"], transition) { + res = append(res, transition) + } else { + return nil, fmt.Errorf("unrecognized transition: %s", transition) + } + } + + case FILE + "-log": + raw := strings.Split(input, "") + for _, access := range raw { + if slices.Contains(requirements[FILE]["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' for %s", input, kind) + } + } + + default: + return toValues(kind, "access", input) + } + + slices.SortFunc(res, cmpFileAccess) + return slices.Compact(res), nil +} diff --git a/pkg/aa/convert_test.go b/pkg/aa/convert_test.go new file mode 100644 index 00000000..8a027ffa --- /dev/null +++ b/pkg/aa/convert_test.go @@ -0,0 +1,92 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "reflect" + "testing" + + "github.com/k0kubun/pp/v3" +) + +func Test_toAccess(t *testing.T) { + tests := []struct { + name string + kind Kind + inputs []string + wants [][]string + wantsErr []bool + }{ + { + name: "empty", + kind: FILE, + inputs: []string{""}, + wants: [][]string{nil}, + wantsErr: []bool{false}, + }, + { + name: "file", + kind: FILE, + inputs: []string{ + "rPx", "rPUx", "mr", "rm", "rix", "rcx", "rCUx", "rmix", "rwlk", + "mrwkl", "", "r", "x", "w", "wr", "px", "Px", "Ux", "mrwlkPix", + }, + wants: [][]string{ + {"r", "Px"}, {"r", "PUx"}, {"m", "r"}, {"m", "r"}, {"r", "ix"}, + {"r", "cx"}, {"r", "CUx"}, {"m", "r", "ix"}, {"r", "w", "l", "k"}, + {"m", "r", "w", "l", "k"}, nil, {"r"}, {"x"}, {"w"}, {"r", "w"}, + {"px"}, {"Px"}, {"Ux"}, {"m", "r", "w", "l", "k", "Pix"}, + }, + wantsErr: []bool{ + false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, + }, + }, + { + name: "file-log", + kind: FILE + "-log", + inputs: []string{ + "mr", "rm", "x", "rwlk", "mrwkl", "r", "c", "wc", "d", "wr", + }, + wants: [][]string{ + {"m", "r"}, {"m", "r"}, {"ix"}, {"r", "w", "l", "k"}, + {"m", "r", "w", "l", "k"}, {"r"}, {"w"}, {"w"}, {"w"}, {"r", "w"}, + }, + wantsErr: []bool{ + false, false, false, false, false, false, false, false, false, false, + }, + }, + { + name: "signal", + kind: SIGNAL, + inputs: []string{"send receive rw"}, + wants: [][]string{{"rw", "send", "receive"}}, + wantsErr: []bool{false}, + }, + { + name: "ptrace", + kind: PTRACE, + inputs: []string{"readby", "tracedby", "read readby", "r w", "rw", ""}, + wants: [][]string{ + {"readby"}, {"tracedby"}, {"read", "readby"}, {"r", "w"}, {"rw"}, {}, + }, + wantsErr: []bool{false, false, false, false, false, false}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for i, input := range tt.inputs { + got, err := toAccess(tt.kind, input) + if (err != nil) != tt.wantsErr[i] { + t.Errorf("toAccess() error = %v, wantErr %v", err, tt.wantsErr[i]) + return + } + if !reflect.DeepEqual(got, tt.wants[i]) { + t.Errorf("toAccess() = %v, want %v", pp.Sprint(got), pp.Sprint(tt.wants[i])) + } + } + }) + } +} diff --git a/pkg/aa/file.go b/pkg/aa/file.go index dd828951..d1ea214a 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -37,23 +37,6 @@ func isOwner(log map[string]string) bool { return false } -// 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[FILE]["access"], i) && - slices.Contains(requirements[FILE]["access"], j) { - return requirementsWeights[FILE]["access"][i] - requirementsWeights[FILE]["access"][j] - } - if slices.Contains(requirements[FILE]["transition"], i) && - slices.Contains(requirements[FILE]["transition"], j) { - return requirementsWeights[FILE]["transition"][i] - requirementsWeights[FILE]["transition"][j] - } - if slices.Contains(requirements[FILE]["access"], i) { - return -1 - } - return 1 -} - type File struct { RuleBase Qualifier diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 5d37ef32..7aeb9752 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -5,9 +5,7 @@ package aa import ( - "fmt" "slices" - "strings" ) type requirement map[string][]string @@ -237,91 +235,3 @@ func (r Rules) Format() Rules { } return r } - -// 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 -} - -func validateValues(kind Kind, key string, values []string) error { - for _, v := range values { - if v == "" { - continue - } - if !slices.Contains(requirements[kind][key], v) { - return fmt.Errorf("invalid mode '%s'", v) - } - } - return nil -} - -// 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(kind Kind, key string, input string) ([]string, error) { - req, ok := requirements[kind][key] - if !ok { - return nil, fmt.Errorf("unrecognized requirement '%s' for rule %s", key, kind) - } - - res := tokenToSlice(input) - for idx := range res { - res[idx] = strings.Trim(res[idx], `" `) - if !slices.Contains(req, res[idx]) { - return nil, fmt.Errorf("unrecognized %s: %s", key, res[idx]) - } - } - slices.SortFunc(res, func(i, j string) int { - return requirementsWeights[kind][key][i] - requirementsWeights[kind][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(kind Kind, input string) ([]string, error) { - var res []string - - switch kind { - case FILE: - raw := strings.Split(input, "") - trans := []string{} - for _, access := range raw { - if slices.Contains(requirements[FILE]["access"], access) { - res = append(res, access) - } else { - trans = append(trans, access) - } - } - - transition := strings.Join(trans, "") - if len(transition) > 0 { - if slices.Contains(requirements[FILE]["transition"], transition) { - res = append(res, transition) - } else { - return nil, fmt.Errorf("unrecognized transition: %s", transition) - } - } - - case FILE + "-log": - raw := strings.Split(input, "") - for _, access := range raw { - if slices.Contains(requirements[FILE]["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(kind, "access", input) - } - - slices.SortFunc(res, cmpFileAccess) - return slices.Compact(res), nil -} From 7efa4b3a4bf66ac804798e01d5b1e64f6876b672 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 30 May 2024 12:34:10 +0100 Subject: [PATCH 58/62] feat(aa): improve log conversion. --- pkg/aa/profile.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 5296d022..44d53a32 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -189,6 +189,10 @@ var ( return newFileFromLog(log) } }, + "exec": newFileFromLog, + "file_inherit": newFileFromLog, + "file_perm": newFileFromLog, + "open": newFileFromLog, } newLogMountMap = map[string]func(log map[string]string) Rule{ "mount": newMountFromLog, @@ -214,15 +218,20 @@ func (p *Profile) AddRule(log map[string]string) { default: } - if newRule, ok := newLogMap[log["class"]]; ok { - p.Rules = append(p.Rules, newRule(log)) - } else { + done := false + for _, key := range []string{"class", "family", "operation"} { + if newRule, ok := newLogMap[log[key]]; ok { + p.Rules = append(p.Rules, newRule(log)) + done = true + break + } + } + + if !done { if strings.Contains(log["operation"], "dbus") { p.Rules = append(p.Rules, newDbusFromLog(log)) - } else if log["family"] == "unix" { - p.Rules = append(p.Rules, newUnixFromLog(log)) } else { - panic("unknown class: " + log["class"]) + fmt.Printf("unknown log type: %s", log) } } } From cfd4786f7660a8c8969263fa9a6953ab1b6d2cfd Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 30 May 2024 13:10:07 +0100 Subject: [PATCH 59/62] chore: cleanup unit test. --- pkg/aa/convert_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/aa/convert_test.go b/pkg/aa/convert_test.go index 8a027ffa..fc2436be 100644 --- a/pkg/aa/convert_test.go +++ b/pkg/aa/convert_test.go @@ -7,8 +7,6 @@ package aa import ( "reflect" "testing" - - "github.com/k0kubun/pp/v3" ) func Test_toAccess(t *testing.T) { @@ -84,7 +82,7 @@ func Test_toAccess(t *testing.T) { return } if !reflect.DeepEqual(got, tt.wants[i]) { - t.Errorf("toAccess() = %v, want %v", pp.Sprint(got), pp.Sprint(tt.wants[i])) + t.Errorf("toAccess() = %v, want %v", got, tt.wants[i]) } } }) From fd46c0de30bdfd624259142c4aa4d21b7993baa4 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 30 May 2024 14:18:57 +0100 Subject: [PATCH 60/62] fix: userspace prebuild test. --- pkg/prebuild/builder/core_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/prebuild/builder/core_test.go b/pkg/prebuild/builder/core_test.go index 838d63df..c242259f 100644 --- a/pkg/prebuild/builder/core_test.go +++ b/pkg/prebuild/builder/core_test.go @@ -7,6 +7,8 @@ package builder import ( "slices" "testing" + + "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" ) func TestBuilder_Apply(t *testing.T) { @@ -216,7 +218,7 @@ func TestBuilder_Apply(t *testing.T) { }`, }, { - name: "userspace-1", + name: "userspace-2", b: Builders["userspace"], profile: ` profile foo /usr/bin/foo { @@ -238,7 +240,7 @@ func TestBuilder_Apply(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - opt := &Option{} + opt := &Option{File: cfg.RootApparmord.Join(tt.name)} got, err := tt.b.Apply(opt, tt.profile) if (err != nil) != tt.wantErr { t.Errorf("Builder.Apply() error = %v, wantErr %v", err, tt.wantErr) @@ -264,7 +266,6 @@ func TestRegister(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { Register(tt.names...) for _, name := range tt.names { From 264f30cf12e3273ce6d77e53e8d90d67b96ad9d3 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 30 May 2024 14:19:56 +0100 Subject: [PATCH 61/62] chore(aa): cosmetic. --- pkg/aa/profile.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 44d53a32..21181378 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -139,11 +139,12 @@ func (p *Profile) Format() { // GetAttachments return a nested attachment string func (p *Profile) GetAttachments() string { - if len(p.Attachments) == 0 { + switch len(p.Attachments) { + case 0: return "" - } else if len(p.Attachments) == 1 { + case 1: return p.Attachments[0] - } else { + default: res := []string{} for _, attachment := range p.Attachments { if strings.HasPrefix(attachment, "/") { From 7f1de3626efdb6f378118bee3825e853841f409a Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 30 May 2024 14:23:56 +0100 Subject: [PATCH 62/62] feat(aa): handle appending value to defined variables. --- pkg/aa/resolve.go | 15 +++++++++ pkg/aa/resolve_test.go | 50 +++++++++++++++++++++-------- pkg/prebuild/directive/exec.go | 6 ++-- pkg/prebuild/directive/exec_test.go | 8 ++--- 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/pkg/aa/resolve.go b/pkg/aa/resolve.go index ec640de3..1b843f8b 100644 --- a/pkg/aa/resolve.go +++ b/pkg/aa/resolve.go @@ -29,6 +29,21 @@ func (f *AppArmorProfileFile) Resolve() error { // } // } + // Append value to variable + seen := map[string]*Variable{} + for idx, variable := range f.Preamble.GetVariables() { + if _, ok := seen[variable.Name]; ok { + if variable.Define { + return fmt.Errorf("variable %s already defined", variable.Name) + } + seen[variable.Name].Values = append(seen[variable.Name].Values, variable.Values...) + f.Preamble = f.Preamble.Delete(idx) + } + if variable.Define { + seen[variable.Name] = variable + } + } + // Resolve variables for _, variable := range f.Preamble.GetVariables() { newValues := []string{} diff --git a/pkg/aa/resolve_test.go b/pkg/aa/resolve_test.go index 8e4ff6b8..8b0a5597 100644 --- a/pkg/aa/resolve_test.go +++ b/pkg/aa/resolve_test.go @@ -110,14 +110,37 @@ func TestAppArmorProfileFile_resolveValues(t *testing.T) { func TestAppArmorProfileFile_Resolve(t *testing.T) { tests := []struct { name string - variables Rules + preamble Rules attachements []string want *AppArmorProfileFile wantErr bool }{ { - name: "firefox", - variables: Rules{ + name: "variables/append", + preamble: Rules{ + &Variable{Name: "lib", Values: []string{"/{usr/,}lib"}, Define: true}, + &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"@{lib}/DiscoverNotifier"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"@{lib}/@{multiarch}/{,libexec/}DiscoverNotifier"}, Define: false}, + }, + want: &AppArmorProfileFile{ + Preamble: Rules{ + &Variable{Name: "lib", Values: []string{"/{usr/,}lib"}, Define: true}, + &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, + &Variable{ + Name: "exec_path", Define: true, + Values: []string{ + "/{usr/,}lib/DiscoverNotifier", + "/{usr/,}lib/*-linux-gnu*/{,libexec/}DiscoverNotifier", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "attachment/firefox", + preamble: Rules{ &Variable{Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}, Define: true}, &Variable{Name: "firefox_lib_dirs", Values: []string{"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}, Define: true}, &Variable{Name: "exec_path", Values: []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}, Define: true}, @@ -155,8 +178,8 @@ func TestAppArmorProfileFile_Resolve(t *testing.T) { wantErr: false, }, { - name: "chromium", - variables: Rules{ + name: "attachment/chromium", + preamble: Rules{ &Variable{Name: "name", Values: []string{"chromium"}, Define: true}, &Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{name}"}, Define: true}, &Variable{Name: "path", Values: []string{"@{lib_dirs}/@{name}"}, Define: true}, @@ -177,8 +200,8 @@ func TestAppArmorProfileFile_Resolve(t *testing.T) { wantErr: false, }, { - name: "geoclue", - variables: Rules{ + name: "attachment/geoclue", + preamble: Rules{ &Variable{Name: "libexec", Values: []string{"/{usr/,}libexec"}, Define: true}, &Variable{Name: "exec_path", Values: []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}, Define: true}, }, @@ -206,8 +229,8 @@ func TestAppArmorProfileFile_Resolve(t *testing.T) { wantErr: false, }, { - name: "opera", - variables: Rules{ + name: "attachment/opera", + preamble: Rules{ &Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true}, &Variable{Name: "name", Values: []string{"opera{,-beta,-developer}"}, Define: true}, &Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{multiarch}/@{name}"}, Define: true}, @@ -234,12 +257,11 @@ func TestAppArmorProfileFile_Resolve(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := &AppArmorProfileFile{ - Profiles: []*Profile{{ - Header: Header{Attachments: tt.attachements}, - }}, + got := &AppArmorProfileFile{Preamble: tt.preamble} + if tt.attachements != nil { + got.Profiles = append(got.Profiles, &Profile{Header: Header{Attachments: tt.attachements}}) } - got.Preamble = tt.variables + if err := got.Resolve(); (err != nil) != tt.wantErr { t.Errorf("AppArmorProfileFile.Resolve() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/pkg/prebuild/directive/exec.go b/pkg/prebuild/directive/exec.go index 214c51b2..3bf89168 100644 --- a/pkg/prebuild/directive/exec.go +++ b/pkg/prebuild/directive/exec.go @@ -42,8 +42,10 @@ func (d Exec) Apply(opt *Option, profileRaw string) (string, error) { for name := range opt.ArgMap { profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name)) dstProfile := aa.DefaultTunables() - err := dstProfile.Parse(profiletoTransition) - if err != nil { + if err := dstProfile.Parse(profiletoTransition); err != nil { + return "", err + } + if err := dstProfile.Resolve(); err != nil { return "", err } for _, variable := range dstProfile.Preamble.GetVariables() { diff --git a/pkg/prebuild/directive/exec_test.go b/pkg/prebuild/directive/exec_test.go index f21544c0..cd363bbc 100644 --- a/pkg/prebuild/directive/exec_test.go +++ b/pkg/prebuild/directive/exec_test.go @@ -31,8 +31,8 @@ func TestExec_Apply(t *testing.T) { Raw: " #aa:exec DiscoverNotifier", }, profile: ` #aa:exec DiscoverNotifier`, - want: ` @{lib}/@{multiarch}/{,libexec/}DiscoverNotifier Px, - @{lib}/DiscoverNotifier Px,`, + want: ` /{,usr/}lib{,exec,32,64}/*-linux-gnu*/{,libexec/}DiscoverNotifier Px, + /{,usr/}lib{,exec,32,64}/DiscoverNotifier Px,`, }, { name: "exec-unconfined", @@ -45,8 +45,8 @@ func TestExec_Apply(t *testing.T) { Raw: " #aa:exec U polkit-agent-helper", }, profile: ` #aa:exec U polkit-agent-helper`, - want: ` @{lib}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux, - @{lib}/polkit-agent-helper-[0-9] Ux,`, + want: ` /{,usr/}lib{,exec,32,64}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux, + /{,usr/}lib{,exec,32,64}/polkit-agent-helper-[0-9] Ux,`, }, } for _, tt := range tests {