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:
commit
89abbae6bd
90 changed files with 4995 additions and 2012 deletions
|
|
@ -6,7 +6,7 @@ abi <abi/3.0>,
|
||||||
|
|
||||||
include <tunables/global>
|
include <tunables/global>
|
||||||
|
|
||||||
profile default-sudo @{exec_path} {
|
profile default-sudo {
|
||||||
include <abstractions/base>
|
include <abstractions/base>
|
||||||
include <abstractions/app/sudo>
|
include <abstractions/app/sudo>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ abi <abi/3.0>,
|
||||||
|
|
||||||
include <tunables/global>
|
include <tunables/global>
|
||||||
|
|
||||||
profile systemd-service @{exec_path} flags=(attach_disconnected) {
|
profile systemd-service flags=(attach_disconnected) {
|
||||||
include <abstractions/base>
|
include <abstractions/base>
|
||||||
include <abstractions/consoles>
|
include <abstractions/consoles>
|
||||||
include <abstractions/nameservice-strict>
|
include <abstractions/nameservice-strict>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ profile aa-status @{exec_path} {
|
||||||
capability dac_read_search,
|
capability dac_read_search,
|
||||||
capability sys_ptrace,
|
capability sys_ptrace,
|
||||||
|
|
||||||
ptrace (read),
|
ptrace read,
|
||||||
|
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,11 @@ func aaLog(logger string, path string, profile string) error {
|
||||||
aaLogs := logs.NewApparmorLogs(file, profile)
|
aaLogs := logs.NewApparmorLogs(file, profile)
|
||||||
if rules {
|
if rules {
|
||||||
profiles := aaLogs.ParseToProfiles()
|
profiles := aaLogs.ParseToProfiles()
|
||||||
for _, profile := range profiles {
|
for _, p := range profiles {
|
||||||
profile.MergeRules()
|
p.Merge()
|
||||||
profile.Sort()
|
p.Sort()
|
||||||
profile.Format()
|
p.Format()
|
||||||
fmt.Print(profile.String() + "\n")
|
fmt.Print(p.String() + "\n\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(aaLogs.String())
|
fmt.Print(aaLogs.String())
|
||||||
|
|
|
||||||
37
pkg/aa/all.go
Normal file
37
pkg/aa/all.go
Normal 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
105
pkg/aa/apparmor.go
Normal 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
250
pkg/aa/apparmor_test.go
Normal 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
121
pkg/aa/base.go
Normal 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
42
pkg/aa/blocks.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -4,27 +4,72 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
type Capability struct {
|
import (
|
||||||
Qualifier
|
"fmt"
|
||||||
Name string
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CAPABILITY Kind = "capability"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[CAPABILITY] = requirement{
|
||||||
|
"name": {
|
||||||
|
"audit_control", "audit_read", "audit_write", "block_suspend", "bpf",
|
||||||
|
"checkpoint_restore", "chown", "dac_override", "dac_read_search",
|
||||||
|
"fowner", "fsetid", "ipc_lock", "ipc_owner", "kill", "lease",
|
||||||
|
"linux_immutable", "mac_admin", "mac_override", "mknod", "net_admin",
|
||||||
|
"net_bind_service", "net_broadcast", "net_raw", "perfmon", "setfcap",
|
||||||
|
"setgid", "setpcap", "setuid", "sys_admin", "sys_boot", "sys_chroot",
|
||||||
|
"sys_module", "sys_nice", "sys_pacct", "sys_ptrace", "sys_rawio",
|
||||||
|
"sys_resource", "sys_time", "sys_tty_config", "syslog", "wake_alarm",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CapabilityFromLog(log map[string]string) ApparmorRule {
|
type Capability struct {
|
||||||
|
RuleBase
|
||||||
|
Qualifier
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCapabilityFromLog(log map[string]string) Rule {
|
||||||
return &Capability{
|
return &Capability{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
Name: log["capname"],
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Names: Must(toValues(CAPABILITY, "name", log["capname"])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Capability) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "name", r.Names); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Capability) Less(other any) bool {
|
func (r *Capability) Less(other any) bool {
|
||||||
o, _ := other.(*Capability)
|
o, _ := other.(*Capability)
|
||||||
if r.Name == o.Name {
|
for i := 0; i < len(r.Names) && i < len(o.Names); i++ {
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
if r.Names[i] != o.Names[i] {
|
||||||
|
return r.Names[i] < o.Names[i]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return r.Name < o.Name
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Capability) Equals(other any) bool {
|
func (r *Capability) Equals(other any) bool {
|
||||||
o, _ := other.(*Capability)
|
o, _ := other.(*Capability)
|
||||||
return r.Name == o.Name && r.Qualifier.Equals(o.Qualifier)
|
return slices.Equal(r.Names, o.Names) && r.Qualifier.Equals(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Capability) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Capability) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Capability) Kind() Kind {
|
||||||
|
return CAPABILITY
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,69 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const CHANGEPROFILE Kind = "change_profile"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[CHANGEPROFILE] = requirement{
|
||||||
|
"mode": []string{"safe", "unsafe"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ChangeProfile struct {
|
type ChangeProfile struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
ExecMode string
|
ExecMode string
|
||||||
Exec string
|
Exec string
|
||||||
ProfileName string
|
ProfileName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChangeProfileFromLog(log map[string]string) ApparmorRule {
|
func newChangeProfileFromLog(log map[string]string) Rule {
|
||||||
return &ChangeProfile{
|
return &ChangeProfile{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
|
Qualifier: newQualifierFromLog(log),
|
||||||
ExecMode: log["mode"],
|
ExecMode: log["mode"],
|
||||||
Exec: log["exec"],
|
Exec: log["exec"],
|
||||||
ProfileName: log["target"],
|
ProfileName: log["target"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ChangeProfile) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "mode", []string{r.ExecMode}); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ChangeProfile) Less(other any) bool {
|
func (r *ChangeProfile) Less(other any) bool {
|
||||||
o, _ := other.(*ChangeProfile)
|
o, _ := other.(*ChangeProfile)
|
||||||
if r.ExecMode == o.ExecMode {
|
if r.ExecMode != o.ExecMode {
|
||||||
if r.Exec == o.Exec {
|
return r.ExecMode < o.ExecMode
|
||||||
return r.ProfileName < o.ProfileName
|
}
|
||||||
}
|
if r.Exec != o.Exec {
|
||||||
return r.Exec < o.Exec
|
return r.Exec < o.Exec
|
||||||
}
|
}
|
||||||
return r.ExecMode < o.ExecMode
|
if r.ProfileName != o.ProfileName {
|
||||||
|
return r.ProfileName < o.ProfileName
|
||||||
|
}
|
||||||
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ChangeProfile) Equals(other any) bool {
|
func (r *ChangeProfile) Equals(other any) bool {
|
||||||
o, _ := other.(*ChangeProfile)
|
o, _ := other.(*ChangeProfile)
|
||||||
return r.ExecMode == o.ExecMode && r.Exec == o.Exec && r.ProfileName == o.ProfileName
|
return r.ExecMode == o.ExecMode && r.Exec == o.Exec &&
|
||||||
|
r.ProfileName == o.ProfileName && r.Qualifier.Equals(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChangeProfile) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChangeProfile) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChangeProfile) Kind() Kind {
|
||||||
|
return CHANGEPROFILE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
120
pkg/aa/convert.go
Normal file
120
pkg/aa/convert.go
Normal 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
90
pkg/aa/convert_test.go
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,17 +5,40 @@
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// Comment
|
||||||
|
comment1 = &Comment{RuleBase: RuleBase{Comment: "comment", IsLineRule: true}}
|
||||||
|
comment2 = &Comment{RuleBase: RuleBase{Comment: "another comment", IsLineRule: true}}
|
||||||
|
|
||||||
|
// Abi
|
||||||
|
abi1 = &Abi{IsMagic: true, Path: "abi/4.0"}
|
||||||
|
abi2 = &Abi{IsMagic: true, Path: "abi/3.0"}
|
||||||
|
|
||||||
|
// Alias
|
||||||
|
alias1 = &Alias{Path: "/mnt/usr", RewrittenPath: "/usr"}
|
||||||
|
alias2 = &Alias{Path: "/mnt/var", RewrittenPath: "/var"}
|
||||||
|
|
||||||
// Include
|
// Include
|
||||||
include1 = &Include{IsMagic: true, Path: "abstraction/base"}
|
include1 = &Include{IsMagic: true, Path: "abstraction/base"}
|
||||||
include2 = &Include{IsMagic: false, Path: "abstraction/base"}
|
include2 = &Include{IsMagic: false, Path: "abstraction/base"}
|
||||||
include3 = &Include{IfExists: true, IsMagic: true, Path: "abstraction/base"}
|
|
||||||
includeLocal1 = &Include{IfExists: true, IsMagic: true, Path: "local/foo"}
|
includeLocal1 = &Include{IfExists: true, IsMagic: true, Path: "local/foo"}
|
||||||
|
|
||||||
|
// Variable
|
||||||
|
variable1 = &Variable{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}, Define: true}
|
||||||
|
variable2 = &Variable{Name: "exec_path", Values: []string{"@{bin}/foo", "@{lib}/foo"}, Define: true}
|
||||||
|
|
||||||
|
// All
|
||||||
|
all1 = &All{}
|
||||||
|
all2 = &All{RuleBase: RuleBase{Comment: "comment"}}
|
||||||
|
|
||||||
// Rlimit
|
// Rlimit
|
||||||
rlimit1 = &Rlimit{Key: "nproc", Op: "<=", Value: "200"}
|
rlimit1 = &Rlimit{Key: "nproc", Op: "<=", Value: "200"}
|
||||||
rlimit2 = &Rlimit{Key: "cpu", Op: "<=", Value: "2"}
|
rlimit2 = &Rlimit{Key: "cpu", Op: "<=", Value: "2"}
|
||||||
rlimit3 = &Rlimit{Key: "nproc", Op: "<", Value: "2"}
|
rlimit3 = &Rlimit{Key: "nproc", Op: "<", Value: "2"}
|
||||||
|
|
||||||
|
// Userns
|
||||||
|
userns1 = &Userns{Create: true}
|
||||||
|
userns2 = &Userns{}
|
||||||
|
|
||||||
// Capability
|
// Capability
|
||||||
capability1Log = map[string]string{
|
capability1Log = map[string]string{
|
||||||
"apparmor": "ALLOWED",
|
"apparmor": "ALLOWED",
|
||||||
|
|
@ -26,8 +49,8 @@ var (
|
||||||
"profile": "pkexec",
|
"profile": "pkexec",
|
||||||
"comm": "pkexec",
|
"comm": "pkexec",
|
||||||
}
|
}
|
||||||
capability1 = &Capability{Name: "net_admin"}
|
capability1 = &Capability{Names: []string{"net_admin"}}
|
||||||
capability2 = &Capability{Name: "sys_ptrace"}
|
capability2 = &Capability{Names: []string{"sys_ptrace"}}
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
network1Log = map[string]string{
|
network1Log = map[string]string{
|
||||||
|
|
@ -71,20 +94,24 @@ var (
|
||||||
"flags": "rw, rbind",
|
"flags": "rw, rbind",
|
||||||
}
|
}
|
||||||
mount1 = &Mount{
|
mount1 = &Mount{
|
||||||
Qualifier: Qualifier{Comment: "failed perms check"},
|
RuleBase: RuleBase{Comment: " failed perms check"},
|
||||||
MountConditions: MountConditions{FsType: "overlay"},
|
MountConditions: MountConditions{FsType: "overlay"},
|
||||||
Source: "overlay",
|
Source: "overlay",
|
||||||
MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/",
|
MountPoint: "/var/lib/docker/overlay2/opaque-bug-check1209538631/merged/",
|
||||||
}
|
}
|
||||||
mount2 = &Mount{
|
mount2 = &Mount{
|
||||||
Qualifier: Qualifier{Comment: "failed perms check"},
|
RuleBase: RuleBase{Comment: " failed perms check"},
|
||||||
MountConditions: MountConditions{Options: []string{"rw", "rbind"}},
|
MountConditions: MountConditions{Options: []string{"rw", "rbind"}},
|
||||||
Source: "/oldroot/dev/tty",
|
Source: "/oldroot/dev/tty",
|
||||||
MountPoint: "/newroot/dev/tty",
|
MountPoint: "/newroot/dev/tty",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remount
|
||||||
|
remount1 = &Remount{MountPoint: "/"}
|
||||||
|
remount2 = &Remount{MountPoint: "/{,**}/"}
|
||||||
|
|
||||||
// Umount
|
// Umount
|
||||||
umount1Log = map[string]string{
|
umount1Log = map[string]string{
|
||||||
"apparmor": "ALLOWED",
|
"apparmor": "ALLOWED",
|
||||||
"class": "mount",
|
"class": "mount",
|
||||||
"operation": "umount",
|
"operation": "umount",
|
||||||
|
|
@ -96,7 +123,6 @@ var (
|
||||||
umount2 = &Umount{MountPoint: "/oldroot/"}
|
umount2 = &Umount{MountPoint: "/oldroot/"}
|
||||||
|
|
||||||
// PivotRoot
|
// PivotRoot
|
||||||
// pivotroot1LogStr = `apparmor="ALLOWED" operation="pivotroot" class="mount" profile="systemd" name="@{run}/systemd/mount-rootfs/" comm="(ostnamed)" srcname="@{run}/systemd/mount-rootfs/"`
|
|
||||||
pivotroot1Log = map[string]string{
|
pivotroot1Log = map[string]string{
|
||||||
"apparmor": "ALLOWED",
|
"apparmor": "ALLOWED",
|
||||||
"class": "mount",
|
"class": "mount",
|
||||||
|
|
@ -120,7 +146,6 @@ var (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change Profile
|
// Change Profile
|
||||||
// changeprofile1LogStr = `apparmor="ALLOWED" operation="change_onexec" class="file" profile="systemd" name="systemd-user" comm="(systemd)" target="systemd-user"`
|
|
||||||
changeprofile1Log = map[string]string{
|
changeprofile1Log = map[string]string{
|
||||||
"apparmor": "ALLOWED",
|
"apparmor": "ALLOWED",
|
||||||
"class": "file",
|
"class": "file",
|
||||||
|
|
@ -134,6 +159,14 @@ var (
|
||||||
changeprofile2 = &ChangeProfile{ProfileName: "brwap"}
|
changeprofile2 = &ChangeProfile{ProfileName: "brwap"}
|
||||||
changeprofile3 = &ChangeProfile{ExecMode: "safe", Exec: "/bin/bash", ProfileName: "brwap//default"}
|
changeprofile3 = &ChangeProfile{ExecMode: "safe", Exec: "/bin/bash", ProfileName: "brwap//default"}
|
||||||
|
|
||||||
|
// Mqueue
|
||||||
|
mqueue1 = &Mqueue{Access: []string{"r"}, Type: "posix", Name: "/"}
|
||||||
|
mqueue2 = &Mqueue{Access: []string{"r"}, Type: "sysv", Name: "/"}
|
||||||
|
|
||||||
|
// IO Uring
|
||||||
|
iouring1 = &IOUring{Access: []string{"sqpoll"}, Label: "foo"}
|
||||||
|
iouring2 = &IOUring{Access: []string{"override_creds"}}
|
||||||
|
|
||||||
// Signal
|
// Signal
|
||||||
signal1Log = map[string]string{
|
signal1Log = map[string]string{
|
||||||
"apparmor": "ALLOWED",
|
"apparmor": "ALLOWED",
|
||||||
|
|
@ -147,13 +180,13 @@ var (
|
||||||
"peer": "firefox//&firejail-default",
|
"peer": "firefox//&firejail-default",
|
||||||
}
|
}
|
||||||
signal1 = &Signal{
|
signal1 = &Signal{
|
||||||
Access: "receive",
|
Access: []string{"receive"},
|
||||||
Set: "kill",
|
Set: []string{"kill"},
|
||||||
Peer: "firefox//&firejail-default",
|
Peer: "firefox//&firejail-default",
|
||||||
}
|
}
|
||||||
signal2 = &Signal{
|
signal2 = &Signal{
|
||||||
Access: "receive",
|
Access: []string{"receive"},
|
||||||
Set: "up",
|
Set: []string{"up"},
|
||||||
Peer: "firefox//&firejail-default",
|
Peer: "firefox//&firejail-default",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,8 +210,8 @@ var (
|
||||||
"denied_mask": "readby",
|
"denied_mask": "readby",
|
||||||
"peer": "systemd-journald",
|
"peer": "systemd-journald",
|
||||||
}
|
}
|
||||||
ptrace1 = &Ptrace{Access: "read", Peer: "nautilus"}
|
ptrace1 = &Ptrace{Access: []string{"read"}, Peer: "nautilus"}
|
||||||
ptrace2 = &Ptrace{Access: "readby", Peer: "systemd-journald"}
|
ptrace2 = &Ptrace{Access: []string{"readby"}, Peer: "systemd-journald"}
|
||||||
|
|
||||||
// Unix
|
// Unix
|
||||||
unix1Log = map[string]string{
|
unix1Log = map[string]string{
|
||||||
|
|
@ -197,17 +230,17 @@ var (
|
||||||
"protocol": "0",
|
"protocol": "0",
|
||||||
}
|
}
|
||||||
unix1 = &Unix{
|
unix1 = &Unix{
|
||||||
Access: "send receive",
|
Access: []string{"send", "receive"},
|
||||||
Type: "stream",
|
Type: "stream",
|
||||||
Protocol: "0",
|
Protocol: "0",
|
||||||
Address: "none",
|
Address: "none",
|
||||||
Peer: "dbus-daemon",
|
PeerAddr: "@/tmp/dbus-AaKMpxzC4k",
|
||||||
PeerAddr: "@/tmp/dbus-AaKMpxzC4k",
|
PeerLabel: "dbus-daemon",
|
||||||
}
|
}
|
||||||
unix2 = &Unix{
|
unix2 = &Unix{
|
||||||
Qualifier: Qualifier{FileInherit: true},
|
RuleBase: RuleBase{FileInherit: true},
|
||||||
Access: "receive",
|
Access: []string{"receive"},
|
||||||
Type: "stream",
|
Type: "stream",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dbus
|
// Dbus
|
||||||
|
|
@ -234,21 +267,21 @@ var (
|
||||||
"label": "evolution-source-registry",
|
"label": "evolution-source-registry",
|
||||||
}
|
}
|
||||||
dbus1 = &Dbus{
|
dbus1 = &Dbus{
|
||||||
Access: "receive",
|
Access: []string{"receive"},
|
||||||
Bus: "session",
|
Bus: "session",
|
||||||
Name: ":1.15",
|
|
||||||
Path: "/org/gtk/vfs/metadata",
|
Path: "/org/gtk/vfs/metadata",
|
||||||
Interface: "org.gtk.vfs.Metadata",
|
Interface: "org.gtk.vfs.Metadata",
|
||||||
Member: "Remove",
|
Member: "Remove",
|
||||||
Label: "tracker-extract",
|
PeerName: ":1.15",
|
||||||
|
PeerLabel: "tracker-extract",
|
||||||
}
|
}
|
||||||
dbus2 = &Dbus{
|
dbus2 = &Dbus{
|
||||||
Access: "bind",
|
Access: []string{"bind"},
|
||||||
Bus: "session",
|
Bus: "session",
|
||||||
Name: "org.gnome.evolution.dataserver.Sources5",
|
Name: "org.gnome.evolution.dataserver.Sources5",
|
||||||
}
|
}
|
||||||
dbus3 = &Dbus{
|
dbus3 = &Dbus{
|
||||||
Access: "bind",
|
Access: []string{"bind"},
|
||||||
Bus: "session",
|
Bus: "session",
|
||||||
Name: "org.gnome.evolution.dataserver",
|
Name: "org.gnome.evolution.dataserver",
|
||||||
}
|
}
|
||||||
|
|
@ -283,10 +316,77 @@ var (
|
||||||
"OUID": "user",
|
"OUID": "user",
|
||||||
"error": "-1",
|
"error": "-1",
|
||||||
}
|
}
|
||||||
file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: "r"}
|
file1 = &File{Path: "/usr/share/poppler/cMap/Identity-H", Access: []string{"r"}}
|
||||||
file2 = &File{
|
file2 = &File{
|
||||||
Qualifier: Qualifier{Owner: true, NoNewPrivs: true},
|
RuleBase: RuleBase{NoNewPrivs: true},
|
||||||
Path: "@{PROC}/4163/cgroup",
|
Owner: true,
|
||||||
Access: "r",
|
Path: "@{PROC}/4163/cgroup",
|
||||||
|
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"}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
109
pkg/aa/dbus.go
109
pkg/aa/dbus.go
|
|
@ -4,59 +4,112 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DBUS Kind = "dbus"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[DBUS] = requirement{
|
||||||
|
"access": []string{
|
||||||
|
"send", "receive", "bind", "eavesdrop", "r", "read",
|
||||||
|
"w", "write", "rw",
|
||||||
|
},
|
||||||
|
"bus": []string{"system", "session", "accessibility"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Dbus struct {
|
type Dbus struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
Access string
|
Access []string
|
||||||
Bus string
|
Bus string
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
Interface string
|
Interface string
|
||||||
Member string
|
Member string
|
||||||
Label string
|
PeerName string
|
||||||
|
PeerLabel string
|
||||||
}
|
}
|
||||||
|
|
||||||
func DbusFromLog(log map[string]string) ApparmorRule {
|
func newDbusFromLog(log map[string]string) Rule {
|
||||||
|
name := ""
|
||||||
|
peerName := ""
|
||||||
|
if log["mask"] == "bind" {
|
||||||
|
name = log["name"]
|
||||||
|
} else {
|
||||||
|
peerName = log["name"]
|
||||||
|
}
|
||||||
return &Dbus{
|
return &Dbus{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
Access: log["mask"],
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Access: []string{log["mask"]},
|
||||||
Bus: log["bus"],
|
Bus: log["bus"],
|
||||||
Name: log["name"],
|
Name: name,
|
||||||
Path: log["path"],
|
Path: log["path"],
|
||||||
Interface: log["interface"],
|
Interface: log["interface"],
|
||||||
Member: log["member"],
|
Member: log["member"],
|
||||||
Label: log["peer_label"],
|
PeerName: peerName,
|
||||||
|
PeerLabel: log["peer_label"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Dbus) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return validateValues(r.Kind(), "bus", []string{r.Bus})
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Dbus) Less(other any) bool {
|
func (r *Dbus) Less(other any) bool {
|
||||||
o, _ := other.(*Dbus)
|
o, _ := other.(*Dbus)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
for i := 0; i < len(r.Access) && i < len(o.Access); i++ {
|
||||||
if r.Access == o.Access {
|
if r.Access[i] != o.Access[i] {
|
||||||
if r.Bus == o.Bus {
|
return r.Access[i] < o.Access[i]
|
||||||
if r.Name == o.Name {
|
|
||||||
if r.Path == o.Path {
|
|
||||||
if r.Interface == o.Interface {
|
|
||||||
if r.Member == o.Member {
|
|
||||||
return r.Label < o.Label
|
|
||||||
}
|
|
||||||
return r.Member < o.Member
|
|
||||||
}
|
|
||||||
return r.Interface < o.Interface
|
|
||||||
}
|
|
||||||
return r.Path < o.Path
|
|
||||||
}
|
|
||||||
return r.Name < o.Name
|
|
||||||
}
|
|
||||||
return r.Bus < o.Bus
|
|
||||||
}
|
}
|
||||||
return r.Access < o.Access
|
}
|
||||||
|
if r.Bus != o.Bus {
|
||||||
|
return r.Bus < o.Bus
|
||||||
|
}
|
||||||
|
if r.Name != o.Name {
|
||||||
|
return r.Name < o.Name
|
||||||
|
}
|
||||||
|
if r.Path != o.Path {
|
||||||
|
return r.Path < o.Path
|
||||||
|
}
|
||||||
|
if r.Interface != o.Interface {
|
||||||
|
return r.Interface < o.Interface
|
||||||
|
}
|
||||||
|
if r.Member != o.Member {
|
||||||
|
return r.Member < o.Member
|
||||||
|
}
|
||||||
|
if r.PeerName != o.PeerName {
|
||||||
|
return r.PeerName < o.PeerName
|
||||||
|
}
|
||||||
|
if r.PeerLabel != o.PeerLabel {
|
||||||
|
return r.PeerLabel < o.PeerLabel
|
||||||
}
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Dbus) Equals(other any) bool {
|
func (r *Dbus) Equals(other any) bool {
|
||||||
o, _ := other.(*Dbus)
|
o, _ := other.(*Dbus)
|
||||||
return r.Access == o.Access && r.Bus == o.Bus && r.Name == o.Name &&
|
return slices.Equal(r.Access, o.Access) && r.Bus == o.Bus && r.Name == o.Name &&
|
||||||
r.Path == o.Path && r.Interface == o.Interface &&
|
r.Path == o.Path && r.Interface == o.Interface &&
|
||||||
r.Member == o.Member && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier)
|
r.Member == o.Member && r.PeerName == o.PeerName &&
|
||||||
|
r.PeerLabel == o.PeerLabel && r.Qualifier.Equals(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Dbus) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Dbus) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Dbus) Kind() Kind {
|
||||||
|
return DBUS
|
||||||
}
|
}
|
||||||
|
|
|
||||||
155
pkg/aa/file.go
155
pkg/aa/file.go
|
|
@ -4,43 +4,164 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LINK Kind = "link"
|
||||||
|
FILE Kind = "file"
|
||||||
|
tokOWNER = "owner"
|
||||||
|
tokSUBSET = "subset"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[FILE] = requirement{
|
||||||
|
"access": {"m", "r", "w", "l", "k"},
|
||||||
|
"transition": {
|
||||||
|
"ix", "ux", "Ux", "px", "Px", "cx", "Cx", "pix", "Pix", "cix",
|
||||||
|
"Cix", "pux", "PUx", "cux", "CUx", "x",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOwner(log map[string]string) bool {
|
||||||
|
fsuid, hasFsUID := log["fsuid"]
|
||||||
|
ouid, hasOuUID := log["ouid"]
|
||||||
|
isDbus := strings.Contains(log["operation"], "dbus")
|
||||||
|
if hasFsUID && hasOuUID && fsuid == ouid && ouid != "0" && !isDbus {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
|
Owner bool
|
||||||
Path string
|
Path string
|
||||||
Access string
|
Access []string
|
||||||
Target string
|
Target string
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileFromLog(log map[string]string) ApparmorRule {
|
func newFileFromLog(log map[string]string) Rule {
|
||||||
|
accesses, err := toAccess("file-log", log["requested_mask"])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("newFileFromLog(%v): %w", log, err))
|
||||||
|
}
|
||||||
|
if slices.Compare(accesses, []string{"l"}) == 0 {
|
||||||
|
return newLinkFromLog(log)
|
||||||
|
}
|
||||||
return &File{
|
return &File{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Owner: isOwner(log),
|
||||||
Path: log["name"],
|
Path: log["name"],
|
||||||
Access: toAccess(log["requested_mask"]),
|
Access: accesses,
|
||||||
Target: log["target"],
|
Target: log["target"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *File) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *File) Less(other any) bool {
|
func (r *File) Less(other any) bool {
|
||||||
o, _ := other.(*File)
|
o, _ := other.(*File)
|
||||||
letterR := getLetterIn(fileAlphabet, r.Path)
|
letterR := getLetterIn(fileAlphabet, r.Path)
|
||||||
letterO := getLetterIn(fileAlphabet, o.Path)
|
letterO := getLetterIn(fileAlphabet, o.Path)
|
||||||
if fileWeights[letterR] == fileWeights[letterO] || letterR == "" || letterO == "" {
|
if fileWeights[letterR] != fileWeights[letterO] && letterR != "" && letterO != "" {
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
return fileWeights[letterR] < fileWeights[letterO]
|
||||||
if r.Path == o.Path {
|
|
||||||
if r.Access == o.Access {
|
|
||||||
return r.Target < o.Target
|
|
||||||
}
|
|
||||||
return r.Access < o.Access
|
|
||||||
}
|
|
||||||
return r.Path < o.Path
|
|
||||||
}
|
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
|
||||||
}
|
}
|
||||||
return fileWeights[letterR] < fileWeights[letterO]
|
if r.Path != o.Path {
|
||||||
|
return r.Path < o.Path
|
||||||
|
}
|
||||||
|
if o.Owner != r.Owner {
|
||||||
|
return r.Owner
|
||||||
|
}
|
||||||
|
if len(r.Access) != len(o.Access) {
|
||||||
|
return len(r.Access) < len(o.Access)
|
||||||
|
}
|
||||||
|
if r.Target != o.Target {
|
||||||
|
return r.Target < o.Target
|
||||||
|
}
|
||||||
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *File) Equals(other any) bool {
|
func (r *File) Equals(other any) bool {
|
||||||
o, _ := other.(*File)
|
o, _ := other.(*File)
|
||||||
return r.Path == o.Path && r.Access == o.Access &&
|
return r.Path == o.Path && slices.Equal(r.Access, o.Access) && r.Owner == o.Owner &&
|
||||||
r.Target == o.Target && r.Qualifier.Equals(o.Qualifier)
|
r.Target == o.Target && r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *File) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *File) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *File) Kind() Kind {
|
||||||
|
return FILE
|
||||||
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
RuleBase
|
||||||
|
Qualifier
|
||||||
|
Owner bool
|
||||||
|
Subset bool
|
||||||
|
Path string
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLinkFromLog(log map[string]string) Rule {
|
||||||
|
return &Link{
|
||||||
|
RuleBase: newRuleFromLog(log),
|
||||||
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Owner: isOwner(log),
|
||||||
|
Path: log["name"],
|
||||||
|
Target: log["target"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Link) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Link) Less(other any) bool {
|
||||||
|
o, _ := other.(*Link)
|
||||||
|
if r.Path != o.Path {
|
||||||
|
return r.Path < o.Path
|
||||||
|
}
|
||||||
|
if o.Owner != r.Owner {
|
||||||
|
return r.Owner
|
||||||
|
}
|
||||||
|
if r.Target != o.Target {
|
||||||
|
return r.Target < o.Target
|
||||||
|
}
|
||||||
|
if r.Subset != o.Subset {
|
||||||
|
return r.Subset
|
||||||
|
}
|
||||||
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Link) Equals(other any) bool {
|
||||||
|
o, _ := other.(*Link)
|
||||||
|
return r.Subset == o.Subset && r.Owner == o.Owner && r.Path == o.Path &&
|
||||||
|
r.Target == o.Target && r.Qualifier.Equals(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Link) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Link) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Link) Kind() Kind {
|
||||||
|
return LINK
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -4,24 +4,66 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const IOURING Kind = "io_uring"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[IOURING] = requirement{
|
||||||
|
"access": []string{"sqpoll", "override_creds"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type IOUring struct {
|
type IOUring struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
Access string
|
Access []string
|
||||||
Label string
|
Label string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newIOUringFromLog(log map[string]string) Rule {
|
||||||
|
return &IOUring{
|
||||||
|
RuleBase: newRuleFromLog(log),
|
||||||
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Access: Must(toAccess(IOURING, log["requested"])),
|
||||||
|
Label: log["label"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IOUring) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *IOUring) Less(other any) bool {
|
func (r *IOUring) Less(other any) bool {
|
||||||
o, _ := other.(*IOUring)
|
o, _ := other.(*IOUring)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if len(r.Access) != len(o.Access) {
|
||||||
if r.Access == o.Access {
|
return len(r.Access) < len(o.Access)
|
||||||
return r.Label < o.Label
|
}
|
||||||
}
|
if r.Label != o.Label {
|
||||||
return r.Access < o.Access
|
return r.Label < o.Label
|
||||||
}
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IOUring) Equals(other any) bool {
|
func (r *IOUring) Equals(other any) bool {
|
||||||
o, _ := other.(*IOUring)
|
o, _ := other.(*IOUring)
|
||||||
return r.Access == o.Access && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier)
|
return slices.Equal(r.Access, o.Access) && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IOUring) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IOUring) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IOUring) Kind() Kind {
|
||||||
|
return IOURING
|
||||||
}
|
}
|
||||||
|
|
|
||||||
147
pkg/aa/mount.go
147
pkg/aa/mount.go
|
|
@ -5,30 +5,54 @@
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MOUNT Kind = "mount"
|
||||||
|
REMOUNT Kind = "remount"
|
||||||
|
UMOUNT Kind = "umount"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[MOUNT] = requirement{
|
||||||
|
"flags": {
|
||||||
|
"acl", "async", "atime", "ro", "rw", "bind", "rbind", "dev",
|
||||||
|
"diratime", "dirsync", "exec", "iversion", "loud", "mand", "move",
|
||||||
|
"noacl", "noatime", "nodev", "nodiratime", "noexec", "noiversion",
|
||||||
|
"nomand", "norelatime", "nosuid", "nouser", "private", "relatime",
|
||||||
|
"remount", "rprivate", "rshared", "rslave", "runbindable", "shared",
|
||||||
|
"silent", "slave", "strictatime", "suid", "sync", "unbindable",
|
||||||
|
"user", "verbose",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MountConditions struct {
|
type MountConditions struct {
|
||||||
FsType string
|
FsType string
|
||||||
Options []string
|
Options []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func MountConditionsFromLog(log map[string]string) MountConditions {
|
func newMountConditionsFromLog(log map[string]string) MountConditions {
|
||||||
if _, present := log["flags"]; present {
|
if _, present := log["flags"]; present {
|
||||||
return MountConditions{
|
return MountConditions{
|
||||||
FsType: log["fstype"],
|
FsType: log["fstype"],
|
||||||
Options: strings.Split(log["flags"], ", "),
|
Options: Must(toValues(MOUNT, "flags", log["flags"])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MountConditions{FsType: log["fstype"]}
|
return MountConditions{FsType: log["fstype"]}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m MountConditions) Validate() error {
|
||||||
|
return validateValues(MOUNT, "flags", m.Options)
|
||||||
|
}
|
||||||
|
|
||||||
func (m MountConditions) Less(other MountConditions) bool {
|
func (m MountConditions) Less(other MountConditions) bool {
|
||||||
if m.FsType == other.FsType {
|
if m.FsType != other.FsType {
|
||||||
return len(m.Options) < len(other.Options)
|
return m.FsType < other.FsType
|
||||||
}
|
}
|
||||||
return m.FsType < other.FsType
|
return len(m.Options) < len(other.Options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MountConditions) Equals(other MountConditions) bool {
|
func (m MountConditions) Equals(other MountConditions) bool {
|
||||||
|
|
@ -36,32 +60,41 @@ func (m MountConditions) Equals(other MountConditions) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mount struct {
|
type Mount struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
MountConditions
|
MountConditions
|
||||||
Source string
|
Source string
|
||||||
MountPoint string
|
MountPoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func MountFromLog(log map[string]string) ApparmorRule {
|
func newMountFromLog(log map[string]string) Rule {
|
||||||
return &Mount{
|
return &Mount{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
MountConditions: MountConditionsFromLog(log),
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
MountConditions: newMountConditionsFromLog(log),
|
||||||
Source: log["srcname"],
|
Source: log["srcname"],
|
||||||
MountPoint: log["name"],
|
MountPoint: log["name"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Mount) Validate() error {
|
||||||
|
if err := r.MountConditions.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Mount) Less(other any) bool {
|
func (r *Mount) Less(other any) bool {
|
||||||
o, _ := other.(*Mount)
|
o, _ := other.(*Mount)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if r.Source != o.Source {
|
||||||
if r.Source == o.Source {
|
|
||||||
if r.MountPoint == o.MountPoint {
|
|
||||||
return r.MountConditions.Less(o.MountConditions)
|
|
||||||
}
|
|
||||||
return r.MountPoint < o.MountPoint
|
|
||||||
}
|
|
||||||
return r.Source < o.Source
|
return r.Source < o.Source
|
||||||
}
|
}
|
||||||
|
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)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,28 +105,49 @@ func (r *Mount) Equals(other any) bool {
|
||||||
r.Qualifier.Equals(o.Qualifier)
|
r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Mount) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Mount) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Mount) Kind() Kind {
|
||||||
|
return MOUNT
|
||||||
|
}
|
||||||
|
|
||||||
type Umount struct {
|
type Umount struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
MountConditions
|
MountConditions
|
||||||
MountPoint string
|
MountPoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func UmountFromLog(log map[string]string) ApparmorRule {
|
func newUmountFromLog(log map[string]string) Rule {
|
||||||
return &Umount{
|
return &Umount{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
MountConditions: MountConditionsFromLog(log),
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
MountConditions: newMountConditionsFromLog(log),
|
||||||
MountPoint: log["name"],
|
MountPoint: log["name"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Umount) Validate() error {
|
||||||
|
if err := r.MountConditions.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Umount) Less(other any) bool {
|
func (r *Umount) Less(other any) bool {
|
||||||
o, _ := other.(*Umount)
|
o, _ := other.(*Umount)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if r.MountPoint != o.MountPoint {
|
||||||
if r.MountPoint == o.MountPoint {
|
|
||||||
return r.MountConditions.Less(o.MountConditions)
|
|
||||||
}
|
|
||||||
return r.MountPoint < o.MountPoint
|
return r.MountPoint < o.MountPoint
|
||||||
}
|
}
|
||||||
|
if r.MountConditions.Equals(o.MountConditions) {
|
||||||
|
return r.MountConditions.Less(o.MountConditions)
|
||||||
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,28 +158,49 @@ func (r *Umount) Equals(other any) bool {
|
||||||
r.Qualifier.Equals(o.Qualifier)
|
r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Umount) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Umount) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Umount) Kind() Kind {
|
||||||
|
return UMOUNT
|
||||||
|
}
|
||||||
|
|
||||||
type Remount struct {
|
type Remount struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
MountConditions
|
MountConditions
|
||||||
MountPoint string
|
MountPoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemountFromLog(log map[string]string) ApparmorRule {
|
func newRemountFromLog(log map[string]string) Rule {
|
||||||
return &Remount{
|
return &Remount{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
MountConditions: MountConditionsFromLog(log),
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
MountConditions: newMountConditionsFromLog(log),
|
||||||
MountPoint: log["name"],
|
MountPoint: log["name"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Remount) Validate() error {
|
||||||
|
if err := r.MountConditions.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Remount) Less(other any) bool {
|
func (r *Remount) Less(other any) bool {
|
||||||
o, _ := other.(*Remount)
|
o, _ := other.(*Remount)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if r.MountPoint != o.MountPoint {
|
||||||
if r.MountPoint == o.MountPoint {
|
|
||||||
return r.MountConditions.Less(o.MountConditions)
|
|
||||||
}
|
|
||||||
return r.MountPoint < o.MountPoint
|
return r.MountPoint < o.MountPoint
|
||||||
}
|
}
|
||||||
|
if r.MountConditions.Equals(o.MountConditions) {
|
||||||
|
return r.MountConditions.Less(o.MountConditions)
|
||||||
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,3 +210,15 @@ func (r *Remount) Equals(other any) bool {
|
||||||
r.MountConditions.Equals(o.MountConditions) &&
|
r.MountConditions.Equals(o.MountConditions) &&
|
||||||
r.Qualifier.Equals(o.Qualifier)
|
r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Remount) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Remount) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Remount) Kind() Kind {
|
||||||
|
return REMOUNT
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,34 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MQUEUE Kind = "mqueue"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[MQUEUE] = requirement{
|
||||||
|
"access": []string{
|
||||||
|
"r", "w", "rw", "read", "write", "create", "open",
|
||||||
|
"delete", "getattr", "setattr",
|
||||||
|
},
|
||||||
|
"type": []string{"posix", "sysv"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Mqueue struct {
|
type Mqueue struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
Access string
|
Access []string
|
||||||
Type string
|
Type string
|
||||||
Label string
|
Label string
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func MqueueFromLog(log map[string]string) ApparmorRule {
|
func newMqueueFromLog(log map[string]string) Rule {
|
||||||
mqueueType := "posix"
|
mqueueType := "posix"
|
||||||
if strings.Contains(log["class"], "posix") {
|
if strings.Contains(log["class"], "posix") {
|
||||||
mqueueType = "posix"
|
mqueueType = "posix"
|
||||||
|
|
@ -22,29 +39,53 @@ func MqueueFromLog(log map[string]string) ApparmorRule {
|
||||||
mqueueType = "sysv"
|
mqueueType = "sysv"
|
||||||
}
|
}
|
||||||
return &Mqueue{
|
return &Mqueue{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
Access: toAccess(log["requested"]),
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Access: Must(toAccess(MQUEUE, log["requested"])),
|
||||||
Type: mqueueType,
|
Type: mqueueType,
|
||||||
Label: log["label"],
|
Label: log["label"],
|
||||||
Name: log["name"],
|
Name: log["name"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Mqueue) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
if err := validateValues(r.Kind(), "type", []string{r.Type}); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Mqueue) Less(other any) bool {
|
func (r *Mqueue) Less(other any) bool {
|
||||||
o, _ := other.(*Mqueue)
|
o, _ := other.(*Mqueue)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if len(r.Access) != len(o.Access) {
|
||||||
if r.Access == o.Access {
|
return len(r.Access) < len(o.Access)
|
||||||
if r.Type == o.Type {
|
}
|
||||||
return r.Label < o.Label
|
if r.Type != o.Type {
|
||||||
}
|
return r.Type < o.Type
|
||||||
return r.Type < o.Type
|
}
|
||||||
}
|
if r.Label != o.Label {
|
||||||
return r.Access < o.Access
|
return r.Label < o.Label
|
||||||
}
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Mqueue) Equals(other any) bool {
|
func (r *Mqueue) Equals(other any) bool {
|
||||||
o, _ := other.(*Mqueue)
|
o, _ := other.(*Mqueue)
|
||||||
return r.Access == o.Access && r.Type == o.Type && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier)
|
return slices.Equal(r.Access, o.Access) && r.Type == o.Type && r.Label == o.Label &&
|
||||||
|
r.Name == o.Name && r.Qualifier.Equals(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Mqueue) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Mqueue) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Mqueue) Kind() Kind {
|
||||||
|
return MQUEUE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,60 +4,110 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NETWORK Kind = "network"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[NETWORK] = requirement{
|
||||||
|
"access": []string{
|
||||||
|
"create", "bind", "listen", "accept", "connect", "shutdown",
|
||||||
|
"getattr", "setattr", "getopt", "setopt", "send", "receive",
|
||||||
|
"r", "w", "rw",
|
||||||
|
},
|
||||||
|
"domains": []string{
|
||||||
|
"unix", "inet", "ax25", "ipx", "appletalk", "netrom", "bridge",
|
||||||
|
"atmpvc", "x25", "inet6", "rose", "netbeui", "security", "key",
|
||||||
|
"netlink", "packet", "ash", "econet", "atmsvc", "rds", "sna", "irda",
|
||||||
|
"pppox", "wanpipe", "llc", "ib", "mpls", "can", "tipc", "bluetooth",
|
||||||
|
"iucv", "rxrpc", "isdn", "phonet", "ieee802154", "caif", "alg",
|
||||||
|
"nfc", "vsock", "kcm", "qipcrtr", "smc", "xdp", "mctp",
|
||||||
|
},
|
||||||
|
"type": []string{
|
||||||
|
"stream", "dgram", "seqpacket", "rdm", "raw", "packet",
|
||||||
|
},
|
||||||
|
"protocol": []string{"tcp", "udp", "icmp"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AddressExpr struct {
|
type AddressExpr struct {
|
||||||
Source string
|
Source string
|
||||||
Destination string
|
Destination string
|
||||||
Port string
|
Port string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newAddressExprFromLog(log map[string]string) AddressExpr {
|
||||||
|
return AddressExpr{
|
||||||
|
Source: log["laddr"],
|
||||||
|
Destination: log["faddr"],
|
||||||
|
Port: log["lport"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r AddressExpr) Less(other AddressExpr) bool {
|
||||||
|
if r.Source != other.Source {
|
||||||
|
return r.Source < other.Source
|
||||||
|
}
|
||||||
|
if r.Destination != other.Destination {
|
||||||
|
return r.Destination < other.Destination
|
||||||
|
}
|
||||||
|
return r.Port < other.Port
|
||||||
|
}
|
||||||
|
|
||||||
func (r AddressExpr) Equals(other AddressExpr) bool {
|
func (r AddressExpr) Equals(other AddressExpr) bool {
|
||||||
return r.Source == other.Source && r.Destination == other.Destination &&
|
return r.Source == other.Source && r.Destination == other.Destination &&
|
||||||
r.Port == other.Port
|
r.Port == other.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r AddressExpr) Less(other AddressExpr) bool {
|
|
||||||
if r.Source == other.Source {
|
|
||||||
if r.Destination == other.Destination {
|
|
||||||
return r.Port < other.Port
|
|
||||||
}
|
|
||||||
return r.Destination < other.Destination
|
|
||||||
}
|
|
||||||
return r.Source < other.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
type Network struct {
|
type Network struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
|
AddressExpr
|
||||||
Domain string
|
Domain string
|
||||||
Type string
|
Type string
|
||||||
Protocol string
|
Protocol string
|
||||||
AddressExpr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NetworkFromLog(log map[string]string) ApparmorRule {
|
func newNetworkFromLog(log map[string]string) Rule {
|
||||||
return &Network{
|
return &Network{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
AddressExpr: AddressExpr{
|
Qualifier: newQualifierFromLog(log),
|
||||||
Source: log["laddr"],
|
AddressExpr: newAddressExprFromLog(log),
|
||||||
Destination: log["faddr"],
|
Domain: log["family"],
|
||||||
Port: log["lport"],
|
Type: log["sock_type"],
|
||||||
},
|
Protocol: log["protocol"],
|
||||||
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 {
|
func (r *Network) Less(other any) bool {
|
||||||
o, _ := other.(*Network)
|
o, _ := other.(*Network)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if r.Domain != o.Domain {
|
||||||
if r.Domain == o.Domain {
|
|
||||||
if r.Type == o.Type {
|
|
||||||
return r.Protocol < o.Protocol
|
|
||||||
}
|
|
||||||
return r.Type < o.Type
|
|
||||||
}
|
|
||||||
return r.Domain < o.Domain
|
return r.Domain < o.Domain
|
||||||
}
|
}
|
||||||
|
if r.Type != o.Type {
|
||||||
|
return r.Type < o.Type
|
||||||
|
}
|
||||||
|
if r.Protocol != o.Protocol {
|
||||||
|
return r.Protocol < o.Protocol
|
||||||
|
}
|
||||||
|
if r.AddressExpr.Less(o.AddressExpr) {
|
||||||
|
return r.AddressExpr.Less(o.AddressExpr)
|
||||||
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,3 +117,15 @@ func (r *Network) Equals(other any) bool {
|
||||||
r.Protocol == o.Protocol && r.AddressExpr.Equals(o.AddressExpr) &&
|
r.Protocol == o.Protocol && r.AddressExpr.Equals(o.AddressExpr) &&
|
||||||
r.Qualifier.Equals(o.Qualifier)
|
r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Network) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Network) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Network) Kind() Kind {
|
||||||
|
return NETWORK
|
||||||
|
}
|
||||||
|
|
|
||||||
245
pkg/aa/parse.go
Normal file
245
pkg/aa/parse.go
Normal 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
281
pkg/aa/parse_test.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -4,33 +4,41 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
const PIVOTROOT = "pivot_root"
|
||||||
|
|
||||||
type PivotRoot struct {
|
type PivotRoot struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
OldRoot string
|
OldRoot string
|
||||||
NewRoot string
|
NewRoot string
|
||||||
TargetProfile string
|
TargetProfile string
|
||||||
}
|
}
|
||||||
|
|
||||||
func PivotRootFromLog(log map[string]string) ApparmorRule {
|
func newPivotRootFromLog(log map[string]string) Rule {
|
||||||
return &PivotRoot{
|
return &PivotRoot{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
|
Qualifier: newQualifierFromLog(log),
|
||||||
OldRoot: log["srcname"],
|
OldRoot: log["srcname"],
|
||||||
NewRoot: log["name"],
|
NewRoot: log["name"],
|
||||||
TargetProfile: "",
|
TargetProfile: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *PivotRoot) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *PivotRoot) Less(other any) bool {
|
func (r *PivotRoot) Less(other any) bool {
|
||||||
o, _ := other.(*PivotRoot)
|
o, _ := other.(*PivotRoot)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if r.OldRoot != o.OldRoot {
|
||||||
if r.OldRoot == o.OldRoot {
|
|
||||||
if r.NewRoot == o.NewRoot {
|
|
||||||
return r.TargetProfile < o.TargetProfile
|
|
||||||
}
|
|
||||||
return r.NewRoot < o.NewRoot
|
|
||||||
}
|
|
||||||
return r.OldRoot < o.OldRoot
|
return r.OldRoot < o.OldRoot
|
||||||
}
|
}
|
||||||
|
if r.NewRoot != o.NewRoot {
|
||||||
|
return r.NewRoot < o.NewRoot
|
||||||
|
}
|
||||||
|
if r.TargetProfile != o.TargetProfile {
|
||||||
|
return r.TargetProfile < o.TargetProfile
|
||||||
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,3 +48,15 @@ func (r *PivotRoot) Equals(other any) bool {
|
||||||
r.TargetProfile == o.TargetProfile &&
|
r.TargetProfile == o.TargetProfile &&
|
||||||
r.Qualifier.Equals(o.Qualifier)
|
r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *PivotRoot) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PivotRoot) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PivotRoot) Kind() Kind {
|
||||||
|
return PIVOTROOT
|
||||||
|
}
|
||||||
|
|
|
||||||
310
pkg/aa/preamble.go
Normal file
310
pkg/aa/preamble.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -5,71 +5,205 @@
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"fmt"
|
||||||
"reflect"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/roddhjav/apparmor.d/pkg/paths"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default Apparmor magic directory: /etc/apparmor.d/.
|
const (
|
||||||
var MagicRoot = paths.New("/etc/apparmor.d")
|
PROFILE Kind = "profile"
|
||||||
|
|
||||||
// AppArmorProfiles represents a full set of apparmor profiles
|
tokATTRIBUTES = "xattrs"
|
||||||
type AppArmorProfiles map[string]*AppArmorProfile
|
tokFLAGS = "flags"
|
||||||
|
)
|
||||||
|
|
||||||
// ApparmorProfile represents a full apparmor profile.
|
func init() {
|
||||||
// Warning: close to the BNF grammar of apparmor profile but not exactly the same (yet):
|
requirements[PROFILE] = requirement{
|
||||||
// - Some rules are not supported yet (subprofile, hat...)
|
tokFLAGS: {
|
||||||
// - The structure is simplified as it only aims at writing profile, not parsing it.
|
"enforce", "complain", "kill", "default_allow", "unconfined",
|
||||||
type AppArmorProfile struct {
|
"prompt", "audit", "mediate_deleted", "attach_disconnected",
|
||||||
Preamble
|
"attach_disconneced.path=", "chroot_relative", "debug",
|
||||||
Profile
|
"interruptible", "kill", "kill.signal=",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preamble section of a profile
|
// Profile represents a single AppArmor profile.
|
||||||
type Preamble struct {
|
|
||||||
Abi []Abi
|
|
||||||
Includes []Include
|
|
||||||
Aliases []Alias
|
|
||||||
Variables []Variable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profile section of a profile
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
|
RuleBase
|
||||||
|
Header
|
||||||
|
Rules Rules
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header represents the header of a profile.
|
||||||
|
type Header struct {
|
||||||
Name string
|
Name string
|
||||||
Attachments []string
|
Attachments []string
|
||||||
Attributes map[string]string
|
Attributes map[string]string
|
||||||
Flags []string
|
Flags []string
|
||||||
Rules Rules
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApparmorRule generic interface
|
func newHeader(rule []string) (Header, error) {
|
||||||
type ApparmorRule interface {
|
if len(rule) == 0 {
|
||||||
Less(other any) bool
|
return Header{}, nil
|
||||||
Equals(other any) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
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 (r *Profile) Validate() error {
|
||||||
func (p *AppArmorProfile) AddRule(log map[string]string) {
|
if err := validateValues(r.Kind(), tokFLAGS, r.Flags); err != nil {
|
||||||
|
return fmt.Errorf("profile %s: %w", r.Name, err)
|
||||||
|
}
|
||||||
|
return r.Rules.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Less(other any) bool {
|
||||||
|
o, _ := other.(*Profile)
|
||||||
|
if p.Name != o.Name {
|
||||||
|
return p.Name < o.Name
|
||||||
|
}
|
||||||
|
return len(p.Attachments) < len(o.Attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Equals(other any) bool {
|
||||||
|
o, _ := other.(*Profile)
|
||||||
|
return p.Name == o.Name && slices.Equal(p.Attachments, o.Attachments) &&
|
||||||
|
maps.Equal(p.Attributes, o.Attributes) &&
|
||||||
|
slices.Equal(p.Flags, o.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) String() string {
|
||||||
|
return renderTemplate(p.Kind(), p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Kind() Kind {
|
||||||
|
return PROFILE
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Merge() {
|
||||||
|
slices.Sort(p.Flags)
|
||||||
|
p.Flags = slices.Compact(p.Flags)
|
||||||
|
p.Rules = p.Rules.Merge()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Sort() {
|
||||||
|
p.Rules = p.Rules.Sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Format() {
|
||||||
|
p.Rules = p.Rules.Format()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAttachments return a nested attachment string
|
||||||
|
func (p *Profile) GetAttachments() string {
|
||||||
|
switch len(p.Attachments) {
|
||||||
|
case 0:
|
||||||
|
return ""
|
||||||
|
case 1:
|
||||||
|
return p.Attachments[0]
|
||||||
|
default:
|
||||||
|
res := []string{}
|
||||||
|
for _, attachment := range p.Attachments {
|
||||||
|
if strings.HasPrefix(attachment, "/") {
|
||||||
|
res = append(res, attachment[1:])
|
||||||
|
} else {
|
||||||
|
res = append(res, attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/{" + strings.Join(res, ",") + "}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
newLogMap = map[string]func(log map[string]string) Rule{
|
||||||
|
"rlimits": newRlimitFromLog,
|
||||||
|
"cap": newCapabilityFromLog,
|
||||||
|
"io_uring": newIOUringFromLog,
|
||||||
|
"signal": newSignalFromLog,
|
||||||
|
"ptrace": newPtraceFromLog,
|
||||||
|
"namespace": newUsernsFromLog,
|
||||||
|
"unix": newUnixFromLog,
|
||||||
|
"dbus": newDbusFromLog,
|
||||||
|
"posix_mqueue": newMqueueFromLog,
|
||||||
|
"sysv_mqueue": newMqueueFromLog,
|
||||||
|
"mount": func(log map[string]string) Rule {
|
||||||
|
if strings.Contains(log["flags"], "remount") {
|
||||||
|
return newRemountFromLog(log)
|
||||||
|
}
|
||||||
|
newRule := newLogMountMap[log["operation"]]
|
||||||
|
return newRule(log)
|
||||||
|
},
|
||||||
|
"net": func(log map[string]string) Rule {
|
||||||
|
if log["family"] == "unix" {
|
||||||
|
return newUnixFromLog(log)
|
||||||
|
} else {
|
||||||
|
return newNetworkFromLog(log)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": func(log map[string]string) Rule {
|
||||||
|
if log["operation"] == "change_onexec" {
|
||||||
|
return newChangeProfileFromLog(log)
|
||||||
|
} else {
|
||||||
|
return newFileFromLog(log)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exec": newFileFromLog,
|
||||||
|
"file_inherit": newFileFromLog,
|
||||||
|
"file_perm": newFileFromLog,
|
||||||
|
"open": newFileFromLog,
|
||||||
|
}
|
||||||
|
newLogMountMap = map[string]func(log map[string]string) Rule{
|
||||||
|
"mount": newMountFromLog,
|
||||||
|
"umount": newUmountFromLog,
|
||||||
|
"remount": newRemountFromLog,
|
||||||
|
"pivotroot": newPivotRootFromLog,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Profile) AddRule(log map[string]string) {
|
||||||
// Generate profile flags and extra rules
|
// Generate profile flags and extra rules
|
||||||
switch log["error"] {
|
switch log["error"] {
|
||||||
case "-2":
|
case "-2":
|
||||||
|
|
@ -78,134 +212,27 @@ func (p *AppArmorProfile) AddRule(log map[string]string) {
|
||||||
}
|
}
|
||||||
case "-13":
|
case "-13":
|
||||||
if strings.Contains(log["info"], "namespace creation restricted") {
|
if strings.Contains(log["info"], "namespace creation restricted") {
|
||||||
p.Rules = append(p.Rules, UsernsFromLog(log))
|
p.Rules = append(p.Rules, newUsernsFromLog(log))
|
||||||
} else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") {
|
} else if strings.Contains(log["info"], "disconnected path") && !slices.Contains(p.Flags, "attach_disconnected") {
|
||||||
p.Flags = append(p.Flags, "attach_disconnected")
|
p.Flags = append(p.Flags, "attach_disconnected")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
switch log["class"] {
|
done := false
|
||||||
case "cap":
|
for _, key := range []string{"class", "family", "operation"} {
|
||||||
p.Rules = append(p.Rules, CapabilityFromLog(log))
|
if newRule, ok := newLogMap[log[key]]; ok {
|
||||||
case "net":
|
p.Rules = append(p.Rules, newRule(log))
|
||||||
if log["family"] == "unix" {
|
done = true
|
||||||
p.Rules = append(p.Rules, UnixFromLog(log))
|
break
|
||||||
} else {
|
|
||||||
p.Rules = append(p.Rules, NetworkFromLog(log))
|
|
||||||
}
|
}
|
||||||
case "mount":
|
}
|
||||||
if strings.Contains(log["flags"], "remount") {
|
|
||||||
p.Rules = append(p.Rules, RemountFromLog(log))
|
if !done {
|
||||||
} else {
|
|
||||||
switch log["operation"] {
|
|
||||||
case "mount":
|
|
||||||
p.Rules = append(p.Rules, MountFromLog(log))
|
|
||||||
case "umount":
|
|
||||||
p.Rules = append(p.Rules, UmountFromLog(log))
|
|
||||||
case "remount":
|
|
||||||
p.Rules = append(p.Rules, RemountFromLog(log))
|
|
||||||
case "pivotroot":
|
|
||||||
p.Rules = append(p.Rules, PivotRootFromLog(log))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "posix_mqueue", "sysv_mqueue":
|
|
||||||
p.Rules = append(p.Rules, MqueueFromLog(log))
|
|
||||||
case "signal":
|
|
||||||
p.Rules = append(p.Rules, SignalFromLog(log))
|
|
||||||
case "ptrace":
|
|
||||||
p.Rules = append(p.Rules, PtraceFromLog(log))
|
|
||||||
case "namespace":
|
|
||||||
p.Rules = append(p.Rules, UsernsFromLog(log))
|
|
||||||
case "unix":
|
|
||||||
p.Rules = append(p.Rules, UnixFromLog(log))
|
|
||||||
case "file":
|
|
||||||
if log["operation"] == "change_onexec" {
|
|
||||||
p.Rules = append(p.Rules, ChangeProfileFromLog(log))
|
|
||||||
} else {
|
|
||||||
p.Rules = append(p.Rules, FileFromLog(log))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if strings.Contains(log["operation"], "dbus") {
|
if strings.Contains(log["operation"], "dbus") {
|
||||||
p.Rules = append(p.Rules, DbusFromLog(log))
|
p.Rules = append(p.Rules, newDbusFromLog(log))
|
||||||
} else if log["family"] == "unix" {
|
} else {
|
||||||
p.Rules = append(p.Rules, UnixFromLog(log))
|
fmt.Printf("unknown log type: %s", log)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the rules in the profile
|
|
||||||
// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines
|
|
||||||
func (p *AppArmorProfile) Sort() {
|
|
||||||
sort.Slice(p.Rules, func(i, j int) bool {
|
|
||||||
typeOfI := reflect.TypeOf(p.Rules[i])
|
|
||||||
typeOfJ := reflect.TypeOf(p.Rules[j])
|
|
||||||
if typeOfI != typeOfJ {
|
|
||||||
valueOfI := typeToValue(typeOfI)
|
|
||||||
valueOfJ := typeToValue(typeOfJ)
|
|
||||||
if typeOfI == reflect.TypeOf((*Include)(nil)) && p.Rules[i].(*Include).IfExists {
|
|
||||||
valueOfI = "include_if_exists"
|
|
||||||
}
|
|
||||||
if typeOfJ == reflect.TypeOf((*Include)(nil)) && p.Rules[j].(*Include).IfExists {
|
|
||||||
valueOfJ = "include_if_exists"
|
|
||||||
}
|
|
||||||
return ruleWeights[valueOfI] < ruleWeights[valueOfJ]
|
|
||||||
}
|
|
||||||
return p.Rules[i].Less(p.Rules[j])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeRules merge similar rules together
|
|
||||||
// Steps:
|
|
||||||
// - Remove identical rules
|
|
||||||
// - Merge rule access. Eg: for same path, 'r' and 'w' becomes 'rw'
|
|
||||||
//
|
|
||||||
// Note: logs.regCleanLogs helps a lot to do a first cleaning
|
|
||||||
func (p *AppArmorProfile) MergeRules() {
|
|
||||||
for i := 0; i < len(p.Rules); i++ {
|
|
||||||
for j := i + 1; j < len(p.Rules); j++ {
|
|
||||||
typeOfI := reflect.TypeOf(p.Rules[i])
|
|
||||||
typeOfJ := reflect.TypeOf(p.Rules[j])
|
|
||||||
if typeOfI != typeOfJ {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If rules are identical, merge them
|
|
||||||
if p.Rules[i].Equals(p.Rules[j]) {
|
|
||||||
p.Rules = append(p.Rules[:j], p.Rules[j+1:]...)
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format the profile for better readability before printing it
|
|
||||||
// Follow: https://apparmor.pujol.io/development/guidelines/#the-file-block
|
|
||||||
func (p *AppArmorProfile) Format() {
|
|
||||||
const prefixOwner = " "
|
|
||||||
hasOwnerRule := false
|
|
||||||
for i := len(p.Rules) - 1; i > 0; i-- {
|
|
||||||
j := i - 1
|
|
||||||
typeOfI := reflect.TypeOf(p.Rules[i])
|
|
||||||
typeOfJ := reflect.TypeOf(p.Rules[j])
|
|
||||||
|
|
||||||
// File rule
|
|
||||||
if typeOfI == reflect.TypeOf((*File)(nil)) && typeOfJ == reflect.TypeOf((*File)(nil)) {
|
|
||||||
letterI := getLetterIn(fileAlphabet, p.Rules[i].(*File).Path)
|
|
||||||
letterJ := getLetterIn(fileAlphabet, p.Rules[j].(*File).Path)
|
|
||||||
|
|
||||||
// Add prefix before rule path to align with other rule
|
|
||||||
if p.Rules[i].(*File).Owner {
|
|
||||||
hasOwnerRule = true
|
|
||||||
} else if hasOwnerRule {
|
|
||||||
p.Rules[i].(*File).Prefix = prefixOwner
|
|
||||||
}
|
|
||||||
|
|
||||||
if letterI != letterJ {
|
|
||||||
// Add a new empty line between Files rule of different type
|
|
||||||
hasOwnerRule = false
|
|
||||||
p.Rules = append(p.Rules[:i], append([]ApparmorRule{&Rule{}}, p.Rules[i:]...)...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,328 +6,128 @@ package aa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/roddhjav/apparmor.d/pkg/paths"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func readprofile(path string) string {
|
func TestProfile_AddRule(t *testing.T) {
|
||||||
file := paths.New("../../").Join(path)
|
|
||||||
lines, err := file.ReadFileAsLines()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
res := ""
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
res += line + "\n"
|
|
||||||
}
|
|
||||||
return res[:len(res)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppArmorProfile_String(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
p *AppArmorProfile
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
p: &AppArmorProfile{},
|
|
||||||
want: ``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "foo",
|
|
||||||
p: &AppArmorProfile{
|
|
||||||
Preamble: Preamble{
|
|
||||||
Abi: []Abi{{IsMagic: true, Path: "abi/4.0"}},
|
|
||||||
Includes: []Include{{IsMagic: true, Path: "tunables/global"}},
|
|
||||||
Aliases: []Alias{{Path: "/mnt/usr", RewrittenPath: "/usr"}},
|
|
||||||
Variables: []Variable{{
|
|
||||||
Name: "exec_path",
|
|
||||||
Values: []string{"@{bin}/foo", "@{lib}/foo"},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
Profile: Profile{
|
|
||||||
Name: "foo",
|
|
||||||
Attachments: []string{"@{exec_path}"},
|
|
||||||
Attributes: map[string]string{"security.tagged": "allowed"},
|
|
||||||
Flags: []string{"complain", "attach_disconnected"},
|
|
||||||
Rules: []ApparmorRule{
|
|
||||||
&Include{IsMagic: true, Path: "abstractions/base"},
|
|
||||||
&Include{IsMagic: true, Path: "abstractions/nameservice-strict"},
|
|
||||||
rlimit1,
|
|
||||||
&Capability{Name: "dac_read_search"},
|
|
||||||
&Capability{Name: "dac_override"},
|
|
||||||
&Network{Domain: "inet", Type: "stream"},
|
|
||||||
&Network{Domain: "inet6", Type: "stream"},
|
|
||||||
&Mount{
|
|
||||||
MountConditions: MountConditions{
|
|
||||||
FsType: "fuse.portal",
|
|
||||||
Options: []string{"rw", "rbind"},
|
|
||||||
},
|
|
||||||
Source: "@{run}/user/@{uid}/ ",
|
|
||||||
MountPoint: "/",
|
|
||||||
},
|
|
||||||
&Umount{
|
|
||||||
MountConditions: MountConditions{},
|
|
||||||
MountPoint: "@{run}/user/@{uid}/",
|
|
||||||
},
|
|
||||||
&Signal{
|
|
||||||
Access: "receive",
|
|
||||||
Set: "term",
|
|
||||||
Peer: "at-spi-bus-launcher",
|
|
||||||
},
|
|
||||||
&Ptrace{Access: "read", Peer: "nautilus"},
|
|
||||||
&Unix{
|
|
||||||
Access: "send receive",
|
|
||||||
Type: "stream",
|
|
||||||
Address: "@/tmp/.ICE-unix/1995",
|
|
||||||
Peer: "gnome-shell",
|
|
||||||
PeerAddr: "none",
|
|
||||||
},
|
|
||||||
&Dbus{
|
|
||||||
Access: "bind",
|
|
||||||
Bus: "session",
|
|
||||||
Name: "org.gnome.*",
|
|
||||||
},
|
|
||||||
&Dbus{
|
|
||||||
Access: "receive",
|
|
||||||
Bus: "system",
|
|
||||||
Name: ":1.3",
|
|
||||||
Path: "/org/freedesktop/DBus",
|
|
||||||
Interface: "org.freedesktop.DBus",
|
|
||||||
Member: "AddMatch",
|
|
||||||
Label: "power-profiles-daemon",
|
|
||||||
},
|
|
||||||
&File{Path: "/opt/intel/oneapi/compiler/*/linux/lib/*.so./*", Access: "rm"},
|
|
||||||
&File{Path: "@{PROC}/@{pid}/task/@{tid}/comm", Access: "rw"},
|
|
||||||
&File{Path: "@{sys}/devices/@{pci}/class", Access: "r"},
|
|
||||||
includeLocal1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: readprofile("tests/string.aa"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := tt.p.String(); got != tt.want {
|
|
||||||
t.Errorf("AppArmorProfile.String() = |%v|, want |%v|", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppArmorProfile_AddRule(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
log map[string]string
|
log map[string]string
|
||||||
want *AppArmorProfile
|
want *Profile
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "capability",
|
name: "capability",
|
||||||
log: capability1Log,
|
log: capability1Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{capability1},
|
||||||
Rules: []ApparmorRule{capability1},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "network",
|
name: "network",
|
||||||
log: network1Log,
|
log: network1Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{network1},
|
||||||
Rules: []ApparmorRule{network1},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mount",
|
name: "mount",
|
||||||
log: mount2Log,
|
log: mount2Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{mount2},
|
||||||
Rules: []ApparmorRule{mount2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "signal",
|
name: "signal",
|
||||||
log: signal1Log,
|
log: signal1Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{signal1},
|
||||||
Rules: []ApparmorRule{signal1},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ptrace",
|
name: "ptrace",
|
||||||
log: ptrace2Log,
|
log: ptrace2Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{ptrace2},
|
||||||
Rules: []ApparmorRule{ptrace2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unix",
|
name: "unix",
|
||||||
log: unix1Log,
|
log: unix1Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{unix1},
|
||||||
Rules: []ApparmorRule{unix1},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dbus",
|
name: "dbus",
|
||||||
log: dbus2Log,
|
log: dbus2Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{dbus2},
|
||||||
Rules: []ApparmorRule{dbus2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "file",
|
name: "file",
|
||||||
log: file2Log,
|
log: file2Log,
|
||||||
want: &AppArmorProfile{
|
want: &Profile{
|
||||||
Profile: Profile{
|
Rules: Rules{file2},
|
||||||
Rules: []ApparmorRule{file2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := NewAppArmorProfile()
|
got := &Profile{}
|
||||||
got.AddRule(tt.log)
|
got.AddRule(tt.log)
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("AppArmorProfile.AddRule() = %v, want %v", got, tt.want)
|
t.Errorf("Profile.AddRule() = |%v|, want |%v|", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppArmorProfile_Sort(t *testing.T) {
|
func TestProfile_GetAttachments(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
origin *AppArmorProfile
|
Attachments []string
|
||||||
want *AppArmorProfile
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all",
|
name: "firefox",
|
||||||
origin: &AppArmorProfile{
|
Attachments: []string{
|
||||||
Profile: Profile{
|
"/{usr/,}bin/firefox{,-esr,-bin}",
|
||||||
Rules: []ApparmorRule{
|
"/{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
|
||||||
file2, network1, includeLocal1, dbus2, signal1, ptrace1,
|
"/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}",
|
||||||
capability2, file1, dbus1, unix2, signal2, mount2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
want: &AppArmorProfile{
|
want: "/{{usr/,}bin/firefox{,-esr,-bin},{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin},opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}}",
|
||||||
Profile: Profile{
|
},
|
||||||
Rules: []ApparmorRule{
|
{
|
||||||
capability2, network1, mount2, signal1, signal2, ptrace1,
|
name: "geoclue",
|
||||||
unix2, dbus2, dbus1, file1, file2, includeLocal1,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := tt.origin
|
p := &Profile{}
|
||||||
got.Sort()
|
p.Attachments = tt.Attachments
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if got := p.GetAttachments(); got != tt.want {
|
||||||
t.Errorf("AppArmorProfile.Sort() = %v, want %v", got, tt.want)
|
t.Errorf("Profile.GetAttachments() = %v, want %v", got, tt.want)
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppArmorProfile_MergeRules(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
origin *AppArmorProfile
|
|
||||||
want *AppArmorProfile
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "all",
|
|
||||||
origin: &AppArmorProfile{
|
|
||||||
Profile: Profile{
|
|
||||||
Rules: []ApparmorRule{capability1, capability1, network1, network1, file1, file1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: &AppArmorProfile{
|
|
||||||
Profile: Profile{
|
|
||||||
Rules: []ApparmorRule{capability1, network1, file1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := tt.origin
|
|
||||||
got.MergeRules()
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("AppArmorProfile.MergeRules() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppArmorProfile_Integration(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
p *AppArmorProfile
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
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"},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
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: readprofile("apparmor.d/profiles-a-f/aa-status"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,33 +4,69 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const PTRACE Kind = "ptrace"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[PTRACE] = requirement{
|
||||||
|
"access": []string{
|
||||||
|
"r", "w", "rw", "read", "readby", "trace", "tracedby",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Ptrace struct {
|
type Ptrace struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
Access string
|
Access []string
|
||||||
Peer string
|
Peer string
|
||||||
}
|
}
|
||||||
|
|
||||||
func PtraceFromLog(log map[string]string) ApparmorRule {
|
func newPtraceFromLog(log map[string]string) Rule {
|
||||||
return &Ptrace{
|
return &Ptrace{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
Access: toAccess(log["requested_mask"]),
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Access: Must(toAccess(PTRACE, log["requested_mask"])),
|
||||||
Peer: log["peer"],
|
Peer: log["peer"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Ptrace) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Ptrace) Less(other any) bool {
|
func (r *Ptrace) Less(other any) bool {
|
||||||
o, _ := other.(*Ptrace)
|
o, _ := other.(*Ptrace)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if len(r.Access) != len(o.Access) {
|
||||||
if r.Access == o.Access {
|
return len(r.Access) < len(o.Access)
|
||||||
return r.Peer == o.Peer
|
}
|
||||||
}
|
if r.Peer != o.Peer {
|
||||||
return r.Access < o.Access
|
return r.Peer == o.Peer
|
||||||
}
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Ptrace) Equals(other any) bool {
|
func (r *Ptrace) Equals(other any) bool {
|
||||||
o, _ := other.(*Ptrace)
|
o, _ := other.(*Ptrace)
|
||||||
return r.Access == o.Access && r.Peer == o.Peer &&
|
return slices.Equal(r.Access, o.Access) && r.Peer == o.Peer &&
|
||||||
r.Qualifier.Equals(o.Qualifier)
|
r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Ptrace) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Ptrace) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Ptrace) Kind() Kind {
|
||||||
|
return PTRACE
|
||||||
|
}
|
||||||
|
|
|
||||||
180
pkg/aa/resolve.go
Normal file
180
pkg/aa/resolve.go
Normal 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
273
pkg/aa/resolve_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,24 +4,69 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const (
|
||||||
|
RLIMIT Kind = "rlimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[RLIMIT] = requirement{
|
||||||
|
"keys": {
|
||||||
|
"cpu", "fsize", "data", "stack", "core", "rss", "nofile", "ofile",
|
||||||
|
"as", "nproc", "memlock", "locks", "sigpending", "msgqueue", "nice",
|
||||||
|
"rtprio", "rttime",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Rlimit struct {
|
type Rlimit struct {
|
||||||
|
RuleBase
|
||||||
Key string
|
Key string
|
||||||
Op string
|
Op string
|
||||||
Value string
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRlimitFromLog(log map[string]string) Rule {
|
||||||
|
return &Rlimit{
|
||||||
|
RuleBase: newRuleFromLog(log),
|
||||||
|
Key: log["key"],
|
||||||
|
Op: log["op"],
|
||||||
|
Value: log["value"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rlimit) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "keys", []string{r.Key}); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Rlimit) Less(other any) bool {
|
func (r *Rlimit) Less(other any) bool {
|
||||||
o, _ := other.(*Rlimit)
|
o, _ := other.(*Rlimit)
|
||||||
if r.Key == o.Key {
|
if r.Key != o.Key {
|
||||||
if r.Op == o.Op {
|
return r.Key < o.Key
|
||||||
return r.Value < o.Value
|
}
|
||||||
}
|
if r.Op != o.Op {
|
||||||
return r.Op < o.Op
|
return r.Op < o.Op
|
||||||
}
|
}
|
||||||
return r.Key < o.Key
|
return r.Value < o.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rlimit) Equals(other any) bool {
|
func (r *Rlimit) Equals(other any) bool {
|
||||||
o, _ := other.(*Rlimit)
|
o, _ := other.(*Rlimit)
|
||||||
return r.Key == o.Key && r.Op == o.Op && r.Value == o.Value
|
return r.Key == o.Key && r.Op == o.Op && r.Value == o.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Rlimit) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rlimit) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rlimit) Kind() Kind {
|
||||||
|
return RLIMIT
|
||||||
|
}
|
||||||
|
|
|
||||||
326
pkg/aa/rules.go
326
pkg/aa/rules.go
|
|
@ -5,133 +5,233 @@
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Rule struct {
|
type requirement map[string][]string
|
||||||
Comment string
|
|
||||||
NoNewPrivs bool
|
type constraint uint
|
||||||
FileInherit bool
|
|
||||||
|
const (
|
||||||
|
anyKind constraint = iota // The rule can be found in either preamble or profile
|
||||||
|
preambleKind // The rule can only be found in the preamble
|
||||||
|
blockKind // The rule can only be found in a profile
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kind represents an AppArmor rule kind.
|
||||||
|
type Kind string
|
||||||
|
|
||||||
|
func (k Kind) String() string {
|
||||||
|
return string(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) Less(other any) bool {
|
func (k Kind) Tok() string {
|
||||||
return false
|
if t, ok := tok[k]; ok {
|
||||||
}
|
return t
|
||||||
|
|
||||||
func (r *Rule) Equals(other any) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
return string(k)
|
||||||
|
}
|
||||||
|
|
||||||
audit := false
|
// Rule generic interface for all AppArmor rules
|
||||||
if log["apparmor"] == "AUDIT" {
|
type Rule interface {
|
||||||
audit = true
|
Validate() error
|
||||||
}
|
Less(other any) bool
|
||||||
|
Equals(other any) bool
|
||||||
|
String() string
|
||||||
|
Constraint() constraint
|
||||||
|
Kind() Kind
|
||||||
|
}
|
||||||
|
|
||||||
fileInherit := false
|
type Rules []Rule
|
||||||
if log["operation"] == "file_inherit" {
|
|
||||||
fileInherit = true
|
|
||||||
}
|
|
||||||
|
|
||||||
noNewPrivs := false
|
func (r Rules) Validate() error {
|
||||||
optional := false
|
for _, rule := range r {
|
||||||
msg := ""
|
if rule == nil {
|
||||||
switch log["error"] {
|
continue
|
||||||
case "-1":
|
|
||||||
if strings.Contains(log["info"], "optional:") {
|
|
||||||
optional = true
|
|
||||||
msg = strings.Replace(log["info"], "optional: ", "", 1)
|
|
||||||
} else {
|
|
||||||
noNewPrivs = true
|
|
||||||
}
|
}
|
||||||
case "-13":
|
if err := rule.Validate(); err != nil {
|
||||||
ignoreProfileInfo := []string{"namespace", "disconnected path"}
|
return err
|
||||||
for _, info := range ignoreProfileInfo {
|
}
|
||||||
if strings.Contains(log["info"], info) {
|
}
|
||||||
break
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// If rules are identical, merge them
|
||||||
|
if r[i].Equals(r[j]) {
|
||||||
|
r = r.Delete(j)
|
||||||
|
j--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
msg = log["info"]
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
return Qualifier{
|
|
||||||
Audit: audit,
|
|
||||||
Owner: owner,
|
|
||||||
NoNewPrivs: noNewPrivs,
|
|
||||||
FileInherit: fileInherit,
|
|
||||||
Optional: optional,
|
|
||||||
Comment: msg,
|
|
||||||
}
|
}
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Qualifier) Less(other Qualifier) bool {
|
// Sort the rules according to the guidelines:
|
||||||
if r.Owner == other.Owner {
|
// https://apparmor.pujol.io/development/guidelines/#guidelines
|
||||||
if r.Audit == other.Audit {
|
func (r Rules) Sort() Rules {
|
||||||
return r.AccessType < other.AccessType
|
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.Audit
|
|
||||||
}
|
}
|
||||||
return other.Owner
|
return r
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Alias) Equals(other Alias) bool {
|
|
||||||
return r.Path == other.Path && r.RewrittenPath == other.RewrittenPath
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,361 +9,455 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRule_FromLog(t *testing.T) {
|
func TestRules_FromLog(t *testing.T) {
|
||||||
tests := []struct {
|
for _, tt := range testRule {
|
||||||
name string
|
if tt.fromLog == nil {
|
||||||
fromLog func(map[string]string) ApparmorRule
|
continue
|
||||||
log map[string]string
|
}
|
||||||
want ApparmorRule
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "capbability",
|
|
||||||
fromLog: CapabilityFromLog,
|
|
||||||
log: capability1Log,
|
|
||||||
want: capability1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "network",
|
|
||||||
fromLog: NetworkFromLog,
|
|
||||||
log: network1Log,
|
|
||||||
want: network1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mount",
|
|
||||||
fromLog: MountFromLog,
|
|
||||||
log: mount1Log,
|
|
||||||
want: mount1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "umount",
|
|
||||||
fromLog: UmountFromLog,
|
|
||||||
log: umount1Log,
|
|
||||||
want: umount1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pivotroot",
|
|
||||||
fromLog: PivotRootFromLog,
|
|
||||||
log: pivotroot1Log,
|
|
||||||
want: pivotroot1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "changeprofile",
|
|
||||||
fromLog: ChangeProfileFromLog,
|
|
||||||
log: changeprofile1Log,
|
|
||||||
want: changeprofile1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "signal",
|
|
||||||
fromLog: SignalFromLog,
|
|
||||||
log: signal1Log,
|
|
||||||
want: signal1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ptrace/xdg-document-portal",
|
|
||||||
fromLog: PtraceFromLog,
|
|
||||||
log: ptrace1Log,
|
|
||||||
want: ptrace1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ptrace/snap-update-ns.firefox",
|
|
||||||
fromLog: PtraceFromLog,
|
|
||||||
log: ptrace2Log,
|
|
||||||
want: ptrace2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unix",
|
|
||||||
fromLog: UnixFromLog,
|
|
||||||
log: unix1Log,
|
|
||||||
want: unix1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dbus",
|
|
||||||
fromLog: DbusFromLog,
|
|
||||||
log: dbus1Log,
|
|
||||||
want: dbus1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file",
|
|
||||||
fromLog: FileFromLog,
|
|
||||||
log: file1Log,
|
|
||||||
want: file1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.want) {
|
if got := tt.fromLog(tt.log); !reflect.DeepEqual(got, tt.rule) {
|
||||||
t.Errorf("RuleFromLog() = %v, want %v", got, tt.want)
|
t.Errorf("RuleFromLog() = %v, want %v", got, tt.rule)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRule_Less(t *testing.T) {
|
func TestRules_Validate(t *testing.T) {
|
||||||
tests := []struct {
|
for _, tt := range testRule {
|
||||||
name string
|
|
||||||
rule ApparmorRule
|
|
||||||
other ApparmorRule
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "include1",
|
|
||||||
rule: include1,
|
|
||||||
other: includeLocal1,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "include2",
|
|
||||||
rule: include1,
|
|
||||||
other: include2,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "include3",
|
|
||||||
rule: include1,
|
|
||||||
other: include3,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rlimit",
|
|
||||||
rule: rlimit1,
|
|
||||||
other: rlimit2,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rlimit2",
|
|
||||||
rule: rlimit2,
|
|
||||||
other: rlimit2,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rlimit3",
|
|
||||||
rule: rlimit1,
|
|
||||||
other: rlimit3,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "capability",
|
|
||||||
rule: capability1,
|
|
||||||
other: capability2,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "network",
|
|
||||||
rule: network1,
|
|
||||||
other: network2,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mount",
|
|
||||||
rule: mount1,
|
|
||||||
other: mount2,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "umount",
|
|
||||||
rule: umount1,
|
|
||||||
other: umount2,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pivot_root1",
|
|
||||||
rule: pivotroot2,
|
|
||||||
other: pivotroot1,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pivot_root2",
|
|
||||||
rule: pivotroot1,
|
|
||||||
other: pivotroot3,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "change_profile1",
|
|
||||||
rule: changeprofile1,
|
|
||||||
other: changeprofile2,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "change_profile2",
|
|
||||||
rule: changeprofile1,
|
|
||||||
other: changeprofile3,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "signal",
|
|
||||||
rule: signal1,
|
|
||||||
other: signal2,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ptrace/less",
|
|
||||||
rule: ptrace1,
|
|
||||||
other: ptrace2,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ptrace/more",
|
|
||||||
rule: ptrace2,
|
|
||||||
other: ptrace1,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unix",
|
|
||||||
rule: unix1,
|
|
||||||
other: unix1,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dbus",
|
|
||||||
rule: dbus1,
|
|
||||||
other: dbus1,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dbus2",
|
|
||||||
rule: dbus2,
|
|
||||||
other: dbus3,
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file",
|
|
||||||
rule: file1,
|
|
||||||
other: file2,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file/empty",
|
|
||||||
rule: &File{},
|
|
||||||
other: &File{},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file/equal",
|
|
||||||
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
|
|
||||||
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file/close",
|
|
||||||
rule: &File{Path: "/usr/share/poppler/cMap/"},
|
|
||||||
other: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := tt.rule
|
if err := tt.rule.Validate(); (err != nil) != tt.wValidErr {
|
||||||
if got := r.Less(tt.other); got != tt.want {
|
t.Errorf("Rules.Validate() error = %v, wantErr %v", err, tt.wValidErr)
|
||||||
t.Errorf("Rule.Less() = %v, want %v", got, tt.want)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRule_Equals(t *testing.T) {
|
func TestRules_Less(t *testing.T) {
|
||||||
tests := []struct {
|
for _, tt := range testRule {
|
||||||
name string
|
if tt.oLess == nil {
|
||||||
rule ApparmorRule
|
continue
|
||||||
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) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := tt.rule
|
if got := tt.rule.Less(tt.oLess); got != tt.wLessErr {
|
||||||
if got := r.Equals(tt.other); got != tt.want {
|
t.Errorf("Rule.Less() = %v, want %v", got, tt.wLessErr)
|
||||||
t.Errorf("Rule.Equals() = %v, want %v", got, tt.want)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
oLess: includeLocal1,
|
||||||
|
wLessErr: false,
|
||||||
|
oEqual: includeLocal1,
|
||||||
|
wEqualErr: false,
|
||||||
|
wString: "include <abstraction/base>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "include2",
|
||||||
|
rule: include1,
|
||||||
|
oLess: include2,
|
||||||
|
wLessErr: false,
|
||||||
|
wString: "include <abstraction/base>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
oLess: rlimit2,
|
||||||
|
wLessErr: false,
|
||||||
|
oEqual: rlimit1,
|
||||||
|
wEqualErr: true,
|
||||||
|
wString: "set rlimit nproc <= 200,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rlimit2",
|
||||||
|
rule: rlimit2,
|
||||||
|
oLess: rlimit2,
|
||||||
|
wLessErr: false,
|
||||||
|
wString: "set rlimit cpu <= 2,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rlimit3",
|
||||||
|
rule: rlimit3,
|
||||||
|
oLess: rlimit1,
|
||||||
|
wLessErr: true,
|
||||||
|
|
||||||
|
wString: "set rlimit nproc < 2,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "userns",
|
||||||
|
rule: userns1,
|
||||||
|
oLess: userns2,
|
||||||
|
wLessErr: true,
|
||||||
|
oEqual: userns1,
|
||||||
|
wEqualErr: true,
|
||||||
|
wString: "userns,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "capbability",
|
||||||
|
fromLog: newCapabilityFromLog,
|
||||||
|
log: capability1Log,
|
||||||
|
rule: capability1,
|
||||||
|
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,
|
||||||
|
wValidErr: true,
|
||||||
|
oLess: network2,
|
||||||
|
wLessErr: false,
|
||||||
|
oEqual: network1,
|
||||||
|
wEqualErr: true,
|
||||||
|
wString: "network netlink raw,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mount",
|
||||||
|
fromLog: newMountFromLog,
|
||||||
|
log: mount1Log,
|
||||||
|
rule: mount1,
|
||||||
|
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,
|
||||||
|
oLess: umount2,
|
||||||
|
wLessErr: true,
|
||||||
|
oEqual: umount1,
|
||||||
|
wEqualErr: true,
|
||||||
|
wString: "umount /,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pivot_root1",
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
oLess: changeprofile2,
|
||||||
|
wLessErr: false,
|
||||||
|
wString: "change_profile -> systemd-user,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "change_profile2",
|
||||||
|
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,
|
||||||
|
oLess: signal2,
|
||||||
|
wLessErr: false,
|
||||||
|
oEqual: signal1,
|
||||||
|
wEqualErr: true,
|
||||||
|
wString: "signal receive set=kill peer=firefox//&firejail-default,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ptrace/xdg-document-portal",
|
||||||
|
fromLog: newPtraceFromLog,
|
||||||
|
log: ptrace1Log,
|
||||||
|
rule: ptrace1,
|
||||||
|
oLess: ptrace2,
|
||||||
|
wLessErr: false,
|
||||||
|
oEqual: ptrace1,
|
||||||
|
wEqualErr: true,
|
||||||
|
wString: "ptrace read peer=nautilus,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ptrace/snap-update-ns.firefox",
|
||||||
|
fromLog: newPtraceFromLog,
|
||||||
|
log: ptrace2Log,
|
||||||
|
rule: ptrace2,
|
||||||
|
oLess: ptrace1,
|
||||||
|
wLessErr: false,
|
||||||
|
oEqual: ptrace1,
|
||||||
|
wEqualErr: false,
|
||||||
|
wString: "ptrace readby peer=systemd-journald,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unix",
|
||||||
|
fromLog: newUnixFromLog,
|
||||||
|
log: unix1Log,
|
||||||
|
rule: unix1,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
oLess: file2,
|
||||||
|
wLessErr: true,
|
||||||
|
oEqual: file2,
|
||||||
|
wEqualErr: false,
|
||||||
|
wString: "/usr/share/poppler/cMap/Identity-H r,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file/empty",
|
||||||
|
rule: &File{},
|
||||||
|
oLess: &File{},
|
||||||
|
wLessErr: false,
|
||||||
|
wString: " ,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file/equal",
|
||||||
|
rule: &File{Path: "/usr/share/poppler/cMap/Identity-H"},
|
||||||
|
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", 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: []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/"},
|
||||||
|
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}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,90 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SIGNAL Kind = "signal"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
requirements[SIGNAL] = requirement{
|
||||||
|
"access": {
|
||||||
|
"r", "w", "rw", "read", "write", "send", "receive",
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"hup", "int", "quit", "ill", "trap", "abrt", "bus", "fpe",
|
||||||
|
"kill", "usr1", "segv", "usr2", "pipe", "alrm", "term", "stkflt",
|
||||||
|
"chld", "cont", "stop", "stp", "ttin", "ttou", "urg", "xcpu",
|
||||||
|
"xfsz", "vtalrm", "prof", "winch", "io", "pwr", "sys", "emt",
|
||||||
|
"exists", "rtmin+0", "rtmin+1", "rtmin+2", "rtmin+3", "rtmin+4",
|
||||||
|
"rtmin+5", "rtmin+6", "rtmin+7", "rtmin+8", "rtmin+9", "rtmin+10",
|
||||||
|
"rtmin+11", "rtmin+12", "rtmin+13", "rtmin+14", "rtmin+15",
|
||||||
|
"rtmin+16", "rtmin+17", "rtmin+18", "rtmin+19", "rtmin+20",
|
||||||
|
"rtmin+21", "rtmin+22", "rtmin+23", "rtmin+24", "rtmin+25",
|
||||||
|
"rtmin+26", "rtmin+27", "rtmin+28", "rtmin+29", "rtmin+30",
|
||||||
|
"rtmin+31", "rtmin+32",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Signal struct {
|
type Signal struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
Access string
|
Access []string
|
||||||
Set string
|
Set []string
|
||||||
Peer string
|
Peer string
|
||||||
}
|
}
|
||||||
|
|
||||||
func SignalFromLog(log map[string]string) ApparmorRule {
|
func newSignalFromLog(log map[string]string) Rule {
|
||||||
return &Signal{
|
return &Signal{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
Access: toAccess(log["requested_mask"]),
|
Qualifier: newQualifierFromLog(log),
|
||||||
Set: log["signal"],
|
Access: Must(toAccess(SIGNAL, log["requested_mask"])),
|
||||||
|
Set: []string{log["signal"]},
|
||||||
Peer: log["peer"],
|
Peer: log["peer"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Signal) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
if err := validateValues(r.Kind(), "set", r.Set); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Signal) Less(other any) bool {
|
func (r *Signal) Less(other any) bool {
|
||||||
o, _ := other.(*Signal)
|
o, _ := other.(*Signal)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if len(r.Access) != len(o.Access) {
|
||||||
if r.Access == o.Access {
|
return len(r.Access) < len(o.Access)
|
||||||
if r.Set == o.Set {
|
}
|
||||||
return r.Peer < o.Peer
|
if len(r.Set) != len(o.Set) {
|
||||||
}
|
return len(r.Set) < len(o.Set)
|
||||||
return r.Set < o.Set
|
}
|
||||||
}
|
if r.Peer != o.Peer {
|
||||||
return r.Access < o.Access
|
return r.Peer < o.Peer
|
||||||
}
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Signal) Equals(other any) bool {
|
func (r *Signal) Equals(other any) bool {
|
||||||
o, _ := other.(*Signal)
|
o, _ := other.(*Signal)
|
||||||
return r.Access == o.Access && r.Set == o.Set &&
|
return slices.Equal(r.Access, o.Access) && slices.Equal(r.Set, o.Set) &&
|
||||||
r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier)
|
r.Peer == o.Peer && r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Signal) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Signal) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Signal) Kind() Kind {
|
||||||
|
return SIGNAL
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,67 +6,90 @@ package aa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default indentation for apparmor profile (2 spaces)
|
|
||||||
const indentation = " "
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// Default indentation for apparmor profile (2 spaces)
|
||||||
|
Indentation = " "
|
||||||
|
|
||||||
|
// The current indentation level
|
||||||
|
IndentationLevel = 0
|
||||||
|
|
||||||
//go:embed templates/*.j2
|
//go:embed templates/*.j2
|
||||||
|
//go:embed templates/rule/*.j2
|
||||||
tmplFiles embed.FS
|
tmplFiles embed.FS
|
||||||
|
|
||||||
// The functions available in the template
|
// The functions available in the template
|
||||||
tmplFunctionMap = template.FuncMap{
|
tmplFunctionMap = template.FuncMap{
|
||||||
"typeof": typeOf,
|
"kindof": kindOf,
|
||||||
"join": join,
|
"join": join,
|
||||||
|
"cjoin": cjoin,
|
||||||
"indent": indent,
|
"indent": indent,
|
||||||
"overindent": indentDbus,
|
"overindent": indentDbus,
|
||||||
|
"setindent": setindent,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The apparmor profile template
|
// The apparmor templates
|
||||||
tmplAppArmorProfile = generateTemplate()
|
tmpl = generateTemplates([]Kind{
|
||||||
|
// Global templates
|
||||||
|
"apparmor",
|
||||||
|
PROFILE,
|
||||||
|
HAT,
|
||||||
|
"rules",
|
||||||
|
|
||||||
|
// Preamble templates
|
||||||
|
ABI,
|
||||||
|
ALIAS,
|
||||||
|
INCLUDE,
|
||||||
|
VARIABLE,
|
||||||
|
COMMENT,
|
||||||
|
|
||||||
|
// Rules templates
|
||||||
|
ALL, RLIMIT, USERNS, CAPABILITY, NETWORK,
|
||||||
|
MOUNT, REMOUNT, UMOUNT, PIVOTROOT, CHANGEPROFILE,
|
||||||
|
MQUEUE, IOURING, UNIX, PTRACE, SIGNAL, DBUS,
|
||||||
|
FILE, LINK,
|
||||||
|
})
|
||||||
|
|
||||||
// convert apparmor requested mask to apparmor access mode
|
// convert apparmor requested mask to apparmor access mode
|
||||||
requestedMaskToAccess = map[string]string{
|
maskToAccess = map[string]string{
|
||||||
"a": "w",
|
"a": "w",
|
||||||
"ac": "w",
|
"c": "w",
|
||||||
"c": "w",
|
"d": "w",
|
||||||
"d": "w",
|
"wc": "w",
|
||||||
"m": "rm",
|
"x": "ix",
|
||||||
"ra": "rw",
|
|
||||||
"wc": "w",
|
|
||||||
"wd": "w",
|
|
||||||
"wr": "rw",
|
|
||||||
"wrc": "rw",
|
|
||||||
"wrd": "rw",
|
|
||||||
"x": "rix",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The order the apparmor rules should be sorted
|
// The order the apparmor rules should be sorted
|
||||||
ruleAlphabet = []string{
|
ruleAlphabet = []Kind{
|
||||||
"include",
|
INCLUDE,
|
||||||
"rlimit",
|
ALL,
|
||||||
"capability",
|
RLIMIT,
|
||||||
"network",
|
USERNS,
|
||||||
"mount",
|
CAPABILITY,
|
||||||
"remount",
|
NETWORK,
|
||||||
"umount",
|
MOUNT,
|
||||||
"pivotroot",
|
REMOUNT,
|
||||||
"changeprofile",
|
UMOUNT,
|
||||||
"mqueue",
|
PIVOTROOT,
|
||||||
"signal",
|
CHANGEPROFILE,
|
||||||
"ptrace",
|
MQUEUE,
|
||||||
"unix",
|
IOURING,
|
||||||
"userns",
|
SIGNAL,
|
||||||
"iouring",
|
PTRACE,
|
||||||
"dbus",
|
UNIX,
|
||||||
"file",
|
DBUS,
|
||||||
|
FILE,
|
||||||
|
LINK,
|
||||||
|
PROFILE,
|
||||||
|
HAT,
|
||||||
"include_if_exists",
|
"include_if_exists",
|
||||||
}
|
}
|
||||||
ruleWeights = map[string]int{}
|
ruleWeights = generateWeights(ruleAlphabet)
|
||||||
|
|
||||||
// The order the apparmor file rules should be sorted
|
// The order the apparmor file rules should be sorted
|
||||||
fileAlphabet = []string{
|
fileAlphabet = []string{
|
||||||
|
|
@ -91,23 +114,65 @@ var (
|
||||||
"@{PROC}", // 10. Proc files
|
"@{PROC}", // 10. Proc files
|
||||||
"/dev", // 11. Dev files
|
"/dev", // 11. Dev files
|
||||||
"deny", // 12. Deny rules
|
"deny", // 12. Deny rules
|
||||||
|
"profile", // 13. Subprofiles
|
||||||
}
|
}
|
||||||
fileWeights = map[string]int{}
|
fileWeights = generateWeights(fileAlphabet)
|
||||||
|
|
||||||
|
// The order the rule values (access, type, domains, etc) should be sorted
|
||||||
|
requirements = map[Kind]requirement{}
|
||||||
|
requirementsWeights map[Kind]map[string]map[string]int
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateTemplate() *template.Template {
|
func init() {
|
||||||
res := template.New("profile.j2").Funcs(tmplFunctionMap)
|
requirementsWeights = generateRequirementsWeights(requirements)
|
||||||
res = template.Must(res.ParseFS(tmplFiles, "templates/*.j2"))
|
}
|
||||||
|
|
||||||
|
func generateTemplates(names []Kind) map[Kind]*template.Template {
|
||||||
|
res := make(map[Kind]*template.Template, len(names))
|
||||||
|
base := template.New("").Funcs(tmplFunctionMap)
|
||||||
|
base = template.Must(base.ParseFS(tmplFiles,
|
||||||
|
"templates/*.j2", "templates/rule/*.j2",
|
||||||
|
))
|
||||||
|
for _, name := range names {
|
||||||
|
t := template.Must(base.Clone())
|
||||||
|
t = template.Must(t.Parse(
|
||||||
|
fmt.Sprintf(`{{- template "%s" . -}}`, name),
|
||||||
|
))
|
||||||
|
res[name] = t
|
||||||
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func renderTemplate(name Kind, data any) string {
|
||||||
for i, r := range fileAlphabet {
|
var res strings.Builder
|
||||||
fileWeights[r] = i
|
template, ok := tmpl[name]
|
||||||
|
if !ok {
|
||||||
|
panic("template '" + name.String() + "' not found")
|
||||||
}
|
}
|
||||||
for i, r := range ruleAlphabet {
|
err := template.Execute(&res, data)
|
||||||
ruleWeights[r] = i
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
return res.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateWeights[T Kind | string](alphabet []T) map[T]int {
|
||||||
|
res := make(map[T]int, len(alphabet))
|
||||||
|
for i, r := range alphabet {
|
||||||
|
res[r] = i
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRequirementsWeights(requirements map[Kind]requirement) map[Kind]map[string]map[string]int {
|
||||||
|
res := make(map[Kind]map[string]map[string]int, len(requirements))
|
||||||
|
for rule, req := range requirements {
|
||||||
|
res[rule] = make(map[string]map[string]int, len(req))
|
||||||
|
for key, values := range req {
|
||||||
|
res[rule][key] = generateWeights(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func join(i any) string {
|
func join(i any) string {
|
||||||
|
|
@ -125,20 +190,48 @@ func join(i any) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func typeOf(i any) string {
|
func cjoin(i any) string {
|
||||||
return strings.TrimPrefix(reflect.TypeOf(i).String(), "*aa.")
|
switch reflect.TypeOf(i).Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
s := i.([]string)
|
||||||
|
if len(s) == 1 {
|
||||||
|
return s[0]
|
||||||
|
}
|
||||||
|
return "(" + strings.Join(s, " ") + ")"
|
||||||
|
case reflect.Map:
|
||||||
|
res := []string{}
|
||||||
|
for k, v := range i.(map[string]string) {
|
||||||
|
res = append(res, k+"="+v)
|
||||||
|
}
|
||||||
|
return "(" + strings.Join(res, " ") + ")"
|
||||||
|
default:
|
||||||
|
return i.(string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func typeToValue(i reflect.Type) string {
|
func kindOf(i any) string {
|
||||||
return strings.ToLower(strings.TrimPrefix(i.String(), "*aa."))
|
if i == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return i.(Rule).Kind().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setindent(i string) string {
|
||||||
|
switch i {
|
||||||
|
case "++":
|
||||||
|
IndentationLevel++
|
||||||
|
case "--":
|
||||||
|
IndentationLevel--
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func indent(s string) string {
|
func indent(s string) string {
|
||||||
return indentation + s
|
return strings.Repeat(Indentation, IndentationLevel) + s
|
||||||
}
|
}
|
||||||
|
|
||||||
func indentDbus(s string) string {
|
func indentDbus(s string) string {
|
||||||
return indentation + " " + s
|
return strings.Join([]string{Indentation, s}, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLetterIn(alphabet []string, in string) string {
|
func getLetterIn(alphabet []string, in string) string {
|
||||||
|
|
@ -149,10 +242,3 @@ func getLetterIn(alphabet []string, in string) string {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAccess(mask string) string {
|
|
||||||
if requestedMaskToAccess[mask] != "" {
|
|
||||||
return requestedMaskToAccess[mask]
|
|
||||||
}
|
|
||||||
return mask
|
|
||||||
}
|
|
||||||
|
|
|
||||||
14
pkg/aa/templates/apparmor.j2
Normal file
14
pkg/aa/templates/apparmor.j2
Normal 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 -}}
|
||||||
|
|
@ -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
18
pkg/aa/templates/hat.j2
Normal 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 -}}
|
||||||
|
|
@ -2,27 +2,8 @@
|
||||||
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
|
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
|
||||||
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
|
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
|
||||||
|
|
||||||
{{- range .Abi -}}
|
{{- define "profile" -}}
|
||||||
{{- if .IsMagic -}}
|
|
||||||
{{ "abi <" }}{{ .Path }}{{ ">,\n" }}
|
|
||||||
{{- else -}}
|
|
||||||
{{ "abi \"" }}{{ .Path }}{{ "\",\n" }}
|
|
||||||
{{- end }}
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{- range .Aliases -}}
|
|
||||||
{{ "alias " }}{{ .Path }}{{ " -> " }}{{ .RewrittenPath }}{{ ",\n" }}
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{- range .Includes -}}
|
|
||||||
{{ template "include" . }}{{ "\n" }}
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{- range .Variables -}}
|
|
||||||
{{ "@{" }}{{ .Name }}{{ "} = " }}{{ join .Values }}
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{- if or .Name .Attachments .Attributes .Flags -}}
|
|
||||||
{{- "profile" -}}
|
{{- "profile" -}}
|
||||||
{{- with .Name -}}
|
{{- with .Name -}}
|
||||||
{{ " " }}{{ . }}
|
{{ " " }}{{ . }}
|
||||||
|
|
@ -36,260 +17,11 @@
|
||||||
{{- with .Flags -}}
|
{{- with .Flags -}}
|
||||||
{{ " flags=(" }}{{ join . }}{{ ")" }}
|
{{ " flags=(" }}{{ join . }}{{ ")" }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{ " {\n" }}
|
|
||||||
{{- end -}}
|
{{- " {\n" -}}
|
||||||
|
{{- setindent "++" -}}
|
||||||
{{- $oldtype := "" -}}
|
{{- template "rules" .Rules -}}
|
||||||
{{- range .Rules -}}
|
{{- setindent "--" -}}
|
||||||
{{- $type := typeof . -}}
|
{{- indent "}" -}}
|
||||||
{{- 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" -}}
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
|
||||||
14
pkg/aa/templates/rule/abi.j2
Normal file
14
pkg/aa/templates/rule/abi.j2
Normal 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 -}}
|
||||||
12
pkg/aa/templates/rule/alias.j2
Normal file
12
pkg/aa/templates/rule/alias.j2
Normal 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 -}}
|
||||||
9
pkg/aa/templates/rule/all.j2
Normal file
9
pkg/aa/templates/rule/all.j2
Normal 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 -}}
|
||||||
13
pkg/aa/templates/rule/capability.j2
Normal file
13
pkg/aa/templates/rule/capability.j2
Normal 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 -}}
|
||||||
19
pkg/aa/templates/rule/change_profile.j2
Normal file
19
pkg/aa/templates/rule/change_profile.j2
Normal 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 -}}
|
||||||
25
pkg/aa/templates/rule/comment.j2
Normal file
25
pkg/aa/templates/rule/comment.j2
Normal 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 -}}
|
||||||
43
pkg/aa/templates/rule/dbus.j2
Normal file
43
pkg/aa/templates/rule/dbus.j2
Normal 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 -}}
|
||||||
41
pkg/aa/templates/rule/file.j2
Normal file
41
pkg/aa/templates/rule/file.j2
Normal 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 -}}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
|
||||||
|
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
|
||||||
|
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
|
||||||
|
|
||||||
{{- define "include" -}}
|
{{- define "include" -}}
|
||||||
{{- "include" -}}
|
{{- "include" -}}
|
||||||
{{- if .IfExists -}}
|
{{- if .IfExists -}}
|
||||||
|
|
@ -8,4 +12,5 @@
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
{{ " \"" }}{{ .Path }}{{ "\"" }}
|
{{ " \"" }}{{ .Path }}{{ "\"" }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- template "comment" . -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
16
pkg/aa/templates/rule/io_uring.j2
Normal file
16
pkg/aa/templates/rule/io_uring.j2
Normal 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 -}}
|
||||||
54
pkg/aa/templates/rule/mount.j2
Normal file
54
pkg/aa/templates/rule/mount.j2
Normal 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 -}}
|
||||||
22
pkg/aa/templates/rule/mqueue.j2
Normal file
22
pkg/aa/templates/rule/mqueue.j2
Normal 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 -}}
|
||||||
20
pkg/aa/templates/rule/network.j2
Normal file
20
pkg/aa/templates/rule/network.j2
Normal 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 -}}
|
||||||
19
pkg/aa/templates/rule/pivot_root.j2
Normal file
19
pkg/aa/templates/rule/pivot_root.j2
Normal 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 -}}
|
||||||
16
pkg/aa/templates/rule/ptrace.j2
Normal file
16
pkg/aa/templates/rule/ptrace.j2
Normal 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 -}}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
{{- /* apparmor.d - Full set of apparmor profiles */ -}}
|
||||||
|
{{- /* Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io> */ -}}
|
||||||
|
{{- /* SPDX-License-Identifier: GPL-2.0-only */ -}}
|
||||||
|
|
||||||
{{- define "qualifier" -}}
|
{{- define "qualifier" -}}
|
||||||
{{- with .Prefix -}}
|
{{- with .Prefix -}}
|
||||||
{{ . }}
|
{{ . }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if .Owner -}}
|
|
||||||
{{- "owner " -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- if .Audit -}}
|
{{- if .Audit -}}
|
||||||
{{- "audit " -}}
|
{{- "audit " -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
7
pkg/aa/templates/rule/rlimit.j2
Normal file
7
pkg/aa/templates/rule/rlimit.j2
Normal 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 -}}
|
||||||
19
pkg/aa/templates/rule/signal.j2
Normal file
19
pkg/aa/templates/rule/signal.j2
Normal 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 -}}
|
||||||
35
pkg/aa/templates/rule/unix.j2
Normal file
35
pkg/aa/templates/rule/unix.j2
Normal 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 -}}
|
||||||
9
pkg/aa/templates/rule/userns.j2
Normal file
9
pkg/aa/templates/rule/userns.j2
Normal 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 -}}
|
||||||
14
pkg/aa/templates/rule/variable.j2
Normal file
14
pkg/aa/templates/rule/variable.j2
Normal 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
125
pkg/aa/templates/rules.j2
Normal 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 -}}
|
||||||
127
pkg/aa/unix.go
127
pkg/aa/unix.go
|
|
@ -4,70 +4,109 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
type Unix struct {
|
import (
|
||||||
Qualifier
|
"fmt"
|
||||||
Access string
|
"slices"
|
||||||
Type string
|
)
|
||||||
Protocol string
|
|
||||||
Address string
|
const UNIX Kind = "unix"
|
||||||
Label string
|
|
||||||
Attr string
|
func init() {
|
||||||
Opt string
|
requirements[UNIX] = requirement{
|
||||||
Peer string
|
"access": []string{
|
||||||
PeerAddr string
|
"create", "bind", "listen", "accept", "connect", "shutdown",
|
||||||
|
"getattr", "setattr", "getopt", "setopt", "send", "receive",
|
||||||
|
"r", "w", "rw",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnixFromLog(log map[string]string) ApparmorRule {
|
type Unix struct {
|
||||||
|
RuleBase
|
||||||
|
Qualifier
|
||||||
|
Access []string
|
||||||
|
Type string
|
||||||
|
Protocol string
|
||||||
|
Address string
|
||||||
|
Label string
|
||||||
|
Attr string
|
||||||
|
Opt string
|
||||||
|
PeerLabel string
|
||||||
|
PeerAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUnixFromLog(log map[string]string) Rule {
|
||||||
return &Unix{
|
return &Unix{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
Access: toAccess(log["requested_mask"]),
|
Qualifier: newQualifierFromLog(log),
|
||||||
|
Access: Must(toAccess(UNIX, log["requested_mask"])),
|
||||||
Type: log["sock_type"],
|
Type: log["sock_type"],
|
||||||
Protocol: log["protocol"],
|
Protocol: log["protocol"],
|
||||||
Address: log["addr"],
|
Address: log["addr"],
|
||||||
Label: log["peer_label"],
|
Label: log["label"],
|
||||||
Attr: log["attr"],
|
Attr: log["attr"],
|
||||||
Opt: log["opt"],
|
Opt: log["opt"],
|
||||||
Peer: log["peer"],
|
PeerLabel: log["peer"],
|
||||||
PeerAddr: log["peer_addr"],
|
PeerAddr: log["peer_addr"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Unix) Validate() error {
|
||||||
|
if err := validateValues(r.Kind(), "access", r.Access); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", r, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Unix) Less(other any) bool {
|
func (r *Unix) Less(other any) bool {
|
||||||
o, _ := other.(*Unix)
|
o, _ := other.(*Unix)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if len(r.Access) != len(o.Access) {
|
||||||
if r.Access == o.Access {
|
return len(r.Access) < len(o.Access)
|
||||||
if r.Type == o.Type {
|
}
|
||||||
if r.Protocol == o.Protocol {
|
if r.Type != o.Type {
|
||||||
if r.Address == o.Address {
|
return r.Type < o.Type
|
||||||
if r.Label == o.Label {
|
}
|
||||||
if r.Attr == o.Attr {
|
if r.Protocol != o.Protocol {
|
||||||
if r.Opt == o.Opt {
|
return r.Protocol < o.Protocol
|
||||||
if r.Peer == o.Peer {
|
}
|
||||||
return r.PeerAddr < o.PeerAddr
|
if r.Address != o.Address {
|
||||||
}
|
return r.Address < o.Address
|
||||||
return r.Peer < o.Peer
|
}
|
||||||
}
|
if r.Label != o.Label {
|
||||||
return r.Opt < o.Opt
|
return r.Label < o.Label
|
||||||
}
|
}
|
||||||
return r.Attr < o.Attr
|
if r.Attr != o.Attr {
|
||||||
}
|
return r.Attr < o.Attr
|
||||||
return r.Label < o.Label
|
}
|
||||||
}
|
if r.Opt != o.Opt {
|
||||||
return r.Address < o.Address
|
return r.Opt < o.Opt
|
||||||
}
|
}
|
||||||
return r.Protocol < o.Protocol
|
if r.PeerLabel != o.PeerLabel {
|
||||||
}
|
return r.PeerLabel < o.PeerLabel
|
||||||
return r.Type < o.Type
|
}
|
||||||
}
|
if r.PeerAddr != o.PeerAddr {
|
||||||
return r.Access < o.Access
|
return r.PeerAddr < o.PeerAddr
|
||||||
}
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Unix) Equals(other any) bool {
|
func (r *Unix) Equals(other any) bool {
|
||||||
o, _ := other.(*Unix)
|
o, _ := other.(*Unix)
|
||||||
return r.Access == o.Access && r.Type == o.Type &&
|
return slices.Equal(r.Access, o.Access) && r.Type == o.Type &&
|
||||||
r.Protocol == o.Protocol && r.Address == o.Address &&
|
r.Protocol == o.Protocol && r.Address == o.Address &&
|
||||||
r.Label == o.Label && r.Attr == o.Attr && r.Opt == o.Opt &&
|
r.Label == o.Label && r.Attr == o.Attr && r.Opt == o.Opt &&
|
||||||
r.Peer == o.Peer && r.PeerAddr == o.PeerAddr && r.Qualifier.Equals(o.Qualifier)
|
r.PeerLabel == o.PeerLabel && r.PeerAddr == o.PeerAddr &&
|
||||||
|
r.Qualifier.Equals(o.Qualifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Unix) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Unix) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Unix) Kind() Kind {
|
||||||
|
return UNIX
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,29 @@
|
||||||
|
|
||||||
package aa
|
package aa
|
||||||
|
|
||||||
|
const USERNS Kind = "userns"
|
||||||
|
|
||||||
type Userns struct {
|
type Userns struct {
|
||||||
|
RuleBase
|
||||||
Qualifier
|
Qualifier
|
||||||
Create bool
|
Create bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func UsernsFromLog(log map[string]string) ApparmorRule {
|
func newUsernsFromLog(log map[string]string) Rule {
|
||||||
return &Userns{
|
return &Userns{
|
||||||
Qualifier: NewQualifierFromLog(log),
|
RuleBase: newRuleFromLog(log),
|
||||||
|
Qualifier: newQualifierFromLog(log),
|
||||||
Create: true,
|
Create: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Userns) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Userns) Less(other any) bool {
|
func (r *Userns) Less(other any) bool {
|
||||||
o, _ := other.(*Userns)
|
o, _ := other.(*Userns)
|
||||||
if r.Qualifier.Equals(o.Qualifier) {
|
if r.Create != o.Create {
|
||||||
return r.Create
|
return r.Create
|
||||||
}
|
}
|
||||||
return r.Qualifier.Less(o.Qualifier)
|
return r.Qualifier.Less(o.Qualifier)
|
||||||
|
|
@ -28,3 +36,15 @@ func (r *Userns) Equals(other any) bool {
|
||||||
o, _ := other.(*Userns)
|
o, _ := other.(*Userns)
|
||||||
return r.Create == o.Create && r.Qualifier.Equals(o.Qualifier)
|
return r.Create == o.Create && r.Qualifier.Equals(o.Qualifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Userns) String() string {
|
||||||
|
return renderTemplate(r.Kind(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Userns) Constraint() constraint {
|
||||||
|
return blockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Userns) Kind() Kind {
|
||||||
|
return USERNS
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, ",") + "}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -197,8 +197,8 @@ func (aaLogs AppArmorLogs) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseToProfiles convert the log data into a new AppArmorProfiles
|
// ParseToProfiles convert the log data into a new AppArmorProfiles
|
||||||
func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles {
|
func (aaLogs AppArmorLogs) ParseToProfiles() map[string]*aa.Profile {
|
||||||
profiles := make(aa.AppArmorProfiles, 0)
|
profiles := make(map[string]*aa.Profile, 0)
|
||||||
for _, log := range aaLogs {
|
for _, log := range aaLogs {
|
||||||
name := ""
|
name := ""
|
||||||
if strings.Contains(log["operation"], "dbus") {
|
if strings.Contains(log["operation"], "dbus") {
|
||||||
|
|
@ -208,8 +208,7 @@ func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := profiles[name]; !ok {
|
if _, ok := profiles[name]; !ok {
|
||||||
profile := &aa.AppArmorProfile{}
|
profile := &aa.Profile{Header: aa.Header{Name: name}}
|
||||||
profile.Name = name
|
|
||||||
profile.AddRule(log)
|
profile.AddRule(log)
|
||||||
profiles[name] = profile
|
profiles[name] = profile
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -292,44 +292,40 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
aaLogs AppArmorLogs
|
aaLogs AppArmorLogs
|
||||||
want aa.AppArmorProfiles
|
want map[string]*aa.Profile
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "",
|
name: "",
|
||||||
aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...),
|
aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...),
|
||||||
want: aa.AppArmorProfiles{
|
want: map[string]*aa.Profile{
|
||||||
"kmod": &aa.AppArmorProfile{
|
"kmod": {
|
||||||
Profile: aa.Profile{
|
Header: aa.Header{Name: "kmod"},
|
||||||
Name: "kmod",
|
Rules: aa.Rules{
|
||||||
Rules: aa.Rules{
|
&aa.Unix{
|
||||||
&aa.Unix{
|
RuleBase: aa.RuleBase{FileInherit: true},
|
||||||
Qualifier: aa.Qualifier{FileInherit: true},
|
Access: []string{"send", "receive"},
|
||||||
Access: "send receive",
|
Type: "stream",
|
||||||
Type: "stream",
|
Protocol: "0",
|
||||||
Protocol: "0",
|
},
|
||||||
},
|
&aa.Unix{
|
||||||
&aa.Unix{
|
RuleBase: aa.RuleBase{FileInherit: true},
|
||||||
Qualifier: aa.Qualifier{FileInherit: true},
|
Access: []string{"send", "receive"},
|
||||||
Access: "send receive",
|
Type: "stream",
|
||||||
Type: "stream",
|
Protocol: "0",
|
||||||
Protocol: "0",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"power-profiles-daemon": &aa.AppArmorProfile{
|
"power-profiles-daemon": {
|
||||||
Profile: aa.Profile{
|
Header: aa.Header{Name: "power-profiles-daemon"},
|
||||||
Name: "power-profiles-daemon",
|
Rules: aa.Rules{
|
||||||
Rules: aa.Rules{
|
&aa.Dbus{
|
||||||
&aa.Dbus{
|
Access: []string{"send"},
|
||||||
Access: "send",
|
Bus: "system",
|
||||||
Bus: "system",
|
Path: "/org/freedesktop/DBus",
|
||||||
Name: "org.freedesktop.DBus",
|
Interface: "org.freedesktop.DBus",
|
||||||
Path: "/org/freedesktop/DBus",
|
Member: "AddMatch",
|
||||||
Interface: "org.freedesktop.DBus",
|
PeerName: "org.freedesktop.DBus",
|
||||||
Member: "AddMatch",
|
PeerLabel: "dbus-daemon",
|
||||||
Label: "dbus-daemon",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,6 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b ABI3) Apply(profile string) string {
|
func (b ABI3) Apply(opt *Option, profile string) (string, error) {
|
||||||
return regAbi4To3.Replace(profile)
|
return regAbi4To3.Replace(profile), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Complain) Apply(profile string) string {
|
func (b Complain) Apply(opt *Option, profile string) (string, error) {
|
||||||
flags := []string{}
|
flags := []string{}
|
||||||
matches := regFlags.FindStringSubmatch(profile)
|
matches := regFlags.FindStringSubmatch(profile)
|
||||||
if len(matches) != 0 {
|
if len(matches) != 0 {
|
||||||
flags = strings.Split(matches[1], ",")
|
flags = strings.Split(matches[1], ",")
|
||||||
if slices.Contains(flags, "complain") {
|
if slices.Contains(flags, "complain") {
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flags = append(flags, "complain")
|
flags = append(flags, "complain")
|
||||||
|
|
@ -44,5 +44,5 @@ func (b Complain) Apply(profile string) string {
|
||||||
|
|
||||||
// Remove all flags definition, then set manifest' flags
|
// Remove all flags definition, then set manifest' flags
|
||||||
profile = regFlags.ReplaceAllLiteralString(profile, "")
|
profile = regFlags.ReplaceAllLiteralString(profile, "")
|
||||||
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags)
|
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ package builder
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/roddhjav/apparmor.d/pkg/paths"
|
||||||
"github.com/roddhjav/apparmor.d/pkg/prebuild/cfg"
|
"github.com/roddhjav/apparmor.d/pkg/prebuild/cfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,7 +22,20 @@ var (
|
||||||
// Main directive interface
|
// Main directive interface
|
||||||
type Builder interface {
|
type Builder interface {
|
||||||
cfg.BaseInterface
|
cfg.BaseInterface
|
||||||
Apply(profile string) string
|
Apply(opt *Option, profile string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder options
|
||||||
|
type Option struct {
|
||||||
|
Name string
|
||||||
|
File *paths.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOption(file *paths.Path) *Option {
|
||||||
|
return &Option{
|
||||||
|
Name: file.Base(),
|
||||||
|
File: file,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register(names ...string) {
|
func Register(names ...string) {
|
||||||
|
|
@ -37,3 +51,15 @@ func Register(names ...string) {
|
||||||
func RegisterBuilder(d Builder) {
|
func RegisterBuilder(d Builder) {
|
||||||
Builders[d.Name()] = d
|
Builders[d.Name()] = d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Run(file *paths.Path, profile string) (string, error) {
|
||||||
|
var err error
|
||||||
|
opt := NewOption(file)
|
||||||
|
for _, b := range Builds {
|
||||||
|
profile, err = b.Apply(opt, profile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s %s: %w", b.Name(), opt.File, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ package builder
|
||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/roddhjav/apparmor.d/pkg/prebuild/cfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuilder_Apply(t *testing.T) {
|
func TestBuilder_Apply(t *testing.T) {
|
||||||
|
|
@ -15,6 +17,7 @@ func TestBuilder_Apply(t *testing.T) {
|
||||||
b Builder
|
b Builder
|
||||||
profile string
|
profile string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "abi3",
|
name: "abi3",
|
||||||
|
|
@ -215,7 +218,7 @@ func TestBuilder_Apply(t *testing.T) {
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "userspace-1",
|
name: "userspace-2",
|
||||||
b: Builders["userspace"],
|
b: Builders["userspace"],
|
||||||
profile: `
|
profile: `
|
||||||
profile foo /usr/bin/foo {
|
profile foo /usr/bin/foo {
|
||||||
|
|
@ -237,7 +240,13 @@ func TestBuilder_Apply(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := tt.b.Apply(tt.profile); got != tt.want {
|
opt := &Option{File: cfg.RootApparmord.Join(tt.name)}
|
||||||
|
got, err := tt.b.Apply(opt, tt.profile)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Builder.Apply() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
t.Errorf("Builder.Apply() = %v, want %v", got, tt.want)
|
t.Errorf("Builder.Apply() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -257,7 +266,6 @@ func TestRegister(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
Register(tt.names...)
|
Register(tt.names...)
|
||||||
for _, name := range tt.names {
|
for _, name := range tt.names {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,6 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Dev) Apply(profile string) string {
|
func (b Dev) Apply(opt *Option, profile string) (string, error) {
|
||||||
return regDev.Replace(profile)
|
return regDev.Replace(profile), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,16 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Enforce) Apply(profile string) string {
|
func (b Enforce) Apply(opt *Option, profile string) (string, error) {
|
||||||
matches := regFlags.FindStringSubmatch(profile)
|
matches := regFlags.FindStringSubmatch(profile)
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := strings.Split(matches[1], ",")
|
flags := strings.Split(matches[1], ",")
|
||||||
idx := slices.Index(flags, "complain")
|
idx := slices.Index(flags, "complain")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
flags = slices.Delete(flags, idx, idx+1)
|
flags = slices.Delete(flags, idx, idx+1)
|
||||||
strFlags := "{"
|
strFlags := "{"
|
||||||
|
|
@ -43,5 +43,5 @@ func (b Enforce) Apply(profile string) string {
|
||||||
|
|
||||||
// Remove all flags definition, then set new flags
|
// Remove all flags definition, then set new flags
|
||||||
profile = regFlags.ReplaceAllLiteralString(profile, "")
|
profile = regFlags.ReplaceAllLiteralString(profile, "")
|
||||||
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags)
|
return regProfileHeader.ReplaceAllLiteralString(profile, strFlags), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,6 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b FullSystemPolicy) Apply(profile string) string {
|
func (b FullSystemPolicy) Apply(opt *Option, profile string) (string, error) {
|
||||||
return regFullSystemPolicy.Replace(profile)
|
return regFullSystemPolicy.Replace(profile), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,26 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Userspace) Apply(profile string) string {
|
func (b Userspace) Apply(opt *Option, profile string) (string, error) {
|
||||||
p := aa.DefaultTunables()
|
if ok, _ := opt.File.IsInsideDir(cfg.RootApparmord.Join("abstractions")); ok {
|
||||||
p.ParseVariables(profile)
|
return profile, nil
|
||||||
p.ResolveAttachments()
|
}
|
||||||
att := p.NestAttachments()
|
if ok, _ := opt.File.IsInsideDir(cfg.RootApparmord.Join("tunables")); ok {
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f := aa.DefaultTunables()
|
||||||
|
if err := f.Parse(profile); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := f.Resolve(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
att := f.GetDefaultProfile().GetAttachments()
|
||||||
matches := regAttachments.FindAllString(profile, -1)
|
matches := regAttachments.FindAllString(profile, -1)
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
strheader := strings.Replace(matches[0], "@{exec_path}", att, -1)
|
strheader := strings.Replace(matches[0], "@{exec_path}", att, -1)
|
||||||
return regAttachments.ReplaceAllLiteralString(profile, strheader)
|
return regAttachments.ReplaceAllLiteralString(profile, strheader), nil
|
||||||
}
|
}
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ var (
|
||||||
// Main directive interface
|
// Main directive interface
|
||||||
type Directive interface {
|
type Directive interface {
|
||||||
cfg.BaseInterface
|
cfg.BaseInterface
|
||||||
Apply(opt *Option, profile string) string
|
Apply(opt *Option, profile string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directive options
|
// Directive options
|
||||||
|
|
@ -72,14 +72,18 @@ func RegisterDirective(d Directive) {
|
||||||
Directives[d.Name()] = d
|
Directives[d.Name()] = d
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(file *paths.Path, profile string) string {
|
func Run(file *paths.Path, profile string) (string, error) {
|
||||||
|
var err error
|
||||||
for _, match := range regDirective.FindAllStringSubmatch(profile, -1) {
|
for _, match := range regDirective.FindAllStringSubmatch(profile, -1) {
|
||||||
opt := NewOption(file, match)
|
opt := NewOption(file, match)
|
||||||
drtv, ok := Directives[opt.Name]
|
drtv, ok := Directives[opt.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("Unknown directive: %s", opt.Name))
|
return "", fmt.Errorf("Unknown directive '%s' in %s", opt.Name, opt.File)
|
||||||
|
}
|
||||||
|
profile, err = drtv.Apply(opt, profile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s %s: %w", drtv.Name(), opt.File, err)
|
||||||
}
|
}
|
||||||
profile = drtv.Apply(opt, profile)
|
|
||||||
}
|
}
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ func TestRun(t *testing.T) {
|
||||||
file *paths.Path
|
file *paths.Path
|
||||||
profile string
|
profile string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "none",
|
name: "none",
|
||||||
|
|
@ -86,7 +87,12 @@ func TestRun(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := Run(tt.file, tt.profile); got != tt.want {
|
got, err := Run(tt.file, tt.profile)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
t.Errorf("Run() = %v, want %v", got, tt.want)
|
t.Errorf("Run() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -50,41 +50,47 @@ func setInterfaces(rules map[string]string) []string {
|
||||||
return interfaces
|
return interfaces
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Dbus) Apply(opt *Option, profile string) string {
|
func (d Dbus) Apply(opt *Option, profile string) (string, error) {
|
||||||
var p *aa.AppArmorProfile
|
var r aa.Rules
|
||||||
|
|
||||||
action := d.sanityCheck(opt)
|
action, err := d.sanityCheck(opt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
switch action {
|
switch action {
|
||||||
case "own":
|
case "own":
|
||||||
p = d.own(opt.ArgMap)
|
r = d.own(opt.ArgMap)
|
||||||
case "talk":
|
case "talk":
|
||||||
p = d.talk(opt.ArgMap)
|
r = d.talk(opt.ArgMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
generatedDbus := p.String()
|
aa.IndentationLevel = strings.Count(
|
||||||
|
strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation,
|
||||||
|
)
|
||||||
|
generatedDbus := r.String()
|
||||||
lenDbus := len(generatedDbus)
|
lenDbus := len(generatedDbus)
|
||||||
generatedDbus = generatedDbus[:lenDbus-1]
|
generatedDbus = generatedDbus[:lenDbus-1]
|
||||||
profile = strings.Replace(profile, opt.Raw, generatedDbus, -1)
|
profile = strings.Replace(profile, opt.Raw, generatedDbus, -1)
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Dbus) sanityCheck(opt *Option) string {
|
func (d Dbus) sanityCheck(opt *Option) (string, error) {
|
||||||
if len(opt.ArgList) < 1 {
|
if len(opt.ArgList) < 1 {
|
||||||
panic(fmt.Sprintf("Unknown dbus action: %s in %s", opt.Name, opt.File))
|
return "", fmt.Errorf("Unknown dbus action: %s in %s", opt.Name, opt.File)
|
||||||
}
|
}
|
||||||
action := opt.ArgList[0]
|
action := opt.ArgList[0]
|
||||||
if action != "own" && action != "talk" {
|
if action != "own" && action != "talk" {
|
||||||
panic(fmt.Sprintf("Unknown dbus action: %s in %s", opt.Name, opt.File))
|
return "", fmt.Errorf("Unknown dbus action: %s in %s", opt.Name, opt.File)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, present := opt.ArgMap["name"]; !present {
|
if _, present := opt.ArgMap["name"]; !present {
|
||||||
panic(fmt.Sprintf("Missing name for 'dbus: %s' in %s", action, opt.File))
|
return "", fmt.Errorf("Missing name for 'dbus: %s' in %s", action, opt.File)
|
||||||
}
|
}
|
||||||
if _, present := opt.ArgMap["bus"]; !present {
|
if _, present := opt.ArgMap["bus"]; !present {
|
||||||
panic(fmt.Sprintf("Missing bus for '%s' in %s", opt.ArgMap["name"], opt.File))
|
return "", fmt.Errorf("Missing bus for '%s' in %s", opt.ArgMap["name"], opt.File)
|
||||||
}
|
}
|
||||||
if _, present := opt.ArgMap["label"]; !present && action == "talk" {
|
if _, present := opt.ArgMap["label"]; !present && action == "talk" {
|
||||||
panic(fmt.Sprintf("Missing label for '%s' in %s", opt.ArgMap["name"], opt.File))
|
return "", fmt.Errorf("Missing label for '%s' in %s", opt.ArgMap["name"], opt.File)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default values
|
// Set default values
|
||||||
|
|
@ -92,66 +98,66 @@ func (d Dbus) sanityCheck(opt *Option) string {
|
||||||
opt.ArgMap["path"] = "/" + strings.Replace(opt.ArgMap["name"], ".", "/", -1) + "{,/**}"
|
opt.ArgMap["path"] = "/" + strings.Replace(opt.ArgMap["name"], ".", "/", -1) + "{,/**}"
|
||||||
}
|
}
|
||||||
opt.ArgMap["name"] += "{,.*}"
|
opt.ArgMap["name"] += "{,.*}"
|
||||||
return action
|
return action, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile {
|
func (d Dbus) own(rules map[string]string) aa.Rules {
|
||||||
interfaces := setInterfaces(rules)
|
interfaces := setInterfaces(rules)
|
||||||
p := &aa.AppArmorProfile{}
|
res := aa.Rules{}
|
||||||
p.Rules = append(p.Rules, &aa.Dbus{
|
res = append(res, &aa.Dbus{
|
||||||
Access: "bind", Bus: rules["bus"], Name: rules["name"],
|
Access: []string{"bind"}, Bus: rules["bus"], Name: rules["name"],
|
||||||
})
|
})
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
p.Rules = append(p.Rules, &aa.Dbus{
|
res = append(res, &aa.Dbus{
|
||||||
Access: "receive",
|
Access: []string{"receive"},
|
||||||
Bus: rules["bus"],
|
Bus: rules["bus"],
|
||||||
Path: rules["path"],
|
Path: rules["path"],
|
||||||
Interface: iface,
|
Interface: iface,
|
||||||
Name: `":1.@{int}"`,
|
PeerName: `":1.@{int}"`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
p.Rules = append(p.Rules, &aa.Dbus{
|
res = append(res, &aa.Dbus{
|
||||||
Access: "send",
|
Access: []string{"send"},
|
||||||
Bus: rules["bus"],
|
Bus: rules["bus"],
|
||||||
Path: rules["path"],
|
Path: rules["path"],
|
||||||
Interface: iface,
|
Interface: iface,
|
||||||
Name: `"{:1.@{int},org.freedesktop.DBus}"`,
|
PeerName: `"{:1.@{int},org.freedesktop.DBus}"`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
p.Rules = append(p.Rules, &aa.Dbus{
|
res = append(res, &aa.Dbus{
|
||||||
Access: "receive",
|
Access: []string{"receive"},
|
||||||
Bus: rules["bus"],
|
Bus: rules["bus"],
|
||||||
Path: rules["path"],
|
Path: rules["path"],
|
||||||
Interface: "org.freedesktop.DBus.Introspectable",
|
Interface: "org.freedesktop.DBus.Introspectable",
|
||||||
Member: "Introspect",
|
Member: "Introspect",
|
||||||
Name: `":1.@{int}"`,
|
PeerName: `":1.@{int}"`,
|
||||||
})
|
})
|
||||||
return p
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile {
|
func (d Dbus) talk(rules map[string]string) aa.Rules {
|
||||||
interfaces := setInterfaces(rules)
|
interfaces := setInterfaces(rules)
|
||||||
p := &aa.AppArmorProfile{}
|
res := aa.Rules{}
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
p.Rules = append(p.Rules, &aa.Dbus{
|
res = append(res, &aa.Dbus{
|
||||||
Access: "send",
|
Access: []string{"send"},
|
||||||
Bus: rules["bus"],
|
Bus: rules["bus"],
|
||||||
Path: rules["path"],
|
Path: rules["path"],
|
||||||
Interface: iface,
|
Interface: iface,
|
||||||
Name: `"{:1.@{int},` + rules["name"] + `}"`,
|
PeerName: `"{:1.@{int},` + rules["name"] + `}"`,
|
||||||
Label: rules["label"],
|
PeerLabel: rules["label"],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
p.Rules = append(p.Rules, &aa.Dbus{
|
res = append(res, &aa.Dbus{
|
||||||
Access: "receive",
|
Access: []string{"receive"},
|
||||||
Bus: rules["bus"],
|
Bus: rules["bus"],
|
||||||
Path: rules["path"],
|
Path: rules["path"],
|
||||||
Interface: iface,
|
Interface: iface,
|
||||||
Name: `"{:1.@{int},` + rules["name"] + `}"`,
|
PeerName: `"{:1.@{int},` + rules["name"] + `}"`,
|
||||||
Label: rules["label"],
|
PeerLabel: rules["label"],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return p
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ func TestDbus_Apply(t *testing.T) {
|
||||||
opt *Option
|
opt *Option
|
||||||
profile string
|
profile string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "own",
|
name: "own",
|
||||||
|
|
@ -137,7 +138,12 @@ func TestDbus_Apply(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := Directives["dbus"].Apply(tt.opt, tt.profile); got != tt.want {
|
got, err := Directives["dbus"].Apply(tt.opt, tt.profile)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Dbus.Apply() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
t.Errorf("Dbus.Apply() = %v, want %v", got, tt.want)
|
t.Errorf("Dbus.Apply() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
|
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
|
||||||
// SPDX-License-Identifier: GPL-2.0-only
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
|
||||||
|
// TODO: Local variables in profile header need to be resolved
|
||||||
|
|
||||||
package directive
|
package directive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -27,7 +29,7 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Exec) Apply(opt *Option, profile string) string {
|
func (d Exec) Apply(opt *Option, profileRaw string) (string, error) {
|
||||||
transition := "Px"
|
transition := "Px"
|
||||||
transitions := []string{"P", "U", "p", "u", "PU", "pu"}
|
transitions := []string{"P", "U", "p", "u", "PU", "pu"}
|
||||||
t := opt.ArgList[0]
|
t := opt.ArgList[0]
|
||||||
|
|
@ -36,26 +38,34 @@ func (d Exec) Apply(opt *Option, profile string) string {
|
||||||
delete(opt.ArgMap, t)
|
delete(opt.ArgMap, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &aa.AppArmorProfile{}
|
rules := aa.Rules{}
|
||||||
for name := range opt.ArgMap {
|
for name := range opt.ArgMap {
|
||||||
profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name))
|
profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name))
|
||||||
dstProfile := aa.DefaultTunables()
|
dstProfile := aa.DefaultTunables()
|
||||||
dstProfile.ParseVariables(profiletoTransition)
|
if err := dstProfile.Parse(profiletoTransition); err != nil {
|
||||||
for _, variable := range dstProfile.Variables {
|
return "", err
|
||||||
|
}
|
||||||
|
if err := dstProfile.Resolve(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, variable := range dstProfile.Preamble.GetVariables() {
|
||||||
if variable.Name == "exec_path" {
|
if variable.Name == "exec_path" {
|
||||||
for _, v := range variable.Values {
|
for _, v := range variable.Values {
|
||||||
p.Rules = append(p.Rules, &aa.File{
|
rules = append(rules, &aa.File{
|
||||||
Path: v,
|
Path: v,
|
||||||
Access: transition,
|
Access: []string{transition},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.Sort()
|
|
||||||
rules := p.String()
|
aa.IndentationLevel = strings.Count(
|
||||||
lenRules := len(rules)
|
strings.SplitN(opt.Raw, Keyword, 1)[0], aa.Indentation,
|
||||||
rules = rules[:lenRules-1]
|
)
|
||||||
return strings.Replace(profile, opt.Raw, rules, -1)
|
rules = rules.Sort()
|
||||||
|
new := rules.String()
|
||||||
|
new = new[:len(new)-1]
|
||||||
|
return strings.Replace(profileRaw, opt.Raw, new, -1), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ func TestExec_Apply(t *testing.T) {
|
||||||
opt *Option
|
opt *Option
|
||||||
profile string
|
profile string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "exec",
|
name: "exec",
|
||||||
|
|
@ -30,8 +31,8 @@ func TestExec_Apply(t *testing.T) {
|
||||||
Raw: " #aa:exec DiscoverNotifier",
|
Raw: " #aa:exec DiscoverNotifier",
|
||||||
},
|
},
|
||||||
profile: ` #aa:exec DiscoverNotifier`,
|
profile: ` #aa:exec DiscoverNotifier`,
|
||||||
want: ` @{lib}/@{multiarch}/{,libexec/}DiscoverNotifier Px,
|
want: ` /{,usr/}lib{,exec,32,64}/*-linux-gnu*/{,libexec/}DiscoverNotifier Px,
|
||||||
@{lib}/DiscoverNotifier Px,`,
|
/{,usr/}lib{,exec,32,64}/DiscoverNotifier Px,`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "exec-unconfined",
|
name: "exec-unconfined",
|
||||||
|
|
@ -44,15 +45,20 @@ func TestExec_Apply(t *testing.T) {
|
||||||
Raw: " #aa:exec U polkit-agent-helper",
|
Raw: " #aa:exec U polkit-agent-helper",
|
||||||
},
|
},
|
||||||
profile: ` #aa:exec U polkit-agent-helper`,
|
profile: ` #aa:exec U polkit-agent-helper`,
|
||||||
want: ` @{lib}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux,
|
want: ` /{,usr/}lib{,exec,32,64}/polkit-[0-9]/polkit-agent-helper-[0-9] Ux,
|
||||||
@{lib}/polkit-agent-helper-[0-9] Ux,`,
|
/{,usr/}lib{,exec,32,64}/polkit-agent-helper-[0-9] Ux,`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg.RootApparmord = tt.rootApparmord
|
cfg.RootApparmord = tt.rootApparmord
|
||||||
if got := Directives["exec"].Apply(tt.opt, tt.profile); got != tt.want {
|
got, err := Directives["exec"].Apply(tt.opt, tt.profile)
|
||||||
t.Errorf("Exec.Apply() = %v, want %v", got, tt.want)
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Exec.Apply() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Exec.Apply() = |%v|, want |%v|", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,12 @@ func filterRuleForUs(opt *Option) bool {
|
||||||
return slices.Contains(opt.ArgList, cfg.Distribution) || slices.Contains(opt.ArgList, cfg.Family)
|
return slices.Contains(opt.ArgList, cfg.Distribution) || slices.Contains(opt.ArgList, cfg.Family)
|
||||||
}
|
}
|
||||||
|
|
||||||
func filter(only bool, opt *Option, profile string) string {
|
func filter(only bool, opt *Option, profile string) (string, error) {
|
||||||
if only && filterRuleForUs(opt) {
|
if only && filterRuleForUs(opt) {
|
||||||
return opt.Clean(profile)
|
return opt.Clean(profile), nil
|
||||||
}
|
}
|
||||||
if !only && !filterRuleForUs(opt) {
|
if !only && !filterRuleForUs(opt) {
|
||||||
return opt.Clean(profile)
|
return opt.Clean(profile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
inline := true
|
inline := true
|
||||||
|
|
@ -64,13 +64,13 @@ func filter(only bool, opt *Option, profile string) string {
|
||||||
regRemoveParagraph := regexp.MustCompile(`(?s)` + opt.Raw + `\n.*?\n\n`)
|
regRemoveParagraph := regexp.MustCompile(`(?s)` + opt.Raw + `\n.*?\n\n`)
|
||||||
profile = regRemoveParagraph.ReplaceAllString(profile, "")
|
profile = regRemoveParagraph.ReplaceAllString(profile, "")
|
||||||
}
|
}
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d FilterOnly) Apply(opt *Option, profile string) string {
|
func (d FilterOnly) Apply(opt *Option, profile string) (string, error) {
|
||||||
return filter(true, opt, profile)
|
return filter(true, opt, profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d FilterExclude) Apply(opt *Option, profile string) string {
|
func (d FilterExclude) Apply(opt *Option, profile string) (string, error) {
|
||||||
return filter(false, opt, profile)
|
return filter(false, opt, profile)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ func TestFilterOnly_Apply(t *testing.T) {
|
||||||
opt *Option
|
opt *Option
|
||||||
profile string
|
profile string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "inline",
|
name: "inline",
|
||||||
|
|
@ -79,7 +80,12 @@ func TestFilterOnly_Apply(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg.Distribution = tt.dist
|
cfg.Distribution = tt.dist
|
||||||
cfg.Family = tt.family
|
cfg.Family = tt.family
|
||||||
if got := Directives["only"].Apply(tt.opt, tt.profile); got != tt.want {
|
got, err := Directives["only"].Apply(tt.opt, tt.profile)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("FilterOnly.Apply() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
t.Errorf("FilterOnly.Apply() = %v, want %v", got, tt.want)
|
t.Errorf("FilterOnly.Apply() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -94,6 +100,7 @@ func TestFilterExclude_Apply(t *testing.T) {
|
||||||
opt *Option
|
opt *Option
|
||||||
profile string
|
profile string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "inline",
|
name: "inline",
|
||||||
|
|
@ -128,7 +135,12 @@ func TestFilterExclude_Apply(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg.Distribution = tt.dist
|
cfg.Distribution = tt.dist
|
||||||
cfg.Family = tt.family
|
cfg.Family = tt.family
|
||||||
if got := Directives["exclude"].Apply(tt.opt, tt.profile); got != tt.want {
|
got, err := Directives["exclude"].Apply(tt.opt, tt.profile)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("FilterExclude.Apply() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
t.Errorf("FilterExclude.Apply() = %v, want %v", got, tt.want)
|
t.Errorf("FilterExclude.Apply() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@ func init() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Stack) Apply(opt *Option, profile string) string {
|
func (s Stack) Apply(opt *Option, profile string) (string, error) {
|
||||||
res := ""
|
res := ""
|
||||||
for name := range opt.ArgMap {
|
for name := range opt.ArgMap {
|
||||||
stackedProfile := util.MustReadFile(cfg.RootApparmord.Join(name))
|
stackedProfile := util.MustReadFile(cfg.RootApparmord.Join(name))
|
||||||
m := regRules.FindStringSubmatch(stackedProfile)
|
m := regRules.FindStringSubmatch(stackedProfile)
|
||||||
if len(m) < 2 {
|
if len(m) < 2 {
|
||||||
panic(fmt.Sprintf("No profile found in %s", name))
|
return "", fmt.Errorf("No profile found in %s", name)
|
||||||
}
|
}
|
||||||
stackedRules := m[1]
|
stackedRules := m[1]
|
||||||
stackedRules = regCleanStakedRules.Replace(stackedRules)
|
stackedRules = regCleanStakedRules.Replace(stackedRules)
|
||||||
|
|
@ -54,9 +54,9 @@ func (s Stack) Apply(opt *Option, profile string) string {
|
||||||
// Insert the stacked profile at the end of the current profile, remove the stack directive
|
// Insert the stacked profile at the end of the current profile, remove the stack directive
|
||||||
m := regEndOfRules.FindStringSubmatch(profile)
|
m := regEndOfRules.FindStringSubmatch(profile)
|
||||||
if len(m) <= 1 {
|
if len(m) <= 1 {
|
||||||
panic(fmt.Sprintf("No end of rules found in %s", opt.File))
|
return "", fmt.Errorf("No end of rules found in %s", opt.File)
|
||||||
}
|
}
|
||||||
profile = strings.Replace(profile, m[0], res+m[0], -1)
|
profile = strings.Replace(profile, m[0], res+m[0], -1)
|
||||||
profile = strings.Replace(profile, opt.Raw, "", -1)
|
profile = strings.Replace(profile, opt.Raw, "", -1)
|
||||||
return profile
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ func TestStack_Apply(t *testing.T) {
|
||||||
opt *Option
|
opt *Option
|
||||||
profile string
|
profile string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "stack",
|
name: "stack",
|
||||||
|
|
@ -68,7 +69,12 @@ profile parent @{exec_path} {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg.RootApparmord = tt.rootApparmord
|
cfg.RootApparmord = tt.rootApparmord
|
||||||
if got := Directives["stack"].Apply(tt.opt, tt.profile); got != tt.want {
|
got, err := Directives["stack"].Apply(tt.opt, tt.profile)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Stack.Apply() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
t.Errorf("Stack.Apply() = %v, want %v", got, tt.want)
|
t.Errorf("Stack.Apply() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,14 @@ func Build() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, b := range builder.Builds {
|
profile, err = builder.Run(file, profile)
|
||||||
profile = b.Apply(profile)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profile, err = directive.Run(file, profile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
profile = directive.Run(file, profile)
|
|
||||||
if err := file.WriteFile([]byte(profile)); err != nil {
|
if err := file.WriteFile([]byte(profile)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# Simple test profile for the AppArmorProfile.String() method
|
# Simple test profile for the AppArmorProfileFile.String() method
|
||||||
|
|
||||||
abi <abi/4.0>,
|
abi <abi/4.0>,
|
||||||
|
|
||||||
alias /mnt/usr -> /usr,
|
alias /mnt/usr -> /usr,
|
||||||
|
|
@ -18,13 +19,13 @@ profile foo @{exec_path} xattrs=(security.tagged=allowed) flags=(complain attach
|
||||||
network inet stream,
|
network inet stream,
|
||||||
network inet6 stream,
|
network inet6 stream,
|
||||||
|
|
||||||
mount fstype=fuse.portal options=(rw rbind) @{run}/user/@{uid}/ -> /,
|
mount fstype=fuse.portal options=(rw rbind) @{run}/user/@{uid}/ -> /, # failed perms check
|
||||||
|
|
||||||
umount @{run}/user/@{uid}/,
|
umount @{run}/user/@{uid}/,
|
||||||
|
|
||||||
signal (receive) set=(term) peer=at-spi-bus-launcher,
|
signal receive set=term peer=at-spi-bus-launcher,
|
||||||
|
|
||||||
ptrace (read) peer=nautilus,
|
ptrace read peer=nautilus,
|
||||||
|
|
||||||
unix (send receive) type=stream addr=@/tmp/.ICE-unix/1995 peer=(label=gnome-shell, addr=none),
|
unix (send receive) type=stream addr=@/tmp/.ICE-unix/1995 peer=(label=gnome-shell, addr=none),
|
||||||
|
|
||||||
|
|
|
||||||
2
tests/testdata/tunables/dir.d/aliases
vendored
Normal file
2
tests/testdata/tunables/dir.d/aliases
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
alias /usr/ -> /User/,
|
||||||
|
alias /lib/ -> /Libraries/,
|
||||||
2
tests/testdata/tunables/dir.d/vars
vendored
Normal file
2
tests/testdata/tunables/dir.d/vars
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# variable declarations for inclusion
|
||||||
|
@{FOO} = /foo /bar /baz /biff /lib /tmp
|
||||||
3
tests/testdata/tunables/global
vendored
Normal file
3
tests/testdata/tunables/global
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
include <tunables/dir.d>
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue