feat(aa): add a string method to all rule struct.

This commit is contained in:
Alexandre Pujol 2024-04-23 21:26:09 +01:00
parent e9fa0660f8
commit 5483668574
No known key found for this signature in database
GPG Key ID: C5469996F0DF68EC
20 changed files with 337 additions and 34 deletions

View File

@ -42,14 +42,9 @@ func NewAppArmorProfile() *AppArmorProfileFile {
return &AppArmorProfileFile{}
}
// String returns the formatted representation of a profile as a string
// String returns the formatted representation of a profile file as a string
func (f *AppArmorProfileFile) String() string {
var res bytes.Buffer
err := tmpl["apparmor"].Execute(&res, f)
if err != nil {
return err.Error()
}
return res.String()
return renderTemplate("apparmor", f)
}
// GetDefaultProfile ensure a profile is always present in the profile file and

View File

@ -5,6 +5,8 @@
package aa
const tokCAPABILITY = "capability"
type Capability struct {
RuleBase
Qualifier
@ -36,4 +38,6 @@ func (r *Capability) Equals(other any) bool {
return slices.Equal(r.Names, o.Names) && r.Qualifier.Equals(o.Qualifier)
}
func (r *Capability) String() string {
return renderTemplate(tokCAPABILITY, r)
}

View File

@ -4,6 +4,8 @@
package aa
const tokCHANGEPROFILE = "change_profile"
type ChangeProfile struct {
RuleBase
Qualifier
@ -41,3 +43,7 @@ func (r *ChangeProfile) Equals(other any) bool {
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(tokCHANGEPROFILE, r)
}

View File

@ -4,6 +4,8 @@
package aa
const tokDBUS = "dbus"
type Dbus struct {
RuleBase
Qualifier
@ -77,3 +79,7 @@ func (r *Dbus) Equals(other any) bool {
r.Member == o.Member && r.PeerName == o.PeerName &&
r.PeerLabel == o.PeerLabel && r.Qualifier.Equals(o.Qualifier)
}
func (r *Dbus) String() string {
return renderTemplate(tokDBUS, r)
}

View File

@ -59,5 +59,9 @@ func (r *File) Equals(other any) bool {
r.Target == o.Target && r.Qualifier.Equals(o.Qualifier)
}
func (r *File) String() string {
return renderTemplate("file", r)
}
r.Target == o.Target && r.Qualifier.Equals(o.Qualifier)
}

View File

@ -4,6 +4,9 @@
package aa
const tokIOURING = "io_uring"
type IOUring struct {
RuleBase
Qualifier
@ -36,4 +39,6 @@ func (r *IOUring) Equals(other any) bool {
return slices.Equal(r.Access, o.Access) && r.Label == o.Label && r.Qualifier.Equals(o.Qualifier)
}
func (r *IOUring) String() string {
return renderTemplate(tokIOURING, r)
}

View File

@ -8,6 +8,13 @@ import (
"slices"
"strings"
)
const (
tokMOUNT = "mount"
tokREMOUNT = "remount"
tokUMOUNT = "umount"
)
)
type MountConditions struct {
@ -75,6 +82,10 @@ func (r *Mount) Equals(other any) bool {
r.Qualifier.Equals(o.Qualifier)
}
func (r *Mount) String() string {
return renderTemplate(tokMOUNT, r)
}
type Umount struct {
RuleBase
Qualifier
@ -109,6 +120,10 @@ func (r *Umount) Equals(other any) bool {
r.Qualifier.Equals(o.Qualifier)
}
func (r *Umount) String() string {
return renderTemplate(tokUMOUNT, r)
}
type Remount struct {
RuleBase
Qualifier
@ -142,3 +157,7 @@ func (r *Remount) Equals(other any) bool {
r.MountConditions.Equals(o.MountConditions) &&
r.Qualifier.Equals(o.Qualifier)
}
func (r *Remount) String() string {
return renderTemplate(tokREMOUNT, r)
}

View File

@ -8,6 +8,9 @@ import (
"strings"
)
const tokMQUEUE = "mqueue"
type Mqueue struct {
RuleBase
Qualifier
@ -53,3 +56,7 @@ func (r *Mqueue) Equals(other any) bool {
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(tokMQUEUE, r)
}

View File

@ -4,6 +4,9 @@
package aa
const tokNETWORK = "network"
type AddressExpr struct {
Source string
Destination string
@ -76,3 +79,7 @@ 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(tokNETWORK, r)
}

View File

@ -4,6 +4,8 @@
package aa
const tokPIVOTROOT = "pivot_root"
type PivotRoot struct {
RuleBase
Qualifier
@ -42,3 +44,7 @@ func (r *PivotRoot) Equals(other any) bool {
r.TargetProfile == o.TargetProfile &&
r.Qualifier.Equals(o.Qualifier)
}
func (r *PivotRoot) String() string {
return renderTemplate(tokPIVOTROOT, r)
}

View File

@ -6,6 +6,12 @@ package aa
import (
"slices"
const (
tokABI = "abi"
tokALIAS = "alias"
tokINCLUDE = "include"
tokIFEXISTS = "if exists"
)
type Abi struct {
@ -27,6 +33,10 @@ func (r *Abi) Equals(other any) bool {
return r.Path == o.Path && r.IsMagic == o.IsMagic
}
func (r *Abi) String() string {
return renderTemplate(tokABI, r)
}
type Alias struct {
RuleBase
Path string
@ -46,6 +56,10 @@ func (r Alias) Equals(other any) bool {
return r.Path == o.Path && r.RewrittenPath == o.RewrittenPath
}
func (r *Alias) String() string {
return renderTemplate(tokALIAS, r)
}
type Include struct {
RuleBase
IfExists bool
@ -69,6 +83,10 @@ func (r *Include) Equals(other any) bool {
return r.Path == o.Path && r.IsMagic == o.IsMagic && r.IfExists == o.IfExists
}
func (r *Include) String() string {
return renderTemplate(tokINCLUDE, r)
}
type Variable struct {
RuleBase
Name string
@ -90,3 +108,7 @@ 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("variable", r)
}

View File

@ -10,6 +10,12 @@ import (
"strings"
)
const (
tokATTRIBUTES = "xattrs"
tokFLAGS = "flags"
tokPROFILE = "profile"
)
// Profile represents a single AppArmor profile.
type Profile struct {
RuleBase
@ -40,6 +46,11 @@ func (p *Profile) Equals(other any) bool {
slices.Equal(p.Flags, o.Flags)
}
func (p *Profile) String() string {
return renderTemplate(tokPROFILE, p)
}
// AddRule adds a new rule to the profile from a log map.
func (p *Profile) AddRule(log map[string]string) {

View File

@ -4,6 +4,8 @@
package aa
const tokPTRACE = "ptrace"
type Ptrace struct {
RuleBase
Qualifier
@ -36,3 +38,7 @@ func (r *Ptrace) Equals(other any) bool {
return slices.Equal(r.Access, o.Access) && r.Peer == o.Peer &&
r.Qualifier.Equals(o.Qualifier)
}
func (r *Ptrace) String() string {
return renderTemplate(tokPTRACE, r)
}

View File

@ -4,6 +4,12 @@
package aa
const (
tokRLIMIT = "rlimit"
tokSET = "set"
)
type Rlimit struct {
RuleBase
Key string
@ -35,3 +41,7 @@ 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(tokRLIMIT, r)
}

View File

@ -8,14 +8,27 @@ import (
"strings"
)
const (
tokALL = "all"
tokALLOW = "allow"
tokAUDIT = "audit"
tokDENY = "deny"
)
// Rule generic interface for all AppArmor rules
type Rule interface {
Less(other any) bool
Equals(other any) bool
String() string
}
type Rules []Rule
func (r Rules) String() string {
return renderTemplate("rules", r)
}
type RuleBase struct {
Comment string
NoNewPrivs bool
@ -69,6 +82,10 @@ func (r RuleBase) Equals(other any) bool {
return false
}
func (r RuleBase) String() string {
return renderTemplate("comment", r)
}
type Qualifier struct {
Audit bool
AccessType string
@ -104,3 +121,7 @@ func (r *All) Less(other any) bool {
func (r *All) Equals(other any) bool {
return false
}
func (r *All) String() string {
return renderTemplate(tokALL, r)
}

View File

@ -367,3 +367,113 @@ func TestRule_Equals(t *testing.T) {
})
}
}
func TestRule_String(t *testing.T) {
tests := []struct {
name string
rule Rule
want string
}{
{
name: "include1",
rule: include1,
want: "include <abstraction/base>",
},
{
name: "include-local",
rule: includeLocal1,
want: "include if exists <local/foo>",
},
{
name: "include-abs",
rule: &Include{Path: "/usr/share/apparmor.d/", IsMagic: false},
want: `include "/usr/share/apparmor.d/"`,
},
{
name: "rlimit",
rule: rlimit1,
want: "set rlimit nproc <= 200,",
},
{
name: "capability",
rule: capability1,
want: "capability net_admin,",
},
{
name: "capability/multi",
rule: &Capability{Names: []string{"dac_override", "dac_read_search"}},
want: "capability dac_override dac_read_search,",
},
{
name: "capability/all",
rule: &Capability{},
want: "capability,",
},
{
name: "network",
rule: network1,
want: "network netlink raw,",
},
{
name: "mount",
rule: mount1,
want: "mount fstype=overlay overlay -> /var/lib/docker/overlay2/opaque-bug-check1209538631/merged/, # failed perms check",
},
{
name: "pivot_root",
rule: pivotroot1,
want: "pivot_root oldroot=@{run}/systemd/mount-rootfs/ @{run}/systemd/mount-rootfs/,",
},
{
name: "change_profile",
rule: changeprofile1,
want: "change_profile -> systemd-user,",
},
{
name: "signal",
rule: signal1,
want: "signal receive set=kill peer=firefox//&firejail-default,",
},
{
name: "ptrace",
rule: ptrace1,
want: "ptrace read peer=nautilus,",
},
{
name: "unix",
rule: unix1,
want: "unix (receive send) type=stream protocol=0 addr=none peer=(label=dbus-daemon, addr=@/tmp/dbus-AaKMpxzC4k),",
},
{
name: "dbus",
rule: dbus1,
want: `dbus receive bus=session path=/org/gtk/vfs/metadata
interface=org.gtk.vfs.Metadata
member=Remove
peer=(name=:1.15, label=tracker-extract),`,
},
{
name: "dbus-bind",
rule: &Dbus{Access: []string{"bind"}, Bus: "session", Name: "org.gnome.*"},
want: `dbus bind bus=session name=org.gnome.*,`,
},
{
name: "dbus-full",
rule: &Dbus{Bus: "accessibility"},
want: `dbus bus=accessibility,`,
},
{
name: "file",
rule: file1,
want: "/usr/share/poppler/cMap/Identity-H r,",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := tt.rule
if got := r.String(); got != tt.want {
t.Errorf("Rule.String() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -4,6 +4,9 @@
package aa
const tokSIGNAL = "signal"
type Signal struct {
RuleBase
Qualifier
@ -41,3 +44,7 @@ func (r *Signal) Equals(other any) bool {
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(tokSIGNAL, r)
}

View File

@ -8,29 +8,40 @@ import (
"embed"
"fmt"
"reflect"
"slices"
"strings"
"text/template"
)
// Default indentation for apparmor profile (2 spaces)
const indentation = " "
var (
// Default indentation for apparmor profile (2 spaces)
TemplateIndentation = " "
// The current indentation level
TemplateIndentationLevel = 0
//go:embed templates/*.j2
//go:embed templates/rule/*.j2
tmplFiles embed.FS
// The functions available in the template
tmplFunctionMap = template.FuncMap{
"typeof": typeOf,
"join": join,
"cjoin": cjoin,
"indent": indent,
"overindent": indentDbus,
"setindent": setindent,
}
// The apparmor templates
tmpl = map[string]*template.Template{
"apparmor": generateTemplate("apparmor.j2"),
}
tmpl = generateTemplates([]string{
"apparmor", tokPROFILE, "rules", // Global templates
tokINCLUDE, tokRLIMIT, tokCAPABILITY, tokNETWORK,
tokMOUNT, tokPIVOTROOT, tokCHANGEPROFILE, tokSIGNAL,
tokPTRACE, tokUNIX, tokUSERNS, tokIOURING,
tokDBUS, "file",
})
// convert apparmor requested mask to apparmor access mode
maskToAccess = map[string]string{
@ -90,30 +101,35 @@ var (
fileWeights = map[string]int{}
)
func generateTemplate(name string) *template.Template {
res := template.New(name).Funcs(tmplFunctionMap)
switch name {
case "apparmor.j2":
res = template.Must(res.ParseFS(tmplFiles,
"templates/*.j2", "templates/rule/*.j2",
))
case "profile.j2":
res = template.Must(res.Parse("{{ template \"profile\" . }}"))
res = template.Must(res.ParseFS(tmplFiles,
"templates/profile.j2", "templates/rule/*.j2",
))
default:
res = template.Must(res.Parse(
fmt.Sprintf("{{ template \"%s\" . }}", name),
))
res = template.Must(res.ParseFS(tmplFiles,
fmt.Sprintf("templates/rule/%s.j2", name),
"templates/rule/qualifier.j2", "templates/rule/comment.j2",
func generateTemplates(names []string) map[string]*template.Template {
res := make(map[string]*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 renderTemplate(name string, data any) string {
var res strings.Builder
template, ok := tmpl[name]
if !ok {
panic("template not found")
}
err := template.Execute(&res, data)
if err != nil {
panic(err)
}
return res.String()
}
func init() {
for i, r := range fileAlphabet {
fileWeights[r] = i
@ -138,6 +154,25 @@ func join(i any) string {
}
}
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 typeOf(i any) string {
return strings.TrimPrefix(reflect.TypeOf(i).String(), "*aa.")
}
@ -146,12 +181,22 @@ func typeToValue(i reflect.Type) string {
return strings.ToLower(strings.TrimPrefix(i.String(), "*aa."))
}
func setindent(i string) string {
switch i {
case "++":
TemplateIndentationLevel++
case "--":
TemplateIndentationLevel--
}
return ""
}
func indent(s string) string {
return indentation + s
return strings.Repeat(TemplateIndentation, TemplateIndentationLevel) + s
}
func indentDbus(s string) string {
return indentation + " " + s
return strings.Join([]string{TemplateIndentation, s}, " ")
}
func getLetterIn(alphabet []string, in string) string {

View File

@ -4,6 +4,8 @@
package aa
const tokUNIX = "unix"
type Unix struct {
RuleBase
Qualifier
@ -74,3 +76,7 @@ func (r *Unix) Equals(other any) bool {
r.PeerLabel == o.PeerLabel && r.PeerAddr == o.PeerAddr &&
r.Qualifier.Equals(o.Qualifier)
}
func (r *Unix) String() string {
return renderTemplate(tokUNIX, r)
}

View File

@ -4,6 +4,8 @@
package aa
const tokUSERNS = "userns"
type Userns struct {
RuleBase
Qualifier
@ -30,3 +32,7 @@ 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(tokUSERNS, r)
}