安全告警通知分发(sec-notify.py)¶
仓库路径:
deploy/nfos/monitoring/sec-notify.py| 线上路径:/opt/Cy/services/system.d/nfos/monitoring/sec-notify.py语言:python3 | 定时器:无(被security-guard.sh/log-audit.py调用,不挂 timer)
用途¶
把安全告警按 rule-library.yml 的通知路由(critical: [feishu_security, mail_security]、warning: [feishu_security])经 notify.py 推到飞书安全群(FEISHU_SECURITY_WEBHOOK)与安全邮箱。
它补的是安全防线的「最后一条腿」。源码 docstring 记下了 2026-09-18 核查时的现状:
security-guard.sh检出的安全告警只写平台库 + 转工单,没有任何推送;- 平台自己的飞书推送(nfarmsd 的
[alert] enabled)默认是 false; - 告警适配器(
alert-adapter.py)只处理 Alertmanager 的 webhook,看不到守卫直接上报给平台的安全告警。
结果就是「检测到了、开了单、但没人知道」。本脚本把这条腿接上,并以 best-effort 约定运行:失败不影响检测。
何时用¶
- 不用手工跑:由
security-guard.sh在POST /alerts/report成功之后调用(best-effort,失败不影响检测)。 - 部署 / 改通道之后的自检:
--selftest(发一条自检消息确认通道可用)。 - 核对「工单号 + 处理链接」是否真的出现在飞书与邮件正文里:
--dry-run——它不只列字段,还把渲染后的正文打出来,部署核对时不用真发一条消息。 - 手工补推一条已定性的安全告警:给全
--level/--name/--resource/--alarm-id/--content。
命令与参数¶
以下参数逐字取自 main() 的 argparse 定义。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--level |
枚举 critical / high / warning / info |
warning |
无 help。(high 是 2026-09-26 四档语义新增;不补枚举会被 argparse 直接拒掉) |
--category |
字符串 | security |
通知类别(默认 security → 飞书安全群 + 安全邮箱) |
--name |
字符串 | 安全告警 |
无 help |
--resource |
字符串 | ""(空) |
无 help |
--alarm-id |
字符串 | ""(空) |
无 help |
--content |
字符串 | ""(空) |
无 help |
--content-file |
路径 | ""(空) |
从文件读正文(- 表示 stdin) |
--extra |
字符串(JSON 对象) | ""(空) |
附加字段,JSON 对象字符串 |
--ticket-no |
字符串 | ""(空) |
工单号(NFarms-<TYPE>-<时间>-<序号>) |
--handle-url |
字符串 | ""(空) |
工单处理链接(https://online.nfarms.cn/#tickets?no=…) |
--dry-run |
开关 | False |
只打印将要发送的内容,不真的推送 |
--selftest |
开关 | False |
发一条自检消息确认通道可用 |
共 12 个参数。
--ticket-no/--handle-url并入extra(键名工单号/处理链接,与notify.TICKET_FIELD_ALIASES对齐),由notify.py在正文里逐项展示(飞书 + 邮件都展示)。默认空串 = 不传,行为与改动前完全一致。--extra若不是合法 JSON,会被降级为{"附加": <原字符串>},不会报错退出。
不带参数时的默认行为¶
args.name = 安全告警,args.level = warning,args.category = security,args.resource / args.alarm_id / args.content / --extra 全空。它会真发一条通知(推给飞书安全群 + 安全邮箱),因为 --dry-run / --selftest 都没开。任何「先看看」的意图都必须显式加 --dry-run。
示例¶
D=/opt/Cy/services/system.d/nfos
# 1) 自检:确认 webhook 真的通
sudo python3 "$D/monitoring/sec-notify.py" --selftest
# 2) 只看会发什么(含渲染后的飞书正文与邮件正文),不推送
sudo python3 "$D/monitoring/sec-notify.py" --level critical --name "Web 攻击特征命中" \
--resource "ip:203.0.113.7" --alarm-id "nfos.sec:WA-002:ip:203.0.113.7" \
--content "在最近 3000 行日志中命中 207 次" --dry-run
# 3) 带「工单号 + 处理链接」的 dry-run(部署核对口径用这一条)
sudo python3 "$D/monitoring/sec-notify.py" --level high --name "持久化变更" \
--resource "host:Cy.AliCOS-BJ" --alarm-id "nfos.sec:PS-002:Cy.AliCOS-BJ" \
--content "新增 systemd 单元" \
--ticket-no "NFarms-SEC-202609261500-00001" \
--handle-url "https://online.nfarms.cn/#tickets?no=NFarms-SEC-202609261500-00001" \
--dry-run
# 4) 从文件读正文(正文很长时避免命令行转义踩坑)
sudo python3 "$D/monitoring/sec-notify.py" --level warning --name "日志异常" \
--content-file /var/tmp/finding.txt
# 5) 从 stdin 读正文(`-`)
cat /var/tmp/finding.txt | sudo python3 "$D/monitoring/sec-notify.py" \
--level warning --name "日志异常" --content-file -
# 6) 附加字段(JSON 对象)
sudo python3 "$D/monitoring/sec-notify.py" --level critical --name "反弹 Shell" \
--resource "pid:12345" --alarm-id "nfos.sec:RS-001:pid:12345" \
--extra '{"主机":"Cy.AliCOS-BJ","证据":"bash -i >& /dev/tcp/…"}' --dry-run
依赖与权限¶
| 项 | 内容 |
|---|---|
| 解释器 | python3(#!/usr/bin/env python3) |
| 同目录模块 | notify.py(必需)。用 sys.path 直连 import notify 加载——源码注释明确记录了不用 importlib.util.spec_from_file_location + exec_module 的原因(部分环境下抛 'NoneType' object has no attribute '__dict__') |
| 第三方模块 | 无(只用标准库 argparse / io / json / os / sys) |
| root | 需要(读 secrets/notification.env,600 root) |
sudo -n |
不需要(脚本自身不调 sudo) |
| 读取 | secrets/notification.env;--content-file 指定的文件(UTF-8,- 表示 stdin) |
| 写入 | 不写任何文件(配置快照与渲染结果只打印到 stdout) |
| 网络 | 出站 HTTPS(飞书 webhook)+ SMTP smtp.feishu.cn。凭据一律从环境文件读,取值见 secrets/notification.env(不得转录) |
| 依赖的调用方 | security-guard.sh(守卫每轮上报成功后)、log-audit.py(SEC_NOTIFY 指向同目录的 monitoring/sec-notify.py) |
幂等与失败语义¶
- 不做去重:它只管「把这一条推出去」;去重 / 恢复由守卫的
dedup_key与平台告警库负责。 - 状态文件:没有。唯一可能的落盘是
notify.py的邮件限速状态state/notify-mail-throttle.json。 - 一半状态的风险:飞书与邮件是两次独立投递。可能出现「飞书到了、邮件被限速 skipped」——这不是丢消息,飞书已实时送达,
digest类别承担汇总兜底。 - 失败不阻断检测:这是与守卫之间的约定——「best-effort,失败不影响检测」。
notify.py内部把发送异常都收成返回值,不向外抛。 - 配置缺失与发送失败被区分开(源码注释明确):便于排障。
- 退出码:
| 退出码 | 含义 |
|---|---|
0 |
已投递(至少一个渠道成功);或 --dry-run 完成;或 --selftest 成功 |
1 |
全部渠道失败(notify 加载失败也走这条前置分支:加载 notify.py 失败:… → return 2,见下) |
2 |
配置缺失:notify.py 加载失败,或 describe() 显示 feishu_enabled 为假 / feishu_channels 为空 |
判定逻辑:
out["ok"] = bool(result["feishu"] or result["mail"]);不 ok 时若 「飞书未启用或没有通道」→2,否则 →1。所以看到 2 先查配置,看到 1 先查网络/口令。
常见错误与排查¶
| 现象 | 代码里的对应分支 | 排查步骤 |
|---|---|---|
stdout {"ok": false, "error": "加载 notify.py 失败:…"},退出码 2 |
load_notify() 的 except |
notify.py 不在同目录或不可读:ls -l "$D/monitoring/notify.py"、python3 -c "import sys;sys.path.insert(0,'$D/monitoring');import notify;print('ok')" |
--dry-run 里 feishu_enabled: false 或 feishu_channels: [] |
cfg.describe() 的输出 |
说明 secrets/notification.env 里没有 FEISHU_SECURITY_WEBHOOK,或 NOTIFY_FEISHU_ENABLED=0。取值见 secrets/notification.env(不得转录) |
| 退出码 1(而非 2) | out["ok"] 为假但配置看起来齐全 |
诊断顺序:① --dry-run 看渲染是否正常;② 直接 python3 "$D/monitoring/notify.py" --test 分辨是飞书还是 SMTP 的问题;③ 邮箱侧看是不是被限速(state/notify-mail-throttle.json) |
--level high 被 argparse 拒掉 |
choices=["critical","high","warning","info"] |
只允许这四个值(没有 error);历史版本曾是三档,high 是 2026-09-26 补齐的 |
--extra 传了非 JSON,收件人看到 {"附加": "…"} |
json.loads 失败后 extra = {"附加": args.extra} |
这是设计行为(不因附加字段格式错就丢整条通知);要结构化请传合法 JSON 对象 |
| 正文里没有「工单号 / 处理链接」两行 | 键名未对齐 notify.TICKET_FIELD_ALIASES |
用 --ticket-no / --handle-url(本脚本会写正确键名),或在 --extra 里直接用 工单号 / 处理链接 |
--content-file 读到空 |
content = io.open(...).read() |
检查文件路径与编码(要求 UTF-8);- 时检查 stdin 是否真的接了管道 |
| 飞书到了、邮件没到 | notify 邮件限速 / 服务商限流 | 见 notify.md 的「邮件」,security 类别最小间隔 900s |
回滚方式¶
本脚本不改系统状态、不写配置,只读环境文件 + 发消息。回滚 = 换回脚本本身:
D=/opt/Cy/services/system.d/nfos
BK=$D/state/unit-backups
# 1) 改之前先备份
TS=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 755 "$BK"
sudo cp -a "$D/monitoring/sec-notify.py" "$BK/sec-notify.py.bak-$TS"
# 2) 回滚
sudo cp -a "$BK/sec-notify.py.bak-<时间戳>" "$D/monitoring/sec-notify.py"
sudo chown root:root "$D/monitoring/sec-notify.py"
sudo chmod 644 "$D/monitoring/sec-notify.py"
# 3) 立即验收(只推送一条自检,不动检测链路)
sudo python3 "$D/monitoring/sec-notify.py" --selftest
# 4) 若要「整条腿先摘掉」:让守卫不再调用它(改守卫、不动本脚本)
# 改守卫前先备份守卫本体到 state/unit-backups/
备份落点铁律:备份放
/opt/Cy/services/system.d/nfos/state/unit-backups/(命名<文件名>.bak-YYYYMMDD-HHMMSS)或仓库侧/opt/Cy/nfarms/.backup/。 绝不能放在/etc/systemd/system/—— 会触发 PS-002 完整性告警。见 timers.md 第 11 节。⚠️ 特别注意:杀掉本脚本等于摘掉「推送到人」这条腿。
security-guard.sh调用它时是 best-effort(不在路径上、不set -e),静默失败不会让守卫报错——这正是docs/CHANGELOG-2026-09-26.md「§9 通知腿 NameError 缺陷」暴露过的形态。
相关 CHANGELOG¶
| CHANGELOG | 章节标题 |
|---|---|
docs/CHANGELOG-2026-09-18.md |
§36. 新增 sec-notify.py(安全告警 → 飞书安全群/安全邮箱) |
docs/CHANGELOG-2026-09-18.md |
§2. 修复「安全防线检测到但从不告警」的致命缺陷 |
docs/CHANGELOG-2026-09-23.md |
§2. 主节点补齐 notify.py,修复 sec-notify.py 加载器 |
docs/CHANGELOG-2026-09-23.md |
§3. 合成命中跑通「检测 → 平台告警 → 飞书推送 → 自动恢复」 |
docs/CHANGELOG-2026-09-26.md |
§9. 通知腿 NameError 缺陷(P0,已修复) |
docs/CHANGELOG-2026-09-26.md |
§1.7 脱敏:出口统一收口在 notify.py 的 render() |
通道与限速细节见 notify.md;出站脱敏见 sanitize.md。