// apparmor.d - Full set of apparmor profiles // Copyright (C) 2021-2023 Alexandre Pujol // SPDX-License-Identifier: GPL-2.0-only package logs import ( "bufio" "fmt" "io" "regexp" "strings" "github.com/roddhjav/apparmor.d/pkg/aa" "github.com/roddhjav/apparmor.d/pkg/util" "golang.org/x/exp/slices" ) // Colors const ( reset = "\033[0m" fgGreen = "\033[32m" fgYellow = "\033[33m" fgBlue = "\033[34m" fgMagenta = "\033[35m" fgCian = "\033[36m" fgWhite = "\033[37m" boldRed = "\033[1;31m" boldGreen = "\033[1;32m" boldYellow = "\033[1;33m" ) var ( quoted bool isAppArmorLogTemplate = regexp.MustCompile(`apparmor=("DENIED"|"ALLOWED"|"AUDIT")`) regCleanLogs = util.ToRegexRepl([]string{ // Clean apparmor log file `.*apparmor="`, `apparmor="`, `(peer_|)pid=[0-9]*\s`, " ", `\x1d`, " ", // Resolve classic user variables `/home/[^/]+/.cache`, `@{user_cache_dirs}`, `/home/[^/]+/.config`, `@{user_config_dirs}`, `/home/[^/]+/.local/share`, `@{user_share_dirs}`, `/home/[^/]+/.local/state`, `@{user_state_dirs}`, `/home/[^/]+/.local/bin`, `@{user_bin_dirs}`, `/home/[^/]+/.local/lib`, `@{user_lib_dirs}`, `/home/[^/]+/.ssh`, `@{HOME}/@{XDG_SSH_DIR}`, `/home/[^/]+/.gnupg`, `@{HOME}/@{XDG_GPG_DIR}`, `/home/[^/]+`, `@{HOME}`, // Resolve classic system variables `/usr/lib(|32|64|exec)`, `@{lib}`, `/usr/(|s)bin`, `@{bin}`, `[^/]+-linux-gnu[^/]?`, `@{multiarch}`, `/run/`, `@{run}/`, `user/[0-9]*/`, `user/@{uid}/`, `/proc/`, `@{PROC}/`, `@{PROC}/[0-9]*/`, `@{PROC}/@{pid}/`, `@{PROC}/@{pid}/task/[0-9]*/`, `@{PROC}/@{pid}/task/@{tid}/`, `/sys/`, `@{sys}/`, `@{PROC}@{sys}/`, `@{PROC}/sys/`, `pci[0-9][0-9][0-9][0-9]:[0-9][0-9]`, `@{pci_bus}`, `@{pci_bus}/([0-9][0-9][0-9][0-9]:[0-9][0-9]:[0-9a-f][0-9a-f]\.[0-9]/)+`, `@{pci}/`, // Some system glob `:1.[0-9]*`, `:*`, // dbus peer name `@{bin}/(|ba|da)sh`, `@{bin}/{,ba,da}sh`, // collect all shell `@{lib}/modules/[^/]+\/`, `@{lib}/modules/*/`, // strip kernel version numbers from kernel module accesses // Remove basic rules from abstractions/base `(?m)^.*/etc/[^/]+so.*$`, ``, `(?m)^.*@{lib}/[^/]+so.*$`, ``, `(?m)^.*@{lib}/locale/.*$`, ``, `(?m)^.*/usr/share/locale[^/]?/.*$`, ``, `(?m)^.*/usr/share/zoneinfo[^/]?/.*$`, ``, `(?m)^.*/dev/(null|zero|full|log).*$`, ``, `(?m)^.*/dev/(u|)random.*$`, ``, }) ) type AppArmorLog map[string]string // AppArmorLogs describes all apparmor log entries type AppArmorLogs []AppArmorLog func splitQuoted(r rune) bool { if r == '"' { quoted = !quoted } return !quoted && r == ' ' } func toQuote(str string) string { if strings.Contains(str, " ") { return `"` + str + `"` } return str } // NewApparmorLogs return a new ApparmorLogs list of map from a log file func NewApparmorLogs(file io.Reader, profile string) AppArmorLogs { log := "" isAppArmorLog := isAppArmorLogTemplate.Copy() if profile != "" { exp := `apparmor=("DENIED"|"ALLOWED"|"AUDIT")` exp = fmt.Sprintf(exp+`.* (profile="%s.*"|label="%s.*")`, profile, profile) isAppArmorLog = regexp.MustCompile(exp) } // Select Apparmor logs scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if isAppArmorLog.MatchString(line) { log += line + "\n" } } // Clean & remove doublon in logs for _, aa := range regCleanLogs { log = aa.Regex.ReplaceAllLiteralString(log, aa.Repl) } logs := strings.Split(log, "\n") logs = util.RemoveDuplicate(logs) // Parse log into ApparmorLog struct aaLogs := make(AppArmorLogs, 0) for _, log := range logs { quoted = false tmp := strings.FieldsFunc(log, splitQuoted) aa := make(AppArmorLog) for _, item := range tmp { kv := strings.Split(item, "=") if len(kv) >= 2 { aa[kv[0]] = strings.Trim(kv[1], `"`) } } aa["profile"] = util.DecodeHex(aa["profile"]) toDecode := []string{"name", "comm"} for _, name := range toDecode { if value, ok := aa[name]; ok { aa[name] = util.DecodeHex(value) } } aaLogs = append(aaLogs, aa) } return aaLogs } // String returns a formatted AppArmor logs string func (aaLogs AppArmorLogs) String() string { // Apparmor log states state := map[string]string{ "DENIED": boldRed + "DENIED " + reset, "ALLOWED": boldGreen + "ALLOWED" + reset, "AUDIT": boldYellow + "AUDIT " + reset, } // Print order of impression keys := []string{ "profile", "label", // Profile name "operation", "name", "target", "mask", "bus", "path", "interface", "member", // dbus "info", "comm", "laddr", "lport", "faddr", "fport", "family", "sock_type", "protocol", "requested_mask", "denied_mask", "signal", "peer", } // Key to not print ignore := []string{ "fsuid", "ouid", "FSUID", "OUID", "exe", "SAUID", "sauid", "terminal", "UID", "AUID", "hostname", "addr", "class", } // Color template to use colors := map[string]string{ "profile": fgBlue, "label": fgBlue, "operation": fgYellow, "name": fgMagenta, "target": "-> " + fgMagenta, "mask": boldRed, "bus": fgCian + "bus=", "path": "path=" + fgWhite, "requested_mask": "requested_mask=" + boldRed, "denied_mask": "denied_mask=" + boldRed, "interface": "interface=" + fgWhite, "member": "member=" + fgGreen, } res := "" for _, log := range aaLogs { seen := map[string]bool{"apparmor": true} res += state[log["apparmor"]] fsuid := log["fsuid"] ouid := log["ouid"] for _, key := range keys { if log[key] != "" { if key == "name" && fsuid == ouid && !strings.Contains(log["operation"], "dbus") { res += colors[key] + " owner" + reset } if colors[key] != "" { res += " " + colors[key] + toQuote(log[key]) + reset } else { res += " " + key + "=" + toQuote(log[key]) } seen[key] = true } } for key, value := range log { if slices.Contains(ignore, key) { continue } if !seen[key] && value != "" { res += " " + key + "=" + toQuote(value) } } res += "\n" } return res } // ParseToProfiles convert the log data into a new AppArmorProfiles func (aaLogs AppArmorLogs) ParseToProfiles() aa.AppArmorProfiles { profiles := make(aa.AppArmorProfiles, 0) for _, log := range aaLogs { name := "" if strings.Contains(log["operation"], "dbus") { name = log["label"] } else { name = log["profile"] } if _, ok := profiles[name]; !ok { profile := &aa.AppArmorProfile{} profile.Name = name profile.AddRule(log) profiles[name] = profile } else { profiles[name].AddRule(log) } } return profiles }