Files
pspy/internal/psscanner/psscanner_test.go

672 lines
15 KiB
Go

package psscanner
import (
//"encoding/hex"
"errors"
"fmt"
"io"
"reflect"
"syscall"
"testing"
"time"
)
const timeout = 100 * time.Millisecond
func TestRun(t *testing.T) {
tests := []struct {
name string
pids []int
events []string
}{
{
name: "nominal",
pids: []int{1, 2, 3},
events: []string{
"UID=??? PID=3 | the-command",
"UID=??? PID=2 | the-command",
"UID=??? PID=1 | the-command",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer mockPidList(tt.pids, t)()
for _, pid := range tt.pids {
defer mockPidCmdLine(pid, []byte("the-command"), nil, nil, t)()
defer mockPidUid(pid, 0, errors.New("file not found"), t)()
}
pss := NewPSScanner(false, 2048)
triggerCh := make(chan struct{})
eventCh, errCh := pss.Run(triggerCh)
// does nothing without triggering
select {
case e := <-eventCh:
t.Errorf("Received event before trigger: %s", e)
case err := <-errCh:
t.Errorf("Received error before trigger: %v", err)
case <-time.After(timeout):
// ok
}
triggerCh <- struct{}{}
// received event after the trigger
for i := 0; i < 3; i++ {
select {
case <-time.After(timeout):
t.Errorf("did not receive event in time")
case e := <-eventCh:
if e.String() != tt.events[i] {
t.Errorf("Wrong event received: got '%s' but wanted '%s'", e, tt.events[i])
}
case err := <-errCh:
t.Errorf("Received unexpected error: %v", err)
}
}
})
}
}
var (
completeStat = []byte("1314 (some proc with) odd chars)) in name) R 5560 1314 5560 34821 1314 4194304 82 0 0 0 0 0 0 0 20 0 1 0 15047943 7790592 196 18446744073709551615 94260770430976 94260770462160 140725974097504 0 0 0 0 0 0 0 0 0 17 1 0 0 0 0 0 94260772559472 94260772561088 94260783992832 140725974106274 140725974106294 140725974106294 140725974110191 0\n")
partialStat = []byte("1314 (ps) ")
invalidPpid = []byte("1314 (ps) R XYZ 1314 5560 34821 1314 4194304 82 0 0 0 0 0 0 0 20 0 1 0 15047943 7790592 196 18446744073709551615 94260770430976 94260770462160 140725974097504 0 0 0 0 0 0 0 0 0 17 1 0 0 0 0 0 94260772559472 94260772561088 94260783992832 140725974106274 140725974106294 140725974106294 140725974110191 0\n")
)
func TestProcessNewPid(t *testing.T) {
tests := []struct {
name string
enablePpid bool
truncate int
pid int
cmdLine []byte
cmdLineErrRead error
cmdLineErrOpen error
stat []byte
statErrRead error
statErrOpen error
lstatUid uint32
lstatErr error
expected PSEvent
}{
{
name: "nominal-no-ppid",
enablePpid: false,
truncate: 100,
pid: 1,
cmdLine: []byte("abc\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: 0,
PID: 1,
PPID: -1,
CMD: "abc 123",
},
},
{
name: "nominal-ppid",
enablePpid: true,
truncate: 100,
pid: 1,
cmdLine: []byte("abc\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 999,
lstatErr: nil,
expected: PSEvent{
UID: 999,
PID: 1,
PPID: 5560,
CMD: "abc 123",
},
},
{
name: "empty-cmd-ok",
enablePpid: true,
truncate: 100,
pid: 1,
cmdLine: []byte{},
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: 0,
PID: 1,
PPID: 5560,
CMD: "",
},
},
{
name: "cmd-truncate",
enablePpid: false,
truncate: 10,
pid: 1,
cmdLine: []byte("abc\x00123\x00alpha"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: 0,
PID: 1,
PPID: -1,
CMD: "abc 123 al",
},
},
{
name: "cmd-io-error",
enablePpid: true,
truncate: 100,
pid: 2,
cmdLine: nil,
cmdLineErrRead: errors.New("file-system-error"),
cmdLineErrOpen: nil,
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: 0,
PID: 2,
PPID: 5560,
CMD: "???",
},
},
{
name: "cmd-io-error2",
enablePpid: true,
truncate: 100,
pid: 2,
cmdLine: nil,
cmdLineErrRead: nil,
cmdLineErrOpen: errors.New("file-system-error"),
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: 0,
PID: 2,
PPID: 5560,
CMD: "???",
},
},
{
name: "stat-io-error",
enablePpid: true,
truncate: 100,
pid: 2,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: nil,
statErrRead: errors.New("file-system-error"),
statErrOpen: nil,
lstatUid: 321,
lstatErr: nil,
expected: PSEvent{
UID: 321,
PID: 2,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "stat-io-error2",
enablePpid: true,
truncate: 100,
pid: 2,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: nil,
statErrRead: nil,
statErrOpen: errors.New("file-system-error"),
lstatUid: 4454,
lstatErr: nil,
expected: PSEvent{
UID: 4454,
PID: 2,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "lstat-fail",
enablePpid: false,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: errors.New("file not found"),
expected: PSEvent{
UID: -1,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "lstat-with-ppid",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: completeStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: errors.New("file not found"),
expected: PSEvent{
UID: -1,
PID: 3,
PPID: 5560,
CMD: "some cmd 123",
},
},
{
name: "stat-too-short",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: partialStat,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 66,
lstatErr: nil,
expected: PSEvent{
UID: 66,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "stat-bad-ppid",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: invalidPpid,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 66,
lstatErr: nil,
expected: PSEvent{
UID: 66,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "stat-empty",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: []byte{},
statErrRead: nil,
statErrOpen: nil,
lstatUid: 88,
lstatErr: nil,
expected: PSEvent{
UID: 88,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
/*{
name: "uid-line-too-short",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: uidLineBroken,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: -1,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "uid-parse-error",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: uidNaN,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: -1,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "ppid-line-too-short",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: ppidLineShort,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: -1,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "ppid-parse-error",
enablePpid: true,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: ppidNaN,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: -1,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "no-ppid-line-too-short",
enablePpid: false,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: ppidLineShort,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: 0,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},
{
name: "no-ppid-parse-error",
enablePpid: false,
truncate: 100,
pid: 3,
cmdLine: []byte("some\x00cmd\x00123"),
cmdLineErrRead: nil,
cmdLineErrOpen: nil,
stat: ppidNaN,
statErrRead: nil,
statErrOpen: nil,
lstatUid: 0,
lstatErr: nil,
expected: PSEvent{
UID: 0,
PID: 3,
PPID: -1,
CMD: "some cmd 123",
},
},*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer mockPidCmdLine(tt.pid, tt.cmdLine, tt.cmdLineErrRead, tt.cmdLineErrOpen, t)()
defer mockPidStat(tt.pid, tt.stat, tt.statErrRead, tt.statErrOpen, t)()
defer mockPidUid(tt.pid, tt.lstatUid, tt.lstatErr, t)()
results := make(chan PSEvent, 1)
scanner := &PSScanner{
enablePpid: tt.enablePpid,
eventCh: results,
maxCmdLength: tt.truncate,
}
go func() {
scanner.processNewPid(tt.pid)
}()
select {
case <-time.After(timeout):
t.Error("Timeout waiting for event")
case event := <-results:
close(results)
if testing.Verbose() {
t.Logf("received event: %#v", event)
}
if !reflect.DeepEqual(event, tt.expected) {
t.Errorf("Event received but format is has unexpected values: got %#v but want %#v", event, tt.expected)
}
}
})
}
}
func mockPidStat(pid int, stat []byte, errRead error, errOpen error, t *testing.T) func() {
return mockFile(fmt.Sprintf("/proc/%d/stat", pid), stat, errRead, errOpen, t)
}
func mockPidCmdLine(pid int, cmdline []byte, errRead error, errOpen error, t *testing.T) func() {
return mockFile(fmt.Sprintf("/proc/%d/cmdline", pid), cmdline, errRead, errOpen, t)
}
type MockFile struct {
content []byte
err error
}
func (f *MockFile) Close() error {
return nil
}
func (f *MockFile) Read(p []byte) (int, error) {
return copy(p, f.content), f.err
}
// Hook/chain a mocked file into the "open" variable
func mockFile(name string, content []byte, errRead error, errOpen error, t *testing.T) func() {
oldopen := open
open = func(n string) (io.ReadCloser, error) {
if name == n {
if testing.Verbose() {
t.Logf("opening mocked file: %s", n)
}
return &MockFile{
content: content,
err: errRead,
}, errOpen
}
return oldopen(n)
}
return func() {
open = oldopen
}
}
func mockPidUid(pid int, uid uint32, err error, t *testing.T) func() {
return mockLStat(fmt.Sprintf("/proc/%d", pid), uid, err, t)
}
func mockLStat(name string, uid uint32, err error, t *testing.T) func() {
oldlstat := lstat
lstat = func(path string, stat *syscall.Stat_t) error {
if path == name {
if testing.Verbose() {
t.Logf("mocking lstat for %s", name)
}
stat.Uid = uid
return err
}
return oldlstat(path, stat)
}
return func() {
lstat = oldlstat
}
}
func TestNewPSScanner(t *testing.T) {
for _, tt := range []struct {
name string
ppid bool
cmdlen int
}{
{
name: "without-ppid",
ppid: false,
cmdlen: 30,
},
{
name: "with-ppid",
ppid: true,
cmdlen: 5000,
},
} {
t.Run(tt.name, func(t *testing.T) {
expected := &PSScanner{
enablePpid: tt.ppid,
eventCh: nil,
maxCmdLength: tt.cmdlen,
}
new := NewPSScanner(tt.ppid, tt.cmdlen)
if !reflect.DeepEqual(new, expected) {
t.Errorf("Unexpected scanner initialisation state: got %#v but want %#v", new, expected)
}
})
}
}
func TestPSEvent(t *testing.T) {
tests := []struct {
name string
uid int
pid int
ppid int
cmd string
expected string
}{
{
name: "nominal-with-ppid",
uid: 999,
pid: 123,
ppid: 321,
cmd: "some cmd",
expected: "UID=999 PID=123 PPID=321 | some cmd",
},
{
name: "nominal-without-ppid",
uid: 999,
pid: 123,
ppid: -1,
cmd: "some cmd",
expected: "UID=999 PID=123 | some cmd",
},
{
name: "nocmd-without-ppid",
uid: 999,
pid: 123,
ppid: -1,
cmd: "",
expected: "UID=999 PID=123 | ",
},
{
name: "nocmd-with-ppid",
uid: 999,
pid: 123,
ppid: 321,
cmd: "",
expected: "UID=999 PID=123 PPID=321 | ",
},
{
name: "nouid",
uid: -1,
pid: 123,
ppid: 321,
cmd: "some cmd",
expected: "UID=??? PID=123 PPID=321 | some cmd",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ps := PSEvent{
UID: tt.uid,
PID: tt.pid,
PPID: tt.ppid,
CMD: tt.cmd,
}
if ps.String() != tt.expected {
t.Errorf("Expecting \"%s\", got \"%s\"", tt.expected, ps.String())
}
})
}
}