auparse/auparse.go (452 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package auparse import ( "errors" "fmt" "regexp" "strconv" "strings" "syscall" "time" "golang.org/x/sys/unix" ) //go:generate sh -c "go run mk_audit_msg_types.go && gofmt -s -w zaudit_msg_types.go" //go:generate sh -c "perl mk_audit_syscalls.pl > zaudit_syscalls.go && gofmt -s -w zaudit_syscalls.go" //go:generate perl mk_audit_arches.pl //go:generate go run mk_audit_exit_codes.go //go:generate go run github.com/elastic/go-licenser const ( typeToken = "type=" msgToken = "msg=" ) var ( // errInvalidAuditHeader means some part of the audit header was invalid. errInvalidAuditHeader = errors.New("invalid audit message header") // errParseFailure indicates a generic failure to parse. errParseFailure = errors.New("failed to parse audit message") // errInvalidResult indicates a that neither "success" and "res" keys are not found errInvalidResult = errors.New("success and res key not found") ) // AuditMessage represents a single audit message. type AuditMessage struct { RecordType AuditMessageType // Record type from netlink header. Timestamp time.Time // Timestamp parsed from payload in netlink message. Sequence uint32 // Sequence parsed from payload. RawData string // Raw message as a string. Payload interface{} // Opaque payload. This can be anything that is needed to be preserved along with the message and returned back after aggregation. offset int // offset is the index into RawData where the header ends and message begins. data map[string]string // The key value pairs parsed from the message. tags []string // The keys associated with the event (e.g. the values set in rules with -F key=exec). error error // Error that occurred while parsing. } type field struct { orig string // Original field value parse from message (including quotes). value string // Parsed and enriched value. } func newField(orig string) field { return field{orig: orig, value: orig} } type fieldMap map[string]field func (fm fieldMap) add(key string, f field) { fm[key] = f } func (fm fieldMap) setFieldValue(key, value string) { if f, ok := fm[key]; ok { fm[key] = field{orig: f.orig, value: value} } } func (fm fieldMap) find(key string) (field, error) { if f, ok := fm[key]; ok { return f, nil } return field{}, fmt.Errorf("%s key not found", key) } func (fm fieldMap) delete(key string) { delete(fm, key) } // Data returns the key-value pairs that are contained in the audit message. // This information is parsed from the raw message text the first time this // method is called, all future invocations return the stored result. A nil // map may be returned error is non-nil. A non-nil error is returned if there // was a failure parsing or enriching the data. func (m *AuditMessage) Data() (map[string]string, error) { if m.data != nil || m.error != nil { return m.data, m.error } if m.offset < 0 { m.error = errors.New("message has no data content") return nil, m.error } message, err := normalizeAuditMessage(m.RecordType, m.RawData[m.offset:]) if err != nil { m.error = err return nil, m.error } extractedKv := extractKeyValuePairs(message) if err = m.enrichData(extractedKv); err != nil { m.error = err return nil, m.error } m.data = make(map[string]string, len(extractedKv)) for k, f := range extractedKv { m.data[k] = f.value } return m.data, m.error } func (m *AuditMessage) Tags() ([]string, error) { _, err := m.Data() return m.tags, err } // ToMapStr returns a new map containing the parsed key value pairs, the // record_type, @timestamp, and sequence. The parsed key value pairs have // a lower precedence than the well-known keys and will not override them. // If an error occurred while parsing the message then an error key will be // present. func (m *AuditMessage) ToMapStr() map[string]interface{} { // Ensure event has been parsed. data, err := m.Data() out := make(map[string]interface{}, len(data)+5) for k, v := range data { out[k] = v } out["record_type"] = m.RecordType.String() out["@timestamp"] = m.Timestamp.UTC().String() out["sequence"] = strconv.FormatUint(uint64(m.Sequence), 10) out["raw_msg"] = m.RawData if len(m.tags) > 0 { out["tags"] = m.tags } if err != nil { out["error"] = err.Error() } return out } // ParseLogLine parses an audit message as logged by the Linux audit daemon. // It expects logs line that begin with the message type. For example, // "type=SYSCALL msg=audit(1488862769.030:19469538)". A non-nil error is // returned if it fails to parse the message header (type, timestamp, sequence). func ParseLogLine(line string) (*AuditMessage, error) { msgIndex := strings.Index(line, msgToken) if msgIndex == -1 { return nil, errInvalidAuditHeader } // Verify type=XXX is before msg= if msgIndex < len(typeToken)+1 { return nil, errInvalidAuditHeader } // Convert the type to a number (i.e. type=SYSCALL -> 1300). typName := line[len(typeToken) : msgIndex-1] typ, err := GetAuditMessageType(typName) if err != nil { return nil, err } msg := line[msgIndex+len(msgToken):] return Parse(typ, msg) } // Parse parses an audit message in the format it was received from the kernel. // It expects a message type, which is the message type value from the netlink // header, and a message, which is raw data from the netlink message. The // message should begin the the audit header that contains the timestamp and // sequence number -- "audit(1488862769.030:19469538)". // // A non-nil error is returned if it fails to parse the message header // (timestamp, sequence). func Parse(typ AuditMessageType, message string) (*AuditMessage, error) { message = strings.TrimSpace(message) timestamp, seq, end, err := parseAuditHeader(message) if err != nil { return nil, err } msg := &AuditMessage{ RecordType: typ, Timestamp: timestamp, Sequence: seq, offset: indexOfMessage(message[end:]), RawData: message, } return msg, nil } // parseAuditHeader parses the timestamp and sequence number from the audit // message header that has the form of "audit(1490137971.011:50406):". func parseAuditHeader(line string) (time.Time, uint32, int, error) { // Find tokens. start := strings.IndexRune(line, '(') if start == -1 { return time.Time{}, 0, 0, errInvalidAuditHeader } dot := strings.IndexRune(line[start:], '.') if dot == -1 { return time.Time{}, 0, 0, errInvalidAuditHeader } dot += start sep := strings.IndexRune(line[dot:], ':') if sep == -1 { return time.Time{}, 0, 0, errInvalidAuditHeader } sep += dot end := strings.IndexRune(line[sep:], ')') if end == -1 { return time.Time{}, 0, 0, errInvalidAuditHeader } end += sep // Parse timestamp. sec, err := strconv.ParseInt(line[start+1:dot], 10, 64) if err != nil { return time.Time{}, 0, 0, errInvalidAuditHeader } msec, err := strconv.ParseInt(line[dot+1:sep], 10, 64) if err != nil { return time.Time{}, 0, 0, errInvalidAuditHeader } tm := time.Unix(sec, msec*int64(time.Millisecond)).UTC() // Parse sequence. sequence, err := strconv.ParseUint(line[sep+1:end], 10, 32) if err != nil { return time.Time{}, 0, 0, errInvalidAuditHeader } return tm, uint32(sequence), end, nil } func indexOfMessage(msg string) int { return strings.IndexFunc(msg, func(r rune) bool { switch r { case ':', ' ': return true default: return false } }) } // Key/Value Parsing Helpers var ( // kvRegex is the regular expression used to match quoted and unquoted key // value pairs. kvRegex = regexp.MustCompile(`([a-z0-9_-]+)=((?:[^"'\s]+)|'(?:\\'|[^'])*'|"(?:\\"|[^"])*")`) // avcMessageRegex matches the beginning of SELinux AVC messages to parse // the seresult and seperms parameters. // Example: "avc: denied { read } for " selinuxAVCMessageRegex = regexp.MustCompile(`avc:\s+(\w+)\s+\{\s*(.*)\s*\}\s+for\s+`) ) // normalizeAuditMessage fixes some of the peculiarities of certain audit // messages in order to make them parsable as key-value pairs. func normalizeAuditMessage(typ AuditMessageType, msg string) (string, error) { switch typ { case AUDIT_AVC: i := selinuxAVCMessageRegex.FindStringSubmatchIndex(msg) if i == nil { // It's a different type of AVC (e.g. AppArmor) and doesn't require // normalization to make it parsable. return msg, nil } // This selinux AVC regex match should return three pairs. if len(i) != 3*2 { return "", errParseFailure } perms := strings.Fields(msg[i[4]:i[5]]) msg = fmt.Sprintf("seresult=%v seperms=%v %v", msg[i[2]:i[3]], strings.Join(perms, ","), msg[i[1]:]) case AUDIT_LOGIN: msg = strings.Replace(msg, "old ", "old_", 2) msg = strings.Replace(msg, "new ", "new_", 2) case AUDIT_CRED_DISP, AUDIT_USER_START, AUDIT_USER_END: msg = strings.Replace(msg, " (hostname=", " hostname=", 2) msg = strings.TrimRight(msg, ")'") } return msg, nil } func extractKeyValuePairs(msg string) fieldMap { data := make(fieldMap, 0) matches := kvRegex.FindAllStringSubmatch(msg, -1) for _, m := range matches { key, orig := m[1], m[2] value := trimQuotesAndSpace(orig) // Drop fields with useless values. switch value { case "", "?", "?,", "(null)": continue } if key == "msg" { for mk, mv := range extractKeyValuePairs(value) { data.add(mk, mv) } continue } data.add(key, field{orig: orig, value: value}) } return data } func trimQuotesAndSpace(v string) string { return strings.Trim(v, `'" `) } // Enrichment after KV parsing. // //nolint:errcheck // Continue enriching even if some fields do not exist. func (m *AuditMessage) enrichData(data fieldMap) error { data.normalizeUnsetID("auid") data.normalizeUnsetID("old-auid") data.normalizeUnsetID("ses") // Many message types can have subj field so check them all. data.parseSELinuxContext("subj") // Normalize success/res to result. data.result() // Convert exit codes to named POSIX exit codes. data.exit() // Normalize keys that are of the form key="key=user_command". m.auditRuleKeyNew(data) data.hexDecode("cwd") switch m.RecordType { case AUDIT_SECCOMP: if err := data.setSignalName(); err != nil { return err } fallthrough case AUDIT_SYSCALL: if err := data.arch(); err != nil { return err } if err := data.setSyscallName(); err != nil { return err } if err := data.hexDecode("exe"); err != nil { return err } case AUDIT_SOCKADDR: if err := data.saddr(); err != nil { return err } case AUDIT_PROCTITLE: if err := data.hexDecode("proctitle"); err != nil { return err } case AUDIT_USER_CMD: if err := data.hexDecode("cmd"); err != nil { return err } case AUDIT_TTY, AUDIT_USER_TTY: if err := data.hexDecode("data"); err != nil { return err } case AUDIT_EXECVE: if err := data.execveArgs(); err != nil { return err } case AUDIT_PATH: data.parseSELinuxContext("obj") data.hexDecode("name") case AUDIT_USER_LOGIN: // acct only exists in failed logins. data.hexDecode("acct") } return nil } func (fm fieldMap) arch() error { const key = "arch" field, err := fm.find(key) if err != nil { return err } arch, err := strconv.ParseInt(field.value, 16, 64) if err != nil { return fmt.Errorf("failed to parse arch: %w", err) } fm.setFieldValue(key, AuditArch(arch).String()) return nil } func (fm fieldMap) setSyscallName() error { field, err := fm.find("syscall") if err != nil { return err } syscall, err := strconv.Atoi(field.value) if err != nil { return fmt.Errorf("failed to parse syscall: %w", err) } arch, err := fm.find("arch") if err != nil { return errors.New("arch key not found so syscall cannot be translated to a name") } if name, found := AuditSyscalls[arch.value][syscall]; found { fm.setFieldValue("syscall", name) } return nil } func (fm fieldMap) setSignalName() error { field, err := fm.find("sig") if err != nil { return err } signalNum, err := strconv.Atoi(field.value) if err != nil { return fmt.Errorf("failed to parse sig: %w", err) } if signalName := unix.SignalName(syscall.Signal(signalNum)); signalName != "" { fm.setFieldValue("sig", signalName) } return nil } func (fm fieldMap) saddr() error { field, err := fm.find("saddr") if err != nil { return err } saddrData, err := parseSockaddr(field.value) if err != nil { return fmt.Errorf("failed to parse saddr: %w", err) } fm.delete("saddr") for k, v := range saddrData { fm.add(k, newField(v)) } return nil } func (fm fieldMap) normalizeUnsetID(key string) { f, err := fm.find(key) if err != nil { return } switch f.value { case "4294967295", "-1": fm.setFieldValue(key, "unset") } } func (fm fieldMap) hexDecode(key string) error { field, err := fm.find(key) if err != nil { return err } // Use the original value that may or may not contain a leading quote. decodedStrings, err := hexToStrings(field.orig) if err != nil { // Field is not in hex. Ignore. return nil } if len(decodedStrings) > 0 { fm.setFieldValue(key, strings.Join(decodedStrings, " ")) } return nil } func (fm fieldMap) execveArgs() error { argc, err := fm.find("argc") if err != nil { return err } count, err := strconv.ParseUint(argc.value, 10, 32) if err != nil { return fmt.Errorf("failed to convert argc='%v' to number: %w", argc, err) } for i := 0; i < int(count); i++ { key := "a" + strconv.Itoa(i) arg, err := fm.find(key) if err != nil { return fmt.Errorf("failed to find arg %v", key) } if ascii, err := hexToString(arg.orig); err == nil { fm.setFieldValue(key, ascii) } } return nil } // parseSELinuxContext parses a SELinux security context of the form // 'user:role:domain:level:category'. func (fm fieldMap) parseSELinuxContext(key string) error { f, err := fm.find(key) if err != nil { return err } keys := []string{"_user", "_role", "_domain", "_level", "_category"} contextParts := strings.SplitN(f.value, ":", len(keys)) if len(contextParts) == 0 { return fmt.Errorf("failed to split SELinux context field %v", key) } fm.delete(key) for i, part := range contextParts { fm.add(key+keys[i], newField(part)) } return nil } func (fm fieldMap) result() error { // Syscall messages use "success". Other messages use "res". field, err := fm.find("success") if err != nil { field, err = fm.find("res") if err != nil { return errInvalidResult } fm.delete("res") } else { fm.delete("success") } switch v := strings.ToLower(field.value); { case v == "yes", v == "1", strings.HasPrefix(v, "suc"): fm.add("result", newField("success")) default: fm.add("result", newField("fail")) } return nil } func (m *AuditMessage) auditRuleKeyNew(data fieldMap) { field, err := data.find("key") if err != nil { return } data.delete("key") // Handle hex encoded data (e.g. key=28696E7). if decodedData, err := decodeUppercaseHexString(field.orig); err == nil { keys := strings.Split(string(decodedData), string([]byte{0x01})) m.tags = keys return } parts := strings.SplitN(field.value, "=", 2) if len(parts) == 1 { // Handle key="net". m.tags = parts } else if len(parts) == 2 { // Handle key="key=net". m.tags = parts[1:] } } func (fm fieldMap) exit() error { field, err := fm.find("exit") if err != nil { return err } exitCode, err := strconv.Atoi(field.value) if err != nil { return fmt.Errorf("failed to parse exit: %w", err) } if exitCode >= 0 { return nil } name, found := AuditErrnoToName[-1*exitCode] if !found { return nil } fm.setFieldValue("exit", name) return nil }