每日系统日志异常审计(log-audit.py)¶
仓库路径:
deploy/nfos/log-audit.py| 线上路径:/opt/Cy/services/system.d/nfos/log-audit.py语言:python3 | 定时器:nfos-log-audit.timer
用途¶
security-guard.sh 每 65 秒做一次「进程/文件/Web 日志」的实时检测;本脚本是它的日度补充:把系统日志(journalctl / messages / secure / 内核)按窗口读一遍,找出那些「不会立刻爆但必须有人知道」的异常:
OOM 杀进程、内核 I/O 与文件系统错误、磁盘写满、服务崩溃循环、段错误/核心转储、认证失败激增、conntrack 表满 / SYN flooding 之类内核网络事件、audit/AppArmor 拒绝。
为什么要单独一条链路:这些信号大多不是「攻击特征」而是「系统在报警」,实时巡检为了不被刷屏会忽略它们;日度窗口 + 阈值 + 去重才既能发现又不吵。
与三库的关系:要检测什么写在特征库里(detect: log_anomaly 的条目),脚本只实现「读取窗口 + 计数 + 上报」三个动作。想加一条异常只需在 signature-library.yml 追加条目,再跑一次 /libs/apply(与 Web 攻击特征同一套路)。
何时用¶
- 不用手工跑:
nfos-log-audit.timer每小时:07:00触发一次(无参数)。 - 补窗口 / 复核一段历史:
--since '3 days ago'(journalctl 语法)。 - 只查不上报:
--dry-run(只打印命中,不上报、不推送)。 - 喂给别的系统:
--json(机器可读汇总,供监控 / 工单系统消费)。 - 调「近期口径」:
--recency-minutes N(只有最近 N 分钟内仍有命中才发告警;报告仍按整窗口统计)。
命令与参数¶
以下参数逐字取自 main() 的 argparse 定义。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--since |
字符串 | ""(空) |
窗口起点(journalctl 语法,如 '24 hours ago') |
--dry-run |
开关 | False |
只打印命中,不上报不推送 |
--json |
开关 | False |
输出 JSON 汇总 |
--recency-minutes |
int | int(os.environ.get("NFOS_LOGAUDIT_RECENCY_MIN", "15")) → 15 |
只有最近 N 分钟内仍有命中才发告警(报告仍按整窗口统计;默认 15) |
共 4 个参数。
不带参数时的默认行为¶
窗口起点按「上次运行时间」自动推算,三级回落(main() 里的分支顺序):
- 给了
--since→ 用它; - 否则若状态文件里有
last_run→since = "<N> seconds ago",其中N = max(600, now - last_run)且N = min(N, 7*86400)(即至少 10 分钟、最多 7 天); - 否则 →
"--24 hours ago"(DEFAULT_WINDOW_HOURS = 24)。
随后:读特征库 → 收集日志 → 按命中数/阈值判定 → (非 dry-run 时)上报 nfarmsd 并按近期口径调 sec-notify.py 推送 → 写人读报告 + 状态 + Prometheus textfile。
--dry-run的边界:它跳过上报与推送,但仍然扫描日志、生成人读报告(文件名另存为*-dryrun.md,以免覆盖真实报告),并且不写状态文件、不做自动恢复。
示例¶
D=/opt/Cy/services/system.d/nfos
# 1) timer 的线上形式(默认 24h/按上次运行推算的窗口)
/usr/bin/python3 "$D/log-audit.py"
# 2) 只打印命中,不上报、不推送(排障首选;报告另存 *-dryrun.md)
sudo python3 "$D/log-audit.py" --dry-run
# 3) 指定窗口 + JSON 输出
sudo python3 "$D/log-audit.py" --since '3 days ago' --dry-run --json
# 4) 调宽近期口径(30 分钟内仍有命中才告警)
sudo python3 "$D/log-audit.py" --recency-minutes 30 --dry-run
# 5) 真跑一次完整链路(上报 + 推送 + 落盘报告/状态/指标)
sudo python3 "$D/log-audit.py"
# 6) 看人读报告与状态
ls -l /opt/Cy/services/logs/backend/nfos/log-audit/
python3 -m json.tool /opt/Cy/services/system.d/nfos/state/log-audit.state.json | head -40
依赖与权限¶
| 项 | 内容 |
|---|---|
| 解释器 | python3 |
| 第三方模块 | PyYAML(必需):load_signatures() 里 import yaml 失败会记 ERROR 缺少 PyYAML,无法读取特征库 并返回空列表 → 本轮等于没审计且退出码 1 |
| 外部命令 | journalctl(--since … --no-pager -o short-iso,超时 180s)、date -d <since> …(把 journalctl 表达式还原成真实时间戳)、python3(调 sec-notify.py) |
| root | 需要。timer 单元 User=root,注释写明「需要 root:读 journalctl(内核与全部单元)与 /var/log 下的受限日志文件」 |
sudo -n |
不需要(脚本自身不调 sudo) |
| 读取 | signature-library.yml(读 detect: log_anomaly 的条目);journalctl 输出;/var/log/messages、/var/log/syslog、/var/log/secure、/var/log/kern.log、/var/log/dmesg(存在才读);状态文件 state/log-audit.state.json |
| 写入 | 本脚本没有 --log 参数:脚本自身的 JSON Lines 结构化日志打到 stdout,由单元 StandardOutput=append: 重定向到 /opt/Cy/services/logs/backend/nfos/log-audit.log;人读报告目录 /opt/Cy/services/logs/backend/nfos/log-audit/(按天一份 YYYY-MM-DD.md,dry-run 为 *-dryrun.md,可用 NFOS_LOG_AUDIT_REPORT_DIR 覆盖);Prometheus textfile /opt/Cy/services/system.d/nfos/monitoring/textfile/nfos_log_audit.prom;状态 state/log-audit.state.json |
| 网络 | 本机回环 HTTP:POST {NFARMSD_API}/alerts/report(身份 IDENT_ID="nfos-log-audit" / IDENT_ROLES="service")、/alerts/recover;推送侧调同目录 monitoring/sec-notify.py |
| 上限 | MAX_LINES_PER_SOURCE = 200000(每个来源最多读这么多行;journalctl 超出的被截断,/var/log/* 取 readlines()[-MAX:] 尾巴) |
| 超时预算 | 单元 TimeoutStartSec=600(注释:扫描一天日志 + 上报 + 推送,给足 10 分钟) |
幂等与失败语义¶
- 幂等:状态文件记
last_run/active_keys,下一轮据此推算窗口并判断「条件是否消失」。 - 告警去重靠「近期口径」:只有最近
--recency-minutes(默认 15)分钟内仍有命中才发告警;全窗口有命中但近期已无的,只在日志里记全窗口有命中但近期已无(按近期口径不告警)。报告仍按整窗口统计。 - 条件消失时自动恢复:
prev_active里不在本轮cur_keys中的键调recover_stale()。关键保护:只有「本轮确实扫到了日志」(scanned > 0)才敢判定条件消失——collect()整体失败时若照常恢复,会把「读不到日志」误判成「异常没了」,那是把真告警销掉。 - 状态文件不存在 / 不可读时的行为:按「首次运行」处理,窗口回落到默认 24h(
last_run取不到)。 - 一半状态的风险:有——报告、状态、指标是分三步写的(
write_report→ 非 dry-run 时state_put→write_metrics),任一步异常只记WARN 指标落盘失败之类,不会回滚已上报的告警。告警一旦上报,恢复要靠下一轮的recover_stale。 - 库坏了 = 本轮等于没审计:特征库里没有
detect=log_anomaly的条目时,记ERROR 特征库里没有 detect=log_anomaly 的条目,无事可做(请先 /libs/apply),仍然写一份报告(否则「报告文件不存在」既可能是没跑、也可能是库缺条目,人读不到线索),返回1。 - 退出码(源码 docstring 与实现一致):
| 退出码 | 含义 |
|---|---|
0 |
正常(有无命中都算正常) |
1 |
运行错误(缺 PyYAML / 特征库不存在 / 特征库里没有 log_anomaly 条目等) |
3 |
有 critical 命中(any(f["severity"] == "critical" for f in findings)) |
注意
3不是失败——它是「本轮发现了 critical」的信号,方便上层判断。 但在 systemd 下这意味着单元会显示failed;请对照单元是否设了SuccessExitStatus。
常见错误与排查¶
| 现象 | 代码里的对应分支 | 排查步骤 |
|---|---|---|
ERROR 缺少 PyYAML,无法读取特征库 |
load_signatures() 的 except ImportError |
python3 -c "import yaml; print(yaml.__version__)";装 python3-pyyaml |
ERROR 特征库不存在 |
if not os.path.exists(SIG_LIB) |
确认 NFOS_DIR(默认 /opt/Cy/services/system.d/nfos)下有 signature-library.yml |
ERROR 特征库里没有 detect=log_anomaly 的条目,无事可做(请先 /libs/apply) |
无 log_anomaly 条目分支(return 1) |
走 lib-ops.sh 的 {"op":"apply"} 补齐;或检查三库是否被误改。备节点实测就是这种情况 |
| 报告里写「没有读到任何来源 —— 需要先确认 journalctl / /var/log 的读取权限」 | 收集到的来源为空(sources 全 0 行时报文里的提示) |
用 User=root 跑;手工 sudo journalctl --since '1 hour ago' --no-pager -o short-iso \| wc -l 对照 |
| 有一堆命中但不发告警 | 近期口径闸门(--recency-minutes,默认 15) |
日志有 全窗口有命中但近期已无(按近期口径不告警),其中带 sig / window_count / recent_count / recency_minutes——这是刻意的降噪 |
单元显示 failed 但日志看着正常 |
return 3(有 critical 命中) |
对照单元是否设了 SuccessExitStatus;3 的语义是「发现 critical」 |
| 告警被错误地销掉 | recover_stale() 的调用条件 |
检查是否出现 scanned > 0 却实际没读到日志(例如 journalctl 超时但返回了部分输出);collect() 对每个来源失败都记 WARN 读取日志失败 |
| 日志扫得太少(一行都没扫到很多) | MAX_LINES_PER_SOURCE = 200000 截断 |
journalctl 会截断超出部分,/var/log/* 只取尾部 MAX_LINES_PER_SOURCE 行。需要更大窗口时缩小 --since |
--since 表达式不生效 / 窗口错乱 |
window_bounds() 用 date -d <since> 还原成时间戳 |
先用 date -d '3 days ago' +%Y-%m-%dT%H:%M:%S%z 验证表达式;date 失败时窗口边界会缺失 |
报告目录里出现 *-dryrun.md 混着 YYYY-MM-DD.md |
write_report(summary, dry_run=...) 的命名分支 |
这是设计行为(dry-run 不覆盖真实报告);清理时只删 *-dryrun.md |
回滚方式¶
本脚本的副作用跨越多个面,按「从软到硬」列出:
| 改动面 | 内容 | 还原方式 |
|---|---|---|
| 告警 / 工单 | 经 /alerts/report 上报(可能开单),并经 sec-notify.py 推送 |
条件消失时脚本自己 recover_stale();否则经平台接口人工处理。没有直接改库的回滚路径 |
| 状态文件 | state/log-audit.state.json(last_run / active_keys) |
删掉 → 下一轮按「首次运行」处理,窗口回落到 24h |
| 报告 | logs/backend/nfos/log-audit/YYYY-MM-DD.md |
直接删/归档(纯文本产物,无副作用) |
| 指标 | monitoring/textfile/nfos_log_audit.prom |
删掉该文件即可;Prometheus 侧体现为指标停更 |
| 脚本本体 | log-audit.py |
用版本化备份覆盖回去 |
D=/opt/Cy/services/system.d/nfos
BK=$D/state/unit-backups
LOGS=/opt/Cy/services/logs/backend/nfos
# A) 停掉审计(立刻停止产生新的告警)
sudo systemctl stop nfos-log-audit.timer
sudo systemctl disable nfos-log-audit.timer
# B) 回滚状态(让窗口重新以"首次运行"起算)
TS=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 755 "$BK"
sudo cp -a "$D/state/log-audit.state.json" "$BK/log-audit.state.json.bak-$TS" 2>/dev/null
sudo rm -f "$D/state/log-audit.state.json"
# C) 回滚脚本本体
sudo cp -a "$D/log-audit.py" "$BK/log-audit.py.bak-$TS"
sudo cp -a "$BK/log-audit.py.bak-<时间戳>" "$D/log-audit.py"
sudo chown root:root "$D/log-audit.py"
sudo chmod 644 "$D/log-audit.py"
# D) 验收(只打印,不上报)
sudo python3 "$D/log-audit.py" --dry-run --json
备份落点铁律:脚本/单元的任何备份放
/opt/Cy/services/system.d/nfos/state/unit-backups/(命名<文件名>.bak-YYYYMMDD-HHMMSS)或仓库侧/opt/Cy/nfarms/.backup/。 绝不能放在/etc/systemd/system/—— 会触发 PS-002 完整性告警。见 timers.md 第 11 节。⚠️ 只删
textfile/nfos_log_audit.prom不会「撤销」已上报的告警——它只影响 Prometheus 侧的指标。 要停告警必须停 timer 或改特征库(走lib-ops.sh+apply同步完整性基线)。
相关 CHANGELOG¶
| CHANGELOG | 章节标题 |
|---|---|
docs/CHANGELOG-2026-09-18.md |
§40. 新增每日系统日志审计 log-audit.py + systemd 单元 |
docs/CHANGELOG-2026-09-18.md |
§39. 三库新增系统日志异常特征(SL-001..SL-008 / RULE-SL-* / TPL-SYSLOG) |
docs/CHANGELOG-2026-09-18.md |
§4. lib-ops.sh 识别新的 detect 类型(log_anomaly) |
docs/CHANGELOG-2026-09-23.md |
§4. 每日系统日志审计可用性核验 |
docs/CHANGELOG-2026-09-23.md |
§12. 修两个真缺口:主节点邮件腿 + 备节点审计跑不了 |
docs/CHANGELOG-2026-09-26.md |
§13.6 最后一公里:日志审计的告警改「近期口径」(SL-004 不再复报) |
docs/CHANGELOG-2026-09-26.md |
§13.3 SL-004/005/007:三条各自的真因 |
定时器节奏、日志与报告落点见 timers.md 第 5 节;
nfos.log:键的回收见 alert-sweep.md;三库写入见 lib-ops.md。