111 lines
2.9 KiB
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
|
|
}
|