160 строки
5.0 KiB
Go
160 строки
5.0 KiB
Go
package webapp
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// makeFakeProc builds a minimal /proc tree suitable for
|
|
// ScanProcApps. It writes:
|
|
//
|
|
// - a "comm" file for each pid
|
|
// - a "cmdline" file (NUL-separated)
|
|
// - a few sockets under fd/ so the listener scan can match
|
|
//
|
|
// We deliberately skip the cwd symlink (root-only) and accept that
|
|
// the readlink call returns an error; ScanProcApps must tolerate
|
|
// EACCES / ENOENT for permission-denied fds.
|
|
func makeFakeProc(t *testing.T, pids []fakeProcEntry) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
for _, e := range pids {
|
|
pdir := filepath.Join(root, strconv.Itoa(e.pid))
|
|
require.NoError(t, os.MkdirAll(pdir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(pdir, "comm"), []byte(e.comm+"\n"), 0o644))
|
|
if e.cmdline != "" {
|
|
require.NoError(t, os.WriteFile(filepath.Join(pdir, "cmdline"), []byte(e.cmdline), 0o644))
|
|
}
|
|
if len(e.sockets) > 0 {
|
|
fdDir := filepath.Join(pdir, "fd")
|
|
require.NoError(t, os.MkdirAll(fdDir, 0o755))
|
|
for i, sock := range e.sockets {
|
|
// fake inode numbers are arbitrary, but must match the
|
|
// /proc/net/tcp "inode" column for ScanProcApps to
|
|
// resolve them. Use a stable mapping.
|
|
target := "socket:[" + strconv.FormatInt(sock, 10) + "]"
|
|
require.NoError(t, os.Symlink(target, filepath.Join(fdDir, strconv.Itoa(i))))
|
|
}
|
|
}
|
|
}
|
|
// /proc/net/tcp with state 0A (LISTEN) entries pointing at the
|
|
// fake inodes. We do this last so test setup is sequential.
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "net"), 0o755))
|
|
var lines []string
|
|
lines = append(lines, " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ")
|
|
for _, e := range pids {
|
|
for _, ino := range e.sockets {
|
|
// "0100007F:1E61" -> 127.0.0.1:7777 in little-endian hex.
|
|
lines = append(lines, fakeTCPLine(ino, "0100007F:1E61"))
|
|
}
|
|
}
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(root, "net", "tcp"),
|
|
[]byte(joinLines(lines)),
|
|
0o644))
|
|
return root
|
|
}
|
|
|
|
type fakeProcEntry struct {
|
|
pid int
|
|
comm string
|
|
cmdline string
|
|
sockets []int64 // fake inode numbers
|
|
}
|
|
|
|
func fakeTCPLine(inode int64, local string) string {
|
|
// 4 hex chars for tx/rx queue (always 0), 8 hex for tr/tm->when,
|
|
// 8 hex for retrnsmt, 1 hex for uid (0), 1 hex for timeout (0),
|
|
// then inode (10 hex zero-padded). The trailing fields are zero-
|
|
// filled so the scanner skips them.
|
|
inodeHex := strconv.FormatInt(inode, 16)
|
|
for len(inodeHex) < 8 {
|
|
inodeHex = "0" + inodeHex
|
|
}
|
|
return " 0: " + local + " 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 " + inodeHex + " 0 0 0 0 0"
|
|
}
|
|
|
|
func joinLines(ls []string) string {
|
|
out := ""
|
|
for i, l := range ls {
|
|
if i > 0 {
|
|
out += "\n"
|
|
}
|
|
out += l
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestScanProcAppsEmpty(t *testing.T) {
|
|
root := t.TempDir()
|
|
apps, err := ScanProcApps(root)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, apps)
|
|
}
|
|
|
|
func TestScanProcAppsSingleProcess(t *testing.T) {
|
|
root := makeFakeProc(t, []fakeProcEntry{
|
|
{pid: 42, comm: "rsmon-worker", cmdline: "rsmon-worker --foo\x00bar", sockets: []int64{1001}},
|
|
})
|
|
apps, err := ScanProcApps(root)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 1)
|
|
assert.Equal(t, "rsmon-worker", apps[0].Name)
|
|
assert.Equal(t, 42, apps[0].PID)
|
|
assert.Equal(t, "rsmon-worker --foo bar", apps[0].Cmdline)
|
|
// ports slice should have the resolved address.
|
|
assert.Contains(t, apps[0].Ports, "127.0.0.1:7777/tcp")
|
|
}
|
|
|
|
func TestScanProcAppsSkipsKernelThreads(t *testing.T) {
|
|
// A pid directory with no comm file is treated as a vanished
|
|
// process; ScanProcApps must skip it rather than panic.
|
|
root := t.TempDir()
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "1"), 0o755))
|
|
apps, err := ScanProcApps(root)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, apps)
|
|
}
|
|
|
|
func TestInventoryStoreReplace(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store, err := OpenStore(filepath.Join(dir, "webapp.db"))
|
|
require.NoError(t, err)
|
|
defer store.Close() //nolint:errcheck
|
|
|
|
inv := NewInventory(store, nil)
|
|
_ = inv // currently no public method to inject scanned rows;
|
|
// we exercise ReplaceApps directly via the store.
|
|
now := mustParseTime(t)
|
|
rows := []App{
|
|
{Name: "rsmon-worker", Source: "process", PID: 1, Ports: "7401/tcp", LastSeen: now},
|
|
{Name: "postgres", Source: "process", PID: 2, Ports: "5432/tcp", LastSeen: now},
|
|
}
|
|
ctx := context.Background()
|
|
require.NoError(t, store.ReplaceApps(ctx, rows))
|
|
got, err := store.ListApps(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, got, 2)
|
|
assert.Equal(t, "rsmon-worker", got[0].Name)
|
|
|
|
require.NoError(t, store.ReplaceApps(ctx, []App{
|
|
{Name: "redis", Source: "process", PID: 3, Ports: "6379/tcp", LastSeen: now},
|
|
}))
|
|
got, err = store.ListApps(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, got, 1)
|
|
assert.Equal(t, "redis", got[0].Name)
|
|
}
|
|
|
|
func mustParseTime(t *testing.T) time.Time {
|
|
t.Helper()
|
|
return time.Now().UTC()
|
|
}
|