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

359 lines
8.4 KiB
Go
Raw Normal View History

2021-11-09 23:41:12 +01:00
// aa-log - Review AppArmor generated messages
// Copyright (C) 2021-2023 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"
"sort"
2021-11-09 23:41:12 +01:00
"strings"
)
2023-03-10 11:32:03 +01:00
const usage = `aa-log [-h] [--systemd] [--file file] [profile]
Review AppArmor generated messages in a colorful way. Supports logs from
auditd, systemd, syslog as well as dbus session events.
It can be given an optional profile name to filter the output with.
Default logs are read from '/var/log/audit/audit.log'. Other files in
'/var/log/audit/' can easily be checked: 'aa-log -f 1' parses 'audit.log.1'
Options:
-h, --help Show this help message and exit.
-f, --file FILE Set a logfile or a suffix to the default log file.
-s, --systemd Parse systemd logs from journalctl.
`
// Command line options
var (
help bool
path string
systemd bool
)
// LogFiles is the list of default path to query
var LogFiles = []string{
"/var/log/audit/audit.log",
"/var/log/syslog",
}
2021-11-09 23:41:12 +01:00
// 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 inSlice(item string, slice []string) bool {
sort.Strings(slice)
i := sort.SearchStrings(slice, item)
return i < len(slice) && slice[i] == item
}
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
}
// getAuditLogs return a reader with the logs entries from Auditd
func getAuditLogs(path string) (io.Reader, error) {
file, err := os.Open(filepath.Clean(path))
if err != nil {
return nil, err
}
return file, err
}
// getJournalctlLogs return a reader with the logs entries from Systemd
func getJournalctlLogs(path string, useFile bool) (io.Reader, error) {
var logs []SystemdLog
var stdout bytes.Buffer
var value string
if useFile {
// content, err := os.ReadFile(filepath.Clean(path))
content, err := ioutil.ReadFile(filepath.Clean(path))
if err != nil {
return nil, err
}
value = string(content)
} else {
// journalctl -b -o json > systemd.log
cmd := exec.Command("journalctl", "--boot", "--output=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"])
toDecode := []string{"name", "comm"}
2023-02-08 17:29:37 +01:00
for _, name := range toDecode {
if value, ok := aa[name]; ok {
aa[name] = decodeHex(value)
}
}
2023-02-08 17:29:37 +01:00
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 getLogFile(path string) string {
info, err := os.Stat(filepath.Clean(path))
if err == nil && !info.IsDir() {
return path
}
for _, logfile := range LogFiles {
if _, err := os.Stat(logfile); err == nil {
oldLogfile := filepath.Clean(logfile + "." + path)
if _, err := os.Stat(oldLogfile); err == nil {
return oldLogfile
} else {
return logfile
}
}
}
return ""
}
func aaLog(logger string, path string, profile string) error {
var err error
var file io.Reader
switch logger {
case "auditd":
file, err = getAuditLogs(path)
case "systemd":
file, err = getJournalctlLogs(path, !inSlice(path, LogFiles))
default:
2022-10-16 13:11:07 +02:00
err = fmt.Errorf("Logger %s not supported.", logger)
}
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
}
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.BoolVar(&help, "help", false, "Show this help message and exit.")
flag.StringVar(&path, "f", "", "Set a logfile or a suffix to the default log file.")
flag.StringVar(&path, "file", "", "Set a logfile or a suffix to the default log file.")
flag.BoolVar(&systemd, "s", false, "Parse systemd logs from journalctl.")
flag.BoolVar(&systemd, "systemd", false, "Parse systemd logs from journalctl.")
}
2021-12-12 13:35:08 +01:00
func main() {
flag.Usage = func() { fmt.Print(usage) }
flag.Parse()
if help {
flag.Usage()
os.Exit(0)
}
profile := ""
if len(flag.Args()) >= 1 {
profile = flag.Args()[0]
}
logger := "auditd"
if systemd {
logger = "systemd"
}
logfile := getLogFile(path)
err := aaLog(logger, logfile, profile)
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
}