安全告警自动处置引擎(auto-respond.py)¶
仓库路径:
deploy/nfos/monitoring/auto-respond.py| 线上路径:/opt/Cy/services/system.d/nfos/monitoring/auto-respond.py语言:python3 | 定时器:无(被security-guard.sh以管道调用,不挂 timer)
用途¶
security-guard.sh 上报循环里的前置评估器:每条命中先来这里评估「能不能当场处置」。能处置的(封 IP / 杀进程 / 隔离载荷)当场处置并留痕,处置不了的才让守卫按规范开单转人工。目标是「能自己搞定就别叫人」,而不是「一检出就开单」。
接口契约(由 security-guard.sh 调用)¶
| 通道 | 内容 |
|---|---|
| stdin | 一行 JSON——一条 plan 条目(key / sig / subject / severity / detail / evidence / template / signature_name / …;可能带 alert-enrich.py 追加的 addr_block / extra) |
| stdout | 一行 JSON——{"handled":bool,"action":str,"detail":str,"reason_code":str,"commands":[[argv...],...],"escalate":bool,"suppress":bool,"at":RFC3339,"api_state":str,"family":str,"targets":[...],"budget":{...},"dry_run":bool} |
| 退出码 | 恒 0。best-effort 组件:自身异常绝不阻断安全防线(异常写 stderr,输出 handled=false / reason_code=INTERNAL,守卫照常上报开单) |
特征族 → 处置策略¶
| 特征 | 动作 | 何时拒绝 |
|---|---|---|
WA-001 / WA-002 |
nginx 封禁来源公网 IP(写入 10-nfos-blocklist.conf 的 deny) |
非公网/回环/链路本地/CGNAT/内网、命中 allow.nets(特征库白名单)、邮件中继 IP、本机地址、当前 SSH 对端、预算用尽、清单满 200 条、nginx -t 不通过(自动回滚) |
RS-001/002/003 |
先 security-guard.sh --forensics <pid> 固定证据 → SIGKILL 进程树 → 若证据里有对端公网 IP 再 nginx 封禁 |
找不到 pid / pid 已不存在 / comm 与命中时不一致(PID 复用防护)/ 受保护进程(ssh/nginx/nfarmsd/systemd/chronyd + 平台告警通道脚本)/ 目标是我们自己的祖先进程 / 取证失败 / 预算用尽 |
MW-001 / MW-002 |
取证 → 杀进程 → 把落地载荷移入隔离目录(绝不删除,留 sha256) | 拿不到落地文件路径 / 文件属关键路径(/etc /usr /bin /sbin /lib /boot /opt/Cy 平台代码 /root/.ssh…)/ 取证失败 / 预算用尽 |
AU-001 |
nftables 封禁来源公网 IP(专用表 inet nfos_autoblock) |
内网/回环/白名单/邮件中继/本机地址/当前 SSH 已建立对端(防自锁)/ 预算用尽 / 无法从证据里解析出唯一攻击源 |
PS-* / IN-* / EG-* / SL-* / 其它 |
不自动处置,直接转人工开单 | (设计如此:这几类无法安全地自动修复) |
特殊:命中「平台告警通道白名单」(notify.py、sec-notify.py、ticket-notify.py、alert-enrich.py、alert-adapter.py、log-audit.py、probe-node.sh、security-guard.sh…)→ 不动作、且不转人工开单(suppress),因为这是已知误报(如 ticket-notify.py 通过 SMTPS 发信被 RS-001 判成反弹 Shell),靠特征库白名单收敛,不该再刷工单。
安全铁律(写进代码,也打印在 --list 里)¶
- 只对公网来源执行封禁;私网/回环/链路本地/CGNAT/白名单一律拒绝处置。RFC5737 文档网段(
192.0.2.0/24、198.51.100.0/24、203.0.113.0/24)不可路由、封禁无副作用,刻意允许用于演练/自测(所以本文件不用ipaddress.is_private判断,只用显式网表)。 - 绝不删除证据:载荷移入隔离目录、保留 sha256 与原始路径。
- 命令一律用参数数组调用(
subprocess.run([...]));禁止shell=True、禁止把不可信字符串拼进命令。 - 单轮动作数上限(默认 3,300s 窗口)+ 封禁清单条目上限(200);超出只记录不动作。
- 每个动作写审计:
logs/backend/nfos/auto-respond.log(jsonlog 风格),并经守卫的/alerts/reporthandled字段进平台总线。 - nginx 任何改动必须先
nginx -t;失败自动回滚并返回未处置。 - 幂等:同一 IP / 路径重复处置不产生重复条目。
何时用¶
- 不用手工跑:守卫每轮对每条命中以管道调用(正常路径)。
- 想看策略矩阵与安全边界:
--list(零副作用,读完就能回答「这条特征会不会被自动处理」)。 - 想演练「会做什么」而不动系统:
--dry-run(只评估并打印将执行的动作,不动系统、不写状态)。 - 人工收尾:
--unblock <IP>(从 nginx 封禁清单摘除该 IP)、--nft-teardown(整表拆除 nft 自动封禁表)。守卫不会调用这两个开关。 - 把处置结果回写平台总线:
--announce(守卫在/alerts/report成功后调用;平台侧未上线时返回api_state=api-missing,不影响处置)。
命令与参数¶
以下参数逐字取自 main() 的 argparse 定义。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--dry-run |
开关 | False |
只评估并打印将执行的动作(不动系统、不写状态) |
--list |
开关 | False |
打印策略矩阵与安全边界 |
--max-actions |
int | NFOS_AUTO_RESPOND_MAX_ACTIONS 或 NFOS_AR_MAX_ACTIONS,都没有则 DEFAULT_MAX_ACTIONS(= 3) |
单轮最多真动几次(默认 3,窗口 300s) |
--allow-private |
开关 | False |
允许处置私网/回环来源(默认拒绝;仅排障用,会破坏安全铁律 1) |
--unblock |
IP(metavar) |
无 | 人工/收尾用:从 nginx 封禁清单摘除该 IP |
--nft-teardown |
开关 | False |
人工/收尾用:整表拆除 nft 自动封禁表 |
--announce |
开关 | False |
把一条已完成的处置结果 best-effort 回写平台总线 POST $NFARMSD_API/alerts/handle(守卫在 /alerts/report 成功后调用;平台侧未上线时返回 api-missing,绝不影响处置) |
共 7 个参数。
不带参数时的默认行为¶
从 stdin 读一条 plan JSON 并真处置。三条分支要分清:
- stdin 是交互式终端(没喂输入)→ 往 stderr 写提示并返回 0:
auto-respond: 需要从 stdin 读入一条 plan JSON;或使用 --list / --unblock / --nft-teardown / --announce; - stdin 内容不是合法 JSON 对象 → 输出
handled=false/escalate=true/reason_code=INTERNAL,返回 0; - 正常解析 → 调
execute()处置,输出结果 JSON,返回 0。
生产路径由
security-guard.sh用管道喂入,不受交互式提示影响。
影响行为的环境变量¶
| 变量 | 作用 |
|---|---|
NFOS_AR_ROOT |
覆盖日志/状态/隔离/取证根目录(默认 /opt/Cy/services/logs/backend/nfos);离线自测用 |
NFOS_AR_NFOS_DIR |
覆盖 NFOS 目录(默认 /opt/Cy/services/system.d/nfos) |
NFOS_AR_NGINX_D |
覆盖 nginx 配置目录(默认 /opt/Cy/services/nginx.d) |
NFOS_AR_GUARD / NFOS_AR_LIBOPS |
覆盖 security-guard.sh / lib-ops.sh 路径 |
NFOS_AR_MAX_ACTIONS / NFOS_AUTO_RESPOND_MAX_ACTIONS |
单轮动作预算(被 --max-actions 覆盖) |
NFOS_AR_ACTION / NFOS_AR_DETAIL / NFOS_AR_HANDLED |
仅 --announce 用:决策结果经环境变量传入(不拼命令) |
NFARMSD_API |
平台 API(默认 http://127.0.0.1:9990) |
示例¶
D=/opt/Cy/services/system.d/nfos
# 1) 打印策略矩阵与安全边界(零副作用,排障第一步)
python3 "$D/monitoring/auto-respond.py" --list
# 2) 演练:只评估并打印将执行的动作(不动系统、不写状态)
printf '%s' '{"key":"nfos.sec:WA-002:ip:203.0.113.7","sig":"WA-002","subject":"ip:203.0.113.7","severity":"high","detail":"Web 攻击特征命中","evidence":"GET /.env 403"}' \
| python3 "$D/monitoring/auto-respond.py" --dry-run
# 3) 人工摘除一个误封 IP(守卫不会调用这个开关)
sudo python3 "$D/monitoring/auto-respond.py" --unblock 203.0.113.7
# 4) 人工整表拆除 nft 自动封禁表(收尾 / 排障)
sudo python3 "$D/monitoring/auto-respond.py" --nft-teardown
# 5) 应急:把单轮预算压到 0(只评估、不动手,等价于"保留判定但不处置")
printf '%s' "$PLAN" | python3 "$D/monitoring/auto-respond.py" --max-actions 0
# 6) 把一条已完成的处置结果回写平台总线(守卫的正常路径)
NFOS_AR_ACTION=block_ip NFOS_AR_DETAIL="nginx deny 203.0.113.7" NFOS_AR_HANDLED=1 \
printf '%s' "$PLAN" | python3 "$D/monitoring/auto-respond.py" --announce
# 7) 排障:允许处置私网来源(**会破坏安全铁律 1,用完立刻停止使用**)
printf '%s' '{"sig":"WA-002","subject":"ip:192.0.2.10","evidence":"GET /.env 200"}' \
| python3 "$D/monitoring/auto-respond.py" --dry-run --allow-private
依赖与权限¶
| 项 | 内容 |
|---|---|
| 解释器 | python3 |
| 第三方模块 | 无(标准库 argparse / hashlib / json / os / re / shutil / signal / socket / subprocess / sys / time / urllib) |
| 外部命令 | nginx(-t 校验、-s reload)、nft(表 inet nfos_autoblock)、security-guard.sh --forensics <pid>(取证);经 subprocess.run([...]) 参数数组调用 |
| root | 需要:写 /opt/Cy/services/nginx.d/10-nfos-blocklist.conf、nginx -s reload、nft 表操作、SIGKILL 进程、写隔离/取证目录 |
sudo -n |
脚本自身不调用 sudo(它假定自己已在 root 下运行,由守卫/单元提供) |
| 读取 | 特征库 allow.nets(经 lib-ops.sh 取,带 300s 缓存)、/proc/<pid>/…、证据串 |
| 写入 | 封禁清单 /opt/Cy/services/nginx.d/10-nfos-blocklist.conf;日志 /opt/Cy/services/logs/backend/nfos/auto-respond.log;状态 /opt/Cy/services/logs/backend/nfos/auto-respond.state.json;隔离区 …/quarantine;取证区 …/forensics;nft 表 inet nfos_autoblock |
| 网络 | 仅本机回环 HTTP(POST $NFARMSD_API/alerts/handle,仅 --announce) |
| 预算常量 | DEFAULT_MAX_ACTIONS = 3、BUDGET_WINDOW = 300(秒)、DEADLINE = 17.0(自身总预算;守卫给 20s 超时,留 3s 收尾)、MAX_BLOCKLIST = 200、NGINX_T_TIMEOUT = 10、FORENSICS_TIMEOUT = 12、API_TIMEOUT = 4、ALLOW_NETS_CACHE_TTL = 300 |
幂等与失败语义¶
- 幂等:同一 IP / 路径重复处置不产生重复条目(封禁清单里的
deny行按 IP 去重;隔离区按 sha256 与原始路径留痕)。 - 状态文件:
/opt/Cy/services/logs/backend/nfos/auto-respond.state.json—— 预算窗口的累计计数(300s窗口内累计,守卫每分钟一轮,≈单轮)。--dry-run不写状态。 - 一半状态的风险:有,且已被显式处理——nginx 改动走「改前
nginx -t,不通过自动回滚」,所以不会留下「配置改了但 nginx 没生效 / 生效了但配置是坏的」。nft 侧是自包含单表(inet nfos_autoblock),单条命令即可整表拆除,爆炸半径仅限表内 IP。 - 幂等回写:
--announce调用的/alerts/handle是幂等接口,重复调用无副作用。 - 自身异常绝不阻断防线:
main()里execute()的异常被捕获,输出handled=false/escalate=true/reason_code=INTERNAL,守卫照常上报开单;__main__最外层还有except BaseException兜底,写 stderr 后sys.exit(0)。 - 退出码:
| 退出码 | 含义 |
|---|---|
0 |
恒 0。包括:正常处置、--list、--dry-run、--unblock 传了非法 IP(输出 {"ok": false, "error": "非法 IP: …"})、内部异常、致命异常 |
不要用退出码判断处置是否成功——看 stdout 的
handled/action/reason_code/ok字段。
常见错误与排查¶
| 现象 | 代码里的对应分支 | 排查步骤 |
|---|---|---|
stderr 出现 auto-respond: 需要从 stdin 读入一条 plan JSON;或使用 --list / --unblock / --nft-teardown / --announce |
if sys.stdin.isatty(): |
这是在终端里裸跑的正常提示(退出码 0)。真调用要走管道,或用 --list 等开关 |
stdout reason_code=INTERNAL、handled=false、escalate=true |
execute() 的 except / 输入解析的 except |
stderr 里有 traceback(traceback.print_exc)与 auto-respond.log 里的 auto-respond 内部异常;守卫侧此时会照常开单,属于「降级不失效」 |
| 明明命中却没封禁 | 各特征族的「拒绝」分支 | 用 --dry-run 看 detail 与 reason_code。常见原因:来源是私网/回环、命中 allow.nets、邮件中继 IP、本机地址、当前 SSH 对端、预算用尽(budget 字段)、清单已满 200 条、nginx -t 不通过 |
| 反复判成「反弹 Shell」但对象是平台自己的脚本 | PROTECTED_SCRIPTS 精确匹配白名单 |
已知误报(ticket-notify.py 经 SMTPS 发信)。白名单是完整绝对路径的整 token 精确匹配;输出里应为 suppress=true 且不开单。若仍开单,核对路径是否与 PROTECTED_SCRIPTS 逐字一致 |
--unblock 输出 {"ok": false, "error": "非法 IP: …"} |
valid_ip() 失败 |
只接受单个合法 IP(不接受网段、不接受通配) |
--announce 返回 api_state=api-missing |
平台侧接口未上线(best-effort 分支) | 不影响处置。确认 NFARMSD_API 与平台版本;--announce 的决策结果是从 NFOS_AR_ACTION / NFOS_AR_DETAIL / NFOS_AR_HANDLED 环境变量读的,不拼命令 |
| 想「立刻停掉自动处置」但保留检测/上报 | 见下「回滚方式」 | 三种粒度:--max-actions 0(单次)、--dry-run(单次演练)、state 预算/策略层(持久) |
| nft 表残留 | --nft-teardown |
sudo python3 "$D/monitoring/auto-respond.py" --nft-teardown |
回滚方式¶
本脚本会改动系统状态——这是它必须被谨慎对待的原因。改动面与还原方式:
| 改动面 | 内容 | 还原方式 |
|---|---|---|
| nginx 封禁清单 | /opt/Cy/services/nginx.d/10-nfos-blocklist.conf 里追加的 deny <IP>; 行 |
--unblock <IP> 摘单条;或把该文件清空为「只留注释头」形态 → nginx -t → nginx -s reload |
| nft 封禁表 | inet nfos_autoblock 表内的 IP |
--nft-teardown 整表拆除(自包含表,爆炸半径仅限表内) |
| 进程 | 被 SIGKILL 的进程树 |
不可逆(进程已死)。证据在 forensics 目录 |
| 载荷隔离 | 被移入 quarantine 的文件(从未删除) |
按 auto-respond.log / 取证记录里的原始路径与 sha256 移回 |
| 状态文件 | /opt/Cy/services/logs/backend/nfos/auto-respond.state.json(预算窗口) |
删除该文件即可重置预算窗口 |
| 脚本本体 | monitoring/auto-respond.py |
用版本化备份覆盖回去 |
D=/opt/Cy/services/system.d/nfos
BK=$D/state/unit-backups
# A) 一键关停「自动处置」但保留检测/上报(推荐首选,可逆)
# 持久化做法:把单轮预算压到 0 —— 脚本会评估但不真动手
# (在 security-guard.sh 的调用处加 --max-actions 0,或设置环境变量)
# export NFOS_AUTO_RESPOND_MAX_ACTIONS=0
# B) 单次演练,确认"本来会做什么"
printf '%s' "$PLAN" | python3 "$D/monitoring/auto-respond.py" --dry-run
# C) 收拾 nginx 侧
sudo python3 "$D/monitoring/auto-respond.py" --unblock <IP>
sudo nginx -t && sudo nginx -s reload
# D) 收拾 nft 侧
sudo python3 "$D/monitoring/auto-respond.py" --nft-teardown
# E) 回滚状态与脚本本体
TS=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 755 "$BK"
sudo cp -a /opt/Cy/services/logs/backend/nfos/auto-respond.state.json \
"$BK/auto-respond.state.json.bak-$TS" 2>/dev/null
sudo rm -f /opt/Cy/services/logs/backend/nfos/auto-respond.state.json
sudo cp -a "$D/monitoring/auto-respond.py" "$BK/auto-respond.py.bak-$TS"
sudo cp -a "$BK/auto-respond.py.bak-<时间戳>" "$D/monitoring/auto-respond.py"
sudo chown root:root "$D/monitoring/auto-respond.py"
sudo chmod 644 "$D/monitoring/auto-respond.py"
# F) 验收:策略矩阵 + 一条 dry-run
python3 "$D/monitoring/auto-respond.py" --list
printf '%s' "$PLAN" | python3 "$D/monitoring/auto-respond.py" --dry-run
备份落点铁律:脚本/单元的任何备份放
/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/rule-library.yml/lib-ops.sh这类治理文件会触发真实的IN-001自检 (见docs/CHANGELOG-2026-09-26.md「§9.9 同时关闭的工单与终态」),改完要走lib-ops.sh的{"op":"apply"}同步完整性基线。
相关 CHANGELOG¶
| CHANGELOG | 章节标题 |
|---|---|
docs/CHANGELOG-2026-09-26.md |
§8.6 一键关停「自动处置」(保留检测/上报) |
docs/CHANGELOG-2026-09-26.md |
§5.1 事故:库哨兵误判导致安全巡检停摆约 45 秒 |
docs/CHANGELOG-2026-09-26.md |
§13.2 PS-002 噪声的根治(18 张单全部可解释) |
docs/CHANGELOG-2026-09-26.md |
§13.8 PS-002 的正确修法(第二次才对)+ 一个「看起来对但永远不生效」的坑 |
docs/CHANGELOG-2026-09-26.md |
§12.3 顺带查出的更严重问题:邮件发件箱被服务商限流,邮件全丢(RS-001 误报与 PROTECTED_SCRIPTS 白名单的由来) |
docs/CHANGELOG-2026-09-18.md |
§2. 修复「安全防线检测到但从不告警」的致命缺陷(守卫调用链的前置评估器背景) |
定时器全景见 timers.md 第 3 节(安全防线);
alert-enrich.py会为本脚本的输入追加addr_block/extra,见 alert-enrich.md。