跳转至

双档告警汇总与四张通报(digest.py)

仓库路径:deploy/nfos/monitoring/digest.py | 线上路径:/opt/Cy/services/system.d/nfos/monitoring/digest.py 语言:python3 | 定时器:nfos-digest-hourly.timer(--window 1h)、nfos-digest-12h.timer(--window 12h)、nfos-report-attack.timer(--report attack)、nfos-report-tickets.timer(--report tickets)、nfos-report-daily-alerts.timer(--report daily-alerts)、nfos-report-libs.timer(--report libs)


用途

把窗口内的告警压成「按 特征号 × 来源 IP 分组」的表格,一条消息说清「这个窗口发生了什么」,而不是逐条刷屏。全平台有 6 条 timer 共用这一个脚本(2 条汇总档 + 4 张通报),差别只在 --window / --report:

档位 覆盖级别 级别语义(与 rule-library.yml / security-guard.sh 同一口径)
--window 1h critical 攻击成功 / 抵御失败
--window 12h high + warning 攻击失败 / 抵御成功 / 抵御结果未知(info 只写库不推送)

四张通报(--report)复用同一套分组 / 渲染 / 脱敏 / 幂等 / 发送逻辑,只换窗口口径与正文:

--report 线上节奏(来自 timer) 窗口口径
daily-alerts 每日 10:00 前一日 00:00–23:59(本地)
attack 07–21 每 2 小时 刚结束的那个窗口(07:00 那次跨日 10 小时,其余 2 小时)
tickets 每日 08:00 当前快照,分「已处理 / 未处理 / 未分配」三部分
libs 每日 12:00 规则库 / 病毒特征库更新通报

两个必须知道的口径细节(写在脚本 docstring 里):

  1. 「攻击成功」的唯一判据是两个条件同时成立:rule 属于 Web 攻击族(WA-* / MW-*)且 effective_level(row) == "critical",即证据里出现 nginx 响应码 2xx(打穿)。证据里只有 403 / handled → high,不算攻击成功。
  2. IN-* / PS-* / SL-* / EG-* / RS-* / AU-* 这类自伤类与主机侧事件在通报里单列为「完整性/持久化/日志异常事件」一段,不混进「攻击成功」数。两个判据互斥且守恒。

所有出站文本在发送前统一过 sanitize.py(见 sanitize.md)。

何时用

  • 不用手工跑:6 条 timer 已覆盖日常通报;本页的手工调用主要用于排障、核对口径、补发证据。
  • 想只看将发送什么:加 --dry-run(脱敏后打印,不发消息、不写成功标记)。
  • 想核对是否被误判:加 --rolling,汇总 [now-window, now) 而不是「刚结束的整点窗口」,这是排查口径而不是生产口径。
  • 想离线验证判据(不需要生产库、不需要网络):--selftest。
  • 想确认某张通报为什么没发:先看状态文件 state/digest.state.json 里的 last_sent_bucket——同一时间桶重复执行不再发送,除非 --force。

命令与参数

以下参数逐字取自 main() 的 argparse 定义(digest.py 的 WINDOW_SPECS 键为 1h / 12h,REPORT_SPECS 键为 attack / daily-alerts / libs / tickets)。

参数 类型 默认值 说明
--window 枚举,choices=sorted(WINDOW_SPECS) → 1h / 12h None 无 help;不传且不用 --report 时 argparse 报错
--db 路径 /opt/Cy/services/system.d/nfarmsd/data/nfarmsd.db(环境变量 NFARMSD_DB 可覆盖) 无 help
--state 路径 /opt/Cy/services/system.d/nfos/state/digest.state.json(NFOS_DIGEST_STATE) 无 help
--log 路径 /opt/Cy/services/logs/backend/nfos/digest.log(NFOS_DIGEST_LOG) 无 help
--category 字符串 digest notify.py 的通知类别(digest=汇总通报专用机器人;security=安全群/安全邮箱)
--quiet-empty 开关 False 窗口内无告警时不发消息
--force 开关 False 忽略幂等标记,强制发送
--rolling 开关 False 排查口径:汇总 [now-window, now),而不是刚结束的整点窗口
--dry-run 开关 False 只打印将发送的文本(脱敏后)
--selftest 开关 False 离线自检
--api 字符串 http://127.0.0.1:9990(NFARMSD_API) nfarmsd API(默认 …)
--rule-lib 路径 /opt/Cy/services/system.d/nfos/rule-library.yml(NFOS_RULE_LIB) rule-library.yml(读 policy.mass_ticket)
--no-mass 开关 False 本次不跑批量开单检查
--mass-threshold int None 条数严格大于才开单
--mass-window int None 滚动窗口(秒)
--mass-cooldown int None 同特征冷却(秒)
--mass-max int None 单轮最多开单数
--mass-levels 字符串 None 参与批量开单的级别(逗号分隔)
--report 枚举,choices=sorted(REPORT_SPECS) → attack / daily-alerts / libs / tickets None 按通报规则跑指定通报:daily-alerts 每日10:00 / attack 07-21每2h / tickets 每日08:00 / libs 每日12:00
--current-version 开关 False 配合 --report libs:只打印当前版本串(供监控探针用)

共 20 个参数。

不带参数时的默认行为

什么都不做,直接报用法错误。 main() 在 --selftest / --current-version / --report 三条分支都不成立时执行:

--window 必填(1h / 12h),或用 --report 跑通报规则里的四张通报

(ap.error(...) → 打印用法到 stderr、退出码 2)。脚本没有任何「无参数即跑某一档」的隐式默认。

示例

D=/opt/Cy/services/system.d/nfos

# 1) 离线自检:不碰网络、不读生产库(判据守恒用例都在里面)
sudo python3 "$D/monitoring/digest.py" --selftest

# 2) 只渲染不发送(脱敏后打印),核对口径最安全的姿势
sudo python3 "$D/monitoring/digest.py" --window 1h --dry-run

# 3) 按 timer 的线上形式跑 1 小时档(critical)
/usr/bin/python3 "$D/monitoring/digest.py" --window 1h

# 4) 12 小时档(high + warning)
/usr/bin/python3 "$D/monitoring/digest.py" --window 12h

# 5) 四张通报:先 dry-run,确认后再真发
sudo python3 "$D/monitoring/digest.py" --report daily-alerts --dry-run
sudo python3 "$D/monitoring/digest.py" --report attack
sudo python3 "$D/monitoring/digest.py" --report tickets
sudo python3 "$D/monitoring/digest.py" --report libs

# 6) 供监控探针取版本串(无副作用、幂等)
python3 "$D/monitoring/digest.py" --report libs --current-version

# 7) 排查口径:汇总「最近 1 小时」而不是「刚结束的整点窗口」
sudo python3 "$D/monitoring/digest.py" --window 1h --rolling --dry-run

依赖与权限

项 内容
解释器 /usr/bin/python3(timer 单元里写死这两个路径)
同目录模块 sanitize.py(顶部 import sanitize,缺失即启动失败);notify.py(发送时函数内 import notify)
第三方模块 PyYAML(读 signature-library.yml / rule-library.yml;缺 PyYAML 时相关口径降级)
外部命令 python3(自身);读 libs.db 非 root 时用 sudo -n python3 -c …;--report libs 读库失败只降级为「仅文件口径」
root 需要。timer 单元注释写明「需要 root:只读打开 nfarmsd 库(mode=ro)+ 读 secrets/notification.env(600)」,单元里 User=root
sudo -n 仅在非 root 时用于读 libs.db(root:nfarms 0640):先 sudo -n true 探测,探测失败即返回「取不到」,不抛异常
读取 nfarmsd SQLite(只读)、secrets/notification.env(600)、rule-library.yml、signature-library.yml、libs.db
写入 state/digest.state.json(幂等键)、--log 指定路径(JSON Lines 结构化日志)、批量开单时经 POST /alerts/report 写 nfarmsd
网络 飞书 webhook(HTTPS)+ SMTP(smtp.feishu.cn)。凭据一律从 secrets/notification.env 读,本页不记录任何取值
超时预算 单元 TimeoutStartSec:1h 档 180s、12h 档 300s、四张通报 180s;脚本自身 nice=10

幂等与失败语义

  • 幂等键:状态文件 state/digest.state.json 的 last_sent_bucket。同一时间桶(1h → 整点;12h → 本地 00:00 / 12:00;四张通报 → 各自的锚点)重复执行不再发送,除非 --force。
  • 发送失败不写成功标记:自检里有专门断言「发送失败的桶不算已发(下次会重试)」。
  • 停机补漏:bucket_bounds() 支持按 last_anchor 回退窗口起点,最多补 catchup_windows 个窗口,避免长时间停机后漏报。
  • 一半状态的风险:状态文件是「发完再记」;若进程在「消息已送达、状态尚未落盘」之间被杀,下一轮会重发同一窗口(宁可重发,不可漏发)。反过来不会出现「记了已发但没发出去」。
  • 失败不阻断防线:汇总只是「通报」,不是防线。任何异常(库读不到 / webhook 没配 / SMTP 挂了)都只记日志并返回非 0,不影响 security-guard 的检测、上报与工单。
  • 退出码:
退出码 含义
0 正常(含 --selftest 全过、--current-version 取到版本串)
1 --selftest 有失败项;或运行/通报抛异常({"ok": false, "error": ...} 已打印);或 --current-version 取不到
2 argparse 用法错误(例如不带 --window 也不带 --report)

常见错误与排查

现象 代码里的对应分支 排查步骤
--window 必填(1h / 12h),或用 --report 跑通报规则里的四张通报 main() 的 ap.error 这是用法错误不是故障;补 --window 1h 或 --report <四选一>
日志出现 digest.error(含 window 与 error 字段),stdout {"ok": false, …} run() 的 except Exception 收口 sudo systemctl status nfos-digest-hourly.service --no-pager、journalctl -u nfos-digest-hourly.service -n 50;再看 --dry-run 是否同样失败(区分「渲染失败」与「投递失败」)
日志出现 report.error(含 report 与 error) --report 分支的 except Exception 同上,先 --report <同名> --dry-run 复现
汇总「什么都没发」但库里明明有告警 幂等分支:同桶已发 查 state/digest.state.json 的 last_sent_bucket;确认要补发用 --force,并先加 --dry-run 看内容
--current-version 打印 unknown: <异常> --current-version 分支的 except 说明 --report libs 的版本口径取不到:查 libs.db 是否可读(root / sudo -n)、三库 YAML 是否在
通报里「当前版本」有值但「最新版本」空 _libs_db_read() 返回 (None, None):非 root 且 sudo -n 探测失败 通报会降级为「仅文件口径」并如实标注哪一项没取到,不抛异常。以 User=root 跑,或给该账号配 sudo -n 读权限
邮件收不到、飞书正常 notify 侧按类别限速(见 notify.md) 看 state/notify-mail-throttle.json 与 notify.py 的 MAIL_MIN_INTERVAL;digest 类别不限速(本就是定时发)
时间桶对不上(差 8 小时) 窗口边界统一用与 created_at 同形的 UTC 串,展示层再转本地 用 --dry-run 看正文里的窗口串;不要按本地时间去反推 last_sent_bucket

回滚方式

本脚本不修改任何配置或库内容,只读库 + 写自己的状态文件与日志。因此「回滚」= 撤销它的副作用,按最小到最大:

D=/opt/Cy/services/system.d/nfos

# 1) 只回滚「本轮不该发的通报」:删掉对应桶的幂等标记,或改用 --force 重发
sudo cat "$D/state/digest.state.json"          # 先看 last_sent_bucket
#    备份该文件后再删键(备份放到 state/unit-backups/,不要放 /etc/systemd/system/)

# 2) 停掉汇总(通知立刻停,检测/上报不受影响)
sudo systemctl stop    nfos-digest-hourly.timer nfos-digest-12h.timer
sudo systemctl disable nfos-digest-hourly.timer nfos-digest-12h.timer
#    需要连四张通报一起停时,同样处理 nfos-report-*.timer

# 3) 批量开单的关停(三层,从软到硬)
#    单次:加 --no-mass
#    改库:rule-library.yml 里 policy.mass_ticket.enabled: false → 再 apply(走 lib-ops.sh)
#    定时:停掉相关 timer(同上)

# 4) 回滚脚本本体:用版本化备份覆盖回去
sudo cp -a "$D/monitoring/digest.py.bak-<时间戳>" "$D/monitoring/digest.py"

备份落点铁律:脚本/单元的任何备份放 /opt/Cy/services/system.d/nfos/state/unit-backups/ (现场既有命名 <文件名>.bak-YYYYMMDD-HHMMSS),或仓库侧的 /opt/Cy/nfarms/.backup/。绝不能放在 /etc/systemd/system/ —— 该目录下出现多余 fragment 会触发 PS-002 完整性告警,把一次正常备份变成安全事件。详见 timers.md 第 11 节。

相关 CHANGELOG

CHANGELOG 章节标题
docs/CHANGELOG-2026-09-26.md §1.4 汇总:1h / 12h 两档,窗口是「刚结束的那个」
docs/CHANGELOG-2026-09-26.md §1.1 级别语义:按「打没打穿」而不是按「看着吓不吓人」
docs/CHANGELOG-2026-09-26.md §1.3 级别重判:只降不升,按证据
docs/CHANGELOG-2026-09-26.md §1.6 批量开单:阈值而不是逐条
docs/CHANGELOG-2026-09-26.md §1.7 脱敏:出口统一收口在 notify.py 的 render()
docs/CHANGELOG-2026-09-26.md §6.3 离线自检 2:digest.py --selftest
docs/CHANGELOG-2026-09-26.md §8.1 告警治理:整包回滚(主节点)
docs/CHANGELOG-2026-09-26.md §10.1 汇总通报统一走新机器人(用户要求)

定时器的触发节奏、日志落点与「怎么手工触发」见 timers.md 第 4 节(通知通报)。