jfut/prec
GitHub: jfut/prec
Stars: 3 | Forks: 0
# prec

[](https://github.com/jfut/prec/blob/main/LICENSE)
`prec` is a Linux command execution observability tool where `precd` uses eBPF to continuously collect process execution events as JSON Lines and `prec` lets you quickly search and inspect those records from the CLI.
## Why use it
- Does not rely on shell history or `LD_PRELOAD`
- Captures external command execution from kernel events
- Stores structured logs with process, user, parent process, and tty context for fast triage
- Logs command execution via `WebShell` on web servers and `OS Command Injection` attacks across various servers
- Logs execution traces of `Local Privilege Escalation: LPE` attacks that are often missing from application logs
- Host-side `precd` also logs commands executed inside `docker`, `runc`, and other containers on the same host
- Supports `audit` workflows with searchable records of who ran what, when, where, and with which result
- Supports `forensic` investigations by reconstructing execution timelines from `start`, `end`, and `fail` records
## Comparison with auditd
`prec` and `auditd` can both observe command execution.
- `prec` is focused on command visibility and quick inspection by humans
- `prec` outputs normalized command-centric records directly
- `prec` focuses on command execution records and includes `exit_status` and `fail`
- `prec` can also be used for forensic investigation by reconstructing execution timelines from structured records
In practice, `prec` is designed for command-focused monitoring and forensic use cases. It can replace parts of `auditd` workflows that only require command execution visibility, and can also be deployed together with `auditd`.
## Scope and limits
- Target is external commands only
- Shell builtins such as `cd`, `export`, and `alias` are not captured
- Non-exec activities are out of scope unless they invoke external commands
- Broad host auditing needs such as full syscall, file access, or network auditing are out of scope
## Recorded fields
Raw log records from `precd`:
- `record_type=start`
- `record_type=end`
- `record_type=fail`
- `record_type=loss`
`start` fields:
- `timestamp`, `event_id`
- `uid`, `gid`, `user`, `group`
- `auid`, `session_id`
- `pid`, `ppid`, `comm`, `exe`, `cwd`
- `argv`, `argc`
- `cgroup`, `tty`, `tty_nr`
- `source`
- `parent_comm`, `parent_exe`, `parent_cmdline`, `parent_tty`, `parent_tty_nr`
`end` fields (compact):
- `timestamp` (end time), `event_id`, `pid`
- `duration_ns`
- `exit_status`
- `source`
- unrelated start-only fields are omitted from `end` JSON records
`fail` and `loss` additional fields:
- `fail`: `exec_errno`, `exec_error`
- `loss`: `lost_samples`, `lost_samples_total`
Notes:
- `argv[0]` is normalized to a full path when possible
- `event_id` format is `precd` start time `YYYYMMDDhhmmss` + sequence number
- `auid` and `session_id` are read from `/proc//loginuid` and `/proc//sessionid`
- `timestamp` is derived from kernel monotonic time and converted to RFC3339Nano in userspace
- `exit_status` is the shell-visible status code range `0-255`
- `start` is written immediately after exec succeeds
- `end` is written when the process exits
- `fail` records are written when `execve` or `execveat` returns an error
- `loss` records are written when the perf ring reports dropped samples
## Source classification
`source=user` is assigned only when:
- command has an interactive tty (`/dev/pts/*` or `/dev/tty`, or `tty_nr != 0` fallback).
If child tty data is unavailable, parent tty is used as fallback
- immediate parent process is a shell
Everything else is `source=system`.
## Installation
`precd` must run as root.
### Manual build and manual install (from source)
Build:
just build
sudo install -m 0755 dist/prec /usr/bin/prec
sudo install -m 0755 dist/precd /usr/sbin/precd
Install config and service files manually:
sudo mkdir -p /etc/prec
sudo chmod 750 /etc/prec
sudo install -m 0640 packaging/precd.conf.example /etc/prec/precd.conf
sudo mkdir -p /var/log/prec
sudo chmod 0750 /var/log/prec
sudo install -m 0640 packaging/systemd/precd.service /usr/lib/systemd/system/
sudo install -m 0640 packaging/logrotate/prec /etc/logrotate.d/prec
### Package-based install
just release
Install one package from `dist/` with your package manager:
Use the matching architecture package name (for example, `arm64` or `aarch64` on ARM64 hosts).
# Debian/Ubuntu
sudo dpkg -i dist/prec_*_amd64.deb
# RHEL/Fedora
sudo rpm -Uvh dist/prec-*.x86_64.rpm
# Alpine
sudo apk add --allow-untrusted dist/prec_*_x86_64.apk
# Arch Linux
sudo pacman -U dist/prec-*-x86_64.pkg.tar.zst
For rpm and deb upgrades (for example with `dnf update` or `apt upgrade`),
package post-install scripts run `systemctl restart precd.service`
automatically.
For rpm and deb uninstall actions, package post-remove scripts run
`systemctl stop precd.service` automatically.
### Enable daemon
sudo systemctl daemon-reload
sudo systemctl enable precd.service
sudo systemctl start precd.service
sudo systemctl status precd.service
## Configuration
Default config path: `/etc/prec/precd.conf`
See: [packaging/precd.conf.example](packaging/precd.conf.example)
Compression modes:
- `compress = "no"` plain JSONL
- `compress = "gz"` gzip-compressed JSONL stream
- `compress = "zstd"` zstd-compressed JSONL stream (default)
Lost sample actions:
- `lost_samples_action = "log"` write `loss` records (default)
- `lost_samples_action = "ignore"` skip `loss` records
- `lost_samples_action = "stop"` write one `loss` record and stop `precd`
Filter rules:
- `filter_default = "allow" | "deny"` controls events that match no rule
- `filter = ["+query", "-query", ...]` uses ordered first-match evaluation
- each rule must start with `+` allow or `-` deny
- query expression syntax is identical to `prec --query`
- if a rule has no `+` or `-` prefix, `precd` fails to start
- legacy `include_*` and `exclude_*` keys are rejected
## CLI behavior
### Default behavior
- `prec` without options is equivalent to applying `--query "source=user"`
- `prec` with `--query` also applies `source=user` unless `--query` contains `source` condition
- Default output fields are `timestamp user group command`
- `prec` joins `start` and `end` by `event_id` and shows one logical `record_type=command`
### Usage
`precd -h`:
Usage: precd [flags]
Flags:
-h, --help Show context-sensitive help.
-c, --config=STRING Path to config file (default: /etc/prec/precd.conf)
--version Show version and build info
`prec -h`:
Usage: prec [flags]
Flags:
-h, --help Show context-sensitive help.
-i, --input=STRING Read log file path (default: /var/log/prec/prec.log)
-a, --all-logs Read current and rotated log files together in list mode and
follow initial output
-A, --all-sources Show both user and system source types. Without source query,
default filter is source=user
-q, --query=QUERY,... Filter expression, repeatable. Clause format: key op value, op is
= != > >= < <= ~= !~=. Use && for AND, || for OR
-f, --fields=STRING Select output fields, comma-separated.
Use + to add and - to remove. Supported:
all,timestamp,end_timestamp,event_id,user,group,
command,uid,gid,auid,session_id,pid,ppid,comm,
exe,cwd,argv,argc,cgroup,tty,tty_nr,source,
record_type,exit_status,duration_ns,duration,
exec_errno,exec_error,lost_samples,
lost_samples_total,parent_comm,parent_exe,
parent_cmdline,parent_tty,parent_tty_nr
--full-time Print full RFC3339Nano timestamp
-n, --limit=0 Max rows in list mode; initial rows before follow in --follow
mode (0 means unlimited in list mode and no initial rows in
--follow mode)
-F, --follow Follow command events
--tree Print command lineage as a tree
-o, --output=STRING Output format: text,json,csv (default: text)
--version Show version and build info
### Modes
- list mode: default
- follow mode: `--follow` or `-F`
- when `start` arrives, `prec` prints a provisional merged row
- `prec` processes `end` only when finalized fields are needed by output or query
- output fields: `end_timestamp`, `duration_ns`, `duration`, `exit_status`
- query keys: `end_timestamp`, `duration_ns`, `duration`, `exit_status`
- when `end` is processed, `prec` prints another row for the same `event_id` with finalized values and then releases in-memory join state
### Core options
- `-i`, `--input`: read from specified log path instead of config `log_path`
- `-a`, `--all-logs`: include rotated logs in list mode and follow initial backfill
- `-A`, `--all-sources`: disable implicit `source=user` default filter
- `-q`, `--query`: filter expression, repeatable
- `-f`, `--fields`: output fields selection
- `--full-time`: keep RFC3339Nano timestamp text
- `-n`, `--limit`: max rows in list mode, or initial rows before follow
- `--tree`: tree view in text list mode only
- `-o`, `--output`: output format selection (`text`, `json`, `csv`)
- `--version`: print version and build info (`version`, `commit`, `date`, `builtBy`, `treeState`)
### Mode constraints
- Default output mode is text
- `--tree` cannot be used with `--follow`
## Query syntax
Syntax:
--query "key op value"
--query "cond1&&cond2||cond3"
Operators:
- numeric, timestamp, and duration: `= != > >= < <=`
- string and argv text: `= != ~= !~=`
Rules:
- `&&` is AND operator
- `||` is OR operator
- AND has higher precedence than OR
- repeated `--query` is AND at top level
- `source` value must be `user` or `system`
- in `prec` merged output, `record_type` is effectively `command`, `fail`, or `loss`
- query parser also accepts `start` and `end` as raw log record types
- string match is case-sensitive
- `timestamp` and `end_timestamp` accept RFC3339 or RFC3339Nano
- `duration` accepts `YYYY-MM-DD HH:MM:SS` and means elapsed `year-month-day hour:minute:second`
- example: `0001-02-03 04:05:06` means 1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds
- conversion rule is fixed to 1 year = 365 days, 1 month = 30 days
- escaping `&&` and `||` inside values is not supported
- `end_timestamp`, `exit_status`, `duration`, and `duration_ns` conditions match only finalized command rows, not provisional rows
Supported query keys:
- all JSON fields in each event record
- derived keys: `command`, `duration`
Type rules:
- numeric/timestamp/duration typed keys keep strict operators:
- numeric: `uid`, `gid`, `auid`, `session_id`, `pid`, `ppid`, `argc`, `tty_nr`, `exit_status`, `duration_ns`, `exec_errno`, `lost_samples`, `lost_samples_total`, `parent_tty_nr`
- timestamp: `timestamp`, `end_timestamp`
- duration: `duration`
- array key: `argv` is string-matched as joined text
- other keys are treated as string fields and support `= != ~= !~=`
## Output fields with `-f`
Supported fields:
- `all`
- `timestamp`, `end_timestamp`, `event_id`, `user`, `group`, `command`
- `uid`, `gid`, `auid`, `session_id`, `pid`, `ppid`
- `comm`, `exe`, `cwd`, `argv`, `argc`
- `cgroup`, `tty`, `tty_nr`, `source`, `record_type`, `exit_status`, `duration_ns`, `duration`, `exec_errno`, `exec_error`, `lost_samples`, `lost_samples_total`
- `parent_comm`, `parent_exe`, `parent_cmdline`, `parent_tty`, `parent_tty_nr`
Selection rules:
- no `-f`: default fields
- plain tokens only: explicit mode, output only specified fields in that order
- token with `+` or `-`: start from defaults, then add or remove
- `all` expands to all fields
Examples:
- `-f timestamp,uid,gid,group,command`
- `-f +uid,gid,group,-timestamp,user`
- `-f all,-end_timestamp,event_id,duration_ns,duration,cgroup,tty,tty_nr,source,parent_comm,parent_exe,parent_cmdline,parent_tty,parent_tty_nr`
- `prec -A --query "record_type=loss" -f timestamp,record_type,lost_samples,lost_samples_total`
- `prec -A --query "record_type=fail" -f timestamp,auid,session_id,exe,exec_errno,exec_error`
## Quick examples
Show recent user-origin command executions with default fields.
prec
prec -F -n 10
prec -F -n 10 -f all -o csv
prec -F -n 10 -f all -o json | jq .
Filter by UID range, useful to focus on regular users except a specific service account.
prec -q "uid>=1000&&uid!=1999"
Include all sources and show commands run by `user1` or `root`.
prec -A -q "user=user1||user=root"
Show `curl` executions after a specific time and add duration and exit status.
prec -q "exe~=curl" --query "timestamp>=2026-01-01T00:00:00+09:00" -f +duration,exit_status
Find long-running commands that executed for 10 minutes or more.
prec -q "duration>=0000-00-00 00:10:00" -f timestamp,end_timestamp,duration,uid,gid,user,group,command
Print selected fields with full RFC3339Nano timestamps.
prec -f timestamp,uid,gid,command --full-time
Disable the default `source=user` filter and show both `user` and `system` events.
prec -A
Monitor commands executed by common web server accounts, which helps detect command execution caused by OS command injection attacks and possible web shell activity.
prec -A -q "uid=48 || uid=976 || user=apache || user=nginx"
## Development
just test
just build
just snapshot
## Release packaging with goreleaser
Build release artifacts locally:
just release
Generated files are stored in `dist/`:
- tar.gz archives including both `prec` and `precd`
- Linux package `prec` including both `prec` and `precd`: `deb`, `rpm`, `apk`, `archlinux`
- `checksums.txt`
## Release
1. Edit the `Draft` on the release page.
2. Update the new version `name` and `tag` on the edit page.
3. Check `Set as a pre-release` and press the `Publish release` button.
4. Wait for the build by GitHub Actions to finish.
- If the build fails due to errors such as download errors of source files, execute `Re-run failed jobs`.
5. Once all release files are automatically uploaded, check `Set as the latest release` and press the `Publish release` button.
## License
Apache-2.0
标签:EVTX分析