From e91b0cc07005aa378ebb95d84fe28a4dac5b5e4c Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Wed, 19 Jun 2024 23:26:19 +0100 Subject: [PATCH] test(aa): add paragraph parser unit tests. --- pkg/aa/parse_test.go | 1005 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 973 insertions(+), 32 deletions(-) diff --git a/pkg/aa/parse_test.go b/pkg/aa/parse_test.go index eae45a06..a1016b19 100644 --- a/pkg/aa/parse_test.go +++ b/pkg/aa/parse_test.go @@ -6,141 +6,540 @@ package aa import ( "reflect" + "strings" "testing" "github.com/roddhjav/apparmor.d/pkg/util" ) func Test_tokenizeRule(t *testing.T) { - for _, tt := range testTokenRules { + inHeader = true + for _, tt := range testRules { t.Run(tt.name, func(t *testing.T) { - if got := tokenize(tt.raw); !reflect.DeepEqual(got, tt.tokens) { + if got := tokenizeRule(tt.raw); !reflect.DeepEqual(got, tt.tokens) { t.Errorf("tokenize() = %v, want %v", got, tt.tokens) } }) } } +func Test_parseRule(t *testing.T) { + inHeader = true + for _, tt := range testRules { + t.Run(tt.name, func(t *testing.T) { + if got := parseRule(tt.raw); !reflect.DeepEqual(got, tt.rule) { + t.Errorf("parseRule() = %v, want %v", got, tt.rule) + } + }) + } +} + +func Test_rule_Getter(t *testing.T) { + for _, tt := range testRules { + t.Run(tt.name, func(t *testing.T) { + if tt.wGetAsMap == nil { + tt.wGetAsMap = map[string][]string{} + } + if tt.wGetSlice == nil { + tt.wGetSlice = []string{} + } + + if tt.getIdx > 0 { + if got := tt.rule.Get(tt.getIdx); got != tt.wGet { + t.Errorf("rule.Get() = %v, want %v", got, tt.wGet) + } + } + + if got := tt.rule.GetString(); got != tt.wGetString { + t.Errorf("rule.GetString() = %v, want %v", got, tt.wGetString) + } + if got := tt.rule.GetSlice(); !reflect.DeepEqual(got, tt.wGetSlice) { + t.Errorf("rule.GetSlice() = %v, want %v", got, tt.wGetSlice) + } + if got := tt.rule.GetAsMap(); !reflect.DeepEqual(got, tt.wGetAsMap) { + t.Errorf("rule.GetAsMap() = %v, want %v", got, tt.wGetAsMap) + } + + if tt.getKey != "" { + if got := tt.rule.GetValues(tt.getKey); !reflect.DeepEqual(got, tt.wGetValues) { + t.Errorf("rule.GetValues() = %v, want %v", got, tt.wGetValues) + } + if got := tt.rule.GetValuesAsSlice(tt.getKey); !reflect.DeepEqual(got, tt.wGetValuesAsSlice) { + t.Errorf("rule.GetValuesAsSlice() = %v, want %v", got, tt.wGetValuesAsSlice) + } + if got := tt.rule.GetValuesAsString(tt.getKey); got != tt.wGetValuesAsString { + t.Errorf("rule.GetValuesAsString() = %v, want %v", got, tt.wGetValuesAsString) + } + } + + if got := tt.rule.String(); got != tt.wString { + t.Errorf("rule.String() = |%v|, want |%v|", got, tt.wString) + } + }) + } +} + +func Test_parseLineRules(t *testing.T) { + for _, tt := range testsLineRules { + t.Run(tt.name, func(t *testing.T) { + got, rules, err := parseLineRules(tt.isPreamble, tt.raw) + if (err != nil) != tt.wantErr { + t.Errorf("parseLineRules() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parseLineRules() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(rules, tt.rules) { + t.Errorf("parseLineRules() rules = %v, want %v", rules, tt.rules) + } + }) + } +} + +func Test_parseCommaRules(t *testing.T) { + for _, tt := range testsCommaRules { + t.Run(tt.name, func(t *testing.T) { + got, err := parseCommaRules(tt.raw) + if (err != nil) != tt.wantErr { + t.Errorf("parseCommaRules() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.rule) { + t.Errorf("parseCommaRules() = %v, want %v", got, tt.rule) + } + }) + } +} + +func Test_newRules(t *testing.T) { + for _, tt := range testRules { + if tt.wRule == nil { + continue + } + t.Run(tt.name, func(t *testing.T) { + got, err := newRules([]rule{tt.rule}) + if (err != nil) != tt.wError { + t.Errorf("newRules() error = %v, wantErr %v", err, tt.wError) + return + } + if len(got) == 0 { + return + } + if !reflect.DeepEqual(got[0], tt.wRule) { + t.Errorf("newRules() = %v, want %v", got[0], tt.wRule) + } + }) + } +} + 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 { + nb, err := got.Parse(tt.raw) + if (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) } + raw := strings.Join(strings.Split(tt.raw, "\n")[nb:], "\n") + gotRules, _, err := ParseRules(raw) + if (err != nil) != tt.wParseRulesErr { + t.Errorf("ParseRules() error = %v, wantErr %v", err, tt.wParseRulesErr) + return + } + if !reflect.DeepEqual(gotRules, tt.wRules) { + t.Errorf("ParseRules() got = %v, want %v", gotRules, tt.wRules) + } }) } } var ( - // Test cases for tokenize - testTokenRules = []struct { - name string - raw string - tokens []string + // Test cases for tokenizeRule, parseRule,rule getters, and newRules + testRules = []struct { + name string + raw string + tokens []string + rule rule + token []string + getIdx int // index of the rule to get + getKey string // key to get + wGet string + wGetString string + wGetSlice []string + wGetAsMap map[string][]string + wGetValues rule + wGetValuesAsSlice []string + wGetValuesAsString string + wString string + wRule Rule + wError bool }{ { - name: "empty", - raw: "", - tokens: []string{}, + name: "empty", + raw: "", + tokens: []string{}, + rule: rule{}, + wString: "", + wError: true, }, { name: "abi", raw: `abi `, tokens: []string{"abi", ""}, + rule: rule{ + {key: "abi"}, {key: ""}, + }, + getIdx: 1, + wGet: "", + wGetString: "abi ", + wGetSlice: []string{"abi", ""}, + wString: "abi ", + wRule: &Abi{IsMagic: true, Path: "abi/4.0"}, }, { name: "alias", raw: `alias /mnt/usr -> /usr`, tokens: []string{"alias", "/mnt/usr", "->", "/usr"}, + rule: rule{ + {key: "alias"}, {key: "/mnt/usr"}, {key: "->"}, {key: "/usr"}, + }, + getIdx: 2, + wGet: "->", + wGetString: "alias /mnt/usr -> /usr", + wGetSlice: []string{"alias", "/mnt/usr", "->", "/usr"}, + wString: "alias /mnt/usr -> /usr", + wRule: &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"}, }, { - name: "variable", + name: "variable-1", raw: `@{name} = torbrowser "tor browser"`, tokens: []string{"@{name}", "=", "torbrowser", `"tor browser"`}, + rule: rule{ + {key: "@{name}"}, {key: "="}, {key: "torbrowser"}, {key: `"tor browser"`}, + }, + getIdx: 2, + wGet: "torbrowser", + wGetString: `@{name} = torbrowser "tor browser"`, + wGetSlice: []string{"@{name}", "=", "torbrowser", `"tor browser"`}, + wString: `@{name} = torbrowser "tor browser"`, }, { name: "variable-2", raw: `@{exec_path} += @{bin}/@{name}`, - tokens: []string{"@{exec_path}", "+", "=", "@{bin}/@{name}"}, + tokens: []string{"@{exec_path}", "+=", "@{bin}/@{name}"}, + rule: rule{ + {key: "@{exec_path}"}, {key: "+="}, {key: "@{bin}/@{name}"}, + }, + getIdx: 1, + wGet: "+=", + wGetString: `@{exec_path} += @{bin}/@{name}`, + wGetSlice: []string{"@{exec_path}", "+=", "@{bin}/@{name}"}, + wString: `@{exec_path} += @{bin}/@{name}`, }, { name: "variable-3", raw: `@{empty}="dummy"`, tokens: []string{"@{empty}", "=", `"dummy"`}, + rule: rule{ + {key: "@{empty}"}, {key: "="}, {key: `"dummy"`}, + }, + getIdx: 1, + wGet: "=", + wGetString: `@{empty} = "dummy"`, + wGetSlice: []string{"@{empty}", "=", `"dummy"`}, + wString: `@{empty} = "dummy"`, }, { name: "variable-4", raw: `@{XDG_PROJECTS_DIR}+="Git"`, - tokens: []string{"@{XDG_PROJECTS_DIR}", "+", "=", `"Git"`}, + tokens: []string{"@{XDG_PROJECTS_DIR}", "+=", `"Git"`}, + rule: rule{ + {key: "@{XDG_PROJECTS_DIR}"}, {key: "+="}, kv{key: "\"Git\""}, + }, + getIdx: 1, + wGet: "+=", + wGetString: `@{XDG_PROJECTS_DIR} += "Git"`, + wGetSlice: []string{"@{XDG_PROJECTS_DIR}", "+=", `"Git"`}, + wString: `@{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)"}, + rule: rule{ + {key: "profile"}, + {key: "foo"}, + {key: "@{exec_path}"}, + {key: "xattrs", values: rule{ + {key: "security.tagged", values: rule{{key: "allowed"}}}, + }}, + {key: "flags", values: rule{ + {key: "complain"}, + {key: "attach_disconnected"}, + }}, + }, + getIdx: 2, + getKey: "flags", + wGet: "@{exec_path}", + wGetString: "profile foo @{exec_path}", + wGetSlice: []string{"profile", "foo", "@{exec_path}"}, + wGetAsMap: map[string][]string{ + "flags": {"complain", "attach_disconnected"}, + "xattrs": {}, + }, + wGetValues: rule{{key: "complain"}, {key: "attach_disconnected"}}, + wGetValuesAsSlice: []string{"complain", "attach_disconnected"}, + wGetValuesAsString: "complain attach_disconnected", + wString: "profile foo @{exec_path} xattrs=(security.tagged=allowed) flags=(complain attach_disconnected)", }, { name: "include", raw: `include `, tokens: []string{"include", ""}, + rule: rule{ + {key: "include"}, {key: ""}, + }, + getIdx: 1, + wGet: "", + wGetString: `include `, + wGetSlice: []string{"include", ""}, + wString: `include `, + wRule: &Include{IfExists: false, IsMagic: true, Path: "tunables/global"}, }, { name: "include-if-exists", raw: `include if exists "/etc/apparmor.d/dummy"`, tokens: []string{"include", "if", "exists", `"/etc/apparmor.d/dummy"`}, + rule: rule{ + {key: "include"}, {key: "if"}, {key: "exists"}, {key: `"/etc/apparmor.d/dummy"`}, + }, + getIdx: 1, + wGet: "if", + wGetString: `include if exists "/etc/apparmor.d/dummy"`, + wGetSlice: []string{"include", "if", "exists", `"/etc/apparmor.d/dummy"`}, + wString: `include if exists "/etc/apparmor.d/dummy"`, + wRule: &Include{IfExists: true, IsMagic: false, Path: "/etc/apparmor.d/dummy"}, }, { name: "rlimit", raw: `set rlimit nproc <= 200`, tokens: []string{"set", "rlimit", "nproc", "<=", "200"}, + rule: rule{ + {key: "set"}, {key: "rlimit"}, {key: "nproc"}, {key: "<="}, {key: "200"}, + }, + getIdx: 4, + wGet: "200", + wGetString: `set rlimit nproc <= 200`, + wGetSlice: []string{"set", "rlimit", "nproc", "<=", "200"}, + wString: `set rlimit nproc <= 200`, + wRule: &Rlimit{Key: "nproc", Op: "<=", Value: "200"}, }, { - name: "userns", - raw: `userns`, - tokens: []string{"userns"}, + name: "userns", + raw: `userns`, + tokens: []string{"userns"}, + rule: rule{{key: "userns"}}, + wGetString: `userns`, + wGetSlice: []string{"userns"}, + wString: `userns`, + wRule: &Userns{Create: true}, }, { name: "capability", raw: `capability dac_read_search`, tokens: []string{"capability", "dac_read_search"}, + rule: rule{ + {key: "capability"}, {key: "dac_read_search"}, + }, + getIdx: 1, + wGet: "dac_read_search", + wGetString: `capability dac_read_search`, + wGetSlice: []string{"capability", "dac_read_search"}, + wString: `capability dac_read_search`, + wRule: &Capability{Names: []string{"dac_read_search"}}, }, { name: "network", raw: `network netlink raw`, tokens: []string{"network", "netlink", "raw"}, + rule: rule{ + {key: "network"}, {key: "netlink"}, {key: "raw"}, + }, + getIdx: 1, + wGet: "netlink", + wGetString: `network netlink raw`, + wGetSlice: []string{"network", "netlink", "raw"}, + wString: `network netlink raw`, + wRule: &Network{Domain: "netlink", Type: "raw"}, }, { name: "mount", raw: `mount /{,**}`, tokens: []string{"mount", "/{,**}"}, + rule: rule{ + {key: "mount"}, {key: "/{,**}"}, + }, + getIdx: 1, + wGet: "/{,**}", + wGetString: `mount /{,**}`, + wGetSlice: []string{"mount", "/{,**}"}, + wString: `mount /{,**}`, + wRule: &Mount{ + MountConditions: MountConditions{Options: []string{}}, + Source: "/{,**}", + }, }, { name: "mount-2", raw: `mount options=(rw rbind) /tmp/newroot/ -> /tmp/newroot/`, tokens: []string{"mount", "options=(rw rbind)", "/tmp/newroot/", "->", "/tmp/newroot/"}, + rule: rule{ + {key: "mount"}, + {key: "options", values: rule{{key: "rw"}, {key: "rbind"}}}, + {key: "/tmp/newroot/"}, {key: "->"}, {key: "/tmp/newroot/"}}, + getIdx: 1, + getKey: "options", + wGet: "options", + wGetString: "mount /tmp/newroot/ -> /tmp/newroot/", + wGetSlice: []string{"mount", "/tmp/newroot/", "->", "/tmp/newroot/"}, + wGetAsMap: map[string][]string{"options": {"rw", "rbind"}}, + wGetValues: rule{{key: "rw"}, {key: "rbind"}}, + wGetValuesAsSlice: []string{"rw", "rbind"}, + wGetValuesAsString: "rw rbind", + wString: "mount options=(rw rbind) /tmp/newroot/ -> /tmp/newroot/", + wRule: &Mount{ + MountConditions: MountConditions{Options: []string{"rw", "rbind"}}, + Source: "/tmp/newroot/", + MountPoint: "/tmp/newroot/", + }, }, { name: "mount-3", raw: `mount options=(rw silent rprivate) -> /oldroot/`, tokens: []string{"mount", "options=(rw silent rprivate)", "->", "/oldroot/"}, + rule: rule{ + {key: "mount"}, + {key: "options", values: rule{{key: "rw"}, {key: "silent"}, {key: "rprivate"}}}, + {key: "->"}, {key: "/oldroot/"}, + }, + getIdx: 3, + getKey: "options", + wGet: "/oldroot/", + wGetString: "mount -> /oldroot/", + wGetSlice: []string{"mount", "->", "/oldroot/"}, + wGetAsMap: map[string][]string{"options": {"rw", "silent", "rprivate"}}, + wGetValues: rule{{key: "rw"}, {key: "silent"}, {key: "rprivate"}}, + wGetValuesAsSlice: []string{"rw", "silent", "rprivate"}, + wGetValuesAsString: "rw silent rprivate", + wString: "mount options=(rw silent rprivate) -> /oldroot/", + wRule: &Mount{ + MountConditions: MountConditions{Options: []string{"rw", "rprivate", "silent"}}, + MountPoint: "/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/"}, + rule: rule{ + {key: "mount"}, + {key: "fstype", values: rule{{key: "devpts"}}}, + {key: "options", values: rule{{key: "rw"}, {key: "nosuid"}, {key: "noexec"}}}, + {key: "devpts"}, {key: "->"}, {key: "/newroot/dev/pts/"}, + }, + getIdx: 1, + getKey: "fstype", + wGet: "fstype", + wGetString: "mount devpts -> /newroot/dev/pts/", + wGetSlice: []string{"mount", "devpts", "->", "/newroot/dev/pts/"}, + wGetAsMap: map[string][]string{ + "fstype": {"devpts"}, + "options": {"rw", "nosuid", "noexec"}, + }, + wGetValues: rule{{key: "devpts"}}, + wGetValuesAsSlice: []string{"devpts"}, + wGetValuesAsString: "devpts", + wString: "mount fstype=devpts options=(rw nosuid noexec) devpts -> /newroot/dev/pts/", + wRule: &Mount{ + MountConditions: MountConditions{ + FsType: "devpts", + Options: []string{"rw", "noexec", "nosuid"}, + }, + Source: "devpts", + MountPoint: "/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"}, + rule: rule{ + {key: "signal"}, {key: "receive"}, + {key: "set", values: rule{{key: "cont"}, {key: "term"}, {key: "winch"}}}, + {key: "peer", values: rule{{key: "at-spi-bus-launcher"}}}, + }, + getIdx: 1, + getKey: "peer", + wGet: "receive", + wGetString: "signal receive", + wGetSlice: []string{"signal", "receive"}, + wGetAsMap: map[string][]string{ + "peer": {"at-spi-bus-launcher"}, + "set": {"cont", "term", "winch"}, + }, + wGetValues: rule{{key: "at-spi-bus-launcher"}}, + wGetValuesAsSlice: []string{"at-spi-bus-launcher"}, + wGetValuesAsString: "at-spi-bus-launcher", + wString: "signal receive set=(cont term winch) peer=at-spi-bus-launcher", + wRule: &Signal{ + Access: []string{"receive"}, + Set: []string{"term", "cont", "winch"}, + Peer: "at-spi-bus-launcher", + }, }, { - name: "unix", + name: "unix-1", 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)"}, + rule: rule{ + {key: "unix"}, {key: "send"}, {key: "receive"}, + {key: "type", values: rule{{key: "stream"}}}, + {key: "addr", values: rule{ + {key: `"@/tmp/.ICE[0-9]*-unix/19 5"`}, + }}, + {key: "peer", values: rule{ + {key: "label", values: rule{{key: `"@{p_systemd}"`}}}, + {key: "addr", values: rule{{key: "none"}}}, + }}, + }, + getIdx: 3, + getKey: "peer", + wGet: "type", + wGetString: "unix send receive", + wGetSlice: []string{"unix", "send", "receive"}, + wGetAsMap: map[string][]string{ + "addr": {`"@/tmp/.ICE[0-9]*-unix/19 5"`}, + "peer": {}, + "type": {"stream"}, + }, + wGetValues: rule{ + {key: "label", values: rule{{key: `"@{p_systemd}"`}}}, + {key: "addr", values: rule{{key: "none"}}}, + }, + wGetValuesAsSlice: []string{}, + wGetValuesAsString: "", + wString: `unix send receive type=stream addr=("@/tmp/.ICE[0-9]*-unix/19 5") peer=(label="@{p_systemd}" addr=none)`, + wRule: &Unix{ + Access: []string{"send", "receive"}, + Type: "stream", + Address: `"@/tmp/.ICE[0-9]*-unix/19 5"`, + PeerLabel: `"@{p_systemd}"`, + PeerAddr: "none", + }, }, { name: "unix-2", @@ -148,6 +547,31 @@ var ( type=stream peer=(addr="@/tmp/ibus/dbus-????????")`, tokens: []string{"unix", "(connect, receive, send)\n", "type=stream\n", `peer=(addr="@/tmp/ibus/dbus-????????")`}, + rule: rule{ + {key: "unix"}, {key: "connect"}, {key: "receive"}, {key: "send"}, + {key: "type", values: rule{{key: "stream"}}}, + {key: "peer", values: rule{ + {key: "addr", values: rule{{key: `"@/tmp/ibus/dbus-????????"`}}}, + }}, + }, + getIdx: 4, + getKey: "type", + wGet: "type", + wGetString: "unix connect receive send", + wGetSlice: []string{"unix", "connect", "receive", "send"}, + wGetAsMap: map[string][]string{ + "type": {"stream"}, + "peer": {}, + }, + wGetValues: rule{{key: "stream"}}, + wGetValuesAsSlice: []string{"stream"}, + wGetValuesAsString: "stream", + wString: `unix connect receive send type=stream peer=(addr="@/tmp/ibus/dbus-????????")`, + wRule: &Unix{ + Access: []string{"connect", "receive", "send"}, + Type: "stream", + PeerAddr: `"@/tmp/ibus/dbus-????????"`, + }, }, { name: "dbus", @@ -157,41 +581,278 @@ var ( "path=/org/freedesktop/DBus", "interface=org.freedesktop.DBus", "member=AddMatch", "peer=(name=:1.3, label=power-profiles-daemon)", }, + rule: rule{ + {key: "dbus"}, {key: "receive"}, + {key: "bus", values: rule{{key: "system"}}}, + {key: "path", values: rule{{key: "/org/freedesktop/DBus"}}}, + {key: "interface", values: rule{{key: "org.freedesktop.DBus"}}}, + {key: "member", values: rule{{key: "AddMatch"}}}, + {key: "peer", values: rule{ + {key: "name", values: rule{{key: ":1.3"}}}, + {key: "label", values: rule{{key: "power-profiles-daemon"}}}, + }}, + }, + getIdx: 2, + getKey: "path", + wGet: "bus", + wGetString: "dbus receive", + wGetSlice: []string{"dbus", "receive"}, + wGetAsMap: map[string][]string{ + "bus": {"system"}, + "interface": {"org.freedesktop.DBus"}, + "member": {"AddMatch"}, + "path": {"/org/freedesktop/DBus"}, + "peer": {}, + }, + wGetValues: rule{{key: "/org/freedesktop/DBus"}}, + wGetValuesAsSlice: []string{"/org/freedesktop/DBus"}, + wGetValuesAsString: "/org/freedesktop/DBus", + wString: `dbus receive bus=system path=/org/freedesktop/DBus interface=org.freedesktop.DBus member=AddMatch peer=(name=:1.3 label=power-profiles-daemon)`, + wRule: &Dbus{ + Access: []string{"receive"}, + Bus: "system", + Path: "/org/freedesktop/DBus", + Interface: "org.freedesktop.DBus", + Member: "AddMatch", + PeerName: ":1.3", + PeerLabel: "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}"}, + rule: rule{ + {key: "owner"}, + {key: "@{user_config_dirs}/powerdevilrc{,.@{rand6}}"}, + {key: "rwl"}, + {key: "->"}, + {key: "@{user_config_dirs}/#@{int}"}, + }, + getIdx: 3, + wGet: "->", + wGetString: "owner @{user_config_dirs}/powerdevilrc{,.@{rand6}} rwl -> @{user_config_dirs}/#@{int}", + wGetSlice: []string{"owner", "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", "rwl", "->", "@{user_config_dirs}/#@{int}"}, + wString: `owner @{user_config_dirs}/powerdevilrc{,.@{rand6}} rwl -> @{user_config_dirs}/#@{int}`, + wRule: &File{ + Owner: true, + Path: "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", + Access: []string{"r", "w", "l"}, + Target: "@{user_config_dirs}/#@{int}", + }, }, { name: "file-2", raw: `@{sys}/devices/@{pci}/class r`, tokens: []string{"@{sys}/devices/@{pci}/class", "r"}, + rule: rule{ + {key: "@{sys}/devices/@{pci}/class"}, {key: "r"}, + }, + getIdx: 1, + wGet: "r", + wGetString: `@{sys}/devices/@{pci}/class r`, + wGetSlice: []string{"@{sys}/devices/@{pci}/class", "r"}, + wString: `@{sys}/devices/@{pci}/class r`, + wRule: &File{Path: "@{sys}/devices/@{pci}/class", Access: []string{"r"}}, }, { name: "file-3", raw: `owner @{PROC}/@{pid}/task/@{tid}/comm rw`, tokens: []string{"owner", "@{PROC}/@{pid}/task/@{tid}/comm", "rw"}, + rule: rule{ + {key: "owner"}, {key: "@{PROC}/@{pid}/task/@{tid}/comm"}, {key: "rw"}, + }, + getIdx: 1, + wGet: "@{PROC}/@{pid}/task/@{tid}/comm", + wGetString: `owner @{PROC}/@{pid}/task/@{tid}/comm rw`, + wGetSlice: []string{"owner", "@{PROC}/@{pid}/task/@{tid}/comm", "rw"}, + wString: `owner @{PROC}/@{pid}/task/@{tid}/comm rw`, + wRule: &File{ + Owner: true, + Path: "@{PROC}/@{pid}/task/@{tid}/comm", + Access: []string{"r", "w"}, + }, }, { name: "file-4", raw: `owner /{var/,}tmp/#@{int} rw`, tokens: []string{"owner", "/{var/,}tmp/#@{int}", "rw"}, + rule: rule{ + {key: "owner"}, {key: "/{var/,}tmp/#@{int}"}, {key: "rw"}, + }, + getIdx: 2, + wGet: "rw", + wGetString: `owner /{var/,}tmp/#@{int} rw`, + wGetSlice: []string{"owner", "/{var/,}tmp/#@{int}", "rw"}, + wString: `owner /{var/,}tmp/#@{int} rw`, + wRule: &File{ + Owner: true, + Path: "/{var/,}tmp/#@{int}", + Access: []string{"r", "w"}, + }, + }, + { + name: "file-5", + raw: `owner @{run}/user/@{uid}/gvfs/smb-share:server=*,share=**/ r`, + tokens: []string{"owner", "@{run}/user/@{uid}/gvfs/smb-share:server=*,share=**/", "r"}, + rule: rule{ + kv{key: "owner"}, + kv{key: "@{run}/user/@{uid}/gvfs/smb-share:server=*,share=**/"}, + kv{key: "r"}, + }, + getIdx: 0, + wGet: "owner", + wGetString: `owner @{run}/user/@{uid}/gvfs/smb-share:server=*,share=**/ r`, + wGetSlice: []string{"owner", "@{run}/user/@{uid}/gvfs/smb-share:server=*,share=**/", "r"}, + wString: `owner @{run}/user/@{uid}/gvfs/smb-share:server=*,share=**/ r`, + wRule: &File{ + Owner: true, + Path: "@{run}/user/@{uid}/gvfs/smb-share:server=*,share=**/", + Access: []string{"r"}, + }, + }, + } + + // Test cases for parseLineRules + testsLineRules = []struct { + name string + isPreamble bool + raw string + want string + rules Rules + wantErr bool + }{ + { + name: "strin.aa", + isPreamble: true, + raw: `alias /mnt/usr -> /usr, + include + include if exists "/etc/apparmor.d/global/dummy space" + @{name} = torbrowser "tor browser" + alias /mnt/{,usr.sbin.}mount.cifs + -> + /sbin/mount.cifs,`, + want: `alias /mnt/usr -> /usr, + + + + alias /mnt/{,usr.sbin.}mount.cifs + -> + /sbin/mount.cifs,`, + rules: Rules{ + &Include{IsMagic: true, Path: "tunables/global"}, + &Include{IfExists: true, Path: "/etc/apparmor.d/global/dummy space"}, + &Variable{Name: "name", Define: true, Values: []string{"torbrowser", "\"tor browser\""}}, + }, + wantErr: false, + }, + { + name: "comment", + isPreamble: true, + raw: ` + # IsLineRule comment + include # comment included + @{lib_dirs} = @{lib}/@{name} /opt/@{name} # comment in variable`, + want: "\n\n\n", + rules: Rules{ + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " IsLineRule comment"}}, + &Include{ + RuleBase: RuleBase{Comment: " comment included"}, + IsMagic: true, Path: "tunables/global", + }, + &Variable{ + RuleBase: RuleBase{Comment: " comment in variable"}, + Name: "lib_dirs", Define: true, + Values: []string{"@{lib}/@{name}", "/opt/@{name}"}, + }, + }, + wantErr: false, + }, + } + + // Test cases for parseCommaRules + testsCommaRules = []struct { + name string + raw string + rule []rule + wantErr bool + }{ + { + name: "string.aa", + raw: `alias /mnt/{,usr.sbin.}mount.cifs + -> + /sbin/mount.cifs, + network inet stream, # inline comment + dbus bind bus=session name=org.mpris.MediaPlayer2, + owner @{user_config_dirs}/powerdevilrc{,.@{rand6}} rwl -> @{user_config_dirs}/#@{int}, + @{sys}/class/ r, + @{run}/udev/data/+pci:* r,`, + rule: []rule{ + { + kv{key: "alias"}, + kv{key: "/mnt/{,usr.sbin.}mount.cifs"}, + kv{key: "->"}, + kv{key: "/sbin/mount.cifs"}, + }, + { + kv{key: "network"}, kv{key: "inet"}, kv{key: "stream", comment: " inline comment"}, + }, + { + kv{key: "dbus"}, kv{key: "bind"}, + kv{key: "bus", values: rule{kv{key: "session"}}}, + kv{key: "name", values: rule{kv{key: "org.mpris.MediaPlayer2"}}}, + }, + { + kv{key: "owner"}, kv{key: "@{user_config_dirs}/powerdevilrc{,.@{rand6}}"}, kv{key: "rwl"}, + kv{key: "->"}, kv{key: "@{user_config_dirs}/#@{int}"}, + }, + { + kv{key: "@{sys}/class/"}, kv{key: "r"}, + }, + { + kv{key: "@{run}/udev/data/+pci:*"}, kv{key: "r"}, + }, + }, + wantErr: false, + }, + { + name: ",-in-aare", + raw: `@{sys}/cgroup/cpu,cpuacct/user.slice/cpu.cfs_quota_us r,`, + rule: []rule{ + { + kv{key: "@{sys}/cgroup/cpu,cpuacct/user.slice/cpu.cfs_quota_us"}, kv{key: "r"}, + }, + }, + wantErr: false, + }, + { + name: "#-in-aare", + raw: `owner @{user_config_dirs}/#** rw,`, + rule: []rule{ + { + kv{key: "owner"}, kv{key: "@{user_config_dirs}/#**"}, kv{key: "rw"}, + }, + }, + wantErr: false, }, } // Test cases for Parse testBlocks = []struct { - name string - raw string - apparmor *AppArmorProfileFile - wParseErr bool + name string + raw string + apparmor *AppArmorProfileFile + wParseErr bool + wRules []Rules + wParseRulesErr bool }{ { - name: "empty", - raw: "", - apparmor: &AppArmorProfileFile{}, - wParseErr: false, + name: "empty", + raw: "", + apparmor: &AppArmorProfileFile{}, + wParseErr: false, + wRules: []Rules{}, + wParseRulesErr: false, }, { name: "comment", @@ -213,7 +874,9 @@ var ( }, }, }, - wParseErr: false, + wParseErr: false, + wRules: []Rules{}, + wParseRulesErr: false, }, { name: "cornercases", @@ -223,6 +886,7 @@ var ( # { commented block } @{name} = {D,d}ummy @{exec_path} = @{bin}/@{name} + @{exec_path} += @{lib}/@{name} alias /mnt/{,usr.sbin.}mount.cifs -> /sbin/mount.cifs, @{coreutils} += gawk {,e,f}grep head profile @{exec_path} { @@ -230,12 +894,13 @@ var ( 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"}, + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " { commented block }"}}, &Variable{Name: "name", Values: []string{"{D,d}ummy"}, Define: true}, &Variable{Name: "exec_path", Values: []string{"@{bin}/@{name}"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"@{lib}/@{name}"}}, + &Variable{Name: "coreutils", Values: []string{"gawk", "{,e,f}grep", "head"}}, &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{ { @@ -248,7 +913,9 @@ var ( }, }, }, - wParseErr: false, + wParseErr: false, + wRules: []Rules{}, + wParseRulesErr: false, }, { name: "string.aa", @@ -256,13 +923,13 @@ var ( 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"}, }, + &Abi{IsMagic: true, Path: "abi/4.0"}, + &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"}, }, Profiles: []*Profile{ { @@ -276,6 +943,280 @@ var ( }, }, wParseErr: false, + wRules: []Rules{ + { + &Include{IsMagic: true, Path: "abstractions/base"}, + &Include{IsMagic: true, Path: "abstractions/nameservice-strict"}, + }, + { + &Rlimit{Key: "nproc", Op: "<=", Value: "200"}, + }, + { + &Capability{Names: []string{"dac_read_search"}}, + &Capability{Names: []string{"dac_override"}}, + }, + { + &Network{Domain: "inet", Type: "stream"}, + &Network{Domain: "inet6", Type: "stream"}, + }, + { + &Mount{ + RuleBase: RuleBase{IsLineRule: false, Comment: " failed perms check"}, + MountConditions: MountConditions{ + FsType: "fuse.portal", + Options: []string{"rw", "rbind"}, + }, + Source: "@{run}/user/@{uid}/", + MountPoint: "/", + }, + }, + { + &Umount{ + MountConditions: MountConditions{Options: []string{}}, + MountPoint: "@{run}/user/@{uid}/", + }, + }, + { + &Signal{ + Access: []string{"receive"}, + Set: []string{"term"}, + Peer: "at-spi-bus-launcher", + }, + }, + { + &Ptrace{Access: []string{"read"}, Peer: "nautilus"}, + }, + { + &Unix{ + Access: []string{"send", "receive"}, + Type: "stream", + Address: "@/tmp/.ICE-unix/1995", + PeerLabel: "gnome-shell", + PeerAddr: "none", + }, + }, + { + &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"}, + &Dbus{ + Access: []string{"receive"}, + Bus: "system", + Path: "/org/freedesktop/DBus", + Interface: "org.freedesktop.DBus", + Member: "AddMatch", + PeerName: ":1.3", + PeerLabel: "power-profiles-daemon", + }, + }, + { + &File{Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", Access: []string{"m", "r"}}, + &File{Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: []string{"r", "w"}}, + &File{Path: "@{sys}/devices/@{pci}/class", Access: []string{"r"}}, + }, + }, + wParseRulesErr: false, + }, + { + name: "full.aa", + raw: util.MustReadFile(testData.Join("full.aa")), + apparmor: &AppArmorProfileFile{ + Preamble: Rules{ + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " Simple test profile with all rules used"}}, + &Include{ + RuleBase: RuleBase{Comment: " a comment", Optional: true}, + IsMagic: true, Path: "tunables/global", + }, + &Include{IfExists: true, Path: "/etc/apparmor.d/global/dummy space"}, + &Variable{Name: "name", Values: []string{"torbrowser", "\"tor browser\""}, Define: true}, + &Variable{ + RuleBase: RuleBase{Comment: " another comment"}, Define: true, + Name: "lib_dirs", Values: []string{"@{lib}/@{name}", "/opt/@{name}"}, + }, + &Variable{Name: "config_dirs", Values: []string{"@{HOME}/.mozilla/"}, Define: true}, + &Variable{Name: "cache_dirs", Values: []string{"@{user_cache_dirs}/mozilla/"}, Define: true}, + &Variable{Name: "exec_path", Values: []string{"@{bin}/@{name}", "@{lib_dirs}/@{name}"}, Define: true}, + &Abi{IsMagic: true, Path: "abi/4.0"}, + &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"}, + &Alias{Path: "/mnt/{,usr.sbin.}mount.cifs", RewrittenPath: "/sbin/mount.cifs"}, + }, + Profiles: []*Profile{ + { + Header: Header{ + Name: "foo", + Attachments: []string{"@{exec_path}"}, + Attributes: map[string]string{"security.tagged": "allowed"}, + Flags: []string{"complain", "attach_disconnected"}, + }, + }, + }, + }, + wParseErr: false, + wRules: []Rules{ + { + &Include{IsMagic: true, Path: "abstractions/base"}, + &Include{IsMagic: true, Path: "abstractions/nameservice-strict"}, + &Include{Path: "/etc/apparmor.d/abstractions/dummy space"}, + }, + { + &All{}, + }, + { + &Rlimit{Key: "nproc", Op: "<=", Value: "200"}, + }, + { + &Userns{Create: true}, + }, + { + &Capability{Names: []string{"dac_read_search"}}, + &Capability{Names: []string{"dac_override"}}, + }, + { + &Network{Domain: "inet", Type: "stream"}, + &Network{Domain: "netlink", Type: "raw"}, + }, + { + &Mount{ + MountConditions: MountConditions{Options: []string{}}, + Source: "/{,**}", + }, + &Mount{ + MountConditions: MountConditions{Options: []string{"rw", "rbind"}}, + Source: "/tmp/newroot/", + MountPoint: "/tmp/newroot/", + }, + &Mount{ + MountConditions: MountConditions{Options: []string{"rw", "rprivate", "silent"}}, + MountPoint: "/oldroot/", + }, + &Mount{ + MountConditions: MountConditions{ + FsType: "devpts", + Options: []string{"rw", "noexec", "nosuid"}, + }, + Source: "devpts", + MountPoint: "/newroot/dev/pts/", + }, + }, + { + &Remount{ + MountConditions: MountConditions{Options: []string{}}, + MountPoint: "/newroot/{,**}", + }, + }, + { + &Umount{ + MountConditions: MountConditions{Options: []string{}}, + MountPoint: "@{run}/user/@{uid}/", + }, + }, + { + &PivotRoot{OldRoot: "/tmp/oldroot/", NewRoot: "/tmp/"}, + }, + { + &ChangeProfile{ProfileName: "libvirt-@{uuid}"}, + }, + { + &Mqueue{Access: []string{"r"}, Type: "posix", Name: "/"}, + }, + { + &IOUring{Access: []string{"sqpoll"}, Label: "foo"}, + }, + { + &Signal{ + Access: []string{"receive"}, + Set: []string{"term", "cont", "winch"}, + Peer: "at-spi-bus-launcher", + }, + }, + { + &Ptrace{Access: []string{"read"}, Peer: "nautilus"}, + }, + { + &Unix{ + Access: []string{"send", "receive"}, + Type: "stream", + Address: "\"@/tmp/.ICE[0-9]-unix/19 5\"", + PeerLabel: "gnome-shell", + PeerAddr: "none", + }, + }, + { + &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"}, + &Dbus{ + Access: []string{"receive"}, + Bus: "system", + Path: "/org/freedesktop/DBus", + Interface: "org.freedesktop.DBus", + Member: "AddMatch", + PeerName: ":1.3", + PeerLabel: "power-profiles-daemon", + }, + }, + { + &Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " A comment! before a paragraph of rules"}}, + &File{ + Path: "\"/opt/Mullvad VPN/resources/*.so*\"", + Access: []string{"m", "r"}, + }, + &File{Path: "\"/opt/Mullvad VPN/resources/*\"", Access: []string{"r"}}, + &File{ + Path: "\"/opt/Mullvad VPN/resources/openvpn\"", + Access: []string{"r", "ix"}, + }, + &File{ + Path: "/usr/share/gnome-shell/extensions/ding@rastersoft.com/{,*/}ding.js", + Access: []string{"r", "Px"}, + }, + &File{ + Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", + Access: []string{"m", "r"}, + }, + }, + { + &File{ + Owner: true, Path: "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", + Access: []string{"r", "w", "l"}, Target: "@{user_config_dirs}/#@{int}", + }, + &Link{Path: "@{user_config_dirs}/kiorc", Target: "@{user_config_dirs}/#@{int}"}, + }, + { + &File{Path: "@{run}/udev/data/+pci:*", Access: []string{"r"}}, + }, + { + &File{Path: "@{sys}/devices/@{pci}/class", Access: []string{"r"}}, + }, + { + &File{Owner: true, Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: []string{"r", "w"}}, + }, + { + &Include{IsMagic: true, Path: "abstractions/base"}, + &Include{IfExists: true, IsMagic: true, Path: "local/foo_action"}, + }, + { + &Include{IsMagic: true, Path: "abstractions/base"}, + &Include{IsMagic: true, Path: "abstractions/systemctl"}, + }, + { + &Include{IfExists: true, IsMagic: true, Path: "local/foo_systemctl"}, + &Capability{Names: []string{"net_admin"}}, + }, + { + &Include{IsMagic: true, Path: "abstractions/base"}, + &Include{IsMagic: true, Path: "abstractions/app/sudo"}, + }, + { + &File{Path: "@{sh_path}", Access: []string{"r", "ix"}}, + }, + { + &Include{IfExists: true, IsMagic: true, Path: "local/foo_sudo"}, + }, + { + &Include{IfExists: true, IsMagic: true, Path: "local/foo"}, + }, + { + &Include{IsMagic: true, Path: "abstractions/base"}, + }, + }, + wParseRulesErr: false, }, } )