Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/internal/proc/proc.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
}