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
Failed to generate hash of commit
90 changed files with 4995 additions and 2012 deletions

View file

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

View file

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

View file

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

View file

@ -67,11 +67,11 @@ func aaLog(logger string, path string, profile string) error {
aaLogs := logs.NewApparmorLogs(file, profile)
if rules {
profiles := aaLogs.ParseToProfiles()
for _, profile := range profiles {
profile.MergeRules()
profile.Sort()
profile.Format()
fmt.Print(profile.String() + "\n")
for _, p := range profiles {
p.Merge()
p.Sort()
p.Format()
fmt.Print(p.String() + "\n\n")
}
} else {
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
type Capability struct {
Qualifier
Name string
import (
"fmt"
"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 {
RuleBase
Qualifier
Names []string
}
func newCapabilityFromLog(log map[string]string) Rule {
return &Capability{
Qualifier: NewQualifierFromLog(log),
Name: log["capname"],
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 {
o, _ := other.(*Capability)
if r.Name == o.Name {
return r.Qualifier.Less(o.Qualifier)
for i := 0; i < len(r.Names) && i < len(o.Names); i++ {
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 {
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
import "fmt"
const CHANGEPROFILE Kind = "change_profile"
func init() {
requirements[CHANGEPROFILE] = requirement{
"mode": []string{"safe", "unsafe"},
}
}
type ChangeProfile struct {
RuleBase
Qualifier
ExecMode string
Exec string
ProfileName string
}
func ChangeProfileFromLog(log map[string]string) ApparmorRule {
func newChangeProfileFromLog(log map[string]string) Rule {
return &ChangeProfile{
Qualifier: NewQualifierFromLog(log),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
ExecMode: log["mode"],
Exec: log["exec"],
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 {
o, _ := other.(*ChangeProfile)
if r.ExecMode == o.ExecMode {
if r.Exec == o.Exec {
return r.ProfileName < o.ProfileName
if r.ExecMode != o.ExecMode {
return r.ExecMode < o.ExecMode
}
if 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 {
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
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
include1 = &Include{IsMagic: true, 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"}
// 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
rlimit1 = &Rlimit{Key: "nproc", Op: "<=", Value: "200"}
rlimit2 = &Rlimit{Key: "cpu", Op: "<=", Value: "2"}
rlimit3 = &Rlimit{Key: "nproc", Op: "<", Value: "2"}
// Userns
userns1 = &Userns{Create: true}
userns2 = &Userns{}
// Capability
capability1Log = map[string]string{
"apparmor": "ALLOWED",
@ -26,8 +49,8 @@ var (
"profile": "pkexec",
"comm": "pkexec",
}
capability1 = &Capability{Name: "net_admin"}
capability2 = &Capability{Name: "sys_ptrace"}
capability1 = &Capability{Names: []string{"net_admin"}}
capability2 = &Capability{Names: []string{"sys_ptrace"}}
// Network
network1Log = map[string]string{
@ -71,18 +94,22 @@ var (
"flags": "rw, rbind",
}
mount1 = &Mount{
Qualifier: Qualifier{Comment: "failed perms check"},
RuleBase: RuleBase{Comment: " failed perms check"},
MountConditions: MountConditions{FsType: "overlay"},
Source: "overlay",
MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/",
}
mount2 = &Mount{
Qualifier: Qualifier{Comment: "failed perms check"},
RuleBase: RuleBase{Comment: " failed perms check"},
MountConditions: MountConditions{Options: []string{"rw", "rbind"}},
Source: "/oldroot/dev/tty",
MountPoint: "/newroot/dev/tty",
}
// Remount
remount1 = &Remount{MountPoint: "/"}
remount2 = &Remount{MountPoint: "/{,**}/"}
// Umount
umount1Log = map[string]string{
"apparmor": "ALLOWED",
@ -96,7 +123,6 @@ var (
umount2 = &Umount{MountPoint: "/oldroot/"}
// 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{
"apparmor": "ALLOWED",
"class": "mount",
@ -120,7 +146,6 @@ var (
}
// Change Profile
// changeprofile1LogStr = `apparmor="ALLOWED" operation="change_onexec" class="file" profile="systemd" name="systemd-user" comm="(systemd)" target="systemd-user"`
changeprofile1Log = map[string]string{
"apparmor": "ALLOWED",
"class": "file",
@ -134,6 +159,14 @@ var (
changeprofile2 = &ChangeProfile{ProfileName: "brwap"}
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
signal1Log = map[string]string{
"apparmor": "ALLOWED",
@ -147,13 +180,13 @@ var (
"peer": "firefox//&firejail-default",
}
signal1 = &Signal{
Access: "receive",
Set: "kill",
Access: []string{"receive"},
Set: []string{"kill"},
Peer: "firefox//&firejail-default",
}
signal2 = &Signal{
Access: "receive",
Set: "up",
Access: []string{"receive"},
Set: []string{"up"},
Peer: "firefox//&firejail-default",
}
@ -177,8 +210,8 @@ var (
"denied_mask": "readby",
"peer": "systemd-journald",
}
ptrace1 = &Ptrace{Access: "read", Peer: "nautilus"}
ptrace2 = &Ptrace{Access: "readby", Peer: "systemd-journald"}
ptrace1 = &Ptrace{Access: []string{"read"}, Peer: "nautilus"}
ptrace2 = &Ptrace{Access: []string{"readby"}, Peer: "systemd-journald"}
// Unix
unix1Log = map[string]string{
@ -197,16 +230,16 @@ var (
"protocol": "0",
}
unix1 = &Unix{
Access: "send receive",
Access: []string{"send", "receive"},
Type: "stream",
Protocol: "0",
Address: "none",
Peer: "dbus-daemon",
PeerAddr: "@/tmp/dbus-AaKMpxzC4k",
PeerLabel: "dbus-daemon",
}
unix2 = &Unix{
Qualifier: Qualifier{FileInherit: true},
Access: "receive",
RuleBase: RuleBase{FileInherit: true},
Access: []string{"receive"},
Type: "stream",
}
@ -234,21 +267,21 @@ var (
"label": "evolution-source-registry",
}
dbus1 = &Dbus{
Access: "receive",
Access: []string{"receive"},
Bus: "session",
Name: ":1.15",
Path: "/org/gtk/vfs/metadata",
Interface: "org.gtk.vfs.Metadata",
Member: "Remove",
Label: "tracker-extract",
PeerName: ":1.15",
PeerLabel: "tracker-extract",
}
dbus2 = &Dbus{
Access: "bind",
Access: []string{"bind"},
Bus: "session",
Name: "org.gnome.evolution.dataserver.Sources5",
}
dbus3 = &Dbus{
Access: "bind",
Access: []string{"bind"},
Bus: "session",
Name: "org.gnome.evolution.dataserver",
}
@ -283,10 +316,77 @@ var (
"OUID": "user",
"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{
Qualifier: Qualifier{Owner: true, NoNewPrivs: true},
RuleBase: RuleBase{NoNewPrivs: true},
Owner: true,
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
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 {
RuleBase
Qualifier
Access string
Access []string
Bus string
Name string
Path string
Interface 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{
Qualifier: NewQualifierFromLog(log),
Access: log["mask"],
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Access: []string{log["mask"]},
Bus: log["bus"],
Name: log["name"],
Name: name,
Path: log["path"],
Interface: log["interface"],
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 {
o, _ := other.(*Dbus)
if r.Qualifier.Equals(o.Qualifier) {
if r.Access == o.Access {
if r.Bus == o.Bus {
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
for i := 0; i < len(r.Access) && i < len(o.Access); i++ {
if r.Access[i] != o.Access[i] {
return r.Access[i] < o.Access[i]
}
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.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)
}
func (r *Dbus) Equals(other any) bool {
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.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
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 {
RuleBase
Qualifier
Owner bool
Path string
Access string
Access []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{
Qualifier: NewQualifierFromLog(log),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Owner: isOwner(log),
Path: log["name"],
Access: toAccess(log["requested_mask"]),
Access: accesses,
Target: log["target"],
}
}
func (r *File) Validate() error {
return nil
}
func (r *File) Less(other any) bool {
o, _ := other.(*File)
letterR := getLetterIn(fileAlphabet, r.Path)
letterO := getLetterIn(fileAlphabet, o.Path)
if fileWeights[letterR] == fileWeights[letterO] || letterR == "" || letterO == "" {
if r.Qualifier.Equals(o.Qualifier) {
if r.Path == o.Path {
if r.Access == o.Access {
return r.Target < o.Target
}
return r.Access < o.Access
if fileWeights[letterR] != fileWeights[letterO] && letterR != "" && letterO != "" {
return fileWeights[letterR] < fileWeights[letterO]
}
if 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 {
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)
}
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
import (
"fmt"
"slices"
)
const IOURING Kind = "io_uring"
func init() {
requirements[IOURING] = requirement{
"access": []string{"sqpoll", "override_creds"},
}
}
type IOUring struct {
RuleBase
Qualifier
Access string
Access []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 {
o, _ := other.(*IOUring)
if r.Qualifier.Equals(o.Qualifier) {
if r.Access == o.Access {
return r.Label < o.Label
if len(r.Access) != len(o.Access) {
return len(r.Access) < len(o.Access)
}
return r.Access < o.Access
if r.Label != o.Label {
return r.Label < o.Label
}
return r.Qualifier.Less(o.Qualifier)
}
func (r *IOUring) Equals(other any) bool {
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,30 +5,54 @@
package aa
import (
"fmt"
"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 {
FsType string
Options []string
}
func MountConditionsFromLog(log map[string]string) MountConditions {
func newMountConditionsFromLog(log map[string]string) MountConditions {
if _, present := log["flags"]; present {
return MountConditions{
FsType: log["fstype"],
Options: strings.Split(log["flags"], ", "),
Options: Must(toValues(MOUNT, "flags", log["flags"])),
}
}
return MountConditions{FsType: log["fstype"]}
}
func (m MountConditions) Validate() error {
return validateValues(MOUNT, "flags", m.Options)
}
func (m MountConditions) Less(other MountConditions) bool {
if m.FsType == other.FsType {
return len(m.Options) < len(other.Options)
}
if m.FsType != other.FsType {
return m.FsType < other.FsType
}
return len(m.Options) < len(other.Options)
}
func (m MountConditions) Equals(other MountConditions) bool {
@ -36,31 +60,40 @@ func (m MountConditions) Equals(other MountConditions) bool {
}
type Mount struct {
RuleBase
Qualifier
MountConditions
Source string
MountPoint string
}
func MountFromLog(log map[string]string) ApparmorRule {
func newMountFromLog(log map[string]string) Rule {
return &Mount{
Qualifier: NewQualifierFromLog(log),
MountConditions: MountConditionsFromLog(log),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
MountConditions: newMountConditionsFromLog(log),
Source: log["srcname"],
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 {
o, _ := other.(*Mount)
if r.Qualifier.Equals(o.Qualifier) {
if r.Source == o.Source {
if r.MountPoint == o.MountPoint {
return r.MountConditions.Less(o.MountConditions)
if r.Source != o.Source {
return r.Source < o.Source
}
if 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)
}
@ -72,28 +105,49 @@ func (r *Mount) Equals(other any) bool {
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 {
RuleBase
Qualifier
MountConditions
MountPoint string
}
func UmountFromLog(log map[string]string) ApparmorRule {
func newUmountFromLog(log map[string]string) Rule {
return &Umount{
Qualifier: NewQualifierFromLog(log),
MountConditions: MountConditionsFromLog(log),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
MountConditions: newMountConditionsFromLog(log),
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 {
o, _ := other.(*Umount)
if r.Qualifier.Equals(o.Qualifier) {
if r.MountPoint == o.MountPoint {
return r.MountConditions.Less(o.MountConditions)
}
if 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)
}
@ -104,28 +158,49 @@ func (r *Umount) Equals(other any) bool {
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 {
RuleBase
Qualifier
MountConditions
MountPoint string
}
func RemountFromLog(log map[string]string) ApparmorRule {
func newRemountFromLog(log map[string]string) Rule {
return &Remount{
Qualifier: NewQualifierFromLog(log),
MountConditions: MountConditionsFromLog(log),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
MountConditions: newMountConditionsFromLog(log),
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 {
o, _ := other.(*Remount)
if r.Qualifier.Equals(o.Qualifier) {
if r.MountPoint == o.MountPoint {
return r.MountConditions.Less(o.MountConditions)
}
if 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)
}
@ -135,3 +210,15 @@ func (r *Remount) Equals(other any) bool {
r.MountConditions.Equals(o.MountConditions) &&
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
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 {
RuleBase
Qualifier
Access string
Access []string
Type string
Label string
Name string
}
func MqueueFromLog(log map[string]string) ApparmorRule {
func newMqueueFromLog(log map[string]string) Rule {
mqueueType := "posix"
if strings.Contains(log["class"], "posix") {
mqueueType = "posix"
@ -22,29 +39,53 @@ func MqueueFromLog(log map[string]string) ApparmorRule {
mqueueType = "sysv"
}
return &Mqueue{
Qualifier: NewQualifierFromLog(log),
Access: toAccess(log["requested"]),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(MQUEUE, log["requested"])),
Type: mqueueType,
Label: log["label"],
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 {
o, _ := other.(*Mqueue)
if r.Qualifier.Equals(o.Qualifier) {
if r.Access == o.Access {
if r.Type == o.Type {
return r.Label < o.Label
if len(r.Access) != len(o.Access) {
return len(r.Access) < len(o.Access)
}
if 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)
}
func (r *Mqueue) Equals(other any) bool {
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
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 {
Source string
Destination 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 {
return r.Source == other.Source && r.Destination == other.Destination &&
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 {
RuleBase
Qualifier
AddressExpr
Domain string
Type string
Protocol string
AddressExpr
}
func NetworkFromLog(log map[string]string) ApparmorRule {
func newNetworkFromLog(log map[string]string) Rule {
return &Network{
Qualifier: NewQualifierFromLog(log),
AddressExpr: AddressExpr{
Source: log["laddr"],
Destination: log["faddr"],
Port: log["lport"],
},
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
AddressExpr: newAddressExprFromLog(log),
Domain: log["family"],
Type: log["sock_type"],
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 {
o, _ := other.(*Network)
if r.Qualifier.Equals(o.Qualifier) {
if r.Domain == o.Domain {
if r.Type == o.Type {
return r.Protocol < o.Protocol
if r.Domain != o.Domain {
return r.Domain < o.Domain
}
if 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)
}
@ -67,3 +117,15 @@ func (r *Network) Equals(other any) bool {
r.Protocol == o.Protocol && r.AddressExpr.Equals(o.AddressExpr) &&
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
const PIVOTROOT = "pivot_root"
type PivotRoot struct {
RuleBase
Qualifier
OldRoot string
NewRoot string
TargetProfile string
}
func PivotRootFromLog(log map[string]string) ApparmorRule {
func newPivotRootFromLog(log map[string]string) Rule {
return &PivotRoot{
Qualifier: NewQualifierFromLog(log),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
OldRoot: log["srcname"],
NewRoot: log["name"],
TargetProfile: "",
}
}
func (r *PivotRoot) Validate() error {
return nil
}
func (r *PivotRoot) Less(other any) bool {
o, _ := other.(*PivotRoot)
if r.Qualifier.Equals(o.Qualifier) {
if r.OldRoot == o.OldRoot {
if r.NewRoot == o.NewRoot {
return r.TargetProfile < o.TargetProfile
if r.OldRoot != o.OldRoot {
return r.OldRoot < o.OldRoot
}
if 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)
}
@ -40,3 +48,15 @@ func (r *PivotRoot) Equals(other any) bool {
r.TargetProfile == o.TargetProfile &&
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
import (
"bytes"
"reflect"
"fmt"
"maps"
"slices"
"sort"
"strings"
"github.com/roddhjav/apparmor.d/pkg/paths"
)
// Default Apparmor magic directory: /etc/apparmor.d/.
var MagicRoot = paths.New("/etc/apparmor.d")
const (
PROFILE Kind = "profile"
// AppArmorProfiles represents a full set of apparmor profiles
type AppArmorProfiles map[string]*AppArmorProfile
tokATTRIBUTES = "xattrs"
tokFLAGS = "flags"
)
// ApparmorProfile represents a full apparmor profile.
// Warning: close to the BNF grammar of apparmor profile but not exactly the same (yet):
// - Some rules are not supported yet (subprofile, hat...)
// - The structure is simplified as it only aims at writing profile, not parsing it.
type AppArmorProfile struct {
Preamble
Profile
func init() {
requirements[PROFILE] = requirement{
tokFLAGS: {
"enforce", "complain", "kill", "default_allow", "unconfined",
"prompt", "audit", "mediate_deleted", "attach_disconnected",
"attach_disconneced.path=", "chroot_relative", "debug",
"interruptible", "kill", "kill.signal=",
},
}
}
// Preamble section of a profile
type Preamble struct {
Abi []Abi
Includes []Include
Aliases []Alias
Variables []Variable
}
// Profile section of a profile
// Profile represents a single AppArmor profile.
type Profile struct {
RuleBase
Header
Rules Rules
}
// Header represents the header of a profile.
type Header struct {
Name string
Attachments []string
Attributes map[string]string
Flags []string
Rules Rules
}
// ApparmorRule generic interface
type ApparmorRule interface {
Less(other any) bool
Equals(other any) bool
}
type Rules []ApparmorRule
func NewAppArmorProfile() *AppArmorProfile {
return &AppArmorProfile{}
}
// String returns the formatted representation of a profile as a string
func (p *AppArmorProfile) String() string {
var res bytes.Buffer
err := tmplAppArmorProfile.Execute(&res, p)
if err != nil {
return err.Error()
func newHeader(rule []string) (Header, error) {
if len(rule) == 0 {
return Header{}, nil
}
return res.String()
if rule[len(rule)-1] == "{" {
rule = rule[:len(rule)-1]
}
if rule[0] == PROFILE.Tok() {
rule = rule[1:]
}
delete := []int{}
flags := []string{}
attributes := make(map[string]string)
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)
}
name, attachments := "", []string{}
if len(rule) >= 1 {
name = rule[0]
if len(rule) > 1 {
attachments = rule[1:]
}
}
return Header{
Name: name,
Attachments: attachments,
Attributes: attributes,
Flags: flags,
}, nil
}
// AddRule adds a new rule to the profile from a log map
func (p *AppArmorProfile) AddRule(log map[string]string) {
func (r *Profile) Validate() error {
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
switch log["error"] {
case "-2":
@ -78,134 +212,27 @@ func (p *AppArmorProfile) AddRule(log map[string]string) {
}
case "-13":
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") {
p.Flags = append(p.Flags, "attach_disconnected")
}
default:
}
switch log["class"] {
case "cap":
p.Rules = append(p.Rules, CapabilityFromLog(log))
case "net":
if log["family"] == "unix" {
p.Rules = append(p.Rules, UnixFromLog(log))
} 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))
done := false
for _, key := range []string{"class", "family", "operation"} {
if newRule, ok := newLogMap[log[key]]; ok {
p.Rules = append(p.Rules, newRule(log))
done = true
break
}
}
case "posix_mqueue", "sysv_mqueue":
p.Rules = append(p.Rules, MqueueFromLog(log))
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 !done {
if strings.Contains(log["operation"], "dbus") {
p.Rules = append(p.Rules, DbusFromLog(log))
} else if log["family"] == "unix" {
p.Rules = append(p.Rules, UnixFromLog(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:]...)...)
}
p.Rules = append(p.Rules, newDbusFromLog(log))
} else {
fmt.Printf("unknown log type: %s", log)
}
}
}

View file

@ -6,328 +6,128 @@ package aa
import (
"reflect"
"strings"
"testing"
"github.com/roddhjav/apparmor.d/pkg/paths"
)
func readprofile(path string) string {
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) {
func TestProfile_AddRule(t *testing.T) {
tests := []struct {
name string
log map[string]string
want *AppArmorProfile
want *Profile
}{
{
name: "capability",
log: capability1Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{capability1},
},
want: &Profile{
Rules: Rules{capability1},
},
},
{
name: "network",
log: network1Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{network1},
},
want: &Profile{
Rules: Rules{network1},
},
},
{
name: "mount",
log: mount2Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{mount2},
},
want: &Profile{
Rules: Rules{mount2},
},
},
{
name: "signal",
log: signal1Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{signal1},
},
want: &Profile{
Rules: Rules{signal1},
},
},
{
name: "ptrace",
log: ptrace2Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{ptrace2},
},
want: &Profile{
Rules: Rules{ptrace2},
},
},
{
name: "unix",
log: unix1Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{unix1},
},
want: &Profile{
Rules: Rules{unix1},
},
},
{
name: "dbus",
log: dbus2Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{dbus2},
},
want: &Profile{
Rules: Rules{dbus2},
},
},
{
name: "file",
log: file2Log,
want: &AppArmorProfile{
Profile: Profile{
Rules: []ApparmorRule{file2},
},
want: &Profile{
Rules: Rules{file2},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewAppArmorProfile()
got := &Profile{}
got.AddRule(tt.log)
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 {
name string
origin *AppArmorProfile
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
Attachments []string
want string
}{
{
name: "aa-status",
p: &AppArmorProfile{
Preamble: Preamble{
Abi: []Abi{{IsMagic: true, Path: "abi/3.0"}},
Includes: []Include{{IsMagic: true, Path: "tunables/global"}},
Variables: []Variable{{
Name: "exec_path",
Values: []string{"@{bin}/aa-status", "@{bin}/apparmor_status"},
}},
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}",
},
Profile: Profile{
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"},
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}",
},
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 {
t.Run(tt.name, func(t *testing.T) {
tt.p.Sort()
tt.p.MergeRules()
tt.p.Format()
if got := tt.p.String(); "\n"+got != tt.want {
t.Errorf("AppArmorProfile = |%v|, want |%v|", "\n"+got, tt.want)
p := &Profile{}
p.Attachments = tt.Attachments
if got := p.GetAttachments(); got != tt.want {
t.Errorf("Profile.GetAttachments() = %v, want %v", got, tt.want)
}
})
}

View file

@ -4,33 +4,69 @@
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 {
RuleBase
Qualifier
Access string
Access []string
Peer string
}
func PtraceFromLog(log map[string]string) ApparmorRule {
func newPtraceFromLog(log map[string]string) Rule {
return &Ptrace{
Qualifier: NewQualifierFromLog(log),
Access: toAccess(log["requested_mask"]),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(PTRACE, log["requested_mask"])),
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 {
o, _ := other.(*Ptrace)
if r.Qualifier.Equals(o.Qualifier) {
if r.Access == o.Access {
return r.Peer == o.Peer
if len(r.Access) != len(o.Access) {
return len(r.Access) < len(o.Access)
}
return r.Access < o.Access
if r.Peer != o.Peer {
return r.Peer == o.Peer
}
return r.Qualifier.Less(o.Qualifier)
}
func (r *Ptrace) Equals(other any) bool {
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)
}
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
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 {
RuleBase
Key string
Op 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 {
o, _ := other.(*Rlimit)
if r.Key == o.Key {
if r.Op == o.Op {
return r.Value < o.Value
if r.Key != o.Key {
return r.Key < o.Key
}
if 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 {
o, _ := other.(*Rlimit)
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
import (
"strings"
"slices"
)
type Rule struct {
Comment string
NoNewPrivs bool
FileInherit bool
type requirement map[string][]string
type constraint uint
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 {
return false
func (k Kind) Tok() string {
if t, ok := tok[k]; ok {
return t
}
return string(k)
}
func (r *Rule) Equals(other any) bool {
return false
// Rule generic interface for all AppArmor rules
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 Qualifier struct {
Audit bool
AccessType string
Owner bool
NoNewPrivs bool
FileInherit bool
Optional bool
Comment string
Prefix string
Padding string
}
type Rules []Rule
func NewQualifierFromLog(log map[string]string) Qualifier {
owner := false
fsuid, hasFsUID := log["fsuid"]
ouid, hasOuUID := log["ouid"]
isDbus := strings.Contains(log["operation"], "dbus")
if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus {
owner = true
func (r Rules) Validate() error {
for _, rule := range r {
if rule == nil {
continue
}
audit := false
if log["apparmor"] == "AUDIT" {
audit = true
}
fileInherit := false
if log["operation"] == "file_inherit" {
fileInherit = true
}
noNewPrivs := false
optional := false
msg := ""
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
if err := rule.Validate(); err != nil {
return err
}
}
msg = log["info"]
default:
return nil
}
func (r Rules) String() string {
return renderTemplate("rules", r)
}
// Index returns the index of the first occurrence of rule rin r, or -1 if not present.
func (r Rules) Index(item Rule) int {
for idx, rule := range r {
if rule == nil {
continue
}
if rule.Kind() == item.Kind() && rule.Equals(item) {
return idx
}
}
return -1
}
// Replace replaces the elements r[i] by the given rules, and returns the
// modified slice.
func (r Rules) Replace(i int, rules ...Rule) Rules {
return append(r[:i], append(rules, r[i+1:]...)...)
}
// Insert inserts the rules into r at index i, returning the modified slice.
func (r Rules) Insert(i int, rules ...Rule) Rules {
return append(r[:i], append(rules, r[i:]...)...)
}
// Delete removes the elements r[i] from r, returning the modified slice.
func (r Rules) Delete(i int) Rules {
return append(r[:i], r[i+1:]...)
}
func (r Rules) DeleteKind(kind Kind) Rules {
res := make(Rules, 0)
for _, rule := range r {
if rule == nil {
continue
}
if rule.Kind() != kind {
res = append(res, rule)
}
}
return res
}
func (r Rules) Filter(filter Kind) Rules {
res := make(Rules, 0)
for _, rule := range r {
if rule == nil {
continue
}
if rule.Kind() != filter {
res = append(res, rule)
}
}
return res
}
func (r Rules) GetVariables() []*Variable {
res := make([]*Variable, 0)
for _, rule := range r {
switch rule := rule.(type) {
case *Variable:
res = append(res, rule)
}
}
return res
}
func (r Rules) GetIncludes() []*Include {
res := make([]*Include, 0)
for _, rule := range r {
switch rule := rule.(type) {
case *Include:
res = append(res, rule)
}
}
return res
}
// Merge 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 (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
}
return Qualifier{
Audit: audit,
Owner: owner,
NoNewPrivs: noNewPrivs,
FileInherit: fileInherit,
Optional: optional,
Comment: msg,
// If rules are identical, merge them
if r[i].Equals(r[j]) {
r = r.Delete(j)
j--
continue
}
}
func (r Qualifier) Less(other Qualifier) bool {
if r.Owner == other.Owner {
if r.Audit == other.Audit {
return r.AccessType < other.AccessType
// File rule
if typeOfI == FILE && typeOfJ == FILE {
// 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.Audit
}
return other.Owner
}
func (r Qualifier) Equals(other Qualifier) bool {
return r.Audit == other.Audit && r.AccessType == other.AccessType &&
r.Owner == other.Owner && r.NoNewPrivs == other.NoNewPrivs &&
r.FileInherit == other.FileInherit
}
// Preamble specific rules
type Abi struct {
Path string
IsMagic bool
}
func (r Abi) Less(other Abi) bool {
if r.Path == other.Path {
return r.IsMagic == other.IsMagic
}
return r.Path < other.Path
}
func (r Abi) Equals(other Abi) bool {
return r.Path == other.Path && r.IsMagic == other.IsMagic
}
type Alias struct {
Path string
RewrittenPath string
}
func (r Alias) Less(other Alias) bool {
if r.Path == other.Path {
return r.RewrittenPath < other.RewrittenPath
}
return r.Path < other.Path
return r
}
func (r Alias) Equals(other Alias) bool {
return r.Path == other.Path && r.RewrittenPath == other.RewrittenPath
// Sort the rules according to the guidelines:
// 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"
)
func TestRule_FromLog(t *testing.T) {
tests := []struct {
name string
fromLog func(map[string]string) ApparmorRule
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,
},
func TestRules_FromLog(t *testing.T) {
for _, tt := range testRule {
if tt.fromLog == nil {
continue
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.want) {
t.Errorf("RuleFromLog() = %v, want %v", got, tt.want)
if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.rule) {
t.Errorf("RuleFromLog() = %v, want %v", got, tt.rule)
}
})
}
}
func TestRule_Less(t *testing.T) {
tests := []struct {
func TestRules_Validate(t *testing.T) {
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
rule ApparmorRule
other ApparmorRule
want bool
fromLog func(map[string]string) Rule
log map[string]string
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",
rule: include1,
other: includeLocal1,
want: true,
oLess: includeLocal1,
wLessErr: false,
oEqual: includeLocal1,
wEqualErr: false,
wString: "include <abstraction/base>",
},
{
name: "include2",
rule: include1,
other: include2,
want: true,
oLess: include2,
wLessErr: false,
wString: "include <abstraction/base>",
},
{
name: "include3",
rule: include1,
other: include3,
want: false,
name: "include-local",
rule: includeLocal1,
oLess: include1,
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",
rule: rlimit1,
other: rlimit2,
want: false,
oLess: rlimit2,
wLessErr: false,
oEqual: rlimit1,
wEqualErr: true,
wString: "set rlimit nproc <= 200,",
},
{
name: "rlimit2",
rule: rlimit2,
other: rlimit2,
want: false,
oLess: rlimit2,
wLessErr: false,
wString: "set rlimit cpu <= 2,",
},
{
name: "rlimit3",
rule: rlimit1,
other: rlimit3,
want: false,
rule: rlimit3,
oLess: rlimit1,
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,
other: capability2,
want: true,
oLess: capability2,
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",
fromLog: newNetworkFromLog,
log: network1Log,
rule: network1,
other: network2,
want: false,
wValidErr: true,
oLess: network2,
wLessErr: false,
oEqual: network1,
wEqualErr: true,
wString: "network netlink raw,",
},
{
name: "mount",
fromLog: newMountFromLog,
log: mount1Log,
rule: mount1,
other: mount2,
want: false,
oEqual: mount2,
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",
fromLog: newUmountFromLog,
log: umount1Log,
rule: umount1,
other: umount2,
want: true,
oLess: umount2,
wLessErr: true,
oEqual: umount1,
wEqualErr: true,
wString: "umount /,",
},
{
name: "pivot_root1",
rule: pivotroot2,
other: pivotroot1,
want: true,
fromLog: newPivotRootFromLog,
log: pivotroot1Log,
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",
rule: pivotroot1,
other: pivotroot3,
want: false,
oLess: pivotroot3,
wLessErr: false,
wString: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,",
},
{
name: "change_profile1",
fromLog: newChangeProfileFromLog,
log: changeprofile1Log,
rule: changeprofile1,
other: changeprofile2,
want: false,
oLess: changeprofile2,
wLessErr: false,
wString: "change_profile -> systemd-user,",
},
{
name: "change_profile2",
rule: changeprofile1,
other: changeprofile3,
want: true,
rule: changeprofile2,
oLess: changeprofile3,
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",
fromLog: newSignalFromLog,
log: signal1Log,
rule: signal1,
other: signal2,
want: true,
oLess: signal2,
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,
other: ptrace2,
want: true,
oLess: ptrace2,
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,
other: ptrace1,
want: false,
oLess: ptrace1,
wLessErr: false,
oEqual: ptrace1,
wEqualErr: false,
wString: "ptrace readby peer=systemd-journald,",
},
{
name: "unix",
fromLog: newUnixFromLog,
log: unix1Log,
rule: unix1,
other: unix1,
want: false,
oLess: unix1,
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",
fromLog: newDbusFromLog,
log: dbus1Log,
rule: dbus1,
other: dbus1,
want: false,
oLess: dbus1,
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",
rule: dbus2,
other: dbus3,
want: false,
oLess: dbus3,
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",
fromLog: newFileFromLog,
log: file1Log,
rule: file1,
other: file2,
want: true,
oLess: file2,
wLessErr: true,
oEqual: file2,
wEqualErr: false,
wString: "/usr/share/poppler/cMap/Identity-H r,",
},
{
name: "file/empty",
rule: &File{},
other: &File{},
want: false,
oLess: &File{},
wLessErr: false,
wString: " ,",
},
{
name: "file/equal",
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
want: false,
oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
wLessErr: false,
wString: "/usr/share/poppler/cMap/Identity-H ,",
},
{
name: "file/owner",
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Qualifier: Qualifier{Owner: true}},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
want: false,
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Owner: true},
oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
wLessErr: true,
wString: "owner /usr/share/poppler/cMap/Identity-H ,",
},
{
name: "file/access",
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "w"},
want: true,
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}},
oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"w"}},
wLessErr: false,
wString: "/usr/share/poppler/cMap/Identity-H r,",
},
{
name: "file/close",
rule: &File{Path: "/usr/share/poppler/cMap/"},
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
want: true,
oLess: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
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
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 {
RuleBase
Qualifier
Access string
Set string
Access []string
Set []string
Peer string
}
func SignalFromLog(log map[string]string) ApparmorRule {
func newSignalFromLog(log map[string]string) Rule {
return &Signal{
Qualifier: NewQualifierFromLog(log),
Access: toAccess(log["requested_mask"]),
Set: log["signal"],
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(SIGNAL, log["requested_mask"])),
Set: []string{log["signal"]},
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 {
o, _ := other.(*Signal)
if r.Qualifier.Equals(o.Qualifier) {
if r.Access == o.Access {
if r.Set == o.Set {
if len(r.Access) != len(o.Access) {
return len(r.Access) < len(o.Access)
}
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.Set < o.Set
}
return r.Access < o.Access
}
return r.Qualifier.Less(o.Qualifier)
}
func (r *Signal) Equals(other any) bool {
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)
}
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 (
"embed"
"fmt"
"reflect"
"strings"
"text/template"
)
// Default indentation for apparmor profile (2 spaces)
const indentation = " "
var (
// Default indentation for apparmor profile (2 spaces)
Indentation = " "
// The current indentation level
IndentationLevel = 0
//go:embed templates/*.j2
//go:embed templates/rule/*.j2
tmplFiles embed.FS
// The functions available in the template
tmplFunctionMap = template.FuncMap{
"typeof": typeOf,
"kindof": kindOf,
"join": join,
"cjoin": cjoin,
"indent": indent,
"overindent": indentDbus,
"setindent": setindent,
}
// The apparmor profile template
tmplAppArmorProfile = generateTemplate()
// The apparmor templates
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
requestedMaskToAccess = map[string]string{
maskToAccess = map[string]string{
"a": "w",
"ac": "w",
"c": "w",
"d": "w",
"m": "rm",
"ra": "rw",
"wc": "w",
"wd": "w",
"wr": "rw",
"wrc": "rw",
"wrd": "rw",
"x": "rix",
"x": "ix",
}
// The order the apparmor rules should be sorted
ruleAlphabet = []string{
"include",
"rlimit",
"capability",
"network",
"mount",
"remount",
"umount",
"pivotroot",
"changeprofile",
"mqueue",
"signal",
"ptrace",
"unix",
"userns",
"iouring",
"dbus",
"file",
ruleAlphabet = []Kind{
INCLUDE,
ALL,
RLIMIT,
USERNS,
CAPABILITY,
NETWORK,
MOUNT,
REMOUNT,
UMOUNT,
PIVOTROOT,
CHANGEPROFILE,
MQUEUE,
IOURING,
SIGNAL,
PTRACE,
UNIX,
DBUS,
FILE,
LINK,
PROFILE,
HAT,
"include_if_exists",
}
ruleWeights = map[string]int{}
ruleWeights = generateWeights(ruleAlphabet)
// The order the apparmor file rules should be sorted
fileAlphabet = []string{
@ -91,23 +114,65 @@ var (
"@{PROC}", // 10. Proc files
"/dev", // 11. Dev files
"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 {
res := template.New("profile.j2").Funcs(tmplFunctionMap)
res = template.Must(res.ParseFS(tmplFiles, "templates/*.j2"))
func init() {
requirementsWeights = generateRequirementsWeights(requirements)
}
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
}
func init() {
for i, r := range fileAlphabet {
fileWeights[r] = i
func renderTemplate(name Kind, data any) string {
var res strings.Builder
template, ok := tmpl[name]
if !ok {
panic("template '" + name.String() + "' not found")
}
for i, r := range ruleAlphabet {
ruleWeights[r] = i
err := template.Execute(&res, data)
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 {
@ -125,20 +190,48 @@ func join(i any) string {
}
}
func typeOf(i any) string {
return strings.TrimPrefix(reflect.TypeOf(i).String(), "*aa.")
func cjoin(i any) string {
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 {
return strings.ToLower(strings.TrimPrefix(i.String(), "*aa."))
func kindOf(i any) string {
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 {
return indentation + s
return strings.Repeat(Indentation, IndentationLevel) + s
}
func indentDbus(s string) string {
return indentation + " " + s
return strings.Join([]string{Indentation, s}, " ")
}
func getLetterIn(alphabet []string, in string) string {
@ -149,10 +242,3 @@ func getLetterIn(alphabet []string, in string) string {
}
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> */ -}}
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
{{- range .Abi -}}
{{- if .IsMagic -}}
{{ "abi <" }}{{ .Path }}{{ ">,\n" }}
{{- else -}}
{{ "abi \"" }}{{ .Path }}{{ "\",\n" }}
{{- end }}
{{ end -}}
{{- define "profile" -}}
{{- 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" -}}
{{- with .Name -}}
{{ " " }}{{ . }}
@ -36,260 +17,11 @@
{{- with .Flags -}}
{{ " flags=(" }}{{ join . }}{{ ")" }}
{{- end -}}
{{ " {\n" }}
{{- end -}}
{{- $oldtype := "" -}}
{{- range .Rules -}}
{{- $type := typeof . -}}
{{- if eq $type "Rule" -}}
{{- "\n" -}}
{{- 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" -}}
{{- " {\n" -}}
{{- setindent "++" -}}
{{- template "rules" .Rules -}}
{{- setindent "--" -}}
{{- indent "}" -}}
{{- 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" -}}
{{- "include" -}}
{{- if .IfExists -}}
@ -8,4 +12,5 @@
{{- else -}}
{{ " \"" }}{{ .Path }}{{ "\"" }}
{{- 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 "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" -}}
{{- with .Prefix -}}
{{ . }}
{{- end -}}
{{- if .Owner -}}
{{- "owner " -}}
{{- end -}}
{{- if .Audit -}}
{{- "audit " -}}
{{- 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
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 {
RuleBase
Qualifier
Access string
Access []string
Type string
Protocol string
Address string
Label string
Attr string
Opt string
Peer string
PeerLabel string
PeerAddr string
}
func UnixFromLog(log map[string]string) ApparmorRule {
func newUnixFromLog(log map[string]string) Rule {
return &Unix{
Qualifier: NewQualifierFromLog(log),
Access: toAccess(log["requested_mask"]),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Access: Must(toAccess(UNIX, log["requested_mask"])),
Type: log["sock_type"],
Protocol: log["protocol"],
Address: log["addr"],
Label: log["peer_label"],
Label: log["label"],
Attr: log["attr"],
Opt: log["opt"],
Peer: log["peer"],
PeerLabel: log["peer"],
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 {
o, _ := other.(*Unix)
if r.Qualifier.Equals(o.Qualifier) {
if r.Access == 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 len(r.Access) != len(o.Access) {
return len(r.Access) < len(o.Access)
}
if 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)
}
func (r *Unix) Equals(other any) bool {
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.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
const USERNS Kind = "userns"
type Userns struct {
RuleBase
Qualifier
Create bool
}
func UsernsFromLog(log map[string]string) ApparmorRule {
func newUsernsFromLog(log map[string]string) Rule {
return &Userns{
Qualifier: NewQualifierFromLog(log),
RuleBase: newRuleFromLog(log),
Qualifier: newQualifierFromLog(log),
Create: true,
}
}
func (r *Userns) Validate() error {
return nil
}
func (r *Userns) Less(other any) bool {
o, _ := other.(*Userns)
if r.Qualifier.Equals(o.Qualifier) {
if r.Create != o.Create {
return r.Create
}
return r.Qualifier.Less(o.Qualifier)
@ -28,3 +36,15 @@ func (r *Userns) Equals(other any) bool {
o, _ := other.(*Userns)
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
func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles {
profiles := make(aa.AppArmorProfiles, 0)
func (aaLogs AppArmorLogs) ParseToProfiles() map[string]*aa.Profile {
profiles := make(map[string]*aa.Profile, 0)
for _, log := range aaLogs {
name := ""
if strings.Contains(log["operation"], "dbus") {
@ -208,8 +208,7 @@ func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles {
}
if _, ok := profiles[name]; !ok {
profile := &aa.AppArmorProfile{}
profile.Name = name
profile := &aa.Profile{Header: aa.Header{Name: name}}
profile.AddRule(log)
profiles[name] = profile
} else {

View file

@ -292,44 +292,40 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) {
tests := []struct {
name string
aaLogs AppArmorLogs
want aa.AppArmorProfiles
want map[string]*aa.Profile
}{
{
name: "",
aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...),
want: aa.AppArmorProfiles{
"kmod": &aa.AppArmorProfile{
Profile: aa.Profile{
Name: "kmod",
want: map[string]*aa.Profile{
"kmod": {
Header: aa.Header{Name: "kmod"},
Rules: aa.Rules{
&aa.Unix{
Qualifier: aa.Qualifier{FileInherit: true},
Access: "send receive",
RuleBase: aa.RuleBase{FileInherit: true},
Access: []string{"send", "receive"},
Type: "stream",
Protocol: "0",
},
&aa.Unix{
Qualifier: aa.Qualifier{FileInherit: true},
Access: "send receive",
RuleBase: aa.RuleBase{FileInherit: true},
Access: []string{"send", "receive"},
Type: "stream",
Protocol: "0",
},
},
},
},
"power-profiles-daemon": &aa.AppArmorProfile{
Profile: aa.Profile{
Name: "power-profiles-daemon",
"power-profiles-daemon": {
Header: aa.Header{Name: "power-profiles-daemon"},
Rules: aa.Rules{
&aa.Dbus{
Access: "send",
Access: []string{"send"},
Bus: "system",
Name: "org.freedesktop.DBus",
Path: "/org/freedesktop/DBus",
Interface: "org.freedesktop.DBus",
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 {
return regAbi4To3.Replace(profile)
func (b ABI3) Apply(opt *Option, profile string) (string, error) {
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{}
matches := regFlags.FindStringSubmatch(profile)
if len(matches) != 0 {
flags = strings.Split(matches[1], ",")
if slices.Contains(flags, "complain") {
return profile
return profile, nil
}
}
flags = append(flags, "complain")
@ -44,5 +44,5 @@ func (b Complain) Apply(profile string) string {
// Remove all flags definition, then set manifest' flags
profile = regFlags.ReplaceAllLiteralString(profile, "")
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags)
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags), nil
}

View file

@ -7,6 +7,7 @@ package builder
import (
"fmt"
"github.com/roddhjav/apparmor.d/pkg/paths"
"github.com/roddhjav/apparmor.d/pkg/prebuild/cfg"
)
@ -21,7 +22,20 @@ var (
// Main directive interface
type Builder interface {
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) {
@ -37,3 +51,15 @@ func Register(names ...string) {
func RegisterBuilder(d Builder) {
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 (
"slices"
"testing"
"github.com/roddhjav/apparmor.d/pkg/prebuild/cfg"
)
func TestBuilder_Apply(t *testing.T) {
@ -15,6 +17,7 @@ func TestBuilder_Apply(t *testing.T) {
b Builder
profile string
want string
wantErr bool
}{
{
name: "abi3",
@ -215,7 +218,7 @@ func TestBuilder_Apply(t *testing.T) {
}`,
},
{
name: "userspace-1",
name: "userspace-2",
b: Builders["userspace"],
profile: `
profile foo /usr/bin/foo {
@ -237,7 +240,13 @@ func TestBuilder_Apply(t *testing.T) {
}
for _, tt := range tests {
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)
}
})
@ -257,7 +266,6 @@ func TestRegister(t *testing.T) {
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Register(tt.names...)
for _, name := range tt.names {

View file

@ -31,6 +31,6 @@ func init() {
})
}
func (b Dev) Apply(profile string) string {
return regDev.Replace(profile)
func (b Dev) Apply(opt *Option, profile string) (string, error) {
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)
if len(matches) == 0 {
return profile
return profile, nil
}
flags := strings.Split(matches[1], ",")
idx := slices.Index(flags, "complain")
if idx == -1 {
return profile
return profile, nil
}
flags = slices.Delete(flags, idx, idx+1)
strFlags := "{"
@ -43,5 +43,5 @@ func (b Enforce) Apply(profile string) string {
// Remove all flags definition, then set new flags
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 {
return regFullSystemPolicy.Replace(profile)
func (b FullSystemPolicy) Apply(opt *Option, profile string) (string, error) {
return regFullSystemPolicy.Replace(profile), nil
}

View file

@ -29,15 +29,26 @@ func init() {
})
}
func (b Userspace) Apply(profile string) string {
p := aa.DefaultTunables()
p.ParseVariables(profile)
p.ResolveAttachments()
att := p.NestAttachments()
func (b Userspace) Apply(opt *Option, profile string) (string, error) {
if ok, _ := opt.File.IsInsideDir(cfg.RootApparmord.Join("abstractions")); ok {
return profile, nil
}
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)
if len(matches) > 0 {
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
type Directive interface {
cfg.BaseInterface
Apply(opt *Option, profile string) string
Apply(opt *Option, profile string) (string, error)
}
// Directive options
@ -72,14 +72,18 @@ func RegisterDirective(d Directive) {
Directives[d.Name()] = d
}
func Run(file *paths.Path, profile string) string {
func Run(file *paths.Path, profile string) (string, error) {
var err error
for _, match := range regDirective.FindAllStringSubmatch(profile, -1) {
opt := NewOption(file, match)
drtv, ok := Directives[opt.Name]
if !ok {
panic(fmt.Sprintf("Unknown directive: %s", opt.Name))
return "", fmt.Errorf("Unknown directive '%s' in %s", opt.Name, opt.File)
}
profile = 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
profile string
want string
wantErr bool
}{
{
name: "none",
@ -86,7 +87,12 @@ func TestRun(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Run(tt.file, tt.profile); got != tt.want {
got, err := Run(tt.file, tt.profile)
if (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Run() = %v, want %v", got, tt.want)
}
})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -83,10 +83,14 @@ func Build() error {
if err != nil {
return err
}
for _, b := range builder.Builds {
profile = b.Apply(profile)
profile, err = builder.Run(file, 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 {
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>,
alias /mnt/usr -> /usr,
@ -18,13 +19,13 @@ profile foo @{exec_path} xattrs=(security.tagged=allowed) flags=(complain attach
network inet 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}/,
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),

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>