跳转至

安全告警自动处置引擎(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 里)

  1. 只对公网来源执行封禁;私网/回环/链路本地/CGNAT/白名单一律拒绝处置。RFC5737 文档网段(192.0.2.0/24、198.51.100.0/24、203.0.113.0/24)不可路由、封禁无副作用,刻意允许用于演练/自测(所以本文件不用 ipaddress.is_private 判断,只用显式网表)。
  2. 绝不删除证据:载荷移入隔离目录、保留 sha256 与原始路径。
  3. 命令一律用参数数组调用(subprocess.run([...]));禁止 shell=True、禁止把不可信字符串拼进命令。
  4. 单轮动作数上限(默认 3,300s 窗口)+ 封禁清单条目上限(200);超出只记录不动作。
  5. 每个动作写审计:logs/backend/nfos/auto-respond.log(jsonlog 风格),并经守卫的 /alerts/report handled 字段进平台总线。
  6. nginx 任何改动必须先 nginx -t;失败自动回滚并返回未处置。
  7. 幂等:同一 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。