☰
Python依赖漏洞自动扫描工具实践:从手工排查到CI/CD持续监控
2026/9/29 3:07:50 网站建设 项目流程

1. 依赖漏洞为什么值得单独做一次自动化排查

1.1 被忽略的第三方依赖风险

说实话,大部分Python项目的安全问题,都不是业务代码写出来的,而是第三方依赖带进来的。你pip install一把梭的时候,可能根本不知道这个包背后引了哪些传递依赖,更不知道这些依赖的某个版本已经公开了CVE漏洞。

举一个非常典型的例子:log4j漏洞在国内Java圈炸锅的时候,很多人下意识觉得“这是Java的事,跟Python没关系”。但Python生态里同样出现过类似的供应链攻击事件,比如PyPI上被投毒的恶意包、某个知名库的旧版本被发现远程代码执行漏洞。区别只是Java的log4j被媒体放大报道了,Python这边更多是在安全公告库和漏洞数据库里悄悄更新。

这时候就需要一个能自动扫描依赖漏洞的工具。它的核心作用很简单:解析你项目里声明的所有依赖清单,逐个比对公开漏洞数据库,把“这个包当前版本存在漏洞、影响范围是什么、修复版本是什么”一次性列出来。说白了,就是给依赖做一次全面体检。

这个工具适合谁用?我觉得至少三类人:

  • 个人开发者:自己维护的开源项目或小工具,没人提醒你依赖过时了,全靠自觉。
  • 团队技术负责人:负责整个项目的依赖治理,需要定期安全巡检,还要能出报告。
  • 安全工程师:做上线前安全检查,或者接到“排查项目供应链风险”这类任务时,手里必须有一套趁手的自动化工具。

1.2 手工排查为什么走不通

有人可能会说,我直接去NVD(美国国家漏洞数据库)逐个CVE翻不就行了?我试过,真的不现实。

首先,你的项目里有多少个直接依赖?通常几十个。它们的传递依赖加起来呢?轻松上百个。每个依赖都去查一遍它是否受某个CVE影响,这个工作量已经不是靠人力能扛住的了。

其次,手工排查特别容易漏。NVD上每天新增几十上百条CVE记录,你不能每天都把所有依赖重新查一遍。往往是出事之后才回头翻,那时候已经晚了。

更重要的是,手工方式只能看到“某个版本有没有漏洞”,看不到“当前项目的依赖树里,哪个漏洞是真的可利用的”。有些CVE影响面很大,但你的代码根本没用到对应的危险函数;有些CVE影响范围很小,却恰好命中你的关键路径。这些判断都需要工具先帮我们把原始数据拉全,再结合实际情况分析。

所以说,自动化不是可选项,而是刚需。下面我先把主流方案拉出来对比,再讲我们自己搭建这套扫描工具的实际过程。

2. 主流扫描方案对比:我为什么选了这套组合

2.1 四类常见的依赖漏洞扫描器

在动手写代码之前,肯定要先看看社区里已经有什么现成的轮子。Python生态里主流的依赖漏洞扫描方案,我分成四类来对比:

方案扫描方式漏洞数据来源优点缺点
pip-audit本地安装环境或依赖清单PyPI JSON + OSV官方维护、使用简单、可做SBOM只能查Python生态
Safety依赖清单Safety DB(pyup.io)商业库数据全、更新快免费版有查询次数限制
OSV-Scanner锁定文件OSV漏洞数据库多语言生态、数据源开放需要项目有lockfile
Trivy镜像、文件系统、仓库多个数据源聚合覆盖面最广,不只是Python体量较重,偏容器场景

我实际用下来,简单项目用Safety查一下也够用,但免费版有次数限制,不适合做定时巡检。Trivy更偏向容器镜像和云原生资产扫描,如果只是查一个纯Python服务,引入Trivy有点大材小用。

2.2 我的选型结论:pip-audit + OSV-Scanner 组合

我最终确定的组合是:核心用pip-audit,辅助用OSV-Scanner,再加上一层自己的封装脚本。

选择pip-audit的主要理由是:

  • 它由Python打包权威机构PyPA维护,跟pip是同一个“家族”的项目,可信度高。
  • 支持从多种依赖来源扫描,包括本地site-packages、requirements.txt、pyproject.toml、Pipfile.lock等。
  • 支持输出JSON、SBOM等结构化格式,方便二次处理。
  • 默认对接OSV和PyPI的安全公告,数据源本身就是公开的。

OSV-Scanner则作为补充,专门处理那些“项目里只有lockfile格式、pip-audit解析不够友好”的场景。比如有些项目用Poetry管理依赖,虽然pip-audit也能读pyproject.toml,但锁定版本信息如果存在poetry.lock里,OSV-Scanner的解析更准。

不要纠结于“一定要找一个万能工具”。实际工程里,工具组合是常态。我的思路是:主扫描器负责大部分场景,辅助扫描器覆盖剩下的边角场景,再用自己写的脚本把两者的报告汇聚起来。

下面详细讲讲这个自动扫描工具的实现逻辑。

3. 扫描工具的核心实现逻辑

3.1 依赖解析:先搞清楚项目里到底装了哪些包

扫描的第一步,是拿到一份可靠的依赖列表。这一步看似简单,实际坑很多。

如果你的项目目录下有requirements.txt,先用pip-audit直接扫它。但要注意一个问题:requirements.txt里写的版本号有可能是不完整的。比如:

requests>=2.20.0 flask gunicorn==20.1.0

这里flask没有指定版本,requests指定的是下限。扫描器只能基于你声明的约束做判断,结果要么是“信息不足跳过”,要么是“按当前环境里实际安装的版本判断”。所以,要拿到准确结果,必须结合一个已经安装好依赖的环境来扫描,或者项目里有精确锁定版本的lockfile。

我一般分两条路径执行:

  • 如果项目里有poetry.lock、Pipfile.lock或uv.lock,优先用这些文件。因为lockfile里把每一个传递依赖的精确版本都锁定了,扫描结果最准确。
  • 如果只有requirements.txt且版本写得不严格,我会先创建一个干净的虚拟环境,执行pip install -r requirements.txt,然后直接扫描这个虚拟环境的site-packages。

用虚拟环境的原因是避免把开发机上的全局包混进来。曾经踩过坑:直接扫当前环境,结果把其他项目装的包也扫出来了,漏报倒没有,但报告里一堆跟当前项目无关的告警,干扰判断。

3.2 漏洞源对接与匹配逻辑

依赖列表拿到手之后,接下来就是漏洞匹配。pip-audit的核心流程是:

  1. 把每个依赖包的包名和版本解析成规范格式。
  2. 调用PyPI JSON API或OSV API查询该版本是否存在已知安全公告。
  3. 如果命中,返回漏洞ID、受影响的版本范围、修复版本、漏洞描述等信息。

这里有一个值得注意的点:很多漏洞的“受影响版本范围”不是精确到某一个版本,而是一个区间,比如>=2.0.0,<2.4.1。扫描器需要做的,是判断你当前锁定的版本是否落在这些区间内。

我自己封装的时候,直接用pip-audit的JSON输出,避免重复实现这个匹配逻辑。命令长这样:

pip-audit --requirement requirements.txt --format json --output pip-audit-report.json

但这里有个问题:如果requirements.txt里版本写得太宽泛,pip-audit无论如何也无法精确判断当前锁定版本,因为它没看到“实际装的版本”。后来我改成了先用pip freeze生成当前环境快照,再扫快照文件:

pip freeze > requirements-lock.txt pip-audit --requirement requirements-lock.txt --format json --output pip-audit-report.json

pip freeze会把当前环境里所有包的精确版本列出来,扫描结果准确度直接提升一个档次。代价是这份requirements-lock.txt只能作为扫描输入,不能替代真正的依赖清单给别人用,因为里面可能包含不需要直接引用的传递依赖。

3.3 输出报告的格式设计

扫描结果如果只是往终端里打几行字,没人愿意看。尤其是要定期巡检出报告的时候,报表的易读性很重要。

我做的输出层分两种格式:

一种是人读的摘要,直接输出到终端或写入Markdown文件。核心字段包括:包名、当前版本、漏洞ID、漏洞描述摘要、CVSS评分、修复版本。按CVSS评分从高到低排,一眼就能看出哪个最紧急。

另一种是机器读的完整数据,用JSON或SBOM格式保留。这样后续可以接入告警系统,也可以做历史数据对比,看“上一轮扫出来5个漏洞,这一轮修掉了几个、新增了几个”。

具体代码封装的时候,我是这样做的:

import json import subprocess def run_scan(requirement_file): cmd = [ "pip-audit", "--requirement", requirement_file, "--format", "json", ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(result.stderr) return json.loads(result.stdout) def summarize(report): dependencies = report.get("dependencies", []) vulnerabilities = [] for dep in dependencies: # 每个依赖下的 vulns 字段就是命中的漏洞信息 for vuln in dep.get("vulns", []): vulnerabilities.append({ "package": dep["name"], "version": dep["version"], "vuln_id": vuln["id"], "summary": vuln.get("description", "")[:80], "cvss": vuln.get("cvss", {}).get("score", "N/A"), "fix_version": vuln.get("fix_versions", []), }) return sorted(vulnerabilities, key=lambda x: x["cvss"], reverse=True)

这里的核心思路是:扫描器本身不关心你用什么数据格式,只要能把原始漏洞数据拿回来,后面的分析和通知都是你自己说了算。

注意:pip-audit的JSON结构在不同版本里字段名会有微调。我在项目里固定了pip-audit的最低版本,避免上游升级后脚本解析报错。这个坑后面还会细说。

4. 实跑结果与避坑记录

4.1 拿一个真实项目跑一遍

为了验证这套工具的效果,我拿自己维护的一个Flask项目做了实测。这个项目规模不大,直接依赖34个,传递依赖70多个。

扫描命令执行之后,结果分成了三类:

  • 高危漏洞(CVSS >= 7.0):2个,都来自同一个底层库。原因是这个库的旧版本有一个反序列化相关的安全问题,而我们项目里的另一个库依赖了它,属于典型的传递依赖风险。
  • 中危漏洞(CVSS 4.0-6.9):3个,问题不大但需要关注。
  • 低危或信息级:8个,基本都是版本过旧导致的潜在风险,升级后即可解除。

最有价值的是,扫描工具直接给出了修复版本。比如某个库当前装的版本是4.2.0,漏洞影响范围是<4.5.1,那么我把版本升级到4.5.1就能同时解决两三个漏洞。这个信息在手工排查时,可能要花半天才能从漏洞公告里捞出来。

4.2 误报和漏报的真实案例

依赖漏洞扫描有一个绕不开的问题:误报。不是工具错了,而是漏洞描述里的受影响范围往往大于实际可利用的场景。

举个例子,我扫描A项目时,工具报出某个JSON处理库存在CVE,影响版本是<1.2.3。但排查之后发现,这个漏洞只有在该库被用作服务端端点解析时才能触发,而A项目里它只是被用来读取本地配置文件。从CVSS评分看是中等偏高的漏洞,实际上对我们的风险非常有限。

那怎么办?我的做法是在报告里保留这些“理论上存在但实际不可利用”的项,但单独加一个分类叫“需人工复核”。判断依据主要是看漏洞通告里的攻击向量、前置条件以及我们项目实际调用该库的方式。这一层判断没法完全自动化,但可以通过一个“已知风险白名单”来管理,避免每次扫描都重复人工确认。

漏报也有案例。有一回我发现某个依赖包在PyPI上的版本定义和本地安装包的实际代码不一致,怀疑是缓存污染。扫描器只比对“包名+版本号”,不会校验包内容的哈希,所以这种情况下会漏掉风险。后来我把扫描工具接入了pip hash校验逻辑,对锁定文件里每个包生成哈希,跟PyPI上发布源文件的哈希对比,不一样的直接标红。这个问题比较罕见,但供应链攻击的可怕之处就在于,你可能装了一个版本号正确但内容被篡改的包。

4.3 升级修复时的连锁反应:依赖冲突坑

扫描工具给了修复版本,你以为直接pip install --upgrade就完事了?太天真了。依赖升级的连锁反应,是这套流程里最容易翻车的一环。

我遇到过一次典型的依赖冲突:A库的新版本修复了漏洞,但A库新版本引入了对C库更高版本的要求;而B库又恰好需要C库的旧版本。结果就是,升级A之后B直接启动失败。这种“依赖地狱”在Python项目里太常见了。

所以我的建议是:扫描工具查出来的高危漏洞,先修;修复时不要贪多,一次只升一个包,每次升级后跑一遍完整测试用例。如果升级某个包导致连锁冲突,就用Python官方推荐的依赖解析器(pip install新版默认行为)去解,解不开就退回原来的版本,手工评估当前漏洞的实际风险,而不是硬升。

这个经验和网上很多“把所有依赖升到最新”的建议相反。把所有依赖升到最新,往往能消除漏洞,但代价是新版本之间的兼容性风险。依赖漏洞扫描的目标是“把风险降到可接受水平”,不是“零漏洞”。

5. 把扫描嵌入日常流程:CI、定时与告警

5.1 本地手动扫描与CI阶段

工具做出来之后,不能只在出事的时候跑一次。我给它设计了三个使用场景。

第一个是本地手动扫描,适合在开发过程中随时自查。这个最简单,一条命令的事。我把它封装成了一个脚本,入口长这样:

python dep_scan.py --source venv --output console python dep_scan.py --source requirements.txt --output markdown --file report.md

第二个场景是接入CI流程。以GitLab CI为例,我在流水线里加了一个dependency-scan阶段,装好依赖之后立刻扫描:

dependency-scan: stage: test script: - pip install pip-audit - pip-audit --requirement requirements-lock.txt --format json --output pip-audit-report.json || true artifacts: paths: - pip-audit-report.json expire_in: 30 days

注意这里用了|| true,目的是不要让扫描阶段的非零退出码直接中断流水线。更合理的做法是:只在发现高危漏洞时中断,中低危漏洞只记录不阻塞。

5.2 定时扫描与通知渠道

第三个场景是定时巡检。我部署了一个简单的定时任务,每周一早上自动拉取项目最新代码、创建虚拟环境、安装依赖、执行扫描,然后把报告推送到团队群里。

通知渠道我用的是自定义脚本,核心就是一个webhookPOST:

import requests def notify_team(payload): webhook_url = "https://your-webhook-url" headers = {"Content-Type": "application/json"} requests.post(webhook_url, json=payload, headers=headers, timeout=10)

这样做的价值在于,把“扫描”从一次性自查变成了持续性的依赖观察。某个依赖在上一轮扫描时还是安全的,这周它的新CVE被公布了,定时任务会在周一早上自动发现并通知你。

这里引出一个关键步骤:漏洞数据库是会持续更新的,上周扫不出来不代表这周扫不出来。我自己就遇到过,态度是宁可每周多扫一次,也别等漏洞被利用之后再补。

5.3 缺少lockfile时的兜底策略

不是所有项目都有规范的lockfile。很多老项目就是一份手写的requirements.txt,版本号写得稀烂,甚至有的连requirements.txt都没有,靠setup.py动态装依赖。

遇到这种情况,我的兜底策略是“先制造lockfile,再扫描”。具体流程:

  1. 在干净的Python虚拟环境里安装项目声明的依赖。
  2. 用pip freeze强制生成一份完整精确的依赖快照。
  3. 拿这份快照去做漏洞扫描。

这个策略的缺点是:生成快照那一刻的版本,可能不是开发机上正在用的版本。所以我会在快照文件头部加一行注释标记生成时间和来源环境,避免别人误以为这是项目官方锁定的依赖。

另外还有一种情况:项目依赖了不受pip管理的私有包或本地包。这些包通常不在PyPI上,也没有对应的漏洞记录。扫描工具会跳过它们,我的处理方式是把它们单独列出来,人工确认它们的安全状态,而不是依赖自动工具的判断。

6. 扫描工具之外的延伸思考

6.1 可以继续扩展的方向

基础版扫描工具跑通之后,我逐渐意识到,它只是依赖安全管理的一小部分。往深了做,还有几个方向值得扩展。

一个是SBOM生成。SBOM(软件物料清单)是近几年供应链安全里非常重要的概念,简单说就是把项目用到的所有组件、版本、依赖关系列成清单,让“项目里有什么”这件事变得透明。pip-audit本身就支持--format sbom输出CycloneDX格式的SBOM,这对企业级合规审计很有帮助。

另一个是漏洞数据的历史对比。我现在会在每次扫描时把结果存一份到本地数据库,按周做对比,看看漏洞数量的变化趋势。这能直观看出依赖治理有没有成效,修漏洞的速度是否跟得上新漏洞发布的速度。

还有一个是我特别想做的:漏洞利用可达性分析。现在的扫描器只能告诉你“存在漏洞”,但不能告诉你“这个漏洞是否真的有可能被外部触发”。这涉及到调用链分析、入口点识别等复杂逻辑,已经不是简单工具能解决的了。目前只能靠人工复核来弥补。

6.2 我这几个月用下来的真实体会

最后聊几点个人经历吧。

第一,工具要趁早做,但别追求一步到位。我最初的版本只是简单封装了pip-audit的终端输出,几个月用下来逐步加了报告、定时任务、通知渠道,才变成现在这样。一开始就搞大而全的架构,反而容易烂尾。

第二,漏洞扫描的关键不是“扫出多少”,而是“扫出来之后有没有人管”。如果扫出来的漏洞没人负责修,那这个工具就只是一个告警制造机,只会让人麻木。我建议在接入扫描流程之前,先确定好漏洞分类分级标准以及对应的处理责任人。

第三,别把扫描结果当唯一依据。依赖漏洞扫描器提供的只是“存在风险”的线索,最终要不要升级、怎么升级,还是要结合项目实际场景、代码调用情况以及运行环境来判断。扫描工具是帮你把地毯翻起来看下面的灰,真正决定怎么打扫的,还是你自己。

一件事能坚持做下去,靠的不是工具多厉害,而是你给它定义了一个清晰的位置:它不负责替你思考,只负责帮你把所有该看的东西按期摆到眼前。这套依赖漏洞自动扫描工具,在我这里的角色就是这样。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询