apparmor.d/cmd/aa-log/main.go

307 lines
6.8 KiB
Go
Raw Normal View History

2021-11-09 23:41:12 +01:00
// aa-log - Review AppArmor generated messages
// Copyright (C) 2021-2022 Alexandre Pujol <alexandre@pujol.io>
2021-11-09 23:41:12 +01:00
// SPDX-License-Identifier: GPL-2.0-only
package main
import (
"bufio"
"bytes"
"encoding/hex"
"encoding/json"
"flag"
2021-11-09 23:41:12 +01:00
"fmt"
"io"
"io/ioutil"
2021-11-09 23:41:12 +01:00
"os"
"os/exec"
2021-12-12 13:35:08 +01:00
"path/filepath"
2021-11-09 23:41:12 +01:00
"regexp"
"strings"
)
// Command line options
var (
2022-08-21 21:16:29 +02:00
dbus bool
help bool
path string
)
// LogFile is the default path to the file to query
2021-11-09 23:41:12 +01:00
const LogFile = "/var/log/audit/audit.log"
// Colors
const (
2022-03-17 15:03:00 +01:00
Reset = "\033[0m"
FgGreen = "\033[32m"
2022-03-17 15:03:00 +01:00
FgYellow = "\033[33m"
FgBlue = "\033[34m"
FgMagenta = "\033[35m"
FgCian = "\033[36m"
FgWhite = "\033[37m"
2022-03-17 15:03:00 +01:00
BoldRed = "\033[1;31m"
BoldGreen = "\033[1;32m"
BoldYellow = "\033[1;33m"
2021-11-09 23:41:12 +01:00
)
// AppArmorLog describes a apparmor log entry
type AppArmorLog map[string]string
// AppArmorLogs describes all apparmor log entries
type AppArmorLogs []AppArmorLog
2021-11-09 23:41:12 +01:00
// SystemdLog is a simplified systemd json log representation.
type SystemdLog struct {
Message string `json:"MESSAGE"`
}
var (
quoted bool
isHexa = regexp.MustCompile("^[0-9A-Fa-f]+$")
)
func splitQuoted(r rune) bool {
if r == '"' {
quoted = !quoted
}
return !quoted && r == ' '
}
2021-12-05 00:53:05 +01:00
func toQuote(str string) string {
if strings.Contains(str, " ") {
return `"` + str + `"`
}
return str
}
func decodeHex(str string) string {
if isHexa.MatchString(str) {
bs, _ := hex.DecodeString(str)
return string(bs)
}
return str
}
2021-11-09 23:41:12 +01:00
func removeDuplicateLog(logs []string) []string {
list := []string{}
2021-11-21 22:57:11 +01:00
keys := map[string]interface{}{"": true}
2021-11-09 23:41:12 +01:00
for _, log := range logs {
if _, v := keys[log]; !v {
keys[log] = true
list = append(list, log)
}
}
return list
}
// getJournalctlDbusSessionLogs return a reader with the logs entries
func getJournalctlDbusSessionLogs(file io.Reader, useFile bool) (io.Reader, error) {
var logs []SystemdLog
var stdout bytes.Buffer
var value string
if useFile {
content, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
value = string(content)
} else {
cmd := exec.Command("journalctl", "--user", "-b", "-u", "dbus.service", "-o", "json")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return nil, err
}
value = stdout.String()
}
value = strings.Replace(value, "\n", ",\n", -1)
value = strings.TrimSuffix(value, ",\n")
value = `[` + value + `]`
if err := json.Unmarshal([]byte(value), &logs); err != nil {
return nil, err
}
res := ""
for _, log := range logs {
res += log.Message + "\n"
}
return strings.NewReader(res), nil
}
2021-12-05 00:53:05 +01:00
// NewApparmorLogs return a new ApparmorLogs list of map from a log file
func NewApparmorLogs(file io.Reader, profile string) AppArmorLogs {
2021-11-09 23:41:12 +01:00
log := ""
2022-09-06 18:49:40 +02:00
exp := `apparmor=("DENIED"|"ALLOWED"|"AUDIT")`
if profile != "" {
2022-09-06 18:49:40 +02:00
exp = fmt.Sprintf(exp+`.* (profile="%s.*"|label="%s.*")`, profile, profile)
}
2021-11-09 23:41:12 +01:00
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 logs
2022-09-06 18:49:40 +02:00
regex := regexp.MustCompile(`.*apparmor="`)
log = regex.ReplaceAllLiteralString(log, `apparmor="`)
regexAppArmorLogs := map[*regexp.Regexp]string{
regexp.MustCompile(`(peer_|)pid=[0-9]* `): "",
regexp.MustCompile(` fsuid.*`): "",
regexp.MustCompile(` exe=.*`): "",
2021-11-09 23:41:12 +01:00
}
for regex, value := range regexAppArmorLogs {
log = regex.ReplaceAllLiteralString(log, value)
2021-11-09 23:41:12 +01:00
}
// Remove doublon in logs
logs := strings.Split(log, "\n")
logs = removeDuplicateLog(logs)
// Parse log into ApparmorLog struct
aaLogs := make(AppArmorLogs, 0)
2021-11-09 23:41:12 +01:00
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 {
2021-12-05 00:53:05 +01:00
aa[kv[0]] = strings.Trim(kv[1], `"`)
}
2021-11-09 23:41:12 +01:00
}
aa["profile"] = decodeHex(aa["profile"])
if name, ok := aa["name"]; ok {
aa["name"] = decodeHex(name)
}
aaLogs = append(aaLogs, aa)
2021-11-09 23:41:12 +01:00
}
return aaLogs
}
2021-12-05 00:53:05 +01:00
// String returns a formatted AppArmor logs string
func (aaLogs AppArmorLogs) String() string {
res := ""
2021-11-09 23:41:12 +01:00
state := map[string]string{
"DENIED": BoldRed + "DENIED " + Reset,
"ALLOWED": BoldGreen + "ALLOWED" + Reset,
2022-03-17 15:03:00 +01:00
"AUDIT": BoldYellow + "AUDIT " + Reset,
2021-11-09 23:41:12 +01:00
}
2021-11-21 22:57:11 +01:00
// Order of impression
keys := []string{
"profile", "label", // Profile name
"operation", "name",
"mask", "bus", "path", "interface", "member", // dbus
"info", "comm",
"laddr", "lport", "faddr", "fport", "family", "sock_type", "protocol",
2021-11-21 22:57:11 +01:00
"requested_mask", "denied_mask", "signal", "peer", // "fsuid", "ouid", "FSUID", "OUID",
}
2021-11-21 22:57:11 +01:00
// Optional colors template to use
colors := map[string]string{
"profile": FgBlue,
"label": FgBlue,
"operation": FgYellow,
"name": 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,
}
2021-11-09 23:41:12 +01:00
for _, log := range aaLogs {
2021-11-21 22:57:11 +01:00
seen := map[string]bool{"apparmor": true}
res += state[log["apparmor"]]
for _, key := range keys {
if log[key] != "" {
if colors[key] != "" {
2021-12-05 00:53:05 +01:00
res += " " + colors[key] + toQuote(log[key]) + Reset
} else {
2021-12-05 00:53:05 +01:00
res += " " + key + "=" + toQuote(log[key])
}
2021-11-21 22:57:11 +01:00
seen[key] = true
}
}
for key, value := range log {
if !seen[key] && value != "" {
2021-12-05 00:53:05 +01:00
res += " " + key + "=" + toQuote(value)
2021-11-21 22:57:11 +01:00
}
}
res += "\n"
2021-11-09 23:41:12 +01:00
}
return res
}
2021-11-09 23:41:12 +01:00
func aaLog(path string, profile string, dbus bool) error {
2021-12-12 13:35:08 +01:00
file, err := os.Open(filepath.Clean(path))
2021-11-09 23:41:12 +01:00
if err != nil {
2021-12-12 13:35:08 +01:00
return err
2021-11-09 23:41:12 +01:00
}
2021-12-12 13:35:08 +01:00
/* #nosec G307 */
2021-11-09 23:41:12 +01:00
defer func() {
if err := file.Close(); err != nil {
2021-12-12 13:35:08 +01:00
fmt.Println(err)
2021-11-09 23:41:12 +01:00
}
}()
if dbus {
file, err := getJournalctlDbusSessionLogs(file, path != LogFile)
if err != nil {
return err
}
aaLogs := NewApparmorLogs(file, profile)
fmt.Print(aaLogs.String())
} else {
aaLogs := NewApparmorLogs(file, profile)
fmt.Print(aaLogs.String())
}
return nil
2021-12-12 13:35:08 +01:00
}
func init() {
flag.BoolVar(&help, "h", false, "Show this help message and exit.")
flag.StringVar(&path, "f", LogFile,
"Set a log`file` or a suffix to the default log file.")
flag.BoolVar(&dbus, "d", false, "Show dbus session event.")
}
2021-12-12 13:35:08 +01:00
func main() {
flag.Parse()
if help {
fmt.Printf(`aa-log [-h] [-d] [-f file] [profile]
Review AppArmor generated messages in a colorful way.
It can be given an optional profile name to filter the output with.
`)
flag.PrintDefaults()
os.Exit(0)
}
profile := ""
if len(flag.Args()) >= 1 {
profile = flag.Args()[0]
}
logfile := filepath.Clean(LogFile + "." + path)
if _, err := os.Stat(logfile); err != nil {
logfile = path
}
err := aaLog(logfile, profile, dbus)
2021-12-12 13:35:08 +01:00
if err != nil {
fmt.Println(err)
os.Exit(1)
}
2021-11-09 23:41:12 +01:00
}