From 305d06dbe04b25506d55717ee4648f352c427bc4 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 5 May 2024 14:09:00 +0100 Subject: [PATCH] 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) - } - }) - } -}