跳转至

安全告警通知分发(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。