跳转至

每日系统日志异常审计(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() 里的分支顺序):

  1. 给了 --since → 用它;
  2. 否则若状态文件里有 last_run → since = "<N> seconds ago",其中 N = max(600, now - last_run) 且 N = min(N, 7*86400)(即至少 10 分钟、最多 7 天);
  3. 否则 → "--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。