Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auditbeat/module/file_integrity: add support for selinux and posix_acl_access xattrs #36310

Merged
merged 4 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ automatic splitting at root level, if root level element is an array. {pull}3415

*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 @@ -21270,6 +21270,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{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming this is linux-only then can we guard this with a if runtime.GOOS == "linux".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system.posix_acl_access should in principle be available on other OSs.

"security.selinux": &fileInfo.SELinux,
"system.posix_acl_access": &fileInfo.POSIXACLAccess,
andrewkroh marked this conversation as resolved.
Show resolved Hide resolved
})
// 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)
andrewkroh marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading
Loading