feat(aa): parse apparmor preamble files.
This commit is contained in:
parent
2e043d4ec8
commit
a99387c323
6 changed files with 710 additions and 8 deletions
238
pkg/aa/parse.go
Normal file
238
pkg/aa/parse.go
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
// 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){
|
||||
tokCOMMENT: newComment,
|
||||
tokABI: newAbi,
|
||||
tokALIAS: newAlias,
|
||||
tokINCLUDE: newInclude,
|
||||
}
|
||||
|
||||
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] == tokVARIABLE {
|
||||
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 == tokCOMMENT {
|
||||
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], tokVARIABLE) {
|
||||
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, tokCOMMENT) {
|
||||
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, tokPROFILE):
|
||||
rawHeader = tmp
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue