// 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 }