尧图精选

用pytest实现自动化渗透测试与安全指数记分法

🕒 发布时间:2026/9/28 17:42:33 📁 来源:尧图网络
1. 核心思路把渗透测试的报告变成能跑的代码做安全这行最烦的一件事就是问题复现基本靠缘分。半年前我手工测出一个管理后台未授权访问的漏洞开发当场修复我现场验证通过以为这事就翻篇了。结果三个月后做例行巡检同一个路径又冒了出来——因为新来的同事在配置里加了一行白名单开发完全不知道这里曾经出过事。这件事让我彻底想明白了一个道理渗透测试的产出如果只是一份报告那它就是一次性消耗品只有当它变成一个随时能重跑的自动化验证安全状态才是可持续的。于是我开始琢磨怎么把渗透测试向自动化测试框架靠拢。一开始我想的是写一大堆Python脚本挨个跑人工看输出。可跑完发现还是老问题——脚本输出的原始日志要靠人判断而且每个脚本风格不一样今天写的明天就忘了怎么维护。后来有个做测试开发的朋友点了我一句你不是要判断“有没有问题”吗那就用pytest它天生就是干这个的。把每个安全检查目标拆成子任务每个子任务的结果用断言表达断言过了就是安全不过就是风险。这句话让我一下子通了。pytest做这件事的优势太明显了。断言机制原生支持断言失败会明确告诉你哪个期望没满足fixture能处理环境初始化、登录态、数据清理插件生态里报告和CI集成都是现成的。最关键的是团队里的开发对pytest都不陌生他们看安全测试的代码就像看单元测试不需要额外的沟通成本。渗透测试本身也从“依赖高级工程师经验的黑盒操作”变成了“每个判断都有明确标准”的白盒验证。这正好对上了标题里那个“Google式记分法”。搜索、广告这类业务评判系统质量习惯把大问题拆成小指标每个指标按影响面配权重最后汇总成一个分数。以前这套思路大多用在可用性、性能考核上安全团队很少正面拿过来用。我的做法是划定安全基线的检查项每一项按严重性配分全部跑完后得到一个安全指数。这个指数比“报告里有几个高危”直观得多——高危漏洞会叠加而加权后的分数能直接反映整体态势是变好还是变坏。这套方案落地到现在我把它整理成一套可以照做的框架。接下来我会从拆子任务的方法论开始逐步讲到断言代码怎么写、记分机制怎么实现、整套流程怎么接进CI最后附上我踩过的坑。如果你也是做安全测试、安全开发或者DevSecOps的这篇内容可以直接抄作业。2. 拆子任务的方法论一个用例只验证一件事先讲清楚什么是断言。写过程序的人都知道assertpytest里的断言就是一句“我期望某件事是对的”。比如我写assert resp.status_code 404就代表我期望请求一个管理后台路径时系统返回“不存在”。如果实际返回了200断言就会失败pytest会把这句话标记为红并抛出自定义失败信息。这就是安全验证和普通脚本最大的区别脚本输出日志要你人肉看断言则直接给出结论和证据。但把“整个渗透测试”当成一个大断言来写是不可能落地的。一个系统有几百个检查面任何一个环节出错都会导致整个测试挂掉排错成本极高。这就是标题里“拆成子任务”的意义。我的经验是任何复杂测试第一步永远是拆。拆得越干净断言越简单用例越好维护。具体到安全场景我习惯按三个维度来拆。按检查对象切分。外部暴露面、Web应用层、中间件配置、基础网络、账号权限体系每一个对象都是独立的关注域。域名解析、证书有效期这种属于暴露面登录接口、会话管理属于Web应用层端口开放情况、SSH配置属于网络与系统层。对象不同验证手段和工具都不一样放在一个用例里只会互相拖累。按验证方式切分。有的任务只需要发一个HTTP请求看响应头和状态码比如检查安全响应头有的任务需要主动建立网络连接做协议协商比如TLS版本检查有的任务需要构造特定输入比如连续提交错误密码后看系统是否限流。验证方式不同断言的粒度也完全不同混在一起会让用例的逻辑变得不可读。按严重等级切分。这直接对应后面的记分机制。被外网直接可达的高危端口是一档网站缺少关键安全头是另一档登录接口没有验证码又是单独一档。每个子任务挂上自己的严重等级标记后面汇总分数时才能按权重计算。拆完之后每个子任务就变成独立的pytest用例函数名以test_开头函数内部是一段标准的“准备→执行→断言”三段式。我拿一个真实用例来展示这个结构# test_tls_config.py import socket import ssl import pytest pytest.mark.severity(critical) def test_tls_rejects_outdated_protocols(): TLS协议版本不得低于1.2 context ssl.create_default_context() with socket.create_connection((staging.example.com, 443), timeout10) as sock: with context.wrap_socket(sock, server_hostnamestaging.example.com) as tls_sock: version tls_sock.version() assert version TLSv1.2, f检测到被允许的过时协议: {version}这段代码做的事情很直接连上服务端的443端口完成TLS握手取回协商出的协议版本断言不低于1.2。如果服务端悄悄把TLSv1.0加回了支持列表断言就会失败报告里会直接写出“检测到被允许的过时协议: TLSv1.0”。安全工程师看到这个信息连日志都不用翻就能定位问题。关于拆子任务我有一个踩过坑之后的体会宁可拆细不要合并。我早期偷懒把安全响应头、TLS版本、Cookie属性三个检查点写进一个用例里结果一次CI中响应头检查挂了导致后面两个检查根本没执行日志里只有响应头的错误TLS和Cookie的真实状态被完全掩盖。后来我把它们拆成三条用例互不干扰跑挂了也能精确定位到具体是哪个安全属性出了问题。这个原则和单元测试完全一致一个用例只验证一件事尤其是安全测试这种失败信息越精确越有价值的场景。3. 手把手搭断言体系从HTTP头到TLS配置3.1 项目结构与基础fixture搞安全的项目不需要复杂的工程结构但目录还是要规整的。我用的是下面这种布局security_tests/ ├── conftest.py # fixture与全局配置 ├── pytest.ini # 标记与命令行参数配置 ├── requirements.txt # requests, pytest, pytest-html 等依赖 └── tests/ ├── test_http_headers.py ├── test_tls_config.py ├── test_authentication.py └── test_info_leak.pyconftest.py里我放了一些共享的fixture。最基础的是一个提供目标地址的fixture这样所有用例都从同一个地方拿环境信息切换测试环境时只改一处# conftest.py import os import pytest import requests pytest.fixture(scopesession) def base_url(): 被测系统的根地址优先从环境变量读取 return os.getenv(SEC_TARGET_URL, https://staging.example.com) pytest.fixture(scopesession) def authed_session(base_url): 带登录态的请求会话供依赖认证的用例使用 session requests.Session() login_url f{base_url}/api/login resp session.post( login_url, json{username: os.getenv(SEC_USER), password: os.getenv(SEC_PASS)}, timeout10, ) assert resp.status_code 200, 登录失败无法继续后续用例 return session使用scopesession是为了让整个测试过程只登录一次避免每个用例都重新登录产生大量脏数据。这里要特别提醒安全测试的用例之间往往需要共享一些状态比如登录Cookie但共享的范围尽量控制在“只读状态”上不要让一个用例修改了数据去影响另一个用例的判断。3.2 高频安全断言示例我整理了五类最常用、也最容易落地的安全断言每一类都给出可直接复制的代码骨架。第一类HTTP安全响应头检查。这类用例成本最低价值却很高适合作为整套框架的切入点。缺少Content-Security-Policy这类响应头往往意味着应用对XSS、点击劫持的防御不足import requests import pytest pytest.mark.severity(high) def test_security_headers_exist(base_url): required_headers { Content-Security-Policy: 未启用CSPXSS风险面扩大, X-Frame-Options: 未配置X-Frame-Options存在点击劫持风险, X-Content-Type-Options: 未配置X-Content-Type-Options浏览器可能嗅探内容类型, } resp requests.get(base_url, timeout10) for header, message in required_headers.items(): assert header in resp.headers, message第二类TLS与证书检查。除了前面提到的协议版本证书有效期、证书链完整性也是重点。这类检查用ssl标准库就能做不需要额外依赖。第三类敏感路径与文件泄露检查。我习惯把.git/config、.env、backup.zip、WEB-INF/web.xml这类路径做成一张清单逐一请求期望返回值是403或者404。如果返回200说明有信息泄露风险pytest.mark.severity(high) def test_common_sensitive_paths_not_exposed(base_url): sensitive_paths [/.git/config, /.env, /backup.zip, /WEB-INF/web.xml] for path in sensitive_paths: resp requests.get(base_url path, timeout10, allow_redirectsFalse) assert resp.status_code in (403, 404), f{path} 返回了 {resp.status_code}**第四类身份认证与会话管理检查。**比如登录接口多次失败后是否触发限流这是防暴力破解的底线能力pytest.mark.severity(high) def test_login_rate_limit_works(base_url): session requests.Session() for _ in range(5): session.post( f{base_url}/api/login, json{username: admin, password: wrong-password}, timeout10, ) final_resp session.post( f{base_url}/api/login, json{username: admin, password: wrong-password}, timeout10, ) assert final_resp.status_code 429 or captcha in final_resp.text, 连续多次错误登录后仍未触发限流第五类会话Cookie属性检查。Cookie是否设置了Secure、HttpOnly这类标志直接关系到会话被截获后能否被利用。做接口测试的朋友应该很熟JMeter里那个Beanshell断言其实原理一样对请求结果做逻辑判断。但pytest的断言是直接用Python语法比Beanshell简洁得多也更方便和第三方库集成pytest.mark.severity(medium) def test_session_cookie_has_secure_flag(authed_session): for cookie in authed_session.cookies: if cookie.name in (JSESSIONID, SESSION): assert cookie.secure, 会话Cookie未设置Secure标志存在被明文传输窃取的风险3.3 用例之间如何避免互相踩踏安全测试比普通单元测试更容易出现“用例互相影响”的问题因为很多用例操作的是同一个系统。我总结了几条纪律。**每条用例尽量使用独立的数据样本。**比如测试信息泄露时请求的路径不要和测试业务功能的用例共用一套数据否则业务逻辑把路径重写了安全用例会莫名失败。**不要依赖用例执行顺序。**pytest默认按文件名字母序执行但我不建议用例之间有任何顺序依赖。如果一个用例要依赖另一个用例的某个状态说明拆分得还不够干净应该把那个状态提取到fixture里统一管理。**对失败要有容忍机制。**安全用例经常要访问外部服务限流、超时这些情况很常见。我习惯在发起请求时统一加timeout参数对偶发的网络抖动做一次轻量重试避免测试本身的不稳定掩盖真实的安全问题。重试逻辑放在fixture里统一处理比散落在每个用例里好维护得多。4. 记分法实现让安全指数变成CI里的硬门槛4.1 严重等级与权重的映射安全测试最怕的就是“报告写出来没人看”。几十页PDF堆到负责人桌上他能扫一眼结论就算不错了。要让上层真正关注安全状态最简单有效的手段就是把结果变成数字。我参考业内通用的严重等级把每个子任务挂到一个权重分上严重等级权重分典型场景critical5.0未授权访问、核心数据可被外部直接读取high3.0敏感路径泄露、缺少安全响应头、无登录限流medium1.0会话Cookie属性缺失、非关键服务暴露low0.5证书使用习惯不佳、冗余信息提示各个用例通过装饰器挂上自己的等级标记也就是前面代码里那个pytest.mark.severity(high)。如果没有这个等级标记我默认按medium算避免有人忘了标记导致权重失真。这里有个设计点值得解释为什么权重分不用线性关系我一开始用的是每个等级一个用例算一分结果发现高危项和中危项在总分里的影响力完全一样——一个系统把高危漏洞全堵上了但一堆中低危项不过安全指数照样拉胯这不符合实际的风险判断。加权之后高危项占据主导地位但中低危项达到一定数量也能让指数明显下降模拟了漏洞风险叠加的效应。4.2 在pytest中统计分数的实现记分逻辑我放在conftest.py里通过pytest的钩子来采集每个用例的结果。做法是在pytest_runtest_makereport钩子中记录每个用例的最终状态和等级标记然后在pytest_sessionfinish钩子里做汇总# conftest.py import pytest from collections import defaultdict SEVERITY_SCORE {critical: 5.0, high: 3.0, medium: 1.0, low: 0.5} TARGET_INDEX 80 # 安全指数目标值低于此值则构建失败 test_records [] pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call: marker item.get_closest_marker(severity) severity marker.args[0] if marker else medium test_records.append({ nodeid: item.nodeid, severity: severity, outcome: report.outcome, }) pytest.hookimpl(tryfirstTrue) def pytest_sessionfinish(session, exitstatus): total_score 0 passed_score 0 for record in test_records: score SEVERITY_SCORE.get(record[severity], 1.0) total_score score if record[outcome] passed: passed_score score if total_score 0: return security_index passed_score / total_score * 100 print(f\n安全指数: {security_index:.1f}% ({passed_score:.1f} / {total_score:.1f} 分)) print(f判定阈值: {TARGET_INDEX}%) if security_index TARGET_INDEX: session.exitstatus 1 print(安全指数未达阈值CI应当阻断本次发版)这套逻辑跑起来的效果是测试执行完毕后终端直接输出安全指数同时如果指数低于80进程退出码为非零CI管道在这个节点就会红掉。配合pytest-html或者pytest-json-report这样的插件还可以生成一份带用例明细的报告开发直接用HTML报告就能看到具体是哪个用例挂了。这里有个我在实际项目中调整过的细节不是所有失败都要阻断CI。高危和严重级用例挂了必须阻断中低危用例挂了我一般只记录告警不阻断。实现方式是再加一个装饰器参数比如pytest.mark.blocker(True)在汇总时如果失败用例全部是低危就让CI以“不稳定”状态通过。否则你会发现每周要处理大量琐碎告警团队很快会对这套体系脱敏。5. 实战实录一次完整的安全回归是怎样跑通的5.1 被测系统背景与测试范围讲理论不如看一次实际操作。我用最近接手的一个内部管理系统为例它是跑在Kubernetes集群里的Spring Boot应用前端是Vue认证走JWT登录接口在/api/login。团队要求在发版流程里加一道安全门槛我来设计这套安全回归。首先和开发、运维对齐测试范围定出二十个项目作为第一版安全基线。我按前面说的三个维度拆完用例清单大致是这样暴露面检查443端口证书有效期、TLS最低版本、HTTP是否自动跳转HTTPSWeb应用检查9个安全响应头是否齐全、常见敏感路径是否返回404、登录接口是否限流认证检查JWT是否使用安全的签名算法、会话是否超时、密码策略是否生效信息泄露检查错误页是否透出堆栈信息、源码仓库是否可被访问、API文档是否公开展示测试环境用独立的staging环境地址已经从fixture中抽取好命令行执行cd security_tests SEC_TARGET_URLhttps://staging.internal.example.com \ SEC_USERautotester \ SEC_PASS****** \ pytest -v --tbshort --htmlreport.html执行过程没出什么意外大概三分钟跑完全部用例总结果被打在了屏幕上同时生成了HTML报告。5.2 执行结果与问题定位跑完以后二十七条用例里有四条失败。汇总出来的安全指数是74.3%没有达到我们预设的80%目标线CI已经被标红。逐一查看失败用例定位如下**失败一X-Frame-Options响应头缺失。**这个是新增的引流页面没有继承公共安全头配置导致的典型的“历史遗留问题”。我顺着失败用例的nodeid找到对应的nginx配置片段发现新路由单独挂了一套location配置没有带上全局安全头。这个问题的严重性在于页面可以被其他站点通过iframe嵌入配合诱导点击会放大钓鱼风险。**失败二TLS仍然允许TLSv1.1。**系统在做TLS终止的入口网关处配置里明确写死了最低版本是TLSv1.2但测试连上去还是协商出了1.1。排查发现网关前面还有一层负载均衡器它的默认配置没改把低版本协议兜底放行了。这种双层架构下的配置遗漏纯靠手工测试很难发现自动化断言反而逼着我们把每一层都查了一遍。**失败三登录接口没有触发限流。**连续提交六次错误密码后接口依然返回200这违反了“密码错误达到五次必须限流”的安全基线。开发确认是今年新上的多节点架构里限流计数器存在单节点的内存里请求被负载均衡分发到不同节点时每个节点的计数器都只有一两次永远触发不了阈值。**失败四错误页泄露了Spring Boot默认堆栈信息。**访问一个不存在的路径时返回体里带出了框架名称和异常类名。这种信息泄露属于低危但会给攻击者提供框架指纹后续定向利用会更容易。这四个问题里三个是最近三个月内引入的只有一个是半年前就存在的隐患。换句话说如果没有这套自动化回归这四个问题大概率又要等到下一次渗透测试周期才能被捞出来中间几个版本等于裸奔。5.3 修复验证与门槛生效问题定位清楚以后推动修复就顺理成章了。nginx配置补上公共安全头负载均衡器的TLS策略同步成和网关一致登录限流改为使用Redis存储计数错误页统一返回通用模板。开发修改完提交代码后CI里再跑一次同一套用例四条失败全部变绿安全指数升到了97.8%。这个案例里我认为最值得强调的不是发现了多少漏洞而是整个过程中责任边界变得非常清晰。在没有自动化断言之前安全工程师说“这里有问题”开发往往要花时间复现、确认现在断言直接把失败信息和期望值打印在CI日志里开发自己看一眼就知道怎么改。安全测试从一种对抗性的负担变成了团队协作中的自动化监督。那次之后我在pytest.ini里加了配置把安全测试接入到日常的夜间巡检任务中。每周一到五晚上定时跑一遍次日早上值班工程师查看报告。历史问题一旦被修复就被封进基线里以后每次回归都在守护这个结论。6. 常见问题与排查技巧实录这套流程跑了几个月踩了不少坑。我把最典型的几个问题整理成一份速查表应该能帮后来者省下不少时间现象可能原因处理方法本地跑通过CI里偶发失败外部服务超时或网络不稳定请求统一加timeout参数对瞬时故障做1-2次重试断言报错信息不具体无法定位断言没写自定义消息每个断言后面加字符串描述把实际值和期望值写清楚用例之间互相污染共享了可写的session状态提取共用状态到fixture确保用例间不依赖执行顺序在CI里收到一堆低危告警中低危项权重过低但数量多实现“仅高危阻断”策略低危只记录不阻断CI环境访问不到测试系统出口IP不在白名单把CI出口IP加入测试环境安全组或用独立测试账号测试数据变化导致偶发失败业务数据被其他流程修改固定测试样本使用专用账号执行前重置数据除了这张表我再分享几个让我少踩坑的长期习惯。**第一个习惯先从配置基线类用例入手。**这类用例不需要构造复杂业务场景一个HTTP请求就能验证开发改起来也快。第一批用例建议以响应头、TLS版本、敏感路径为主跑通整套流程建立信任之后再逐步加认证、授权这些业务逻辑更重的用例。一上来就想覆盖全部安全面大概率会因为排查成本太高而放弃。**第二个习惯失败信息一定要写到断言里。**不要嫌麻烦。assert header in resp.headers和assert header in resp.headers, 未找到X-Frame-Options响应头在天差地别。后者让开发打开CI日志就能看明白要修哪里前者还得让他们去翻响应报文。我们团队的标准是任何断言失败解说里必须包含“期望值实际值为什么重要”。**第三个习惯把安全用例的维护责任交给“最近的负责人”。**每个自动化用例都有对应的开发团队谁维护的服务挂了这个用例谁就要负责修复用例或者说明为什么当前策略要调整。安全团队只负责基线的定义和审核不负责日常维护。如果所有用例都压在安全团队身上这套体系是跑不起来的。我个人在实际操作中的体会是安全自动化的核心价值不在于工具链多高级而在于把“已知的安全知识”沉淀成可执行、可回归的断言。这套方法真正改变的不是测试效率本身而是让安全变成一个持续的、有量化标准的工程过程。每次发版前自动跑一遍等于每次都请了一个不会累、不会忘的初级安全测试员。后续我还在规划把容器镜像扫描结果、依赖库漏洞库比对结果也转成断言喂给pytest让这套框架覆盖的范围再扩大一些。方向很清晰继续做就是了。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →