mirror of
https://github.com/roddhjav/apparmor.d.git
synced 2025-01-18 17:08:09 +01:00
feat(aa): rename the main profile struct.
This commit is contained in:
parent
4b753210e7
commit
890275fb22
11 changed files with 287 additions and 274 deletions
214
pkg/aa/apparmor.go
Normal file
214
pkg/aa/apparmor.go
Normal file
|
@ -0,0 +1,214 @@
|
|||
// 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 (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/arduino/go-paths-helper"
|
||||
)
|
||||
|
||||
// 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
|
||||
Profiles []*Profile
|
||||
}
|
||||
|
||||
// Preamble section of a profile file,
|
||||
type Preamble struct {
|
||||
Abi []*Abi
|
||||
Includes []*Include
|
||||
Aliases []*Alias
|
||||
Variables []*Variable
|
||||
}
|
||||
|
||||
func NewAppArmorProfile() *AppArmorProfileFile {
|
||||
return &AppArmorProfileFile{}
|
||||
}
|
||||
|
||||
// String returns the formatted representation of a profile as a string
|
||||
func (f *AppArmorProfileFile) String() string {
|
||||
var res bytes.Buffer
|
||||
err := tmplAppArmorProfile.Execute(&res, f)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return res.String()
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// AddRule adds a new rule to the profile from a log map
|
||||
// See utils/apparmor/logparser.py for the format of the log map
|
||||
func (f *AppArmorProfileFile) AddRule(log map[string]string) {
|
||||
p := f.GetDefaultProfile()
|
||||
|
||||
// Generate profile flags and extra rules
|
||||
switch log["error"] {
|
||||
case "-2":
|
||||
if !slices.Contains(p.Flags, "mediate_deleted") {
|
||||
p.Flags = append(p.Flags, "mediate_deleted")
|
||||
}
|
||||
case "-13":
|
||||
if strings.Contains(log["info"], "namespace creation restricted") {
|
||||
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, newCapabilityFromLog(log))
|
||||
case "net":
|
||||
if log["family"] == "unix" {
|
||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
||||
} else {
|
||||
p.Rules = append(p.Rules, newNetworkFromLog(log))
|
||||
}
|
||||
case "mount":
|
||||
if strings.Contains(log["flags"], "remount") {
|
||||
p.Rules = append(p.Rules, newRemountFromLog(log))
|
||||
} else {
|
||||
switch log["operation"] {
|
||||
case "mount":
|
||||
p.Rules = append(p.Rules, newMountFromLog(log))
|
||||
case "umount":
|
||||
p.Rules = append(p.Rules, newUmountFromLog(log))
|
||||
case "remount":
|
||||
p.Rules = append(p.Rules, newRemountFromLog(log))
|
||||
case "pivotroot":
|
||||
p.Rules = append(p.Rules, newPivotRootFromLog(log))
|
||||
}
|
||||
}
|
||||
case "posix_mqueue", "sysv_mqueue":
|
||||
p.Rules = append(p.Rules, newMqueueFromLog(log))
|
||||
case "signal":
|
||||
p.Rules = append(p.Rules, newSignalFromLog(log))
|
||||
case "ptrace":
|
||||
p.Rules = append(p.Rules, newPtraceFromLog(log))
|
||||
case "namespace":
|
||||
p.Rules = append(p.Rules, newUsernsFromLog(log))
|
||||
case "unix":
|
||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
||||
case "dbus":
|
||||
p.Rules = append(p.Rules, newDbusFromLog(log))
|
||||
case "file":
|
||||
if log["operation"] == "change_onexec" {
|
||||
p.Rules = append(p.Rules, newChangeProfileFromLog(log))
|
||||
} else {
|
||||
p.Rules = append(p.Rules, newFileFromLog(log))
|
||||
}
|
||||
default:
|
||||
if strings.Contains(log["operation"], "dbus") {
|
||||
p.Rules = append(p.Rules, newDbusFromLog(log))
|
||||
} else if log["family"] == "unix" {
|
||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the rules in the profile
|
||||
// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines
|
||||
func (f *AppArmorProfileFile) Sort() {
|
||||
for _, p := range f.Profiles {
|
||||
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 (f *AppArmorProfileFile) MergeRules() {
|
||||
for _, p := range f.Profiles {
|
||||
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 (f *AppArmorProfileFile) Format() {
|
||||
const prefixOwner = " "
|
||||
for _, p := range f.Profiles {
|
||||
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:]...)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,17 +31,17 @@ func readprofile(path string) string {
|
|||
func TestAppArmorProfile_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p *AppArmorProfile
|
||||
f *AppArmorProfileFile
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
p: &AppArmorProfile{},
|
||||
f: &AppArmorProfileFile{},
|
||||
want: ``,
|
||||
},
|
||||
{
|
||||
name: "foo",
|
||||
p: &AppArmorProfile{
|
||||
f: &AppArmorProfileFile{
|
||||
Preamble: Preamble{
|
||||
Abi: []*Abi{{IsMagic: true, Path: "abi/4.0"}},
|
||||
Includes: []*Include{{IsMagic: true, Path: "tunables/global"}},
|
||||
|
@ -117,7 +117,7 @@ func TestAppArmorProfile_String(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.p.String(); got != tt.want {
|
||||
if got := tt.f.String(); got != tt.want {
|
||||
t.Errorf("AppArmorProfile.String() = |%v|, want |%v|", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
@ -128,12 +128,12 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
log map[string]string
|
||||
want *AppArmorProfile
|
||||
want *AppArmorProfileFile
|
||||
}{
|
||||
{
|
||||
name: "capability",
|
||||
log: capability1Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{capability1},
|
||||
}},
|
||||
|
@ -142,7 +142,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
{
|
||||
name: "network",
|
||||
log: network1Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{network1},
|
||||
}},
|
||||
|
@ -151,7 +151,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
{
|
||||
name: "mount",
|
||||
log: mount2Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{mount2},
|
||||
}},
|
||||
|
@ -160,7 +160,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
{
|
||||
name: "signal",
|
||||
log: signal1Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{signal1},
|
||||
}},
|
||||
|
@ -169,7 +169,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
{
|
||||
name: "ptrace",
|
||||
log: ptrace2Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{ptrace2},
|
||||
}},
|
||||
|
@ -178,7 +178,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
{
|
||||
name: "unix",
|
||||
log: unix1Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{unix1},
|
||||
}},
|
||||
|
@ -187,7 +187,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
{
|
||||
name: "dbus",
|
||||
log: dbus2Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{dbus2},
|
||||
}},
|
||||
|
@ -196,7 +196,7 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
{
|
||||
name: "file",
|
||||
log: file2Log,
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{file2},
|
||||
}},
|
||||
|
@ -217,12 +217,12 @@ func TestAppArmorProfile_AddRule(t *testing.T) {
|
|||
func TestAppArmorProfile_Sort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
origin *AppArmorProfile
|
||||
want *AppArmorProfile
|
||||
origin *AppArmorProfileFile
|
||||
want *AppArmorProfileFile
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
origin: &AppArmorProfile{
|
||||
origin: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{
|
||||
file2, network1, includeLocal1, dbus2, signal1, ptrace1,
|
||||
|
@ -230,7 +230,7 @@ func TestAppArmorProfile_Sort(t *testing.T) {
|
|||
},
|
||||
}},
|
||||
},
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{
|
||||
capability2, network1, mount2, signal1, signal2, ptrace1,
|
||||
|
@ -254,17 +254,17 @@ func TestAppArmorProfile_Sort(t *testing.T) {
|
|||
func TestAppArmorProfile_MergeRules(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
origin *AppArmorProfile
|
||||
want *AppArmorProfile
|
||||
origin *AppArmorProfileFile
|
||||
want *AppArmorProfileFile
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
origin: &AppArmorProfile{
|
||||
origin: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{capability1, capability1, network1, network1, file1, file1},
|
||||
}},
|
||||
},
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Profiles: []*Profile{{
|
||||
Rules: []ApparmorRule{capability1, network1, file1},
|
||||
}},
|
||||
|
@ -285,12 +285,12 @@ func TestAppArmorProfile_MergeRules(t *testing.T) {
|
|||
func TestAppArmorProfile_Integration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p *AppArmorProfile
|
||||
f *AppArmorProfileFile
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "aa-status",
|
||||
p: &AppArmorProfile{
|
||||
f: &AppArmorProfileFile{
|
||||
Preamble: Preamble{
|
||||
Abi: []*Abi{{IsMagic: true, Path: "abi/3.0"}},
|
||||
Includes: []*Include{{IsMagic: true, Path: "tunables/global"}},
|
||||
|
@ -327,10 +327,10 @@ func TestAppArmorProfile_Integration(t *testing.T) {
|
|||
}
|
||||
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 {
|
||||
tt.f.Sort()
|
||||
tt.f.MergeRules()
|
||||
tt.f.Format()
|
||||
if got := tt.f.String(); "\n"+got != tt.want {
|
||||
t.Errorf("AppArmorProfile = |%v|, want |%v|", "\n"+got, tt.want)
|
||||
}
|
||||
})
|
|
@ -5,46 +5,19 @@
|
|||
package aa
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/arduino/go-paths-helper"
|
||||
)
|
||||
|
||||
// Default Apparmor magic directory: /etc/apparmor.d/.
|
||||
var MagicRoot = paths.New("/etc/apparmor.d")
|
||||
|
||||
// AppArmorProfiles represents a full set of apparmor profiles
|
||||
type AppArmorProfiles map[string]*AppArmorProfile
|
||||
|
||||
// ApparmorProfile 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 AppArmorProfile struct {
|
||||
Preamble
|
||||
Profiles []*Profile
|
||||
}
|
||||
|
||||
// Preamble section of a profile file,
|
||||
type Preamble struct {
|
||||
Abi []*Abi
|
||||
Includes []*Include
|
||||
Aliases []*Alias
|
||||
Variables []*Variable
|
||||
}
|
||||
|
||||
// Profile represent a single AppArmor profile.
|
||||
// Profile represents a single AppArmor profile.
|
||||
type Profile struct {
|
||||
Rule
|
||||
Header
|
||||
Rules Rules
|
||||
}
|
||||
|
||||
// Header represents the header of a profile.
|
||||
type Header struct {
|
||||
Name string
|
||||
Attachments []string
|
||||
|
@ -53,7 +26,11 @@ type Header struct {
|
|||
}
|
||||
|
||||
func (r *Profile) Less(other any) bool {
|
||||
return false // TBD
|
||||
o, _ := other.(*Profile)
|
||||
if r.Name != o.Name {
|
||||
return r.Name < o.Name
|
||||
}
|
||||
return len(r.Attachments) < len(o.Attachments)
|
||||
}
|
||||
|
||||
func (r *Profile) Equals(other any) bool {
|
||||
|
@ -62,187 +39,3 @@ func (r *Profile) Equals(other any) bool {
|
|||
maps.Equal(r.Attributes, o.Attributes) &&
|
||||
slices.Equal(r.Flags, o.Flags)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
return res.String()
|
||||
}
|
||||
|
||||
// GetDefaultProfile ensure a profile is always present in the profile file and
|
||||
// return it, as a default profile.
|
||||
func (p *AppArmorProfile) GetDefaultProfile() *Profile {
|
||||
if len(p.Profiles) == 0 {
|
||||
p.Profiles = append(p.Profiles, &Profile{})
|
||||
}
|
||||
return p.Profiles[0]
|
||||
}
|
||||
|
||||
// AddRule adds a new rule to the profile from a log map
|
||||
// See utils/apparmor/logparser.py for the format of the log map
|
||||
func (profile *AppArmorProfile) AddRule(log map[string]string) {
|
||||
p := profile.GetDefaultProfile()
|
||||
|
||||
// Generate profile flags and extra rules
|
||||
switch log["error"] {
|
||||
case "-2":
|
||||
if !slices.Contains(p.Flags, "mediate_deleted") {
|
||||
p.Flags = append(p.Flags, "mediate_deleted")
|
||||
}
|
||||
case "-13":
|
||||
if strings.Contains(log["info"], "namespace creation restricted") {
|
||||
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, newCapabilityFromLog(log))
|
||||
case "net":
|
||||
if log["family"] == "unix" {
|
||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
||||
} else {
|
||||
p.Rules = append(p.Rules, newNetworkFromLog(log))
|
||||
}
|
||||
case "mount":
|
||||
if strings.Contains(log["flags"], "remount") {
|
||||
p.Rules = append(p.Rules, newRemountFromLog(log))
|
||||
} else {
|
||||
switch log["operation"] {
|
||||
case "mount":
|
||||
p.Rules = append(p.Rules, newMountFromLog(log))
|
||||
case "umount":
|
||||
p.Rules = append(p.Rules, newUmountFromLog(log))
|
||||
case "remount":
|
||||
p.Rules = append(p.Rules, newRemountFromLog(log))
|
||||
case "pivotroot":
|
||||
p.Rules = append(p.Rules, newPivotRootFromLog(log))
|
||||
}
|
||||
}
|
||||
case "posix_mqueue", "sysv_mqueue":
|
||||
p.Rules = append(p.Rules, newMqueueFromLog(log))
|
||||
case "signal":
|
||||
p.Rules = append(p.Rules, newSignalFromLog(log))
|
||||
case "ptrace":
|
||||
p.Rules = append(p.Rules, newPtraceFromLog(log))
|
||||
case "namespace":
|
||||
p.Rules = append(p.Rules, newUsernsFromLog(log))
|
||||
case "unix":
|
||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
||||
case "dbus":
|
||||
p.Rules = append(p.Rules, newDbusFromLog(log))
|
||||
case "file":
|
||||
if log["operation"] == "change_onexec" {
|
||||
p.Rules = append(p.Rules, newChangeProfileFromLog(log))
|
||||
} else {
|
||||
p.Rules = append(p.Rules, newFileFromLog(log))
|
||||
}
|
||||
default:
|
||||
if strings.Contains(log["operation"], "dbus") {
|
||||
p.Rules = append(p.Rules, newDbusFromLog(log))
|
||||
} else if log["family"] == "unix" {
|
||||
p.Rules = append(p.Rules, newUnixFromLog(log))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the rules in the profile
|
||||
// Follow: https://apparmor.pujol.io/development/guidelines/#guidelines
|
||||
func (profile *AppArmorProfile) Sort() {
|
||||
for _, p := range profile.Profiles {
|
||||
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 (profile *AppArmorProfile) MergeRules() {
|
||||
for _, p := range profile.Profiles {
|
||||
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 (profile *AppArmorProfile) Format() {
|
||||
const prefixOwner = " "
|
||||
for _, p := range profile.Profiles {
|
||||
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:]...)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,14 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// ApparmorRule generic interface
|
||||
type ApparmorRule interface {
|
||||
Less(other any) bool
|
||||
Equals(other any) bool
|
||||
}
|
||||
|
||||
type Rules []ApparmorRule
|
||||
|
||||
type Rule struct {
|
||||
Comment string
|
||||
NoNewPrivs bool
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{{- define "profile" -}}
|
||||
|
||||
{{- if or .Name .Attachments .Attributes .Flags -}}
|
||||
{{- with .Header -}}
|
||||
{{- "profile" -}}
|
||||
{{- with .Name -}}
|
||||
{{ " " }}{{ . }}
|
||||
|
@ -18,9 +18,7 @@
|
|||
{{- with .Flags -}}
|
||||
{{ " flags=(" }}{{ join . }}{{ ")" }}
|
||||
{{- end -}}
|
||||
{{ " {" }}
|
||||
{{- template "comment" . -}}
|
||||
{{- "\n" -}}
|
||||
{{- "{\n" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $oldtype := "" -}}
|
||||
|
@ -296,7 +294,7 @@
|
|||
{{- $oldtype = $type -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if or .Name .Attachments .Attributes .Flags -}}
|
||||
{{- with .Header -}}
|
||||
{{- "}\n" -}}
|
||||
{{- end -}}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ var (
|
|||
|
||||
// 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{
|
||||
func DefaultTunables() *AppArmorProfileFile {
|
||||
return &AppArmorProfileFile{
|
||||
Preamble: Preamble{
|
||||
Variables: []*Variable{
|
||||
{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}},
|
||||
|
@ -36,41 +36,41 @@ func DefaultTunables() *AppArmorProfile {
|
|||
}
|
||||
|
||||
// ParseVariables extract all variables from the profile
|
||||
func (p *AppArmorProfile) ParseVariables(content string) {
|
||||
func (f *AppArmorProfileFile) 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 {
|
||||
for idx, variable := range f.Variables {
|
||||
if variable.Name == key {
|
||||
p.Variables[idx].Values = append(p.Variables[idx].Values, values...)
|
||||
f.Variables[idx].Values = append(f.Variables[idx].Values, values...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
variable := &Variable{Name: key, Values: values}
|
||||
p.Variables = append(p.Variables, variable)
|
||||
f.Variables = append(f.Variables, variable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolve recursively resolves all variables references
|
||||
func (p *AppArmorProfile) resolve(str string) []string {
|
||||
func (f *AppArmorProfileFile) 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 {
|
||||
for _, vrbl := range f.Variables {
|
||||
if vrbl.Name == varname {
|
||||
for _, value := range vrbl.Values {
|
||||
newVar := strings.ReplaceAll(str, variable, value)
|
||||
vars = append(vars, p.resolve(newVar)...)
|
||||
vars = append(vars, f.resolve(newVar)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,8 +83,8 @@ func (p *AppArmorProfile) resolve(str string) []string {
|
|||
}
|
||||
|
||||
// ResolveAttachments resolve profile attachments defined in exec_path
|
||||
func (profile *AppArmorProfile) ResolveAttachments() {
|
||||
p := profile.GetDefaultProfile()
|
||||
func (f *AppArmorProfileFile) ResolveAttachments() {
|
||||
p := f.GetDefaultProfile()
|
||||
|
||||
for _, variable := range profile.Variables {
|
||||
if variable.Name == "exec_path" {
|
||||
|
@ -100,8 +100,8 @@ func (profile *AppArmorProfile) ResolveAttachments() {
|
|||
}
|
||||
|
||||
// NestAttachments return a nested attachment string
|
||||
func (profile *AppArmorProfile) NestAttachments() string {
|
||||
p := profile.GetDefaultProfile()
|
||||
func (f *AppArmorProfileFile) NestAttachments() string {
|
||||
p := f.GetDefaultProfile()
|
||||
if len(p.Attachments) == 0 {
|
||||
return ""
|
||||
} else if len(p.Attachments) == 1 {
|
||||
|
|
|
@ -16,11 +16,11 @@ import (
|
|||
func TestDefaultTunables(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want *AppArmorProfile
|
||||
want *AppArmorProfileFile
|
||||
}{
|
||||
{
|
||||
name: "aa",
|
||||
want: &AppArmorProfile{
|
||||
want: &AppArmorProfileFile{
|
||||
Preamble: Preamble{
|
||||
Variables: []*Variable{
|
||||
{Name: "bin", Values: []string{"/{,usr/}{,s}bin"}},
|
||||
|
|
|
@ -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() aa.AppArmorProfileFiles {
|
||||
profiles := make(aa.AppArmorProfileFiles, 0)
|
||||
for _, log := range aaLogs {
|
||||
name := ""
|
||||
if strings.Contains(log["operation"], "dbus") {
|
||||
|
@ -208,7 +208,7 @@ func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles {
|
|||
}
|
||||
|
||||
if _, ok := profiles[name]; !ok {
|
||||
profile := &aa.AppArmorProfile{
|
||||
profile := &aa.AppArmorProfileFile{
|
||||
Profiles: []*aa.Profile{{Header: aa.Header{Name: name}}},
|
||||
}
|
||||
profile.AddRule(log)
|
||||
|
|
|
@ -292,13 +292,13 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
aaLogs AppArmorLogs
|
||||
want aa.AppArmorProfiles
|
||||
want aa.AppArmorProfileFiles
|
||||
}{
|
||||
{
|
||||
name: "",
|
||||
aaLogs: append(append(refKmod, refPowerProfiles...), refKmod...),
|
||||
want: aa.AppArmorProfiles{
|
||||
"kmod": &aa.AppArmorProfile{
|
||||
want: aa.AppArmorProfileFiles{
|
||||
"kmod": &aa.AppArmorProfileFile{
|
||||
Profiles: []*aa.Profile{{
|
||||
Header: aa.Header{Name: "kmod"},
|
||||
Rules: aa.Rules{
|
||||
|
@ -317,7 +317,7 @@ func TestAppArmorLogs_ParseToProfiles(t *testing.T) {
|
|||
},
|
||||
}},
|
||||
},
|
||||
"power-profiles-daemon": &aa.AppArmorProfile{
|
||||
"power-profiles-daemon": &aa.AppArmorProfileFile{
|
||||
Profiles: []*aa.Profile{{
|
||||
Header: aa.Header{Name: "power-profiles-daemon"},
|
||||
Rules: aa.Rules{
|
||||
|
|
|
@ -51,7 +51,7 @@ func setInterfaces(rules map[string]string) []string {
|
|||
}
|
||||
|
||||
func (d Dbus) Apply(opt *Option, profile string) string {
|
||||
var p *aa.AppArmorProfile
|
||||
var p *aa.AppArmorProfileFile
|
||||
|
||||
action := d.sanityCheck(opt)
|
||||
switch action {
|
||||
|
@ -95,9 +95,9 @@ func (d Dbus) sanityCheck(opt *Option) string {
|
|||
return action
|
||||
}
|
||||
|
||||
func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile {
|
||||
func (d Dbus) own(rules map[string]string) *aa.AppArmorProfileFile {
|
||||
interfaces := setInterfaces(rules)
|
||||
profile := &aa.AppArmorProfile{}
|
||||
profile := &aa.AppArmorProfileFile{}
|
||||
p := profile.GetDefaultProfile()
|
||||
p.Rules = append(p.Rules, &aa.Dbus{
|
||||
Access: "bind", Bus: rules["bus"], Name: rules["name"],
|
||||
|
@ -131,9 +131,9 @@ func (d Dbus) own(rules map[string]string) *aa.AppArmorProfile {
|
|||
return profile
|
||||
}
|
||||
|
||||
func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfile {
|
||||
func (d Dbus) talk(rules map[string]string) *aa.AppArmorProfileFile {
|
||||
interfaces := setInterfaces(rules)
|
||||
profile := &aa.AppArmorProfile{}
|
||||
profile := &aa.AppArmorProfileFile{}
|
||||
p := profile.GetDefaultProfile()
|
||||
for _, iface := range interfaces {
|
||||
p.Rules = append(p.Rules, &aa.Dbus{
|
||||
|
|
|
@ -36,7 +36,7 @@ func (d Exec) Apply(opt *Option, profileRaw string) string {
|
|||
delete(opt.ArgMap, t)
|
||||
}
|
||||
|
||||
profile := &aa.AppArmorProfile{}
|
||||
profile := &aa.AppArmorProfileFile{}
|
||||
p := profile.GetDefaultProfile()
|
||||
for name := range opt.ArgMap {
|
||||
profiletoTransition := util.MustReadFile(cfg.RootApparmord.Join(name))
|
||||
|
|
Loading…
Reference in a new issue