最近在 arxiv 上翻预印本的时候,看到一篇讨论安全测试自动化的文章,核心思路是“把渗透测试拆成 pytest 能断言的子任务,再用 Google 式记分法做评估”。说实话,我第一反应是:这也太工程化了,渗透测试这种充满不确定性的活儿,怎么可能像单元测试一样,拿 assert 一锤子定生死?但等我认真读完整理完,发现这个思路其实非常值得聊,特别是对防守方的价值,比攻击方还要大。
简单说,这个思路就是把一次渗透测试从开始到结束切成若干个子任务,每个子任务用一条或几条 pytest 断言去判定“完成/未完成”,最后再把所有子任务按权重汇总成一张分。听起来像是一套“红队自动化评分系统”,但我自己实际搭建完才发现,它更接近一个“可自动回归的安全测试用例库”:蓝队可以拿同一套用例,在每一次安全改动后反复重跑,用分数变化来量化防护效果。这篇文章我就从项目拆解、设计原理、实际操作和坑位四个角度,把这一套完整讲清楚,适合正在做安全自动化、想给红蓝队工作流加量化指标的工程师,以及研究 LLM 安全智能体的朋友参考。
注意我说的是“参考”。我看到的那篇 arxiv 预印本更多是给了思路和框架,没有直接放完整代码,所以我根据自己的理解,在本地隔离环境里搭了一套可复现的 demo。下面所有内容都来自那套实验,所有命令和脚本都基于授权靶场,千万别拿到公网环境里乱跑,这是前提。
1. 项目起点:为什么安全测试需要变成“可断言”的
1.1 传统渗透测试报告的最大痛点:没法自动复验
我们先聊一个问题:一份渗透测试报告,交付之后到底怎么验收?大部分团队的现状是:乙方或者内部红队写一份 Word 或者 PDF,里面描述“在某台服务器上发现了信息泄露,利用该信息进一步拿到了某系统的权限,并成功执行了一条命令”。听起来没问题,但如果你是防守方,想验证这段话的真实性,或者想在下一次安全版本上线后复测,怎么办?你只能再找一次红队,或者自己手工照着报告步骤再打一次。如果报告里的描述不够精确,比如“拿到了权限”具体是拿到了 shell、拿到的数据库账号、还是拿到了某个管理后台的登录态,验收就成了一个靠猜的过程。
这种主观性在攻防里还好,毕竟红队是有人做判断的。但一旦你想把安全测试接入自动化流水线,比如每次发布前自动跑一轮“轻量级渗透回归”,传统报告模式就完全失效了。你没法让一条流水线去阅读 Word 文档,更没法让机器判断“报告说成功了,那就成功了吧”这种模糊表述。所以大家才开始思考:能不能把渗透测试的每个步骤都变成一个机器可判定的布尔值?行就是行,不行就是不行。这个想法,就是那篇 arxiv 文章的出发点。
1.2 pytest 为什么适合干这件事
可能有人会问:安全工具那么多,为什么偏偏用 pytest?我自己体验下来,pytest 的优势不是它有多强的安全能力,而是它天生适合做“目标状态验证”。渗透测试的每个子任务,本质上都是“去改变或者探测某个对象的状态,然后判断状态是否符合预期”。比如“目标主机的 8080 端口是否开放”“某个接口是否返回了包含特定标识的响应”“某个临时文件是否被写入到指定路径”——这些都非常自然地能被翻译成 assert。
pytest 又恰好有 fixture、依赖控制、HTML 报告、JUnit XML 导出这些能力,放在安全自动化里几乎不用额外开发。你需要做的只是把渗透测试的经验拆成一个个最小的可验证单元,剩下的事,pytest 当测试框架用就行。这比重新造一个“安全测试脚本引擎”要省太多功夫。
1.3 这套思路的本质:从“讲故事”到“证明状态可达”
我读完那篇预印本之后,感觉它最大的贡献不是技术实现,而是视角转换:传统渗透测试关注“攻击者如何一步步完成攻击”,而 pytest 子任务模型关注的是“某个攻击状态是否可达”。你不再需要描述“我们利用了一个命令执行漏洞”,你只需要定义一个子任务——“在授权靶机上构造一次 HTTP 请求,并将命令执行结果写入指定文件,然后断言该文件存在”。如果断言通过,就证明这个能力是存在的;如果断言失败,就说明当前防护措施可能堵住了这条路径。
这种视角对防守方尤其好用。因为防守方真正关心的不是“红队多牛”,而是“这条攻击路径现在到底通不通”。把“通不通”变成一个布尔值,所有后续的记分、趋势统计、回归验证就都能自动化了。
2. 核心设计:把攻击链拆成可“断言”的原子任务
2.1 拆任务的逻辑:什么才算一个“原子任务”
我第一次尝试拆解的时候,犯了一个典型错误:把“漏洞利用”当成一个子任务。后来发现这太粗了,因为“漏洞利用”本身由很多步组成,你没法用一个assert去表达“利用成功”。后来我重新定义:一个原子子任务,必须满足三个条件。
第一,它必须有一个清晰的前置条件。比如“探测到目标存在某个服务”才能进入下一层;第二,它必须有一个可观察的最终状态,也就是说这个状态能在目标系统或辅助系统里被检测到;第三,它必须能在一段相对独立的时间内完成,不能和其他子任务产生共享临时文件之类的隐式耦合。
举个具体例子:侦察阶段我不会定义“完成信息收集”这种大任务,而是拆成“从公网 IP 可以访问 TCP 8080 端口”“HTTP 服务返回的 Server 头包含特定特征”“某个登录接口存在并返回 200”这样三个子任务。每个子任务都是一个最小断言单位,跑完就知道“这是什么状态的靶机”,而不是“我感觉我收集到了信息”。
2.2 用生活类比理解“断言”
这个设计对不常写测试的人可能有点抽象,我用一个生活类比解释:假设你要验证“一道菜熟了”。传统渗透测试的方法是拍个照、写段描述:“看起来肉质变色,应该熟了。”这是讲故事。而 pytest 子任务模型是:“切一块肉,插入温度计,断言中心温度 ≥ 75 摄氏度;观察切面,断言没有血水渗出。”一句话,就是找到一个可以被客观观测的指标来代表“熟了”。
渗透测试也一样。不要断言“攻击成功了”,而要断言“攻击产生的痕迹/状态出现了”。比如攻击导致了一个特定文件被创建,或者目标进程名单里出现了一个特定进程名,或者某个外带数据回连到了一个你控制的监听端口。只要这些状态能被检测到,断言就成立。这样,整条攻击链就从“人的叙事”变成了一串“机器可验证的布尔运算”。
2.3 子任务的数据结构
为了管理这些任务,我给每个子任务设计了一个简单的描述结构,没有用太复杂的格式:
- 任务ID:
recon_port_8080 - 前置条件:
none(第一个任务) - 操作:向目标IP发起一次 TCP 连接测试
- 预期可观察状态:
tcpport8080 is open - 权重:
1
这里为什么要强调“预期可观察状态”而不是“预期命令输出”?因为真实攻击链路里,命令输出可能被终端宽度、语言环境、转义字符影响,而状态(文件存在、端口开放、特定标记出现在响应体)更稳定,更容易被机器跨平台判定。后面前置条件变成recon_port_8080,这样整个攻击链就形成一个依赖图:前驱任务断言失败,后继任务自动跳过并记 0 分,避免产生“基础没打好但后面硬跑”的混乱。这也是后面能自动化记分的关键。
2.4 用 pytest 写一个最小可运行的断言
光说没用,直接看代码。下面这个子任务是最简单的“侦察”任务,它的目标是确认目标主机的 8080 端口是开放的:
import socket import pytest @pytest.mark.dependency(name="recon_port_8080") def test_recon_port_8080_is_open(): host = "127.0.0.1" # 这里替换成你的靶机地址 port = 8080 timeout = 3 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: result = sock.connect_ex((host, port)) assert result == 0, f"port {port} is not open" finally: sock.close()注意我用connect_ex而不是connect,这样即使端口不通,进程也不会抛异常,而是返回一个非零的错误码,方便断言。这个细节虽然小,但在自动化回归里很重要:如果直接connect,一旦端口不通,测试会以异常结束,日志里的信息就没有那么干净了。
再举一个带请求特征的例子。假设靶机上有一个登录接口,我们要断言这个接口能返回预期的 JSON 结构:
import requests import pytest @pytest.mark.dependency(depends=["recon_port_8080"]) def test_login_api_returns_challenge(): resp = requests.get("http://127.0.0.1:8080/api/login", timeout=5) assert resp.status_code == 200 assert "challenge" in resp.text这里我通过pytest.mark.dependency声明了它依赖recon_port_8080。如果端口扫描没通过,这个用例会被自动 skip,后续权重计算时按 0 分处理,不会产生虚假的“接口不可访问却报错”噪音。
2.5 依赖关系与顺序编排
渗透测试和普通单元测试最大的不同,就是它的子任务之间大多有严格顺序。你不能在还没扫到服务之前就去测漏洞利用。pytest 默认并不会保证用例执行顺序,所以你需要在任务定义里显式声明依赖关系。我用的是pytest-dependency插件,整体非常轻。
实际项目里我会把所有子任务抽象成三层。第一层是侦察层,只做端口和协议探测;第二层是探测层,针对已知服务做请求特征判断;第三层是利用层,只有前两层全部通过才允许执行。每一层内部不互相依赖,但层与层之间通过depends串联。这样就算某个环节失败,我依然能从报告里看到失败的具体环节,而不是一整个“渗透测试失败”的模糊结论。
3. “Google 式记分法”是怎么轮到防守方用的
3.1 我理解的“Google 式记分法”
标题里那个“Google 式记分法”可能会让不少人一头雾水,我第一次看到也觉得这个名词有点唬人。按我的理解,它指的是 Google 在大型工程中常用来管理复杂目标的思路:把一个大目标拆成若干个可衡量的子项,每个子项配一个权重,用加权总分来反映整体完成度。有点像你在做一个季度目标时,把“提升系统稳定性”拆成“可用性达标、CPU 水位下降、告警数量降低”这三项,每项占不同的百分比。安全测试也可以完全照搬。
另一层“Google 式”的含义,是像 Google VRP(漏洞奖励计划)那样,把漏洞的重要程度和分值挂钩。每个子任务不应该是平均用力,而是按这条子任务在攻击链中的关键程度、利用难度、目标资产的重要性来动态调整分值。所以“Google 式记分法”本质上是“按业务风险加权计分”,不是一个固定公式,而是一种思路。
3.2 分值体系怎么设计
设计记分规则时,我一开始拼凑了很多复杂参数,比如“任务复杂度系数”“环境随机性系数”,结果算出来的分数连自己都解释不清楚。后来退回到最简单的一版:每个子任务有一个基础分,再乘以一个“关键程度系数”,再根据该子任务是否依赖前置任务,决定是否做衰减。
举个例子:
| 子任务ID | 基础分 | 关键程度系数 | 最高得分 |
|---|---|---|---|
| recon_port_8080 | 1 | 1.0 | 1 |
| detect_login_api | 2 | 1.2 | 2.4 |
| exec_marker_insert | 5 | 2.0 | 10 |
| cleanup_artifacts | 2 | 0.8 | 1.6 |
关键程度系数的设定逻辑是:能拿到最终结果的步骤(比如往目标系统写入一个标记)系数最高,因为它直接证明攻击链路是通的;而一些清理性、临时性步骤系数低一些,防止团队为了刷分去做一些“看起来做了很多”但实际上不关键的操作。总得分就是所有通过子任务的分值之和。没有通过的任务,记 0 分,并且其后续依赖任务也自动 0 分。
这套评分逻辑和 Google 内部管理目标的思路非常像:只衡量客观结果,不衡量“你为此付出了多少努力”。放在攻防场景里,就是只关心你实际打通了哪些环节,不关心你跑了几百个扫描器、调了几个小时参数。
3.3 防守方为什么更受益
如果只把这套东西当成红队自动化工具,那格局就小了。我自己在蓝队场景里试了一轮,发现它的价值主要在三个地方。
第一,防护改动能被量化。过去蓝队上个 WAF、调个策略、加一条访问控制,到下一次渗透测试才能知道有没有用。现在你只需要把红队用过的所有 pytest 子任务重新跑一遍,看哪些原本能通过的用例现在失败了,哪些分数降了。每一条失败都是一个明确的信号:“这条攻击路径被堵住了。”这对做安全加固的同事来说,比“我觉得应该没问题了”有说服力得多。
第二,它天然适合做回归。一个复杂系统的安全防护,最怕的就是改一处、挂一处。比如你为了防一个注入点,在网关里加了一条规则,结果把另一个正常业务接口也误杀了。传统做法靠人工回归测试,范围总是有限。换成 pytest 子任务库之后,所有已知攻击路径都变成了一个可以随时重跑的回归集,每次安全变更后本地跑一遍,马上能看到是否引入新破绽或者误伤。
第三,它能生成时间维度上的安全趋势。同样的十道“渗透测试题”,月初跑一次、月末跑一次、安全整改后再跑一次,三次的得分差异就是防护能力的变动情况。虽然不能完全代表真实攻防能力,但作为内部安全质量的量化指标,它比拍脑袋的“安全感”靠谱太多了。
3.4 接入 CI/CD 的安全回归管道
既然能跑 pytest,接进 CI/CD 就是很自然的事。我在实验环境里就是这么做的:安全团队维护一份“攻击断言集”,每次核心服务编排变更后,触发一个安全回归 Job,跑完自动出 JUnit XML,再用一个记分脚本解析 XML 计算总分。总分低于某个阈值直接打断发布流水线,或者至少向负责人发送告警。
这个流程对“高频发布、安全人力不足”的团队尤其友好。你不用每次发布都找一个渗透测试工程师手工看,机器先帮你把“已知攻击路径是否回归”这一层筛掉,剩下真正需要人类发挥创造力的未知问题,再交给红队专家去补。这也是我认为“Google 式记分法轮到防守方用了”的真正含义——防守方终于拿到了一件可以量化和反复验证的武器。
4. 实操:在隔离靶场上跑通整套流程
4.1 准备一个可重复使用的隔离环境
正式动手前,我先说一下合规前提:以下所有命令和脚本我都在本地 Docker 靶机环境里执行,目标是 127.0.0.1 的模拟服务,没有任何真实公网 IP、域名或系统。如果你要在公司或者客户环境复现,请务必确认已经获得书面授权,并且全程在隔离网络中操作。
我的实验环境非常简单。靶机是一个带模拟登录接口的容器,我把它映射到本机的 8080 端口;测试端就是同一台电脑上的 Python 虚拟环境。先装工具:
python3 -m venv sec-test-env source sec-test-env/bin/activate pip install pytest pytest-dependency pytest-html requests这里pytest-html用来生成可视化报告,pytest-dependency用来管理任务依赖。我没用额外扫描器,因为这篇文章重心是框架,不是工具链。
4.2 写一个侦察层子任务
先写第一个测试文件test_recon.py,内容就是上面提到的最小例子,但我会扩展成同时检查 8080 和 22 两个端口。实际经验是:侦察层不要只检查一个端口,因为在真实环境里,一个服务端口可能因为负载均衡策略变化而临时不可达,多一个备选信号能降低误报率。
import socket import pytest TARGET_HOST = "127.0.0.1" TARGET_PORTS = [8080, 22] @pytest.mark.dependency(name="recon_ports") def test_recon_target_ports_open(): open_ports = [] for port in TARGET_PORTS: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) result = sock.connect_ex((TARGET_HOST, port)) if result == 0: open_ports.append(port) sock.close() assert 8080 in open_ports, "web service port is not reachable"注意我没有要求所有端口都开放,只要求关键端口 8080 可达,这样即使 22 端口被防火墙挡掉,也不会影响后续 Web 类测试。
4.3 写一个探测层子任务
第二层我要验证登录接口的存在和响应结构。这里有一个关键细节:断言不要只写在“接口返回了 200”,因为很多 Web 框架对所有路径都返回 200,但内容是 404 页面。所以我会同时断言响应体里包含一个稳定的字段名,比如challenge或nonce。这比断言版本号、版权字符串要稳得多,因为那些太容易被改。
import requests import pytest @pytest.mark.dependency(name="detect_login_api", depends=["recon_ports"]) def test_detect_login_api(): resp = requests.get( "http://127.0.0.1:8080/api/login", timeout=5, headers={"User-Agent": "security-regression-check"} ) assert resp.status_code == 200 assert "challenge" in resp.text, "login api challenge field missing"这里我用了自定义 User-Agent,是为了在靶机日志里留下一个标志,配合后续“标记插入”类断言使用。你在真实环境里做安全测试时,也应该用一个唯一的请求头来标识自己的流量,方便事后审计和清洗日志。
4.4 写一个“标记插入”利用层子任务
利用层听起来吓人,但在授权靶场里其实可以设计成多次无害请求。我的实验设计是:向靶机上的一个模拟“导出文件”接口发送带有构造参数的请求,该接口会在容器内部生成一个包含指定标记的文件。然后断言我能从测试机侧读到这个文件。这个过程没有任何破坏性,但它证明了整条“请求可以从外部影响到内部状态变化”的链路是打通的。
import requests import pytest MARKER = "sec-regression-marker-7f3a" @pytest.mark.dependency(name="exec_marker_insert", depends=["detect_login_api"]) def test_exec_marker_insert(): params = {"marker": MARKER} resp = requests.get( "http://127.0.0.1:8080/api/export", params=params, timeout=5, headers={"User-Agent": "security-regression-check"} ) assert resp.status_code == 200 assert MARKER in resp.text这条断言没有去检查“命令是否执行成功”,而是直接检查“我想要的标记是否出现在响应里”。这就是前面说的“状态可达”:不管底层是命令执行、文件读取还是日志回显,只要那个可观察状态出现了,任务就算完成;如果没出现,说明某层防护拦截了它。这个设计的好处是,当底层漏洞被修复但返回结构变化时,这个断言依然能准确反映链路可达性。
4.5 写一个记分脚本
跑完 pytest 只是第一步,真正要输出“Google 式记分法”下的总分,还需要解析结果。我用 pytest 的 JUnit XML 导出,然后写一个小脚本按任务权重汇总分数。这里先看一眼工作区里的 pytest 命令:
pytest -v --junitxml=security-regression.xml --html=security-report.html然后是一个简单的计分脚本片段:
import xml.etree.ElementTree as ET WEIGHTS = { "recon_ports": 1, "detect_login_api": 2, "exec_marker_insert": 10, } tree = ET.parse("security-regression.xml") total = 0 earned = 0 for testcase in tree.getroot().iter("testcase"): name = testcase.get("name") failure = testcase.find("failure") skipped = testcase.find("skipped") weight = WEIGHTS.get(name, 1) total += weight if failure is None and skipped is None: earned += weight print(f"score = {earned}/{total}") print(f"coverage = {earned/total:.1%}")在这个脚本里,总分是所有已知攻击路径子任务权重的总和,实得分是全部通过任务权重的总和。如果一条路径因为前置任务失败而被跳过,它在junitxml里会被标成skipped,我们直接算 0 分,不区分“跳过”和“失败”。这也是刻意为之:对整个攻击链来说,没有达到预期状态和因为前置条件不足而没跑,结果都是“没有得分”。
4.6 看一次典型输出
我跑完一个小 demo 后,控制台输出大概长这样:
RECON: 1 passed DETECT: 1 passed EXEC: 1 passed然后是计分脚本的输出:
score = 13/13 coverage = 100.0%如果我把靶机的登录接口故意关掉再跑一次,输出会变成:
RECON: 1 passed DETECT: 1 failed EXEC: 1 skipped计分输出变成:
score = 1/13 coverage = 7.7%这个 7.7% 的数字不是在说“这台机器已经安全”,而是在说“我们已知的攻击路径回归覆盖里,当前有 7.7% 仍然能被验证为可达”。它对防守方的价值在于:你可以拿这个数字做趋势,拿变化率定位问题,拿失败明细去推动修复。
5. 常见问题与避坑实录
5.1 断言写得“太恰好”导致误报
我在最初版本里,很多断言都是直接检查响应里的版本号或者 banner,比如“Server 头包含 nginx/1.18.0”。结果靶机升级了一次镜像,nginx 版本变了,我所有基于版本号的子任务全部失败,但实际上安全链路并没有任何变化。后来我把这类断言改成检查“服务类型标识”,比如是否包含nginx关键字,版本细节不再纳入断言。原则是:断言要检测链路是否可达,而不是检测特定实现细节。除非你的子任务目的就是确认某个已知漏洞版本,否则不要把版本写死。
5.2 服务还没就绪就开跑的时序问题
自动化回归里最常见的失败就是“服务还没起来,测试就在断言端口”。解决方案有两个:一是在 pytest 里写一个 fixture,先循环等待目标端口可连接,超时后再抛AssertionError;二是给整个测试 Job 加一个固定的预热等待。推荐第一种,因为第二种在负载高的时候还是可能出现随机失败。我的习惯是写一个简单的wait_for_port工具函数,放到公共模块里,所有涉及网络连接的任务都先调用它。
5.3 子任务之间互相污染
这是安全自动化特有的大坑。同一个靶机上,如果前一个子任务创建了一个临时文件,后一个子任务恰好也要用到同一个路径,就会出现并发冲突。我的解决方式有两个:一是让每个子任务尽量“只读”,也就是只去探测状态,不去修改环境;二是如果确实需要修改,比如写入标记文件,就用一个带随机后缀的名字,并在测试结束时清理。实在清理不掉的,就为这个子任务单独开一个靶机快照,跑完直接回滚。宁可多花点资源,也不要让状态污染影响你的回归结果。
5.4 pytest 默认不保证执行顺序
我最早写这套子任务时没装pytest-dependency,结果 pytest 按文件名顺序执行,导致利用层在探测层之前跑,失败得很莫名其妙。后来我用pytest.mark.dependency把所有跨层依赖显式标了出来,这个问题才彻底解决。所以强烈建议:所有子任务只要涉及先后关系,就显式声明依赖,不要靠文件名排序或者运气。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 所有任务立即失败 | 靶机没启动或端口没映射 | 先用netstat或curl检查服务 |
| 探测层成功但利用层 skipped | 依赖标签写错 | 检查depends里的任务名是否完全一致 |
| 分数上下波动 | 网络超时或者服务重启 | 在 fixture 里加重试与轮询等待 |
| 报告里缺少某些用例 | 文件命名导致 pytest 未收集 | 测试文件必须以test_开头 |
| 靶机状态被污染 | 前序任务写入临时文件 | 使用随机资源名并清理 |
这些坑都是我在实际搭这套东西时踩过的。写代码本身不难,难得是让“安全测试断言”在工程上稳定、可重复。如果你把它当普通 pytest 写,服务一启动、状态一乱,所有分数都会失真,那这套体系也就失去意义了。
6. 往真实场景扩展的三条路子
6.1 把红队经验沉淀成“进攻用例库”
我现在的工作习惯是,每一次授权渗透测试结束,不只是交付报告,还会把红队过程中验证过的关键路径整理成标准子任务,纳入团队的 pytest 用例库。下次再遇到同类业务系统,我不用从头开始打,直接跑一轮回归,先看哪些已知路径仍然存在。这就好比单元测试用例库,安全测试也有了自己的“测试用例积累”。
这个思路的副作用是:用例库会慢慢变大,需要定期清理和维护。我的建议是每季度至少审视一遍,抛弃那些目标系统已经彻底下线或重构的用例,避免花大量时间在无意义的断言上。
6.2 给 LLM 安全智能体提供奖励信号
现在很多人都在研究用大模型自动做渗透测试。这类系统最大的问题之一就是缺少可判别的奖励信号。你可以让模型跑很多步,但它到底有没有“更接近成功”,很难定义。而“arxiv 那篇预印本”把渗透测试拆成 pytest 断言子任务的思路,天然就是给 LLM 设计的奖励函数:每通过一个子任务,就获得一个明确的分数反馈;每失败一个,就看到具体断言失败原因。这样智能体可以更有方向地调整下一步动作,而不是靠随机试探。
在我自己的一个小实验里,我甚至直接把 pytest 的输出文本丢给模型,让它根据“哪个断言失败”来自我反思下一步命令。效果还比较初步,但至少比完全黑盒地评估“渗透是否成功”要清晰得多。
6.3 与漏洞管理平台打通
最后一条路是把这套得分体系输出到漏洞管理或者安全运营平台。你可以在计分脚本里把每个失败子任务和它对应的资产标识、风险等级、关联漏洞编号一起生成 JSON,然后推送给工单系统或者安全大屏。这样安全团队每天看到的就不是一堆“有人又在乱打”的日志,而是一份有结构的“已知攻击面回归报告”。
要实现这一步不需要改核心逻辑,只需要在计分脚本里加一个输出模块。难点在于子任务和漏洞编号的映射关系要有人维护,这个工作没法完全自动化,我目前也是半手工维护一张映射表。
6.4 我实际用的一个小技巧
最后分享一个我踩了几次坑后沉淀下来的小经验:一开始设计权重表时,不要追求算法复杂。先用三档权重——普通侦察 1 分、关键路径 3 分、能证明权限影响的终态步骤 5 分——把整体流程跑顺,等到你积累了一定量数据,再根据实际命中率和防护优先级去调整系数。一上来就想搞一套完美加权公式,最后整个体系都容易被公式卡住,很难落地。
如果你现在正好在推进安全测试自动化,我觉得可以试着先拿一个最熟悉的靶场、选三个最大颗粒度的子任务、写一次 pytest 跑通再谈扩展。这套思路真正的门槛根本不是技术,而是你能不能把自己脑子里“这步算成功”的经验,翻译成一个稳定、可观测、不骗人的状态断言。做完这一步,后面的记分和回归就都是水到渠成的事了。