ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Plannotator 隔离式 OWASP ZAP DAST 实战:一次性会话被动扫描的设计与证据校验

Plannotator 隔离式 OWASP ZAP DAST 实战:一次性会话被动扫描的设计与证据校验 【免费下载链接】plannotatorAnnotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click.项目地址https://gitcode.com/gh_mirrors/pl/plannotator点击查看免费下载本文基于 Plannotator 仓库 scripts/dast/README.md 及其配套实现展开。Plannotator 是一个用于可视化标注与评审编码 Agent 计划plan和代码 diff 的开源工具本文讲解其安全工程中至关重要的一环在完全隔离的 Docker 内网中对一次性 annotate 会话运行 OWASP ZAP 被动基线扫描passive baseline scan的完整方案。读完本文你将掌握该工作流的隔离网络设计、路由守卫与扫描钩子hook的实现、ZAP 大响应缓存调优、报告证据校验fail-closed机制以及如何手动触发该扫描并正确解读其产物。工作流总览为什么 DAST 必须“隔离”且“一次性”Plannotator 的 ZAP DAST 工作流对一次性disposableannotate 会话运行被动基线扫描。所谓“一次性”是指整个被测目标由工作流动态创建、仅存在于 GitHub Actions runner 内扫描结束即销毁。所谓“隔离”体现在两个层面网络隔离应用与 ZAP 两个容器都挂在通过docker network create --internal创建的 Docker 内网中该网络无外部出口因此应用和扫描器都无法触达生产环境或任何外部服务凭据隔离仓库、云服务、npm、模型提供商model-provider以及用户凭据一律不传入两个容器。正如 target.ts 中实现的强制校验所示被测目标入口只有在检测到PLANNOTATOR_DAST_ISOLATED1时才允许启动否则直接抛出异常拒绝运行。这个环境变量是一道确认开关acknowledgement它只应在隔离的、一次性的环境中设置绝不能在隔离环境之外设置该工作流是受支持的执行路径而target.ts本身不是生产服务器。工作流刻意采用定时/手动触发而非随每个 PR 运行原因在于被动基线扫描的定位是周期性的安全健康检查而不是每次提交的快速回归门禁。手动触发命令为gh workflow run dast.yml --repo backnotprop/plannotator工作流定义位于 .github/workflows/dast.yml通过schedule中的cron: 47 16 * * 0每周日 16:47 UTC自动运行同时开放workflow_dispatch支持手动触发权限被压缩为contents: read并使用concurrency保证同一 ref 下不会并发重复扫描。运行环境与运行时固定pinning为保证扫描结果的可复现性与供应链安全工作流对两个运行时镜像做了摘要级固定digest pinning而非仅固定 tag。见 dast.yml运行时镜像版本用途Bun 目标oven/bun:1.3.11-slimsha256:4782...42d91.3.11运行一次性 annotate 目标服务器ZAPghcr.io/zaproxy/zaproxy:stablesha256:781a...81ef2.17.0被动基线扫描器工作流在启动前执行一系列“Pull and verify pinned runtime images”校验dast.yml拉取镜像失败重试 3 次每次间隔递增 5 秒通过docker image inspect --format {{index .RepoDigests 0}}核对实际 RepoDigest 与固定摘要一致在--network none下运行镜像验证版本bun --version必须等于 1.3.11Bun 用户 UID 必须为 1000验证 ZAP 镜像的Config.User为zap且zap.sh -version输出等于 2.17.0将验证结果写入$GITHUB_STEP_SUMMARY。这些校验的意义在于摘要固定防止 tag 漂移版本与用户校验则保证容器内运行身份、工具链版本与后续步骤的假设一致例如后续以--user 1000:1000运行。隔离网络与容器加固创建 internal 网络并验证无外联工作流创建名为plannotator-dast-${{ github.run_id }}-${{ github.run_attempt }}的 internal 网络dast.yml并通过docker network inspect --format {{.Internal}}断言Internaltrue。随后用一个探针容器实际验证隔离性在网络上向保留地址http://192.0.2.1发起 2 秒超时请求若请求成功说明有外联能力则直接让步骤失败。目标容器加固被测目标以如下加固参数启动dast.yml--user 1000:1000 --read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 --tmpfs /tmp:rw,nosuid,nodev,size128m并以只读方式挂载$GITHUB_WORKSPACE:/workspace:ro设置HOME/tmp/home、PLANNOTATOR_DATA_DIR/tmp/plannotator同时注入PLANNOTATOR_DAST_ISOLATED1最后执行bun run scripts/dast/target.ts。容器的数据目录指向 tmpfs叠加只读根文件系统进一步保证了“零落盘、零凭据”。目标就绪健康检查启动后工作流以最多 60 次、每秒一次的轮询执行健康检查dast.yml验证扫描端口19432上/、/api/plan、/api/ai/capabilities均返回 200/api/definitely-missing、/robots.txt返回 404哨兵端口19433上/返回 200对19432/api/approve发起 POST 必须返回 405证明状态变更方法被守卫拦截上游真实应用端口19434在内网中不可达fetch必须失败防止扫描器绕过守卫直连上游。任一检查失败且容器仍在运行时继续重试容器若退出则直接输出docker logs并失败。一次性 annotate 目标target.ts 的结构与语义启动闸门与环境“纵深防御”默认值scripts/dast/target.ts 是扫描目标的实现结构非常清晰闸门缺少PLANNOTATOR_DAST_ISOLATED1立即抛错L9-L14纵深防御环境变量L20-L35这些是针对测试目标的加固默认值真正的出站边界是 Docker 内网应用层配置则是第二道防线——即使某个功能被意外触达也无法调用 Agent、分享内容、持久化评审数据、打开浏览器或安装可选运行时Object.assign(process.env, { PLANNOTATOR_REMOTE: 0, PLANNOTATOR_PORT: String(APP_PORT), PLANNOTATOR_AI: disabled, PLANNOTATOR_SHARE: disabled, PLANNOTATOR_JINA: 0, PLANNOTATOR_ANNOTATE_HISTORY: 0, PLANNOTATOR_GUIDE_HISTORY: 0, PLANNOTATOR_TODO_PROVIDER: off, PLANNOTATOR_GLIMPSE: 0, PLANNOTATOR_SKIP_BROWSER_OPEN: 1, PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL: 1, PLANNOTATOR_SKIP_SEM_INSTALL: 1, PLANNOTATOR_FILE_BROWSER_MAX_FILES: 64, BROWSER: true, });这些变量在 packages/shared/config.ts 中都有对应的解析逻辑例如PLANNOTATOR_AI对应resolveAIEnabled值为disabled时阻止 provider 运行时初始化并隐藏对应 UI见 config.tsPLANNOTATOR_SHARE对应resolveSharingEnabled隐藏全部分享 UI见 config.tsPLANNOTATOR_ANNOTATE_HISTORY对应resolveAnnotateHistory值为1或true时启用见 config.ts。启动真实 annotate 服务器目标调用packages/server/annotate.ts导出的startAnnotateServer启动真实的应用服务器target.ts注入一段无任何凭据与用户数据的纯文本 fixture 文档dast-fixture.md并指定mode: annotate-last、origin: claude-code、sharingEnabled: false、gate: false、project: dast-fixture。这样 ZAP 被动规则检查的是真实 Plannotator UI 与 API 响应而不是一个玩具桩。只读路由守卫scanTarget真正的设计亮点是守卫层一个基于Bun.serve的反向代理式只读守卫target.tsconst forwardedPaths new Set([ /, /api/plan, /api/ai/capabilities, /api/definitely-missing, ]); const readOnlyMethods new Set([GET, HEAD]);守卫只放行GET/HEAD且路径命中上述四个白名单的请求并代理到127.0.0.1:APP_PORTredirect: manual响应头附加X-Plannotator-DAST-Guard: forwarded其他任何路径或非只读方法一律返回小体积响应GET/HEAD 返回 404其他方法返回 405并带X-Plannotator-DAST-Guard: blocked头。设计动因写得很清楚ZAP 基线蜘蛛会请求/robots.txt、/sitemap.xml等爬虫元数据路径应用的浏览器回退browser fallback会为这些路径正确返回 SPA但把 20 MiB 的 bundle 喂给静态蜘蛛既浪费又误导。同时守卫使被动工作流不可能触达任何状态变更的应用端点——这正是“被动”语义的强制保证。探测器健康哨兵sentinel目标还同时启动一个哨兵服务target.ts监听19433端口/healthz返回纯文本ok其余路径返回一个刻意缺失防点击劫持头的简单 HTML 页面。其作用是验证“扫描器本身工作正常”——该页面应当触发 ZAP 规则 10020anti-clickjacking header missing。它独立于应用报告永不进入产品构建属于扫描可信性detector health控制。ZAP 扫描钩子种子路由、规则裁剪与蜘蛛收敛扫描通过--hook /zap/hooks.py挂载 scripts/dast/zap-hooks.py包含三个确定性钩子zap_access_target种子真实只读路由for path in (, /api/plan, /api/ai/capabilities, /api/definitely-missing): response zap.urlopen(target.rstrip(/) path) if response.startswith(ZAP Error): raise RuntimeError(fZAP failed to seed {path or /}: {response})它通过 ZAP 的urlopen主动访问四个路由让被动规则检查 SPA 首页、plan JSON、AI 能力响应disabled-AI 形态以及未知 API 的 404 响应——全程不调用任何状态变更端点。种子失败返回ZAP Error会直接令扫描失败。zap_tuned裁剪两条规则zap.pscan.disable_scanners(10003,10109)规则 10003Vulnerable JS Library依赖 CVE 的检测职责由 Trivy仓库依赖与 Grype发布依赖负责ZAP 重复检测既冗余更重要的是该规则试图把 20 MiB 的单文件 bundle 整体存入 alert 证据超过 ZAP 的 alert 列上限会把冗余检查变成引擎错误规则 10109Modern App Detection同样因证据体积原因被禁用它属于 informational 级别不是漏洞判定。除这两条外其余全部被动 Web 规则仍然接收完整响应。zap_spider收敛传统蜘蛛if target.rstrip(/).endswith(:19432): return zap, target.rstrip(/) /api/definitely-missing return zap, target传统蜘蛛会把单文件 bundle 内部的字符串误判为链接因此把蜘蛛目标收敛到一个小体积、只读的 API 404 路径/api/definitely-missing而不是应用主页favicon 路径由 SPA 回退处理、会返回完整 bundle同样不适合蜘蛛。种子路由zap_access_target负责把真实 UI/API 响应送入被动扫描器将蜘蛛留在应用源上也保证探测器哨兵19433的发现不会混入应用报告。Browser/AJAX 爬取评估后推迟README 明确说明浏览器/AJAX 爬取被评估后有意推迟ZAP 固定镜像未携带 WebDriver而 internal 网络又正确阻止了运行时下载 WebDriver。这一取舍保证了扫描的确定性与离线性同时仍能被动检查真实 Plannotator UI 与 API 响应。大响应缓存调优32 MiB → 128 MiB 的派生模板Plannotator 的单文件 UI bundle 超过 20 MiB远超 ZAP 自带 HSQLDB 模板默认的32 MiB响应缓存一旦计入扫描元数据即超限。工作流在运行时从摘要固定镜像中提取该模板只修改缓存上限后只读挂载dast.ymldocker run --rm --network none --entrypoint cat $ZAP_IMAGE /zap/db/zapdb.script security-results/zap/zapdb.script grep -c ^SET FILES CACHE SIZE 32000$ security-results/zap/zapdb.script # 必须恰好匹配 1 次 sed -i s/^SET FILES CACHE SIZE 32000$/SET FILES CACHE SIZE 131072/ security-results/zap/zapdb.script grep -qx SET FILES CACHE SIZE 131072 security-results/zap/zapdb.script chmod 444 security-results/zap/zapdb.script流程为提取模板 →验证预期设置存在grep -c必须等于 1防止镜像升级后设置漂移导致 sed 落空→ 仅修改缓存为 128 MiB131072KiB→ 校验修改结果 → 只读挂载chmod 444。扫描参数中同时通过-z -Xmx2048m ... -config database.response.bodysize33554432将响应体上限配置为 32 MiB。校验器validator对缓存/磁盘耗尽可能fail-closed如果 ZAP 报告缓存耗尽Data cache size limit is reached、磁盘耗尽No space left on device或响应体截断greater than the configured response body length工作流都会失败详见下文报告校验。两次扫描探测器健康扫描 应用扫描工作流在同一 internal 网络上先后执行两次zap-baseline.py第一次哨兵扫描detector health目标http://plannotator-dast-target:19433dast.yml参数-m 0 -T 5产出sentinel.json/sentinel.html/sentinel.md用于验证 ZAP 探测器能正常发现受控 fixture规则 10020。第二次应用扫描目标http://plannotator-dast-target:19432dast.yml参数-m 1 -T 10产出plannotator.json/plannotator.html/plannotator.md。两次扫描共享以下加固与调优参数dast.yml--read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 512 --tmpfs /home/zap/.ZAP /home/zap/.cache /home/zap/.config /home/zap/.mozilla按用途分配 64M~1G 不等 --volume security-results/zap:/zap/wrk:rw --volume zapdb.script:/zap/db/zapdb.script:ro --volume zap-hooks.py:/zap/hooks.py:ro zap-baseline.py --autooff -t target -m 1|0 -T 10|5 -I -z -Xmx1024m|2048m -silent -config database.response.bodysize33554432 --hook /zap/hooks.py -J json -r html -w md -l WARN关键参数说明--autooff关闭主动扫描仅保留被动规则-m 1分钟级最大爬取耗时应用扫描 1 分钟哨兵扫描 0-T 5/10超时秒-I跳过该 URL 的 spider-l WARN日志级别-J/-r/-w分别输出 JSON、HTML、Markdown 三种报告。扫描退出码按 ZAP 惯例处理0表示无告警、1表示有告警、2表示异常退出码 ≥ 3 视为扫描器自身失败直接使步骤失败。两次扫描的输出分别重命名为sentinel-zap.log与plannotator-zap.log保留。报告证据校验fail-closed 的 validator扫描本身是“monitor-only”发现仅监控不阻断真正的门禁是扫描后运行的证据校验器 scripts/dast/report.ts。它以--name value形式接收十个必填参数report.ts在 dast.yml 中的实际调用为bun run scripts/dast/report.ts \ --report security-results/zap/plannotator.json \ --sentinel-report security-results/zap/sentinel.json \ --scan-log security-results/zap/plannotator.log \ --zap-log security-results/zap/plannotator-zap.log \ --sentinel-zap-log security-results/zap/sentinel-zap.log \ --minimum-urls 8 \ --expected-host plannotator-dast-target \ --sentinel-rule 10020 \ --summary security-results/zap/summary.md \ --evidence security-results/zap/evidence.json校验器执行以下检查任一项失败都会让工作流失败报告结构校验parseZapReport要求 JSON 可解析、顶层含site数组、每个 site 含alerts数组report.ts目标主机白名单校验assertExpectedHost要求报告必须包含期望主机plannotator-dast-target且不允许出现白名单之外的主机——任何越界主机都会抛错“outside the allowlist”report.ts从根本上防止扫描“悄悄打到别处”探测器健康校验assertAlert要求哨兵报告必须包含规则10020report.ts否则说明探测器失效结论不可信覆盖率下限从扫描日志解析Total of N URLs并取最大值要求 ≥--minimum-urls8同时校验该数字是安全正整数report.ts——防止空覆盖率的“假通过”引擎健康日志assertHealthyZapLog用正则扫描引擎日志命中以下任一模式即失败report.tsERROR、OutOfMemoryError、响应体超限、ZAP 启动失败、无 URL、ClientSpiderTask失败、数据缓存上限、磁盘无空间风险汇总summarizeAlerts按riskdesc首词将告警归类为 high/medium/low/informational/unknown生成 Markdown 摘要写入summary.mdreport.ts。校验通过后还会生成机器可读的evidence.jsonreport.ts记录 schema 版本、commitGITHUB_SHA、workflow runGITHUB_RUN_ID、扫描模式、目标形态disposable annotate session、outbound blocked、AI/sharing/persistence 均 disabled、扫描器版本与镜像、响应体上限33554432 字节、数据库缓存131072 KiB、覆盖情况、探测器健康状态、被禁用规则及其理由、告警汇总以及执行模式monitor-findings-fail-closed-health——即“发现仅监控健康失败即阻断”。这些校验逻辑均有对应的单元测试覆盖见 scripts/dast/report.test.ts包括目标主机/受控规则校验、白名单外主机拒绝、覆盖率不足拒绝、各类引擎错误模式拒绝、风险汇总正确性、畸形/空报告拒绝等。工作流在扫描前也会先运行bun test scripts/dast/report.test.ts确保校验器本身正确。证据保留、摘要输出与清理每次运行保留以下产物30 天actions/upload-artifact的retention-days: 30见 dast.yml人类可读的 HTML 报告、机器可读的 JSON 报告、Markdown 报告、目标/扫描器日志plannotator.log、plannotator-zap.log、sentinel.log、sentinel-zap.log、target.log以及小体积证据清单evidence.json、summary.md。if-no-files-found: error保证产物缺失也会失败summary.md被追加到$GITHUB_STEP_SUMMARY在 Actions 页面直接呈现“URLs crawled / Alert rules / High / Medium / Low / Informational / Unclassified / Enforcement / Target”汇总清理步骤在if: always()下执行无论扫描成败都抓取目标日志到target.log、强制删除目标容器并删除 internal 网络dast.yml确保隔离环境不留残余。监控优先的定位与安全边界总结该工作流的设计哲学可以归纳为三点发现监控、健康阻断扫描发现的告警本身不阻断发布monitor-only但“目标缺失、覆盖为空、报告畸形、扫描器失败、受控被动 fixture 未被检测到”等工程可信性问题一律 fail-closed——这是“扫描结果可信”与“扫描结果好坏”两个维度的严格分离隔离是物理边界应用配置是纵深防御internal 网络提供真正的出站隔离target.ts中的PLANNOTATOR_DAST_ISOLATED1闸门与全套 disabled 环境变量则是即使隔离失效也不至于泄露凭据或触达外部能力的第二、第三道防线确定性优先于覆盖广度通过种子路由、蜘蛛收敛、规则裁剪与离线决策推迟 Browser/AJAX 爬取在“确定性、离线、被动”约束下最大化真实 UI/API 的被动检查覆盖面。对安全工程实践者而言这套方案最值得借鉴的并非“跑一次 ZAP”而是如何在扫描前就通过镜像摘要固定、网络隔离、只读守卫、探测器哨兵把“扫描环境的可信性”变成可验证的事实再用 fail-closed 的证据校验器把这份事实固化进 CI 门禁。附本仓库相关的纵深资料——扫描钩子实现见 scripts/dast/zap-hooks.py目标服务器见 scripts/dast/target.ts证据校验器见 scripts/dast/report.ts 及其测试 scripts/dast/report.test.ts完整工作流见 .github/workflows/dast.yml。赞分享【免费下载链接】plannotatorAnnotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click.项目地址https://gitcode.com/gh_mirrors/pl/plannotator点击查看免费下载相关推荐Activepieces DAST 安全实践用 OWASP ZAP 对 Staging 环境做全量主动漏洞扫描Activepieces DAST 安全实践用 OWASP ZAP 对 Staging 环境做全量主动漏洞扫描 本文围绕 Activepieces 仓库中 .工作流自动化低代码AI 应用人工智能AI AgentMCP 服务后端前端DBeaver终极指南如何用免费工具统一管理100数据库系统DBeaver终极指南如何用免费工具统一管理100数据库系统 在当今数据驱动的时代数据库管理已成为开发者和数据分析师的日常必备技能。面对MySQL、Pos数据库客户端桌面应用数据库终极指南如何实现多设备同步zsh历史记录 - zsh-histdb数据库同步教程 终极指南如何实现多设备同步zsh历史记录 zsh histdb数据库同步教程 你是否厌倦了在不同电脑上重复输入相同的命令想要在所有设备上都能快速访问完开发工具上一篇ImmortalWrt 路由器容器指南从编译固件到跑起第一个服务的 5 步路径下一篇Temporal Python SDK与机器学习模型评估工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表