// apparmor.d - Full set of apparmor profiles // Copyright (C) 2021-2024 Alexandre Pujol // 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, ",") + "}" } }