diff --git a/topbeat/beat/sigar.go b/topbeat/beat/sigar.go index 65ac0d912a8..1817cd2905c 100644 --- a/topbeat/beat/sigar.go +++ b/topbeat/beat/sigar.go @@ -32,14 +32,15 @@ type SwapStat struct { } type Process struct { - Pid int `json:"pid"` - Ppid int `json:"ppid"` - Name string `json:"name"` - State string `json:"state"` - CmdLine string `json:"cmdline"` - Mem sigar.ProcMem - Cpu sigar.ProcTime - ctime time.Time + Pid int `json:"pid"` + Ppid int `json:"ppid"` + Name string `json:"name"` + Username string `json:"username"` + State string `json:"state"` + CmdLine string `json:"cmdline"` + Mem sigar.ProcMem + Cpu sigar.ProcTime + ctime time.Time } type FileSystemStat struct { @@ -174,13 +175,14 @@ func GetProcess(pid int) (*Process, error) { cmdLine := strings.Join(args.List, " ") proc := Process{ - Pid: pid, - Ppid: state.Ppid, - Name: state.Name, - State: getProcState(byte(state.State)), - CmdLine: cmdLine, - Mem: mem, - Cpu: cpu, + Pid: pid, + Ppid: state.Ppid, + Name: state.Name, + State: getProcState(byte(state.State)), + Username: state.Username, + CmdLine: cmdLine, + Mem: mem, + Cpu: cpu, } proc.ctime = time.Now() diff --git a/topbeat/beat/sigar_test.go b/topbeat/beat/sigar_test.go index 884089b47c5..8ca5f9157d9 100644 --- a/topbeat/beat/sigar_test.go +++ b/topbeat/beat/sigar_test.go @@ -93,6 +93,7 @@ func TestGetProcess(t *testing.T) { assert.True(t, (process.Pid > 0)) assert.True(t, (process.Ppid >= 0)) assert.True(t, (len(process.Name) > 0)) + assert.True(t, (len(process.Username) > 0)) assert.NotEqual(t, "unknown", process.State) // Memory Checks diff --git a/topbeat/beat/topbeat.go b/topbeat/beat/topbeat.go index 93b0cdb6b0e..8f25551fb71 100644 --- a/topbeat/beat/topbeat.go +++ b/topbeat/beat/topbeat.go @@ -205,10 +205,11 @@ func (t *Topbeat) exportProcStats() error { newProcs[process.Pid] = process proc := common.MapStr{ - "pid": process.Pid, - "ppid": process.Ppid, - "name": process.Name, - "state": process.State, + "pid": process.Pid, + "ppid": process.Ppid, + "name": process.Name, + "state": process.State, + "username": process.Username, "mem": common.MapStr{ "size": process.Mem.Size, "rss": process.Mem.Resident, diff --git a/topbeat/tests/system/test_procs.py b/topbeat/tests/system/test_procs.py index 7dc0ad44bf0..14b132038f2 100644 --- a/topbeat/tests/system/test_procs.py +++ b/topbeat/tests/system/test_procs.py @@ -1,7 +1,8 @@ from topbeat import BaseTest +import getpass import re - +import os """ Contains tests for per process statistics. @@ -31,6 +32,7 @@ def test_procs(self): assert re.match("(?i).*topbeat.test(.exe)? -e -c", output["proc.cmdline"]) assert isinstance(output["proc.state"], basestring) assert isinstance(output["proc.cpu.start_time"], basestring) + self.check_username(output["proc.username"]) for key in [ "proc.pid", @@ -49,3 +51,14 @@ def test_procs(self): "proc.mem.rss_p", ]: assert type(output[key]) in [int, float] + + def check_username(self, observed, expected = None): + if expected == None: + expected = getpass.getuser() + + if os.name == 'nt': + parts = observed.split("\\", 2) + assert len(parts) == 2, "Expected proc.username to be of form DOMAIN\username, but was %s" % observed + observed = parts[1] + + assert expected == observed, "proc.username = %s, but expected %s" % (observed, expected) diff --git a/vendor/github.com/elastic/gosigar/.appveyor.yml b/vendor/github.com/elastic/gosigar/.appveyor.yml new file mode 100644 index 00000000000..d7733836c60 --- /dev/null +++ b/vendor/github.com/elastic/gosigar/.appveyor.yml @@ -0,0 +1,68 @@ +# Version format +version: "{build}" + +# Operating system (build VM template) +os: Windows Server 2012 R2 + +# Environment variables +environment: + GOVERSION: 1.5.3 + GOROOT: c:\go1.5.3 + GOPATH: c:\gopath + +# Custom clone folder (variables are not expanded here). +clone_folder: c:\gopath\src\github.com\elastic\gosigar + +# Cache mingw install until appveyor.yml is modified. +cache: +- C:\ProgramData\chocolatey\bin -> .appveyor.yml +- C:\ProgramData\chocolatey\lib -> .appveyor.yml +- C:\go1.5.3 -> .appveyor.yml +- C:\tools\mingw64 -> .appveyor.yml + +# Scripts that run after cloning repository +install: + - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/elastic/beats/master/libbeat/scripts/install-go.ps1')) + - set PATH=%GOROOT%\bin;%PATH% + # AppVeyor installed mingw is 32-bit only. + - cinst mingw > mingw-install.txt + - ps: Push-AppveyorArtifact mingw-install.txt + - set PATH=C:\tools\mingw64\bin;%GOROOT%\bin;%PATH% + - set PATH=%GOPATH%\bin;%PATH% + - set GO15VENDOREXPERIMENT=1 + - go version + - go env + - python --version + +# To run your custom scripts instead of automatic MSBuild +build_script: + # Compile + - appveyor AddCompilationMessage "Starting Compile" + - cd c:\gopath\src\github.com\elastic\gosigar + - go get -t + - go build + - appveyor AddCompilationMessage "Compile Success" + +# To run your custom scripts instead of automatic tests +test_script: + # Unit tests + - ps: Add-AppveyorTest "Unit Tests" -Outcome Running + - mkdir build\coverage + # TODO (#14): Currently this only runs the tests that pass. + - go test -race -cover -coverprofile=build\coverage\unit.cov -v sigar_suite_test.go sigar_windows_test.go + - ps: Update-AppveyorTest "Unit Tests" -Outcome Passed + +after_test: + - go tool cover -html=build\coverage\unit.cov -o build\coverage\unit.html + - ps: Push-AppveyorArtifact build\coverage\unit.cov + - ps: Push-AppveyorArtifact build\coverage\unit.html + # Upload coverage report. + - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" + - pip install codecov + - codecov -X gcov -f "build\coverage\unit.cov" + +# To disable deployment +deploy: off + +# Notifications should only be setup using the AppVeyor UI so that +# forks can be created without inheriting the settings. diff --git a/vendor/github.com/elastic/gosigar/.travis.yml b/vendor/github.com/elastic/gosigar/.travis.yml index 266fd7fd943..b3a18778719 100644 --- a/vendor/github.com/elastic/gosigar/.travis.yml +++ b/vendor/github.com/elastic/gosigar/.travis.yml @@ -5,7 +5,7 @@ os: - osx go: - - 1.5.1 + - 1.5.3 env: global: @@ -24,6 +24,8 @@ install: - go get -v -t -d script: + - gofmt -l . | read && echo "Code differs from gofmt's style. Run 'gofmt -w .'" 1>&2 && exit 1 || true + - go vet - go build - go test diff --git a/vendor/github.com/elastic/gosigar/sigar_darwin.go b/vendor/github.com/elastic/gosigar/sigar_darwin.go index 63167cdab29..e6173862d3d 100644 --- a/vendor/github.com/elastic/gosigar/sigar_darwin.go +++ b/vendor/github.com/elastic/gosigar/sigar_darwin.go @@ -20,6 +20,8 @@ import ( "encoding/binary" "fmt" "io" + "os/user" + "strconv" "syscall" "time" "unsafe" @@ -256,6 +258,15 @@ func (self *ProcState) Get(pid int) error { self.Nice = int(info.pbsd.pbi_nice) + // Get process username. Fallback to UID if username is not available. + uid := strconv.Itoa(int(info.pbsd.pbi_uid)) + user, err := user.LookupId(uid) + if err == nil && user.Username != "" { + self.Username = user.Username + } else { + self.Username = uid + } + return nil } diff --git a/vendor/github.com/elastic/gosigar/sigar_interface.go b/vendor/github.com/elastic/gosigar/sigar_interface.go index dd72a76b069..f4e99a56e7d 100644 --- a/vendor/github.com/elastic/gosigar/sigar_interface.go +++ b/vendor/github.com/elastic/gosigar/sigar_interface.go @@ -106,6 +106,7 @@ const ( type ProcState struct { Name string + Username string State RunState Ppid int Tty int diff --git a/vendor/github.com/elastic/gosigar/sigar_linux.go b/vendor/github.com/elastic/gosigar/sigar_linux.go index 385588127b2..bf003e59b88 100644 --- a/vendor/github.com/elastic/gosigar/sigar_linux.go +++ b/vendor/github.com/elastic/gosigar/sigar_linux.go @@ -10,6 +10,8 @@ import ( "io" "io/ioutil" "os" + "os/user" + "path/filepath" "strconv" "strings" "syscall" @@ -218,6 +220,22 @@ func (self *ProcState) Get(pid int) error { self.Processor, _ = strconv.Atoi(fields[36]) + // Read /proc/[pid]/status to get the uid, then lookup uid to get username. + status, err := getProcStatus(pid) + if err != nil { + return fmt.Errorf("failed to read process status for pid %d. %v", pid, err) + } + uids, err := getUIDs(status) + if err != nil { + return fmt.Errorf("failed to read process status for pid %d. %v", pid, err) + } + user, err := user.LookupId(uids[0]) + if err == nil { + self.Username = user.Username + } else { + self.Username = uids[0] + } + return nil } @@ -393,3 +411,35 @@ func readProcFile(pid int, name string) ([]byte, error) { return contents, err } + +// getProcStatus reads /proc/[pid]/status which contains process status +// information in human readable form. +func getProcStatus(pid int) (map[string]string, error) { + status := make(map[string]string, 42) + path := filepath.Join(Procd, strconv.Itoa(pid), "status") + err := readFile(path, func(line string) bool { + fields := strings.SplitN(line, ":", 2) + if len(fields) == 2 { + status[fields[0]] = strings.TrimSpace(fields[1]) + } + + return true + }) + return status, err +} + +// getUIDs reads the "Uid" value from status and splits it into four values -- +// real, effective, saved set, and file system UIDs. +func getUIDs(status map[string]string) ([]string, error) { + uidLine, ok := status["Uid"] + if !ok { + return nil, fmt.Errorf("Uid not found in proc status") + } + + uidStrs := strings.Fields(uidLine) + if len(uidStrs) != 4 { + return nil, fmt.Errorf("Uid line ('%s') did not contain four values", uidLine) + } + + return uidStrs, nil +} diff --git a/vendor/github.com/elastic/gosigar/sigar_test.go b/vendor/github.com/elastic/gosigar/sigar_test.go new file mode 100644 index 00000000000..f9fb52beba7 --- /dev/null +++ b/vendor/github.com/elastic/gosigar/sigar_test.go @@ -0,0 +1,29 @@ +// +build linux darwin windows + +package sigar_test + +import ( + "os" + "os/user" + "testing" + + sigar "github.com/elastic/gosigar" +) + +func TestProcStateUsername(t *testing.T) { + proc := sigar.ProcState{} + err := proc.Get(os.Getpid()) + if err != nil { + t.Fatal(err) + } + + user, err := user.Current() + if err != nil { + t.Fatal(err) + } + + if user.Username != proc.Username { + t.Fatalf("Usernames don't match, expected %s, but got %s", + user.Username, proc.Username) + } +} diff --git a/vendor/github.com/elastic/gosigar/sigar_windows.go b/vendor/github.com/elastic/gosigar/sigar_windows.go index 7e4fe45a445..e9f744b7d3c 100644 --- a/vendor/github.com/elastic/gosigar/sigar_windows.go +++ b/vendor/github.com/elastic/gosigar/sigar_windows.go @@ -278,6 +278,12 @@ func (self *ProcState) Get(pid int) error { if err != nil { return err } + + self.Username, err = GetProcCredName(pid) + if err != nil { + return err + } + return nil } @@ -306,6 +312,47 @@ func GetProcName(pid int) (string, error) { } +func GetProcCredName(pid int) (string, error) { + var err error + + handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid)) + + if err != nil { + return "", fmt.Errorf("OpenProcess fails with %v", err) + } + + defer syscall.CloseHandle(handle) + + var token syscall.Token + + // Find process token via win32 + err = syscall.OpenProcessToken(handle, syscall.TOKEN_QUERY, &token) + + if err != nil { + return "", fmt.Errorf("Error opening process token %v", err) + } + + // Find the token user + tokenUser, err := token.GetTokenUser() + if err != nil { + return "", fmt.Errorf("Error getting token user %v", err) + } + + // Close token to prevent handle leaks + err = token.Close() + if err != nil { + return "", fmt.Errorf("Error failed to closed process token") + } + + // look up domain account by sid + account, domain, _, err := tokenUser.User.Sid.LookupAccount("localhost") + if err != nil { + return "", fmt.Errorf("Error looking up sid %v", err) + } + + return fmt.Sprintf("%s\\%s", domain, account), nil +} + func GetProcStatus(pid int) (RunState, error) { handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid)) diff --git a/vendor/github.com/elastic/gosigar/sigar_windows_test.go b/vendor/github.com/elastic/gosigar/sigar_windows_test.go index 38d66f36f96..f0db19501ec 100644 --- a/vendor/github.com/elastic/gosigar/sigar_windows_test.go +++ b/vendor/github.com/elastic/gosigar/sigar_windows_test.go @@ -3,6 +3,7 @@ package sigar_test import ( "math" "os" + "os/user" "strings" "testing" @@ -32,6 +33,18 @@ var _ = Describe("SigarWindows", func() { Ω(usage.Total).Should(BeNumerically(">", 0)) }) }) + + Describe("Process", func() { + It("gets the current process user name", func() { + proc := sigar.ProcState{} + err := proc.Get(os.Getpid()) + user, usererr := user.Current() + + Ω(err).ShouldNot(HaveOccurred()) + Ω(usererr).ShouldNot(HaveOccurred()) + Ω(proc.Username).Should(Equal(user.Username)) + }) + }) }) func TestProcArgs(t *testing.T) {