jfut/prec

GitHub: jfut/prec

prec 是一个基于 eBPF 的 Linux 命令执行监控与取证工具,通过结构化日志和 CLI 查询帮助运维人员追踪和分析命令执行活动。

Stars: 3 | Forks: 0

# prec [![Tag](https://img.shields.io/github/tag/jfut/prec.svg)](https://github.com/jfut/dnf-plugin-anyrepo/releases) [![License](https://img.shields.io/badge/license-Apache%202-blue)](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工具, 可观测性, 命令执行监控, 安全取证, 审计日志, 日志审计, 运维监控