From a8470dfa38de49e4f3fa4282fb2ec7c08d0b4dec Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Thu, 17 Aug 2023 23:00:52 +0100 Subject: [PATCH] feat(aa-log): add a new apparmor profile struct Also rewrite variables resolution to this new struct. --- pkg/aa/profile.go | 103 +-------- pkg/aa/rules.go | 202 ++++++++++++++++++ pkg/aa/variables.go | 117 ++++++++++ pkg/aa/{profile_test.go => variables_test.go} | 111 +++++----- pkg/prebuild/build.go | 2 +- 5 files changed, 379 insertions(+), 156 deletions(-) create mode 100644 pkg/aa/rules.go create mode 100644 pkg/aa/variables.go rename pkg/aa/{profile_test.go => variables_test.go} (62%) diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 73d14d5e..94c14fd5 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -2,104 +2,19 @@ // Copyright (C) 2023 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 +// AppArmorProfiles represents a full set of apparmor profiles +type AppArmorProfiles map[string]*AppArmorProfile -import ( - "regexp" - "strings" - - "golang.org/x/exp/maps" -) - -var ( - regVariablesDef = regexp.MustCompile(`@{(.*)}\s*[+=]+\s*(.*)`) - regVariablesRef = regexp.MustCompile(`@{([^{}]+)}`) - - // Tunables - Tunables = map[string][]string{ - "bin": {"/{usr/,}{s,}bin"}, - "lib": {"/{usr/,}lib{,exec,32,64}"}, - "multiarch": {"*-linux-gnu*"}, - "user_share_dirs": {"/home/*/.local/share"}, - "etc_ro": {"/{usr/,}etc/"}, - } -) - +// ApparmorProfile represents a full apparmor profile. +// Warning: close to the BNF grammar of apparmor profile but not exactly the same (yet): +// - Some rules are not supported yet (subprofile, hat...) +// - The structure is simplified as it only aims at writting profile, not parsing it. type AppArmorProfile struct { - Variables map[string][]string - Attachments []string + Preamble + Profile } func NewAppArmorProfile() *AppArmorProfile { - variables := make(map[string][]string) - maps.Copy(variables, Tunables) - return &AppArmorProfile{ - Variables: variables, - Attachments: []string{}, - } -} - -// ParseVariables extract all variables from the profile -func (p *AppArmorProfile) ParseVariables(content string) { - matches := regVariablesDef.FindAllStringSubmatch(content, -1) - for _, match := range matches { - if len(match) > 2 { - key := match[1] - values := match[2] - if _, ok := p.Variables[key]; ok { - p.Variables[key] = append(p.Variables[key], strings.Split(values, " ")...) - } else { - p.Variables[key] = strings.Split(values, " ") - } - } - } -} - -// resolve recursively resolves all variables references -func (p *AppArmorProfile) 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 _, value := range p.Variables[varname] { - newVar := strings.ReplaceAll(str, variable, value) - vars = append(vars, p.resolve(newVar)...) - } - } else { - vars = append(vars, str) - } - return vars - } - return []string{str} -} - -// ResolveAttachments resolve profile attachments defined in exec_path -func (p *AppArmorProfile) ResolveAttachments() { - for _, exec := range p.Variables["exec_path"] { - p.Attachments = append(p.Attachments, p.resolve(exec)...) - } -} - -// NestAttachments return a nested attachment string -func (p *AppArmorProfile) NestAttachments() 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, ",") + "}" - } + return &AppArmorProfile{} } diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go new file mode 100644 index 00000000..cc778309 --- /dev/null +++ b/pkg/aa/rules.go @@ -0,0 +1,202 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +// Preamble section of a profile +type Preamble struct { + Abi []Abi + PreambleIncludes []Include + Aliases []Alias + Variables []Variable +} + +// Profile section of a profile +type Profile struct { + Name string + Attachments []string + Attributes []string + Flags []string + Rules +} + +type Rules struct { + Includes []Include + Rlimit []Rlimit + Userns Userns + Capability []Capability + Network []Network + Mount []Mount + Umount []Umount + Remount []Remount + PivotRoot []PivotRoot + ChangeProfile []ChangeProfile + Unix []Unix + Ptrace []Ptrace + Signal []Signal + Dbus []Dbus + File []File +} + + +// Qualifier to apply extra settings to a rule +type Qualifier struct { + Audit bool + AccessType string + Owner bool + NoNewPrivs bool + FileInherit bool +} + +// Preamble rules + +type Abi struct { + AbsPath string + MagicPath string +} + +type Alias struct { + Path string + RewrittenPath string +} + +type Include struct { + IfExists bool + AbsPath string + MagicPath string +} + +type Variable struct { + Name string + Values []string +} + +// Profile rules + +type Rlimit struct { + Key string + Op string + Value string +} + +type Userns struct { + Qualifier + Create bool +} + +type Capability struct { + Qualifier + Name string +} + +type AddressExpr struct { + Source string + Destination string + Port string +} + +type Network struct { + Qualifier + Domain string + Type string + Protocol string + AddressExpr +} + +type MountConditions struct { + Fs string + Op string + FsType string + Options []string +} + +type Mount struct { + Qualifier + MountConditions + Source string + MountPoint string +} + +type Umount struct { + Qualifier + MountConditions + MountPoint string +} + +type Remount struct { + Qualifier + MountConditions + MountPoint string +} + +type PivotRoot struct { + Qualifier + OldRoot string + NewRoot string + TargetProfile string +} + +type ChangeProfile struct { + ExecMode string + Exec string + ProfileName string +} + +type IOUring struct { + Qualifier + Access string + Label string +} + +type Signal struct { + Qualifier + Access string + Set string + Peer string +} + +type Ptrace struct { + Qualifier + Access string + Peer string +} + +type Unix struct { + Qualifier + Access string + Type string + Protocol string + Address string + Label string + Attr string + Opt string + Peer string + PeerAddr string +} + +type Mqueue struct { + Qualifier + Access string + Type string + Label string +} + +type Dbus struct { + Qualifier + Access string + Bus string + Name string + Path string + Interface string + Member string + Label string +} + +type File struct { + Qualifier + Path string + Access string + Target string +} + diff --git a/pkg/aa/variables.go b/pkg/aa/variables.go new file mode 100644 index 00000000..635c2c86 --- /dev/null +++ b/pkg/aa/variables.go @@ -0,0 +1,117 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2021-2023 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" + + "github.com/arduino/go-paths-helper" +) + +var ( + regVariablesDef = regexp.MustCompile(`@{(.*)}\s*[+=]+\s*(.*)`) + regVariablesRef = regexp.MustCompile(`@{([^{}]+)}`) +) + +// Default Apparmor magic directory: /etc/apparmor.d/. +var MagicRoot = paths.New("/etc/apparmor.d") + +// DefaultTunables return a minimal working profile to build the profile +// It should not be used when loading file from /etc/apparmor.d +func DefaultTunables() *AppArmorProfile { + return &AppArmorProfile{ + Preamble: Preamble{ + Variables: []Variable{ + {"bin", []string{"/{usr/,}{s,}bin"}}, + {"lib", []string{"/{usr/,}lib{,exec,32,64}"}}, + {"multiarch", []string{"*-linux-gnu*"}}, + {"user_share_dirs", []string{"/home/*/.local/share"}}, + {"etc_ro", []string{"/{usr/,}etc/"}}, + }, + }, + } +} + +// ParseVariables extract all variables from the profile +func (p *AppArmorProfile) 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 p.Variables { + if variable.Name == key { + p.Variables[idx].Values = append(p.Variables[idx].Values, values...) + found = true + break + } + } + if !found { + variable := Variable{Name: key, Values: values} + p.Variables = append(p.Variables, variable) + } + } + } +} + +// resolve recursively resolves all variables references +func (p *AppArmorProfile) 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 p.Variables { + if vrbl.Name == varname { + for _, value := range vrbl.Values { + newVar := strings.ReplaceAll(str, variable, value) + vars = append(vars, p.resolve(newVar)...) + } + } + } + } else { + vars = append(vars, str) + } + return vars + } + return []string{str} +} + +// ResolveAttachments resolve profile attachments defined in exec_path +func (p *AppArmorProfile) ResolveAttachments() { + for _, variable := range p.Variables { + if variable.Name == "exec_path" { + for _, value := range variable.Values { + p.Attachments = append(p.Attachments, p.resolve(value)...) + } + } + } +} + +// NestAttachments return a nested attachment string +func (p *AppArmorProfile) NestAttachments() 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, ",") + "}" + } +} + diff --git a/pkg/aa/profile_test.go b/pkg/aa/variables_test.go similarity index 62% rename from pkg/aa/profile_test.go rename to pkg/aa/variables_test.go index bd47e7a6..c9f1425a 100644 --- a/pkg/aa/profile_test.go +++ b/pkg/aa/variables_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -func TestNewAppArmorProfile(t *testing.T) { +func TestDefaultTunables(t *testing.T) { tests := []struct { name string want *AppArmorProfile @@ -17,20 +17,21 @@ func TestNewAppArmorProfile(t *testing.T) { { name: "aa", want: &AppArmorProfile{ - Variables: map[string][]string{ - "bin": {"/{usr/,}{s,}bin"}, - "lib": {"/{usr/,}lib{,exec,32,64}"}, - "multiarch": {"*-linux-gnu*"}, - "user_share_dirs": {"/home/*/.local/share"}, - "etc_ro": {"/{usr/,}etc/"}, + Preamble: Preamble{ + Variables: []Variable{ + {"bin", []string{"/{usr/,}{s,}bin"}}, + {"lib", []string{"/{usr/,}lib{,exec,32,64}"}}, + {"multiarch", []string{"*-linux-gnu*"}}, + {"user_share_dirs", []string{"/home/*/.local/share"}}, + {"etc_ro", []string{"/{usr/,}etc/"}}, + }, }, - Attachments: []string{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := NewAppArmorProfile(); !reflect.DeepEqual(got, tt.want) { + if got := DefaultTunables(); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewAppArmorProfile() = %v, want %v", got, tt.want) } }) @@ -41,7 +42,7 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { tests := []struct { name string content string - want map[string][]string + want []Variable }{ { name: "firefox", @@ -51,12 +52,12 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { @{firefox_cache_dirs} = @{user_cache_dirs}/mozilla/ @{exec_path} = /{usr/,}bin/@{firefox_name} @{firefox_lib_dirs}/@{firefox_name} `, - want: map[string][]string{ - "firefox_name": {"firefox{,-esr,-bin}"}, - "firefox_config_dirs": {"@{HOME}/.mozilla/"}, - "firefox_lib_dirs": {"/{usr/,}lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}, - "firefox_cache_dirs": {"@{user_cache_dirs}/mozilla/"}, - "exec_path": {"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}, + want: []Variable{ + {"firefox_name", []string{"firefox{,-esr,-bin}"}}, + {"firefox_lib_dirs", []string{"/{usr/,}lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, + {"firefox_config_dirs", []string{"@{HOME}/.mozilla/"}}, + {"firefox_cache_dirs", []string{"@{user_cache_dirs}/mozilla/"}}, + {"exec_path", []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, }, }, { @@ -65,23 +66,19 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { @{exec_path} += /{usr/,}bin/Xorg{,.bin} @{exec_path} += /{usr/,}lib/Xorg{,.wrap} @{exec_path} += /{usr/,}lib/xorg/Xorg{,.wrap}`, - want: map[string][]string{ - "exec_path": { + want: []Variable{ + {"exec_path", []string{ "/{usr/,}bin/X", "/{usr/,}bin/Xorg{,.bin}", "/{usr/,}lib/Xorg{,.wrap}", - "/{usr/,}lib/xorg/Xorg{,.wrap}", + "/{usr/,}lib/xorg/Xorg{,.wrap}"}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := &AppArmorProfile{ - Variables: map[string][]string{}, - Attachments: []string{}, - } - + p := NewAppArmorProfile() p.ParseVariables(tt.content) if !reflect.DeepEqual(p.Variables, tt.want) { t.Errorf("AppArmorProfile.ParseVariables() = %v, want %v", p.Variables, tt.want) @@ -92,24 +89,19 @@ func TestAppArmorProfile_ParseVariables(t *testing.T) { func TestAppArmorProfile_resolve(t *testing.T) { tests := []struct { - name string - variables map[string][]string - input string - want []string + name string + input string + want []string }{ { - name: "empty", - variables: Tunables, - input: "@{}", - want: []string{"@{}"}, + name: "empty", + input: "@{}", + want: []string{"@{}"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := &AppArmorProfile{ - Variables: tt.variables, - Attachments: []string{}, - } + p := DefaultTunables() if got := p.resolve(tt.input); !reflect.DeepEqual(got, tt.want) { t.Errorf("AppArmorProfile.resolve() = %v, want %v", got, tt.want) } @@ -120,15 +112,15 @@ func TestAppArmorProfile_resolve(t *testing.T) { func TestAppArmorProfile_ResolveAttachments(t *testing.T) { tests := []struct { name string - variables map[string][]string + variables []Variable want []string }{ { name: "firefox", - variables: map[string][]string{ - "firefox_name": {"firefox{,-esr,-bin}"}, - "firefox_lib_dirs": {"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}, - "exec_path": {"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}, + variables: []Variable{ + {"firefox_name", []string{"firefox{,-esr,-bin}"}}, + {"firefox_lib_dirs", []string{"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}}, + {"exec_path", []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}}, }, want: []string{ "/{usr/,}bin/firefox{,-esr,-bin}", @@ -138,10 +130,10 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { }, { name: "chromium", - variables: map[string][]string{ - "chromium_name": {"chromium"}, - "chromium_lib_dirs": {"/{usr/,}lib/@{chromium_name}"}, - "exec_path": {"@{chromium_lib_dirs}/@{chromium_name}"}, + variables: []Variable{ + {"chromium_name", []string{"chromium"}}, + {"chromium_lib_dirs", []string{"/{usr/,}lib/@{chromium_name}"}}, + {"exec_path", []string{"@{chromium_lib_dirs}/@{chromium_name}"}}, }, want: []string{ "/{usr/,}lib/chromium/chromium", @@ -149,9 +141,9 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { }, { name: "geoclue", - variables: map[string][]string{ - "libexec": {"/{usr/,}libexec"}, - "exec_path": {"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}, + variables: []Variable{ + {"libexec", []string{"/{usr/,}libexec"}}, + {"exec_path", []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}}, }, want: []string{ "/{usr/,}libexec/geoclue", @@ -160,11 +152,11 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { }, { name: "opera", - variables: map[string][]string{ - "multiarch": {"*-linux-gnu*"}, - "chromium_name": {"opera{,-beta,-developer}"}, - "chromium_lib_dirs": {"/{usr/,}lib/@{multiarch}/@{chromium_name}"}, - "exec_path": {"@{chromium_lib_dirs}/@{chromium_name}"}, + variables: []Variable{ + {"multiarch", []string{"*-linux-gnu*"}}, + {"chromium_name", []string{"opera{,-beta,-developer}"}}, + {"chromium_lib_dirs", []string{"/{usr/,}lib/@{multiarch}/@{chromium_name}"}}, + {"exec_path", []string{"@{chromium_lib_dirs}/@{chromium_name}"}}, }, want: []string{ "/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}", @@ -173,10 +165,8 @@ func TestAppArmorProfile_ResolveAttachments(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := &AppArmorProfile{ - Variables: tt.variables, - Attachments: []string{}, - } + p := NewAppArmorProfile() + p.Variables = tt.variables p.ResolveAttachments() if !reflect.DeepEqual(p.Attachments, tt.want) { t.Errorf("AppArmorProfile.ResolveAttachments() = %v, want %v", p.Attachments, tt.want) @@ -226,13 +216,12 @@ func TestAppArmorProfile_NestAttachments(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := &AppArmorProfile{ - Variables: map[string][]string{}, - Attachments: tt.Attachments, - } + p := NewAppArmorProfile() + p.Attachments = tt.Attachments if got := p.NestAttachments(); got != tt.want { t.Errorf("AppArmorProfile.NestAttachments() = %v, want %v", got, tt.want) } }) } } + diff --git a/pkg/prebuild/build.go b/pkg/prebuild/build.go index 8c0738be..0b43a615 100644 --- a/pkg/prebuild/build.go +++ b/pkg/prebuild/build.go @@ -45,7 +45,7 @@ func BuildComplain(profile string) string { // Bypass userspace tools restriction func BuildUserspace(profile string) string { - p := aa.NewAppArmorProfile() + p := aa.DefaultTunables() p.ParseVariables(profile) p.ResolveAttachments() att := p.NestAttachments()