4. 编写第一个ZAP自动化脚本

ZAP 的图形界面适合探索和手工验证,但一旦目标站点需要反复测试、纳入 CI 或按周期巡检,就必须把“打开界面点按钮”变成“脚本调用接口”。ZAP 从设计之初就提供了完整的 REST API,默认监听在 http://127.0.0.1:8080,所有界面能做的动作几乎都能通过 HTTP 请求触发。本节用 Python 写第一个真正可运行的自动化脚本:启动会话、爬取目标、等待被动扫描、拉取告警、生成报告。

先理解 ZAP API 的调用约定

ZAP API 按“组件 + 动作”组织路径,格式为 /JSON/{component}/{action}/?参数。例如 /JSON/core/action/newSession/ 新建会话,/JSON/spider/action/scan/ 启动爬虫,/JSON/alert/view/alertsSummary/ 读取告警统计。返回体是 JSON,Python 用 requests 解析即可。

有两个关键参数必须提前处理。第一是 API Key:ZAP 默认对来自非本机的请求校验 key,可在“工具 → 选项 → API”中查看或关闭。脚本里统一用 apikey 参数携带。第二是“动作”与“视图”的区别:action 会改变 ZAP 状态(如启动扫描),view 只读取数据(如查询进度)。理解这一点,才能正确设计轮询逻辑。

本地脚本与 ZAP 在同一台机器时,可把 ZAP_HOST 设为 127.0.0.1;若 ZAP 跑在容器或远程主机,需同时修改主机地址、端口,并确保 API Key 已启用。

脚本的整体数据流

自动化脚本不是简单地把几个 API 串起来,它有一条明确的“输入—处理—输出”链路:目标 URL 进入爬虫,爬虫把发现的请求交给被动扫描器,被动扫描器在后台分析流量并沉淀告警,最后脚本读取告警并落盘为报告。下面这张图展示了这条链路以及每一步对应的 API 组件。

Python 脚本调用 ZAP API 的爬取与被动扫描数据流 目标站点 URL http://demo.testfire.net 新建会话 core/action/newSession 爬虫扫描 spider/action/scan 被动扫描器(后台) 分析流量 → 沉淀告警 轮询进度 spider/view/status 未完成则重试 读取告警 alert/view/alerts 生成报告 JSON / HTML 关键判断点: ① 爬虫 status 达到 100 才代表 URL 收集完成;② 被动扫描无需单独启动,但需等待队列消化; ③ 告警按 risk(High/Medium/Low/Informational)分级,报告应保留 confidence 与 evidence 字段。

编写脚本:从会话到报告

下面给出一个结构完整、可直接运行的示例脚本。它把 API 调用封装成函数,主流程按“建会话 → 爬取 → 轮询 → 读告警 → 写报告”推进。示例目标使用公开的测试站点 http://demo.testfire.net,实际使用时替换为已获授权的目标。

import requests, time, json

ZAP = "http://127.0.0.1:8080"
KEY = "your-api-key"          # 在 ZAP 选项 → API 中查看
TARGET = "http://demo.testfire.net"

def api(component, action, **params):
    params["apikey"] = KEY
    url = f"{ZAP}/JSON/{component}/{action}/"
    r = requests.get(url, params=params, timeout=30)
    r.raise_for_status()
    return r.json()

# 1. 新建会话,避免污染历史数据
api("core", "action/newSession", name="auto-scan-01", overwrite="true")

# 2. 启动爬虫,拿到 scanId
scan_id = api("spider", "action/scan", url=TARGET, recurse="true")["scan"]

# 3. 轮询爬虫进度,直到 100%
while True:
    status = int(api("spider", "view/status", scanId=scan_id)["status"])
    print(f"spider progress: {status}%")
    if status >= 100:
        break
    time.sleep(2)

# 4. 等待被动扫描队列消化(示例等待 5 秒)
time.sleep(5)

# 5. 读取告警
alerts = api("alert", "view/alerts", baseurl=TARGET)["alerts"]
summary = api("alert", "view/alertsSummary", baseurl=TARGET)["alertsSummary"]

# 6. 输出基础报告
report = {"target": TARGET, "summary": summary, "alerts": alerts}
with open("zap_report.json", "w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False, indent=2)
print("report saved, total alerts:", len(alerts))

这段代码有几个容易踩坑的地方。第一,spider/action/scan 返回的是 scan 字段而不是 scanId,字段名写错会直接抛 KeyError。第二,爬虫进度到 100% 只表示 URL 收集结束,被动扫描器可能仍在处理队列,因此需要额外等待或轮询 core/view/numberOfMessages 判断流量是否稳定。第三,alert/view/alerts 的 baseurl 参数用于过滤目标范围,不传会返回整个会话的所有告警。

结果如何判断

脚本运行结束后,先看控制台输出的告警总数,再打开 zap_report.json。报告里 summary 字段按风险等级给出计数,例如 {"High": 0, "Medium": 2, "Low": 5, "Informational": 3}。如果 High 为 0 但 Medium 不为 0,说明站点没有严重漏洞,但存在需要关注的配置或输入处理问题。每条告警应重点看四个字段:alert(漏洞名称)、risk(风险等级)、confidence(置信度)、evidence(证据片段)。evidence 是判断告警真伪的关键,例如一个 XSS 告警的 evidence 里如果出现了未编码的 <script>,就值得进一步手工验证。

被动扫描只分析爬虫已经访问过的流量,不会主动构造攻击请求。因此它发现的告警数量与爬虫覆盖范围强相关。如果爬虫只抓到了首页,报告里的告警会明显偏少,这不代表站点安全,而是覆盖不足。

把脚本变成可复用的基础模板

第一个脚本的目标不是功能完备,而是跑通链路。跑通之后,可以按以下方向逐步加固:把 API Key 和 TARGET 抽到配置文件或环境变量;把轮询逻辑封装成带超时和重试的 wait_until 函数;把报告输出从 JSON 扩展为 HTML,便于直接查看;在爬虫之前加入 core/action/accessUrl 预热,确保目标可达。这些改动都不需要推翻现有结构,只是在同一条数据流上增加节点。

需要强调的是,自动化脚本的合法性边界由授权决定。对 demo.testfire.net 这类公开测试站点可以直接练习,但对任何生产系统,必须先取得书面授权,并在脚本中限制目标范围,避免爬虫递归到非授权域名。