双档告警汇总与四张通报(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 里):
- 「攻击成功」的唯一判据是两个条件同时成立:
rule属于 Web 攻击族(WA-*/MW-*)且effective_level(row) == "critical",即证据里出现 nginx 响应码 2xx(打穿)。证据里只有 403 /handled→high,不算攻击成功。 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 三条分支都不成立时执行:
(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 节(通知通报)。