enodia-sentinal/go-agent/internal/system/state.go

111 lines
2.9 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
// Package system captures one cached, injectable view of live host state.
package system
import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
)
// Paths makes host filesystem inputs injectable for parity and unit tests.
type Paths struct {
ProcRoot string
LDPreloadPath string
}
// Capture reads process state once from procRoot. Transient per-process read
// failures degrade to empty fields, matching Python's fail-open Process view.
func Capture(procRoot string) (model.State, error) {
return CaptureWithPaths(Paths{
ProcRoot: procRoot,
LDPreloadPath: "/etc/ld.so.preload",
})
}
// CaptureWithPaths reads one process view and the global preload file.
func CaptureWithPaths(paths Paths) (model.State, error) {
procRoot := paths.ProcRoot
if procRoot == "" {
procRoot = "/proc"
}
entries, err := os.ReadDir(procRoot)
if err != nil {
return model.State{}, err
}
processes := make([]model.Process, 0, len(entries))
for _, entry := range entries {
pid, err := strconv.Atoi(entry.Name())
if err != nil || !entry.IsDir() {
continue
}
base := filepath.Join(procRoot, entry.Name())
processes = append(processes, model.Process{
PID: pid,
Comm: strings.TrimSpace(readText(filepath.Join(base, "comm"))),
Cmdline: strings.TrimSpace(strings.ReplaceAll(readText(filepath.Join(base, "cmdline")), "\x00", " ")),
Exe: readLink(filepath.Join(base, "exe")),
Environ: parseEnviron(readText(filepath.Join(base, "environ"))),
FDTargets: readFDTargets(filepath.Join(base, "fd")),
})
}
sort.Slice(processes, func(i, j int) bool { return processes[i].PID < processes[j].PID })
return model.State{
Processes: processes,
LDPreload: strings.TrimSpace(readText(paths.LDPreloadPath)),
}, nil
}
func parseEnviron(raw string) map[string]string {
result := make(map[string]string)
for _, item := range strings.Split(raw, "\x00") {
key, value, ok := strings.Cut(item, "=")
if ok {
result[key] = value
}
}
return result
}
func readFDTargets(path string) map[string]string {
result := make(map[string]string)
entries, err := os.ReadDir(path)
if err != nil {
return result
}
sort.Slice(entries, func(i, j int) bool {
left, leftErr := strconv.Atoi(entries[i].Name())
right, rightErr := strconv.Atoi(entries[j].Name())
if leftErr == nil && rightErr == nil {
return left < right
}
return entries[i].Name() < entries[j].Name()
})
for _, entry := range entries {
if target := readLink(filepath.Join(path, entry.Name())); target != "" {
result[entry.Name()] = target
}
}
return result
}
func readText(path string) string {
raw, err := os.ReadFile(path)
if err != nil {
return ""
}
return string(raw)
}
func readLink(path string) string {
target, err := os.Readlink(path)
if err != nil {
return ""
}
return target
}