jfut/prec
GitHub: jfut/prec
prec 是一个基于 eBPF 的 Linux 命令执行监控与取证工具,通过结构化日志和 CLI 查询帮助运维人员追踪和分析命令执行活动。
Stars: 3 | Forks: 0
# prec
[](https://github.com/jfut/dnf-plugin-anyrepo/releases)
[](https://github.com/jfut/prec/blob/main/LICENSE)
`prec` 是一个 Linux 命令执行可观测性工具,其中 `precd` 使用 eBPF 持续收集进程执行事件并以 JSON Lines 格式存储,而 `prec` 允许您从 CLI 快速搜索和检查这些记录。
实现说明:
- `internal/` 下的 Go 包属于私有实现细节,不作为受支持的公开导入 API
## 为什么使用它
- 不依赖 shell 历史记录或 `LD_PRELOAD`
- 从 kernel 事件捕获外部命令执行
- 存储包含进程、用户、父进程和 tty 上下文的结构化日志,以便快速分流排查
- 记录各种服务器上通过 Web 服务器上的 `WebShell` 以及 `OS Command Injection` 攻击执行的命令
- 记录应用程序日志中通常缺失的 `Local Privilege Escalation: LPE` 攻击的执行痕迹
- 主机端的 `precd` 也会记录同一主机上 `docker`、`runc` 及其他容器内部执行的命令
- 通过可搜索的记录(包括谁在何时何地运行了什么以及结果如何)支持 `audit` 工作流
- 通过从 `start`、`end` 和 `fail` 记录中重建执行时间线来支持 `forensic`(取证)调查
## 与 auditd 的比较
`prec` 和 `auditd` 都可以用来观察命令执行。
- `prec` 专注于命令的可见性以及供人类快速检查
- `prec` 直接输出以命令为中心的标准化记录
- `prec` 专注于命令执行记录,并包含 `exit_status` 和 `fail`
- `prec` 还可以通过从结构化记录中重建执行时间线,用于取证调查
在实践中,`prec` 专为侧重命令的监控和取证用例而设计。它可以替代仅需要命令执行可见性的部分 `auditd` 工作流,也可以与 `auditd` 一起部署。
## 范围与限制
- 目标仅限于外部命令
- 不捕获诸如 `cd`、`export` 和 `alias` 等 shell 内建命令
- 除非调用了外部命令,否则非 exec 活动不在范围内
- 诸如完整 syscall、文件访问或网络审计等广泛的主机审计需求不在范围内
## 记录字段
来自 `precd` 的原始日志记录:
- `record_type=start`
- `record_type=end`
- `record_type=fail`
- `record_type=loss`
`start` 字段:
- `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` 字段(紧凑模式):
- `timestamp`(结束时间), `event_id`, `pid`
- `duration_ns`
- `exit_status`
- `source`
- 与 start 无关的独有字段会从 `end` JSON 记录中省略
`fail` 和 `loss` 的附加字段:
- `fail`:`exec_errno`, `exec_error`
- `loss`:`lost_samples`, `lost_samples_total`
注意事项:
- `argv[0]` 在可能的情况下会被标准化为完整路径
- `event_id` 格式为 `precd` 启动时间 `YYYYMMDDhhmmss` + 序列号
- `auid` 和 `session_id` 从 `/proc//loginuid` 和 `/proc//sessionid` 读取
- `timestamp` 源自 kernel 单调时间,并在用户空间中转换为 RFC3339Nano 格式
- `exit_status` 是 shell 可见的状态码范围 `0-255`
- `start` 在 exec 成功后立即写入
- `end` 在进程退出时写入
- `fail` 记录在 `execve` 或 `execveat` 返回错误时写入
- `loss` 记录在 perf ring 报告丢弃的样本时写入
## 来源分类
仅在以下情况分配 `source=user`:
- 命令具有交互式 tty(`/dev/pts/*` 或 `/dev/tty`,或者作为 `tty_nr != 0` 的后备)。
如果子进程的 tty 数据不可用,则回退使用父进程的 tty
- 直接父进程是一个 shell
其他所有情况均为 `source=system`。
## 安装
从 [Releases](https://github.com/jfut/prec/releases) 安装发布包,或者在兼容 RHEL 的发行版上使用下面配置的 RPM 仓库。
以 root 身份运行软件包安装、手动系统安装和服务管理命令。
对于 rpm 和 deb 升级(例如使用 `dnf update` 或 `apt upgrade`),软件包的安装后脚本会自动运行 `systemctl restart precd.service`。
对于 rpm 和 deb 的卸载操作,软件包的卸载后脚本会自动运行 `systemctl stop precd.service`。
### 使用 dnf-plugin-anyrepo 的 RPM 发布版
安装 [dnf-plugin-anyrepo](https://github.com/jfut/dnf-plugin-anyrepo),导入 RPM GPG 密钥,添加此仓库,然后使用 `dnf` 安装软件包。
```
rpm --import https://raw.githubusercontent.com/jfut/prec/refs/heads/main/packaging/RPM-GPG-KEY-jfut-github
dnf-anyrepo add https://github.com/jfut/prec
dnf install prec
```
使用 `dnf` 升级已安装的软件包。
```
dnf upgrade prec
```
### 下载或本地构建的软件包
使用从 [Releases](https://github.com/jfut/prec/releases) 下载的一个发布包,或者首先构建本地软件包文件:
```
just release
```
使用您的包管理器安装一个软件包。对于下载的发布包,请将 `dist/...` 路径替换为下载的文件路径。
使用匹配的架构包名称(例如,在 ARM64 主机上使用 `arm64` 或 `aarch64`)。
```
# Debian/Ubuntu
dpkg -i dist/prec_*_amd64.deb
# RHEL/Fedora
rpm -Uvh dist/prec-*.x86_64.rpm
# Alpine
apk add --allow-untrusted dist/prec_*_x86_64.apk
# Arch Linux
pacman -U dist/prec-*-x86_64.pkg.tar.zst
```
软件包安装时会放置 `/etc/logrotate.d/prec`,其默认策略为 `daily` 和 `rotate 7305`(大约 20 年的保留期)。
根据需要调整 `rotate` 值以符合您的保留策略。
### 从源码手动安装
首先构建二进制文件:
```
just build
```
安装二进制文件:
```
install -m 0755 dist/prec /usr/bin/prec
install -m 0755 dist/precd /usr/sbin/precd
```
手动安装配置和 service 文件:
```
mkdir -p /etc/prec
chmod 750 /etc/prec
install -m 0640 packaging/precd.conf.example /etc/prec/precd.conf
mkdir -p /var/log/prec
chmod 0750 /var/log/prec
install -m 0640 packaging/systemd/precd.service /usr/lib/systemd/system/
install -m 0640 packaging/logrotate/prec /etc/logrotate.d/prec
```
安装的 `/etc/logrotate.d/prec` 默认值为 `daily` 以及 `rotate 7305`(大约 20 年的保留期)。
根据需要调整 `rotate` 值以符合您的保留策略。
### 启用并启动 precd
```
systemctl daemon-reload
systemctl enable precd.service
systemctl start precd.service
systemctl status precd.service
```
## 配置
默认配置路径:`/etc/prec/precd.conf`
参见:[packaging/precd.conf.example](packaging/precd.conf.example)
单实例行为:
- `precd` 在启动时获取 `.lock` 上的独占 `flock`
- 如果该锁已被持有,则启动立即失败,以防止重复收集
压缩模式:
- `compress = "no"` 普通 JSONL
- `compress = "gz"` gzip 压缩的 JSONL 流
- `compress = "zstd"` zstd 压缩的 JSONL 流(默认)
样本丢失动作:
- `lost_samples_action = "log"` 写入 `loss` 记录(默认)
- `lost_samples_action = "ignore"` 跳过 `loss` 记录
- `lost_samples_action = "stop"` 写入一条 `loss` 记录并停止 `precd`
过滤规则:
- `filter_default = "allow" | "deny"` 控制不匹配任何规则的事件
- `filter = ["+query", "-query", ...]` 使用有序的首次匹配评估
- 每条规则必须以 `+`(允许)或 `-`(拒绝)开头
- 查询表达式语法与 `prec --query` 相同
- 如果规则没有 `+` 或 `-` 前缀,`precd` 将无法启动
- 将拒绝旧版的 `include_*` 和 `exclude_*` 键
## CLI 行为
### 默认行为
- 不带选项的 `prec` 等同于应用 `--source user`
- 带有 `--query` 的 `prec` 也会应用所选的 `--source` 过滤器,除非指定了 `--source any`
- 默认输出字段为 `timestamp user group command`
- `prec` 通过 `event_id` 连接 `start` 和 `end`,并显示一条逻辑上的 `record_type=command`
### 用法
`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
-s, --source="user" Select source: user,system,any (default: 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
```
### 模式
- list 模式:默认
- follow 模式:`--follow` 或 `-F`
follow 模式语义:
- 当 `start` 到达时,`prec` 打印一行临时的合并数据
- `prec` 仅在输出或查询需要最终确定的字段时才处理 `end`
- 输出字段:`end_timestamp`, `duration_ns`, `duration`, `exit_status`
- 查询键:`end_timestamp`, `duration_ns`, `duration`, `exit_status`
- 当处理 `end` 时,`prec` 会为同一个 `event_id` 打印另一行包含最终值的记录,然后释放内存中的 join 状态
### 核心选项
- `-i`, `--input`:从指定的日志路径读取,而不是使用配置的 `log_path`
- `-a`, `--all-logs`:在 list 模式和 follow 初始回填中包含轮转的日志
- gzip 和 zstd 层会根据文件内容进行检测并递归解包,因此 gzip 压缩的轮转 zstd 日志也会被包含在内
- `-s`, `--source`:选择来源(`user`、`system`、`any`)
- 当 `--query` 包含自定义 `source` 逻辑时使用 `any`
- `-q`, `--query`:过滤表达式,可重复使用
- `-f`, `--fields`:输出字段选择
- `--full-time`:保留 RFC3339Nano 时间戳文本
- `-n`, `--limit`:list 模式中的最大行数,或 follow 之前的初始行数
- `--tree`:仅在文本 list 模式下显示树状视图
- `-o`, `--output`:输出格式选择(`text`、`json`、`csv`)
- `--version`:打印版本和构建信息(`version`、`commit`、`date`、`builtBy`、`treeState`)
### 模式限制
- 默认输出模式为 text
- `--tree` 不能与 `--follow` 一起使用
### 使用压缩日志进行 follow
`--follow` 适用于 `compress = "gz"` 和 `compress = "zstd"` 日志。
它跟踪日志轮转的方式类似于 `tail -F`。
`--all-logs` 在 follow 模式下的细节:
- 轮转文件仅用于初始回填(`-n`)
- 实时 follow 继续在基础日志文件路径上进行
## 查询语法
语法:
```
--query "key op value"
--query "cond1&&cond2||cond3"
```
运算符:
- 数值、时间戳和持续时间:`= != > >= < <=`
- 字符串和 argv 文本:`= != ~= !~=`
规则:
- `&&` 是 AND 运算符
- `||` 是 OR 运算符
- AND 的优先级高于 OR
- 重复的 `--query` 在顶层是 AND 关系
- `--query` 中的 `source` 值必须是 `user` 或 `system`
- 在 `prec` 的合并输出中,`record_type` 实际上是 `command`、`fail` 或 `loss`
- 查询解析器也接受 `start` 和 `end` 作为原始日志记录类型
- 字符串匹配区分大小写
- `timestamp` 和 `end_timestamp` 接受 RFC3339 或 RFC3339Nano 格式
- `duration` 接受 `YYYY-MM-DD HH:MM:SS`,表示流逝的 `年-月-日 小时:分钟:秒`
- 例如:`0001-02-03 04:05:06` 表示 1 年、2 个月、3 天、4 小时、5 分钟、6 秒
- 换算规则固定为 1 年 = 365 天,1 个月 = 30 天
- 不支持在值内部对 `&&` 和 `||` 进行转义
- `end_timestamp`、`exit_status`、`duration` 和 `duration_ns` 条件仅匹配最终确定的命令行,而不匹配临时行
支持的查询键:
- 每个事件记录中的所有 JSON 字段
- 派生键:`command`、`duration`
类型规则:
- 数值/时间戳/类型的键保留严格运算符:
- 数值:`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`, `end_timestamp`
- 持续时间:`duration`
- 数组键:`argv` 作为连接后的文本进行字符串匹配
- 其他键被视为字符串字段,并支持 `= != ~= !~=`
## 使用 `-f` 指定输出字段
支持的字段:
- `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`
选择规则:
- 没有 `-f`:默认字段
- 仅包含普通标记:显式模式,仅按该顺序输出指定的字段
- 带有 `+` 或 `-` 的标记:从默认值开始,然后添加或移除
- `all` 展开为所有字段
示例:
- `-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 -s any --query "record_type=loss" -f timestamp,record_type,lost_samples,lost_samples_total`
- `prec -s any --query "record_type=fail" -f timestamp,auid,session_id,exe,exec_errno,exec_error`
## 快速示例
使用默认字段显示最近源于用户的命令执行。
```
prec
```
带有 10 行初始数据的 follow 模式,然后保持持续输出新事件。
```
prec -F -n 10
```
带有所有可用字段的 follow 模式 CSV 输出。
```
prec -F -n 10 -f all -o csv
```
用于 pipeline 分析的格式化打印 JSON 的 follow 模式。
```
prec -F -n 10 -f all -o json | jq .
```
按 UID 范围过滤,有助于专注于排除特定服务账户后的普通用户。
```
prec -q "uid>=1000&&uid!=1999"
```
包含所有来源,并显示由 `user1` 或 `root` 运行的命令。
```
prec -s any -q "user=user1||user=root"
```
显示特定时间之后的 `curl` 执行情况,并添加持续时间和退出状态。
```
prec -q "exe~=curl" --query "timestamp>=2026-01-01T00:00:00+09:00" -f +duration,exit_status
```
查找执行时间达到 10 分钟或更长时间的长期运行命令。
```
prec -q "duration>=0000-00-00 00:10:00" -f timestamp,end_timestamp,duration,uid,gid,user,group,command
```
打印带有完整 RFC3339Nano 时间戳的选定字段。
```
prec -f timestamp,uid,gid,command --full-time
```
显示 `user` 和 `system` 事件。
```
prec -s any
```
仅显示 `system` 事件。
```
prec -s system
```
监控常见 Web 服务器账户执行的命令,这有助于检测由 OS 命令注入攻击引起的命令执行以及可能的 web shell 活动。
```
prec -s any -q "uid=48 || uid=976 || user=apache || user=nginx"
```
## 开发
```
just test
just build
just snapshot
```
## 使用 goreleaser 进行发布打包
在本地构建发布制品:
```
just release
```
生成的文件存储在 `dist/` 中:
- 包含 `prec` 和 `precd` 的 tar.gz 压缩包
- 包含 `prec` 和 `precd` 的 Linux 软件包:`deb`、`rpm`、`apk`、`archlinux`
- `checksums.txt`
## 发布
GitHub Actions 使用存储在 `RPM_SIGNING_KEY` 中的 GPG 私钥对 RPM 制品进行签名。如果该密钥有密码,请将其存储在 `NFPM_PASSPHRASE` 中。
1. 运行 `git tag -s vX.Y.Z -m vX.Y.Z`。
2. 运行 `git push origin vX.Y.Z` 并等待 Release 被创建。
3. 编辑创建的 Release。
4. 按 `Generate release notes` 按钮并编辑发行说明。
5. 按 `Update release` 按钮。
## 许可证
Apache-2.0
版权归 prec 项目的贡献者所有。
## 作者
Jun Futagawa (jfut)
标签:API集成, Docker镜像, EVTX分析, Go, Ruby工具, 可观测性, 命令执行监控, 安全取证, 审计日志, 日志审计, 运维监控