feat(aa-log): add a new apparmor profile struct

Also rewrite variables resolution to this new struct.
This commit is contained in:
Alexandre Pujol 2023-08-17 23:00:52 +01:00
parent b2d093e125
commit a8470dfa38
Failed to generate hash of commit
5 changed files with 379 additions and 156 deletions

View file

@ -2,104 +2,19 @@
// Copyright (C) 2023 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
// 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{}
}

202
pkg/aa/rules.go Normal file
View file

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

117
pkg/aa/variables.go Normal file
View file

@ -0,0 +1,117 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2023 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"
"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, ",") + "}"
}
}

View file

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

View file

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