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

@ -6,7 +6,7 @@ abi <abi/3.0>,
include <tunables/global> include <tunables/global>
profile default-sudo @{exec_path} { profile default-sudo {
include <abstractions/base> include <abstractions/base>
include <abstractions/app/sudo> include <abstractions/app/sudo>

View file

@ -12,7 +12,7 @@ abi <abi/3.0>,
include <tunables/global> include <tunables/global>
profile systemd-service @{exec_path} flags=(attach_disconnected) { profile systemd-service flags=(attach_disconnected) {
include <abstractions/base> include <abstractions/base>
include <abstractions/consoles> include <abstractions/consoles>
include <abstractions/nameservice-strict> include <abstractions/nameservice-strict>

View file

@ -14,7 +14,7 @@ profile aa-status @{exec_path} {
capability dac_read_search, capability dac_read_search,
capability sys_ptrace, capability sys_ptrace,
ptrace (read), ptrace read,
@{exec_path} mr, @{exec_path} mr,

View file

@ -67,11 +67,11 @@ func aaLog(logger string, path string, profile string) error {
aaLogs := logs.NewApparmorLogs(file, profile) aaLogs := logs.NewApparmorLogs(file, profile)
if rules { if rules {
profiles := aaLogs.ParseToProfiles() profiles := aaLogs.ParseToProfiles()
for _, profile := range profiles { for _, p := range profiles {
profile.MergeRules() p.Merge()
profile.Sort() p.Sort()
profile.Format() p.Format()
fmt.Print(profile.String() + "\n") fmt.Print(p.String() + "\n\n")
} }
} else { } else {
fmt.Print(aaLogs.String()) fmt.Print(aaLogs.String())

37
pkg/aa/all.go Normal file
View file

@ -0,0 +1,37 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
const (
ALL Kind = "all"
)
type All struct {
RuleBase
}
func (r *All) Validate() error {
return nil
}
func (r *All) Less(other any) bool {
return false
}
func (r *All) Equals(other any) bool {
return false
}
func (r *All) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *All) Constraint() constraint {
return blockKind
}
func (r *All) Kind() Kind {
return ALL
}

105
pkg/aa/apparmor.go Normal file
View file

@ -0,0 +1,105 @@
// 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
import (
"github.com/roddhjav/apparmor.d/pkg/paths"
)
// Default Apparmor magic directory: /etc/apparmor.d/.
var MagicRoot = paths.New("/etc/apparmor.d")
// AppArmorProfileFiles represents a full set of apparmor profiles
type AppArmorProfileFiles map[string]*AppArmorProfileFile
// AppArmorProfileFile represents a full apparmor profile file.
// 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 writing profile, not parsing it.
type AppArmorProfileFile struct {
Preamble Rules
Profiles []*Profile
}
func NewAppArmorProfile() *AppArmorProfileFile {
return &AppArmorProfileFile{}
}
// DefaultTunables return a minimal working profile to build the profile
// It should not be used when loading file from /etc/apparmor.d
func DefaultTunables() *AppArmorProfileFile {
return &AppArmorProfileFile{
Preamble: Rules{
&Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}, Define: true},
&Variable{Name: "etc_ro", Values: []string{"/{,usr/}etc/"}, Define: true},
&Variable{Name: "HOME", Values: []string{"/home/*"}, Define: true},
&Variable{Name: "int", Values: []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}, Define: true},
&Variable{Name: "int2", Values: []string{"[0-9][0-9]"}, Define: true},
&Variable{Name: "lib", Values: []string{"/{,usr/}lib{,exec,32,64}"}, Define: true},
&Variable{Name: "MOUNTS", Values: []string{"/media/*/", "/run/media/*/*/", "/mnt/*/"}, Define: true},
&Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true},
&Variable{Name: "run", Values: []string{"/run/", "/var/run/"}, Define: true},
&Variable{Name: "uid", Values: []string{"{[0-9],[1-9][0-9],[1-9][0-9][0-9],[1-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9],[1-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9],[1-4][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]}"}, Define: true},
&Variable{Name: "user_cache_dirs", Values: []string{"/home/*/.cache"}, Define: true},
&Variable{Name: "user_config_dirs", Values: []string{"/home/*/.config"}, Define: true},
&Variable{Name: "user_share_dirs", Values: []string{"/home/*/.local/share"}, Define: true},
},
}
}
// String returns the formatted representation of a profile file as a string
func (f *AppArmorProfileFile) String() string {
return renderTemplate("apparmor", f)
}
// Validate the profile file
func (f *AppArmorProfileFile) Validate() error {
if err := f.Preamble.Validate(); err != nil {
return err
}
for _, p := range f.Profiles {
if err := p.Validate(); err != nil {
return err
}
}
return nil
}
// GetDefaultProfile ensure a profile is always present in the profile file and
// return it, as a default profile.
func (f *AppArmorProfileFile) GetDefaultProfile() *Profile {
if len(f.Profiles) == 0 {
f.Profiles = append(f.Profiles, &Profile{})
}
return f.Profiles[0]
}
// Sort the rules in the profile
// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines
func (f *AppArmorProfileFile) Sort() {
for _, p := range f.Profiles {
p.Sort()
}
}
// MergeRules merge similar rules together.
// Steps:
// - Remove identical rules
// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw'
//
// Note: logs.regCleanLogs helps a lot to do a first cleaning
func (f *AppArmorProfileFile) MergeRules() {
for _, p := range f.Profiles {
p.Merge()
}
}
// Format the profile for better readability before printing it.
// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block
func (f *AppArmorProfileFile) Format() {
for _, p := range f.Profiles {
p.Format()
}
}

250
pkg/aa/apparmor_test.go Normal file
View file

@ -0,0 +1,250 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"reflect"
"testing"
"github.com/roddhjav/apparmor.d/pkg/paths"
"github.com/roddhjav/apparmor.d/pkg/util"
)
var (
testData = paths.New("../../").Join("tests")
intData = paths.New("../../").Join("apparmor.d")
)
func TestAppArmorProfileFile_String(t *testing.T) {
tests := []struct {
name string
f *AppArmorProfileFile
want string
}{
{
name: "empty",
f: &AppArmorProfileFile{},
want: ``,
},
{
name: "foo",
f: &AppArmorProfileFile{
Preamble: Rules{
&Comment{RuleBase: RuleBase{Comment: " Simple test profile for the AppArmorProfileFile.String() method", IsLineRule: true}},
nil,
&Abi{IsMagic: true, Path: "abi/4.0"},
&Alias{Path: "/mnt/usr", RewrittenPath: "/usr"},
&Include{IsMagic: true, Path: "tunables/global"},
&Variable{
Name: "exec_path", Define: true,
Values: []string{"@{bin}/foo", "@{lib}/foo"},
},
},
Profiles: []*Profile{{
Header: Header{
Name: "foo",
Attachments: []string{"@{exec_path}"},
Attributes: map[string]string{"security.tagged": "allowed"},
Flags: []string{"complain", "attach_disconnected"},
},
Rules: Rules{
&Include{IsMagic: true, Path: "abstractions/base"},
&Include{IsMagic: true, Path: "abstractions/nameservice-strict"},
rlimit1,
&Capability{Names: []string{"dac_read_search"}},
&Capability{Names: []string{"dac_override"}},
&Network{Domain: "inet", Type: "stream"},
&Network{Domain: "inet6", Type: "stream"},
&Mount{
RuleBase: RuleBase{Comment: " failed perms check"},
MountConditions: MountConditions{
FsType: "fuse.portal",
Options: []string{"rw", "rbind"},
},
Source: "@{run}/user/@{uid}/",
MountPoint: "/",
},
&Umount{
MountConditions: MountConditions{},
MountPoint: "@{run}/user/@{uid}/",
},
&Signal{
Access: []string{"receive"},
Set: []string{"term"},
Peer: "at-spi-bus-launcher",
},
&Ptrace{Access: []string{"read"}, Peer: "nautilus"},
&Unix{
Access: []string{"send", "receive"},
Type: "stream",
Address: "@/tmp/.ICE-unix/1995",
PeerLabel: "gnome-shell",
PeerAddr: "none",
},
&Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"},
&Dbus{
Access: []string{"receive"},
Bus: "system",
Path: "/org/freedesktop/DBus",
Interface: "org.freedesktop.DBus",
Member: "AddMatch",
PeerName: ":1.3",
PeerLabel: "power-profiles-daemon",
},
&File{Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", Access: []string{"r", "m"}},
&File{Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: []string{"r", "w"}},
&File{Path: "@{sys}/devices/@{pci}/class", Access: []string{"r"}},
includeLocal1,
},
}},
},
want: util.MustReadFile(testData.Join("string.aa")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.f.String(); got != tt.want {
t.Errorf("AppArmorProfile.String() = |%v|, want |%v|", got, tt.want)
}
})
}
}
func TestAppArmorProfileFile_Sort(t *testing.T) {
tests := []struct {
name string
origin *AppArmorProfileFile
want *AppArmorProfileFile
}{
{
name: "all",
origin: &AppArmorProfileFile{
Profiles: []*Profile{{
Rules: []Rule{
file2, network1, userns1, include1, dbus2, signal1,
ptrace1, includeLocal1, rlimit3, capability1, network2,
mqueue2, iouring2, dbus1, link2, capability2, file1,
unix2, signal2, mount2, all1, umount2, mount1, remount2,
pivotroot1, changeprofile2,
},
}},
},
want: &AppArmorProfileFile{
Profiles: []*Profile{{
Rules: []Rule{
include1, all1, rlimit3, userns1, capability1, capability2,
network2, network1, mount2, mount1, remount2, umount2,
pivotroot1, changeprofile2, mqueue2, iouring2, signal2,
signal1, ptrace1, unix2, dbus2, dbus1, file1, file2,
link2, includeLocal1,
},
}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.origin
got.Sort()
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfile.Sort() = %v, want %v", got, tt.want)
}
})
}
}
func TestAppArmorProfileFile_MergeRules(t *testing.T) {
tests := []struct {
name string
origin *AppArmorProfileFile
want *AppArmorProfileFile
}{
{
name: "all",
origin: &AppArmorProfileFile{
Profiles: []*Profile{{
Rules: []Rule{capability1, capability1, network1, network1, file1, file1},
}},
},
want: &AppArmorProfileFile{
Profiles: []*Profile{{
Rules: []Rule{capability1, network1, file1},
}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.origin
got.MergeRules()
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfile.MergeRules() = %v, want %v", got, tt.want)
}
})
}
}
func TestAppArmorProfileFile_Integration(t *testing.T) {
tests := []struct {
name string
f *AppArmorProfileFile
want string
}{
{
name: "aa-status",
f: &AppArmorProfileFile{
Preamble: Rules{
&Comment{RuleBase: RuleBase{Comment: " apparmor.d - Full set of apparmor profiles", IsLineRule: true}},
&Comment{RuleBase: RuleBase{Comment: " Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>", IsLineRule: true}},
&Comment{RuleBase: RuleBase{Comment: " SPDX-License-Identifier: GPL-2.0-only", IsLineRule: true}},
nil,
&Abi{IsMagic: true, Path: "abi/3.0"},
&Include{IsMagic: true, Path: "tunables/global"},
&Variable{
Name: "exec_path", Define: true,
Values: []string{"@{bin}/aa-status", "@{bin}/apparmor_status"},
},
},
Profiles: []*Profile{{
Header: Header{
Name: "aa-status",
Attachments: []string{"@{exec_path}"},
},
Rules: Rules{
&Include{IfExists: true, IsMagic: true, Path: "local/aa-status"},
&Capability{Names: []string{"dac_read_search"}},
&File{Path: "@{exec_path}", Access: []string{"m", "r"}},
&File{Path: "@{PROC}/@{pids}/attr/apparmor/current", Access: []string{"r"}},
&File{Path: "@{PROC}/", Access: []string{"r"}},
&File{Path: "@{sys}/module/apparmor/parameters/enabled", Access: []string{"r"}},
&File{Path: "@{sys}/kernel/security/apparmor/profiles", Access: []string{"r"}},
&File{Path: "@{PROC}/@{pids}/attr/current", Access: []string{"r"}},
&Include{IsMagic: true, Path: "abstractions/consoles"},
&File{Owner: true, Path: "@{PROC}/@{pid}/mounts", Access: []string{"r"}},
&Include{IsMagic: true, Path: "abstractions/base"},
&File{Path: "/dev/tty@{int}", Access: []string{"r", "w"}},
&Capability{Names: []string{"sys_ptrace"}},
&Ptrace{Access: []string{"read"}},
},
}},
},
want: util.MustReadFile(intData.Join("profiles-a-f/aa-status")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.f.Sort()
tt.f.MergeRules()
tt.f.Format()
err := tt.f.Validate()
if err != nil {
t.Errorf("AppArmorProfile.Validate() = %v", err)
}
if got := tt.f.String(); got != tt.want {
t.Errorf("AppArmorProfile = |%v|, want |%v|", got, tt.want)
}
})
}
}

121
pkg/aa/base.go Normal file
View file

@ -0,0 +1,121 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"strings"
)
type RuleBase struct {
IsLineRule bool
Comment string
NoNewPrivs bool
FileInherit bool
Prefix string
Padding string
Optional bool
}
func newRule(rule []string) RuleBase {
comment := ""
fileInherit, noNewPrivs, optional := false, false, false
idx := 0
for idx < len(rule) {
if rule[idx] == COMMENT.Tok() {
comment = " " + strings.Join(rule[idx+1:], " ")
break
}
idx++
}
switch {
case strings.Contains(comment, "file_inherit"):
fileInherit = true
comment = strings.Replace(comment, "file_inherit ", "", 1)
case strings.HasPrefix(comment, "no new privs"):
noNewPrivs = true
comment = strings.Replace(comment, "no new privs ", "", 1)
case strings.Contains(comment, "optional:"):
optional = true
comment = strings.Replace(comment, "optional: ", "", 1)
}
return RuleBase{
Comment: comment,
NoNewPrivs: noNewPrivs,
FileInherit: fileInherit,
Optional: optional,
}
}
func newRuleFromLog(log map[string]string) RuleBase {
comment := ""
fileInherit, noNewPrivs, optional := false, false, false
if log["operation"] == "file_inherit" {
fileInherit = true
}
if log["error"] == "-1" {
if strings.Contains(log["info"], "optional:") {
optional = true
comment = strings.Replace(log["info"], "optional: ", "", 1)
} else {
noNewPrivs = true
}
}
if log["info"] != "" {
comment += " " + log["info"]
}
return RuleBase{
IsLineRule: false,
Comment: comment,
NoNewPrivs: noNewPrivs,
FileInherit: fileInherit,
Optional: optional,
}
}
func (r RuleBase) Less(other any) bool {
return false
}
func (r RuleBase) Equals(other any) bool {
return false
}
func (r RuleBase) String() string {
return renderTemplate(r.Kind(), r)
}
func (r RuleBase) Constraint() constraint {
return anyKind
}
func (r RuleBase) Kind() Kind {
return COMMENT
}
type Qualifier struct {
Audit bool
AccessType string
}
func newQualifierFromLog(log map[string]string) Qualifier {
audit := false
if log["apparmor"] == "AUDIT" {
audit = true
}
return Qualifier{Audit: audit}
}
func (r Qualifier) Less(other Qualifier) bool {
if r.Audit != other.Audit {
return r.Audit
}
return r.AccessType < other.AccessType
}
func (r Qualifier) Equals(other Qualifier) bool {
return r.Audit == other.Audit && r.AccessType == other.AccessType
}

42
pkg/aa/blocks.go Normal file
View file

@ -0,0 +1,42 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
const (
HAT Kind = "hat"
)
// Hat represents a single AppArmor hat.
type Hat struct {
RuleBase
Name string
Rules Rules
}
func (r *Hat) Validate() error {
return nil
}
func (p *Hat) Less(other any) bool {
o, _ := other.(*Hat)
return p.Name < o.Name
}
func (p *Hat) Equals(other any) bool {
o, _ := other.(*Hat)
return p.Name == o.Name
}
func (p *Hat) String() string {
return renderTemplate(p.Kind(), p)
}
func (p *Hat) Constraint() constraint {
return blockKind
}
func (p *Hat) Kind() Kind {
return HAT
}

View file

@ -4,27 +4,72 @@
package aa package aa
type Capability struct { import (
Qualifier "fmt"
Name string "slices"
)
const CAPABILITY Kind = "capability"
func init() {
requirements[CAPABILITY] = requirement{
"name": {
"audit_control", "audit_read", "audit_write", "block_suspend", "bpf",
"checkpoint_restore", "chown", "dac_override", "dac_read_search",
"fowner", "fsetid", "ipc_lock", "ipc_owner", "kill", "lease",
"linux_immutable", "mac_admin", "mac_override", "mknod", "net_admin",
"net_bind_service", "net_broadcast", "net_raw", "perfmon", "setfcap",
"setgid", "setpcap", "setuid", "sys_admin", "sys_boot", "sys_chroot",
"sys_module", "sys_nice", "sys_pacct", "sys_ptrace", "sys_rawio",
"sys_resource", "sys_time", "sys_tty_config", "syslog", "wake_alarm",
},
}
} }
func CapabilityFromLog(log map[string]string) ApparmorRule { type Capability struct {
return &Capability{ RuleBase
Qualifier: NewQualifierFromLog(log), Qualifier
Name: log["capname"], Names []string
} }
func newCapabilityFromLog(log map[string]string) Rule {
return &Capability{
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Names: Must(toValues(CAPABILITY, "name", log["capname"])),
}
}
func (r *Capability) Validate() error {
if err := validateValues(r.Kind(), "name", r.Names); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
} }
func (r *Capability) Less(other any) bool { func (r *Capability) Less(other any) bool {
o, _ := other.(*Capability) o, _ := other.(*Capability)
if r.Name == o.Name { for i := 0; i < len(r.Names) && i < len(o.Names); i++ {
return r.Qualifier.Less(o.Qualifier) if r.Names[i] != o.Names[i] {
return r.Names[i] < o.Names[i]
} }
return r.Name < o.Name }
return r.Qualifier.Less(o.Qualifier)
} }
func (r *Capability) Equals(other any) bool { func (r *Capability) Equals(other any) bool {
o, _ := other.(*Capability) o, _ := other.(*Capability)
return r.Name == o.Name && r.Qualifier.Equals(o.Qualifier) return slices.Equal(r.Names, o.Names) && r.Qualifier.Equals(o.Qualifier)
}
func (r *Capability) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Capability) Constraint() constraint {
return blockKind
}
func (r *Capability) Kind() Kind {
return CAPABILITY
} }

View file

@ -4,34 +4,69 @@
package aa package aa
import "fmt"
const CHANGEPROFILE Kind = "change_profile"
func init() {
requirements[CHANGEPROFILE] = requirement{
"mode": []string{"safe", "unsafe"},
}
}
type ChangeProfile struct { type ChangeProfile struct {
RuleBase
Qualifier Qualifier
ExecMode string ExecMode string
Exec string Exec string
ProfileName string ProfileName string
} }
func ChangeProfileFromLog(log map[string]string) ApparmorRule { func newChangeProfileFromLog(log map[string]string) Rule {
return &ChangeProfile{ return &ChangeProfile{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
ExecMode: log["mode"], ExecMode: log["mode"],
Exec: log["exec"], Exec: log["exec"],
ProfileName: log["target"], ProfileName: log["target"],
} }
} }
func (r *ChangeProfile) Validate() error {
if err := validateValues(r.Kind(), "mode", []string{r.ExecMode}); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *ChangeProfile) Less(other any) bool { func (r *ChangeProfile) Less(other any) bool {
o, _ := other.(*ChangeProfile) o, _ := other.(*ChangeProfile)
if r.ExecMode == o.ExecMode { if r.ExecMode != o.ExecMode {
if r.Exec == o.Exec { return r.ExecMode < o.ExecMode
return r.ProfileName < o.ProfileName
} }
if r.Exec != o.Exec {
return r.Exec < o.Exec return r.Exec < o.Exec
} }
return r.ExecMode < o.ExecMode if r.ProfileName != o.ProfileName {
return r.ProfileName < o.ProfileName
}
return r.Qualifier.Less(o.Qualifier)
} }
func (r *ChangeProfile) Equals(other any) bool { func (r *ChangeProfile) Equals(other any) bool {
o, _ := other.(*ChangeProfile) o, _ := other.(*ChangeProfile)
return r.ExecMode == o.ExecMode && r.Exec == o.Exec && r.ProfileName == o.ProfileName return r.ExecMode == o.ExecMode && r.Exec == o.Exec &&
r.ProfileName == o.ProfileName && r.Qualifier.Equals(o.Qualifier)
}
func (r *ChangeProfile) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *ChangeProfile) Constraint() constraint {
return blockKind
}
func (r *ChangeProfile) Kind() Kind {
return CHANGEPROFILE
} }

120
pkg/aa/convert.go Normal file
View file

@ -0,0 +1,120 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"fmt"
"slices"
"strings"
)
// Must is a helper that wraps a call to a function returning (any, error) and
// panics if the error is non-nil.
func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
// cmpFileAccess compares two access strings for file rules.
// It is aimed to be used in slices.SortFunc.
func cmpFileAccess(i, j string) int {
if slices.Contains(requirements[FILE]["access"], i) &&
slices.Contains(requirements[FILE]["access"], j) {
return requirementsWeights[FILE]["access"][i] - requirementsWeights[FILE]["access"][j]
}
if slices.Contains(requirements[FILE]["transition"], i) &&
slices.Contains(requirements[FILE]["transition"], j) {
return requirementsWeights[FILE]["transition"][i] - requirementsWeights[FILE]["transition"][j]
}
if slices.Contains(requirements[FILE]["access"], i) {
return -1
}
return 1
}
func validateValues(kind Kind, key string, values []string) error {
for _, v := range values {
if v == "" {
continue
}
if !slices.Contains(requirements[kind][key], v) {
return fmt.Errorf("invalid mode '%s'", v)
}
}
return nil
}
// Helper function to convert a string to a slice of rule values according to
// the rule requirements as defined in the requirements map.
func toValues(kind Kind, key string, input string) ([]string, error) {
req, ok := requirements[kind][key]
if !ok {
return nil, fmt.Errorf("unrecognized requirement '%s' for rule %s", key, kind)
}
res := tokenToSlice(input)
for idx := range res {
res[idx] = strings.Trim(res[idx], `" `)
if res[idx] == "" {
res = slices.Delete(res, idx, idx+1)
continue
}
if !slices.Contains(req, res[idx]) {
return nil, fmt.Errorf("unrecognized %s: %s", key, res[idx])
}
}
slices.SortFunc(res, func(i, j string) int {
return requirementsWeights[kind][key][i] - requirementsWeights[kind][key][j]
})
return slices.Compact(res), nil
}
// Helper function to convert an access string to a slice of access according to
// the rule requirements as defined in the requirements map.
func toAccess(kind Kind, input string) ([]string, error) {
var res []string
switch kind {
case FILE:
raw := strings.Split(input, "")
trans := []string{}
for _, access := range raw {
if slices.Contains(requirements[FILE]["access"], access) {
res = append(res, access)
} else {
trans = append(trans, access)
}
}
transition := strings.Join(trans, "")
if len(transition) > 0 {
if slices.Contains(requirements[FILE]["transition"], transition) {
res = append(res, transition)
} else {
return nil, fmt.Errorf("unrecognized transition: %s", transition)
}
}
case FILE + "-log":
raw := strings.Split(input, "")
for _, access := range raw {
if slices.Contains(requirements[FILE]["access"], access) {
res = append(res, access)
} else if maskToAccess[access] != "" {
res = append(res, maskToAccess[access])
} else {
return nil, fmt.Errorf("toAccess: unrecognized file access '%s' for %s", input, kind)
}
}
default:
return toValues(kind, "access", input)
}
slices.SortFunc(res, cmpFileAccess)
return slices.Compact(res), nil
}

90
pkg/aa/convert_test.go Normal file
View file

@ -0,0 +1,90 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"reflect"
"testing"
)
func Test_toAccess(t *testing.T) {
tests := []struct {
name string
kind Kind
inputs []string
wants [][]string
wantsErr []bool
}{
{
name: "empty",
kind: FILE,
inputs: []string{""},
wants: [][]string{nil},
wantsErr: []bool{false},
},
{
name: "file",
kind: FILE,
inputs: []string{
"rPx", "rPUx", "mr", "rm", "rix", "rcx", "rCUx", "rmix", "rwlk",
"mrwkl", "", "r", "x", "w", "wr", "px", "Px", "Ux", "mrwlkPix",
},
wants: [][]string{
{"r", "Px"}, {"r", "PUx"}, {"m", "r"}, {"m", "r"}, {"r", "ix"},
{"r", "cx"}, {"r", "CUx"}, {"m", "r", "ix"}, {"r", "w", "l", "k"},
{"m", "r", "w", "l", "k"}, nil, {"r"}, {"x"}, {"w"}, {"r", "w"},
{"px"}, {"Px"}, {"Ux"}, {"m", "r", "w", "l", "k", "Pix"},
},
wantsErr: []bool{
false, false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
},
},
{
name: "file-log",
kind: FILE + "-log",
inputs: []string{
"mr", "rm", "x", "rwlk", "mrwkl", "r", "c", "wc", "d", "wr",
},
wants: [][]string{
{"m", "r"}, {"m", "r"}, {"ix"}, {"r", "w", "l", "k"},
{"m", "r", "w", "l", "k"}, {"r"}, {"w"}, {"w"}, {"w"}, {"r", "w"},
},
wantsErr: []bool{
false, false, false, false, false, false, false, false, false, false,
},
},
{
name: "signal",
kind: SIGNAL,
inputs: []string{"send receive rw"},
wants: [][]string{{"rw", "send", "receive"}},
wantsErr: []bool{false},
},
{
name: "ptrace",
kind: PTRACE,
inputs: []string{"readby", "tracedby", "read readby", "r w", "rw", ""},
wants: [][]string{
{"readby"}, {"tracedby"}, {"read", "readby"}, {"r", "w"}, {"rw"}, {},
},
wantsErr: []bool{false, false, false, false, false, false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for i, input := range tt.inputs {
got, err := toAccess(tt.kind, input)
if (err != nil) != tt.wantsErr[i] {
t.Errorf("toAccess() error = %v, wantErr %v", err, tt.wantsErr[i])
return
}
if !reflect.DeepEqual(got, tt.wants[i]) {
t.Errorf("toAccess() = %v, want %v", got, tt.wants[i])
}
}
})
}
}

View file

@ -5,17 +5,40 @@
package aa package aa
var ( var (
// Comment
comment1 = &Comment{RuleBase: RuleBase{Comment: "comment", IsLineRule: true}}
comment2 = &Comment{RuleBase: RuleBase{Comment: "another comment", IsLineRule: true}}
// Abi
abi1 = &Abi{IsMagic: true, Path: "abi/4.0"}
abi2 = &Abi{IsMagic: true, Path: "abi/3.0"}
// Alias
alias1 = &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"}
alias2 = &Alias{Path: "/mnt/var", RewrittenPath: "/var"}
// Include // Include
include1 = &Include{IsMagic: true, Path: "abstraction/base"} include1 = &Include{IsMagic: true, Path: "abstraction/base"}
include2 = &Include{IsMagic: false, Path: "abstraction/base"} include2 = &Include{IsMagic: false, Path: "abstraction/base"}
include3 = &Include{IfExists: true, IsMagic: true, Path: "abstraction/base"}
includeLocal1 = &Include{IfExists: true, IsMagic: true, Path: "local/foo"} includeLocal1 = &Include{IfExists: true, IsMagic: true, Path: "local/foo"}
// Variable
variable1 = &Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}, Define: true}
variable2 = &Variable{Name: "exec_path", Values: []string{"@{bin}/foo", "@{lib}/foo"}, Define: true}
// All
all1 = &All{}
all2 = &All{RuleBase: RuleBase{Comment: "comment"}}
// Rlimit // Rlimit
rlimit1 = &Rlimit{Key: "nproc", Op: "<=", Value: "200"} rlimit1 = &Rlimit{Key: "nproc", Op: "<=", Value: "200"}
rlimit2 = &Rlimit{Key: "cpu", Op: "<=", Value: "2"} rlimit2 = &Rlimit{Key: "cpu", Op: "<=", Value: "2"}
rlimit3 = &Rlimit{Key: "nproc", Op: "<", Value: "2"} rlimit3 = &Rlimit{Key: "nproc", Op: "<", Value: "2"}
// Userns
userns1 = &Userns{Create: true}
userns2 = &Userns{}
// Capability // Capability
capability1Log = map[string]string{ capability1Log = map[string]string{
"apparmor": "ALLOWED", "apparmor": "ALLOWED",
@ -26,8 +49,8 @@ var (
"profile": "pkexec", "profile": "pkexec",
"comm": "pkexec", "comm": "pkexec",
} }
capability1 = &Capability{Name: "net_admin"} capability1 = &Capability{Names: []string{"net_admin"}}
capability2 = &Capability{Name: "sys_ptrace"} capability2 = &Capability{Names: []string{"sys_ptrace"}}
// Network // Network
network1Log = map[string]string{ network1Log = map[string]string{
@ -71,18 +94,22 @@ var (
"flags": "rw, rbind", "flags": "rw, rbind",
} }
mount1 = &Mount{ mount1 = &Mount{
Qualifier: Qualifier{Comment: "failed perms check"}, RuleBase: RuleBase{Comment: " failed perms check"},
MountConditions: MountConditions{FsType: "overlay"}, MountConditions: MountConditions{FsType: "overlay"},
Source: "overlay", Source: "overlay",
MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/", MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/",
} }
mount2 = &Mount{ mount2 = &Mount{
Qualifier: Qualifier{Comment: "failed perms check"}, RuleBase: RuleBase{Comment: " failed perms check"},
MountConditions: MountConditions{Options: []string{"rw", "rbind"}}, MountConditions: MountConditions{Options: []string{"rw", "rbind"}},
Source: "/oldroot/dev/tty", Source: "/oldroot/dev/tty",
MountPoint: "/newroot/dev/tty", MountPoint: "/newroot/dev/tty",
} }
// Remount
remount1 = &Remount{MountPoint: "/"}
remount2 = &Remount{MountPoint: "/{,**}/"}
// Umount // Umount
umount1Log = map[string]string{ umount1Log = map[string]string{
"apparmor": "ALLOWED", "apparmor": "ALLOWED",
@ -96,7 +123,6 @@ var (
umount2 = &Umount{MountPoint: "/oldroot/"} umount2 = &Umount{MountPoint: "/oldroot/"}
// PivotRoot // PivotRoot
// pivotroot1LogStr = `apparmor="ALLOWED" operation="pivotroot" class="mount" profile="systemd" name="@{run}/systemd/mount-rootfs/" comm="(ostnamed)" srcname="@{run}/systemd/mount-rootfs/"`
pivotroot1Log = map[string]string{ pivotroot1Log = map[string]string{
"apparmor": "ALLOWED", "apparmor": "ALLOWED",
"class": "mount", "class": "mount",
@ -120,7 +146,6 @@ var (
} }
// Change Profile // Change Profile
// changeprofile1LogStr = `apparmor="ALLOWED" operation="change_onexec" class="file" profile="systemd" name="systemd-user" comm="(systemd)" target="systemd-user"`
changeprofile1Log = map[string]string{ changeprofile1Log = map[string]string{
"apparmor": "ALLOWED", "apparmor": "ALLOWED",
"class": "file", "class": "file",
@ -134,6 +159,14 @@ var (
changeprofile2 = &ChangeProfile{ProfileName: "brwap"} changeprofile2 = &ChangeProfile{ProfileName: "brwap"}
changeprofile3 = &ChangeProfile{ExecMode: "safe", Exec: "/bin/bash", ProfileName: "brwap//default"} changeprofile3 = &ChangeProfile{ExecMode: "safe", Exec: "/bin/bash", ProfileName: "brwap//default"}
// Mqueue
mqueue1 = &Mqueue{Access: []string{"r"}, Type: "posix", Name: "/"}
mqueue2 = &Mqueue{Access: []string{"r"}, Type: "sysv", Name: "/"}
// IO Uring
iouring1 = &IOUring{Access: []string{"sqpoll"}, Label: "foo"}
iouring2 = &IOUring{Access: []string{"override_creds"}}
// Signal // Signal
signal1Log = map[string]string{ signal1Log = map[string]string{
"apparmor": "ALLOWED", "apparmor": "ALLOWED",
@ -147,13 +180,13 @@ var (
"peer": "firefox//&firejail-default", "peer": "firefox//&firejail-default",
} }
signal1 = &Signal{ signal1 = &Signal{
Access: "receive", Access: []string{"receive"},
Set: "kill", Set: []string{"kill"},
Peer: "firefox//&firejail-default", Peer: "firefox//&firejail-default",
} }
signal2 = &Signal{ signal2 = &Signal{
Access: "receive", Access: []string{"receive"},
Set: "up", Set: []string{"up"},
Peer: "firefox//&firejail-default", Peer: "firefox//&firejail-default",
} }
@ -177,8 +210,8 @@ var (
"denied_mask": "readby", "denied_mask": "readby",
"peer": "systemd-journald", "peer": "systemd-journald",
} }
ptrace1 = &Ptrace{Access: "read", Peer: "nautilus"} ptrace1 = &Ptrace{Access: []string{"read"}, Peer: "nautilus"}
ptrace2 = &Ptrace{Access: "readby", Peer: "systemd-journald"} ptrace2 = &Ptrace{Access: []string{"readby"}, Peer: "systemd-journald"}
// Unix // Unix
unix1Log = map[string]string{ unix1Log = map[string]string{
@ -197,16 +230,16 @@ var (
"protocol": "0", "protocol": "0",
} }
unix1 = &Unix{ unix1 = &Unix{
Access: "send receive", Access: []string{"send", "receive"},
Type: "stream", Type: "stream",
Protocol: "0", Protocol: "0",
Address: "none", Address: "none",
Peer: "dbus-daemon",
PeerAddr: "@/tmp/dbus-AaKMpxzC4k", PeerAddr: "@/tmp/dbus-AaKMpxzC4k",
PeerLabel: "dbus-daemon",
} }
unix2 = &Unix{ unix2 = &Unix{
Qualifier: Qualifier{FileInherit: true}, RuleBase: RuleBase{FileInherit: true},
Access: "receive", Access: []string{"receive"},
Type: "stream", Type: "stream",
} }
@ -234,21 +267,21 @@ var (
"label": "evolution-source-registry", "label": "evolution-source-registry",
} }
dbus1 = &Dbus{ dbus1 = &Dbus{
Access: "receive", Access: []string{"receive"},
Bus: "session", Bus: "session",
Name: ":1.15",
Path: "/org/gtk/vfs/metadata", Path: "/org/gtk/vfs/metadata",
Interface: "org.gtk.vfs.Metadata", Interface: "org.gtk.vfs.Metadata",
Member: "Remove", Member: "Remove",
Label: "tracker-extract", PeerName: ":1.15",
PeerLabel: "tracker-extract",
} }
dbus2 = &Dbus{ dbus2 = &Dbus{
Access: "bind", Access: []string{"bind"},
Bus: "session", Bus: "session",
Name: "org.gnome.evolution.dataserver.Sources5", Name: "org.gnome.evolution.dataserver.Sources5",
} }
dbus3 = &Dbus{ dbus3 = &Dbus{
Access: "bind", Access: []string{"bind"},
Bus: "session", Bus: "session",
Name: "org.gnome.evolution.dataserver", Name: "org.gnome.evolution.dataserver",
} }
@ -283,10 +316,77 @@ var (
"OUID": "user", "OUID": "user",
"error": "-1", "error": "-1",
} }
file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"} file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}}
file2 = &File{ file2 = &File{
Qualifier: Qualifier{Owner: true, NoNewPrivs: true}, RuleBase: RuleBase{NoNewPrivs: true},
Owner: true,
Path: "@{PROC}/4163/cgroup", Path: "@{PROC}/4163/cgroup",
Access: "r", Access: []string{"r"},
} }
// Link
link1Log = map[string]string{
"apparmor": "ALLOWED",
"operation": "link",
"class": "file",
"profile": "mkinitcpio",
"name": "/tmp/mkinitcpio.QDWtza/early@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst",
"comm": "cp",
"requested_mask": "l",
"denied_mask": "l",
"fsuid": "0",
"ouid": "0",
"target": "/tmp/mkinitcpio.QDWtza/root@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst",
"FSUID": "root",
"OUID": "root",
}
link3Log = map[string]string{
"apparmor": "ALLOWED",
"operation": "link",
"class": "file",
"profile": "dolphin",
"name": "@{user_config_dirs}/kiorc",
"comm": "dolphin",
"requested_mask": "l",
"denied_mask": "l",
"fsuid": "1000",
"ouid": "1000",
"target": "@{user_config_dirs}/#3954",
}
link1 = &Link{
Path: "/tmp/mkinitcpio.QDWtza/early@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst",
Target: "/tmp/mkinitcpio.QDWtza/root@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst",
}
link2 = &Link{
Owner: true,
Path: "@{user_config_dirs}/powerdevilrc{,.@{rand6}}",
Target: "@{user_config_dirs}/#@{int}",
}
link3 = &Link{
Owner: true,
Path: "@{user_config_dirs}/kiorc",
Target: "@{user_config_dirs}/#3954",
}
// Profile
profile1 = &Profile{
Header: Header{
Name: "sudo",
Attachments: []string{},
Attributes: map[string]string{},
Flags: []string{},
},
}
profile2 = &Profile{
Header: Header{
Name: "systemctl",
Attachments: []string{},
Attributes: map[string]string{},
Flags: []string{},
},
}
// Hat
hat1 = &Hat{Name: "user"}
hat2 = &Hat{Name: "root"}
) )

View file

@ -4,59 +4,112 @@
package aa package aa
import (
"fmt"
"slices"
)
const DBUS Kind = "dbus"
func init() {
requirements[DBUS] = requirement{
"access": []string{
"send", "receive", "bind", "eavesdrop", "r", "read",
"w", "write", "rw",
},
"bus": []string{"system", "session", "accessibility"},
}
}
type Dbus struct { type Dbus struct {
RuleBase
Qualifier Qualifier
Access string Access []string
Bus string Bus string
Name string Name string
Path string Path string
Interface string Interface string
Member string Member string
Label string PeerName string
PeerLabel string
} }
func DbusFromLog(log map[string]string) ApparmorRule { func newDbusFromLog(log map[string]string) Rule {
name := ""
peerName := ""
if log["mask"] == "bind" {
name = log["name"]
} else {
peerName = log["name"]
}
return &Dbus{ return &Dbus{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Access: log["mask"], Qualifier: newQualifierFromLog(log),
Access: []string{log["mask"]},
Bus: log["bus"], Bus: log["bus"],
Name: log["name"], Name: name,
Path: log["path"], Path: log["path"],
Interface: log["interface"], Interface: log["interface"],
Member: log["member"], Member: log["member"],
Label: log["peer_label"], PeerName: peerName,
PeerLabel: log["peer_label"],
} }
} }
func (r *Dbus) Validate() error {
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return validateValues(r.Kind(), "bus", []string{r.Bus})
}
func (r *Dbus) Less(other any) bool { func (r *Dbus) Less(other any) bool {
o, _ := other.(*Dbus) o, _ := other.(*Dbus)
if r.Qualifier.Equals(o.Qualifier) { for i := 0; i < len(r.Access) && i < len(o.Access); i++ {
if r.Access == o.Access { if r.Access[i] != o.Access[i] {
if r.Bus == o.Bus { return r.Access[i] < o.Access[i]
if r.Name == o.Name {
if r.Path == o.Path {
if r.Interface == o.Interface {
if r.Member == o.Member {
return r.Label < o.Label
} }
return r.Member < o.Member
}
return r.Interface < o.Interface
}
return r.Path < o.Path
}
return r.Name < o.Name
} }
if r.Bus != o.Bus {
return r.Bus < o.Bus return r.Bus < o.Bus
} }
return r.Access < o.Access if r.Name != o.Name {
return r.Name < o.Name
}
if r.Path != o.Path {
return r.Path < o.Path
}
if r.Interface != o.Interface {
return r.Interface < o.Interface
}
if r.Member != o.Member {
return r.Member < o.Member
}
if r.PeerName != o.PeerName {
return r.PeerName < o.PeerName
}
if r.PeerLabel != o.PeerLabel {
return r.PeerLabel < o.PeerLabel
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
func (r *Dbus) Equals(other any) bool { func (r *Dbus) Equals(other any) bool {
o, _ := other.(*Dbus) o, _ := other.(*Dbus)
return r.Access == o.Access && r.Bus == o.Bus && r.Name == o.Name && return slices.Equal(r.Access, o.Access) && r.Bus == o.Bus && r.Name == o.Name &&
r.Path == o.Path && r.Interface == o.Interface && r.Path == o.Path && r.Interface == o.Interface &&
r.Member == o.Member && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) r.Member == o.Member && r.PeerName == o.PeerName &&
r.PeerLabel == o.PeerLabel && r.Qualifier.Equals(o.Qualifier)
}
func (r *Dbus) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Dbus) Constraint() constraint {
return blockKind
}
func (r *Dbus) Kind() Kind {
return DBUS
} }

View file

@ -4,43 +4,164 @@
package aa package aa
import (
"fmt"
"slices"
"strings"
)
const (
LINK Kind = "link"
FILE Kind = "file"
tokOWNER = "owner"
tokSUBSET = "subset"
)
func init() {
requirements[FILE] = requirement{
"access": {"m", "r", "w", "l", "k"},
"transition": {
"ix", "ux", "Ux", "px", "Px", "cx", "Cx", "pix", "Pix", "cix",
"Cix", "pux", "PUx", "cux", "CUx", "x",
},
}
}
func isOwner(log map[string]string) bool {
fsuid, hasFsUID := log["fsuid"]
ouid, hasOuUID := log["ouid"]
isDbus := strings.Contains(log["operation"], "dbus")
if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus {
return true
}
return false
}
type File struct { type File struct {
RuleBase
Qualifier Qualifier
Owner bool
Path string Path string
Access string Access []string
Target string Target string
} }
func FileFromLog(log map[string]string) ApparmorRule { func newFileFromLog(log map[string]string) Rule {
accesses, err := toAccess("file-log", log["requested_mask"])
if err != nil {
panic(fmt.Errorf("newFileFromLog(%v): %w", log, err))
}
if slices.Compare(accesses, []string{"l"}) == 0 {
return newLinkFromLog(log)
}
return &File{ return &File{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Owner: isOwner(log),
Path: log["name"], Path: log["name"],
Access: toAccess(log["requested_mask"]), Access: accesses,
Target: log["target"], Target: log["target"],
} }
} }
func (r *File) Validate() error {
return nil
}
func (r *File) Less(other any) bool { func (r *File) Less(other any) bool {
o, _ := other.(*File) o, _ := other.(*File)
letterR := getLetterIn(fileAlphabet, r.Path) letterR := getLetterIn(fileAlphabet, r.Path)
letterO := getLetterIn(fileAlphabet, o.Path) letterO := getLetterIn(fileAlphabet, o.Path)
if fileWeights[letterR] == fileWeights[letterO] || letterR == "" || letterO == "" { if fileWeights[letterR] != fileWeights[letterO] && letterR != "" && letterO != "" {
if r.Qualifier.Equals(o.Qualifier) { return fileWeights[letterR] < fileWeights[letterO]
if r.Path == o.Path {
if r.Access == o.Access {
return r.Target < o.Target
}
return r.Access < o.Access
} }
if r.Path != o.Path {
return r.Path < o.Path return r.Path < o.Path
} }
return r.Qualifier.Less(o.Qualifier) if o.Owner != r.Owner {
return r.Owner
} }
return fileWeights[letterR] < fileWeights[letterO] if len(r.Access) != len(o.Access) {
return len(r.Access) < len(o.Access)
}
if r.Target != o.Target {
return r.Target < o.Target
}
return r.Qualifier.Less(o.Qualifier)
} }
func (r *File) Equals(other any) bool { func (r *File) Equals(other any) bool {
o, _ := other.(*File) o, _ := other.(*File)
return r.Path == o.Path && r.Access == o.Access && return r.Path == o.Path && slices.Equal(r.Access, o.Access) && r.Owner == o.Owner &&
r.Target == o.Target && r.Qualifier.Equals(o.Qualifier) r.Target == o.Target && r.Qualifier.Equals(o.Qualifier)
} }
func (r *File) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *File) Constraint() constraint {
return blockKind
}
func (r *File) Kind() Kind {
return FILE
}
type Link struct {
RuleBase
Qualifier
Owner bool
Subset bool
Path string
Target string
}
func newLinkFromLog(log map[string]string) Rule {
return &Link{
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Owner: isOwner(log),
Path: log["name"],
Target: log["target"],
}
}
func (r *Link) Validate() error {
return nil
}
func (r *Link) Less(other any) bool {
o, _ := other.(*Link)
if r.Path != o.Path {
return r.Path < o.Path
}
if o.Owner != r.Owner {
return r.Owner
}
if r.Target != o.Target {
return r.Target < o.Target
}
if r.Subset != o.Subset {
return r.Subset
}
return r.Qualifier.Less(o.Qualifier)
}
func (r *Link) Equals(other any) bool {
o, _ := other.(*Link)
return r.Subset == o.Subset && r.Owner == o.Owner && r.Path == o.Path &&
r.Target == o.Target && r.Qualifier.Equals(o.Qualifier)
}
func (r *Link) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Link) Constraint() constraint {
return blockKind
}
func (r *Link) Kind() Kind {
return LINK
}

View file

@ -1,28 +0,0 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
type Include struct {
IfExists bool
Path string
IsMagic bool
}
func (r *Include) Less(other any) bool {
o, _ := other.(*Include)
if r.Path == o.Path {
if r.IsMagic == o.IsMagic {
return r.IfExists
}
return r.IsMagic
}
return r.Path < o.Path
}
func (r *Include) Equals(other any) bool {
o, _ := other.(*Include)
return r.Path == o.Path && r.IsMagic == o.IsMagic &&
r.IfExists == o.IfExists
}

View file

@ -4,24 +4,66 @@
package aa package aa
import (
"fmt"
"slices"
)
const IOURING Kind = "io_uring"
func init() {
requirements[IOURING] = requirement{
"access": []string{"sqpoll", "override_creds"},
}
}
type IOUring struct { type IOUring struct {
RuleBase
Qualifier Qualifier
Access string Access []string
Label string Label string
} }
func newIOUringFromLog(log map[string]string) Rule {
return &IOUring{
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(IOURING, log["requested"])),
Label: log["label"],
}
}
func (r *IOUring) Validate() error {
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *IOUring) Less(other any) bool { func (r *IOUring) Less(other any) bool {
o, _ := other.(*IOUring) o, _ := other.(*IOUring)
if r.Qualifier.Equals(o.Qualifier) { if len(r.Access) != len(o.Access) {
if r.Access == o.Access { return len(r.Access) < len(o.Access)
return r.Label < o.Label
} }
return r.Access < o.Access if r.Label != o.Label {
return r.Label < o.Label
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
func (r *IOUring) Equals(other any) bool { func (r *IOUring) Equals(other any) bool {
o, _ := other.(*IOUring) o, _ := other.(*IOUring)
return r.Access == o.Access && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) return slices.Equal(r.Access, o.Access) && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier)
}
func (r *IOUring) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *IOUring) Constraint() constraint {
return blockKind
}
func (r *IOUring) Kind() Kind {
return IOURING
} }

View file

@ -5,62 +5,95 @@
package aa package aa
import ( import (
"fmt"
"slices" "slices"
"strings"
) )
const (
MOUNT Kind = "mount"
REMOUNT Kind = "remount"
UMOUNT Kind = "umount"
)
func init() {
requirements[MOUNT] = requirement{
"flags": {
"acl", "async", "atime", "ro", "rw", "bind", "rbind", "dev",
"diratime", "dirsync", "exec", "iversion", "loud", "mand", "move",
"noacl", "noatime", "nodev", "nodiratime", "noexec", "noiversion",
"nomand", "norelatime", "nosuid", "nouser", "private", "relatime",
"remount", "rprivate", "rshared", "rslave", "runbindable", "shared",
"silent", "slave", "strictatime", "suid", "sync", "unbindable",
"user", "verbose",
},
}
}
type MountConditions struct { type MountConditions struct {
FsType string FsType string
Options []string Options []string
} }
func MountConditionsFromLog(log map[string]string) MountConditions { func newMountConditionsFromLog(log map[string]string) MountConditions {
if _, present := log["flags"]; present { if _, present := log["flags"]; present {
return MountConditions{ return MountConditions{
FsType: log["fstype"], FsType: log["fstype"],
Options: strings.Split(log["flags"], ", "), Options: Must(toValues(MOUNT, "flags", log["flags"])),
} }
} }
return MountConditions{FsType: log["fstype"]} return MountConditions{FsType: log["fstype"]}
} }
func (m MountConditions) Less(other MountConditions) bool { func (m MountConditions) Validate() error {
if m.FsType == other.FsType { return validateValues(MOUNT, "flags", m.Options)
return len(m.Options) < len(other.Options)
} }
func (m MountConditions) Less(other MountConditions) bool {
if m.FsType != other.FsType {
return m.FsType < other.FsType return m.FsType < other.FsType
} }
return len(m.Options) < len(other.Options)
}
func (m MountConditions) Equals(other MountConditions) bool { func (m MountConditions) Equals(other MountConditions) bool {
return m.FsType == other.FsType && slices.Equal(m.Options, other.Options) return m.FsType == other.FsType && slices.Equal(m.Options, other.Options)
} }
type Mount struct { type Mount struct {
RuleBase
Qualifier Qualifier
MountConditions MountConditions
Source string Source string
MountPoint string MountPoint string
} }
func MountFromLog(log map[string]string) ApparmorRule { func newMountFromLog(log map[string]string) Rule {
return &Mount{ return &Mount{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
MountConditions: MountConditionsFromLog(log), Qualifier: newQualifierFromLog(log),
MountConditions: newMountConditionsFromLog(log),
Source: log["srcname"], Source: log["srcname"],
MountPoint: log["name"], MountPoint: log["name"],
} }
} }
func (r *Mount) Validate() error {
if err := r.MountConditions.Validate(); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Mount) Less(other any) bool { func (r *Mount) Less(other any) bool {
o, _ := other.(*Mount) o, _ := other.(*Mount)
if r.Qualifier.Equals(o.Qualifier) { if r.Source != o.Source {
if r.Source == o.Source { return r.Source < o.Source
if r.MountPoint == o.MountPoint {
return r.MountConditions.Less(o.MountConditions)
} }
if r.MountPoint != o.MountPoint {
return r.MountPoint < o.MountPoint return r.MountPoint < o.MountPoint
} }
return r.Source < o.Source if r.MountConditions.Equals(o.MountConditions) {
return r.MountConditions.Less(o.MountConditions)
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
@ -72,28 +105,49 @@ func (r *Mount) Equals(other any) bool {
r.Qualifier.Equals(o.Qualifier) r.Qualifier.Equals(o.Qualifier)
} }
func (r *Mount) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Mount) Constraint() constraint {
return blockKind
}
func (r *Mount) Kind() Kind {
return MOUNT
}
type Umount struct { type Umount struct {
RuleBase
Qualifier Qualifier
MountConditions MountConditions
MountPoint string MountPoint string
} }
func UmountFromLog(log map[string]string) ApparmorRule { func newUmountFromLog(log map[string]string) Rule {
return &Umount{ return &Umount{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
MountConditions: MountConditionsFromLog(log), Qualifier: newQualifierFromLog(log),
MountConditions: newMountConditionsFromLog(log),
MountPoint: log["name"], MountPoint: log["name"],
} }
} }
func (r *Umount) Validate() error {
if err := r.MountConditions.Validate(); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Umount) Less(other any) bool { func (r *Umount) Less(other any) bool {
o, _ := other.(*Umount) o, _ := other.(*Umount)
if r.Qualifier.Equals(o.Qualifier) { if r.MountPoint != o.MountPoint {
if r.MountPoint == o.MountPoint {
return r.MountConditions.Less(o.MountConditions)
}
return r.MountPoint < o.MountPoint return r.MountPoint < o.MountPoint
} }
if r.MountConditions.Equals(o.MountConditions) {
return r.MountConditions.Less(o.MountConditions)
}
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
@ -104,28 +158,49 @@ func (r *Umount) Equals(other any) bool {
r.Qualifier.Equals(o.Qualifier) r.Qualifier.Equals(o.Qualifier)
} }
func (r *Umount) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Umount) Constraint() constraint {
return blockKind
}
func (r *Umount) Kind() Kind {
return UMOUNT
}
type Remount struct { type Remount struct {
RuleBase
Qualifier Qualifier
MountConditions MountConditions
MountPoint string MountPoint string
} }
func RemountFromLog(log map[string]string) ApparmorRule { func newRemountFromLog(log map[string]string) Rule {
return &Remount{ return &Remount{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
MountConditions: MountConditionsFromLog(log), Qualifier: newQualifierFromLog(log),
MountConditions: newMountConditionsFromLog(log),
MountPoint: log["name"], MountPoint: log["name"],
} }
} }
func (r *Remount) Validate() error {
if err := r.MountConditions.Validate(); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Remount) Less(other any) bool { func (r *Remount) Less(other any) bool {
o, _ := other.(*Remount) o, _ := other.(*Remount)
if r.Qualifier.Equals(o.Qualifier) { if r.MountPoint != o.MountPoint {
if r.MountPoint == o.MountPoint {
return r.MountConditions.Less(o.MountConditions)
}
return r.MountPoint < o.MountPoint return r.MountPoint < o.MountPoint
} }
if r.MountConditions.Equals(o.MountConditions) {
return r.MountConditions.Less(o.MountConditions)
}
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
@ -135,3 +210,15 @@ func (r *Remount) Equals(other any) bool {
r.MountConditions.Equals(o.MountConditions) && r.MountConditions.Equals(o.MountConditions) &&
r.Qualifier.Equals(o.Qualifier) r.Qualifier.Equals(o.Qualifier)
} }
func (r *Remount) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Remount) Constraint() constraint {
return blockKind
}
func (r *Remount) Kind() Kind {
return REMOUNT
}

View file

@ -4,17 +4,34 @@
package aa package aa
import "strings" import (
"fmt"
"slices"
"strings"
)
const MQUEUE Kind = "mqueue"
func init() {
requirements[MQUEUE] = requirement{
"access": []string{
"r", "w", "rw", "read", "write", "create", "open",
"delete", "getattr", "setattr",
},
"type": []string{"posix", "sysv"},
}
}
type Mqueue struct { type Mqueue struct {
RuleBase
Qualifier Qualifier
Access string Access []string
Type string Type string
Label string Label string
Name string Name string
} }
func MqueueFromLog(log map[string]string) ApparmorRule { func newMqueueFromLog(log map[string]string) Rule {
mqueueType := "posix" mqueueType := "posix"
if strings.Contains(log["class"], "posix") { if strings.Contains(log["class"], "posix") {
mqueueType = "posix" mqueueType = "posix"
@ -22,29 +39,53 @@ func MqueueFromLog(log map[string]string) ApparmorRule {
mqueueType = "sysv" mqueueType = "sysv"
} }
return &Mqueue{ return &Mqueue{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Access: toAccess(log["requested"]), Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(MQUEUE, log["requested"])),
Type: mqueueType, Type: mqueueType,
Label: log["label"], Label: log["label"],
Name: log["name"], Name: log["name"],
} }
} }
func (r *Mqueue) Validate() error {
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
if err := validateValues(r.Kind(), "type", []string{r.Type}); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Mqueue) Less(other any) bool { func (r *Mqueue) Less(other any) bool {
o, _ := other.(*Mqueue) o, _ := other.(*Mqueue)
if r.Qualifier.Equals(o.Qualifier) { if len(r.Access) != len(o.Access) {
if r.Access == o.Access { return len(r.Access) < len(o.Access)
if r.Type == o.Type {
return r.Label < o.Label
} }
if r.Type != o.Type {
return r.Type < o.Type return r.Type < o.Type
} }
return r.Access < o.Access if r.Label != o.Label {
return r.Label < o.Label
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
func (r *Mqueue) Equals(other any) bool { func (r *Mqueue) Equals(other any) bool {
o, _ := other.(*Mqueue) o, _ := other.(*Mqueue)
return r.Access == o.Access && r.Type == o.Type && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier) return slices.Equal(r.Access, o.Access) && r.Type == o.Type && r.Label == o.Label &&
r.Name == o.Name && r.Qualifier.Equals(o.Qualifier)
}
func (r *Mqueue) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Mqueue) Constraint() constraint {
return blockKind
}
func (r *Mqueue) Kind() Kind {
return MQUEUE
} }

View file

@ -4,59 +4,109 @@
package aa package aa
import (
"fmt"
)
const NETWORK Kind = "network"
func init() {
requirements[NETWORK] = requirement{
"access": []string{
"create", "bind", "listen", "accept", "connect", "shutdown",
"getattr", "setattr", "getopt", "setopt", "send", "receive",
"r", "w", "rw",
},
"domains": []string{
"unix", "inet", "ax25", "ipx", "appletalk", "netrom", "bridge",
"atmpvc", "x25", "inet6", "rose", "netbeui", "security", "key",
"netlink", "packet", "ash", "econet", "atmsvc", "rds", "sna", "irda",
"pppox", "wanpipe", "llc", "ib", "mpls", "can", "tipc", "bluetooth",
"iucv", "rxrpc", "isdn", "phonet", "ieee802154", "caif", "alg",
"nfc", "vsock", "kcm", "qipcrtr", "smc", "xdp", "mctp",
},
"type": []string{
"stream", "dgram", "seqpacket", "rdm", "raw", "packet",
},
"protocol": []string{"tcp", "udp", "icmp"},
}
}
type AddressExpr struct { type AddressExpr struct {
Source string Source string
Destination string Destination string
Port string Port string
} }
func newAddressExprFromLog(log map[string]string) AddressExpr {
return AddressExpr{
Source: log["laddr"],
Destination: log["faddr"],
Port: log["lport"],
}
}
func (r AddressExpr) Less(other AddressExpr) bool {
if r.Source != other.Source {
return r.Source < other.Source
}
if r.Destination != other.Destination {
return r.Destination < other.Destination
}
return r.Port < other.Port
}
func (r AddressExpr) Equals(other AddressExpr) bool { func (r AddressExpr) Equals(other AddressExpr) bool {
return r.Source == other.Source && r.Destination == other.Destination && return r.Source == other.Source && r.Destination == other.Destination &&
r.Port == other.Port r.Port == other.Port
} }
func (r AddressExpr) Less(other AddressExpr) bool {
if r.Source == other.Source {
if r.Destination == other.Destination {
return r.Port < other.Port
}
return r.Destination < other.Destination
}
return r.Source < other.Source
}
type Network struct { type Network struct {
RuleBase
Qualifier Qualifier
AddressExpr
Domain string Domain string
Type string Type string
Protocol string Protocol string
AddressExpr
} }
func NetworkFromLog(log map[string]string) ApparmorRule { func newNetworkFromLog(log map[string]string) Rule {
return &Network{ return &Network{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
AddressExpr: AddressExpr{ Qualifier: newQualifierFromLog(log),
Source: log["laddr"], AddressExpr: newAddressExprFromLog(log),
Destination: log["faddr"],
Port: log["lport"],
},
Domain: log["family"], Domain: log["family"],
Type: log["sock_type"], Type: log["sock_type"],
Protocol: log["protocol"], Protocol: log["protocol"],
} }
} }
func (r *Network) Validate() error {
if err := validateValues(r.Kind(), "domains", []string{r.Domain}); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
if err := validateValues(r.Kind(), "type", []string{r.Type}); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
if err := validateValues(r.Kind(), "protocol", []string{r.Protocol}); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Network) Less(other any) bool { func (r *Network) Less(other any) bool {
o, _ := other.(*Network) o, _ := other.(*Network)
if r.Qualifier.Equals(o.Qualifier) { if r.Domain != o.Domain {
if r.Domain == o.Domain { return r.Domain < o.Domain
if r.Type == o.Type {
return r.Protocol < o.Protocol
} }
if r.Type != o.Type {
return r.Type < o.Type return r.Type < o.Type
} }
return r.Domain < o.Domain if r.Protocol != o.Protocol {
return r.Protocol < o.Protocol
}
if r.AddressExpr.Less(o.AddressExpr) {
return r.AddressExpr.Less(o.AddressExpr)
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
@ -67,3 +117,15 @@ func (r *Network) Equals(other any) bool {
r.Protocol == o.Protocol && r.AddressExpr.Equals(o.AddressExpr) && r.Protocol == o.Protocol && r.AddressExpr.Equals(o.AddressExpr) &&
r.Qualifier.Equals(o.Qualifier) r.Qualifier.Equals(o.Qualifier)
} }
func (r *Network) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Network) Constraint() constraint {
return blockKind
}
func (r *Network) Kind() Kind {
return NETWORK
}

245
pkg/aa/parse.go Normal file
View file

@ -0,0 +1,245 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"fmt"
"slices"
"strings"
)
const (
tokARROW = "->"
tokEQUAL = "="
tokLESS = "<"
tokPLUS = "+"
tokCLOSEBRACE = '}'
tokCLOSEBRACKET = ']'
tokCLOSEPAREN = ')'
tokCOLON = ','
tokOPENBRACE = '{'
tokOPENBRACKET = '['
tokOPENPAREN = '('
)
var (
newRuleMap = map[string]func([]string) (Rule, error){
COMMENT.Tok(): newComment,
ABI.Tok(): newAbi,
ALIAS.Tok(): newAlias,
INCLUDE.Tok(): newInclude,
}
tok = map[Kind]string{
COMMENT: "#",
VARIABLE: "@{",
HAT: "^",
}
openBlocks = []rune{tokOPENPAREN, tokOPENBRACE, tokOPENBRACKET}
closeBlocks = []rune{tokCLOSEPAREN, tokCLOSEBRACE, tokCLOSEBRACKET}
)
// Split a raw input rule string into tokens by space or =, but ignore spaces
// within quotes, brakets, or parentheses.
//
// Example:
//
// `owner @{user_config_dirs}/powerdevilrc{,.@{rand6}} rwl -> @{user_config_dirs}/#@{int}`
//
// Returns:
//
// []string{"owner", "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", "rwl", "->", "@{user_config_dirs}/#@{int}"}
func tokenize(str string) []string {
var currentToken strings.Builder
var isVariable bool
var quoted bool
blockStack := []rune{}
tokens := make([]string, 0, len(str)/2)
if len(str) > 2 && str[0:2] == VARIABLE.Tok() {
isVariable = true
}
for _, r := range str {
switch {
case (r == ' ' || r == '\t') && len(blockStack) == 0 && !quoted:
// Split on space/tab if not in a block or quoted
if currentToken.Len() != 0 {
tokens = append(tokens, currentToken.String())
currentToken.Reset()
}
case (r == '=' || r == '+') && len(blockStack) == 0 && !quoted && isVariable:
// Handle variable assignment
if currentToken.Len() != 0 {
tokens = append(tokens, currentToken.String())
currentToken.Reset()
}
tokens = append(tokens, string(r))
case r == '"' && len(blockStack) == 0:
quoted = !quoted
currentToken.WriteRune(r)
case slices.Contains(openBlocks, r):
blockStack = append(blockStack, r)
currentToken.WriteRune(r)
case slices.Contains(closeBlocks, r):
if len(blockStack) > 0 {
blockStack = blockStack[:len(blockStack)-1]
} else {
panic(fmt.Sprintf("Unbalanced block, missing '{' or '}' on: %s\n", str))
}
currentToken.WriteRune(r)
default:
currentToken.WriteRune(r)
}
}
if currentToken.Len() != 0 {
tokens = append(tokens, currentToken.String())
}
return tokens
}
func tokenToSlice(token string) []string {
res := []string{}
token = strings.Trim(token, "()\n")
if strings.ContainsAny(token, ", ") {
var sep string
switch {
case strings.Contains(token, ","):
sep = ","
case strings.Contains(token, " "):
sep = " "
}
for _, v := range strings.Split(token, sep) {
res = append(res, strings.Trim(v, " "))
}
} else {
res = append(res, token)
}
return res
}
func tokensStripComment(tokens []string) []string {
res := []string{}
for _, v := range tokens {
if v == COMMENT.Tok() {
break
}
res = append(res, v)
}
return res
}
// Convert a slice of internal rules to a slice of ApparmorRule.
func newRules(rules [][]string) (Rules, error) {
var err error
var r Rule
res := make(Rules, 0, len(rules))
for _, rule := range rules {
if len(rule) == 0 {
return nil, fmt.Errorf("Empty rule")
}
if newRule, ok := newRuleMap[rule[0]]; ok {
r, err = newRule(rule)
if err != nil {
return nil, err
}
res = append(res, r)
} else if strings.HasPrefix(rule[0], VARIABLE.Tok()) {
r, err = newVariable(rule)
if err != nil {
return nil, err
}
res = append(res, r)
} else {
return nil, fmt.Errorf("Unrecognized rule: %s", rule)
}
}
return res, nil
}
func (f *AppArmorProfileFile) parsePreamble(input []string) error {
var err error
var r Rule
var rules Rules
tokenizedRules := [][]string{}
for _, line := range input {
if strings.HasPrefix(line, COMMENT.Tok()) {
r, err = newComment(strings.Split(line, " "))
if err != nil {
return err
}
rules = append(rules, r)
} else {
tokens := tokenize(line)
tokenizedRules = append(tokenizedRules, tokens)
}
}
rr, err := newRules(tokenizedRules)
if err != nil {
return err
}
f.Preamble = append(f.Preamble, rules...)
f.Preamble = append(f.Preamble, rr...)
return nil
}
// Parse an apparmor profile file.
//
// Only supports parsing of apparmor file preamble and profile headers.
//
// Warning: It is purposelly an uncomplete basic parser for apparmor profile,
// it is only aimed for internal tooling purpose. For "simplicity", it is not
// using antlr / participle. It is only used for experimental feature in the
// apparmor.d project.
//
// Stop at the first profile header. Does not support multiline coma rules.
//
// Current use case:
//
// - Parse include and tunables
// - Parse variable in profile preamble and in tunable files
// - Parse (sub) profiles header to edit flags
func (f *AppArmorProfileFile) Parse(input string) error {
rawHeader := ""
rawPreamble := []string{}
done:
for _, line := range strings.Split(input, "\n") {
tmp := strings.TrimLeft(line, "\t ")
tmp = strings.TrimRight(tmp, ",")
switch {
case tmp == "":
continue
case strings.HasPrefix(tmp, PROFILE.Tok()):
rawHeader = tmp
break done
case strings.HasPrefix(tmp, HAT.String()), strings.HasPrefix(tmp, HAT.Tok()):
break done
default:
rawPreamble = append(rawPreamble, tmp)
}
}
if err := f.parsePreamble(rawPreamble); err != nil {
return err
}
if rawHeader != "" {
header, err := newHeader(tokenize(rawHeader))
if err != nil {
return err
}
profile := &Profile{Header: header}
f.Profiles = append(f.Profiles, profile)
}
return nil
}

281
pkg/aa/parse_test.go Normal file
View file

@ -0,0 +1,281 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"reflect"
"testing"
"github.com/roddhjav/apparmor.d/pkg/util"
)
func Test_tokenizeRule(t *testing.T) {
for _, tt := range testTokenRules {
t.Run(tt.name, func(t *testing.T) {
if got := tokenize(tt.raw); !reflect.DeepEqual(got, tt.tokens) {
t.Errorf("tokenize() = %v, want %v", got, tt.tokens)
}
})
}
}
func Test_AppArmorProfileFile_Parse(t *testing.T) {
for _, tt := range testBlocks {
t.Run(tt.name, func(t *testing.T) {
got := &AppArmorProfileFile{}
if err := got.Parse(tt.raw); (err != nil) != tt.wParseErr {
t.Errorf("AppArmorProfileFile.Parse() error = %v, wantErr %v", err, tt.wParseErr)
}
if !reflect.DeepEqual(got, tt.apparmor) {
t.Errorf("AppArmorProfileFile.Parse() = |%v|, want |%v|", got, tt.apparmor)
}
})
}
}
var (
// Test cases for tokenize
testTokenRules = []struct {
name string
raw string
tokens []string
}{
{
name: "empty",
raw: "",
tokens: []string{},
},
{
name: "abi",
raw: `abi <abi/4.0>`,
tokens: []string{"abi", "<abi/4.0>"},
},
{
name: "alias",
raw: `alias /mnt/usr -> /usr`,
tokens: []string{"alias", "/mnt/usr", "->", "/usr"},
},
{
name: "variable",
raw: `@{name} = torbrowser "tor browser"`,
tokens: []string{"@{name}", "=", "torbrowser", `"tor browser"`},
},
{
name: "variable-2",
raw: `@{exec_path} += @{bin}/@{name}`,
tokens: []string{"@{exec_path}", "+", "=", "@{bin}/@{name}"},
},
{
name: "variable-3",
raw: `@{empty}="dummy"`,
tokens: []string{"@{empty}", "=", `"dummy"`},
},
{
name: "variable-4",
raw: `@{XDG_PROJECTS_DIR}+="Git"`,
tokens: []string{"@{XDG_PROJECTS_DIR}", "+", "=", `"Git"`},
},
{
name: "header",
raw: `profile foo @{exec_path} xattrs=(security.tagged=allowed) flags=(complain attach_disconnected)`,
tokens: []string{"profile", "foo", "@{exec_path}", "xattrs=(security.tagged=allowed)", "flags=(complain attach_disconnected)"},
},
{
name: "include",
raw: `include <tunables/global>`,
tokens: []string{"include", "<tunables/global>"},
},
{
name: "include-if-exists",
raw: `include if exists "/etc/apparmor.d/dummy"`,
tokens: []string{"include", "if", "exists", `"/etc/apparmor.d/dummy"`},
},
{
name: "rlimit",
raw: `set rlimit nproc <= 200`,
tokens: []string{"set", "rlimit", "nproc", "<=", "200"},
},
{
name: "userns",
raw: `userns`,
tokens: []string{"userns"},
},
{
name: "capability",
raw: `capability dac_read_search`,
tokens: []string{"capability", "dac_read_search"},
},
{
name: "network",
raw: `network netlink raw`,
tokens: []string{"network", "netlink", "raw"},
},
{
name: "mount",
raw: `mount /{,**}`,
tokens: []string{"mount", "/{,**}"},
},
{
name: "mount-2",
raw: `mount options=(rw rbind) /tmp/newroot/ -> /tmp/newroot/`,
tokens: []string{"mount", "options=(rw rbind)", "/tmp/newroot/", "->", "/tmp/newroot/"},
},
{
name: "mount-3",
raw: `mount options=(rw silent rprivate) -> /oldroot/`,
tokens: []string{"mount", "options=(rw silent rprivate)", "->", "/oldroot/"},
},
{
name: "mount-4",
raw: `mount fstype=devpts options=(rw nosuid noexec) devpts -> /newroot/dev/pts/`,
tokens: []string{"mount", "fstype=devpts", "options=(rw nosuid noexec)", "devpts", "->", "/newroot/dev/pts/"},
},
{
name: "signal",
raw: `signal (receive) set=(cont, term,winch) peer=at-spi-bus-launcher`,
tokens: []string{"signal", "(receive)", "set=(cont, term,winch)", "peer=at-spi-bus-launcher"},
},
{
name: "unix",
raw: `unix (send receive) type=stream addr="@/tmp/.ICE[0-9]*-unix/19 5" peer=(label="@{p_systemd}", addr=none)`,
tokens: []string{"unix", "(send receive)", "type=stream", "addr=\"@/tmp/.ICE[0-9]*-unix/19 5\"", "peer=(label=\"@{p_systemd}\", addr=none)"},
},
{
name: "unix-2",
raw: ` unix (connect, receive, send)
type=stream
peer=(addr="@/tmp/ibus/dbus-????????")`,
tokens: []string{"unix", "(connect, receive, send)\n", "type=stream\n", `peer=(addr="@/tmp/ibus/dbus-????????")`},
},
{
name: "dbus",
raw: `dbus receive bus=system path=/org/freedesktop/DBus interface=org.freedesktop.DBus member=AddMatch peer=(name=:1.3, label=power-profiles-daemon)`,
tokens: []string{
"dbus", "receive", "bus=system",
"path=/org/freedesktop/DBus", "interface=org.freedesktop.DBus",
"member=AddMatch", "peer=(name=:1.3, label=power-profiles-daemon)",
},
},
{
name: "file-1",
raw: `owner @{user_config_dirs}/powerdevilrc{,.@{rand6}} rwl -> @{user_config_dirs}/#@{int}`,
tokens: []string{"owner", "@{user_config_dirs}/powerdevilrc{,.@{rand6}}", "rwl", "->", "@{user_config_dirs}/#@{int}"},
},
{
name: "file-2",
raw: `@{sys}/devices/@{pci}/class r`,
tokens: []string{"@{sys}/devices/@{pci}/class", "r"},
},
{
name: "file-3",
raw: `owner @{PROC}/@{pid}/task/@{tid}/comm rw`,
tokens: []string{"owner", "@{PROC}/@{pid}/task/@{tid}/comm", "rw"},
},
{
name: "file-4",
raw: `owner /{var/,}tmp/#@{int} rw`,
tokens: []string{"owner", "/{var/,}tmp/#@{int}", "rw"},
},
}
// Test cases for Parse
testBlocks = []struct {
name string
raw string
apparmor *AppArmorProfileFile
wParseErr bool
}{
{
name: "empty",
raw: "",
apparmor: &AppArmorProfileFile{},
wParseErr: false,
},
{
name: "comment",
raw: `
# IsLineRule comment
include <tunables/global> # comment included
@{lib_dirs} = @{lib}/@{name} /opt/@{name} # comment in variable`,
apparmor: &AppArmorProfileFile{
Preamble: Rules{
&Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " IsLineRule comment"}},
&Include{
RuleBase: RuleBase{Comment: " comment included"},
Path: "tunables/global", IsMagic: true,
},
&Variable{
RuleBase: RuleBase{Comment: " comment in variable"},
Name: "lib_dirs", Define: true,
Values: []string{"@{lib}/@{name}", "/opt/@{name}"},
},
},
},
wParseErr: false,
},
{
name: "cornercases",
raw: `# Simple test
include <tunables/global>
# { commented block }
@{name} = {D,d}ummy
@{exec_path} = @{bin}/@{name}
alias /mnt/{,usr.sbin.}mount.cifs -> /sbin/mount.cifs,
@{coreutils} += gawk {,e,f}grep head
profile @{exec_path} {
`,
apparmor: &AppArmorProfileFile{
Preamble: Rules{
&Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " Simple test"}},
&Comment{RuleBase: RuleBase{IsLineRule: true, Comment: " { commented block }"}},
&Include{IsMagic: true, Path: "tunables/global"},
&Variable{Name: "name", Values: []string{"{D,d}ummy"}, Define: true},
&Variable{Name: "exec_path", Values: []string{"@{bin}/@{name}"}, Define: true},
&Alias{Path: "/mnt/{,usr.sbin.}mount.cifs", RewrittenPath: "/sbin/mount.cifs"},
&Variable{Name: "coreutils", Values: []string{"gawk", "{,e,f}grep", "head"}, Define: false},
},
Profiles: []*Profile{
{
Header: Header{
Name: "@{exec_path}",
Attachments: []string{},
Attributes: map[string]string{},
Flags: []string{},
},
},
},
},
wParseErr: false,
},
{
name: "string.aa",
raw: util.MustReadFile(testData.Join("string.aa")),
apparmor: &AppArmorProfileFile{
Preamble: Rules{
&Comment{RuleBase: RuleBase{Comment: " Simple test profile for the AppArmorProfileFile.String() method", IsLineRule: true}},
&Abi{IsMagic: true, Path: "abi/4.0"},
&Alias{Path: "/mnt/usr", RewrittenPath: "/usr"},
&Include{IsMagic: true, Path: "tunables/global"},
&Variable{
Name: "exec_path", Define: true,
Values: []string{"@{bin}/foo", "@{lib}/foo"},
},
},
Profiles: []*Profile{
{
Header: Header{
Name: "foo",
Attachments: []string{"@{exec_path}"},
Attributes: map[string]string{"security.tagged": "allowed"},
Flags: []string{"complain", "attach_disconnected"},
},
},
},
},
wParseErr: false,
},
}
)

View file

@ -4,32 +4,40 @@
package aa package aa
const PIVOTROOT = "pivot_root"
type PivotRoot struct { type PivotRoot struct {
RuleBase
Qualifier Qualifier
OldRoot string OldRoot string
NewRoot string NewRoot string
TargetProfile string TargetProfile string
} }
func PivotRootFromLog(log map[string]string) ApparmorRule { func newPivotRootFromLog(log map[string]string) Rule {
return &PivotRoot{ return &PivotRoot{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
OldRoot: log["srcname"], OldRoot: log["srcname"],
NewRoot: log["name"], NewRoot: log["name"],
TargetProfile: "", TargetProfile: "",
} }
} }
func (r *PivotRoot) Validate() error {
return nil
}
func (r *PivotRoot) Less(other any) bool { func (r *PivotRoot) Less(other any) bool {
o, _ := other.(*PivotRoot) o, _ := other.(*PivotRoot)
if r.Qualifier.Equals(o.Qualifier) { if r.OldRoot != o.OldRoot {
if r.OldRoot == o.OldRoot { return r.OldRoot < o.OldRoot
if r.NewRoot == o.NewRoot {
return r.TargetProfile < o.TargetProfile
} }
if r.NewRoot != o.NewRoot {
return r.NewRoot < o.NewRoot return r.NewRoot < o.NewRoot
} }
return r.OldRoot < o.OldRoot if r.TargetProfile != o.TargetProfile {
return r.TargetProfile < o.TargetProfile
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
@ -40,3 +48,15 @@ func (r *PivotRoot) Equals(other any) bool {
r.TargetProfile == o.TargetProfile && r.TargetProfile == o.TargetProfile &&
r.Qualifier.Equals(o.Qualifier) r.Qualifier.Equals(o.Qualifier)
} }
func (r *PivotRoot) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *PivotRoot) Constraint() constraint {
return blockKind
}
func (r *PivotRoot) Kind() Kind {
return PIVOTROOT
}

310
pkg/aa/preamble.go Normal file
View file

@ -0,0 +1,310 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"fmt"
"slices"
"strings"
)
const (
ABI Kind = "abi"
ALIAS Kind = "alias"
INCLUDE Kind = "include"
VARIABLE Kind = "variable"
COMMENT Kind = "comment"
tokIFEXISTS = "if exists"
)
type Comment struct {
RuleBase
}
func newComment(rule []string) (Rule, error) {
base := newRule(rule)
base.IsLineRule = true
return &Comment{RuleBase: base}, nil
}
func (r *Comment) Validate() error {
return nil
}
func (r *Comment) Less(other any) bool {
return false
}
func (r *Comment) Equals(other any) bool {
return false
}
func (r *Comment) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Comment) IsPreamble() bool {
return true
}
func (r *Comment) Constraint() constraint {
return anyKind
}
func (r *Comment) Kind() Kind {
return COMMENT
}
type Abi struct {
RuleBase
Path string
IsMagic bool
}
func newAbi(rule []string) (Rule, error) {
var magic bool
if len(rule) > 0 && rule[0] == ABI.Tok() {
rule = rule[1:]
}
if len(rule) != 1 {
return nil, fmt.Errorf("invalid abi format: %s", rule)
}
path := rule[0]
switch {
case path[0] == '"':
magic = false
case path[0] == '<':
magic = true
default:
return nil, fmt.Errorf("invalid path %s in rule: %s", path, rule)
}
return &Abi{
RuleBase: newRule(rule),
Path: strings.Trim(path, "\"<>"),
IsMagic: magic,
}, nil
}
func (r *Abi) Validate() error {
return nil
}
func (r *Abi) Less(other any) bool {
o, _ := other.(*Abi)
if r.Path != o.Path {
return r.Path < o.Path
}
return r.IsMagic == o.IsMagic
}
func (r *Abi) Equals(other any) bool {
o, _ := other.(*Abi)
return r.Path == o.Path && r.IsMagic == o.IsMagic
}
func (r *Abi) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Abi) Constraint() constraint {
return preambleKind
}
func (r *Abi) Kind() Kind {
return ABI
}
type Alias struct {
RuleBase
Path string
RewrittenPath string
}
func newAlias(rule []string) (Rule, error) {
if len(rule) > 0 && rule[0] == ALIAS.Tok() {
rule = rule[1:]
}
if len(rule) != 3 {
return nil, fmt.Errorf("invalid alias format: %s", rule)
}
if rule[1] != tokARROW {
return nil, fmt.Errorf("invalid alias format, missing %s in: %s", tokARROW, rule)
}
return &Alias{
RuleBase: newRule(rule),
Path: rule[0],
RewrittenPath: rule[2],
}, nil
}
func (r *Alias) Validate() error {
return nil
}
func (r Alias) Less(other any) bool {
o, _ := other.(*Alias)
if r.Path != o.Path {
return r.Path < o.Path
}
return r.RewrittenPath < o.RewrittenPath
}
func (r Alias) Equals(other any) bool {
o, _ := other.(*Alias)
return r.Path == o.Path && r.RewrittenPath == o.RewrittenPath
}
func (r *Alias) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Alias) Constraint() constraint {
return preambleKind
}
func (r *Alias) Kind() Kind {
return ALIAS
}
type Include struct {
RuleBase
IfExists bool
Path string
IsMagic bool
}
func newInclude(rule []string) (Rule, error) {
var magic bool
var ifexists bool
if len(rule) > 0 && rule[0] == INCLUDE.Tok() {
rule = rule[1:]
}
size := len(rule)
if size == 0 {
return nil, fmt.Errorf("invalid include format: %v", rule)
}
if size >= 3 && strings.Join(rule[:2], " ") == tokIFEXISTS {
ifexists = true
rule = rule[2:]
}
path := rule[0]
switch {
case path[0] == '"':
magic = false
case path[0] == '<':
magic = true
default:
return nil, fmt.Errorf("invalid path format: %v", path)
}
return &Include{
RuleBase: newRule(rule),
IfExists: ifexists,
Path: strings.Trim(path, "\"<>"),
IsMagic: magic,
}, nil
}
func (r *Include) Validate() error {
return nil
}
func (r *Include) Less(other any) bool {
o, _ := other.(*Include)
if r.Path == o.Path {
return r.Path < o.Path
}
if r.IsMagic != o.IsMagic {
return r.IsMagic
}
return r.IfExists
}
func (r *Include) Equals(other any) bool {
o, _ := other.(*Include)
return r.Path == o.Path && r.IsMagic == o.IsMagic && r.IfExists == o.IfExists
}
func (r *Include) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Include) Constraint() constraint {
return anyKind
}
func (r *Include) Kind() Kind {
return INCLUDE
}
type Variable struct {
RuleBase
Name string
Values []string
Define bool
}
func newVariable(rule []string) (Rule, error) {
var define bool
var values []string
if len(rule) < 3 {
return nil, fmt.Errorf("invalid variable format: %v", rule)
}
name := strings.Trim(rule[0], VARIABLE.Tok()+"}")
switch rule[1] {
case tokEQUAL:
define = true
values = tokensStripComment(rule[2:])
case tokPLUS:
if rule[2] != tokEQUAL {
return nil, fmt.Errorf("invalid operator in variable: %v", rule)
}
define = false
values = tokensStripComment(rule[3:])
default:
return nil, fmt.Errorf("invalid operator in variable: %v", rule)
}
return &Variable{
RuleBase: newRule(rule),
Name: name,
Values: values,
Define: define,
}, nil
}
func (r *Variable) Validate() error {
return nil
}
func (r *Variable) Less(other any) bool {
o, _ := other.(*Variable)
if r.Name != o.Name {
return r.Name < o.Name
}
return len(r.Values) < len(o.Values)
}
func (r *Variable) Equals(other any) bool {
o, _ := other.(*Variable)
return r.Name == o.Name && slices.Equal(r.Values, o.Values)
}
func (r *Variable) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Variable) Constraint() constraint {
return preambleKind
}
func (r *Variable) Kind() Kind {
return VARIABLE
}

View file

@ -5,71 +5,205 @@
package aa package aa
import ( import (
"bytes" "fmt"
"reflect" "maps"
"slices" "slices"
"sort"
"strings" "strings"
"github.com/roddhjav/apparmor.d/pkg/paths"
) )
// Default Apparmor magic directory: /etc/apparmor.d/. const (
var MagicRoot = paths.New("/etc/apparmor.d") PROFILE Kind = "profile"
// AppArmorProfiles represents a full set of apparmor profiles tokATTRIBUTES = "xattrs"
type AppArmorProfiles map[string]*AppArmorProfile tokFLAGS = "flags"
)
// ApparmorProfile represents a full apparmor profile. func init() {
// Warning: close to the BNF grammar of apparmor profile but not exactly the same (yet): requirements[PROFILE] = requirement{
// - Some rules are not supported yet (subprofile, hat...) tokFLAGS: {
// - The structure is simplified as it only aims at writing profile, not parsing it. "enforce", "complain", "kill", "default_allow", "unconfined",
type AppArmorProfile struct { "prompt", "audit", "mediate_deleted", "attach_disconnected",
Preamble "attach_disconneced.path=", "chroot_relative", "debug",
Profile "interruptible", "kill", "kill.signal=",
},
}
} }
// Preamble section of a profile // Profile represents a single AppArmor profile.
type Preamble struct {
Abi []Abi
Includes []Include
Aliases []Alias
Variables []Variable
}
// Profile section of a profile
type Profile struct { type Profile struct {
RuleBase
Header
Rules Rules
}
// Header represents the header of a profile.
type Header struct {
Name string Name string
Attachments []string Attachments []string
Attributes map[string]string Attributes map[string]string
Flags []string Flags []string
Rules Rules
} }
// ApparmorRule generic interface func newHeader(rule []string) (Header, error) {
type ApparmorRule interface { if len(rule) == 0 {
Less(other any) bool return Header{}, nil
Equals(other any) bool }
if rule[len(rule)-1] == "{" {
rule = rule[:len(rule)-1]
}
if rule[0] == PROFILE.Tok() {
rule = rule[1:]
} }
type Rules []ApparmorRule delete := []int{}
flags := []string{}
func NewAppArmorProfile() *AppArmorProfile { attributes := make(map[string]string)
return &AppArmorProfile{} for idx, token := range rule {
if item, ok := strings.CutPrefix(token, tokFLAGS+"="); ok {
flags = tokenToSlice(item)
delete = append(delete, idx)
} else if item, ok := strings.CutPrefix(token, tokATTRIBUTES+"="); ok {
for _, m := range tokenToSlice(item) {
kv := strings.SplitN(m, "=", 2)
attributes[kv[0]] = kv[1]
}
delete = append(delete, idx)
}
}
for i := len(delete) - 1; i >= 0; i-- {
rule = slices.Delete(rule, delete[i], delete[i]+1)
} }
// String returns the formatted representation of a profile as a string name, attachments := "", []string{}
func (p *AppArmorProfile) String() string { if len(rule) >= 1 {
var res bytes.Buffer name = rule[0]
err := tmplAppArmorProfile.Execute(&res, p) if len(rule) > 1 {
if err != nil { attachments = rule[1:]
return err.Error()
} }
return res.String() }
return Header{
Name: name,
Attachments: attachments,
Attributes: attributes,
Flags: flags,
}, nil
} }
// AddRule adds a new rule to the profile from a log map func (r *Profile) Validate() error {
func (p *AppArmorProfile) AddRule(log map[string]string) { if err := validateValues(r.Kind(), tokFLAGS, r.Flags); err != nil {
return fmt.Errorf("profile %s: %w", r.Name, err)
}
return r.Rules.Validate()
}
func (p *Profile) Less(other any) bool {
o, _ := other.(*Profile)
if p.Name != o.Name {
return p.Name < o.Name
}
return len(p.Attachments) < len(o.Attachments)
}
func (p *Profile) Equals(other any) bool {
o, _ := other.(*Profile)
return p.Name == o.Name && slices.Equal(p.Attachments, o.Attachments) &&
maps.Equal(p.Attributes, o.Attributes) &&
slices.Equal(p.Flags, o.Flags)
}
func (p *Profile) String() string {
return renderTemplate(p.Kind(), p)
}
func (p *Profile) Constraint() constraint {
return blockKind
}
func (p *Profile) Kind() Kind {
return PROFILE
}
func (p *Profile) Merge() {
slices.Sort(p.Flags)
p.Flags = slices.Compact(p.Flags)
p.Rules = p.Rules.Merge()
}
func (p *Profile) Sort() {
p.Rules = p.Rules.Sort()
}
func (p *Profile) Format() {
p.Rules = p.Rules.Format()
}
// GetAttachments return a nested attachment string
func (p *Profile) GetAttachments() string {
switch len(p.Attachments) {
case 0:
return ""
case 1:
return p.Attachments[0]
default:
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, ",") + "}"
}
}
var (
newLogMap = map[string]func(log map[string]string) Rule{
"rlimits": newRlimitFromLog,
"cap": newCapabilityFromLog,
"io_uring": newIOUringFromLog,
"signal": newSignalFromLog,
"ptrace": newPtraceFromLog,
"namespace": newUsernsFromLog,
"unix": newUnixFromLog,
"dbus": newDbusFromLog,
"posix_mqueue": newMqueueFromLog,
"sysv_mqueue": newMqueueFromLog,
"mount": func(log map[string]string) Rule {
if strings.Contains(log["flags"], "remount") {
return newRemountFromLog(log)
}
newRule := newLogMountMap[log["operation"]]
return newRule(log)
},
"net": func(log map[string]string) Rule {
if log["family"] == "unix" {
return newUnixFromLog(log)
} else {
return newNetworkFromLog(log)
}
},
"file": func(log map[string]string) Rule {
if log["operation"] == "change_onexec" {
return newChangeProfileFromLog(log)
} else {
return newFileFromLog(log)
}
},
"exec": newFileFromLog,
"file_inherit": newFileFromLog,
"file_perm": newFileFromLog,
"open": newFileFromLog,
}
newLogMountMap = map[string]func(log map[string]string) Rule{
"mount": newMountFromLog,
"umount": newUmountFromLog,
"remount": newRemountFromLog,
"pivotroot": newPivotRootFromLog,
}
)
func (p *Profile) AddRule(log map[string]string) {
// Generate profile flags and extra rules // Generate profile flags and extra rules
switch log["error"] { switch log["error"] {
case "-2": case "-2":
@ -78,134 +212,27 @@ func (p *AppArmorProfile) AddRule(log map[string]string) {
} }
case "-13": case "-13":
if strings.Contains(log["info"], "namespace creation restricted") { if strings.Contains(log["info"], "namespace creation restricted") {
p.Rules = append(p.Rules, UsernsFromLog(log)) p.Rules = append(p.Rules, newUsernsFromLog(log))
} else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") { } else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") {
p.Flags = append(p.Flags, "attach_disconnected") p.Flags = append(p.Flags, "attach_disconnected")
} }
default: default:
} }
switch log["class"] { done := false
case "cap": for _, key := range []string{"class", "family", "operation"} {
p.Rules = append(p.Rules, CapabilityFromLog(log)) if newRule, ok := newLogMap[log[key]]; ok {
case "net": p.Rules = append(p.Rules, newRule(log))
if log["family"] == "unix" { done = true
p.Rules = append(p.Rules, UnixFromLog(log)) break
} else {
p.Rules = append(p.Rules, NetworkFromLog(log))
}
case "mount":
if strings.Contains(log["flags"], "remount") {
p.Rules = append(p.Rules, RemountFromLog(log))
} else {
switch log["operation"] {
case "mount":
p.Rules = append(p.Rules, MountFromLog(log))
case "umount":
p.Rules = append(p.Rules, UmountFromLog(log))
case "remount":
p.Rules = append(p.Rules, RemountFromLog(log))
case "pivotroot":
p.Rules = append(p.Rules, PivotRootFromLog(log))
} }
} }
case "posix_mqueue", "sysv_mqueue":
p.Rules = append(p.Rules, MqueueFromLog(log)) if !done {
case "signal":
p.Rules = append(p.Rules, SignalFromLog(log))
case "ptrace":
p.Rules = append(p.Rules, PtraceFromLog(log))
case "namespace":
p.Rules = append(p.Rules, UsernsFromLog(log))
case "unix":
p.Rules = append(p.Rules, UnixFromLog(log))
case "file":
if log["operation"] == "change_onexec" {
p.Rules = append(p.Rules, ChangeProfileFromLog(log))
} else {
p.Rules = append(p.Rules, FileFromLog(log))
}
default:
if strings.Contains(log["operation"], "dbus") { if strings.Contains(log["operation"], "dbus") {
p.Rules = append(p.Rules, DbusFromLog(log)) p.Rules = append(p.Rules, newDbusFromLog(log))
} else if log["family"] == "unix" { } else {
p.Rules = append(p.Rules, UnixFromLog(log)) fmt.Printf("unknown log type: %s", log)
}
}
}
// Sort the rules in the profile
// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines
func (p *AppArmorProfile) Sort() {
sort.Slice(p.Rules, func(i, j int) bool {
typeOfI := reflect.TypeOf(p.Rules[i])
typeOfJ := reflect.TypeOf(p.Rules[j])
if typeOfI != typeOfJ {
valueOfI := typeToValue(typeOfI)
valueOfJ := typeToValue(typeOfJ)
if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists {
valueOfI = "include_if_exists"
}
if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists {
valueOfJ = "include_if_exists"
}
return ruleWeights[valueOfI] < ruleWeights[valueOfJ]
}
return p.Rules[i].Less(p.Rules[j])
})
}
// MergeRules merge similar rules together
// Steps:
// - Remove identical rules
// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw'
//
// Note: logs.regCleanLogs helps a lot to do a first cleaning
func (p *AppArmorProfile) MergeRules() {
for i := 0; i < len(p.Rules); i++ {
for j := i + 1; j < len(p.Rules); j++ {
typeOfI := reflect.TypeOf(p.Rules[i])
typeOfJ := reflect.TypeOf(p.Rules[j])
if typeOfI != typeOfJ {
continue
}
// If rules are identical, merge them
if p.Rules[i].Equals(p.Rules[j]) {
p.Rules = append(p.Rules[:j], p.Rules[j+1:]...)
j--
}
}
}
}
// Format the profile for better readability before printing it
// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block
func (p *AppArmorProfile) Format() {
const prefixOwner = " "
hasOwnerRule := false
for i := len(p.Rules) - 1; i > 0; i-- {
j := i - 1
typeOfI := reflect.TypeOf(p.Rules[i])
typeOfJ := reflect.TypeOf(p.Rules[j])
// File rule
if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) {
letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path)
letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path)
// Add prefix before rule path to align with other rule
if p.Rules[i].(*File).Owner {
hasOwnerRule = true
} else if hasOwnerRule {
p.Rules[i].(*File).Prefix = prefixOwner
}
if letterI != letterJ {
// Add a new empty line between Files rule of different type
hasOwnerRule = false
p.Rules = append(p.Rules[:i], append([]ApparmorRule{&Rule{}}, p.Rules[i:]...)...)
}
} }
} }
} }

View file

@ -6,328 +6,128 @@ package aa
import ( import (
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/roddhjav/apparmor.d/pkg/paths"
) )
func readprofile(path string) string { func TestProfile_AddRule(t *testing.T) {
file := paths.New("../../").Join(path)
lines, err := file.ReadFileAsLines()
if err != nil {
panic(err)
}
res := ""
for _, line := range lines {
if strings.HasPrefix(line, "#") {
continue
}
res += line + "\n"
}
return res[:len(res)-1]
}
func TestAppArmorProfile_String(t *testing.T) {
tests := []struct {
name string
p *AppArmorProfile
want string
}{
{
name: "empty",
p: &AppArmorProfile{},
want: ``,
},
{
name: "foo",
p: &AppArmorProfile{
Preamble: Preamble{
Abi: []Abi{{IsMagic: true, Path: "abi/4.0"}},
Includes: []Include{{IsMagic: true, Path: "tunables/global"}},
Aliases: []Alias{{Path: "/mnt/usr", RewrittenPath: "/usr"}},
Variables: []Variable{{
Name: "exec_path",
Values: []string{"@{bin}/foo", "@{lib}/foo"},
}},
},
Profile: Profile{
Name: "foo",
Attachments: []string{"@{exec_path}"},
Attributes: map[string]string{"security.tagged": "allowed"},
Flags: []string{"complain", "attach_disconnected"},
Rules: []ApparmorRule{
&Include{IsMagic: true, Path: "abstractions/base"},
&Include{IsMagic: true, Path: "abstractions/nameservice-strict"},
rlimit1,
&Capability{Name: "dac_read_search"},
&Capability{Name: "dac_override"},
&Network{Domain: "inet", Type: "stream"},
&Network{Domain: "inet6", Type: "stream"},
&Mount{
MountConditions: MountConditions{
FsType: "fuse.portal",
Options: []string{"rw", "rbind"},
},
Source: "@{run}/user/@{uid}/ ",
MountPoint: "/",
},
&Umount{
MountConditions: MountConditions{},
MountPoint: "@{run}/user/@{uid}/",
},
&Signal{
Access: "receive",
Set: "term",
Peer: "at-spi-bus-launcher",
},
&Ptrace{Access: "read", Peer: "nautilus"},
&Unix{
Access: "send receive",
Type: "stream",
Address: "@/tmp/.ICE-unix/1995",
Peer: "gnome-shell",
PeerAddr: "none",
},
&Dbus{
Access: "bind",
Bus: "session",
Name: "org.gnome.*",
},
&Dbus{
Access: "receive",
Bus: "system",
Name: ":1.3",
Path: "/org/freedesktop/DBus",
Interface: "org.freedesktop.DBus",
Member: "AddMatch",
Label: "power-profiles-daemon",
},
&File{Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", Access: "rm"},
&File{Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: "rw"},
&File{Path: "@{sys}/devices/@{pci}/class", Access: "r"},
includeLocal1,
},
},
},
want: readprofile("tests/string.aa"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.String(); got != tt.want {
t.Errorf("AppArmorProfile.String() = |%v|, want |%v|", got, tt.want)
}
})
}
}
func TestAppArmorProfile_AddRule(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
log map[string]string log map[string]string
want *AppArmorProfile want *Profile
}{ }{
{ {
name: "capability", name: "capability",
log: capability1Log, log: capability1Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{capability1},
Rules: []ApparmorRule{capability1},
},
}, },
}, },
{ {
name: "network", name: "network",
log: network1Log, log: network1Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{network1},
Rules: []ApparmorRule{network1},
},
}, },
}, },
{ {
name: "mount", name: "mount",
log: mount2Log, log: mount2Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{mount2},
Rules: []ApparmorRule{mount2},
},
}, },
}, },
{ {
name: "signal", name: "signal",
log: signal1Log, log: signal1Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{signal1},
Rules: []ApparmorRule{signal1},
},
}, },
}, },
{ {
name: "ptrace", name: "ptrace",
log: ptrace2Log, log: ptrace2Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{ptrace2},
Rules: []ApparmorRule{ptrace2},
},
}, },
}, },
{ {
name: "unix", name: "unix",
log: unix1Log, log: unix1Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{unix1},
Rules: []ApparmorRule{unix1},
},
}, },
}, },
{ {
name: "dbus", name: "dbus",
log: dbus2Log, log: dbus2Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{dbus2},
Rules: []ApparmorRule{dbus2},
},
}, },
}, },
{ {
name: "file", name: "file",
log: file2Log, log: file2Log,
want: &AppArmorProfile{ want: &Profile{
Profile: Profile{ Rules: Rules{file2},
Rules: []ApparmorRule{file2},
},
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := NewAppArmorProfile() got := &Profile{}
got.AddRule(tt.log) got.AddRule(tt.log)
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfile.AddRule() = %v, want %v", got, tt.want) t.Errorf("Profile.AddRule() = |%v|, want |%v|", got, tt.want)
} }
}) })
} }
} }
func TestAppArmorProfile_Sort(t *testing.T) { func TestProfile_GetAttachments(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
origin *AppArmorProfile Attachments []string
want *AppArmorProfile
}{
{
name: "all",
origin: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{
file2, network1, includeLocal1, dbus2, signal1, ptrace1,
capability2, file1, dbus1, unix2, signal2, mount2,
},
},
},
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{
capability2, network1, mount2, signal1, signal2, ptrace1,
unix2, dbus2, dbus1, file1, file2, includeLocal1,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.origin
got.Sort()
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfile.Sort() = %v, want %v", got, tt.want)
}
})
}
}
func TestAppArmorProfile_MergeRules(t *testing.T) {
tests := []struct {
name string
origin *AppArmorProfile
want *AppArmorProfile
}{
{
name: "all",
origin: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{capability1, capability1, network1, network1, file1, file1},
},
},
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{capability1, network1, file1},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.origin
got.MergeRules()
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfile.MergeRules() = %v, want %v", got, tt.want)
}
})
}
}
func TestAppArmorProfile_Integration(t *testing.T) {
tests := []struct {
name string
p *AppArmorProfile
want string want string
}{ }{
{ {
name: "aa-status", name: "firefox",
p: &AppArmorProfile{ Attachments: []string{
Preamble: Preamble{ "/{usr/,}bin/firefox{,-esr,-bin}",
Abi: []Abi{{IsMagic: true, Path: "abi/3.0"}}, "/{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
Includes: []Include{{IsMagic: true, Path: "tunables/global"}}, "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
Variables: []Variable{{
Name: "exec_path",
Values: []string{"@{bin}/aa-status", "@{bin}/apparmor_status"},
}},
}, },
Profile: Profile{ want: "/{{usr/,}bin/firefox{,-esr,-bin},{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin},opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}}",
Name: "aa-status",
Attachments: []string{"@{exec_path}"},
Rules: Rules{
&Include{IfExists: true, IsMagic: true, Path: "local/aa-status"},
&Capability{Name: "dac_read_search"},
&File{Path: "@{exec_path}", Access: "mr"},
&File{Path: "@{PROC}/@{pids}/attr/apparmor/current", Access: "r"},
&File{Path: "@{PROC}/", Access: "r"},
&File{Path: "@{sys}/module/apparmor/parameters/enabled", Access: "r"},
&File{Path: "@{sys}/kernel/security/apparmor/profiles", Access: "r"},
&File{Path: "@{PROC}/@{pids}/attr/current", Access: "r"},
&Include{IsMagic: true, Path: "abstractions/consoles"},
&File{Qualifier: Qualifier{Owner: true}, Path: "@{PROC}/@{pid}/mounts", Access: "r"},
&Include{IsMagic: true, Path: "abstractions/base"},
&File{Path: "/dev/tty@{int}", Access: "rw"},
&Capability{Name: "sys_ptrace"},
&Ptrace{Access: "read"},
}, },
{
name: "geoclue",
Attachments: []string{
"/{usr/,}libexec/geoclue",
"/{usr/,}libexec/geoclue-2.0/demos/agent",
}, },
want: "/{{usr/,}libexec/geoclue,{usr/,}libexec/geoclue-2.0/demos/agent}",
}, },
want: readprofile("apparmor.d/profiles-a-f/aa-status"), {
name: "null",
Attachments: []string{},
want: "",
},
{
name: "empty",
Attachments: []string{""},
want: "",
},
{
name: "not valid aare",
Attachments: []string{"/file", "relative"},
want: "/{file,relative}",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
tt.p.Sort() p := &Profile{}
tt.p.MergeRules() p.Attachments = tt.Attachments
tt.p.Format() if got := p.GetAttachments(); got != tt.want {
if got := tt.p.String(); "\n"+got != tt.want { t.Errorf("Profile.GetAttachments() = %v, want %v", got, tt.want)
t.Errorf("AppArmorProfile = |%v|, want |%v|", "\n"+got, tt.want)
} }
}) })
} }

View file

@ -4,33 +4,69 @@
package aa package aa
import (
"fmt"
"slices"
)
const PTRACE Kind = "ptrace"
func init() {
requirements[PTRACE] = requirement{
"access": []string{
"r", "w", "rw", "read", "readby", "trace", "tracedby",
},
}
}
type Ptrace struct { type Ptrace struct {
RuleBase
Qualifier Qualifier
Access string Access []string
Peer string Peer string
} }
func PtraceFromLog(log map[string]string) ApparmorRule { func newPtraceFromLog(log map[string]string) Rule {
return &Ptrace{ return &Ptrace{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Access: toAccess(log["requested_mask"]), Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(PTRACE, log["requested_mask"])),
Peer: log["peer"], Peer: log["peer"],
} }
} }
func (r *Ptrace) Validate() error {
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Ptrace) Less(other any) bool { func (r *Ptrace) Less(other any) bool {
o, _ := other.(*Ptrace) o, _ := other.(*Ptrace)
if r.Qualifier.Equals(o.Qualifier) { if len(r.Access) != len(o.Access) {
if r.Access == o.Access { return len(r.Access) < len(o.Access)
return r.Peer == o.Peer
} }
return r.Access < o.Access if r.Peer != o.Peer {
return r.Peer == o.Peer
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
func (r *Ptrace) Equals(other any) bool { func (r *Ptrace) Equals(other any) bool {
o, _ := other.(*Ptrace) o, _ := other.(*Ptrace)
return r.Access == o.Access && r.Peer == o.Peer && return slices.Equal(r.Access, o.Access) && r.Peer == o.Peer &&
r.Qualifier.Equals(o.Qualifier) r.Qualifier.Equals(o.Qualifier)
} }
func (r *Ptrace) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Ptrace) Constraint() constraint {
return blockKind
}
func (r *Ptrace) Kind() Kind {
return PTRACE
}

180
pkg/aa/resolve.go Normal file
View file

@ -0,0 +1,180 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"fmt"
"regexp"
"strings"
"github.com/roddhjav/apparmor.d/pkg/paths"
"github.com/roddhjav/apparmor.d/pkg/util"
)
var (
includeCache map[*Include]*AppArmorProfileFile = make(map[*Include]*AppArmorProfileFile)
regVariableReference = regexp.MustCompile(`@{([^{}]+)}`)
)
// 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
// }
// }
// Append value to variable
seen := map[string]*Variable{}
for idx, variable := range f.Preamble.GetVariables() {
if _, ok := seen[variable.Name]; ok {
if variable.Define {
return fmt.Errorf("variable %s already defined", variable.Name)
}
seen[variable.Name].Values = append(seen[variable.Name].Values, variable.Values...)
f.Preamble = f.Preamble.Delete(idx)
}
if variable.Define {
seen[variable.Name] = variable
}
}
// Resolve variables
for _, variable := range f.Preamble.GetVariables() {
newValues := []string{}
for _, value := range variable.Values {
vars, err := f.resolveValues(value)
if err != nil {
return err
}
newValues = append(newValues, vars...)
}
variable.Values = newValues
}
// Resolve variables in attachements
for _, profile := range f.Profiles {
attachments := []string{}
for _, att := range profile.Attachments {
vars, err := f.resolveValues(att)
if err != nil {
return err
}
attachments = append(attachments, vars...)
}
profile.Attachments = attachments
}
return nil
}
func (f *AppArmorProfileFile) resolveValues(input string) ([]string, error) {
if !strings.Contains(input, VARIABLE.Tok()) {
return []string{input}, nil
}
values := []string{}
match := regVariableReference.FindStringSubmatch(input)
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, VARIABLE.Tok()+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...)
}
}
}
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(INCLUDE)
// Cache the included file
includeCache[include] = iFile
}
// Insert iFile in the place of include in the current file
index := f.Preamble.Index(include)
f.Preamble = f.Preamble.Replace(index, includeCache[include].Preamble...)
return nil
}

273
pkg/aa/resolve_test.go Normal file
View file

@ -0,0 +1,273 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"reflect"
"testing"
"github.com/roddhjav/apparmor.d/pkg/paths"
)
func TestAppArmorProfileFile_resolveInclude(t *testing.T) {
tests := []struct {
name string
include *Include
want *AppArmorProfileFile
wantErr bool
}{
{
name: "empty",
include: &Include{Path: "", IsMagic: true},
want: &AppArmorProfileFile{Preamble: Rules{&Include{Path: "", IsMagic: true}}},
wantErr: true,
},
{
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",
input: "@{etc_ro}",
want: []string{"/{,usr/}etc/"},
},
{
name: "simple",
input: "@{bin}/foo",
want: []string{"/{,usr/}{,s}bin/foo"},
},
{
name: "double",
input: "@{lib}/@{multiarch}",
want: []string{"/{,usr/}lib{,exec,32,64}/*-linux-gnu*"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := DefaultTunables()
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.resolveValues() = %v, want %v", got, tt.want)
}
})
}
}
func TestAppArmorProfileFile_Resolve(t *testing.T) {
tests := []struct {
name string
preamble Rules
attachements []string
want *AppArmorProfileFile
wantErr bool
}{
{
name: "variables/append",
preamble: Rules{
&Variable{Name: "lib", Values: []string{"/{usr/,}lib"}, Define: true},
&Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true},
&Variable{Name: "exec_path", Values: []string{"@{lib}/DiscoverNotifier"}, Define: true},
&Variable{Name: "exec_path", Values: []string{"@{lib}/@{multiarch}/{,libexec/}DiscoverNotifier"}, Define: false},
},
want: &AppArmorProfileFile{
Preamble: Rules{
&Variable{Name: "lib", Values: []string{"/{usr/,}lib"}, Define: true},
&Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true},
&Variable{
Name: "exec_path", Define: true,
Values: []string{
"/{usr/,}lib/DiscoverNotifier",
"/{usr/,}lib/*-linux-gnu*/{,libexec/}DiscoverNotifier",
},
},
},
},
wantErr: false,
},
{
name: "attachment/firefox",
preamble: Rules{
&Variable{Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}, Define: true},
&Variable{Name: "firefox_lib_dirs", Values: []string{"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}, Define: true},
&Variable{Name: "exec_path", Values: []string{"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}, Define: true},
},
attachements: []string{"@{exec_path}"},
want: &AppArmorProfileFile{
Preamble: Rules{
&Variable{Name: "firefox_name", Values: []string{"firefox{,-esr,-bin}"}, Define: true},
&Variable{
Name: "firefox_lib_dirs", Define: true,
Values: []string{
"/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}",
"/opt/firefox{,-esr,-bin}",
},
},
&Variable{
Name: "exec_path", Define: true,
Values: []string{
"/{usr/,}bin/firefox{,-esr,-bin}",
"/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
"/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
},
},
},
Profiles: []*Profile{
{Header: Header{
Attachments: []string{
"/{usr/,}bin/firefox{,-esr,-bin}",
"/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
"/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
},
}},
},
},
wantErr: false,
},
{
name: "attachment/chromium",
preamble: Rules{
&Variable{Name: "name", Values: []string{"chromium"}, Define: true},
&Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{name}"}, Define: true},
&Variable{Name: "path", Values: []string{"@{lib_dirs}/@{name}"}, Define: true},
},
attachements: []string{"@{path}/pass"},
want: &AppArmorProfileFile{
Preamble: Rules{
&Variable{Name: "name", Values: []string{"chromium"}, Define: true},
&Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/chromium"}, Define: true},
&Variable{Name: "path", Values: []string{"/{usr/,}lib/chromium/chromium"}, Define: true},
},
Profiles: []*Profile{
{Header: Header{
Attachments: []string{"/{usr/,}lib/chromium/chromium/pass"},
}},
},
},
wantErr: false,
},
{
name: "attachment/geoclue",
preamble: Rules{
&Variable{Name: "libexec", Values: []string{"/{usr/,}libexec"}, Define: true},
&Variable{Name: "exec_path", Values: []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}, Define: true},
},
attachements: []string{"@{exec_path}"},
want: &AppArmorProfileFile{
Preamble: Rules{
&Variable{Name: "libexec", Values: []string{"/{usr/,}libexec"}, Define: true},
&Variable{
Name: "exec_path", Define: true,
Values: []string{
"/{usr/,}libexec/geoclue",
"/{usr/,}libexec/geoclue-2.0/demos/agent",
},
},
},
Profiles: []*Profile{
{Header: Header{
Attachments: []string{
"/{usr/,}libexec/geoclue",
"/{usr/,}libexec/geoclue-2.0/demos/agent",
},
}},
},
},
wantErr: false,
},
{
name: "attachment/opera",
preamble: Rules{
&Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true},
&Variable{Name: "name", Values: []string{"opera{,-beta,-developer}"}, Define: true},
&Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/@{multiarch}/@{name}"}, Define: true},
&Variable{Name: "exec_path", Values: []string{"@{lib_dirs}/@{name}"}, Define: true},
},
attachements: []string{"@{exec_path}"},
want: &AppArmorProfileFile{
Preamble: Rules{
&Variable{Name: "multiarch", Values: []string{"*-linux-gnu*"}, Define: true},
&Variable{Name: "name", Values: []string{"opera{,-beta,-developer}"}, Define: true},
&Variable{Name: "lib_dirs", Values: []string{"/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}"}, Define: true},
&Variable{Name: "exec_path", Values: []string{"/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}"}, Define: true},
},
Profiles: []*Profile{
{Header: Header{
Attachments: []string{
"/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}",
},
}},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := &AppArmorProfileFile{Preamble: tt.preamble}
if tt.attachements != nil {
got.Profiles = append(got.Profiles, &Profile{Header: Header{Attachments: tt.attachements}})
}
if err := got.Resolve(); (err != nil) != tt.wantErr {
t.Errorf("AppArmorProfileFile.Resolve() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfile.Resolve() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -4,24 +4,69 @@
package aa package aa
import "fmt"
const (
RLIMIT Kind = "rlimit"
)
func init() {
requirements[RLIMIT] = requirement{
"keys": {
"cpu", "fsize", "data", "stack", "core", "rss", "nofile", "ofile",
"as", "nproc", "memlock", "locks", "sigpending", "msgqueue", "nice",
"rtprio", "rttime",
},
}
}
type Rlimit struct { type Rlimit struct {
RuleBase
Key string Key string
Op string Op string
Value string Value string
} }
func newRlimitFromLog(log map[string]string) Rule {
return &Rlimit{
RuleBase: newRuleFromLog(log),
Key: log["key"],
Op: log["op"],
Value: log["value"],
}
}
func (r *Rlimit) Validate() error {
if err := validateValues(r.Kind(), "keys", []string{r.Key}); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Rlimit) Less(other any) bool { func (r *Rlimit) Less(other any) bool {
o, _ := other.(*Rlimit) o, _ := other.(*Rlimit)
if r.Key == o.Key { if r.Key != o.Key {
if r.Op == o.Op { return r.Key < o.Key
return r.Value < o.Value
} }
if r.Op != o.Op {
return r.Op < o.Op return r.Op < o.Op
} }
return r.Key < o.Key return r.Value < o.Value
} }
func (r *Rlimit) Equals(other any) bool { func (r *Rlimit) Equals(other any) bool {
o, _ := other.(*Rlimit) o, _ := other.(*Rlimit)
return r.Key == o.Key && r.Op == o.Op && r.Value == o.Value return r.Key == o.Key && r.Op == o.Op && r.Value == o.Value
} }
func (r *Rlimit) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Rlimit) Constraint() constraint {
return blockKind
}
func (r *Rlimit) Kind() Kind {
return RLIMIT
}

View file

@ -5,133 +5,233 @@
package aa package aa
import ( import (
"strings" "slices"
) )
type Rule struct { type requirement map[string][]string
Comment string
NoNewPrivs bool type constraint uint
FileInherit bool
const (
anyKind constraint = iota // The rule can be found in either preamble or profile
preambleKind // The rule can only be found in the preamble
blockKind // The rule can only be found in a profile
)
// Kind represents an AppArmor rule kind.
type Kind string
func (k Kind) String() string {
return string(k)
} }
func (r *Rule) Less(other any) bool { func (k Kind) Tok() string {
return false if t, ok := tok[k]; ok {
return t
}
return string(k)
} }
func (r *Rule) Equals(other any) bool { // Rule generic interface for all AppArmor rules
return false type Rule interface {
Validate() error
Less(other any) bool
Equals(other any) bool
String() string
Constraint() constraint
Kind() Kind
} }
// Qualifier to apply extra settings to a rule type Rules []Rule
type Qualifier struct {
Audit bool func (r Rules) Validate() error {
AccessType string for _, rule := range r {
Owner bool if rule == nil {
NoNewPrivs bool continue
FileInherit bool }
Optional bool if err := rule.Validate(); err != nil {
Comment string return err
Prefix string }
Padding string }
return nil
} }
func NewQualifierFromLog(log map[string]string) Qualifier { func (r Rules) String() string {
owner := false return renderTemplate("rules", r)
fsuid, hasFsUID := log["fsuid"]
ouid, hasOuUID := log["ouid"]
isDbus := strings.Contains(log["operation"], "dbus")
if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus {
owner = true
} }
audit := false // Index returns the index of the first occurrence of rule rin r, or -1 if not present.
if log["apparmor"] == "AUDIT" { func (r Rules) Index(item Rule) int {
audit = true for idx, rule := range r {
if rule == nil {
continue
}
if rule.Kind() == item.Kind() && rule.Equals(item) {
return idx
}
}
return -1
} }
fileInherit := false // Replace replaces the elements r[i] by the given rules, and returns the
if log["operation"] == "file_inherit" { // modified slice.
fileInherit = true func (r Rules) Replace(i int, rules ...Rule) Rules {
return append(r[:i], append(rules, r[i+1:]...)...)
} }
noNewPrivs := false // Insert inserts the rules into r at index i, returning the modified slice.
optional := false func (r Rules) Insert(i int, rules ...Rule) Rules {
msg := "" return append(r[:i], append(rules, r[i:]...)...)
switch log["error"] {
case "-1":
if strings.Contains(log["info"], "optional:") {
optional = true
msg = strings.Replace(log["info"], "optional: ", "", 1)
} else {
noNewPrivs = true
}
case "-13":
ignoreProfileInfo := []string{"namespace", "disconnected path"}
for _, info := range ignoreProfileInfo {
if strings.Contains(log["info"], info) {
break
}
}
msg = log["info"]
default:
} }
return Qualifier{ // Delete removes the elements r[i] from r, returning the modified slice.
Audit: audit, func (r Rules) Delete(i int) Rules {
Owner: owner, return append(r[:i], r[i+1:]...)
NoNewPrivs: noNewPrivs,
FileInherit: fileInherit,
Optional: optional,
Comment: msg,
}
} }
func (r Qualifier) Less(other Qualifier) bool { func (r Rules) DeleteKind(kind Kind) Rules {
if r.Owner == other.Owner { res := make(Rules, 0)
if r.Audit == other.Audit { for _, rule := range r {
return r.AccessType < other.AccessType if rule == nil {
continue
} }
return r.Audit if rule.Kind() != kind {
res = append(res, rule)
} }
return other.Owner }
return res
} }
func (r Qualifier) Equals(other Qualifier) bool { func (r Rules) Filter(filter Kind) Rules {
return r.Audit == other.Audit && r.AccessType == other.AccessType && res := make(Rules, 0)
r.Owner == other.Owner && r.NoNewPrivs == other.NoNewPrivs && for _, rule := range r {
r.FileInherit == other.FileInherit if rule == nil {
continue
}
if rule.Kind() != filter {
res = append(res, rule)
}
}
return res
} }
// Preamble specific rules func (r Rules) GetVariables() []*Variable {
res := make([]*Variable, 0)
type Abi struct { for _, rule := range r {
Path string switch rule := rule.(type) {
IsMagic bool case *Variable:
res = append(res, rule)
}
}
return res
} }
func (r Abi) Less(other Abi) bool { func (r Rules) GetIncludes() []*Include {
if r.Path == other.Path { res := make([]*Include, 0)
return r.IsMagic == other.IsMagic for _, rule := range r {
switch rule := rule.(type) {
case *Include:
res = append(res, rule)
} }
return r.Path < other.Path }
return res
} }
func (r Abi) Equals(other Abi) bool { // Merge merge similar rules together.
return r.Path == other.Path && r.IsMagic == other.IsMagic // Steps:
// - Remove identical rules
// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw'
//
// Note: logs.regCleanLogs helps a lot to do a first cleaning
func (r Rules) Merge() Rules {
for i := 0; i < len(r); i++ {
for j := i + 1; j < len(r); j++ {
typeOfI := r[i].Kind()
typeOfJ := r[j].Kind()
if typeOfI != typeOfJ {
continue
} }
type Alias struct { // If rules are identical, merge them
Path string if r[i].Equals(r[j]) {
RewrittenPath string r = r.Delete(j)
j--
continue
} }
func (r Alias) Less(other Alias) bool { // File rule
if r.Path == other.Path { if typeOfI == FILE && typeOfJ == FILE {
return r.RewrittenPath < other.RewrittenPath // Merge access
fileI := r[i].(*File)
fileJ := r[j].(*File)
if fileI.Path == fileJ.Path {
fileI.Access = append(fileI.Access, fileJ.Access...)
slices.SortFunc(fileI.Access, cmpFileAccess)
fileI.Access = slices.Compact(fileI.Access)
r = r.Delete(j)
j--
} }
return r.Path < other.Path }
}
}
return r
} }
func (r Alias) Equals(other Alias) bool { // Sort the rules according to the guidelines:
return r.Path == other.Path && r.RewrittenPath == other.RewrittenPath // https://apparmor.pujol.io/development/guidelines/#guidelines
func (r Rules) Sort() Rules {
slices.SortFunc(r, func(a, b Rule) int {
kindOfA := a.Kind()
kindOfB := b.Kind()
if kindOfA != kindOfB {
if kindOfA == INCLUDE && a.(*Include).IfExists {
kindOfA = "include_if_exists"
}
if kindOfB == INCLUDE && b.(*Include).IfExists {
kindOfB = "include_if_exists"
}
return ruleWeights[kindOfA] - ruleWeights[kindOfB]
}
if a.Equals(b) {
return 0
}
if a.Less(b) {
return -1
}
return 1
})
return r
}
// Format the rules for better readability before printing it.
// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block
func (r Rules) Format() Rules {
const prefixOwner = " "
hasOwnerRule := false
for i := len(r) - 1; i > 0; i-- {
j := i - 1
typeOfI := r[i].Kind()
typeOfJ := r[j].Kind()
// File rule
if typeOfI == FILE && typeOfJ == FILE {
letterI := getLetterIn(fileAlphabet, r[i].(*File).Path)
letterJ := getLetterIn(fileAlphabet, r[j].(*File).Path)
// Add prefix before rule path to align with other rule
if r[i].(*File).Owner {
hasOwnerRule = true
} else if hasOwnerRule {
r[i].(*File).Prefix = prefixOwner
}
if letterI != letterJ {
// Add a new empty line between Files rule of different type
hasOwnerRule = false
r = r.Insert(i, nil)
}
}
}
return r
} }

View file

@ -9,361 +9,455 @@ import (
"testing" "testing"
) )
func TestRule_FromLog(t *testing.T) { func TestRules_FromLog(t *testing.T) {
tests := []struct { for _, tt := range testRule {
name string if tt.fromLog == nil {
fromLog func(map[string]string) ApparmorRule continue
log map[string]string
want ApparmorRule
}{
{
name: "capbability",
fromLog: CapabilityFromLog,
log: capability1Log,
want: capability1,
},
{
name: "network",
fromLog: NetworkFromLog,
log: network1Log,
want: network1,
},
{
name: "mount",
fromLog: MountFromLog,
log: mount1Log,
want: mount1,
},
{
name: "umount",
fromLog: UmountFromLog,
log: umount1Log,
want: umount1,
},
{
name: "pivotroot",
fromLog: PivotRootFromLog,
log: pivotroot1Log,
want: pivotroot1,
},
{
name: "changeprofile",
fromLog: ChangeProfileFromLog,
log: changeprofile1Log,
want: changeprofile1,
},
{
name: "signal",
fromLog: SignalFromLog,
log: signal1Log,
want: signal1,
},
{
name: "ptrace/xdg-document-portal",
fromLog: PtraceFromLog,
log: ptrace1Log,
want: ptrace1,
},
{
name: "ptrace/snap-update-ns.firefox",
fromLog: PtraceFromLog,
log: ptrace2Log,
want: ptrace2,
},
{
name: "unix",
fromLog: UnixFromLog,
log: unix1Log,
want: unix1,
},
{
name: "dbus",
fromLog: DbusFromLog,
log: dbus1Log,
want: dbus1,
},
{
name: "file",
fromLog: FileFromLog,
log: file1Log,
want: file1,
},
} }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.want) { if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.rule) {
t.Errorf("RuleFromLog() = %v, want %v", got, tt.want) t.Errorf("RuleFromLog() = %v, want %v", got, tt.rule)
} }
}) })
} }
} }
func TestRule_Less(t *testing.T) { func TestRules_Validate(t *testing.T) {
tests := []struct { for _, tt := range testRule {
t.Run(tt.name, func(t *testing.T) {
if err := tt.rule.Validate(); (err != nil) != tt.wValidErr {
t.Errorf("Rules.Validate() error = %v, wantErr %v", err, tt.wValidErr)
}
})
}
}
func TestRules_Less(t *testing.T) {
for _, tt := range testRule {
if tt.oLess == nil {
continue
}
t.Run(tt.name, func(t *testing.T) {
if got := tt.rule.Less(tt.oLess); got != tt.wLessErr {
t.Errorf("Rule.Less() = %v, want %v", got, tt.wLessErr)
}
})
}
}
func TestRules_Equals(t *testing.T) {
for _, tt := range testRule {
if tt.oEqual == nil {
continue
}
t.Run(tt.name, func(t *testing.T) {
r := tt.rule
if got := r.Equals(tt.oEqual); got != tt.wEqualErr {
t.Errorf("Rule.Equals() = %v, want %v", got, tt.wEqualErr)
}
})
}
}
func TestRules_String(t *testing.T) {
for _, tt := range testRule {
t.Run(tt.name, func(t *testing.T) {
if got := tt.rule.String(); got != tt.wString {
t.Errorf("Rule.String() = %v, want %v", got, tt.wString)
}
})
}
}
var (
// Test cases for the Rule interface
testRule = []struct {
name string name string
rule ApparmorRule fromLog func(map[string]string) Rule
other ApparmorRule log map[string]string
want bool rule Rule
wValidErr bool
oLess Rule
wLessErr bool
oEqual Rule
wEqualErr bool
wString string
}{ }{
{
name: "comment",
rule: comment1,
oLess: comment2,
wLessErr: false,
oEqual: comment2,
wEqualErr: false,
wString: "#comment",
},
{
name: "abi",
rule: abi1,
oLess: abi2,
wLessErr: false,
oEqual: abi1,
wEqualErr: true,
wString: "abi <abi/4.0>,",
},
{
name: "alias",
rule: alias1,
oLess: alias2,
wLessErr: true,
oEqual: alias2,
wEqualErr: false,
wString: "alias /mnt/usr -> /usr,",
},
{ {
name: "include1", name: "include1",
rule: include1, rule: include1,
other: includeLocal1, oLess: includeLocal1,
want: true, wLessErr: false,
oEqual: includeLocal1,
wEqualErr: false,
wString: "include <abstraction/base>",
}, },
{ {
name: "include2", name: "include2",
rule: include1, rule: include1,
other: include2, oLess: include2,
want: true, wLessErr: false,
wString: "include <abstraction/base>",
}, },
{ {
name: "include3", name: "include-local",
rule: include1, rule: includeLocal1,
other: include3, oLess: include1,
want: false, wLessErr: true,
wString: "include if exists <local/foo>",
},
{
name: "include/abs",
rule: &Include{Path: "/usr/share/apparmor.d/", IsMagic: false},
wString: `include "/usr/share/apparmor.d/"`,
},
{
name: "variable",
rule: variable1,
oLess: variable2,
wLessErr: true,
oEqual: variable1,
wEqualErr: true,
wString: "@{bin} = /{,usr/}{,s}bin",
},
{
name: "all",
rule: all1,
oLess: all2,
wLessErr: false,
oEqual: all2,
wEqualErr: false,
wString: "all,",
}, },
{ {
name: "rlimit", name: "rlimit",
rule: rlimit1, rule: rlimit1,
other: rlimit2, oLess: rlimit2,
want: false, wLessErr: false,
oEqual: rlimit1,
wEqualErr: true,
wString: "set rlimit nproc <= 200,",
}, },
{ {
name: "rlimit2", name: "rlimit2",
rule: rlimit2, rule: rlimit2,
other: rlimit2, oLess: rlimit2,
want: false, wLessErr: false,
wString: "set rlimit cpu <= 2,",
}, },
{ {
name: "rlimit3", name: "rlimit3",
rule: rlimit1, rule: rlimit3,
other: rlimit3, oLess: rlimit1,
want: false, wLessErr: true,
wString: "set rlimit nproc < 2,",
}, },
{ {
name: "capability", name: "userns",
rule: userns1,
oLess: userns2,
wLessErr: true,
oEqual: userns1,
wEqualErr: true,
wString: "userns,",
},
{
name: "capbability",
fromLog: newCapabilityFromLog,
log: capability1Log,
rule: capability1, rule: capability1,
other: capability2, oLess: capability2,
want: true, wLessErr: true,
oEqual: capability1,
wEqualErr: true,
wString: "capability net_admin,",
},
{
name: "capability/multi",
rule: &Capability{Names: []string{"dac_override", "dac_read_search"}},
wString: "capability dac_override dac_read_search,",
},
{
name: "capability/all",
rule: &Capability{},
wString: "capability,",
}, },
{ {
name: "network", name: "network",
fromLog: newNetworkFromLog,
log: network1Log,
rule: network1, rule: network1,
other: network2, wValidErr: true,
want: false, oLess: network2,
wLessErr: false,
oEqual: network1,
wEqualErr: true,
wString: "network netlink raw,",
}, },
{ {
name: "mount", name: "mount",
fromLog: newMountFromLog,
log: mount1Log,
rule: mount1, rule: mount1,
other: mount2, oEqual: mount2,
want: false, wEqualErr: false,
wString: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check",
},
{
name: "remount",
rule: remount1,
oLess: remount2,
wLessErr: true,
oEqual: remount1,
wEqualErr: true,
wString: "remount /,",
}, },
{ {
name: "umount", name: "umount",
fromLog: newUmountFromLog,
log: umount1Log,
rule: umount1, rule: umount1,
other: umount2, oLess: umount2,
want: true, wLessErr: true,
oEqual: umount1,
wEqualErr: true,
wString: "umount /,",
}, },
{ {
name: "pivot_root1", name: "pivot_root1",
rule: pivotroot2, fromLog: newPivotRootFromLog,
other: pivotroot1, log: pivotroot1Log,
want: true, rule: pivotroot1,
oLess: pivotroot2,
wLessErr: false,
oEqual: pivotroot2,
wEqualErr: false,
wString: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,",
}, },
{ {
name: "pivot_root2", name: "pivot_root2",
rule: pivotroot1, rule: pivotroot1,
other: pivotroot3, oLess: pivotroot3,
want: false, wLessErr: false,
wString: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,",
}, },
{ {
name: "change_profile1", name: "change_profile1",
fromLog: newChangeProfileFromLog,
log: changeprofile1Log,
rule: changeprofile1, rule: changeprofile1,
other: changeprofile2, oLess: changeprofile2,
want: false, wLessErr: false,
wString: "change_profile -> systemd-user,",
}, },
{ {
name: "change_profile2", name: "change_profile2",
rule: changeprofile1, rule: changeprofile2,
other: changeprofile3, oLess: changeprofile3,
want: true, wLessErr: true,
oEqual: changeprofile1,
wEqualErr: false,
wString: "change_profile -> brwap,",
},
{
name: "mqueue",
rule: mqueue1,
oLess: mqueue2,
wLessErr: true,
oEqual: mqueue1,
wEqualErr: true,
wString: "mqueue r type=posix /,",
},
{
name: "iouring",
rule: iouring1,
oLess: iouring2,
wLessErr: false,
oEqual: iouring2,
wEqualErr: false,
wString: "io_uring sqpoll label=foo,",
}, },
{ {
name: "signal", name: "signal",
fromLog: newSignalFromLog,
log: signal1Log,
rule: signal1, rule: signal1,
other: signal2, oLess: signal2,
want: true, wLessErr: false,
oEqual: signal1,
wEqualErr: true,
wString: "signal receive set=kill peer=firefox//&firejail-default,",
}, },
{ {
name: "ptrace/less", name: "ptrace/xdg-document-portal",
fromLog: newPtraceFromLog,
log: ptrace1Log,
rule: ptrace1, rule: ptrace1,
other: ptrace2, oLess: ptrace2,
want: true, wLessErr: false,
oEqual: ptrace1,
wEqualErr: true,
wString: "ptrace read peer=nautilus,",
}, },
{ {
name: "ptrace/more", name: "ptrace/snap-update-ns.firefox",
fromLog: newPtraceFromLog,
log: ptrace2Log,
rule: ptrace2, rule: ptrace2,
other: ptrace1, oLess: ptrace1,
want: false, wLessErr: false,
oEqual: ptrace1,
wEqualErr: false,
wString: "ptrace readby peer=systemd-journald,",
}, },
{ {
name: "unix", name: "unix",
fromLog: newUnixFromLog,
log: unix1Log,
rule: unix1, rule: unix1,
other: unix1, oLess: unix1,
want: false, wLessErr: false,
oEqual: unix1,
wEqualErr: true,
wString: "unix (send receive) type=stream protocol=0 addr=none peer=(label=dbus-daemon, addr=@/tmp/dbus-AaKMpxzC4k),",
}, },
{ {
name: "dbus", name: "dbus",
fromLog: newDbusFromLog,
log: dbus1Log,
rule: dbus1, rule: dbus1,
other: dbus1, oLess: dbus1,
want: false, wLessErr: false,
oEqual: dbus2,
wEqualErr: false,
wString: "dbus receive bus=session path=/org/gtk/vfs/metadata\n interface=org.gtk.vfs.Metadata\n member=Remove\n peer=(name=:1.15, label=tracker-extract),",
}, },
{ {
name: "dbus2", name: "dbus2",
rule: dbus2, rule: dbus2,
other: dbus3, oLess: dbus3,
want: false, wLessErr: false,
wString: "dbus bind bus=session name=org.gnome.evolution.dataserver.Sources5,",
},
{
name: "dbus/bind",
rule: &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"},
wString: `dbus bind bus=session name=org.gnome.*,`,
},
{
name: "dbus/full",
rule: &Dbus{Bus: "accessibility"},
wString: `dbus bus=accessibility,`,
}, },
{ {
name: "file", name: "file",
fromLog: newFileFromLog,
log: file1Log,
rule: file1, rule: file1,
other: file2, oLess: file2,
want: true, wLessErr: true,
oEqual: file2,
wEqualErr: false,
wString: "/usr/share/poppler/cMap/Identity-H r,",
}, },
{ {
name: "file/empty", name: "file/empty",
rule: &File{}, rule: &File{},
other: &File{}, oLess: &File{},
want: false, wLessErr: false,
wString: " ,",
}, },
{ {
name: "file/equal", name: "file/equal",
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, rule: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
want: false, wLessErr: false,
wString: "/usr/share/poppler/cMap/Identity-H ,",
}, },
{ {
name: "file/owner", name: "file/owner",
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Qualifier: Qualifier{Owner: true}}, rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Owner: true},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
want: false, wLessErr: true,
wString: "owner /usr/share/poppler/cMap/Identity-H ,",
}, },
{ {
name: "file/access", name: "file/access",
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"}, rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "w"}, oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"w"}},
want: true, wLessErr: false,
wString: "/usr/share/poppler/cMap/Identity-H r,",
}, },
{ {
name: "file/close", name: "file/close",
rule: &File{Path: "/usr/share/poppler/cMap/"}, rule: &File{Path: "/usr/share/poppler/cMap/"},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"}, oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
want: true, wLessErr: true,
wString: "/usr/share/poppler/cMap/ ,",
},
{
name: "link",
fromLog: newLinkFromLog,
log: link1Log,
rule: link1,
oLess: link2,
wLessErr: true,
oEqual: link3,
wEqualErr: false,
wString: "link /tmp/mkinitcpio.QDWtza/early@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst -> /tmp/mkinitcpio.QDWtza/root@{lib}/firmware/i915/dg1_dmc_ver2_02.bin.zst,",
},
{
name: "link",
fromLog: newFileFromLog,
log: link3Log,
rule: link3,
wString: "owner link @{user_config_dirs}/kiorc -> @{user_config_dirs}/#3954,",
},
{
name: "profile",
rule: profile1,
oLess: profile2,
wLessErr: true,
oEqual: profile1,
wEqualErr: true,
wString: "profile sudo {\n}",
},
{
name: "hat",
rule: hat1,
oLess: hat2,
wLessErr: false,
oEqual: hat1,
wEqualErr: true,
wString: "hat user {\n}",
}, },
} }
for _, tt := range tests { )
t.Run(tt.name, func(t *testing.T) {
r := tt.rule
if got := r.Less(tt.other); got != tt.want {
t.Errorf("Rule.Less() = %v, want %v", got, tt.want)
}
})
}
}
func TestRule_Equals(t *testing.T) {
tests := []struct {
name string
rule ApparmorRule
other ApparmorRule
want bool
}{
{
name: "include1",
rule: include1,
other: includeLocal1,
want: false,
},
{
name: "rlimit",
rule: rlimit1,
other: rlimit1,
want: true,
},
{
name: "capability/equal",
rule: capability1,
other: capability1,
want: true,
},
{
name: "network/equal",
rule: network1,
other: network1,
want: true,
},
{
name: "mount",
rule: mount1,
other: mount1,
want: true,
},
{
name: "pivot_root",
rule: pivotroot1,
other: pivotroot2,
want: false,
},
{
name: "change_profile",
rule: changeprofile1,
other: changeprofile2,
want: false,
},
{
name: "signal1/equal",
rule: signal1,
other: signal1,
want: true,
},
{
name: "ptrace/equal",
rule: ptrace1,
other: ptrace1,
want: true,
},
{
name: "ptrace/not_equal",
rule: ptrace1,
other: ptrace2,
want: false,
},
{
name: "unix",
rule: unix1,
other: unix1,
want: true,
},
{
name: "dbus",
rule: dbus1,
other: dbus2,
want: false,
},
{
name: "file",
rule: file2,
other: file2,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := tt.rule
if got := r.Equals(tt.other); got != tt.want {
t.Errorf("Rule.Equals() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -4,38 +4,90 @@
package aa package aa
import (
"fmt"
"slices"
)
const SIGNAL Kind = "signal"
func init() {
requirements[SIGNAL] = requirement{
"access": {
"r", "w", "rw", "read", "write", "send", "receive",
},
"set": {
"hup", "int", "quit", "ill", "trap", "abrt", "bus", "fpe",
"kill", "usr1", "segv", "usr2", "pipe", "alrm", "term", "stkflt",
"chld", "cont", "stop", "stp", "ttin", "ttou", "urg", "xcpu",
"xfsz", "vtalrm", "prof", "winch", "io", "pwr", "sys", "emt",
"exists", "rtmin+0", "rtmin+1", "rtmin+2", "rtmin+3", "rtmin+4",
"rtmin+5", "rtmin+6", "rtmin+7", "rtmin+8", "rtmin+9", "rtmin+10",
"rtmin+11", "rtmin+12", "rtmin+13", "rtmin+14", "rtmin+15",
"rtmin+16", "rtmin+17", "rtmin+18", "rtmin+19", "rtmin+20",
"rtmin+21", "rtmin+22", "rtmin+23", "rtmin+24", "rtmin+25",
"rtmin+26", "rtmin+27", "rtmin+28", "rtmin+29", "rtmin+30",
"rtmin+31", "rtmin+32",
},
}
}
type Signal struct { type Signal struct {
RuleBase
Qualifier Qualifier
Access string Access []string
Set string Set []string
Peer string Peer string
} }
func SignalFromLog(log map[string]string) ApparmorRule { func newSignalFromLog(log map[string]string) Rule {
return &Signal{ return &Signal{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Access: toAccess(log["requested_mask"]), Qualifier: newQualifierFromLog(log),
Set: log["signal"], Access: Must(toAccess(SIGNAL, log["requested_mask"])),
Set: []string{log["signal"]},
Peer: log["peer"], Peer: log["peer"],
} }
} }
func (r *Signal) Validate() error {
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
if err := validateValues(r.Kind(), "set", r.Set); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Signal) Less(other any) bool { func (r *Signal) Less(other any) bool {
o, _ := other.(*Signal) o, _ := other.(*Signal)
if r.Qualifier.Equals(o.Qualifier) { if len(r.Access) != len(o.Access) {
if r.Access == o.Access { return len(r.Access) < len(o.Access)
if r.Set == o.Set { }
if len(r.Set) != len(o.Set) {
return len(r.Set) < len(o.Set)
}
if r.Peer != o.Peer {
return r.Peer < o.Peer return r.Peer < o.Peer
} }
return r.Set < o.Set
}
return r.Access < o.Access
}
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
func (r *Signal) Equals(other any) bool { func (r *Signal) Equals(other any) bool {
o, _ := other.(*Signal) o, _ := other.(*Signal)
return r.Access == o.Access && r.Set == o.Set && return slices.Equal(r.Access, o.Access) && slices.Equal(r.Set, o.Set) &&
r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier) r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier)
} }
func (r *Signal) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Signal) Constraint() constraint {
return blockKind
}
func (r *Signal) Kind() Kind {
return SIGNAL
}

View file

@ -6,67 +6,90 @@ package aa
import ( import (
"embed" "embed"
"fmt"
"reflect" "reflect"
"strings" "strings"
"text/template" "text/template"
) )
// Default indentation for apparmor profile (2 spaces)
const indentation = " "
var ( var (
// Default indentation for apparmor profile (2 spaces)
Indentation = " "
// The current indentation level
IndentationLevel = 0
//go:embed templates/*.j2 //go:embed templates/*.j2
//go:embed templates/rule/*.j2
tmplFiles embed.FS tmplFiles embed.FS
// The functions available in the template // The functions available in the template
tmplFunctionMap = template.FuncMap{ tmplFunctionMap = template.FuncMap{
"typeof": typeOf, "kindof": kindOf,
"join": join, "join": join,
"cjoin": cjoin,
"indent": indent, "indent": indent,
"overindent": indentDbus, "overindent": indentDbus,
"setindent": setindent,
} }
// The apparmor profile template // The apparmor templates
tmplAppArmorProfile = generateTemplate() tmpl = generateTemplates([]Kind{
// Global templates
"apparmor",
PROFILE,
HAT,
"rules",
// Preamble templates
ABI,
ALIAS,
INCLUDE,
VARIABLE,
COMMENT,
// Rules templates
ALL, RLIMIT, USERNS, CAPABILITY, NETWORK,
MOUNT, REMOUNT, UMOUNT, PIVOTROOT, CHANGEPROFILE,
MQUEUE, IOURING, UNIX, PTRACE, SIGNAL, DBUS,
FILE, LINK,
})
// convert apparmor requested mask to apparmor access mode // convert apparmor requested mask to apparmor access mode
requestedMaskToAccess = map[string]string{ maskToAccess = map[string]string{
"a": "w", "a": "w",
"ac": "w",
"c": "w", "c": "w",
"d": "w", "d": "w",
"m": "rm",
"ra": "rw",
"wc": "w", "wc": "w",
"wd": "w", "x": "ix",
"wr": "rw",
"wrc": "rw",
"wrd": "rw",
"x": "rix",
} }
// The order the apparmor rules should be sorted // The order the apparmor rules should be sorted
ruleAlphabet = []string{ ruleAlphabet = []Kind{
"include", INCLUDE,
"rlimit", ALL,
"capability", RLIMIT,
"network", USERNS,
"mount", CAPABILITY,
"remount", NETWORK,
"umount", MOUNT,
"pivotroot", REMOUNT,
"changeprofile", UMOUNT,
"mqueue", PIVOTROOT,
"signal", CHANGEPROFILE,
"ptrace", MQUEUE,
"unix", IOURING,
"userns", SIGNAL,
"iouring", PTRACE,
"dbus", UNIX,
"file", DBUS,
FILE,
LINK,
PROFILE,
HAT,
"include_if_exists", "include_if_exists",
} }
ruleWeights = map[string]int{} ruleWeights = generateWeights(ruleAlphabet)
// The order the apparmor file rules should be sorted // The order the apparmor file rules should be sorted
fileAlphabet = []string{ fileAlphabet = []string{
@ -91,23 +114,65 @@ var (
"@{PROC}", // 10. Proc files "@{PROC}", // 10. Proc files
"/dev", // 11. Dev files "/dev", // 11. Dev files
"deny", // 12. Deny rules "deny", // 12. Deny rules
"profile", // 13. Subprofiles
} }
fileWeights = map[string]int{} fileWeights = generateWeights(fileAlphabet)
// The order the rule values (access, type, domains, etc) should be sorted
requirements = map[Kind]requirement{}
requirementsWeights map[Kind]map[string]map[string]int
) )
func generateTemplate() *template.Template { func init() {
res := template.New("profile.j2").Funcs(tmplFunctionMap) requirementsWeights = generateRequirementsWeights(requirements)
res = template.Must(res.ParseFS(tmplFiles, "templates/*.j2")) }
func generateTemplates(names []Kind) map[Kind]*template.Template {
res := make(map[Kind]*template.Template, len(names))
base := template.New("").Funcs(tmplFunctionMap)
base = template.Must(base.ParseFS(tmplFiles,
"templates/*.j2", "templates/rule/*.j2",
))
for _, name := range names {
t := template.Must(base.Clone())
t = template.Must(t.Parse(
fmt.Sprintf(`{{- template "%s" . -}}`, name),
))
res[name] = t
}
return res return res
} }
func init() { func renderTemplate(name Kind, data any) string {
for i, r := range fileAlphabet { var res strings.Builder
fileWeights[r] = i template, ok := tmpl[name]
if !ok {
panic("template '" + name.String() + "' not found")
} }
for i, r := range ruleAlphabet { err := template.Execute(&res, data)
ruleWeights[r] = i if err != nil {
panic(err)
} }
return res.String()
}
func generateWeights[T Kind | string](alphabet []T) map[T]int {
res := make(map[T]int, len(alphabet))
for i, r := range alphabet {
res[r] = i
}
return res
}
func generateRequirementsWeights(requirements map[Kind]requirement) map[Kind]map[string]map[string]int {
res := make(map[Kind]map[string]map[string]int, len(requirements))
for rule, req := range requirements {
res[rule] = make(map[string]map[string]int, len(req))
for key, values := range req {
res[rule][key] = generateWeights(values)
}
}
return res
} }
func join(i any) string { func join(i any) string {
@ -125,20 +190,48 @@ func join(i any) string {
} }
} }
func typeOf(i any) string { func cjoin(i any) string {
return strings.TrimPrefix(reflect.TypeOf(i).String(), "*aa.") switch reflect.TypeOf(i).Kind() {
case reflect.Slice:
s := i.([]string)
if len(s) == 1 {
return s[0]
}
return "(" + strings.Join(s, " ") + ")"
case reflect.Map:
res := []string{}
for k, v := range i.(map[string]string) {
res = append(res, k+"="+v)
}
return "(" + strings.Join(res, " ") + ")"
default:
return i.(string)
}
} }
func typeToValue(i reflect.Type) string { func kindOf(i any) string {
return strings.ToLower(strings.TrimPrefix(i.String(), "*aa.")) if i == nil {
return ""
}
return i.(Rule).Kind().String()
}
func setindent(i string) string {
switch i {
case "++":
IndentationLevel++
case "--":
IndentationLevel--
}
return ""
} }
func indent(s string) string { func indent(s string) string {
return indentation + s return strings.Repeat(Indentation, IndentationLevel) + s
} }
func indentDbus(s string) string { func indentDbus(s string) string {
return indentation + " " + s return strings.Join([]string{Indentation, s}, " ")
} }
func getLetterIn(alphabet []string, in string) string { func getLetterIn(alphabet []string, in string) string {
@ -149,10 +242,3 @@ func getLetterIn(alphabet []string, in string) string {
} }
return "" return ""
} }
func toAccess(mask string) string {
if requestedMaskToAccess[mask] != "" {
return requestedMaskToAccess[mask]
}
return mask
}

View file

@ -0,0 +1,14 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "apparmor" -}}
{{- template "rules" .Preamble -}}
{{- range .Profiles -}}
{{- template "profile" . -}}
{{- "\n" -}}
{{- end -}}
{{- end -}}

View file

@ -1,17 +0,0 @@
{{- define "comment" -}}
{{- if or .FileInherit .NoNewPrivs .Optional .Comment -}}
{{- " #" -}}
{{- end -}}
{{- if .FileInherit -}}
{{- " file_inherit" -}}
{{- end -}}
{{- if .NoNewPrivs -}}
{{- " no new privs" -}}
{{- end -}}
{{- if .Optional -}}
{{- " optional:" -}}
{{- end -}}
{{- with .Comment -}}
{{ " " }}{{ . }}
{{- end -}}
{{- end -}}

18
pkg/aa/templates/hat.j2 Normal file
View file

@ -0,0 +1,18 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "hat" -}}
{{- "hat" -}}
{{- with .Name -}}
{{ " " }}{{ . }}
{{- end -}}
{{- " {\n" -}}
{{- setindent "++" -}}
{{- template "rules" .Rules -}}
{{- setindent "--" -}}
{{- indent "}" -}}
{{- end -}}

View file

@ -2,27 +2,8 @@
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}} {{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}} {{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- range .Abi -}} {{- define "profile" -}}
{{- if .IsMagic -}}
{{ "abi <" }}{{ .Path }}{{ ">,\n" }}
{{- else -}}
{{ "abi \"" }}{{ .Path }}{{ "\",\n" }}
{{- end }}
{{ end -}}
{{- range .Aliases -}}
{{ "alias " }}{{ .Path }}{{ " -> " }}{{ .RewrittenPath }}{{ ",\n" }}
{{ end -}}
{{- range .Includes -}}
{{ template "include" . }}{{ "\n" }}
{{ end -}}
{{- range .Variables -}}
{{ "@{" }}{{ .Name }}{{ "} = " }}{{ join .Values }}
{{ end -}}
{{- if or .Name .Attachments .Attributes .Flags -}}
{{- "profile" -}} {{- "profile" -}}
{{- with .Name -}} {{- with .Name -}}
{{ " " }}{{ . }} {{ " " }}{{ . }}
@ -36,260 +17,11 @@
{{- with .Flags -}} {{- with .Flags -}}
{{ " flags=(" }}{{ join . }}{{ ")" }} {{ " flags=(" }}{{ join . }}{{ ")" }}
{{- end -}} {{- end -}}
{{ " {\n" }}
{{- end -}}
{{- $oldtype := "" -}} {{- " {\n" -}}
{{- range .Rules -}} {{- setindent "++" -}}
{{- $type := typeof . -}} {{- template "rules" .Rules -}}
{{- if eq $type "Rule" -}} {{- setindent "--" -}}
{{- "\n" -}} {{- indent "}" -}}
{{- continue -}}
{{- end -}}
{{- if and (ne $type $oldtype) (ne $oldtype "") -}}
{{- "\n" -}}
{{- end -}}
{{- indent "" -}}
{{- if eq $type "Include" -}}
{{ template "include" . }}
{{- end -}}
{{- if eq $type "Rlimit" -}}
{{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }}
{{- end -}}
{{- if eq $type "Capability" -}}
{{ template "qualifier" . }}{{ "capability " }}{{ .Name }}{{ "," }}{{ template "comment" . }}
{{- end -}}
{{- if eq $type "Network" -}}
{{- template "qualifier" . -}}
{{ "network" }}
{{- with .Domain -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .Type -}}
{{ " " }}{{ . }}
{{- else -}}
{{- with .Protocol -}}
{{ " " }}{{ . }}
{{- end -}}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Mount" -}}
{{- template "qualifier" . -}}
{{- "mount" -}}
{{- with .FsType -}}
{{ " fstype=" }}{{ . }}
{{- end -}}
{{- with .Options -}}
{{ " options=(" }}{{ join . }}{{ ")" }}
{{- end -}}
{{- with .Source -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .MountPoint -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Umount" -}}
{{- template "qualifier" . -}}
{{- "umount" -}}
{{- with .FsType -}}
{{ " fstype=" }}{{ . }}
{{- end -}}
{{- with .Options -}}
{{ " options=(" }}{{ join . }}{{ ")" }}
{{- end -}}
{{- with .MountPoint -}}
{{ " " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Remount" -}}
{{- template "qualifier" . -}}
{{- "remount" -}}
{{- with .FsType -}}
{{ " fstype=" }}{{ . }}
{{- end -}}
{{- with .Options -}}
{{ " options=(" }}{{ join . }}{{ ")" }}
{{- end -}}
{{- with .MountPoint -}}
{{ " " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "PivotRoot" -}}
{{- template "qualifier" . -}}
{{- "pivot_root" -}}
{{- with .OldRoot -}}
{{ " oldroot=" }}{{ . }}
{{- end -}}
{{- with .NewRoot -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .TargetProfile -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "ChangeProfile" -}}
{{- template "qualifier" . -}}
{{- "change_profile" -}}
{{- with .ExecMode -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .Exec -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .ProfileName -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Mqueue" -}}
{{- template "qualifier" . -}}
{{- "mqueue" -}}
{{- with .Access -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .Type -}}
{{ " type=" }}{{ . }}
{{- end -}}
{{- with .Label -}}
{{ " label=" }}{{ . }}
{{- end -}}
{{- with .Name -}}
{{ " " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Unix" -}}
{{- template "qualifier" . -}}
{{- "unix" -}}
{{- with .Access -}}
{{ " (" }}{{ . }}{{ ")" }}
{{- end -}}
{{- with .Type -}}
{{ " type=" }}{{ . }}
{{- end -}}
{{- with .Address -}}
{{ " addr=" }}{{ . }}
{{- end -}}
{{- if .Peer -}}
{{ " peer=(label=" }}{{ .Peer }}
{{- with .PeerAddr -}}
{{ ", addr="}}{{ . }}
{{- end -}}
{{- ")" -}}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Ptrace" -}}
{{- template "qualifier" . -}}
{{- "ptrace" -}}
{{- with .Access -}}
{{ " (" }}{{ . }}{{ ")" }}
{{- end -}}
{{- with .Peer -}}
{{ " peer=" }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Signal" -}}
{{- template "qualifier" . -}}
{{- "signal" -}}
{{- with .Access -}}
{{ " (" }}{{ . }}{{ ")" }}
{{- end -}}
{{- with .Set -}}
{{ " set=(" }}{{ . }}{{ ")" }}
{{- end -}}
{{- with .Peer -}}
{{ " peer=" }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "Dbus" -}}
{{- template "qualifier" . -}}
{{- "dbus" -}}
{{- if eq .Access "bind" -}}
{{ " bind bus=" }}{{ .Bus }}{{ " name=" }}{{ .Name }}
{{- else -}}
{{- with .Access -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .Bus -}}
{{ " bus=" }}{{ . }}
{{- end -}}
{{- with .Path -}}
{{ " path=" }}{{ . }}
{{- end -}}
{{ "\n" }}
{{- with .Interface -}}
{{ overindent "interface=" }}{{ . }}{{ "\n" }}
{{- end -}}
{{- with .Member -}}
{{ overindent "member=" }}{{ . }}{{ "\n" }}
{{- end -}}
{{- if and .Name .Label -}}
{{ overindent "peer=(name=" }}{{ .Name }}{{ ", label="}}{{ .Label }}{{ ")" }}
{{- else -}}
{{- with .Name -}}
{{ overindent "peer=(name=" }}{{ . }}{{ ")" }}
{{- end -}}
{{- with .Label -}}
{{ overindent "peer=(label=" }}{{ . }}{{ ")" }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- if eq $type "File" -}}
{{- template "qualifier" . -}}
{{- .Path -}}
{{- " " -}}
{{- with .Padding -}}
{{ . }}
{{- end -}}
{{- .Access -}}
{{- with .Target -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- "\n" -}}
{{- $oldtype = $type -}}
{{- end -}}
{{- if or .Name .Attachments .Attributes .Flags -}}
{{- "}\n" -}}
{{- end -}} {{- end -}}

View file

@ -0,0 +1,14 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "abi" -}}
{{- "abi" -}}
{{- if .IsMagic -}}
{{ " <" }}{{ .Path }}{{ ">" }}
{{- else -}}
{{ " \"" }}{{ .Path }}{{ "\"" }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,12 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "alias" -}}
{{- "alias " -}}
{{- .Path -}}
{{- " -> " -}}
{{- .RewrittenPath -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,9 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "all" -}}
{{- "all" -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,13 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "capability" -}}
{{- template "qualifier" . -}}
{{- "capability" -}}
{{- range .Names -}}
{{ " " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,19 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "change_profile" -}}
{{- template "qualifier" . -}}
{{- "change_profile" -}}
{{- with .ExecMode -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .Exec -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .ProfileName -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,25 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "comment" -}}
{{- if or .FileInherit .NoNewPrivs .Optional .Comment -}}
{{- if .IsLineRule }}
{{- "#" -}}
{{- else -}}
{{- " #" -}}
{{- end -}}
{{- if .FileInherit -}}
{{- " file_inherit" -}}
{{- end -}}
{{- if .NoNewPrivs -}}
{{- " no new privs" -}}
{{- end -}}
{{- if .Optional -}}
{{- " optional:" -}}
{{- end -}}
{{- with .Comment -}}
{{ . }}
{{- end -}}
{{- end -}}
{{- end -}}

View file

@ -0,0 +1,43 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "dbus" -}}
{{- template "qualifier" . -}}
{{- "dbus" -}}
{{- $access := "" -}}
{{- if .Access -}}
{{- $access = index .Access 0 -}}
{{- end -}}
{{- if eq $access "bind" -}}
{{ " bind bus=" }}{{ .Bus }}{{ " name=" }}{{ .Name }}
{{- else -}}
{{- with .Access -}}
{{ " " }}{{ cjoin . }}
{{- end -}}
{{- with .Bus -}}
{{ " bus=" }}{{ . }}
{{- end -}}
{{- with .Path -}}
{{ " path=" }}{{ . }}
{{- end -}}
{{- with .Interface -}}
{{ "\n" }}{{ overindent "interface=" }}{{ . }}
{{- end -}}
{{- with .Member -}}
{{ "\n" }}{{ overindent "member=" }}{{ . }}
{{- end -}}
{{- if and .PeerName .PeerLabel -}}
{{ "\n" }}{{ overindent "peer=(name=" }}{{ .PeerName }}{{ ", label="}}{{ .PeerLabel }}{{ ")" }}
{{- else -}}
{{- with .PeerName -}}
{{ "\n" }}{{ overindent "peer=(name=" }}{{ . }}{{ ")" }}
{{- end -}}
{{- with .PeerLabel -}}
{{ "\n" }}{{ overindent "peer=(label=" }}{{ . }}{{ ")" }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,41 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "file" -}}
{{- template "qualifier" . -}}
{{- if .Owner -}}
{{- "owner " -}}
{{- end -}}
{{- .Path -}}
{{- " " -}}
{{- with .Padding -}}
{{ . }}
{{- end -}}
{{- range .Access -}}
{{- . -}}
{{- end -}}
{{- with .Target -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- define "link" -}}
{{- template "qualifier" . -}}
{{- if .Owner -}}
{{- "owner " -}}
{{- end -}}
{{- "link " -}}
{{- if .Subset -}}
{{- "subset " -}}
{{- end -}}
{{- .Path -}}
{{- " " -}}
{{- with .Target -}}
{{ "-> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -1,3 +1,7 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "include" -}} {{- define "include" -}}
{{- "include" -}} {{- "include" -}}
{{- if .IfExists -}} {{- if .IfExists -}}
@ -8,4 +12,5 @@
{{- else -}} {{- else -}}
{{ " \"" }}{{ .Path }}{{ "\"" }} {{ " \"" }}{{ .Path }}{{ "\"" }}
{{- end -}} {{- end -}}
{{- template "comment" . -}}
{{- end -}} {{- end -}}

View file

@ -0,0 +1,16 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "io_uring" -}}
{{- template "qualifier" . -}}
{{- "io_uring" -}}
{{- range .Access -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .Label -}}
{{ " label=" }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,54 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "mount" -}}
{{- template "qualifier" . -}}
{{- "mount" -}}
{{- with .FsType -}}
{{ " fstype=" }}{{ . }}
{{- end -}}
{{- with .Options -}}
{{ " options=" }}{{ cjoin . }}
{{- end -}}
{{- with .Source -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .MountPoint -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- define "remount" -}}
{{- template "qualifier" . -}}
{{- "remount" -}}
{{- with .FsType -}}
{{ " fstype=" }}{{ . }}
{{- end -}}
{{- with .Options -}}
{{ " options=" }}{{ cjoin . }}
{{- end -}}
{{- with .MountPoint -}}
{{ " " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}
{{- define "umount" -}}
{{- template "qualifier" . -}}
{{- "umount" -}}
{{- with .FsType -}}
{{ " fstype=" }}{{ . }}
{{- end -}}
{{- with .Options -}}
{{ " options=" }}{{ cjoin . }}
{{- end -}}
{{- with .MountPoint -}}
{{ " " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,22 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "mqueue" -}}
{{- template "qualifier" . -}}
{{- "mqueue" -}}
{{- with .Access -}}
{{ " " }}{{ cjoin . }}
{{- end -}}
{{- with .Type -}}
{{ " type=" }}{{ . }}
{{- end -}}
{{- with .Label -}}
{{ " label=" }}{{ . }}
{{- end -}}
{{- with .Name -}}
{{ " " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,20 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "network" -}}
{{- template "qualifier" . -}}
{{ "network" }}
{{- with .Domain -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .Type -}}
{{ " " }}{{ . }}
{{- else -}}
{{- with .Protocol -}}
{{ " " }}{{ . }}
{{- end -}}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,19 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "pivot_root" -}}
{{- template "qualifier" . -}}
{{- "pivot_root" -}}
{{- with .OldRoot -}}
{{ " oldroot=" }}{{ . }}
{{- end -}}
{{- with .NewRoot -}}
{{ " " }}{{ . }}
{{- end -}}
{{- with .TargetProfile -}}
{{ " -> " }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,16 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "ptrace" -}}
{{- template "qualifier" . -}}
{{- "ptrace" -}}
{{- with .Access -}}
{{ " " }}{{ cjoin . }}
{{- end -}}
{{- with .Peer -}}
{{ " peer=" }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -1,10 +1,11 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "qualifier" -}} {{- define "qualifier" -}}
{{- with .Prefix -}} {{- with .Prefix -}}
{{ . }} {{ . }}
{{- end -}} {{- end -}}
{{- if .Owner -}}
{{- "owner " -}}
{{- end -}}
{{- if .Audit -}} {{- if .Audit -}}
{{- "audit " -}} {{- "audit " -}}
{{- end -}} {{- end -}}

View file

@ -0,0 +1,7 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "rlimit" -}}
{{ "set rlimit " }}{{ .Key }} {{ .Op }} {{ .Value }}{{ "," }}{{ template "comment" . }}
{{- end -}}

View file

@ -0,0 +1,19 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "signal" -}}
{{- template "qualifier" . -}}
{{- "signal" -}}
{{- with .Access -}}
{{ " " }}{{ cjoin . }}
{{- end -}}
{{- with .Set -}}
{{ " set=" }}{{ cjoin . }}
{{- end -}}
{{- with .Peer -}}
{{ " peer=" }}{{ . }}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,35 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "unix" -}}
{{- template "qualifier" . -}}
{{- "unix" -}}
{{- with .Access -}}
{{ " " }}{{ cjoin . }}
{{- end -}}
{{- with .Type -}}
{{ " type=" }}{{ . }}
{{- end -}}
{{- with .Protocol -}}
{{ " protocol=" }}{{ . }}
{{- end -}}
{{- with .Address -}}
{{ " addr=" }}{{ . }}
{{- end -}}
{{- with .Label -}}
{{ " label=" }}{{ . }}
{{- end -}}
{{- if and .PeerLabel .PeerAddr -}}
{{ " peer=(label=" }}{{ .PeerLabel }}{{ ", addr="}}{{ .PeerAddr }}{{ ")" }}
{{- else -}}
{{- with .PeerLabel -}}
{{ overindent "peer=(label=" }}{{ . }}{{ ")" }}
{{- end -}}
{{- with .PeerAddr -}}
{{ overindent "peer=(addr=" }}{{ . }}{{ ")" }}
{{- end -}}
{{- end -}}
{{- "," -}}
{{- template "comment" . -}}
{{- end -}}

View file

@ -0,0 +1,9 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "userns" -}}
{{- if .Create -}}
{{ template "qualifier" . }}{{ "userns," }}{{ template "comment" . }}
{{- end -}}
{{- end -}}

View file

@ -0,0 +1,14 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "variable" -}}
{{- "@{" -}}{{- .Name -}}{{- "}" -}}
{{- if .Define }}
{{- " = " -}}
{{- else -}}
{{- " += " -}}
{{- end -}}
{{- join .Values -}}
{{- template "comment" . -}}
{{- end -}}

125
pkg/aa/templates/rules.j2 Normal file
View file

@ -0,0 +1,125 @@
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- define "rules" -}}
{{- $oldkind := "" -}}
{{- range . -}}
{{- $kind := kindof . -}}
{{- if eq $kind "" -}}
{{- "\n" -}}
{{- continue -}}
{{- end -}}
{{- if eq $kind "comment" -}}
{{- template "comment" . -}}
{{- "\n" -}}
{{- continue -}}
{{- end -}}
{{- if and (ne $kind $oldkind) (ne $oldkind "") -}}
{{- "\n" -}}
{{- end -}}
{{- indent "" -}}
{{- if eq $kind "abi" -}}
{{- template "abi" . -}}
{{- end -}}
{{- if eq $kind "alias" -}}
{{- template "alias" . -}}
{{- end -}}
{{- if eq $kind "include" -}}
{{- template "include" . -}}
{{- end -}}
{{- if eq $kind "variable" -}}
{{- template "variable" . -}}
{{- end -}}
{{- if eq $kind "all" -}}
{{- template "all" . -}}
{{- end -}}
{{- if eq $kind "rlimit" -}}
{{- template "rlimit" . -}}
{{- end -}}
{{- if eq $kind "userns" -}}
{{- template "userns" . -}}
{{- end -}}
{{- if eq $kind "capability" -}}
{{- template "capability" . -}}
{{- end -}}
{{- if eq $kind "network" -}}
{{- template "network" . -}}
{{- end -}}
{{- if eq $kind "mount" -}}
{{- template "mount" . -}}
{{- end -}}
{{- if eq $kind "remount" -}}
{{- template "remount" . -}}
{{- end -}}
{{- if eq $kind "umount" -}}
{{- template "umount" . -}}
{{- end -}}
{{- if eq $kind "pivot_root" -}}
{{- template "pivot_root" . -}}
{{- end -}}
{{- if eq $kind "change_profile" -}}
{{- template "change_profile" . -}}
{{- end -}}
{{- if eq $kind "mqueue" -}}
{{- template "mqueue" . -}}
{{- end -}}
{{- if eq $kind "io_uring" -}}
{{- template "io_uring" . -}}
{{- end -}}
{{- if eq $kind "unix" -}}
{{- template "unix" . -}}
{{- end -}}
{{- if eq $kind "ptrace" -}}
{{- template "ptrace" . -}}
{{- end -}}
{{- if eq $kind "signal" -}}
{{- template "signal" . -}}
{{- end -}}
{{- if eq $kind "dbus" -}}
{{- template "dbus" . -}}
{{- end -}}
{{- if eq $kind "file" -}}
{{- template "file" . -}}
{{- end -}}
{{- if eq $kind "link" -}}
{{- template "link" . -}}
{{- end -}}
{{- if eq $kind "profile" -}}
{{- template "profile" . -}}
{{- end -}}
{{- if eq $kind "hat" -}}
{{- template "hat" . -}}
{{- end -}}
{{- "\n" -}}
{{- $oldkind = $kind -}}
{{- end -}}
{{- end -}}

View file

@ -4,70 +4,109 @@
package aa package aa
import (
"fmt"
"slices"
)
const UNIX Kind = "unix"
func init() {
requirements[UNIX] = requirement{
"access": []string{
"create", "bind", "listen", "accept", "connect", "shutdown",
"getattr", "setattr", "getopt", "setopt", "send", "receive",
"r", "w", "rw",
},
}
}
type Unix struct { type Unix struct {
RuleBase
Qualifier Qualifier
Access string Access []string
Type string Type string
Protocol string Protocol string
Address string Address string
Label string Label string
Attr string Attr string
Opt string Opt string
Peer string PeerLabel string
PeerAddr string PeerAddr string
} }
func UnixFromLog(log map[string]string) ApparmorRule { func newUnixFromLog(log map[string]string) Rule {
return &Unix{ return &Unix{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Access: toAccess(log["requested_mask"]), Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(UNIX, log["requested_mask"])),
Type: log["sock_type"], Type: log["sock_type"],
Protocol: log["protocol"], Protocol: log["protocol"],
Address: log["addr"], Address: log["addr"],
Label: log["peer_label"], Label: log["label"],
Attr: log["attr"], Attr: log["attr"],
Opt: log["opt"], Opt: log["opt"],
Peer: log["peer"], PeerLabel: log["peer"],
PeerAddr: log["peer_addr"], PeerAddr: log["peer_addr"],
} }
} }
func (r *Unix) Validate() error {
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
return fmt.Errorf("%s: %w", r, err)
}
return nil
}
func (r *Unix) Less(other any) bool { func (r *Unix) Less(other any) bool {
o, _ := other.(*Unix) o, _ := other.(*Unix)
if r.Qualifier.Equals(o.Qualifier) { if len(r.Access) != len(o.Access) {
if r.Access == o.Access { return len(r.Access) < len(o.Access)
if r.Type == o.Type {
if r.Protocol == o.Protocol {
if r.Address == o.Address {
if r.Label == o.Label {
if r.Attr == o.Attr {
if r.Opt == o.Opt {
if r.Peer == o.Peer {
return r.PeerAddr < o.PeerAddr
}
return r.Peer < o.Peer
}
return r.Opt < o.Opt
}
return r.Attr < o.Attr
}
return r.Label < o.Label
}
return r.Address < o.Address
}
return r.Protocol < o.Protocol
} }
if r.Type != o.Type {
return r.Type < o.Type return r.Type < o.Type
} }
return r.Access < o.Access if r.Protocol != o.Protocol {
return r.Protocol < o.Protocol
}
if r.Address != o.Address {
return r.Address < o.Address
}
if r.Label != o.Label {
return r.Label < o.Label
}
if r.Attr != o.Attr {
return r.Attr < o.Attr
}
if r.Opt != o.Opt {
return r.Opt < o.Opt
}
if r.PeerLabel != o.PeerLabel {
return r.PeerLabel < o.PeerLabel
}
if r.PeerAddr != o.PeerAddr {
return r.PeerAddr < o.PeerAddr
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
} }
func (r *Unix) Equals(other any) bool { func (r *Unix) Equals(other any) bool {
o, _ := other.(*Unix) o, _ := other.(*Unix)
return r.Access == o.Access && r.Type == o.Type && return slices.Equal(r.Access, o.Access) && r.Type == o.Type &&
r.Protocol == o.Protocol && r.Address == o.Address && r.Protocol == o.Protocol && r.Address == o.Address &&
r.Label == o.Label && r.Attr == o.Attr && r.Opt == o.Opt && r.Label == o.Label && r.Attr == o.Attr && r.Opt == o.Opt &&
r.Peer == o.Peer && r.PeerAddr == o.PeerAddr && r.Qualifier.Equals(o.Qualifier) r.PeerLabel == o.PeerLabel && r.PeerAddr == o.PeerAddr &&
r.Qualifier.Equals(o.Qualifier)
}
func (r *Unix) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Unix) Constraint() constraint {
return blockKind
}
func (r *Unix) Kind() Kind {
return UNIX
} }

View file

@ -4,21 +4,29 @@
package aa package aa
const USERNS Kind = "userns"
type Userns struct { type Userns struct {
RuleBase
Qualifier Qualifier
Create bool Create bool
} }
func UsernsFromLog(log map[string]string) ApparmorRule { func newUsernsFromLog(log map[string]string) Rule {
return &Userns{ return &Userns{
Qualifier: NewQualifierFromLog(log), RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Create: true, Create: true,
} }
} }
func (r *Userns) Validate() error {
return nil
}
func (r *Userns) Less(other any) bool { func (r *Userns) Less(other any) bool {
o, _ := other.(*Userns) o, _ := other.(*Userns)
if r.Qualifier.Equals(o.Qualifier) { if r.Create != o.Create {
return r.Create return r.Create
} }
return r.Qualifier.Less(o.Qualifier) return r.Qualifier.Less(o.Qualifier)
@ -28,3 +36,15 @@ func (r *Userns) Equals(other any) bool {
o, _ := other.(*Userns) o, _ := other.(*Userns)
return r.Create == o.Create && r.Qualifier.Equals(o.Qualifier) return r.Create == o.Create && r.Qualifier.Equals(o.Qualifier)
} }
func (r *Userns) String() string {
return renderTemplate(r.Kind(), r)
}
func (r *Userns) Constraint() constraint {
return blockKind
}
func (r *Userns) Kind() Kind {
return USERNS
}

View file

@ -1,135 +0,0 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 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"
"slices"
"strings"
)
var (
regVariablesDef = regexp.MustCompile(`@{(.*)}\s*[+=]+\s*(.*)`)
regVariablesRef = regexp.MustCompile(`@{([^{}]+)}`)
)
type Variable struct {
Name string
Values []string
}
func (r Variable) Less(other Variable) bool {
if r.Name == other.Name {
return len(r.Values) < len(other.Values)
}
return r.Name < other.Name
}
func (r Variable) Equals(other Variable) bool {
return r.Name == other.Name && slices.Equal(r.Values, other.Values)
}
// 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{
{"int2", []string{"[0-9][0-9]"}},
{"bin", []string{"/{,usr/}{,s}bin"}},
{"lib", []string{"/{,usr/}lib{,exec,32,64}"}},
{"multiarch", []string{"*-linux-gnu*"}},
{"HOME", []string{"/home/*"}},
{"user_share_dirs", []string{"/home/*/.local/share"}},
{"etc_ro", []string{"/{,usr/}etc/"}},
{"int", []string{"[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}"}},
},
},
}
}
// 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 {
attachments := p.resolve(value)
if len(attachments) == 0 {
panic("Variable not defined in: " + value)
}
p.Attachments = append(p.Attachments, attachments...)
}
}
}
}
// 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

@ -1,206 +0,0 @@
// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2023-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only
package aa
import (
"reflect"
"testing"
)
func TestAppArmorProfile_ParseVariables(t *testing.T) {
tests := []struct {
name string
content string
want []Variable
}{
{
name: "firefox",
content: `@{firefox_name} = firefox{,-esr,-bin}
@{firefox_lib_dirs} = /{usr/,}lib{,32,64}/@{firefox_name} /opt/@{firefox_name}
@{firefox_config_dirs} = @{HOME}/.mozilla/
@{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}"}},
},
},
{
name: "xorg",
content: `@{exec_path} = /{usr/,}bin/X
@{exec_path} += /{usr/,}bin/Xorg{,.bin}
@{exec_path} += /{usr/,}lib/Xorg{,.wrap}
@{exec_path} += /{usr/,}lib/xorg/Xorg{,.wrap}`,
want: []Variable{
{"exec_path", []string{
"/{usr/,}bin/X",
"/{usr/,}bin/Xorg{,.bin}",
"/{usr/,}lib/Xorg{,.wrap}",
"/{usr/,}lib/xorg/Xorg{,.wrap}"},
},
},
},
{
name: "snapd",
content: `@{lib_dirs} = @{lib}/ /snap/snapd/@{int}@{lib}
@{exec_path} = @{lib_dirs}/snapd/snapd`,
want: []Variable{
{"lib_dirs", []string{"@{lib}/", "/snap/snapd/@{int}@{lib}"}},
{"exec_path", []string{"@{lib_dirs}/snapd/snapd"}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewAppArmorProfile()
p.ParseVariables(tt.content)
if !reflect.DeepEqual(p.Variables, tt.want) {
t.Errorf("AppArmorProfile.ParseVariables() = %v, want %v", p.Variables, tt.want)
}
})
}
}
func TestAppArmorProfile_resolve(t *testing.T) {
tests := []struct {
name string
input string
want []string
}{
{
name: "empty",
input: "@{}",
want: []string{"@{}"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := DefaultTunables()
if got := p.resolve(tt.input); !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppArmorProfile.resolve() = %v, want %v", got, tt.want)
}
})
}
}
func TestAppArmorProfile_ResolveAttachments(t *testing.T) {
tests := []struct {
name string
variables []Variable
want []string
}{
{
name: "firefox",
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}",
"/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
"/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
},
},
{
name: "chromium",
variables: []Variable{
{"name", []string{"chromium"}},
{"lib_dirs", []string{"/{usr/,}lib/@{name}"}},
{"exec_path", []string{"@{lib_dirs}/@{name}"}},
},
want: []string{
"/{usr/,}lib/chromium/chromium",
},
},
{
name: "geoclue",
variables: []Variable{
{"libexec", []string{"/{usr/,}libexec"}},
{"exec_path", []string{"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}},
},
want: []string{
"/{usr/,}libexec/geoclue",
"/{usr/,}libexec/geoclue-2.0/demos/agent",
},
},
{
name: "opera",
variables: []Variable{
{"multiarch", []string{"*-linux-gnu*"}},
{"name", []string{"opera{,-beta,-developer}"}},
{"lib_dirs", []string{"/{usr/,}lib/@{multiarch}/@{name}"}},
{"exec_path", []string{"@{lib_dirs}/@{name}"}},
},
want: []string{
"/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
}
})
}
}
func TestAppArmorProfile_NestAttachments(t *testing.T) {
tests := []struct {
name string
Attachments []string
want string
}{
{
name: "firefox",
Attachments: []string{
"/{usr/,}bin/firefox{,-esr,-bin}",
"/{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
"/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
},
want: "/{{usr/,}bin/firefox{,-esr,-bin},{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin},opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}}",
},
{
name: "geoclue",
Attachments: []string{
"/{usr/,}libexec/geoclue",
"/{usr/,}libexec/geoclue-2.0/demos/agent",
},
want: "/{{usr/,}libexec/geoclue,{usr/,}libexec/geoclue-2.0/demos/agent}",
},
{
name: "null",
Attachments: []string{},
want: "",
},
{
name: "empty",
Attachments: []string{""},
want: "",
},
{
name: "not valid aare",
Attachments: []string{"/file", "relative"},
want: "/{file,relative}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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

@ -197,8 +197,8 @@ func (aaLogs AppArmorLogs) String() string {
} }
// ParseToProfiles convert the log data into a new AppArmorProfiles // ParseToProfiles convert the log data into a new AppArmorProfiles
func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles { func (aaLogs AppArmorLogs) ParseToProfiles() map[string]*aa.Profile {
profiles := make(aa.AppArmorProfiles, 0) profiles := make(map[string]*aa.Profile, 0)
for _, log := range aaLogs { for _, log := range aaLogs {
name := "" name := ""
if strings.Contains(log["operation"], "dbus") { if strings.Contains(log["operation"], "dbus") {
@ -208,8 +208,7 @@ func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles {
} }
if _, ok := profiles[name]; !ok { if _, ok := profiles[name]; !ok {
profile := &aa.AppArmorProfile{} profile := &aa.Profile{Header: aa.Header{Name: name}}
profile.Name = name
profile.AddRule(log) profile.AddRule(log)
profiles[name] = profile profiles[name] = profile
} else { } else {

View file

@ -292,44 +292,40 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
aaLogs AppArmorLogs aaLogs AppArmorLogs
want aa.AppArmorProfiles want map[string]*aa.Profile
}{ }{
{ {
name: "", name: "",
aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...), aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...),
want: aa.AppArmorProfiles{ want: map[string]*aa.Profile{
"kmod": &aa.AppArmorProfile{ "kmod": {
Profile: aa.Profile{ Header: aa.Header{Name: "kmod"},
Name: "kmod",
Rules: aa.Rules{ Rules: aa.Rules{
&aa.Unix{ &aa.Unix{
Qualifier: aa.Qualifier{FileInherit: true}, RuleBase: aa.RuleBase{FileInherit: true},
Access: "send receive", Access: []string{"send", "receive"},
Type: "stream", Type: "stream",
Protocol: "0", Protocol: "0",
}, },
&aa.Unix{ &aa.Unix{
Qualifier: aa.Qualifier{FileInherit: true}, RuleBase: aa.RuleBase{FileInherit: true},
Access: "send receive", Access: []string{"send", "receive"},
Type: "stream", Type: "stream",
Protocol: "0", Protocol: "0",
}, },
}, },
}, },
}, "power-profiles-daemon": {
"power-profiles-daemon": &aa.AppArmorProfile{ Header: aa.Header{Name: "power-profiles-daemon"},
Profile: aa.Profile{
Name: "power-profiles-daemon",
Rules: aa.Rules{ Rules: aa.Rules{
&aa.Dbus{ &aa.Dbus{
Access: "send", Access: []string{"send"},
Bus: "system", Bus: "system",
Name: "org.freedesktop.DBus",
Path: "/org/freedesktop/DBus", Path: "/org/freedesktop/DBus",
Interface: "org.freedesktop.DBus", Interface: "org.freedesktop.DBus",
Member: "AddMatch", Member: "AddMatch",
Label: "dbus-daemon", PeerName: "org.freedesktop.DBus",
}, PeerLabel: "dbus-daemon",
}, },
}, },
}, },

View file

@ -30,6 +30,6 @@ func init() {
}) })
} }
func (b ABI3) Apply(profile string) string { func (b ABI3) Apply(opt *Option, profile string) (string, error) {
return regAbi4To3.Replace(profile) return regAbi4To3.Replace(profile), nil
} }

View file

@ -30,13 +30,13 @@ func init() {
}) })
} }
func (b Complain) Apply(profile string) string { func (b Complain) Apply(opt *Option, profile string) (string, error) {
flags := []string{} flags := []string{}
matches := regFlags.FindStringSubmatch(profile) matches := regFlags.FindStringSubmatch(profile)
if len(matches) != 0 { if len(matches) != 0 {
flags = strings.Split(matches[1], ",") flags = strings.Split(matches[1], ",")
if slices.Contains(flags, "complain") { if slices.Contains(flags, "complain") {
return profile return profile, nil
} }
} }
flags = append(flags, "complain") flags = append(flags, "complain")
@ -44,5 +44,5 @@ func (b Complain) Apply(profile string) string {
// Remove all flags definition, then set manifest' flags // Remove all flags definition, then set manifest' flags
profile = regFlags.ReplaceAllLiteralString(profile, "") profile = regFlags.ReplaceAllLiteralString(profile, "")
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags) return regProfileHeader.ReplaceAllLiteralString(profile, strFlags), nil
} }

View file

@ -7,6 +7,7 @@ package builder
import ( import (
"fmt" "fmt"
"github.com/roddhjav/apparmor.d/pkg/paths"
"github.com/roddhjav/apparmor.d/pkg/prebuild/cfg" "github.com/roddhjav/apparmor.d/pkg/prebuild/cfg"
) )
@ -21,7 +22,20 @@ var (
// Main directive interface // Main directive interface
type Builder interface { type Builder interface {
cfg.BaseInterface cfg.BaseInterface
Apply(profile string) string Apply(opt *Option, profile string) (string, error)
}
// Builder options
type Option struct {
Name string
File *paths.Path
}
func NewOption(file *paths.Path) *Option {
return &Option{
Name: file.Base(),
File: file,
}
} }
func Register(names ...string) { func Register(names ...string) {
@ -37,3 +51,15 @@ func Register(names ...string) {
func RegisterBuilder(d Builder) { func RegisterBuilder(d Builder) {
Builders[d.Name()] = d Builders[d.Name()] = d
} }
func Run(file *paths.Path, profile string) (string, error) {
var err error
opt := NewOption(file)
for _, b := range Builds {
profile, err = b.Apply(opt, profile)
if err != nil {
return "", fmt.Errorf("%s %s: %w", b.Name(), opt.File, err)
}
}
return profile, nil
}

View file

@ -7,6 +7,8 @@ package builder
import ( import (
"slices" "slices"
"testing" "testing"
"github.com/roddhjav/apparmor.d/pkg/prebuild/cfg"
) )
func TestBuilder_Apply(t *testing.T) { func TestBuilder_Apply(t *testing.T) {
@ -15,6 +17,7 @@ func TestBuilder_Apply(t *testing.T) {
b Builder b Builder
profile string profile string
want string want string
wantErr bool
}{ }{
{ {
name: "abi3", name: "abi3",
@ -215,7 +218,7 @@ func TestBuilder_Apply(t *testing.T) {
}`, }`,
}, },
{ {
name: "userspace-1", name: "userspace-2",
b: Builders["userspace"], b: Builders["userspace"],
profile: ` profile: `
profile foo /usr/bin/foo { profile foo /usr/bin/foo {
@ -237,7 +240,13 @@ func TestBuilder_Apply(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) {
if got := tt.b.Apply(tt.profile); got != tt.want { opt := &Option{File: cfg.RootApparmord.Join(tt.name)}
got, err := tt.b.Apply(opt, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("Builder.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Builder.Apply() = %v, want %v", got, tt.want) t.Errorf("Builder.Apply() = %v, want %v", got, tt.want)
} }
}) })
@ -257,7 +266,6 @@ func TestRegister(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) {
Register(tt.names...) Register(tt.names...)
for _, name := range tt.names { for _, name := range tt.names {

View file

@ -31,6 +31,6 @@ func init() {
}) })
} }
func (b Dev) Apply(profile string) string { func (b Dev) Apply(opt *Option, profile string) (string, error) {
return regDev.Replace(profile) return regDev.Replace(profile), nil
} }

View file

@ -24,16 +24,16 @@ func init() {
}) })
} }
func (b Enforce) Apply(profile string) string { func (b Enforce) Apply(opt *Option, profile string) (string, error) {
matches := regFlags.FindStringSubmatch(profile) matches := regFlags.FindStringSubmatch(profile)
if len(matches) == 0 { if len(matches) == 0 {
return profile return profile, nil
} }
flags := strings.Split(matches[1], ",") flags := strings.Split(matches[1], ",")
idx := slices.Index(flags, "complain") idx := slices.Index(flags, "complain")
if idx == -1 { if idx == -1 {
return profile return profile, nil
} }
flags = slices.Delete(flags, idx, idx+1) flags = slices.Delete(flags, idx, idx+1)
strFlags := "{" strFlags := "{"
@ -43,5 +43,5 @@ func (b Enforce) Apply(profile string) string {
// Remove all flags definition, then set new flags // Remove all flags definition, then set new flags
profile = regFlags.ReplaceAllLiteralString(profile, "") profile = regFlags.ReplaceAllLiteralString(profile, "")
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags) return regProfileHeader.ReplaceAllLiteralString(profile, strFlags), nil
} }

View file

@ -28,6 +28,6 @@ func init() {
}) })
} }
func (b FullSystemPolicy) Apply(profile string) string { func (b FullSystemPolicy) Apply(opt *Option, profile string) (string, error) {
return regFullSystemPolicy.Replace(profile) return regFullSystemPolicy.Replace(profile), nil
} }

View file

@ -29,15 +29,26 @@ func init() {
}) })
} }
func (b Userspace) Apply(profile string) string { func (b Userspace) Apply(opt *Option, profile string) (string, error) {
p := aa.DefaultTunables() if ok, _ := opt.File.IsInsideDir(cfg.RootApparmord.Join("abstractions")); ok {
p.ParseVariables(profile) return profile, nil
p.ResolveAttachments() }
att := p.NestAttachments() if ok, _ := opt.File.IsInsideDir(cfg.RootApparmord.Join("tunables")); ok {
return profile, nil
}
f := aa.DefaultTunables()
if err := f.Parse(profile); err != nil {
return "", err
}
if err := f.Resolve(); err != nil {
return "", err
}
att := f.GetDefaultProfile().GetAttachments()
matches := regAttachments.FindAllString(profile, -1) matches := regAttachments.FindAllString(profile, -1)
if len(matches) > 0 { if len(matches) > 0 {
strheader := strings.Replace(matches[0], "@{exec_path}", att, -1) strheader := strings.Replace(matches[0], "@{exec_path}", att, -1)
return regAttachments.ReplaceAllLiteralString(profile, strheader) return regAttachments.ReplaceAllLiteralString(profile, strheader), nil
} }
return profile return profile, nil
} }

View file

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

View file

@ -70,6 +70,7 @@ func TestRun(t *testing.T) {
file *paths.Path file *paths.Path
profile string profile string
want string want string
wantErr bool
}{ }{
{ {
name: "none", name: "none",
@ -86,7 +87,12 @@ func TestRun(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) {
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) t.Errorf("Run() = %v, want %v", got, tt.want)
} }
}) })

View file

@ -50,41 +50,47 @@ func setInterfaces(rules map[string]string) []string {
return interfaces return interfaces
} }
func (d Dbus) Apply(opt *Option, profile string) string { func (d Dbus) Apply(opt *Option, profile string) (string, error) {
var p *aa.AppArmorProfile var r aa.Rules
action := d.sanityCheck(opt) action, err := d.sanityCheck(opt)
if err != nil {
return "", err
}
switch action { switch action {
case "own": case "own":
p = d.own(opt.ArgMap) r = d.own(opt.ArgMap)
case "talk": 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) lenDbus := len(generatedDbus)
generatedDbus = generatedDbus[:lenDbus-1] generatedDbus = generatedDbus[:lenDbus-1]
profile = strings.Replace(profile, opt.Raw, generatedDbus, -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 { 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] action := opt.ArgList[0]
if action != "own" && action != "talk" { 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 { 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 { 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" { 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 // 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["path"] = "/" + strings.Replace(opt.ArgMap["name"], ".", "/", -1) + "{,/**}"
} }
opt.ArgMap["name"] += "{,.*}" 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) interfaces := setInterfaces(rules)
p := &aa.AppArmorProfile{} res := aa.Rules{}
p.Rules = append(p.Rules, &aa.Dbus{ res = append(res, &aa.Dbus{
Access: "bind", Bus: rules["bus"], Name: rules["name"], Access: []string{"bind"}, Bus: rules["bus"], Name: rules["name"],
}) })
for _, iface := range interfaces { for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{ res = append(res, &aa.Dbus{
Access: "receive", Access: []string{"receive"},
Bus: rules["bus"], Bus: rules["bus"],
Path: rules["path"], Path: rules["path"],
Interface: iface, Interface: iface,
Name: `":1.@{int}"`, PeerName: `":1.@{int}"`,
}) })
} }
for _, iface := range interfaces { for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{ res = append(res, &aa.Dbus{
Access: "send", Access: []string{"send"},
Bus: rules["bus"], Bus: rules["bus"],
Path: rules["path"], Path: rules["path"],
Interface: iface, Interface: iface,
Name: `"{:1.@{int},org.freedesktop.DBus}"`, PeerName: `"{:1.@{int},org.freedesktop.DBus}"`,
}) })
} }
p.Rules = append(p.Rules, &aa.Dbus{ res = append(res, &aa.Dbus{
Access: "receive", Access: []string{"receive"},
Bus: rules["bus"], Bus: rules["bus"],
Path: rules["path"], Path: rules["path"],
Interface: "org.freedesktop.DBus.Introspectable", Interface: "org.freedesktop.DBus.Introspectable",
Member: "Introspect", 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) interfaces := setInterfaces(rules)
p := &aa.AppArmorProfile{} res := aa.Rules{}
for _, iface := range interfaces { for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{ res = append(res, &aa.Dbus{
Access: "send", Access: []string{"send"},
Bus: rules["bus"], Bus: rules["bus"],
Path: rules["path"], Path: rules["path"],
Interface: iface, Interface: iface,
Name: `"{:1.@{int},` + rules["name"] + `}"`, PeerName: `"{:1.@{int},` + rules["name"] + `}"`,
Label: rules["label"], PeerLabel: rules["label"],
}) })
} }
for _, iface := range interfaces { for _, iface := range interfaces {
p.Rules = append(p.Rules, &aa.Dbus{ res = append(res, &aa.Dbus{
Access: "receive", Access: []string{"receive"},
Bus: rules["bus"], Bus: rules["bus"],
Path: rules["path"], Path: rules["path"],
Interface: iface, Interface: iface,
Name: `"{:1.@{int},` + rules["name"] + `}"`, PeerName: `"{:1.@{int},` + rules["name"] + `}"`,
Label: rules["label"], PeerLabel: rules["label"],
}) })
} }
return p return res
} }

View file

@ -38,6 +38,7 @@ func TestDbus_Apply(t *testing.T) {
opt *Option opt *Option
profile string profile string
want string want string
wantErr bool
}{ }{
{ {
name: "own", name: "own",
@ -137,7 +138,12 @@ func TestDbus_Apply(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) {
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) 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> // Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only // SPDX-License-Identifier: GPL-2.0-only
// TODO: Local variables in profile header need to be resolved
package directive package directive
import ( 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" transition := "Px"
transitions := []string{"P", "U", "p", "u", "PU", "pu"} transitions := []string{"P", "U", "p", "u", "PU", "pu"}
t := opt.ArgList[0] t := opt.ArgList[0]
@ -36,26 +38,34 @@ func (d Exec) Apply(opt *Option, profile string) string {
delete(opt.ArgMap, t) delete(opt.ArgMap, t)
} }
p := &aa.AppArmorProfile{} rules := aa.Rules{}
for name := range opt.ArgMap { for name := range opt.ArgMap {
profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name)) profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name))
dstProfile := aa.DefaultTunables() dstProfile := aa.DefaultTunables()
dstProfile.ParseVariables(profiletoTransition) if err := dstProfile.Parse(profiletoTransition); err != nil {
for _, variable := range dstProfile.Variables { return "", err
}
if err := dstProfile.Resolve(); err != nil {
return "", err
}
for _, variable := range dstProfile.Preamble.GetVariables() {
if variable.Name == "exec_path" { if variable.Name == "exec_path" {
for _, v := range variable.Values { for _, v := range variable.Values {
p.Rules = append(p.Rules, &aa.File{ rules = append(rules, &aa.File{
Path: v, Path: v,
Access: transition, Access: []string{transition},
}) })
} }
break break
} }
} }
} }
p.Sort()
rules := p.String() aa.IndentationLevel = strings.Count(
lenRules := len(rules) strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation,
rules = rules[:lenRules-1] )
return strings.Replace(profile, opt.Raw, rules, -1) 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 opt *Option
profile string profile string
want string want string
wantErr bool
}{ }{
{ {
name: "exec", name: "exec",
@ -30,8 +31,8 @@ func TestExec_Apply(t *testing.T) {
Raw: " #aa:exec DiscoverNotifier", Raw: " #aa:exec DiscoverNotifier",
}, },
profile: ` #aa:exec DiscoverNotifier`, profile: ` #aa:exec DiscoverNotifier`,
want: ` @{lib}/@{multiarch}/{,libexec/}DiscoverNotifier Px, want: ` /{,usr/}lib{,exec,32,64}/*-linux-gnu*/{,libexec/}DiscoverNotifier Px,
@{lib}/DiscoverNotifier Px,`, /{,usr/}lib{,exec,32,64}/DiscoverNotifier Px,`,
}, },
{ {
name: "exec-unconfined", name: "exec-unconfined",
@ -44,15 +45,20 @@ func TestExec_Apply(t *testing.T) {
Raw: " #aa:exec U polkit-agent-helper", Raw: " #aa:exec U polkit-agent-helper",
}, },
profile: ` #aa:exec U polkit-agent-helper`, profile: ` #aa:exec U polkit-agent-helper`,
want: ` @{lib}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux, want: ` /{,usr/}lib{,exec,32,64}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux,
@{lib}/polkit-agent-helper-[0-9] Ux,`, /{,usr/}lib{,exec,32,64}/polkit-agent-helper-[0-9] Ux,`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cfg.RootApparmord = tt.rootApparmord cfg.RootApparmord = tt.rootApparmord
if got := Directives["exec"].Apply(tt.opt, tt.profile); got != tt.want { got, err := Directives["exec"].Apply(tt.opt, tt.profile)
t.Errorf("Exec.Apply() = %v, want %v", got, tt.want) 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) 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) { if only && filterRuleForUs(opt) {
return opt.Clean(profile) return opt.Clean(profile), nil
} }
if !only && !filterRuleForUs(opt) { if !only && !filterRuleForUs(opt) {
return opt.Clean(profile) return opt.Clean(profile), nil
} }
inline := true inline := true
@ -64,13 +64,13 @@ func filter(only bool, opt *Option, profile string) string {
regRemoveParagraph := regexp.MustCompile(`(?s)` + opt.Raw + `\n.*?\n\n`) regRemoveParagraph := regexp.MustCompile(`(?s)` + opt.Raw + `\n.*?\n\n`)
profile = regRemoveParagraph.ReplaceAllString(profile, "") 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) 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) return filter(false, opt, profile)
} }

View file

@ -18,6 +18,7 @@ func TestFilterOnly_Apply(t *testing.T) {
opt *Option opt *Option
profile string profile string
want string want string
wantErr bool
}{ }{
{ {
name: "inline", name: "inline",
@ -79,7 +80,12 @@ func TestFilterOnly_Apply(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cfg.Distribution = tt.dist cfg.Distribution = tt.dist
cfg.Family = tt.family 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) t.Errorf("FilterOnly.Apply() = %v, want %v", got, tt.want)
} }
}) })
@ -94,6 +100,7 @@ func TestFilterExclude_Apply(t *testing.T) {
opt *Option opt *Option
profile string profile string
want string want string
wantErr bool
}{ }{
{ {
name: "inline", name: "inline",
@ -128,7 +135,12 @@ func TestFilterExclude_Apply(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cfg.Distribution = tt.dist cfg.Distribution = tt.dist
cfg.Family = tt.family 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) 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 := "" res := ""
for name := range opt.ArgMap { for name := range opt.ArgMap {
stackedProfile := util.MustReadFile(cfg.RootApparmord.Join(name)) stackedProfile := util.MustReadFile(cfg.RootApparmord.Join(name))
m := regRules.FindStringSubmatch(stackedProfile) m := regRules.FindStringSubmatch(stackedProfile)
if len(m) < 2 { 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 := m[1]
stackedRules = regCleanStakedRules.Replace(stackedRules) 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 // Insert the stacked profile at the end of the current profile, remove the stack directive
m := regEndOfRules.FindStringSubmatch(profile) m := regEndOfRules.FindStringSubmatch(profile)
if len(m) <= 1 { 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, m[0], res+m[0], -1)
profile = strings.Replace(profile, opt.Raw, "", -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 opt *Option
profile string profile string
want string want string
wantErr bool
}{ }{
{ {
name: "stack", name: "stack",
@ -68,7 +69,12 @@ profile parent @{exec_path} {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cfg.RootApparmord = tt.rootApparmord 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) t.Errorf("Stack.Apply() = %v, want %v", got, tt.want)
} }
}) })

View file

@ -83,10 +83,14 @@ func Build() error {
if err != nil { if err != nil {
return err return err
} }
for _, b := range builder.Builds { profile, err = builder.Run(file, profile)
profile = b.Apply(profile) if err != nil {
return err
}
profile, err = directive.Run(file, profile)
if err != nil {
return err
} }
profile = directive.Run(file, profile)
if err := file.WriteFile([]byte(profile)); err != nil { if err := file.WriteFile([]byte(profile)); err != nil {
return err return err
} }

View file

@ -1,4 +1,5 @@
# Simple test profile for the AppArmorProfile.String() method # Simple test profile for the AppArmorProfileFile.String() method
abi <abi/4.0>, abi <abi/4.0>,
alias /mnt/usr -> /usr, alias /mnt/usr -> /usr,
@ -18,13 +19,13 @@ profile foo @{exec_path} xattrs=(security.tagged=allowed) flags=(complain attach
network inet stream, network inet stream,
network inet6 stream, network inet6 stream,
mount fstype=fuse.portal options=(rw rbind) @{run}/user/@{uid}/ -> /, mount fstype=fuse.portal options=(rw rbind) @{run}/user/@{uid}/ -> /, # failed perms check
umount @{run}/user/@{uid}/, umount @{run}/user/@{uid}/,
signal (receive) set=(term) peer=at-spi-bus-launcher, signal receive set=term peer=at-spi-bus-launcher,
ptrace (read) peer=nautilus, ptrace read peer=nautilus,
unix (send receive) type=stream addr=@/tmp/.ICE-unix/1995 peer=(label=gnome-shell, addr=none), unix (send receive) type=stream addr=@/tmp/.ICE-unix/1995 peer=(label=gnome-shell, addr=none),

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>