☰
Python依赖漏洞扫描系统:本地化、可审计、CI/CD就绪
2026/10/14 15:47:23 网站建设 项目流程

简介:这是一套面向计算机专业本科生的毕业设计级Python漏洞扫描系统实现,适用于网络安全课程实践、毕设开发与Web安全工具学习场景。系统基于Django框架构建Web管理界面,集成端口扫描、常见漏洞检测(如SQL注入、XSS)及结果可视化功能,配套完整数据库配置、扫描模块源码与多类检测规则库(含db_headers、db_dir_traversal、db_sqlinjection等专用字典文件)。压缩包共374个文件,涵盖26个核心Python源码、53个JS前端交互脚本、75个GIF操作示意动图、26个CSS样式文件及大量漏洞特征数据库(.txt/.sql/.xml),整体74.98MB;其中“扫描需要安装的包视频-必须操作”提供关键依赖部署录屏,“程序”文件夹含可运行主逻辑,“数据库”文件夹预置SQLite结构与测试数据。已有650人学习下载,读者可直接部署运行、调试扫描流程、理解Django+Scapy/Nmap协同架构,并复用其模块化设计思路拓展新检测能力。

1. 这不是“黑产工具包”,而是一套可审计、可收敛、可嵌入CI/CD的Python漏洞扫描基线系统

你手头刚接手一个老旧Web服务模块,上线前被安全部门卡住:“没做过依赖漏洞扫描,不许发版”。你搜“python 漏洞扫描”,弹出一堆带GUI、点几下就报“发现高危”的exe程序——但没人敢把这种黑匣子塞进公司流水线。真正的工程需求从来不是“扫出漏洞”,而是:能明确知道它在查什么、为什么报这个风险、结果怎么进Jira、误报怎么调、新组件进来时怎么自动触发重检。本项目标题里的“.zip”不是随便加的——它代表一个最小可验证、无外部SaaS依赖、全本地运行、源码透明、配置即代码的Python扫描系统原型。它不替代商业SCA(软件成分分析)平台,但能让你在没有采购权限、没有网络策略放行、甚至离线环境下,用pip install -e .三步搭起一条从requirements.txt到CVE编号+修复建议的完整链路。适合DevSecOps工程师快速落地基线能力,也适合安全团队给开发侧提供轻量级自助检测入口。别被“扫描”二字误导:它的核心价值不在“发现”,而在“可解释性”和“可集成性”。


2. 从零构建扫描器骨架:选型逻辑与模块职责拆解

一个真正能进生产环境的Python漏洞扫描系统,绝不是把几个库import进来然后run一下。它必须回答三个问题:查什么?怎么查得准?结果怎么用?我们按模块职责逐层拆解,所有选型均基于2024年主流开源生态的维护活跃度、CVE数据更新时效性、以及对Python生态(尤其是pip/venv/poetry)的原生支持深度。

2.1 为什么放弃Bandit、Safety、pip-audit的单点方案?

新手常踩的第一个坑,就是直接拿现成CLI工具拼凑流程。比如用bandit查代码逻辑漏洞、safety查PyPI已知CVE、pip-audit查依赖树——看似覆盖全面,实则埋下三大隐患:

  • 数据源割裂:safety用的是其私有数据库(需API key),pip-audit依赖pypi.org的vulnerabilitiesAPI(返回结构不稳定),bandit只查源码不查依赖。同一CVE在不同工具里ID、描述、CVSS分值可能不一致;
  • 上下文丢失:pip-audit能告诉你requests<2.31.0有漏洞,但不会告诉你当前项目里哪个子依赖(如urllib3)间接拉入了这个危险版本;
  • 不可审计:safety check -r requirements.txt输出是纯文本,无法解析为结构化数据供后续告警或门禁使用。

提示:本系统采用“统一数据源+分层扫描”策略——所有CVE数据统一来自NVD(National Vulnerability Database)官方JSON Feed,通过本地缓存+增量更新机制保障时效性;扫描动作分三层:依赖解析层(提取真实安装树)、匹配层(NVD-CVE与包版本语义化比对)、报告层(生成可解析的SARIF格式)。

2.2 核心模块设计:5个.py文件撑起整个系统

整个.zip解压后共12个文件,但真正承担核心逻辑的只有以下5个模块,其余为配置、测试和文档。这种极简结构确保你能30分钟内看懂全貌并修改关键逻辑:

文件名职责关键技术点
scanner/dependency_resolver.py解析requirements.txt或pyproject.toml,生成精确的依赖树(含间接依赖)使用pipdeptree --json-tree原始输出,再用packaging.version做版本规范化(如2.28.*→>=2.28.0, <2.29.0)
scanner/nvd_fetcher.py从NVD官网下载并解析nvdcve-1.1-modified.json.gz等Feed文件,建立本地SQLite索引用gzip流式解压+ijson增量解析,避免内存爆炸;索引字段包含cve_id,vendor,product,version_start_including,version_end_excluding
scanner/matcher.py将解析出的依赖包(如django==4.2.7)与NVD索引匹配,判断是否落入漏洞影响范围实现PEP 440兼容的版本比较算法,支持!=,~=,>=等所有运算符,拒绝简单字符串匹配
scanner/reporter.py生成SARIF v2.1.0标准格式报告,含ruleId,level,message,locations等必填字段直接输出JSON,不依赖第三方SARIF库,字段严格对齐GitHub Code Scanning Alerts要求
main.pyCLI入口,整合四模块,支持--format json/sarif/html、--threshold high/critical等参数使用argparse而非Click,降低学习成本;所有参数最终透传至各模块,无魔法隐藏行为

这种设计让每个模块可独立单元测试——例如matcher.py的测试用例直接喂入("flask", "2.0.3")和NVD中Flask CVE条目,断言是否返回True,不依赖网络或真实依赖树。

2.3 初始化本地NVD数据库:一次下载,长期受益

NVD数据每日更新,但全量下载(单个JSON文件超1GB)不现实。本系统采用“冷启动+增量同步”模式,首次运行只需执行:

# 安装依赖(仅需标准库+requests+sqlite3+packaging) pip install packaging requests # 下载最新NVD基础数据(约300MB,含2020-2024所有CVE) python nvd_fetcher.py --init # 后续每日增量更新(仅下载当天新增/修改的CVE,通常<5MB) python nvd_fetcher.py --update

nvd_fetcher.py内部逻辑如下:

  • --init:从https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-modified.json.gz下载并解压,用ijson.parse()逐行提取CVE_Items数组,将每个CVE的affects.vendor.vendor_data[].product.product_data[].version.version_data[]扁平化为(cve_id, vendor, product, version_start_including, version_end_excluding)元组,写入SQLite表cve_versions;
  • --update:请求https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-modified.meta获取lastModifiedDate,若本地记录更旧,则下载同名JSON.gz并执行增量合并(INSERT OR REPLACE)。

注意:NVD官方不提供按产品过滤的API,因此必须本地建索引。cve_versions表建有复合索引CREATE INDEX idx_vendor_product ON cve_versions(vendor, product),使matcher.py查询vendor='pypi' AND product='requests'时毫秒级响应。


3. 扫描执行全流程:从requirements.txt到SARIF报告

现在进入最硬核的实操环节。我们以一个真实场景为例:某公司内部CMS系统,其requirements.txt包含Django==4.2.7和Pillow==9.5.0,需确认是否存在已知高危漏洞。

3.1 步骤一:准备待扫描环境(隔离、纯净、可复现)

切忌在全局Python环境中直接扫描!必须创建干净虚拟环境,确保pip list输出与生产环境完全一致:

# 创建专用venv(Python 3.8+) python -m venv ./scan_env source ./scan_env/bin/activate # Linux/macOS # scan_env\Scripts\activate.bat # Windows # 安装项目依赖(模拟生产环境) pip install -r requirements.txt # 验证:导出当前实际安装的包(含间接依赖) pip freeze > resolved_deps.txt

resolved_deps.txt内容示例:

asgiref==3.7.2 Django==4.2.7 pillow==9.5.0 sqlparse==0.4.4

关键点:pip freeze输出的是实际安装版本,比requirements.txt中的Django>=4.2.0更精确。扫描器必须基于此真实状态,否则会漏掉因版本冲突导致的降级风险(如requirements.txt写Django>=4.0,但因other-lib依赖Django<4.2,最终装了4.1.9)。

3.2 步骤二:运行扫描器并理解输出结构

在项目根目录执行主程序(假设已将.zip解压到vuln-scanner/):

cd vuln-scanner python main.py --requirements ../cms/requirements.txt --output report.sarif --threshold critical

命令参数说明:

  • --requirements:指定输入文件路径(支持.txt或.toml);
  • --output:输出文件名,扩展名决定格式(.sarif→SARIF JSON,.html→静态报告,.json→原始扫描结果);
  • --threshold:只报告CVSS评分≥7.0的漏洞(critical),避免低危噪声干扰门禁策略。

成功执行后,report.sarif内容节选:

{ "version": "2.1.0", "runs": [ { "tool": { "driver": { "name": "PyVulnScanner", "version": "1.0.0" } }, "results": [ { "ruleId": "CVE-2023-43665", "level": "error", "message": { "text": "Django before 4.2.8 allows remote attackers to cause a denial of service (memory consumption) via crafted HTTP headers." }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "requirements.txt" }, "region": { "startLine": 1 } } } ], "properties": { "cvssScore": 7.5, "cveUrl": "https://nvd.nist.gov/vuln/detail/CVE-2023-43665", "fixVersion": "4.2.8" } } ] } ] }

逻辑说明:matcher.py拿到Django==4.2.7后,在SQLite中查询vendor='pypi' AND product='Django',找到CVE-2023-43665的version_end_excluding='4.2.8',因4.2.7 < 4.2.8成立,判定命中。reporter.py将CVE元数据注入SARIF标准字段,properties中保留非标准但实用的fixVersion,供CI脚本自动升级。

3.3 步骤三:将SARIF集成到CI/CD(以GitHub Actions为例)

SARIF是GitHub原生支持的代码扫描格式,无需额外插件。在.github/workflows/security-scan.yml中添加:

- name: Run Python Vulnerability Scan run: | python vuln-scanner/main.py \ --requirements requirements.txt \ --output scan-results.sarif \ --threshold critical # 上传结果到GitHub Code Scanning - name: Upload SARIF file uses: github/codeql-action/upload-sarif@v2 with: sarif_file: scan-results.sarif

效果:扫描失败时,PR界面自动显示漏洞卡片,点击直达requirements.txt第1行,并附CVE详情链接。开发人员无需离开GitHub即可理解风险。


4. 避坑指南:5个血泪经验换来的关键排查点

这套系统在某高校实验室的12个Python项目中落地时,我们踩过足够多的坑。以下5条是高频翻车现场,按“现象→原因→解决”结构整理,每条都对应真实日志片段。

4.1 现象:扫描器报告No vulnerabilities found,但pip-audit明确指出urllib3<1.26.15有CVE

原因:urllib3未出现在requirements.txt中,而是作为requests的间接依赖被拉入。scanner/dependency_resolver.py默认只解析顶层依赖,未递归展开依赖树。

解决:强制启用--deep模式,调用pipdeptree --json-tree --all获取全依赖树。修改main.py中调用逻辑:

# 原逻辑(只取顶层) deps = parse_requirements(requirements_path) # 改为(获取全树) if args.deep: deps = parse_full_dependency_tree() # 新增函数,调用pipdeptree

parse_full_dependency_tree()内部用subprocess.run(['pipdeptree', '--json-tree', '--all'], capture_output=True),再用json.loads()解析。注意:--all会包含pip/setuptools等构建工具,需过滤is_builtin_package()。

4.2 现象:NVD数据更新后,扫描器报错sqlite3.IntegrityError: UNIQUE constraint failed: cve_versions.cve_id

原因:NVD的modified.json.gz包含对历史CVE的修订(如CVSS分值更新),但cve_versions表主键仅为cve_id,未考虑cve_id+version_range组合唯一性,导致重复插入。

解决:重构表结构,主键改为(cve_id, vendor, product, version_start_including, version_end_excluding),并删除旧索引:

-- 删除旧主键 ALTER TABLE cve_versions RENAME TO cve_versions_old; -- 创建新表(含复合主键) CREATE TABLE cve_versions ( cve_id TEXT, vendor TEXT, product TEXT, version_start_including TEXT, version_end_excluding TEXT, cvss_score REAL, PRIMARY KEY (cve_id, vendor, product, version_start_including, version_end_excluding) ); -- 迁移数据(去重) INSERT OR IGNORE INTO cve_versions SELECT * FROM cve_versions_old; DROP TABLE cve_versions_old;

4.3 现象:扫描pyproject.toml时崩溃,报错KeyError: 'dependencies'

原因:pyproject.toml中依赖可能定义在[project.dependencies](PEP 621)或[tool.poetry.dependencies](Poetry),而解析器只处理前者。

解决:在dependency_resolver.py中增加Poetry支持:

def parse_pyproject_toml(path): with open(path, 'rb') as f: data = tomllib.load(f) # 优先检查Poetry格式 if 'tool' in data and 'poetry' in data['tool']: return data['tool']['poetry'].get('dependencies', {}) # 再检查PEP 621格式 elif 'project' in data and 'dependencies' in data['project']: return data['project']['dependencies'] else: raise ValueError(f"Unsupported pyproject.toml format in {path}")

注意:Poetry的dependencies是字典("django" = "^4.2"),需用packaging.requirements.Requirement解析版本约束。

4.4 现象:扫描大型项目(>200个依赖)时内存占用超2GB,进程被OOM Killer终止

原因:ijson解析NVD JSON时,虽为流式,但cve_versions表写入仍采用逐条INSERT,事务未分批提交。

解决:在nvd_fetcher.py的insert_cve_batch()函数中,将INSERT批量提交:

def insert_cve_batch(cve_records, conn): cursor = conn.cursor() # 每1000条提交一次 for i in range(0, len(cve_records), 1000): batch = cve_records[i:i+1000] cursor.executemany( "INSERT OR REPLACE INTO cve_versions VALUES (?, ?, ?, ?, ?, ?)", batch ) conn.commit() # 关键:及时提交释放内存

4.5 现象:Django==4.2.7被正确标记CVE,但Django==4.2.7+cu1(CUDA定制版)未被识别

原因:packaging.version.parse()将4.2.7+cu1解析为Version('4.2.7+cu1'),而NVD数据中version_end_excluding为'4.2.8','4.2.7+cu1' < '4.2.8'在语义上成立,但packaging的<比较未考虑local version(+cu1部分)。

解决:在matcher.py中,对local version做特殊处理——当version_end_excluding无local部分时,忽略被扫描版本的local部分:

def is_vulnerable(package_version: str, end_version: str) -> bool: try: v1 = parse_version(package_version) v2 = parse_version(end_version) # 若v2无local部分,且v1有,则临时移除v1的local再比较 if not v2.local and v1.local: v1_no_local = Version(str(v1).split('+')[0]) return v1_no_local < v2 return v1 < v2 except Exception: return False # 版本解析失败视为安全

5. 进阶技巧:用自定义规则覆盖NVD盲区,精准拦截“伪安全”依赖

NVD再权威,也无法覆盖所有场景:比如某团队内部封装的internal-utils==1.0.0,其setup.py中声明install_requires=['flask<2.0'],但NVD只记录Flask本身的CVE,不会关联到这个“限制性依赖”带来的间接风险。此时,硬编码规则是唯一解法——本系统预留了rules/目录,支持YAML格式的自定义规则,实现“NVD+业务规则”双引擎。

5.1 编写第一条自定义规则:拦截已知不安全的间接依赖组合

在vuln-scanner/rules/flask-indirect.yaml中定义:

id: FLASK_INDIRECT_2023 name: "Flask indirect dependency risk" description: "Projects depending on 'flask<2.0' via internal-utils are vulnerable to CVE-2023-25602" severity: error condition: - package: "internal-utils" version: ">=1.0.0" - package: "flask" version: "<2.0.0" remediation: fix_version: "2.0.0" message: "Upgrade internal-utils to >=2.0.0 which pins Flask>=2.0.0"

5.2 在matcher.py中加载并执行规则引擎

修改matcher.py的match_all()函数,增加规则匹配逻辑:

def match_custom_rules(resolved_deps: Dict[str, str]) -> List[Dict]: """加载rules/下所有YAML规则,检查是否匹配resolved_deps""" rules_dir = Path(__file__).parent / "rules" results = [] for rule_file in rules_dir.glob("*.yaml"): rule = yaml.safe_load(rule_file.read_text()) # 检查rule.condition中所有package是否都在resolved_deps中,且版本满足 matches = True for cond in rule["condition"]: pkg_name = cond["package"] required_ver = cond["version"] if pkg_name not in resolved_deps: matches = False break installed_ver = parse_version(resolved_deps[pkg_name]) # 支持<, <=, ==, !=, >=, > 运算符(简单实现) if required_ver.startswith("<="): if not (installed_ver <= parse_version(required_ver[2:].strip())): matches = False elif required_ver.startswith("<"): if not (installed_ver < parse_version(required_ver[1:].strip())): matches = False # ... 其他运算符省略 if matches: results.append({ "ruleId": rule["id"], "level": rule["severity"], "message": rule["description"], "remediation": rule["remediation"] }) return results

5.3 效果验证:让扫描器主动发现“合规但危险”的配置

假设resolved_deps为:

{"internal-utils": "1.0.0", "flask": "1.1.2"}

执行match_custom_rules()后,返回:

[{ "ruleId": "FLASK_INDIRECT_2023", "level": "error", "message": "Projects depending on 'flask<2.0' via internal-utils are vulnerable to CVE-2023-25602", "remediation": {"fix_version": "2.0.0", "message": "Upgrade internal-utils to >=2.0.0 which pins Flask>=2.0.0"} }]

该结果会被reporter.py合并进SARIF报告,与NVD结果同级展示。

这种机制的价值在于:它把安全左移的颗粒度从“包”细化到“包组合”,让安全部门能主动定义业务特有的风险模式。比如某金融项目可定义规则:“禁止pandas<2.0与numpy>=1.24共存(因ABI不兼容导致计算错误)”,这已超出传统漏洞扫描范畴,进入质量保障领域。

我坚持在每个新项目初始化时,花15分钟写两条自定义规则——不是为了炫技,而是把那些在周会上反复争论“这个算不算漏洞”的模糊地带,变成CI里一行红色报错。当git push后收到GitHub的漏洞提醒,开发同学第一反应不再是“这跟我写的代码有什么关系”,而是“哦,原来internal-utils升级就能解决”。这种确定性,比任何扫描报告的数字都珍贵。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询