From 322866d902b756ad9a6c1a76fb38725a5f57507f Mon Sep 17 00:00:00 2001 From: Alexandre Pujol Date: Sun, 16 Apr 2023 23:33:07 +0100 Subject: [PATCH] refractor: update aa-log to the new structure. --- .gitlab-ci.yml | 2 +- cmd/aa-log/main.go | 278 +-------------------------------- cmd/aa-log/main_test.go | 329 ---------------------------------------- 3 files changed, 8 insertions(+), 601 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d8209e5d..01bea9c3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,7 +49,7 @@ tests: - chmod 755 /usr/bin/journalctl - mkdir -p /var/log/audit/ - touch /var/log/audit/audit.log /var/log/audit/audit.log.1 - - go test ./cmd/aa-log -v -cover -coverprofile=coverage.out + - go test ./cmd/... ./pkg/... -v -cover -coverprofile=coverage.out - go tool cover -func=coverage.out diff --git a/cmd/aa-log/main.go b/cmd/aa-log/main.go index 8e9d3651..8cd66f52 100644 --- a/cmd/aa-log/main.go +++ b/cmd/aa-log/main.go @@ -5,20 +5,13 @@ package main import ( - "bufio" - "bytes" - "encoding/hex" - "encoding/json" "flag" "fmt" "io" - "io/ioutil" "os" - "os/exec" - "path/filepath" - "regexp" - "sort" - "strings" + + "github.com/roddhjav/apparmor.d/pkg/logs" + "github.com/roddhjav/apparmor.d/pkg/util" ) const usage = `aa-log [-h] [--systemd] [--file file] [profile] @@ -45,279 +38,22 @@ var ( systemd bool ) -// LogFiles is the list of default path to query -var LogFiles = []string{ - "/var/log/audit/audit.log", - "/var/log/syslog", -} - -// 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" -) - -// AppArmorLog describes a apparmor log entry -type AppArmorLog map[string]string - -// AppArmorLogs describes all apparmor log entries -type AppArmorLogs []AppArmorLog - -// 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 == ' ' -} - -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 -} - -func removeDuplicateLog(logs []string) []string { - list := []string{} - keys := map[string]interface{}{"": true} - 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 -} - -// NewApparmorLogs return a new ApparmorLogs list of map from a log file -func NewApparmorLogs(file io.Reader, profile string) AppArmorLogs { - log := "" - exp := `apparmor=("DENIED"|"ALLOWED"|"AUDIT")` - if profile != "" { - 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 logs - 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=.*`): "", - } - for regex, value := range regexAppArmorLogs { - log = regex.ReplaceAllLiteralString(log, value) - } - - // Remove doublon in logs - logs := strings.Split(log, "\n") - logs = removeDuplicateLog(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"] = decodeHex(aa["profile"]) - toDecode := []string{"name", "comm"} - for _, name := range toDecode { - if value, ok := aa[name]; ok { - aa[name] = decodeHex(value) - } - } - - aaLogs = append(aaLogs, aa) - } - - return aaLogs -} - -// String returns a formatted AppArmor logs string -func (aaLogs AppArmorLogs) String() string { - res := "" - state := map[string]string{ - "DENIED": BoldRed + "DENIED " + Reset, - "ALLOWED": BoldGreen + "ALLOWED" + Reset, - "AUDIT": BoldYellow + "AUDIT " + Reset, - } - // 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", - "requested_mask", "denied_mask", "signal", "peer", // "fsuid", "ouid", "FSUID", "OUID", - } - // 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, - } - - for _, log := range aaLogs { - seen := map[string]bool{"apparmor": true} - res += state[log["apparmor"]] - - for _, key := range keys { - if log[key] != "" { - 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 !seen[key] && value != "" { - res += " " + key + "=" + toQuote(value) - } - } - res += "\n" - } - return res -} - -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) + file, err = logs.GetAuditLogs(path) case "systemd": - file, err = getJournalctlLogs(path, !inSlice(path, LogFiles)) + file, err = logs.GetJournalctlLogs(path, !util.InSlice(path, logs.LogFiles)) default: err = fmt.Errorf("Logger %s not supported.", logger) } if err != nil { return err } - aaLogs := NewApparmorLogs(file, profile) + aaLogs := logs.NewApparmorLogs(file, profile) fmt.Print(aaLogs.String()) return nil } @@ -349,7 +85,7 @@ func main() { logger = "systemd" } - logfile := getLogFile(path) + logfile := logs.GetLogFile(path) err := aaLog(logger, logfile, profile) if err != nil { fmt.Println(err) diff --git a/cmd/aa-log/main_test.go b/cmd/aa-log/main_test.go index b5a8b54a..e07da7d7 100644 --- a/cmd/aa-log/main_test.go +++ b/cmd/aa-log/main_test.go @@ -5,338 +5,9 @@ package main import ( - "os" - "reflect" - "strings" "testing" ) -var refKmod = AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "kmod", - "operation": "file_inherit", - "comm": "modprobe", - "family": "unix", - "sock_type": "stream", - "protocol": "0", - "requested_mask": "send receive", - }, -} - -var refMan = AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "man", - "operation": "exec", - "name": "/usr/bin/preconv", - "info": "no new privs", - "comm": "man", - "requested_mask": "x", - "denied_mask": "x", - "error": "-1", - }, -} - -func TestAppArmorEvents(t *testing.T) { - tests := []struct { - name string - event string - want AppArmorLogs - }{ - { - name: "event_audit_1", - event: `type=AVC msg=audit(1345027352.096:499): apparmor="ALLOWED" operation="rename_dest" parent=6974 profile="/usr/sbin/httpd2-prefork//vhost_foo" name=2F686F6D652F7777772F666F6F2E6261722E696E2F68747470646F63732F61707061726D6F722F696D616765732F746573742F696D61676520312E6A7067 pid=20143 comm="httpd2-prefork" requested_mask="wc" denied_mask="wc" fsuid=30 ouid=30`, - want: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "/usr/sbin/httpd2-prefork//vhost_foo", - "operation": "rename_dest", - "name": "/home/www/foo.bar.in/httpdocs/apparmor/images/test/image 1.jpg", - "comm": "httpd2-prefork", - "requested_mask": "wc", - "denied_mask": "wc", - "parent": "6974", - }, - }, - }, - { - name: "event_audit_2", - event: `type=AVC msg=audit(1322614918.292:4376): apparmor="ALLOWED" operation="file_perm" parent=16001 profile=666F6F20626172 name="/home/foo/.bash_history" pid=17011 comm="bash" requested_mask="rw" denied_mask="rw" fsuid=0 ouid=1000`, - want: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "foo bar", - "operation": "file_perm", - "name": "/home/foo/.bash_history", - "comm": "bash", - "requested_mask": "rw", - "denied_mask": "rw", - "parent": "16001", - }, - }, - }, - { - name: "disconnected_path", - event: `type=AVC msg=audit(1424425690.883:716630): apparmor="ALLOWED" operation="file_mmap" info="Failed name lookup - disconnected path" error=-13 profile="/sbin/klogd" name="var/run/nscd/passwd" pid=25333 comm="id" requested_mask="r" denied_mask="r" fsuid=1002 ouid=0`, - want: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "/sbin/klogd", - "operation": "file_mmap", - "name": "var/run/nscd/passwd", - "comm": "id", - "info": "Failed name lookup - disconnected path", - "requested_mask": "r", - "denied_mask": "r", - "error": "-13", - }, - }, - }, - { - name: "dbus_system", - event: `type=USER_AVC msg=audit(1111111111.111:1111): pid=1780 uid=102 auid=4294967295 ses=4294967295 subj=? msg='apparmor="ALLOWED" operation="dbus_method_call" bus="system" path="/org/freedesktop/PolicyKit1/Authority" interface="org.freedesktop.PolicyKit1.Authority" member="CheckAuthorization" mask="send" name="org.freedesktop.PolicyKit1" pid=1794 label="snapd" peer_pid=1790 peer_label="polkitd" exe="/usr/bin/dbus-daemon" sauid=102 hostname=? addr=? terminal=?'UID="messagebus" AUID="unset" SAUID="messagebus"`, - want: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "", - "label": "snapd", - "operation": "dbus_method_call", - "name": "org.freedesktop.PolicyKit1", - "mask": "send", - "bus": "system", - "path": "/org/freedesktop/PolicyKit1/Authority", - "interface": "org.freedesktop.PolicyKit1.Authority", - "member": "CheckAuthorization", - "peer_label": "polkitd", - }, - }, - }, - { - name: "dbus_session", - event: `apparmor="ALLOWED" operation="dbus_bind" bus="session" name="org.freedesktop.portal.Documents" mask="bind" pid=2174 label="xdg-document-portal"`, - want: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "", - "label": "xdg-document-portal", - "operation": "dbus_bind", - "name": "org.freedesktop.portal.Documents", - "mask": "bind", - "bus": "session", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - file := strings.NewReader(tt.event) - if got := NewApparmorLogs(file, ""); !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewApparmorLogs() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNewApparmorLogs(t *testing.T) { - tests := []struct { - name string - path string - want AppArmorLogs - }{ - { - name: "dnsmasq", - path: "../../tests/audit.log", - want: AppArmorLogs{ - { - "apparmor": "DENIED", - "profile": "dnsmasq", - "operation": "open", - "name": "/proc/sys/kernel/osrelease", - "comm": "dnsmasq", - "requested_mask": "r", - "denied_mask": "r", - }, - { - "apparmor": "DENIED", - "profile": "dnsmasq", - "operation": "open", - "name": "/proc/1/environ", - "comm": "dnsmasq", - "requested_mask": "r", - "denied_mask": "r", - }, - { - "apparmor": "DENIED", - "profile": "dnsmasq", - "operation": "open", - "name": "/proc/cmdline", - "comm": "dnsmasq", - "requested_mask": "r", - "denied_mask": "r", - }, - }, - }, - { - name: "kmod", - path: "../../tests/audit.log", - want: refKmod, - }, - { - name: "man", - path: "../../tests/audit.log", - want: refMan, - }, - { - name: "power-profiles-daemon", - path: "../../tests/audit.log", - want: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "", - "label": "power-profiles-daemon", - "operation": "dbus_method_call", - "name": "org.freedesktop.DBus", - "mask": "send", - "bus": "system", - "path": "/org/freedesktop/DBus", - "interface": "org.freedesktop.DBus", - "member": "AddMatch", - "peer_label": "dbus-daemon", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - file, _ := os.Open(tt.path) - if got := NewApparmorLogs(file, tt.name); !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewApparmorLogs() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_getJournalctlLogs(t *testing.T) { - tests := []struct { - name string - path string - useFile bool - want AppArmorLogs - }{ - { - name: "gsd-xsettings", - useFile: true, - path: "../../tests/systemd.log", - want: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "", - "label": "gsd-xsettings", - "operation": "dbus_method_call", - "name": ":1.88", - "mask": "receive", - "bus": "session", - "path": "/org/gtk/Settings", - "interface": "org.freedesktop.DBus.Properties", - "member": "GetAll", - "peer_label": "gnome-extension-ding", - }, - }, - }, - { - name: "journalctl", - useFile: false, - path: "", - want: AppArmorLogs{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - reader, _ := getJournalctlLogs(tt.path, tt.useFile) - if got := NewApparmorLogs(reader, tt.name); !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewApparmorLogs() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAppArmorLogs_String(t *testing.T) { - tests := []struct { - name string - aaLogs AppArmorLogs - want string - }{ - { - name: "kmod", - aaLogs: refKmod, - want: "\033[1;32mALLOWED\033[0m \033[34mkmod\033[0m \033[33mfile_inherit\033[0m comm=modprobe family=unix sock_type=stream protocol=0 requested_mask=\033[1;31m\"send receive\"\033[0m\n", - }, - { - name: "man", - aaLogs: refMan, - want: "\033[1;32mALLOWED\033[0m \033[34mman\033[0m \033[33mexec\033[0m \033[35m/usr/bin/preconv\033[0m info=\"no new privs\" comm=man requested_mask=\033[1;31mx\033[0m denied_mask=\033[1;31mx\033[0m error=-1\n", - }, - { - name: "power-profiles-daemon", - aaLogs: AppArmorLogs{ - { - "apparmor": "ALLOWED", - "profile": "", - "label": "power-profiles-daemon", - "operation": "dbus_method_call", - "name": "org.freedesktop.DBus", - "mask": "send", - "bus": "system", - "path": "/org/freedesktop/DBus", - "interface": "org.freedesktop.DBus", - "member": "AddMatch", - "peer_label": "dbus-daemon", - }, - }, - want: "\033[1;32mALLOWED\033[0m \033[34mpower-profiles-daemon\033[0m \033[33mdbus_method_call\033[0m \033[35morg.freedesktop.DBus\033[0m \033[1;31msend\033[0m \033[36mbus=system\033[0m path=\033[37m/org/freedesktop/DBus\033[0m interface=\033[37morg.freedesktop.DBus\033[0m member=\033[32mAddMatch\033[0m peer_label=dbus-daemon\n", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.aaLogs.String(); got != tt.want { - t.Errorf("AppArmorLogs.String() = %v, want %v len: %d - %d", got, tt.want, len(got), len(tt.want)) - } - }) - } -} - -func Test_getLogFile(t *testing.T) { - tests := []struct { - name string - path string - want string - }{ - { - name: "Get audit.log", - path: "../../tests/audit.log", - want: "../../tests/audit.log", - }, - { - name: "Get /var/log/audit/audit.log.1", - path: "1", - want: "/var/log/audit/audit.log.1", - }, - { - name: "Get default log file", - path: "", - want: "/var/log/audit/audit.log", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getLogFile(tt.path); got != tt.want { - t.Errorf("getLogFile() = %v, want %v", got, tt.want) - } - }) - } -} - func Test_app(t *testing.T) { tests := []struct { name string