From 54836685744db13efff1d13654b408a0d9a25557 Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Tue, 23 Apr 2024 21:26:09 +0100 Subject: [PATCH] feat(aa): add a string method to all rule struct. --- pkg/aa/apparmor.go | 9 +--- pkg/aa/capability.go | 4 ++ pkg/aa/change_profile.go | 6 +++ pkg/aa/dbus.go | 6 +++ pkg/aa/file.go | 4 ++ pkg/aa/io_uring.go | 5 ++ pkg/aa/mount.go | 19 +++++++ pkg/aa/mqueue.go | 7 +++ pkg/aa/network.go | 7 +++ pkg/aa/pivot_root.go | 6 +++ pkg/aa/preamble.go | 22 ++++++++ pkg/aa/profile.go | 11 ++++ pkg/aa/ptrace.go | 6 +++ pkg/aa/rlimit.go | 10 ++++ pkg/aa/rules.go | 21 ++++++++ pkg/aa/rules_test.go | 110 +++++++++++++++++++++++++++++++++++++++ pkg/aa/signal.go | 7 +++ pkg/aa/template.go | 99 +++++++++++++++++++++++++---------- pkg/aa/unix.go | 6 +++ pkg/aa/userns.go | 6 +++ 20 files changed, 337 insertions(+), 34 deletions(-) diff --git a/pkg/aa/apparmor.go b/pkg/aa/apparmor.go index a7f6cc262..7a10c085b 100644 --- a/pkg/aa/apparmor.go +++ b/pkg/aa/apparmor.go @@ -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 diff --git a/pkg/aa/capability.go b/pkg/aa/capability.go index 0e4918fab..14b272091 100644 --- a/pkg/aa/capability.go +++ b/pkg/aa/capability.go @@ -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) } diff --git a/pkg/aa/change_profile.go b/pkg/aa/change_profile.go index c8114653e..32106f9bf 100644 --- a/pkg/aa/change_profile.go +++ b/pkg/aa/change_profile.go @@ -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) +} diff --git a/pkg/aa/dbus.go b/pkg/aa/dbus.go index 45a5dabfd..3ab30ae75 100644 --- a/pkg/aa/dbus.go +++ b/pkg/aa/dbus.go @@ -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) +} diff --git a/pkg/aa/file.go b/pkg/aa/file.go index 266989d80..9390afbe7 100644 --- a/pkg/aa/file.go +++ b/pkg/aa/file.go @@ -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) } diff --git a/pkg/aa/io_uring.go b/pkg/aa/io_uring.go index 90aae2498..eedee8452 100644 --- a/pkg/aa/io_uring.go +++ b/pkg/aa/io_uring.go @@ -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) } diff --git a/pkg/aa/mount.go b/pkg/aa/mount.go index 103a871da..7f0f5621e 100644 --- a/pkg/aa/mount.go +++ b/pkg/aa/mount.go @@ -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) +} diff --git a/pkg/aa/mqueue.go b/pkg/aa/mqueue.go index 42b674b56..0035f2cd6 100644 --- a/pkg/aa/mqueue.go +++ b/pkg/aa/mqueue.go @@ -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) +} diff --git a/pkg/aa/network.go b/pkg/aa/network.go index ec8188ecf..d4fd96690 100644 --- a/pkg/aa/network.go +++ b/pkg/aa/network.go @@ -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) +} diff --git a/pkg/aa/pivot_root.go b/pkg/aa/pivot_root.go index 94f289c59..66829f56c 100644 --- a/pkg/aa/pivot_root.go +++ b/pkg/aa/pivot_root.go @@ -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) +} diff --git a/pkg/aa/preamble.go b/pkg/aa/preamble.go index d10b4dfbb..6970bee56 100644 --- a/pkg/aa/preamble.go +++ b/pkg/aa/preamble.go @@ -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) +} diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go index 24576e205..c47f33f2e 100644 --- a/pkg/aa/profile.go +++ b/pkg/aa/profile.go @@ -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) { diff --git a/pkg/aa/ptrace.go b/pkg/aa/ptrace.go index 17b04cb9a..ffe69dfc9 100644 --- a/pkg/aa/ptrace.go +++ b/pkg/aa/ptrace.go @@ -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) +} diff --git a/pkg/aa/rlimit.go b/pkg/aa/rlimit.go index 66ed3c5e1..585211e7e 100644 --- a/pkg/aa/rlimit.go +++ b/pkg/aa/rlimit.go @@ -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) +} diff --git a/pkg/aa/rules.go b/pkg/aa/rules.go index 16b06503d..ed3594e66 100644 --- a/pkg/aa/rules.go +++ b/pkg/aa/rules.go @@ -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) +} diff --git a/pkg/aa/rules_test.go b/pkg/aa/rules_test.go index 96d7a5df1..1f035278c 100644 --- a/pkg/aa/rules_test.go +++ b/pkg/aa/rules_test.go @@ -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 ", + }, + { + name: "include-local", + rule: includeLocal1, + want: "include if exists ", + }, + { + 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) + } + }) + } +} diff --git a/pkg/aa/signal.go b/pkg/aa/signal.go index 13deb74df..237607d2a 100644 --- a/pkg/aa/signal.go +++ b/pkg/aa/signal.go @@ -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) +} diff --git a/pkg/aa/template.go b/pkg/aa/template.go index 2e94480b2..ab9764692 100644 --- a/pkg/aa/template.go +++ b/pkg/aa/template.go @@ -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 { diff --git a/pkg/aa/unix.go b/pkg/aa/unix.go index f734f327e..ee92fe381 100644 --- a/pkg/aa/unix.go +++ b/pkg/aa/unix.go @@ -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) +} diff --git a/pkg/aa/userns.go b/pkg/aa/userns.go index e6c41bc0b..24087e111 100644 --- a/pkg/aa/userns.go +++ b/pkg/aa/userns.go @@ -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) +}