mirror of
https://github.com/roddhjav/apparmor.d.git
synced 2025-01-18 17:08:09 +01:00
feat(aa): rewrite variable handling.
This commit is contained in:
parent
28f4294774
commit
305d06dbe0
7 changed files with 368 additions and 422 deletions
|
@ -27,6 +27,23 @@ func NewAppArmorProfile() *AppArmorProfileFile {
|
||||||
return &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
|
// String returns the formatted representation of a profile file as a string
|
||||||
func (f *AppArmorProfileFile) String() string {
|
func (f *AppArmorProfileFile) String() string {
|
||||||
return renderTemplate("apparmor", f)
|
return renderTemplate("apparmor", f)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"maps"
|
"maps"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -86,7 +87,22 @@ func (p *Profile) Merge() {
|
||||||
// Sort the rules in a profile.
|
// Sort the rules in a profile.
|
||||||
// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines
|
// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines
|
||||||
func (p *Profile) Sort() {
|
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.
|
// 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.
|
// GetAttachments return a nested attachment string
|
||||||
func (p *Profile) AddRule(log map[string]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
|
// Generate profile flags and extra rules
|
||||||
switch log["error"] {
|
switch log["error"] {
|
||||||
case "-2":
|
case "-2":
|
||||||
|
@ -139,57 +214,15 @@ func (p *Profile) AddRule(log map[string]string) {
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
switch log["class"] {
|
if newRule, ok := newLogMap[log["class"]]; ok {
|
||||||
case "rlimits":
|
p.Rules = append(p.Rules, newRule(log))
|
||||||
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 {
|
} else {
|
||||||
p.Rules = append(p.Rules, newNetworkFromLog(log))
|
|
||||||
}
|
|
||||||
case "io_uring":
|
|
||||||
p.Rules = append(p.Rules, newIOUringFromLog(log))
|
|
||||||
case "mount":
|
|
||||||
if strings.Contains(log["flags"], "remount") {
|
|
||||||
p.Rules = append(p.Rules, newRemountFromLog(log))
|
|
||||||
} else {
|
|
||||||
switch log["operation"] {
|
|
||||||
case "mount":
|
|
||||||
p.Rules = append(p.Rules, newMountFromLog(log))
|
|
||||||
case "umount":
|
|
||||||
p.Rules = append(p.Rules, newUmountFromLog(log))
|
|
||||||
case "remount":
|
|
||||||
p.Rules = append(p.Rules, newRemountFromLog(log))
|
|
||||||
case "pivotroot":
|
|
||||||
p.Rules = append(p.Rules, newPivotRootFromLog(log))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "posix_mqueue", "sysv_mqueue":
|
|
||||||
p.Rules = append(p.Rules, newMqueueFromLog(log))
|
|
||||||
case "signal":
|
|
||||||
p.Rules = append(p.Rules, newSignalFromLog(log))
|
|
||||||
case "ptrace":
|
|
||||||
p.Rules = append(p.Rules, newPtraceFromLog(log))
|
|
||||||
case "namespace":
|
|
||||||
p.Rules = append(p.Rules, newUsernsFromLog(log))
|
|
||||||
case "unix":
|
|
||||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
|
||||||
case "dbus":
|
|
||||||
p.Rules = append(p.Rules, newDbusFromLog(log))
|
|
||||||
case "file":
|
|
||||||
if log["operation"] == "change_onexec" {
|
|
||||||
p.Rules = append(p.Rules, newChangeProfileFromLog(log))
|
|
||||||
} else {
|
|
||||||
p.Rules = append(p.Rules, newFileFromLog(log))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if strings.Contains(log["operation"], "dbus") {
|
if strings.Contains(log["operation"], "dbus") {
|
||||||
p.Rules = append(p.Rules, newDbusFromLog(log))
|
p.Rules = append(p.Rules, newDbusFromLog(log))
|
||||||
} else if log["family"] == "unix" {
|
} else if log["family"] == "unix" {
|
||||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
p.Rules = append(p.Rules, newUnixFromLog(log))
|
||||||
|
} else {
|
||||||
|
panic("unknown class: " + log["class"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
71
pkg/aa/resolve.go
Normal file
71
pkg/aa/resolve.go
Normal 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
197
pkg/aa/resolve_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tokALL = "all"
|
|
||||||
tokALLOW = "allow"
|
tokALLOW = "allow"
|
||||||
tokAUDIT = "audit"
|
tokAUDIT = "audit"
|
||||||
tokDENY = "deny"
|
tokDENY = "deny"
|
||||||
|
|
|
@ -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, ",") + "}"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue