From a5b6373b02d995b2a3ac9f7aa909fc7e19b0664e Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Mon, 25 Sep 2023 00:22:41 +0100 Subject: [PATCH] test(aa-log): add unit tests for profile rules. --- pkg/aa/data_test.go | 268 +++++++++++++++++++++++++++++++++++++++++ pkg/aa/profile_test.go | 155 +++++++++++++++++++++++- pkg/aa/rules_test.go | 225 ++++++++++++++++++++++++++++++++++ 3 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 pkg/aa/data_test.go create mode 100644 pkg/aa/rules_test.go diff --git a/pkg/aa/data_test.go b/pkg/aa/data_test.go new file mode 100644 index 00000000..e25c78fe --- /dev/null +++ b/pkg/aa/data_test.go @@ -0,0 +1,268 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +var ( + // Capability + capability1Log = map[string]string{ + "apparmor": "ALLOWED", + "class": "cap", + "operation": "capable", + "capname": "net_admin", + "capability": "12", + "profile": "pkexec", + "comm": "pkexec", + } + capability1 = &Capability{ + Qualifier: Qualifier{}, + Name: "net_admin", + } + capability2 = &Capability{ + Qualifier: Qualifier{}, + Name: "sys_ptrace", + } + + // Network + network1Log = map[string]string{ + "apparmor": "ALLOWED", + "class": "net", + "operation": "create", + "family": "netlink", + "profile": "sddm-greeter", + "sock_type": "raw", + "protocol": "15", + "requested_mask": "create", + "denied_mask": "create", + "comm": "sddm-greeter", + } + network1 = &Network{ + Qualifier: Qualifier{}, + Domain: "netlink", + Type: "raw", + Protocol: "15", + AddressExpr: AddressExpr{}, + } + network2 = &Network{ + Qualifier: Qualifier{}, + Domain: "inet", + Type: "dgram", + Protocol: "", + AddressExpr: AddressExpr{}, + } + + // Mount + mount1Log = map[string]string{ + "apparmor": "ALLOWED", + "class": "mount", + "operation": "mount", + "info": "failed perms check", + "error": "-13", + "profile": "dockerd", + "name": "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/", + "comm": "dockerd", + "fstype": "overlay", + "srcname": "overlay", + } + mount2Log = map[string]string{ + "apparmor": "ALLOWED", + "class": "mount", + "operation": "mount", + "info": "failed perms check", + "error": "-13", + "profile": "dockerd", + "name": "/var/lib/docker/overlay2/metacopy-check906831159/merged/", + "comm": "dockerd", + "fstype": "overlay", + "srcname": "overlay", + } + mount1 = &Mount{ + Qualifier: Qualifier{}, + MountConditions: MountConditions{ + Fs: "", + Op: "", + FsType: "overlay", + Options: []string{}, + }, + Source: "overlay", + MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/", + } + mount2 = &Mount{ + Qualifier: Qualifier{}, + MountConditions: MountConditions{ + Fs: "", + Op: "", + FsType: "overlay", + Options: []string{}, + }, + Source: "overlay", + MountPoint: "/var/lib/docker/overlay2/metacopy-check906831159/merged/", + } + + // Signal + signal1Log = map[string]string{ + "apparmor": "ALLOWED", + "class": "signal", + "profile": "firefox", + "operation": "signal", + "comm": "49504320492F4F20506172656E74", + "requested_mask": "receive", + "denied_mask": "receive", + "signal": "kill", + "peer": "firefox//&firejail-default", + } + signal1 = &Signal{ + Qualifier: Qualifier{}, + Access: "receive", + Set: "kill", + Peer: "firefox//&firejail-default", + } + signal2 = &Signal{ + Qualifier: Qualifier{}, + Access: "receive", + Set: "up", + Peer: "firefox//&firejail-default", + } + + // Ptrace + ptrace1Log = map[string]string{ + "apparmor": "ALLOWED", + "class": "ptrace", + "profile": "xdg-document-portal", + "operation": "ptrace", + "comm": "pool-/usr/lib/x", + "requested_mask": "read", + "denied_mask": "read", + "peer": "nautilus", + } + ptrace2Log = map[string]string{ + "apparmor": "DENIED", + "class": "ptrace", + "operation": "ptrace", + "comm": "systemd-journal", + "requested_mask": "readby", + "denied_mask": "readby", + "peer": "systemd-journald", + } + ptrace1 = &Ptrace{ + Qualifier: Qualifier{}, + Access: "read", + Peer: "nautilus", + } + ptrace2 = &Ptrace{ + Qualifier: Qualifier{}, + Access: "readby", + Peer: "systemd-journald", + } + + // Unix + unix1Log = map[string]string{ + "apparmor": "ALLOWED", + "class": "net", + "family": "unix", + "operation": "file_perm", + "profile": "gsettings", + "comm": "dbus-daemon", + "requested_mask": "send receive", + "addr": "none", + "peer_addr": "@/tmp/dbus-AaKMpxzC4k", + "peer": "dbus-daemon", + "denied_mask": "send receive", + "sock_type": "stream", + "protocol": "0", + } + unix1 = &Unix{ + Access: "send receive", + Type: "stream", + Protocol: "0", + Address: "none", + Peer: "dbus-daemon", + PeerAddr: "@/tmp/dbus-AaKMpxzC4k", + } + unix2 = &Unix{ + Qualifier: Qualifier{FileInherit: true}, + Access: "receive", + Type: "stream", + } + + // Dbus + dbus1Log = map[string]string{ + "apparmor": "ALLOWED", + "operation": "dbus_method_call", + "bus": "session", + "path": "/org/gtk/vfs/metadata", + "interface": "org.gtk.vfs.Metadata", + "member": "Remove", + "name": ":1.15", + "mask": "receive", + "label": "gvfsd-metadata", + "peer_pid": "3888", + "peer_label": "tracker-extract", + } + dbus2Log = map[string]string{ + "apparmor": "ALLOWED", + "operation": "dbus_bind", + "bus": "session", + "name": "org.gnome.evolution.dataserver.Sources5", + "mask": "bind", + "pid": "3442", + "label": "evolution-source-registry", + } + dbus1 = &Dbus{ + Access: "receive", + Bus: "session", + Name: ":1.15", + Path: "/org/gtk/vfs/metadata", + Interface: "org.gtk.vfs.Metadata", + Member: "Remove", + Label: "tracker-extract", + } + dbus2 = &Dbus{ + Access: "bind", + Bus: "session", + Name: "org.gnome.evolution.dataserver.Sources5", + } + + // File + file1Log = map[string]string{ + "apparmor": "ALLOWED", + "operation": "open", + "class": "file", + "profile": "cupsd", + "name": "/usr/share/poppler/cMap/Identity-H", + "comm": "gs", + "requested_mask": "r", + "denied_mask": "r", + "fsuid": "209", + "FSUID": "cups", + "ouid": "0", + "OUID": "root", + } + file2Log = map[string]string{ + "apparmor": "ALLOWED", + "operation": "open", + "class": "file", + "profile": "gsd-print-notifications", + "name": "/proc/4163/cgroup", + "comm": "gsd-print-notif", + "requested_mask": "r", + "denied_mask": "r", + "fsuid": "1000", + "FSUID": "user", + "ouid": "1000", + "OUID": "user", + "error": "-1", + } + file1 = &File{ + Path: "/usr/share/poppler/cMap/Identity-H", + Access: "r", + Target: "", + } + file2 = &File{ + Qualifier: Qualifier{Owner: true, NoNewPrivs: true}, + Path: "/proc/4163/cgroup", + Access: "r", + Target: "", + } +) diff --git a/pkg/aa/profile_test.go b/pkg/aa/profile_test.go index db3ec359..99f1a065 100644 --- a/pkg/aa/profile_test.go +++ b/pkg/aa/profile_test.go @@ -25,7 +25,160 @@ 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 { - t.Errorf("AppArmorProfile.String() = %v, want %v", got, tt.want) + t.Errorf("AppArmorProfile.String() = |%v|, want |%v|", got, tt.want) + } + }) + } +} + +func TestAppArmorProfile_AddRule(t *testing.T) { + tests := []struct { + name string + log map[string]string + want *AppArmorProfile + }{ + { + name: "capability", + log: capability1Log, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{capability1}, + }, + }, + }, + { + name: "network", + log: network1Log, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{network1}, + }, + }, + }, + { + name: "mount", + log: mount2Log, + want: &AppArmorProfile{ + Profile: Profile{ + Flags: []string{"attach_disconnected"}, + Rules: []ApparmorRule{mount2}, + }, + }, + }, + { + name: "signal", + log: signal1Log, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{signal1}, + }, + }, + }, + { + name: "ptrace", + log: ptrace2Log, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{ptrace2}, + }, + }, + }, + { + name: "unix", + log: unix1Log, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{unix1}, + }, + }, + }, + { + name: "dbus", + log: dbus2Log, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{dbus2}, + }, + }, + }, + { + name: "file", + log: file2Log, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{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) { + tests := []struct { + name string + origin *AppArmorProfile + want *AppArmorProfile + }{ + { + name: "all", + origin: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{file2, network1, dbus2, signal1, ptrace1, capability2, file1, dbus1, unix2, signal2, mount2}, + }, + }, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{capability2, network1, mount2, signal1, signal2, ptrace1, unix2, dbus2, dbus1, file2, file1}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.origin + got.Sort() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AppArmorProfile.Sort() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAppArmorProfile_MergeRules(t *testing.T) { + tests := []struct { + name string + origin *AppArmorProfile + want *AppArmorProfile + }{ + { + name: "all", + origin: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{capability1, capability1, network1, network1, file1, file1}, + }, + }, + want: &AppArmorProfile{ + Profile: Profile{ + Rules: []ApparmorRule{capability1, network1, file1}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.origin + got.MergeRules() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AppArmorProfile.MergeRules() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go new file mode 100644 index 00000000..60cb8fe9 --- /dev/null +++ b/pkg/aa/rules_test.go @@ -0,0 +1,225 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "reflect" + "testing" +) + +func TestRule_FromLog(t *testing.T) { + tests := []struct { + name string + fromLog func(map[string]string, bool, bool) ApparmorRule + log map[string]string + want ApparmorRule + }{ + { + name: "capbability", + fromLog: CapabilityFromLog, + log: capability1Log, + want: capability1, + }, + { + name: "network", + fromLog: NetworkFromLog, + log: network1Log, + want: network1, + }, + { + name: "mount", + fromLog: MountFromLog, + log: mount1Log, + want: mount1, + }, + { + name: "signal", + fromLog: SignalFromLog, + log: signal1Log, + want: signal1, + }, + { + name: "ptrace/xdg-document-portal", + fromLog: PtraceFromLog, + log: ptrace1Log, + want: ptrace1, + }, + { + name: "ptrace/snap-update-ns.firefox", + fromLog: PtraceFromLog, + log: ptrace2Log, + want: ptrace2, + }, + { + name: "unix", + fromLog: UnixFromLog, + log: unix1Log, + want: unix1, + }, + { + name: "dbus", + fromLog: DbusFromLog, + log: dbus1Log, + want: dbus1, + }, + { + name: "file", + fromLog: FileFromLog, + log: file1Log, + want: file1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.fromLog(tt.log, false, false); !reflect.DeepEqual(got, tt.want) { + t.Errorf("RuleFromLog() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRule_Less(t *testing.T) { + tests := []struct { + name string + rule ApparmorRule + other ApparmorRule + want bool + }{ + { + 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: "signal", + rule: signal1, + other: signal2, + want: true, + }, + { + name: "ptrace/less", + rule: ptrace1, + other: ptrace2, + want: true, + }, + { + 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: "file", + rule: file1, + other: file2, + want: false, + }, + } + for _, tt := range tests { + 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) + } + }) + } +} + +func TestRule_Equals(t *testing.T) { + tests := []struct { + name string + rule ApparmorRule + other ApparmorRule + want bool + }{ + { + name: "capability/equal", + rule: capability1, + other: capability1, + want: true, + }, + { + name: "network/equal", + rule: network1, + other: network1, + want: true, + }, + { + name: "mount", + rule: mount1, + other: mount1, + want: true, + }, + { + 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, + }, + } + for _, tt := range tests { + 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) + } + }) + } +}