Skip to content

Commit

Permalink
[Auditbeat] Add user information to processes (elastic#9963)
Browse files Browse the repository at this point in the history
Adds real, effective, and saved UID and GID information to the process dataset.

(cherry picked from commit fa40a54)
  • Loading branch information
Christoph Wurm committed Jan 29, 2019
1 parent 7207729 commit b2e4866
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 78 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ https://github.com/elastic/beats/compare/v6.6.0...6.x[Check the HEAD diff]
*Auditbeat*

- Add system module. {pull}9546[9546]
- Add `user.id` (UID) and `user.name` for ECS. {pull}10195[10195]
- Add `group.id` (GID) and `group.name` for ECS. {pull}10195[10195]
- System module `process` dataset: Add user information to processes. {pull}9963[9963]

*Filebeat*

Expand Down
35 changes: 35 additions & 0 deletions auditbeat/_meta/fields.common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,38 @@
type: keyword
example: s0
description: The object's SELinux level.

- name: user
type: group
description: User information.
fields:

- name: effective
type: group
description: Effective user information.
fields:
- name: id
type: keyword
description: Effective user ID.
- name: group
type: group
description: Effective group information.
fields:
- name: id
type: keyword
description: Effective group ID.

- name: saved
type: group
description: Saved user information.
fields:
- name: id
type: keyword
description: Saved user ID.
- name: group
type: group
description: Saved group information.
fields:
- name: id
type: keyword
description: Saved group ID.
66 changes: 66 additions & 0 deletions auditbeat/docs/fields.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -3243,6 +3243,72 @@ The object's SELinux level.
--
[float]
== user fields
User information.
[float]
== effective fields
Effective user information.
*`user.effective.id`*::
+
--
type: keyword
Effective user ID.
--
[float]
== group fields
Effective group information.
*`user.effective.group.id`*::
+
--
type: keyword
Effective group ID.
--
[float]
== saved fields
Saved user information.
*`user.saved.id`*::
+
--
type: keyword
Saved user ID.
--
[float]
== group fields
Saved group information.
*`user.saved.group.id`*::
+
--
type: keyword
Saved group ID.
--
[[exported-fields-docker-processor]]
== Docker fields
Expand Down
2 changes: 1 addition & 1 deletion auditbeat/include/fields.go

Large diffs are not rendered by default.

39 changes: 29 additions & 10 deletions x-pack/auditbeat/module/system/process/_meta/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,44 @@
"name": "host.example.com"
},
"event": {
"action": "existing_process",
"action": "process_started",
"dataset": "process",
"id": "5795d53b-f7c2-463c-9c04-f316ae876d51",
"module": "system",
"kind": "state"
"kind": "event",
"module": "system"
},
"message": "Process zsh (PID: 2363) is RUNNING",
"message": "Process zsh (PID: 12936) by user elastic STARTED",
"process": {
"args": [
"/usr/bin/zsh"
"zsh"
],
"executable": "/bin/zsh",
"name": "zsh",
"pid": 2363,
"ppid": 2362,
"start": "2018-12-10T16:36:25.21Z",
"working_directory": "/home/elastic"
"pid": 12936,
"ppid": 3858,
"start": "2019-01-21T15:01:54.782288Z",
"working_directory": "/Users/elastic"
},
"service": {
"type": "system"
},
"user": {
"effective": {
"group": {
"id": "1000"
},
"id": "1000"
},
"group": {
"id": "1000",
"name": "elastic"
},
"id": "1000",
"name": "elastic",
"saved": {
"group": {
"id": "1000"
},
"id": "1000"
}
}
}
144 changes: 82 additions & 62 deletions x-pack/auditbeat/module/system/process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package process
import (
"fmt"
"os"
"runtime"
"os/user"
"strconv"
"time"

Expand All @@ -19,7 +19,6 @@ import (
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/common/cfgwarn"
"github.com/elastic/beats/libbeat/logp"
"github.com/elastic/beats/libbeat/metric/system/process"
"github.com/elastic/beats/metricbeat/mb"
"github.com/elastic/beats/x-pack/auditbeat/cache"
"github.com/elastic/go-sysinfo"
Expand Down Expand Up @@ -84,8 +83,11 @@ type MetricSet struct {

// Process represents information about a process.
type Process struct {
Info types.ProcessInfo
Error error
Info types.ProcessInfo
UserInfo *types.UserInfo
User *user.User
Group *user.Group
Error error
}

// Hash creates a hash for Process.
Expand Down Expand Up @@ -273,13 +275,42 @@ func processEvent(process *Process, eventType string, action eventAction) mb.Eve
},
}

if process.UserInfo != nil {
putIfNotEmpty(&event.RootFields, "user.id", process.UserInfo.UID)
putIfNotEmpty(&event.RootFields, "user.group.id", process.UserInfo.GID)

putIfNotEmpty(&event.RootFields, "user.effective.id", process.UserInfo.EUID)
putIfNotEmpty(&event.RootFields, "user.effective.group.id", process.UserInfo.EGID)

putIfNotEmpty(&event.RootFields, "user.saved.id", process.UserInfo.SUID)
putIfNotEmpty(&event.RootFields, "user.saved.group.id", process.UserInfo.SGID)
}

if process.User != nil {
if process.User.Username != "" {
event.RootFields.Put("user.name", process.User.Username)
} else if process.User.Name != "" {
event.RootFields.Put("user.name", process.User.Name)
}
}

if process.Group != nil {
event.RootFields.Put("user.group.name", process.Group.Name)
}

if process.Error != nil {
event.RootFields.Put("error.message", process.Error.Error())
}

return event
}

func putIfNotEmpty(mapstr *common.MapStr, key string, value string) {
if value != "" {
mapstr.Put(key, value)
}
}

func processMessage(process *Process, action eventAction) string {
if process.Error != nil {
return fmt.Sprintf("ERROR for PID %d: %v", process.Info.PID, process.Error)
Expand All @@ -295,8 +326,13 @@ func processMessage(process *Process, action eventAction) string {
actionString = "is RUNNING"
}

return fmt.Sprintf("Process %v (PID: %d) %v",
process.Info.Name, process.Info.PID, actionString)
var userString string
if process.User != nil {
userString = fmt.Sprintf(" by user %v", process.User.Username)
}

return fmt.Sprintf("Process %v (PID: %d)%v %v",
process.Info.Name, process.Info.PID, userString, actionString)
}

func convertToCacheable(processes []*Process) []cache.Cacheable {
Expand All @@ -310,82 +346,66 @@ func convertToCacheable(processes []*Process) []cache.Cacheable {
}

func (ms *MetricSet) getProcesses() ([]*Process, error) {
// TODO: Implement Processes() in go-sysinfo
// e.g. https://github.com/elastic/go-sysinfo/blob/master/providers/darwin/process_darwin_amd64.go#L41
pids, err := process.Pids()
var processes []*Process

sysinfoProcs, err := sysinfo.Processes()
if err != nil {
return nil, errors.Wrap(err, "failed to fetch the list of PIDs")
return nil, errors.Wrap(err, "failed to fetch processes")
}

var processes []*Process
for _, pid := range pids {
for _, sysinfoProc := range sysinfoProcs {
var process *Process

sysinfoProc, err := sysinfo.Process(pid)
pInfo, err := sysinfoProc.Info()
if err != nil {
if os.IsNotExist(err) {
// Skip - process probably just terminated since our call
// to Pids()
// Skip - process probably just terminated since our call to Processes().
continue
}

if runtime.GOOS == "windows" && (pid == 0 || os.IsPermission(err)) {
// On Windows, the call to Process() can fail if Auditbeat does not have
// the necessary access rights, while trying to open the System Process (PID: 0)
// will always fail.
if os.Geteuid() != 0 && os.IsPermission(err) {
// Running as non-root, permission issues when trying to access
// other user's private process information are expected.

if !ms.suppressPermissionWarnings {
ms.log.Warnf("Failed to load process information for PID %d as non-root user. "+
"Will suppress further errors of this kind. Error: %v", sysinfoProc.PID(), err)

// Only warn once at the start of Auditbeat.
ms.suppressPermissionWarnings = true
}

continue
}

// Record what we can and continue
process = &Process{
Info: types.ProcessInfo{
PID: pid,
},
Error: errors.Wrapf(err, "failed to load process with PID %d", pid),
Info: pInfo,
Error: errors.Wrapf(err, "failed to load process information for PID %d", sysinfoProc.PID()),
}
process.Info.PID = sysinfoProc.PID() // in case pInfo did not contain it
} else {
pInfo, err := sysinfoProc.Info()
if err == nil {
process = &Process{
Info: pInfo,
}
} else {
if os.IsNotExist(err) {
// Skip - process probably just terminated since our call
// to Pids()
continue
}

if os.Geteuid() != 0 {
if os.IsPermission(err) || runtime.GOOS == "darwin" {
/*
Running as non-root, permission issues when trying to access other user's private
process information are expected.
Unfortunately, for darwin os.IsPermission() does not
work because it is a custom error created using errors.New() in
getProcTaskAllInfo() in go-sysinfo/providers/darwin/process_darwin_amd64.go
TODO: Fix go-sysinfo to have better error for darwin.
*/
if !ms.suppressPermissionWarnings {
ms.log.Warnf("Failed to load process information for PID %d as non-root user. "+
"Will suppress further errors of this kind. Error: %v", pid, err)
process = &Process{
Info: pInfo,
}
}

// Only warn once at the start of Auditbeat.
ms.suppressPermissionWarnings = true
}
userInfo, err := sysinfoProc.User()
if err != nil {
if process.Error == nil {
process.Error = errors.Wrapf(err, "failed to load user for PID %d", sysinfoProc.PID())
}
} else {
process.UserInfo = &userInfo

//continue
}
}
goUser, err := user.LookupId(userInfo.UID)
if err == nil {
process.User = goUser
}

// Record what we can and continue
process = &Process{
Info: pInfo,
Error: errors.Wrapf(err, "failed to load process information for PID %d", pid),
}
process.Info.PID = pid // in case pInfo did not contain it
group, err := user.LookupGroupId(userInfo.GID)
if err == nil {
process.Group = group
}
}

Expand Down
Loading

0 comments on commit b2e4866

Please sign in to comment.