Skip to content
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
5 changes: 5 additions & 0 deletions x-pack/auditbeat/auditbeat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ auditbeat.modules:

state.period: 12h

# Enabled by default. Auditbeat will read password fields in
# /etc/passwd and /etc/shadow and store a hash locally to
# detect any changes.
user.detect_password_changes: true

report_changes: true


Expand Down
5 changes: 5 additions & 0 deletions x-pack/auditbeat/auditbeat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ auditbeat.modules:

state.period: 12h

# Enabled by default. Auditbeat will read password fields in
# /etc/passwd and /etc/shadow and store a hash locally to
# detect any changes.
user.detect_password_changes: true

report_changes: true


Expand Down
7 changes: 7 additions & 0 deletions x-pack/auditbeat/module/system/_meta/config.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@

state.period: 12h

{{ if eq .GOOS "linux" -}}
# Enabled by default. Auditbeat will read password fields in
# /etc/passwd and /etc/shadow and store a hash locally to
# detect any changes.
user.detect_password_changes: true
{{- end }}

report_changes: true
{{- end }}
{{ if .Reference }}
Expand Down
23 changes: 11 additions & 12 deletions x-pack/auditbeat/module/system/user/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,23 @@ import (
"time"
)

// Config defines the metricset's configuration options.
type Config struct {
StatePeriod time.Duration `config:"state.period"`
UserStatePeriod time.Duration `config:"user.state.period"`
// config defines the metricset's configuration options.
type config struct {
StatePeriod time.Duration `config:"state.period"`
UserStatePeriod time.Duration `config:"user.state.period"`
DetectPasswordChanges bool `config:"user.detect_password_changes"`
}

// Validate validates the host metricset config.
func (c *Config) Validate() error {
return nil
}

func (c *Config) effectiveStatePeriod() time.Duration {
func (c *config) effectiveStatePeriod() time.Duration {
if c.UserStatePeriod != 0 {
return c.UserStatePeriod
}
return c.StatePeriod
}

var defaultConfig = Config{
StatePeriod: 12 * time.Hour,
func defaultConfig() config {
return config{
StatePeriod: 12 * time.Hour,
DetectPasswordChanges: false,
}
}
148 changes: 92 additions & 56 deletions x-pack/auditbeat/module/system/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package user

import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"io"
Expand All @@ -32,6 +33,10 @@ const (
moduleName = "system"
metricsetName = "user"

passwdFile = "/etc/passwd"
groupFile = "/etc/group"
shadowFile = "/etc/shadow"

bucketName = "user.v1"
bucketKeyUsers = "users"
bucketKeyStateTimestamp = "state_timestamp"
Expand All @@ -46,10 +51,35 @@ const (
eventActionPasswordChanged = "password_changed"
)

type passwordType uint8

const (
detectionDisabled passwordType = iota
shadowPassword
passwordDisabled
noPassword
cryptPassword
)

func (t passwordType) String() string {
switch t {
case shadowPassword:
return "shadow_password"
case passwordDisabled:
return "password_disabled"
case noPassword:
return "no_password"
case cryptPassword:
return "crypt_password"
default:
return ""
}
}

// User represents a user. Fields according to getpwent(3).
type User struct {
Name string
PasswordType string
PasswordType passwordType
PasswordChanged time.Time
PasswordHashHash []byte
UID uint32
Expand All @@ -72,7 +102,7 @@ func (user User) Hash() uint64 {
h := xxhash.New64()
// Use everything except userInfo
h.WriteString(user.Name)
h.WriteString(user.PasswordType)
binary.Write(h, binary.BigEndian, uint8(user.PasswordType))
h.WriteString(user.PasswordChanged.String())
h.Write(user.PasswordHashHash)
h.WriteString(strconv.Itoa(int(user.UID)))
Expand All @@ -90,10 +120,7 @@ func (user User) Hash() uint64 {

func (user User) toMapStr() common.MapStr {
evt := common.MapStr{
"name": user.Name,
"password": common.MapStr{
"type": user.PasswordType,
},
"name": user.Name,
"uid": user.UID,
"gid": user.GID,
"dir": user.Dir,
Expand All @@ -104,6 +131,10 @@ func (user User) toMapStr() common.MapStr {
evt.Put("user_information", user.UserInfo)
}

if user.PasswordType != detectionDisabled {
evt.Put("password.type", user.PasswordType.String())
}

if !user.PasswordChanged.IsZero() {
evt.Put("password.last_changed", user.PasswordChanged)
}
Expand Down Expand Up @@ -131,12 +162,13 @@ func init() {
// MetricSet collects data about a system's users.
type MetricSet struct {
mb.BaseMetricSet
config Config
log *logp.Logger
cache *cache.Cache
bucket datastore.Bucket
lastState time.Time
lastChange time.Time
config config
log *logp.Logger
cache *cache.Cache
bucket datastore.Bucket
lastState time.Time
userFiles []string
lastRead time.Time
}

// New constructs a new MetricSet.
Expand All @@ -146,7 +178,7 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
return nil, fmt.Errorf("the %v/%v dataset is only supported on Linux", moduleName, metricsetName)
}

config := defaultConfig
config := defaultConfig()
if err := base.Module().UnpackConfig(&config); err != nil {
return nil, errors.Wrapf(err, "failed to unpack the %v/%v config", moduleName, metricsetName)
}
Expand All @@ -164,6 +196,12 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
bucket: bucket,
}

if ms.config.DetectPasswordChanges {
ms.userFiles = []string{passwdFile, groupFile, shadowFile}
} else {
ms.userFiles = []string{passwdFile, groupFile}
}

// Load from disk: Time when state was last sent
err = bucket.Load(bucketKeyStateTimestamp, func(blob []byte) error {
if len(blob) > 0 {
Expand Down Expand Up @@ -224,7 +262,7 @@ func (ms *MetricSet) Fetch(report mb.ReporterV2) {
func (ms *MetricSet) reportState(report mb.ReporterV2) error {
ms.lastState = time.Now()

users, err := GetUsers()
users, err := GetUsers(ms.config.DetectPasswordChanges)
if err != nil {
return errors.Wrap(err, "failed to get users")
}
Expand Down Expand Up @@ -261,16 +299,21 @@ func (ms *MetricSet) reportState(report mb.ReporterV2) error {
// reportChanges detects and reports any changes to users on this system since the last call.
func (ms *MetricSet) reportChanges(report mb.ReporterV2) error {
currentTime := time.Now()
changed, err := haveFilesChanged(ms.lastChange)
if err != nil {
return err
}
if !changed {
return nil

// If this is not the first call to Fetch/reportChanges,
// check if files have changed since the last time before going any further.
if !ms.lastRead.IsZero() {
changed, err := ms.haveFilesChanged()
if err != nil {
return err
}
if !changed {
return nil
}
}
ms.lastChange = currentTime
ms.lastRead = currentTime

users, err := GetUsers()
users, err := GetUsers(ms.config.DetectPasswordChanges)
if err != nil {
return errors.Wrap(err, "failed to get users")
}
Expand All @@ -287,25 +330,31 @@ func (ms *MetricSet) reportChanges(report mb.ReporterV2) error {

for _, userFromCache := range newInCache {
newUser := userFromCache.(*User)
matchingMissingUser, found := missingUserMap[newUser.UID]
oldUser, found := missingUserMap[newUser.UID]

if found {
// Report password change separately
if newUser.PasswordChanged.Before(matchingMissingUser.PasswordChanged) ||
!bytes.Equal(newUser.PasswordHashHash, matchingMissingUser.PasswordHashHash) ||
newUser.PasswordType != matchingMissingUser.PasswordType {
report.Event(userEvent(newUser, eventTypeEvent, eventActionPasswordChanged))
if ms.config.DetectPasswordChanges && newUser.PasswordType != detectionDisabled &&
oldUser.PasswordType != detectionDisabled {

passwordChanged := newUser.PasswordChanged.Before(oldUser.PasswordChanged) ||
!bytes.Equal(newUser.PasswordHashHash, oldUser.PasswordHashHash) ||
newUser.PasswordType != oldUser.PasswordType

if passwordChanged {
report.Event(userEvent(newUser, eventTypeEvent, eventActionPasswordChanged))
}
}

// Hack to check if only the password changed
matchingMissingUser.PasswordChanged = newUser.PasswordChanged
matchingMissingUser.PasswordHashHash = newUser.PasswordHashHash
matchingMissingUser.PasswordType = newUser.PasswordType
if newUser.Hash() != matchingMissingUser.Hash() {
oldUser.PasswordChanged = newUser.PasswordChanged
oldUser.PasswordHashHash = newUser.PasswordHashHash
oldUser.PasswordType = newUser.PasswordType
if newUser.Hash() != oldUser.Hash() {
report.Event(userEvent(newUser, eventTypeEvent, eventActionUserChanged))
}

delete(missingUserMap, matchingMissingUser.UID)
delete(missingUserMap, oldUser.UID)
} else {
report.Event(userEvent(newUser, eventTypeEvent, eventActionUserAdded))
}
Expand Down Expand Up @@ -409,33 +458,20 @@ func (ms *MetricSet) saveUsersToDisk(users []*User) error {
return nil
}

// haveFilesChanged checks if any of the relevant files (/etc/passwd, /etc/shadow, /etc/group)
// have changed.
func haveFilesChanged(since time.Time) (bool, error) {
const passwdFile = "/etc/passwd"
const shadowFile = "/etc/shadow"
const groupFile = "/etc/group"

// haveFilesChanged checks if the ctime of any of the user files has changed.
func (ms *MetricSet) haveFilesChanged() (bool, error) {
var stats syscall.Stat_t
if err := syscall.Stat(passwdFile, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", passwdFile)
}
if since.Before(time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)) {
return true, nil
}
for _, path := range ms.userFiles {
if err := syscall.Stat(path, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", path)
}

if err := syscall.Stat(shadowFile, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", shadowFile)
}
if since.Before(time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)) {
return true, nil
}
ctime := time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)
if ms.lastRead.Before(ctime) {
ms.log.Debugf("File changed: %v (lastRead=%v, ctime=%v)", path, ms.lastRead, ctime)

if err := syscall.Stat(groupFile, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", groupFile)
}
if since.Before(time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)) {
return true, nil
return true, nil
}
}

return false, nil
Expand Down
Loading