Skip to content
/ psc Public
forked from loresuso/psc

the ps utility, with an eBPF twist and container context

License

Notifications You must be signed in to change notification settings

carverauto/psc

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

psc

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.

The Problem

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.

How psc Works

eBPF Iterators for Kernel-Level Visibility

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.

Google CEL for Flexible Queries

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

Debug Containers from the Host

With psc, you can inspect any container's processes, open files, and network connections directly from the host.

Building

Requirements

  • 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)

Install Dependencies

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

Build

# Generate vmlinux.h (required once per kernel version)
make vmlinux

# Build the binary
make build

Or manually:

bpftool btf dump file /sys/kernel/btf/vmlinux format c > bpf/vmlinux.h
go generate ./...
go build -o psc

Install

sudo make install

Usage

Basic Usage

# List all processes
psc

# List all processes as a tree
psc --tree

Filtering with CEL Expressions

Pass 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"'

Container Filtering

# 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' --tree

Socket and File Descriptor Filtering

Understanding 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")'

Available Fields

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, use uint() for comparisons: socket.srcPort == uint(80))
  • dstPort - Destination port (uint, use uint() 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)

Available Constants

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

String Functions

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

Options

  • --tree, -t - Display processes as a tree
  • --no-color - Disable colored output

Examples

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"' --tree

Find processes with connections to external services:

psc 'socket.state == established && socket.dstPort == uint(443)'

License

MIT

About

the ps utility, with an eBPF twist and container context

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 90.8%
  • C 8.5%
  • Makefile 0.7%