psc (ps container) is a process scanner that uses eBPF iterators and Google CEL to query system state with precision and full container context.
psc requires root privileges to load eBPF programs.
Traditional Linux tools like ps, lsof, and ss are powerful but inflexible. They output fixed formats that require extensive piping through grep, awk, and sed to extract useful information:
# Find all nginx processes owned by root
ps aux | grep nginx | grep root | grep -v grep
# With psc:
psc 'process.name == "nginx" && process.user == "root"'# Find processes with established connections on port 443
ss -tnp | grep ESTAB | grep :443 | awk '{print $6}' | cut -d'"' -f2
# With psc:
psc 'socket.state == established && socket.dstPort == uint(443)'# Find containerized processes
ps aux | xargs -I{} sh -c 'cat /proc/{}/cgroup 2>/dev/null | grep -q docker && echo {}'
# With psc:
psc 'container.runtime == docker'These tools also read from /proc, a virtual filesystem that can be manipulated by userland rootkits. A compromised library loaded via LD_PRELOAD can intercept system calls and hide processes, network connections, or files from these traditional utilities.
psc uses eBPF iterators to read process and file descriptor information directly from kernel data structures. This bypasses the /proc filesystem entirely, providing visibility that cannot be subverted by userland rootkits or LD_PRELOAD tricks. When an attacker uses LD_PRELOAD to inject a malicious shared library that intercepts calls to readdir() or open(), traditional tools see only what the rootkit allows. psc reads kernel memory directly via eBPF, seeing the true system state.
Instead of chaining grep commands, psc uses the Common Expression Language (CEL) to filter processes. CEL is a simple, safe expression language designed for evaluating boolean conditions. It allows you to answer:
- What is running: Filter by process name, command line, user, or PID
- Where it is running: Filter by container runtime, container name, image, or labels
- Why it is running: Inspect open file descriptors, network connections (ports, states, protocols), and socket types to understand what a process is doing and why it exists
With psc, you can inspect any container's processes, open files, and network connections directly from the host.
- Linux kernel 5.8 or later (eBPF iterators were introduced in this version)
- Go 1.25 or later
- Clang and LLVM
- libbpf development headers
- Linux kernel headers
- bpftool (for generating vmlinux.h)
On Debian/Ubuntu:
sudo apt-get install clang llvm libbpf-dev linux-headers-$(uname -r) linux-tools-$(uname -r)On Fedora/RHEL:
sudo dnf install clang llvm libbpf-devel kernel-devel bpftool# Generate vmlinux.h (required once per kernel version)
make vmlinux
# Build the binary
make buildOr manually:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > bpf/vmlinux.h
go generate ./...
go build -o pscsudo make install# List all processes
psc
# List all processes as a tree
psc --treePass a CEL expression as the first argument to filter processes:
# Filter by process name
psc 'process.name == "nginx"'
# Filter by user
psc 'process.user == "root"'
# Filter by command line content
psc 'process.cmdline.contains("--config")'
# Filter by PID range
psc 'process.pid > 1000 && process.pid < 2000'
# Combine conditions
psc 'process.name == "bash" || process.name == "zsh"'# Show only containerized processes
psc 'container.id != ""'
# Filter by container runtime (constants: docker, containerd, crio, podman)
psc 'container.runtime == docker'
# Filter by container name
psc 'container.name == "nginx"'
# Filter by container image
psc 'container.image.contains("nginx:latest")'
# Show as tree to see container process hierarchy
psc 'container.runtime == docker' --treeUnderstanding why a process exists often requires looking at its open file descriptors and network connections:
# Find processes with listening TCP sockets
psc 'socket.type == tcp && socket.state == listen'
# Find processes with established connections
psc 'socket.state == established'
# Find processes connected to a specific port
psc 'socket.dstPort == uint(443)'
# Find processes using Unix sockets
psc 'socket.family == unix'
# Find processes with files open in /etc
psc 'file.path.startsWith("/etc")'Process fields (process.X):
name- Process name (string)pid- Process ID (int)ppid- Parent process ID (int)tid- Thread ID (int)euid- Effective user ID (int)user- Username (string)cmdline- Full command line (string)state- Process state (uint)
Container fields (container.X):
id- Container ID (string)name- Container name (string)image- Container image (string)runtime- Container runtime (string)labels- Container labels (map)
File/Socket fields (file.X or socket.X):
path- File path (string)fd- File descriptor number (int)srcPort- Source port (uint, useuint()for comparisons:socket.srcPort == uint(80))dstPort- Destination port (uint, useuint()for comparisons:socket.dstPort == uint(443))type- Socket type (tcp, udp)state- TCP state (listen, established, close_wait, etc.)family- Address family (unix, inet, inet6)unixPath- Unix socket path (string)fdType- FD type (file_type, socket_type)
Use these without quotes in expressions:
- Runtimes:
docker,containerd,crio,podman - Socket types:
tcp,udp - Address families:
unix,inet,inet6 - TCP states:
established,listen,syn_sent,syn_recv,fin_wait1,fin_wait2,time_wait,close,close_wait,last_ack,closing - FD types:
file_type,socket_type
CEL provides string manipulation functions:
.contains("substr")- Check if string contains substring.startsWith("prefix")- Check if string starts with prefix.endsWith("suffix")- Check if string ends with suffix
--tree,-t- Display processes as a tree--no-color- Disable colored output
Find all web servers:
psc 'process.name == "nginx" || process.name == "apache2" || process.name == "httpd"'Find processes listening on privileged ports:
psc 'socket.state == listen && socket.srcPort < uint(1024)'Find Docker containers running as root:
psc 'container.runtime == docker && process.user == "root"'Debug a specific container:
psc 'container.name == "my-app"' --treeFind processes with connections to external services:
psc 'socket.state == established && socket.dstPort == uint(443)'MIT