Skip to content

Commit

Permalink
auditbeat/module/file_integrity: add support for selinux and posix_ac…
Browse files Browse the repository at this point in the history
…l_access xattrs (#36310) (#36350)

(cherry picked from commit d771501)

Co-authored-by: Dan Kortschak <[email protected]>
  • Loading branch information
mergify[bot] and efd6 authored Aug 21, 2023
1 parent d80eb52 commit 31febf5
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]

*Auditbeat*

- Add support for `security.selinux` and `system.posix_acl_access` extended attributes to FIM. {issue}36265[36265] {pull}36310[36310]

*Filebeat*

Expand Down
35 changes: 35 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21301,6 +21301,41 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


--------------------------------------------------------------------------------
Dependency : github.com/pkg/xattr
Version: v0.4.9
Licence type (autodetected): BSD-2-Clause
--------------------------------------------------------------------------------

Contents of probable licence file $GOMODCACHE/github.com/pkg/[email protected]/LICENSE:

Copyright (c) 2012 Dave Cheney. All rights reserved.
Copyright (c) 2014 Kuba Podgórski. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


--------------------------------------------------------------------------------
Dependency : github.com/pmezard/go-difflib
Version: v1.0.0
Expand Down
126 changes: 104 additions & 22 deletions auditbeat/module/file_integrity/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import (
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"encoding/hex"
"fmt"
"hash"
"io"
"math"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -127,20 +129,22 @@ type Event struct {

// Metadata contains file metadata.
type Metadata struct {
Inode uint64 `json:"inode"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
SID string `json:"sid"`
Owner string `json:"owner"`
Group string `json:"group"`
Size uint64 `json:"size"`
MTime time.Time `json:"mtime"` // Last modification time.
CTime time.Time `json:"ctime"` // Last metadata change time.
Type Type `json:"type"` // File type (dir, file, symlink).
Mode os.FileMode `json:"mode"` // Permissions
SetUID bool `json:"setuid"` // setuid bit (POSIX only)
SetGID bool `json:"setgid"` // setgid bit (POSIX only)
Origin []string `json:"origin"` // External origin info for the file (MacOS only)
Inode uint64 `json:"inode"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
SID string `json:"sid"`
Owner string `json:"owner"`
Group string `json:"group"`
Size uint64 `json:"size"`
MTime time.Time `json:"mtime"` // Last modification time.
CTime time.Time `json:"ctime"` // Last metadata change time.
Type Type `json:"type"` // File type (dir, file, symlink).
Mode os.FileMode `json:"mode"` // Permissions
SetUID bool `json:"setuid"` // setuid bit (POSIX only)
SetGID bool `json:"setgid"` // setgid bit (POSIX only)
Origin []string `json:"origin"` // External origin info for the file (MacOS only)
SELinux string `json:"selinux"` // security.selinux xattr value (Linux only)
POSIXACLAccess string `json:"posix_acl_access"` // system.posix_acl_access xattr value (Linux only)
}

// NewEventFromFileInfo creates a new Event based on data from a os.FileInfo
Expand Down Expand Up @@ -319,6 +323,15 @@ func buildMetricbeatEvent(e *Event, existedBefore bool) mb.Event {
if len(info.Origin) > 0 {
file["origin"] = info.Origin
}
if info.SELinux != "" {
file["selinux"] = info.SELinux
}
if info.POSIXACLAccess != "" {
a, err := aclText([]byte(info.POSIXACLAccess))
if err == nil {
file["posix_acl_access"] = a
}
}
}

if len(e.Hashes) > 0 {
Expand All @@ -332,14 +345,14 @@ func buildMetricbeatEvent(e *Event, existedBefore bool) mb.Event {
file[k] = v
}

out.MetricSetFields.Put("event.kind", "event") //nolint:errcheck // Will not error.
out.MetricSetFields.Put("event.category", []string{"file"}) //nolint:errcheck // Will not error.
out.MetricSetFields.Put("event.kind", "event")
out.MetricSetFields.Put("event.category", []string{"file"})
if e.Action > 0 {
actions := e.Action.InOrder(existedBefore, e.Info != nil)
out.MetricSetFields.Put("event.type", actions.ECSTypes()) //nolint:errcheck // Will not error.
out.MetricSetFields.Put("event.action", actions.StringArray()) //nolint:errcheck // Will not error.
out.MetricSetFields.Put("event.type", actions.ECSTypes())
out.MetricSetFields.Put("event.action", actions.StringArray())
} else {
out.MetricSetFields.Put("event.type", None.ECSTypes()) //nolint:errcheck // Will not error.
out.MetricSetFields.Put("event.type", None.ECSTypes())
}

if n := len(e.errors); n > 0 {
Expand All @@ -348,14 +361,82 @@ func buildMetricbeatEvent(e *Event, existedBefore bool) mb.Event {
errors[idx] = err.Error()
}
if n == 1 {
out.MetricSetFields.Put("error.message", errors[0]) //nolint:errcheck // Will not error.
out.MetricSetFields.Put("error.message", errors[0])
} else {
out.MetricSetFields.Put("error.message", errors) //nolint:errcheck // Will not error.
out.MetricSetFields.Put("error.message", errors)
}
}
return out
}

func aclText(b []byte) ([]string, error) {
if (len(b)-4)%8 != 0 {
return nil, fmt.Errorf("unexpected ACL length: %d", len(b))
}
b = b[4:] // The first four bytes is the version, discard it.
a := make([]string, 0, len(b)/8)
for len(b) != 0 {
tag := binary.LittleEndian.Uint16(b)
perm := binary.LittleEndian.Uint16(b[2:])
qual := binary.LittleEndian.Uint32(b[4:])
a = append(a, fmt.Sprintf("%s:%s:%s", tags[tag], qualString(qual, tag), modeString(perm)))
b = b[8:]
}
return a, nil
}

var tags = map[uint16]string{
0x00: "undefined",
0x01: "user",
0x02: "user",
0x04: "group",
0x08: "group",
0x10: "mask",
0x20: "other",
}

func qualString(qual uint32, tag uint16) string {
if qual == math.MaxUint32 {
// 0xffffffff is undefined ID, so return zero.
return ""
}
const (
tagUser = 0x02
tagGroup = 0x08
)
id := strconv.Itoa(int(qual))
switch tag {
case tagUser:
u, err := user.LookupId(id)
if err == nil {
return u.Username
}
case tagGroup:
g, err := user.LookupGroupId(id)
if err == nil {
return g.Name
}
}
// Fallback to the numeric ID if we can't get a name
// or the tag is other than user/group.
return id
}

func modeString(perm uint16) string {
var buf [3]byte
w := 0
const rwx = "rwx"
for i, c := range rwx {
if perm&(1<<uint(len(rwx)-1-i)) != 0 {
buf[w] = byte(c)
} else {
buf[w] = '-'
}
w++
}
return string(buf[:w])
}

// diffEvents returns true if the file info differs between the old event and
// the new event. Changes to the timestamp and action are ignored. If old
// contains a superset of new's hashes then false is returned.
Expand Down Expand Up @@ -405,7 +486,8 @@ func diffEvents(old, new *Event) (Action, bool) {
if o, n := old.Info, new.Info; o != nil && n != nil {
// The owner and group names are ignored (they aren't persisted).
if o.Inode != n.Inode || o.UID != n.UID || o.GID != n.GID || o.SID != n.SID ||
o.Mode != n.Mode || o.Type != n.Type || o.SetUID != n.SetUID || o.SetGID != n.SetGID {
o.Mode != n.Mode || o.Type != n.Type || o.SetUID != n.SetUID || o.SetGID != n.SetGID ||
o.SELinux != n.SELinux || o.POSIXACLAccess != n.POSIXACLAccess {
result |= AttributesModified
}

Expand Down
50 changes: 50 additions & 0 deletions auditbeat/module/file_integrity/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ package file_integrity

import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"math"
"os"
"os/user"
"reflect"
"runtime"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -553,3 +557,49 @@ func assertHasKey(t testing.TB, m mapstr.M, key string) bool {
}
return true
}

func TestACLText(t *testing.T) {
// The xattr package returns raw bytes, but command line tools such as getfattr
// return a base64-encoded format, so use that here to make test validation
// easier.
//
// Depending on the system we are running this test on, we may or may not
// have a username associated with the user's UID in the xattr string, so
// dynamically determine the username here.
tests := []struct {
encoded string
want []string
}{
0: {
encoded: "0sAgAAAAEABgD/////AgAGAG8AAAAEAAQA/////xAABgD/////IAAEAP////8=",
want: []string{"user::rw-", "user:" + userNameOrUID("111") + ":rw-", "group::r--", "mask::rw-", "other::r--"},
},
1: { // Encoded string from https://www.bityard.org/wiki/tech/os/linux/xattrs.
encoded: "0sAgAAAAEABgD/////AgAHAHwAAAAEAAQA/////xAABwD/////IAAEAP////8=",
want: []string{"user::rw-", "user:" + userNameOrUID("124") + ":rwx", "group::r--", "mask::rwx", "other::r--"},
},
}
for i, test := range tests {
b, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(test.encoded, "0s"))
if err != nil {
t.Errorf("invalid test: unexpected base64 encoding error for test %d: %v", i, err)
continue
}
got, err := aclText(b)
if err != nil {
t.Errorf("unexpected error for test %d: %v", i, err)
continue
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("unexpected result for test %d:\ngot: %#v\nwant:%#v", i, got, test.want)
}
}
}

func userNameOrUID(uid string) string {
u, err := user.LookupId(uid)
if err != nil {
return uid
}
return u.Username
}
29 changes: 29 additions & 0 deletions auditbeat/module/file_integrity/fileinfo_posix.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import (
"os"
"os/user"
"strconv"
"strings"
"syscall"

"github.com/joeshaw/multierror"
"github.com/pkg/xattr"
)

// NewMetadata returns a new Metadata object. If an error is returned it is
Expand Down Expand Up @@ -67,6 +69,17 @@ func NewMetadata(path string, info os.FileInfo) (*Metadata, error) {
fileInfo.Owner = owner.Username
}

getExtendedAttributes(path, map[string]*string{
"security.selinux": &fileInfo.SELinux,
"system.posix_acl_access": &fileInfo.POSIXACLAccess,
})
// The selinux attr may be null terminated. It would be cheaper
// to use strings.TrimRight, but absent documentation saying
// that there is only ever a final null terminator, take the
// guaranteed correct path of terminating at the first found
// null byte.
fileInfo.SELinux, _, _ = strings.Cut(fileInfo.SELinux, "\x00")

group, err := user.LookupGroupId(strconv.Itoa(int(fileInfo.GID)))
if err != nil {
errs = append(errs, err)
Expand All @@ -78,3 +91,19 @@ func NewMetadata(path string, info os.FileInfo) (*Metadata, error) {
}
return fileInfo, errs.Err()
}

func getExtendedAttributes(path string, dst map[string]*string) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close()

for n, d := range dst {
att, err := xattr.FGet(f, n)
if err != nil {
continue
}
*d = string(att)
}
}
Loading

0 comments on commit 31febf5

Please sign in to comment.