Merge branch 'feat/aa'

Improve go apparmor lib.

* aa: (62 commits)
  feat(aa): handle appending value to defined variables.
  chore(aa): cosmetic.
  fix: userspace prebuild test.
  chore: cleanup unit test.
  feat(aa): improve log conversion.
  feat(aa): move conversion function to its own file & add unit tests.
  fix: go linter issue & not defined variables.
  tests(aa): improve aa unit tests.
  tests(aa): improve rules unit tests.
  feat(aa): ensure the prebuild jobs are working.
  feat(aa): add more unit tests.
  chore(aa): cleanup.
  feat(aa): Move sort, merge and format methods to the rules interface.
  feat(aa): add the hat template.
  feat(aa): add the Kind struct to manage aa rules.
  feat(aa): cleanup rules methods.
  feat(aa): add function to resolve include preamble.
  feat(aa): updaqte mount flags order.
  feat(aa): update default tunable selection.
  feat(aa): parse apparmor preamble files.
  ...
This commit is contained in:
Alexandre Pujol 2024-05-30 19:29:34 +01:00
commit 89abbae6bd
No known key found for this signature in database
GPG key ID: C5469996F0DF68EC
90 changed files with 4995 additions and 2012 deletions

View file

@ -26,7 +26,7 @@ var (
// Main directive interface
type Directive interface {
cfg.BaseInterface
Apply(opt *Option, profile string) string
Apply(opt *Option, profile string) (string, error)
}
// Directive options
@ -72,14 +72,18 @@ func RegisterDirective(d Directive) {
Directives[d.Name()] = d
}
func Run(file *paths.Path, profile string) string {
func Run(file *paths.Path, profile string) (string, error) {
var err error
for _, match := range regDirective.FindAllStringSubmatch(profile, -1) {
opt := NewOption(file, match)
drtv, ok := Directives[opt.Name]
if !ok {
panic(fmt.Sprintf("Unknown directive: %s", opt.Name))
return "", fmt.Errorf("Unknown directive '%s' in %s", opt.Name, opt.File)
}
profile, err = drtv.Apply(opt, profile)
if err != nil {
return "", fmt.Errorf("%s %s: %w", drtv.Name(), opt.File, err)
}
profile = drtv.Apply(opt, profile)
}
return profile
return profile, nil
}

View file

@ -70,6 +70,7 @@ func TestRun(t *testing.T) {
file *paths.Path
profile string
want string
wantErr bool
}{
{
name: "none",
@ -86,7 +87,12 @@ func TestRun(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Run(tt.file, tt.profile); got != tt.want {
got, err := Run(tt.file, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Run() = %v, want %v", got, tt.want)
}
})

View file

@ -50,41 +50,47 @@ func setInterfaces(rules map[string]string) []string {
return interfaces
}
func (d Dbus) Apply(opt *Option, profile string) string {
var p *aa.AppArmorProfile
func (d Dbus) Apply(opt *Option, profile string) (string, error) {
var r aa.Rules
action := d.sanityCheck(opt)
action, err := d.sanityCheck(opt)
if err != nil {
return "", err
}
switch action {
case "own":
p = d.own(opt.ArgMap)
r = d.own(opt.ArgMap)
case "talk":
p = d.talk(opt.ArgMap)
r = d.talk(opt.ArgMap)
}
generatedDbus := p.String()
aa.IndentationLevel = strings.Count(
strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation,
)
generatedDbus := r.String()
lenDbus := len(generatedDbus)
generatedDbus = generatedDbus[:lenDbus-1]
profile = strings.Replace(profile, opt.Raw, generatedDbus, -1)
return profile
return profile, nil
}
func (d Dbus) sanityCheck(opt *Option) string {
func (d Dbus) sanityCheck(opt *Option) (string, error) {
if len(opt.ArgList) < 1 {
panic(fmt.Sprintf("Unknown dbus action: %s in %s", opt.Name, opt.File))
return "", fmt.Errorf("Unknown dbus action: %s in %s", opt.Name, opt.File)
}
action := opt.ArgList[0]
if action != "own" && action != "talk" {
panic(fmt.Sprintf("Unknown dbus action: %s in %s", opt.Name, opt.File))
return "", fmt.Errorf("Unknown dbus action: %s in %s", opt.Name, opt.File)
}
if _, present := opt.ArgMap["name"]; !present {
panic(fmt.Sprintf("Missing name for 'dbus: %s' in %s", action, opt.File))
return "", fmt.Errorf("Missing name for 'dbus: %s' in %s", action, opt.File)
}
if _, present := opt.ArgMap["bus"]; !present {
panic(fmt.Sprintf("Missing bus for '%s' in %s", opt.ArgMap["name"], opt.File))
return "", fmt.Errorf("Missing bus for '%s' in %s", opt.ArgMap["name"], opt.File)
}
if _, present := opt.ArgMap["label"]; !present && action == "talk" {
panic(fmt.Sprintf("Missing label for '%s' in %s", opt.ArgMap["name"], opt.File))
return "", fmt.Errorf("Missing label for '%s' in %s", opt.ArgMap["name"], opt.File)
}
// Set default values
@ -92,66 +98,66 @@ func (d Dbus) sanityCheck(opt *Option) string {
opt.ArgMap["path"] = "/" + strings.Replace(opt.ArgMap["name"], ".", "/", -1) + "{,/**}"
}
opt.ArgMap["name"] += "{,.*}"
return action
return action, nil
}
func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile {
func (d Dbus) own(rules map[string]string) aa.Rules {
interfaces := setInterfaces(rules)
p := &aa.AppArmorProfile{}
p.Rules = append(p.Rules, &aa.Dbus{
Access: "bind", Bus: rules["bus"], Name: rules["name"],
res := aa.Rules{}
res = append(res, &aa.Dbus{
Access: []string{"bind"}, Bus: rules["bus"], Name: rules["name"],
})
for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{
Access: "receive",
res = append(res, &aa.Dbus{
Access: []string{"receive"},
Bus: rules["bus"],
Path: rules["path"],
Interface: iface,
Name: `":1.@{int}"`,
PeerName: `":1.@{int}"`,
})
}
for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{
Access: "send",
res = append(res, &aa.Dbus{
Access: []string{"send"},
Bus: rules["bus"],
Path: rules["path"],
Interface: iface,
Name: `"{:1.@{int},org.freedesktop.DBus}"`,
PeerName: `"{:1.@{int},org.freedesktop.DBus}"`,
})
}
p.Rules = append(p.Rules, &aa.Dbus{
Access: "receive",
res = append(res, &aa.Dbus{
Access: []string{"receive"},
Bus: rules["bus"],
Path: rules["path"],
Interface: "org.freedesktop.DBus.Introspectable",
Member: "Introspect",
Name: `":1.@{int}"`,
PeerName: `":1.@{int}"`,
})
return p
return res
}
func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile {
func (d Dbus) talk(rules map[string]string) aa.Rules {
interfaces := setInterfaces(rules)
p := &aa.AppArmorProfile{}
res := aa.Rules{}
for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{
Access: "send",
res = append(res, &aa.Dbus{
Access: []string{"send"},
Bus: rules["bus"],
Path: rules["path"],
Interface: iface,
Name: `"{:1.@{int},` + rules["name"] + `}"`,
Label: rules["label"],
PeerName: `"{:1.@{int},` + rules["name"] + `}"`,
PeerLabel: rules["label"],
})
}
for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{
Access: "receive",
res = append(res, &aa.Dbus{
Access: []string{"receive"},
Bus: rules["bus"],
Path: rules["path"],
Interface: iface,
Name: `"{:1.@{int},` + rules["name"] + `}"`,
Label: rules["label"],
PeerName: `"{:1.@{int},` + rules["name"] + `}"`,
PeerLabel: rules["label"],
})
}
return p
return res
}

View file

@ -38,6 +38,7 @@ func TestDbus_Apply(t *testing.T) {
opt *Option
profile string
want string
wantErr bool
}{
{
name: "own",
@ -137,7 +138,12 @@ func TestDbus_Apply(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Directives["dbus"].Apply(tt.opt, tt.profile); got != tt.want {
got, err := Directives["dbus"].Apply(tt.opt, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("Dbus.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Dbus.Apply() = %v, want %v", got, tt.want)
}
})

View file

@ -2,6 +2,8 @@
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
// TODO: Local variables in profile header need to be resolved
package directive
import (
@ -27,7 +29,7 @@ func init() {
})
}
func (d Exec) Apply(opt *Option, profile string) string {
func (d Exec) Apply(opt *Option, profileRaw string) (string, error) {
transition := "Px"
transitions := []string{"P", "U", "p", "u", "PU", "pu"}
t := opt.ArgList[0]
@ -36,26 +38,34 @@ func (d Exec) Apply(opt *Option, profile string) string {
delete(opt.ArgMap, t)
}
p := &aa.AppArmorProfile{}
rules := aa.Rules{}
for name := range opt.ArgMap {
profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name))
dstProfile := aa.DefaultTunables()
dstProfile.ParseVariables(profiletoTransition)
for _, variable := range dstProfile.Variables {
if err := dstProfile.Parse(profiletoTransition); err != nil {
return "", err
}
if err := dstProfile.Resolve(); err != nil {
return "", err
}
for _, variable := range dstProfile.Preamble.GetVariables() {
if variable.Name == "exec_path" {
for _, v := range variable.Values {
p.Rules = append(p.Rules, &aa.File{
rules = append(rules, &aa.File{
Path: v,
Access: transition,
Access: []string{transition},
})
}
break
}
}
}
p.Sort()
rules := p.String()
lenRules := len(rules)
rules = rules[:lenRules-1]
return strings.Replace(profile, opt.Raw, rules, -1)
aa.IndentationLevel = strings.Count(
strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation,
)
rules = rules.Sort()
new := rules.String()
new = new[:len(new)-1]
return strings.Replace(profileRaw, opt.Raw, new, -1), nil
}

View file

@ -18,6 +18,7 @@ func TestExec_Apply(t *testing.T) {
opt *Option
profile string
want string
wantErr bool
}{
{
name: "exec",
@ -30,8 +31,8 @@ func TestExec_Apply(t *testing.T) {
Raw: " #aa:exec DiscoverNotifier",
},
profile: ` #aa:exec DiscoverNotifier`,
want: ` @{lib}/@{multiarch}/{,libexec/}DiscoverNotifier Px,
@{lib}/DiscoverNotifier Px,`,
want: ` /{,usr/}lib{,exec,32,64}/*-linux-gnu*/{,libexec/}DiscoverNotifier Px,
/{,usr/}lib{,exec,32,64}/DiscoverNotifier Px,`,
},
{
name: "exec-unconfined",
@ -44,15 +45,20 @@ func TestExec_Apply(t *testing.T) {
Raw: " #aa:exec U polkit-agent-helper",
},
profile: ` #aa:exec U polkit-agent-helper`,
want: ` @{lib}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux,
@{lib}/polkit-agent-helper-[0-9] Ux,`,
want: ` /{,usr/}lib{,exec,32,64}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux,
/{,usr/}lib{,exec,32,64}/polkit-agent-helper-[0-9] Ux,`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg.RootApparmord = tt.rootApparmord
if got := Directives["exec"].Apply(tt.opt, tt.profile); got != tt.want {
t.Errorf("Exec.Apply() = %v, want %v", got, tt.want)
got, err := Directives["exec"].Apply(tt.opt, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("Exec.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Exec.Apply() = |%v|, want |%v|", got, tt.want)
}
})
}

View file

@ -41,12 +41,12 @@ func filterRuleForUs(opt *Option) bool {
return slices.Contains(opt.ArgList, cfg.Distribution) || slices.Contains(opt.ArgList, cfg.Family)
}
func filter(only bool, opt *Option, profile string) string {
func filter(only bool, opt *Option, profile string) (string, error) {
if only && filterRuleForUs(opt) {
return opt.Clean(profile)
return opt.Clean(profile), nil
}
if !only && !filterRuleForUs(opt) {
return opt.Clean(profile)
return opt.Clean(profile), nil
}
inline := true
@ -64,13 +64,13 @@ func filter(only bool, opt *Option, profile string) string {
regRemoveParagraph := regexp.MustCompile(`(?s)` + opt.Raw + `\n.*?\n\n`)
profile = regRemoveParagraph.ReplaceAllString(profile, "")
}
return profile
return profile, nil
}
func (d FilterOnly) Apply(opt *Option, profile string) string {
func (d FilterOnly) Apply(opt *Option, profile string) (string, error) {
return filter(true, opt, profile)
}
func (d FilterExclude) Apply(opt *Option, profile string) string {
func (d FilterExclude) Apply(opt *Option, profile string) (string, error) {
return filter(false, opt, profile)
}

View file

@ -18,6 +18,7 @@ func TestFilterOnly_Apply(t *testing.T) {
opt *Option
profile string
want string
wantErr bool
}{
{
name: "inline",
@ -79,7 +80,12 @@ func TestFilterOnly_Apply(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
cfg.Distribution = tt.dist
cfg.Family = tt.family
if got := Directives["only"].Apply(tt.opt, tt.profile); got != tt.want {
got, err := Directives["only"].Apply(tt.opt, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("FilterOnly.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("FilterOnly.Apply() = %v, want %v", got, tt.want)
}
})
@ -94,6 +100,7 @@ func TestFilterExclude_Apply(t *testing.T) {
opt *Option
profile string
want string
wantErr bool
}{
{
name: "inline",
@ -128,7 +135,12 @@ func TestFilterExclude_Apply(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
cfg.Distribution = tt.dist
cfg.Family = tt.family
if got := Directives["exclude"].Apply(tt.opt, tt.profile); got != tt.want {
got, err := Directives["exclude"].Apply(tt.opt, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("FilterExclude.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("FilterExclude.Apply() = %v, want %v", got, tt.want)
}
})

View file

@ -38,13 +38,13 @@ func init() {
})
}
func (s Stack) Apply(opt *Option, profile string) string {
func (s Stack) Apply(opt *Option, profile string) (string, error) {
res := ""
for name := range opt.ArgMap {
stackedProfile := util.MustReadFile(cfg.RootApparmord.Join(name))
m := regRules.FindStringSubmatch(stackedProfile)
if len(m) < 2 {
panic(fmt.Sprintf("No profile found in %s", name))
return "", fmt.Errorf("No profile found in %s", name)
}
stackedRules := m[1]
stackedRules = regCleanStakedRules.Replace(stackedRules)
@ -54,9 +54,9 @@ func (s Stack) Apply(opt *Option, profile string) string {
// Insert the stacked profile at the end of the current profile, remove the stack directive
m := regEndOfRules.FindStringSubmatch(profile)
if len(m) <= 1 {
panic(fmt.Sprintf("No end of rules found in %s", opt.File))
return "", fmt.Errorf("No end of rules found in %s", opt.File)
}
profile = strings.Replace(profile, m[0], res+m[0], -1)
profile = strings.Replace(profile, opt.Raw, "", -1)
return profile
return profile, nil
}

View file

@ -18,6 +18,7 @@ func TestStack_Apply(t *testing.T) {
opt *Option
profile string
want string
wantErr bool
}{
{
name: "stack",
@ -68,7 +69,12 @@ profile parent @{exec_path} {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg.RootApparmord = tt.rootApparmord
if got := Directives["stack"].Apply(tt.opt, tt.profile); got != tt.want {
got, err := Directives["stack"].Apply(tt.opt, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("Stack.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Stack.Apply() = %v, want %v", got, tt.want)
}
})