eBPF provenance tracking,
for when you need to know what happened.

provctl is a process/file/network provenance tracker and flight recorder for Linux. One capture engine, backed by five eBPF hooks, answers the two questions a forensics tool actually exists for.

"Where did this file come from?" β€” provctl trace "What did this process do?" β€” provctl timeline
Get started View on GitHub
Terminal recording: provctl trace reconstructing a payload's provenance chain, then the live provctl top dashboard

One capture engine, several views

The kernel side stays deliberately dumb: capture events, tag them with identity, hand them to userspace. Every interesting question is a query over one durable event log, not a separate subsystem.

πŸ›°οΈ watch

Attaches five eBPF hooks (fork/exec/exit, file opens, TCP connects) and streams live, colorized events to a persistent SQLite store.

πŸ”Ž trace

Reconstructs a file's provenance: who wrote it, who reopened it, who executed it, and what that execution did next.

🎞️ timeline

The full recorded life of one process β€” every file it opened, every connection it made, every child it spawned β€” merged and sorted by time.

🌳 ps

The recorded process tree, reconstructed from parent/child relationships captured at fork time.

πŸ“Š top

A live TUI: process tree on the left, scrolling activity feed on the right β€” a separate, unprivileged process reading the same store watch is writing to.

🧩 CO-RE eBPF

Compiled once, runs across kernel versions that expose the same fields β€” no recompiling per target, no kernel headers needed at runtime.

Architecture

Five eBPF hooks feed one ring buffer. A Go daemon decodes that stream and persists it to SQLite (WAL mode, batched writes). Every CLI command is a read-only query over that same store.

                 kernel                          userspace
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ tp/sched_process_fork        β”‚   β”‚                              β”‚
    β”‚ tp/sched_process_exec        β”‚   β”‚  engine.Engine               β”‚
    β”‚ tp/sched_process_exit        │──▢│   - loads probes (bpf2go)    β”‚
    β”‚ fentry/security_file_open    β”‚   β”‚   - attaches all hooks       β”‚
    β”‚ fentry/tcp_v4_connect        β”‚   β”‚   - reads BPF ring buffer    β”‚
    β”‚ fentry/tcp_v6_connect        β”‚   β”‚   - decodes -> model.Event   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚ BPF ring buffer                    β”‚
                    β”‚ (struct event, provctl.h)          β–Ό
                    β”‚                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚                          β”‚ store.Store (SQLite) β”‚
                    └─────────────────────────▢│  processes           β”‚
                                                β”‚  file_events         β”‚
                                                β”‚  net_events           β”‚
                                                β”‚  activity_log         β”‚
                                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                           β”‚
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β–Ό                  β–Ό               β–Ό                   β–Ό
                 provctl trace   provctl timeline  provctl ps      provctl top

Full write-up on the wiki β†’

Install

No C dependencies β€” both eBPF and SQLite bindings are pure Go, so it's a single static binary.

# clone, build, install globally (needed so `sudo provctl watch` can find it)
git clone https://github.com/thefoulowl/provctl
cd provctl
go build -trimpath -o provctl ./cmd/provctl
sudo install -Dm755 provctl /usr/local/bin/provctl
go install github.com/thefoulowl/provctl/cmd/provctl@latest
# lands in $(go env GOPATH)/bin β€” put that on PATH.
# note: sudo provctl watch needs the binary on root's PATH too,
# which ~/go/bin usually isn't. See the README for the full story.
rpmbuild -bb packaging/rpm/provctl.spec
# or via Fedora COPR, once published β€” see the wiki
cd packaging/deb
dpkg-buildpackage -us -uc -b
cd packaging/aur
makepkg -si

Requires a kernel exposing BTF at /sys/kernel/btf/vmlinux (5.8+, default on most distros since ~2021).

Usage

Start capturing as root, then ask questions from anywhere β€” the read-only commands need no privileges beyond file access.

CommandAccessWhat it does
provctl watchrootAttaches the probes, streams events, persists them to SQLite.
provctl trace <path>unprivilegedReconstructs a file's provenance chain.
provctl timeline <pid>unprivilegedEvery recorded event for one process, time-sorted.
provctl psunprivilegedThe recorded process tree.
provctl topunprivilegedLive TUI: process tree + scrolling activity feed.

Known limitations

v1 matches file provenance by exact path string (no inode/rename tracking), doesn't track dynamic library loads, and isn't container/namespace-aware. Full list and roadmap on the wiki β†’