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