告警地址与计数增强(alert-enrich.py)¶
仓库路径:
deploy/nfos/monitoring/alert-enrich.py| 线上路径:/opt/Cy/services/system.d/nfos/monitoring/alert-enrich.py语言:python3 | 定时器:无(被security-guard.sh以管道调用,不挂 timer)
用途¶
把安全告警的「源 / 目的地址、同类次数 / 同 IP 次数、是否公网」补齐。守卫每轮调用一次:把 plan JSON 从 stdin 读进来,输出增强后的 plan JSON。
源码 docstring 说明了为什么不把它塞进 security-guard.sh 的内嵌 heredoc:
- 在线防线的爆炸半径:
security-guard.sh每 60 秒被拉一次,是实时检测链路的全部;把「纯字符串/统计」逻辑放进它的内嵌 heredoc,会让这类逻辑的每次改动都直接改动防线本体(且bash -n查不出 python 语法错误)。 - 可离线自测:本模块的函数是纯函数(输入字符串/JSON,输出字符串/JSON),
.scratch/alert-enrich-test.py直接import就能覆盖公网/私网边界、次数统计、异常输入。 - 与既有模式一致:守卫已经在用
monitoring/sec-notify.py这条腿(同目录、同「best-effort、失败不影响检测」的约定)。 - 可复用:
log-audit.py等其它上报方也能直接吃同一套口径。
「公网」的口径¶
与前端 apps/web/*/js/core/config.js 的 NF.util.isPublicIp 完全一致——即「除下列非公网网段之外的地址」:
| 族 | 非公网网段 |
|---|---|
| IPv4 | 0.0.0.0/8(未指定/本网络)、10.0.0.0/8、127.0.0.0/8、169.254.0.0/16、172.16.0.0/12、192.168.0.0/16、224.0.0.0/4(组播)、240.0.0.0/4(保留) |
| IPv6 | ::/128、::1/128、fc00::/7(ULA)、fe80::/10(链路本地)、ff00::/8(组播) |
IPv4-mapped IPv6(::ffff:1.2.3.4)按内嵌 IPv4 判定;非 IP 字符串一律不是公网(classify → "unknown")。
何时用¶
- 不用手工跑:由
security-guard.sh每轮以管道调用(printf '%s' "$PLAN" | alert-enrich.py --alerts-file /tmp/alerts.json)。 - 单条命中调试:
--hit '{"sig":"WA-002","subject":"ip:8.8.8.8","detail":"…","evidence":"…"}'—— 读一行 JSON,打印增强结果。 - 改了口径之后:
--selftest(内置最小自检;完整用例在.scratch/alert-enrich-test.py)。 - 接新的上报方:
log-audit.py等也能直接复用同一套口径。
命令与参数¶
以下参数逐字取自 main() 的 argparse 定义。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--alerts-file |
路径 | ""(空) |
/alerts?limit=500 的原始响应(- 表示 stdin 冲突,勿用) |
--hit |
字符串(JSON) | ""(空) |
单条命中 JSON(调试用),输出增强记录 |
--host |
字符串 | ""(空) |
本机名(主机类特征的目的地址用) |
--window |
int | 500 |
计数窗口(条数,默认 500) |
--selftest |
开关 | False |
无 help |
共 5 个参数。
不带参数时的默认行为¶
不给 --alerts-file 时 _read_alerts("") 直接返回 None(不会去猜默认路径、不联网);--host 为空时回落到 socket.gethostname();然后从 stdin 读 plan JSON:
- stdin 为空 → 打印
{"ok": false, "error": "stdin 为空"},退出码 1; - stdin 不是合法 JSON → 打印
{"ok": false, "error": "plan JSON 不可解析:…"},退出码 1; - 解析成功 → 把增强后的 plan JSON 打到 stdout,退出码 0。
即在终端里裸跑(没有管道喂输入)会立刻返回 stdin 为空 并退出 1,不会卡住。
示例¶
D=/opt/Cy/services/system.d/nfos
# 1) 自检(公网/私网边界 + 次数统计 + 异常输入)
python3 "$D/monitoring/alert-enrich.py" --selftest
# 2) 守卫的线上调用形式:plan 从 stdin 进,增强结果从 stdout 出
curl -s http://127.0.0.1:9990/alerts?limit=500 > /var/tmp/alerts.json
printf '%s' "$PLAN" | python3 "$D/monitoring/alert-enrich.py" --alerts-file /var/tmp/alerts.json
# 3) 单条命中调试(对照公网/私网标记与同类计数)
python3 "$D/monitoring/alert-enrich.py" \
--hit '{"sig":"WA-002","subject":"ip:8.8.8.8","detail":"Web 攻击特征命中"}' \
--alerts-file /var/tmp/alerts.json --window 500
# 4) 指定本机名(主机类特征的目的地址用)
python3 "$D/monitoring/alert-enrich.py" --host Cy.AliCOS-BJ \
--hit '{"sig":"AU-001","subject":"ssh-failed","detail":"SSH 爆破"}'
# 5) 缩小计数窗口(只看最近 50 条)
python3 "$D/monitoring/alert-enrich.py" --window 50 \
--hit '{"sig":"WA-001","subject":"ip:203.0.113.7"}' --alerts-file /var/tmp/alerts.json
# 6) 走 stdin 的完整 plan(与守卫一致)
printf '%s' '{"items":[{"sig":"WA-002","subject":"ip:8.8.8.8"}]}' \
| python3 "$D/monitoring/alert-enrich.py" --alerts-file /var/tmp/alerts.json
依赖与权限¶
| 项 | 内容 |
|---|---|
| 解释器 | python3 |
| 第三方模块 | 无。ipaddress 也是标准库(源码写成 try: import ipaddress / except ImportError: ipaddress = None,缺它时一切判为 unknown,属降级不属崩溃) |
| 外部命令 | 无 |
| root | 不需要。纯字符串/JSON 变换;--alerts-file 需可读 |
sudo -n |
不需要 |
| 读取 | --alerts-file 指定的文件(UTF-8,errors="replace");读取失败或内容为空时静默返回 None(不报错) |
| 写入 | 不写任何文件。唯一输出是 stdout 的一行 JSON |
| 网络 | 不联网(调用方自己用 curl 取 /alerts) |
| 公开符号 | classify_ip、enrich_hit、enrich_plan、parse_alerts 等(.scratch/alert-enrich-test.py 直接 import 使用) |
幂等与失败语义¶
- 纯函数,天然幂等:同样输入 → 同样输出;重复执行无副作用。
- 状态文件:没有。无落盘、无缓存(
_NETS只是进程内惰性编译的网表)。 - 一半状态的风险:没有。所有工作都在内存里完成,一次性输出。
- 失败不阻断调用方:源码 docstring 明确——「任何情况都不要让调用方因本脚本失败而中断检测/上报」;调用方(守卫)对空输出 / 非零退出的处理是「退化为次数未知」。
- 退出码:
| 退出码 | 含义 |
|---|---|
0 |
成功(含 --hit 单条输出、--selftest 全部通过) |
1 |
输入不可解析:--hit 的 JSON 非法、stdin 为空、stdin 的 plan JSON 非法、--selftest 有反例失败 |
2 |
参数错误(argparse 自身行为;例如 --window 给了非整数) |
常见错误与排查¶
| 现象 | 代码里的对应分支 | 排查步骤 |
|---|---|---|
{"ok": false, "error": "stdin 为空"} |
if not raw.strip(): |
忘了用管道喂 plan,或上游写的是空串。守卫侧检查是 printf '%s' "$PLAN" \| … 还是 echo 了空变量 |
{"ok": false, "error": "plan JSON 不可解析:…"} |
json.loads(raw) 的 except |
上游传的不是 JSON。先用 printf '%s' "$PLAN" \| python3 -m json.tool 验一遍 |
{"ok": false, "error": "hit JSON 不可解析:…"} |
--hit 的 json.loads 的 except |
引号 / 转义问题;改用文件 + stdin 的方式,避免命令行转义 |
| 计数全是 0 / 次数未知 | _read_alerts() 返回 None(文件不存在、空、或 JSON 非法 → 静默) |
这是最隐蔽的一条:--alerts-file 读不到不报错。ls -l <file>、python3 -m json.tool < <file> \| head 确认内容;再确认 /alerts?limit=500 本身返回了什么 |
| 私网 IP 被判成公网(或反之) | classify_ip() / NON_PUBLIC_V4 / NON_PUBLIC_V6 |
--selftest 里有 12 条边界用例(172.16.0.1 private / 172.32.0.1 public、2001:db8::1 public、fe80::1 private 等)。任何口径改动必须与前端 NF.util.isPublicIp 逐条对应 |
| 主机类特征的目的地址是空的 | _dst_for_host() 按 sig 分支解析 进程= / 对端= / 本地= / 监听= |
说明证据串里没有对应字段;核对 security-guard.sh 传的 evidence 格式 |
--selftest 返回 1 |
_selftest() 的 bad 列表非空或计数断言失败 |
它会打印 {"ok": false, "bad": […], "sample": …};bad 里就是失败用例 |
缺少 ipaddress 导致一切判为 unknown |
except ImportError: ipaddress = None → _nets() 返回 None |
正常 CPython 不会缺;出现说明解释器环境异常(python3 -c "import ipaddress")。注意这是降级不报错 |
回滚方式¶
本脚本不修改任何文件或系统状态(连状态文件都没有)。回滚 = 换回脚本本体:
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/alert-enrich.py" "$BK/alert-enrich.py.bak-$TS"
# 2) 回滚
sudo cp -a "$BK/alert-enrich.py.bak-<时间戳>" "$D/monitoring/alert-enrich.py"
sudo chown root:root "$D/monitoring/alert-enrich.py"
sudo chmod 644 "$D/monitoring/alert-enrich.py"
# 3) 验收(离线,零副作用)
python3 "$D/monitoring/alert-enrich.py" --selftest
python3 "$D/monitoring/alert-enrich.py" --hit '{"sig":"WA-002","subject":"ip:8.8.8.8"}'
# 4) 若要「临时摘掉这一段」:让守卫不再调用它(改守卫前先备份守卫本体)
备份落点铁律:备份放
/opt/Cy/services/system.d/nfos/state/unit-backups/(命名<文件名>.bak-YYYYMMDD-HHMMSS)或仓库侧/opt/Cy/nfarms/.backup/。 绝不能放在/etc/systemd/system/—— 会触发 PS-002 完整性告警。见 timers.md 第 11 节。⚠️ 摘掉它的代价是降级而非失效:守卫对空输出/非零退出的处理是「退化为次数未知」, 检测与上报照常——所以静默的降级不会自己报错,改动后务必用示例 2 的形式真跑一次。
相关 CHANGELOG¶
| CHANGELOG | 章节标题 |
|---|---|
docs/CHANGELOG-2026-09-23.md |
§11. 安全告警内容增强:源/目的地址、同类与同 IP 次数、公网标记 |
docs/CHANGELOG-2026-09-23.md |
§12. 修两个真缺口:主节点邮件腿 + 备节点审计跑不了 |
docs/CHANGELOG-2026-09-26.md |
§1.1 级别语义:按「打没打穿」而不是按「看着吓不吓人」(同一套「公网/私网 + 响应码」判据口径) |
docs/CHANGELOG-2026-09-26.md |
§5.1 事故:库哨兵误判导致安全巡检停摆约 45 秒(守卫侧改动的爆炸半径,正是本脚本独立成文件的原因) |
定时器全景见 timers.md;
auto-respond.py消费本脚本追加的addr_block/extra,见 auto-respond.md。