feat(aa): rewrite variable handling.

This commit is contained in:
Alexandre Pujol 2024-05-05 14:09:00 +01:00
parent 28f4294774
commit 305d06dbe0
Failed to generate hash of commit
7 changed files with 368 additions and 422 deletions

View file

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

View file

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

71
pkg/aa/resolve.go Normal file
View file

@ -0,0 +1,71 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// 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
}

197
pkg/aa/resolve_test.go Normal file
View file

@ -0,0 +1,197 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// 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)
}
})
}
}

View file

@ -10,7 +10,6 @@ import (
)
const (
tokALL = "all"
tokALLOW = "allow"
tokAUDIT = "audit"
tokDENY = "deny"

View file

@ -1,120 +0,0 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// 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, ",") + "}"
}
}

View file

@ -1,251 +0,0 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2023-2024 Alexandre Pujol <alexandre@pujol.io>
// 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)
}
})
}
}