feat(aa): add function to resolve include preamble.

This commit is contained in:
Alexandre Pujol 2024-05-28 11:53:32 +01:00
parent 04a91bbd9b
commit dc0e0084a0
Failed to generate hash of commit
6 changed files with 191 additions and 36 deletions

View file

@ -8,21 +8,34 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/arduino/go-paths-helper"
"github.com/roddhjav/apparmor.d/pkg/util"
) )
var ( var (
includeCache map[*Include]*AppArmorProfileFile = make(map[*Include]*AppArmorProfileFile)
regVariableReference = regexp.MustCompile(`@{([^{}]+)}`) regVariableReference = regexp.MustCompile(`@{([^{}]+)}`)
) )
// Resolve resolves all variables and includes in the profile and merge the rules in the profile // Resolve resolves variables and includes definied in the profile preamble
func (f *AppArmorProfileFile) Resolve() error { func (f *AppArmorProfileFile) Resolve() error {
// Resolve preamble includes
for _, include := range f.Preamble.GetIncludes() {
err := f.resolveInclude(include)
if err != nil {
return err
}
}
// Resolve variables // Resolve variables
for _, variable := range f.Preamble.GetVariables() { for _, variable := range f.Preamble.GetVariables() {
newValues := []string{} newValues := []string{}
for _, value := range variable.Values { for _, value := range variable.Values {
vars := f.resolveVariable(value) vars, err := f.resolveValues(value)
if len(vars) == 0 { if err != nil {
return fmt.Errorf("Variable not defined in: %s", value) return err
} }
newValues = append(newValues, vars...) newValues = append(newValues, vars...)
} }
@ -33,9 +46,9 @@ func (f *AppArmorProfileFile) Resolve() error {
for _, profile := range f.Profiles { for _, profile := range f.Profiles {
attachments := []string{} attachments := []string{}
for _, att := range profile.Attachments { for _, att := range profile.Attachments {
vars := f.resolveVariable(att) vars, err := f.resolveValues(att)
if len(vars) == 0 { if err != nil {
return fmt.Errorf("Variable not defined in: %s", att) return err
} }
attachments = append(attachments, vars...) attachments = append(attachments, vars...)
} }
@ -45,27 +58,108 @@ func (f *AppArmorProfileFile) Resolve() error {
return nil return nil
} }
func (f *AppArmorProfileFile) resolveVariable(input string) []string { func (f *AppArmorProfileFile) resolveValues(input string) ([]string, error) {
if !strings.Contains(input, tokVARIABLE) { if !strings.Contains(input, tokVARIABLE) {
return []string{input} return []string{input}, nil
} }
vars := []string{} values := []string{}
match := regVariableReference.FindStringSubmatch(input) match := regVariableReference.FindStringSubmatch(input)
if len(match) > 1 { if len(match) == 0 {
return nil, fmt.Errorf("Invalid variable reference: %s", input)
}
variable := match[0] variable := match[0]
varname := match[1] varname := match[1]
found := false
for _, vrbl := range f.Preamble.GetVariables() { for _, vrbl := range f.Preamble.GetVariables() {
if vrbl.Name == varname { if vrbl.Name == varname {
found = true
for _, v := range vrbl.Values { for _, v := range vrbl.Values {
newVar := strings.ReplaceAll(input, variable, v) if strings.Contains(v, tokVARIABLE+varname+"}") {
res := f.resolveVariable(newVar) return nil, fmt.Errorf("recursive variable found in: %s", varname)
vars = append(vars, res...) }
newValues := strings.ReplaceAll(input, variable, v)
newValues = strings.ReplaceAll(newValues, "//", "/")
res, err := f.resolveValues(newValues)
if err != nil {
return nil, err
}
values = append(values, res...)
} }
} }
} }
if !found {
return nil, fmt.Errorf("Variable %s not defined", varname)
}
return values, nil
}
// resolveInclude resolves all includes defined in the profile preamble
func (f *AppArmorProfileFile) resolveInclude(include *Include) error {
if include == nil || include.Path == "" {
return fmt.Errorf("Invalid include: %v", include)
}
_, isCached := includeCache[include]
if !isCached {
var files paths.PathList
var err error
path := MagicRoot.Join(include.Path)
if !include.IsMagic {
path = paths.New(include.Path)
}
if path.IsDir() {
files, err = path.ReadDir(paths.FilterOutDirectories())
if err != nil {
if include.IfExists {
return nil
}
return fmt.Errorf("File %s not found: %v", path, err)
}
} else if path.Exist() {
files = append(files, path)
} else { } else {
vars = append(vars, input) if include.IfExists {
return nil
} }
return vars return fmt.Errorf("File %s not found", path)
}
iFile := &AppArmorProfileFile{}
for _, file := range files {
raw, err := util.ReadFile(file)
if err != nil {
return err
}
if err := iFile.Parse(raw); err != nil {
return err
}
}
if err := iFile.Validate(); err != nil {
return err
}
for _, inc := range iFile.Preamble.GetIncludes() {
if err := iFile.resolveInclude(inc); err != nil {
return err
}
}
// Remove all includes in iFile
iFile.Preamble = iFile.Preamble.DeleteKind(tokINCLUDE)
// Cache the included file
includeCache[include] = iFile
}
// Insert iFile in the place of include in the current file
index := f.Preamble.IndexOf(include)
f.Preamble = f.Preamble.Insert(index, includeCache[include].Preamble...)
return nil
} }

View file

@ -7,24 +7,75 @@ package aa
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/arduino/go-paths-helper"
) )
func TestAppArmorProfileFile_resolveVariable(t *testing.T) { func TestAppArmorProfileFile_resolveInclude(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
f AppArmorProfileFile include *Include
input string want *AppArmorProfileFile
want []string wantErr bool
}{ }{
{ {
name: "nil", name: "empty",
input: "@{newvar}", include: &Include{Path: "", IsMagic: true},
want: []string{}, want: &AppArmorProfileFile{Preamble: Rules{&Include{Path: "", IsMagic: true}}},
wantErr: true,
}, },
{ {
name: "empty", name: "tunables",
include: &Include{Path: "tunables/global", IsMagic: true},
want: &AppArmorProfileFile{
Preamble: Rules{
&Alias{Path: "/usr/", RewrittenPath: "/User/"},
&Alias{Path: "/lib/", RewrittenPath: "/Libraries/"},
&Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " variable declarations for inclusion"}},
&Variable{
Name: "FOO", Define: true,
Values: []string{
"/foo", "/bar", "/baz", "/biff", "/lib", "/tmp",
},
},
},
},
wantErr: false,
},
}
MagicRoot = paths.New("../../tests/testdata")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := &AppArmorProfileFile{}
got.Preamble = append(got.Preamble, tt.include)
if err := got.resolveInclude(tt.include); (err != nil) != tt.wantErr {
t.Errorf("AppArmorProfileFile.resolveInclude() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfileFile.resolveValues() = %v, want %v", got, tt.want)
}
})
}
}
func TestAppArmorProfileFile_resolveValues(t *testing.T) {
tests := []struct {
name string
input string
want []string
wantErr bool
}{
{
name: "not-defined",
input: "@{newvar}",
want: nil,
wantErr: true,
},
{
name: "no-name",
input: "@{}", input: "@{}",
want: []string{"@{}"}, want: nil,
wantErr: true,
}, },
{ {
name: "default", name: "default",
@ -45,9 +96,12 @@ func TestAppArmorProfileFile_resolveVariable(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
f := DefaultTunables() f := DefaultTunables()
got := f.resolveVariable(tt.input) got, err := f.resolveValues(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("AppArmorProfileFile.resolveValues() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfileFile.resolveVariable() = %v, want %v", got, tt.want) t.Errorf("AppArmorProfileFile.resolveValues() = %v, want %v", got, tt.want)
} }
}) })
} }

View file

@ -80,7 +80,7 @@ func (r Rules) Remove(rule Rule) Rules {
} }
func (r Rules) Insert(idx int, rules ...Rule) Rules { func (r Rules) Insert(idx int, rules ...Rule) Rules {
return append(r[:idx], append(rules, r[idx:]...)...) return append(r[:idx], append(rules, r[idx+1:]...)...)
} }
func (r Rules) Sort() Rules { func (r Rules) Sort() Rules {

2
tests/testdata/tunables/dir.d/aliases vendored Normal file
View file

@ -0,0 +1,2 @@
alias /usr/ -> /User/,
alias /lib/ -> /Libraries/,

2
tests/testdata/tunables/dir.d/vars vendored Normal file
View file

@ -0,0 +1,2 @@
# variable declarations for inclusion
@{FOO} = /foo /bar /baz /biff /lib /tmp

3
tests/testdata/tunables/global vendored Normal file
View file

@ -0,0 +1,3 @@
include <tunables/dir.d>