357 lines
8.1 KiB
Go
357 lines
8.1 KiB
Go
/*
|
|
CarterPerez-dev | 2025
|
|
proc.go
|
|
|
|
Linux /proc filesystem reader for process metadata, namespaces, and cgroup
|
|
info
|
|
|
|
Reads /proc/<pid>/status, cmdline, cgroup, and ns to build a
|
|
ProcessInfo struct. Supports cgroup-based container detection and
|
|
64-character hex container ID extraction. Optional fields (namespaces,
|
|
cmdline, cgroups) fail silently for graceful degradation on non-Linux
|
|
systems.
|
|
|
|
Key exports:
|
|
ProcessInfo - process metadata including capabilities, cgroups,
|
|
namespaces
|
|
GetProcessInfo - builds ProcessInfo from /proc/<pid>
|
|
GetContainerPID1, ListContainerProcesses - cgroup-based process
|
|
discovery
|
|
IsInContainer, ContainerID - container detection from cgroup paths
|
|
|
|
Connects to:
|
|
proc/capabilities.go - CapabilitySet populated from status hex fields
|
|
proc/security.go - SecurityProfile builds on the same /proc data
|
|
*/
|
|
|
|
package proc
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type ProcessInfo struct {
|
|
PID int
|
|
Name string
|
|
State string
|
|
PPID int
|
|
UID int
|
|
GID int
|
|
Threads int
|
|
VmSize int64
|
|
VmRSS int64
|
|
Cmdline []string
|
|
Cgroups []CgroupEntry
|
|
Namespaces map[string]uint64
|
|
Capabilities *CapabilitySet
|
|
SeccompMode string
|
|
NoNewPrivs bool
|
|
}
|
|
|
|
type CgroupEntry struct {
|
|
HierarchyID int
|
|
Controllers []string
|
|
Path string
|
|
}
|
|
|
|
func GetProcessInfo(pid int) (*ProcessInfo, error) {
|
|
procPath := fmt.Sprintf("/proc/%d", pid)
|
|
|
|
if _, err := os.Stat(procPath); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("process %d does not exist", pid)
|
|
}
|
|
|
|
info := &ProcessInfo{
|
|
PID: pid,
|
|
Namespaces: make(map[string]uint64),
|
|
}
|
|
|
|
if err := info.readStatus(procPath); err != nil {
|
|
return nil, fmt.Errorf("reading status: %w", err)
|
|
}
|
|
|
|
//nolint:staticcheck // graceful degradation - errors intentionally ignored
|
|
if err := info.readCmdline(procPath); err != nil {
|
|
}
|
|
|
|
//nolint:staticcheck // graceful degradation - errors intentionally ignored
|
|
if err := info.readCgroups(procPath); err != nil {
|
|
}
|
|
|
|
//nolint:staticcheck // graceful degradation - errors intentionally ignored
|
|
if err := info.readNamespaces(procPath); err != nil {
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func (p *ProcessInfo) readStatus(procPath string) error {
|
|
file, err := os.Open(filepath.Join(procPath, "status"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
|
|
switch key {
|
|
case "Name":
|
|
p.Name = value
|
|
case "State":
|
|
p.State = strings.Split(value, " ")[0]
|
|
case "PPid":
|
|
p.PPID, _ = strconv.Atoi(value)
|
|
case "Uid":
|
|
fields := strings.Fields(value)
|
|
if len(fields) > 0 {
|
|
p.UID, _ = strconv.Atoi(fields[0])
|
|
}
|
|
case "Gid":
|
|
fields := strings.Fields(value)
|
|
if len(fields) > 0 {
|
|
p.GID, _ = strconv.Atoi(fields[0])
|
|
}
|
|
case "Threads":
|
|
p.Threads, _ = strconv.Atoi(value)
|
|
case "VmSize":
|
|
p.VmSize = parseMemValue(value)
|
|
case "VmRSS":
|
|
p.VmRSS = parseMemValue(value)
|
|
case "CapInh":
|
|
if p.Capabilities == nil {
|
|
p.Capabilities = &CapabilitySet{}
|
|
}
|
|
p.Capabilities.Inheritable, _ = strconv.ParseUint(value, 16, 64)
|
|
case "CapPrm":
|
|
if p.Capabilities == nil {
|
|
p.Capabilities = &CapabilitySet{}
|
|
}
|
|
p.Capabilities.Permitted, _ = strconv.ParseUint(value, 16, 64)
|
|
case "CapEff":
|
|
if p.Capabilities == nil {
|
|
p.Capabilities = &CapabilitySet{}
|
|
}
|
|
p.Capabilities.Effective, _ = strconv.ParseUint(value, 16, 64)
|
|
case "CapBnd":
|
|
if p.Capabilities == nil {
|
|
p.Capabilities = &CapabilitySet{}
|
|
}
|
|
p.Capabilities.Bounding, _ = strconv.ParseUint(value, 16, 64)
|
|
case "CapAmb":
|
|
if p.Capabilities == nil {
|
|
p.Capabilities = &CapabilitySet{}
|
|
}
|
|
p.Capabilities.Ambient, _ = strconv.ParseUint(value, 16, 64)
|
|
case "Seccomp":
|
|
p.SeccompMode = parseSeccompMode(value)
|
|
case "NoNewPrivs":
|
|
p.NoNewPrivs = value == "1"
|
|
}
|
|
}
|
|
|
|
return scanner.Err()
|
|
}
|
|
|
|
func (p *ProcessInfo) readCmdline(procPath string) error {
|
|
data, err := os.ReadFile(filepath.Join(procPath, "cmdline"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(data) > 0 {
|
|
cmdline := strings.TrimRight(string(data), "\x00")
|
|
p.Cmdline = strings.Split(cmdline, "\x00")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ProcessInfo) readCgroups(procPath string) error {
|
|
file, err := os.Open(filepath.Join(procPath, "cgroup"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
parts := strings.SplitN(line, ":", 3)
|
|
if len(parts) != 3 {
|
|
continue
|
|
}
|
|
|
|
hierarchyID, _ := strconv.Atoi(parts[0])
|
|
controllers := strings.Split(parts[1], ",")
|
|
if parts[1] == "" {
|
|
controllers = nil
|
|
}
|
|
|
|
p.Cgroups = append(p.Cgroups, CgroupEntry{
|
|
HierarchyID: hierarchyID,
|
|
Controllers: controllers,
|
|
Path: parts[2],
|
|
})
|
|
}
|
|
|
|
return scanner.Err()
|
|
}
|
|
|
|
func (p *ProcessInfo) readNamespaces(procPath string) error {
|
|
nsPath := filepath.Join(procPath, "ns")
|
|
entries, err := os.ReadDir(nsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
link, err := os.Readlink(filepath.Join(nsPath, entry.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var inode uint64
|
|
_, _ = fmt.Sscanf(link, "%*[^[]:[%d]", &inode)
|
|
p.Namespaces[entry.Name()] = inode
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ProcessInfo) IsInContainer() bool {
|
|
for _, cg := range p.Cgroups {
|
|
if strings.Contains(cg.Path, "docker") ||
|
|
strings.Contains(cg.Path, "containerd") ||
|
|
strings.Contains(cg.Path, "crio") ||
|
|
strings.Contains(cg.Path, "kubepods") ||
|
|
strings.Contains(cg.Path, "lxc") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *ProcessInfo) ContainerID() string {
|
|
for _, cg := range p.Cgroups {
|
|
parts := strings.Split(cg.Path, "/")
|
|
for _, part := range parts {
|
|
if len(part) == 64 && isHex(part) {
|
|
return part
|
|
}
|
|
if strings.HasPrefix(part, "docker-") &&
|
|
strings.HasSuffix(part, ".scope") {
|
|
id := strings.TrimPrefix(part, "docker-")
|
|
id = strings.TrimSuffix(id, ".scope")
|
|
if len(id) == 64 && isHex(id) {
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func GetContainerPID1(containerID string) (int, error) {
|
|
cgroupPaths := []string{
|
|
"/sys/fs/cgroup/memory/docker/" + containerID + "/cgroup.procs",
|
|
"/sys/fs/cgroup/cpu/docker/" + containerID + "/cgroup.procs",
|
|
"/sys/fs/cgroup/docker/" + containerID + "/cgroup.procs",
|
|
"/sys/fs/cgroup/system.slice/docker-" + containerID + ".scope/cgroup.procs",
|
|
}
|
|
|
|
for _, path := range cgroupPaths {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
|
if len(lines) > 0 {
|
|
pid, err := strconv.Atoi(lines[0])
|
|
if err == nil {
|
|
return pid, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("could not find PID 1 for container %s", containerID)
|
|
}
|
|
|
|
func ListContainerProcesses(containerID string) ([]int, error) {
|
|
cgroupPaths := []string{
|
|
"/sys/fs/cgroup/memory/docker/" + containerID + "/cgroup.procs",
|
|
"/sys/fs/cgroup/docker/" + containerID + "/cgroup.procs",
|
|
"/sys/fs/cgroup/system.slice/docker-" + containerID + ".scope/cgroup.procs",
|
|
}
|
|
|
|
for _, path := range cgroupPaths {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var pids []int
|
|
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
|
for _, line := range lines {
|
|
if pid, err := strconv.Atoi(line); err == nil {
|
|
pids = append(pids, pid)
|
|
}
|
|
}
|
|
|
|
if len(pids) > 0 {
|
|
return pids, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf(
|
|
"could not list processes for container %s",
|
|
containerID,
|
|
)
|
|
}
|
|
|
|
func parseMemValue(s string) int64 {
|
|
s = strings.TrimSpace(s)
|
|
s = strings.TrimSuffix(s, " kB")
|
|
val, _ := strconv.ParseInt(s, 10, 64)
|
|
return val * 1024
|
|
}
|
|
|
|
func parseSeccompMode(s string) string {
|
|
switch s {
|
|
case "0":
|
|
return "disabled"
|
|
case "1":
|
|
return "strict"
|
|
case "2":
|
|
return "filter"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func isHex(s string) bool {
|
|
for _, c := range s {
|
|
isDigit := c >= '0' && c <= '9'
|
|
isLowerHex := c >= 'a' && c <= 'f'
|
|
isUpperHex := c >= 'A' && c <= 'F'
|
|
if !isDigit && !isLowerHex && !isUpperHex {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|