From dc0e0084a05c769d88aff587beaa1c67d0852cc3 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 28 May 2024 11:53:32 +0100 Subject: [PATCH] feat(aa): add function to resolve include preamble. --- pkg/aa/resolve.go | 138 ++++++++++++++++++++++---- pkg/aa/resolve_test.go | 80 ++++++++++++--- pkg/aa/rules.go | 2 +- tests/testdata/tunables/dir.d/aliases | 2 + tests/testdata/tunables/dir.d/vars | 2 + tests/testdata/tunables/global | 3 + 6 files changed, 191 insertions(+), 36 deletions(-) create mode 100644 tests/testdata/tunables/dir.d/aliases create mode 100644 tests/testdata/tunables/dir.d/vars create mode 100644 tests/testdata/tunables/global diff --git a/pkg/aa/resolve.go b/pkg/aa/resolve.go index 308c22f1..85426340 100644 --- a/pkg/aa/resolve.go +++ b/pkg/aa/resolve.go @@ -8,21 +8,34 @@ import ( "fmt" "regexp" "strings" + + "github.com/arduino/go-paths-helper" + "github.com/roddhjav/apparmor.d/pkg/util" ) var ( + includeCache map[*Include]*AppArmorProfileFile = make(map[*Include]*AppArmorProfileFile) + 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 { + // Resolve preamble includes + for _, include := range f.Preamble.GetIncludes() { + err := f.resolveInclude(include) + if err != nil { + return err + } + } + // 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) + vars, err := f.resolveValues(value) + if err != nil { + return err } newValues = append(newValues, vars...) } @@ -33,9 +46,9 @@ func (f *AppArmorProfileFile) Resolve() error { 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) + vars, err := f.resolveValues(att) + if err != nil { + return err } attachments = append(attachments, vars...) } @@ -45,27 +58,108 @@ func (f *AppArmorProfileFile) Resolve() error { return nil } -func (f *AppArmorProfileFile) resolveVariable(input string) []string { +func (f *AppArmorProfileFile) resolveValues(input string) ([]string, error) { if !strings.Contains(input, tokVARIABLE) { - return []string{input} + return []string{input}, nil } - vars := []string{} + values := []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...) + if len(match) == 0 { + return nil, fmt.Errorf("Invalid variable reference: %s", input) + } + + variable := match[0] + varname := match[1] + found := false + for _, vrbl := range f.Preamble.GetVariables() { + if vrbl.Name == varname { + found = true + for _, v := range vrbl.Values { + if strings.Contains(v, tokVARIABLE+varname+"}") { + return nil, fmt.Errorf("recursive variable found in: %s", varname) } + 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...) } } - } else { - vars = append(vars, input) } - return vars + + 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 { + if include.IfExists { + return nil + } + 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 } diff --git a/pkg/aa/resolve_test.go b/pkg/aa/resolve_test.go index 3c6f20c6..8e4ff6b8 100644 --- a/pkg/aa/resolve_test.go +++ b/pkg/aa/resolve_test.go @@ -7,24 +7,75 @@ package aa import ( "reflect" "testing" + + "github.com/arduino/go-paths-helper" ) -func TestAppArmorProfileFile_resolveVariable(t *testing.T) { +func TestAppArmorProfileFile_resolveInclude(t *testing.T) { tests := []struct { - name string - f AppArmorProfileFile - input string - want []string + name string + include *Include + want *AppArmorProfileFile + wantErr bool }{ { - name: "nil", - input: "@{newvar}", - want: []string{}, + name: "empty", + include: &Include{Path: "", IsMagic: true}, + want: &AppArmorProfileFile{Preamble: Rules{&Include{Path: "", IsMagic: true}}}, + wantErr: true, }, { - name: "empty", - input: "@{}", - want: []string{"@{}"}, + 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: "@{}", + want: nil, + wantErr: true, }, { name: "default", @@ -45,9 +96,12 @@ func TestAppArmorProfileFile_resolveVariable(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { 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) { - t.Errorf("AppArmorProfileFile.resolveVariable() = %v, want %v", got, tt.want) + t.Errorf("AppArmorProfileFile.resolveValues() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index ec28779f..895d5f28 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -80,7 +80,7 @@ func (r Rules) Remove(rule 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 { diff --git a/tests/testdata/tunables/dir.d/aliases b/tests/testdata/tunables/dir.d/aliases new file mode 100644 index 00000000..61db9635 --- /dev/null +++ b/tests/testdata/tunables/dir.d/aliases @@ -0,0 +1,2 @@ +alias /usr/ -> /User/, +alias /lib/ -> /Libraries/, diff --git a/tests/testdata/tunables/dir.d/vars b/tests/testdata/tunables/dir.d/vars new file mode 100644 index 00000000..505ca6d3 --- /dev/null +++ b/tests/testdata/tunables/dir.d/vars @@ -0,0 +1,2 @@ +# variable declarations for inclusion +@{FOO} = /foo /bar /baz /biff /lib /tmp diff --git a/tests/testdata/tunables/global b/tests/testdata/tunables/global new file mode 100644 index 00000000..145ba3b7 --- /dev/null +++ b/tests/testdata/tunables/global @@ -0,0 +1,3 @@ + +include +