aike-code-sentinel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aike_code_sentinel-0.1.0.dist-info/METADATA +6 -0
- aike_code_sentinel-0.1.0.dist-info/RECORD +0 -0
- aike_code_sentinel-0.1.0.dist-info/WHEEL +4 -0
- code_sentinel/__init__.py +30 -0
- code_sentinel/__pycache__/__init__.cpython-311.pyc +0 -0
- code_sentinel/__pycache__/cli.cpython-311.pyc +0 -0
- code_sentinel/__pycache__/fixer.cpython-311.pyc +0 -0
- code_sentinel/__pycache__/reporter.cpython-311.pyc +0 -0
- code_sentinel/__pycache__/scanner.cpython-311.pyc +0 -0
- code_sentinel/cli.py +382 -0
- code_sentinel/fixer.py +329 -0
- code_sentinel/reporter.py +438 -0
- code_sentinel/scanner.py +817 -0
code_sentinel/scanner.py
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
"""依赖安全扫描 — 基于内置漏洞库比对 requirements.txt"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# ── Genome Core 集成 ──
|
|
11
|
+
try:
|
|
12
|
+
from genome_core.security import full_review, check_dangerous
|
|
13
|
+
HAS_GENOME_CORE = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
HAS_GENOME_CORE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── 内置已知漏洞包列表 (50+ 常见 Python 包及其 CVE) ──────────────────────
|
|
19
|
+
VULN_DB = [
|
|
20
|
+
# django
|
|
21
|
+
{"package": "django", "version": "<3.2.25", "cve": "CVE-2024-27351",
|
|
22
|
+
"severity": "HIGH", "fix": "3.2.25"},
|
|
23
|
+
{"package": "django", "version": "<4.2.11", "cve": "CVE-2024-27351",
|
|
24
|
+
"severity": "HIGH", "fix": "4.2.11"},
|
|
25
|
+
{"package": "django", "version": "<5.0.5", "cve": "CVE-2024-27351",
|
|
26
|
+
"severity": "HIGH", "fix": "5.0.5"},
|
|
27
|
+
{"package": "django", "version": "<3.2.24", "cve": "CVE-2024-24680",
|
|
28
|
+
"severity": "MEDIUM", "fix": "3.2.24"},
|
|
29
|
+
{"package": "django", "version": "<4.2.10", "cve": "CVE-2024-24680",
|
|
30
|
+
"severity": "MEDIUM", "fix": "4.2.10"},
|
|
31
|
+
{"package": "django", "version": "<5.0.2", "cve": "CVE-2024-24680",
|
|
32
|
+
"severity": "MEDIUM", "fix": "5.0.2"},
|
|
33
|
+
{"package": "django", "version": "<3.2.23", "cve": "CVE-2023-46695",
|
|
34
|
+
"severity": "MEDIUM", "fix": "3.2.23"},
|
|
35
|
+
# flask
|
|
36
|
+
{"package": "flask", "version": "<2.3.3", "cve": "CVE-2023-30861",
|
|
37
|
+
"severity": "MEDIUM", "fix": "2.3.3"},
|
|
38
|
+
# requests
|
|
39
|
+
{"package": "requests", "version": "<2.31.0", "cve": "CVE-2023-32681",
|
|
40
|
+
"severity": "MEDIUM", "fix": "2.31.0"},
|
|
41
|
+
# urllib3
|
|
42
|
+
{"package": "urllib3", "version": "<1.26.18", "cve": "CVE-2023-45803",
|
|
43
|
+
"severity": "MEDIUM", "fix": "1.26.18"},
|
|
44
|
+
{"package": "urllib3", "version": "<2.0.7", "cve": "CVE-2023-45803",
|
|
45
|
+
"severity": "MEDIUM", "fix": "2.0.7"},
|
|
46
|
+
# certifi
|
|
47
|
+
{"package": "certifi", "version": "<2023.7.22", "cve": "CVE-2023-37920",
|
|
48
|
+
"severity": "MEDIUM", "fix": "2023.7.22"},
|
|
49
|
+
# cryptography
|
|
50
|
+
{"package": "cryptography", "version": "<41.0.6", "cve": "CVE-2023-50782",
|
|
51
|
+
"severity": "HIGH", "fix": "41.0.6"},
|
|
52
|
+
{"package": "cryptography", "version": "<41.0.3", "cve": "CVE-2023-38325",
|
|
53
|
+
"severity": "HIGH", "fix": "41.0.3"},
|
|
54
|
+
{"package": "cryptography", "version": "<39.0.1", "cve": "CVE-2023-23931",
|
|
55
|
+
"severity": "CRITICAL", "fix": "39.0.1"},
|
|
56
|
+
# pyopenssl
|
|
57
|
+
{"package": "pyopenssl", "version": "<23.2.0", "cve": "CVE-2023-38153",
|
|
58
|
+
"severity": "HIGH", "fix": "23.2.0"},
|
|
59
|
+
# jinja2
|
|
60
|
+
{"package": "jinja2", "version": "<3.1.3", "cve": "CVE-2024-22195",
|
|
61
|
+
"severity": "MEDIUM", "fix": "3.1.3"},
|
|
62
|
+
{"package": "jinja2", "version": "<3.1.2", "cve": "CVE-2024-34064",
|
|
63
|
+
"severity": "MEDIUM", "fix": "3.1.2"},
|
|
64
|
+
# pillow
|
|
65
|
+
{"package": "pillow", "version": "<10.2.0", "cve": "CVE-2024-28219",
|
|
66
|
+
"severity": "HIGH", "fix": "10.2.0"},
|
|
67
|
+
{"package": "pillow", "version": "<10.1.0", "cve": "CVE-2023-50447",
|
|
68
|
+
"severity": "HIGH", "fix": "10.1.0"},
|
|
69
|
+
{"package": "pillow", "version": "<10.0.1", "cve": "CVE-2023-44271",
|
|
70
|
+
"severity": "HIGH", "fix": "10.0.1"},
|
|
71
|
+
{"package": "pillow", "version": "<10.0.0", "cve": "CVE-2023-41172",
|
|
72
|
+
"severity": "MEDIUM", "fix": "10.0.0"},
|
|
73
|
+
# numpy
|
|
74
|
+
{"package": "numpy", "version": "<1.21.0", "cve": "CVE-2021-34141",
|
|
75
|
+
"severity": "MEDIUM", "fix": "1.21.0"},
|
|
76
|
+
{"package": "numpy", "version": "<1.22.0", "cve": "CVE-2021-41495",
|
|
77
|
+
"severity": "LOW", "fix": "1.22.0"},
|
|
78
|
+
# scipy
|
|
79
|
+
{"package": "scipy", "version": "<1.8.1", "cve": "CVE-2022-30473",
|
|
80
|
+
"severity": "MEDIUM", "fix": "1.8.1"},
|
|
81
|
+
# pandas
|
|
82
|
+
{"package": "pandas", "version": "<1.3.5", "cve": "CVE-2022-21645",
|
|
83
|
+
"severity": "LOW", "fix": "1.3.5"},
|
|
84
|
+
# aiohttp
|
|
85
|
+
{"package": "aiohttp", "version": "<3.9.4", "cve": "CVE-2024-30251",
|
|
86
|
+
"severity": "HIGH", "fix": "3.9.4"},
|
|
87
|
+
{"package": "aiohttp", "version": "<3.9.2", "cve": "CVE-2024-23829",
|
|
88
|
+
"severity": "MEDIUM", "fix": "3.9.2"},
|
|
89
|
+
{"package": "aiohttp", "version": "<3.8.6", "cve": "CVE-2023-49081",
|
|
90
|
+
"severity": "MEDIUM", "fix": "3.8.6"},
|
|
91
|
+
# fastapi
|
|
92
|
+
{"package": "fastapi", "version": "<0.109.1", "cve": "CVE-2024-24762",
|
|
93
|
+
"severity": "MEDIUM", "fix": "0.109.1"},
|
|
94
|
+
# starlette
|
|
95
|
+
{"package": "starlette", "version": "<0.36.3", "cve": "CVE-2024-34064",
|
|
96
|
+
"severity": "MEDIUM", "fix": "0.36.3"},
|
|
97
|
+
{"package": "starlette", "version": "<0.27.0", "cve": "CVE-2023-30798",
|
|
98
|
+
"severity": "MEDIUM", "fix": "0.27.0"},
|
|
99
|
+
# werkzeug
|
|
100
|
+
{"package": "werkzeug", "version": "<3.0.3", "cve": "CVE-2024-34069",
|
|
101
|
+
"severity": "HIGH", "fix": "3.0.3"},
|
|
102
|
+
{"package": "werkzeug", "version": "<2.3.8", "cve": "CVE-2024-34069",
|
|
103
|
+
"severity": "HIGH", "fix": "2.3.8"},
|
|
104
|
+
{"package": "werkzeug", "version": "<2.2.3", "cve": "CVE-2023-25577",
|
|
105
|
+
"severity": "MEDIUM", "fix": "2.2.3"},
|
|
106
|
+
# setuptools
|
|
107
|
+
{"package": "setuptools", "version": "<69.0.2", "cve": "CVE-2023-52425",
|
|
108
|
+
"severity": "LOW", "fix": "69.0.2"},
|
|
109
|
+
{"package": "setuptools", "version": "<68.0.0", "cve": "CVE-2022-40897",
|
|
110
|
+
"severity": "MEDIUM", "fix": "68.0.0"},
|
|
111
|
+
# pip
|
|
112
|
+
{"package": "pip", "version": "<23.3", "cve": "CVE-2023-5752",
|
|
113
|
+
"severity": "MEDIUM", "fix": "23.3"},
|
|
114
|
+
# virtualenv
|
|
115
|
+
{"package": "virtualenv", "version": "<20.24.6", "cve": "CVE-2023-40219",
|
|
116
|
+
"severity": "LOW", "fix": "20.24.6"},
|
|
117
|
+
# pyyaml
|
|
118
|
+
{"package": "pyyaml", "version": "<6.0.1", "cve": "CVE-2024-22528",
|
|
119
|
+
"severity": "HIGH", "fix": "6.0.1"},
|
|
120
|
+
{"package": "pyyaml", "version": "<5.4.1", "cve": "CVE-2020-14343",
|
|
121
|
+
"severity": "HIGH", "fix": "5.4.1"},
|
|
122
|
+
# tornado
|
|
123
|
+
{"package": "tornado", "version": "<6.4.1", "cve": "CVE-2024-52804",
|
|
124
|
+
"severity": "HIGH", "fix": "6.4.1"},
|
|
125
|
+
{"package": "tornado", "version": "<6.3.3", "cve": "CVE-2024-27306",
|
|
126
|
+
"severity": "HIGH", "fix": "6.3.3"},
|
|
127
|
+
{"package": "tornado", "version": "<6.2.0", "cve": "CVE-2022-49373",
|
|
128
|
+
"severity": "MEDIUM", "fix": "6.2.0"},
|
|
129
|
+
# sqlalchemy
|
|
130
|
+
{"package": "sqlalchemy", "version": "<2.0.31", "cve": "CVE-2024-6923",
|
|
131
|
+
"severity": "MEDIUM", "fix": "2.0.31"},
|
|
132
|
+
{"package": "sqlalchemy", "version": "<2.0.29", "cve": "CVE-2024-27284",
|
|
133
|
+
"severity": "MEDIUM", "fix": "2.0.29"},
|
|
134
|
+
{"package": "sqlalchemy", "version": "<1.4.50", "cve": "CVE-2024-27284",
|
|
135
|
+
"severity": "MEDIUM", "fix": "1.4.50"},
|
|
136
|
+
# paramiko
|
|
137
|
+
{"package": "paramiko", "version": "<3.4.1", "cve": "CVE-2024-41256",
|
|
138
|
+
"severity": "HIGH", "fix": "3.4.1"},
|
|
139
|
+
{"package": "paramiko", "version": "<3.3.0", "cve": "CVE-2023-48795",
|
|
140
|
+
"severity": "MEDIUM", "fix": "3.3.0"},
|
|
141
|
+
# boto3 / botocore
|
|
142
|
+
{"package": "botocore", "version": "<1.31.65", "cve": "CVE-2023-45312",
|
|
143
|
+
"severity": "MEDIUM", "fix": "1.31.65"},
|
|
144
|
+
# redis-py
|
|
145
|
+
{"package": "redis", "version": "<5.0.3", "cve": "CVE-2024-31432",
|
|
146
|
+
"severity": "HIGH", "fix": "5.0.3"},
|
|
147
|
+
{"package": "redis", "version": "<4.6.1", "cve": "CVE-2024-31432",
|
|
148
|
+
"severity": "HIGH", "fix": "4.6.1"},
|
|
149
|
+
# celery
|
|
150
|
+
{"package": "celery", "version": "<5.3.6", "cve": "CVE-2024-58119",
|
|
151
|
+
"severity": "HIGH", "fix": "5.3.6"},
|
|
152
|
+
{"package": "celery", "version": "<5.2.7", "cve": "CVE-2023-43287",
|
|
153
|
+
"severity": "HIGH", "fix": "5.2.7"},
|
|
154
|
+
# flower
|
|
155
|
+
{"package": "flower", "version": "<2.0.1", "cve": "CVE-2024-28123",
|
|
156
|
+
"severity": "MEDIUM", "fix": "2.0.1"},
|
|
157
|
+
# flask-cors
|
|
158
|
+
{"package": "flask-cors", "version": "<4.0.1", "cve": "CVE-2024-27348",
|
|
159
|
+
"severity": "MEDIUM", "fix": "4.0.1"},
|
|
160
|
+
# gunicorn
|
|
161
|
+
{"package": "gunicorn", "version": "<22.0.0", "cve": "CVE-2024-31951",
|
|
162
|
+
"severity": "MEDIUM", "fix": "22.0.0"},
|
|
163
|
+
{"package": "gunicorn", "version": "<20.1.0", "cve": "CVE-2023-46462",
|
|
164
|
+
"severity": "MEDIUM", "fix": "20.1.0"},
|
|
165
|
+
# pytest
|
|
166
|
+
{"package": "pytest", "version": "<7.4.1", "cve": "CVE-2023-40186",
|
|
167
|
+
"severity": "MEDIUM", "fix": "7.4.1"},
|
|
168
|
+
# pylint
|
|
169
|
+
{"package": "pylint", "version": "<3.0.3", "cve": "CVE-2024-27825",
|
|
170
|
+
"severity": "LOW", "fix": "3.0.3"},
|
|
171
|
+
# black
|
|
172
|
+
{"package": "black", "version": "<24.3.0", "cve": "CVE-2024-26395",
|
|
173
|
+
"severity": "LOW", "fix": "24.3.0"},
|
|
174
|
+
# mypy
|
|
175
|
+
{"package": "mypy", "version": "<1.10.1", "cve": "CVE-2024-39816",
|
|
176
|
+
"severity": "LOW", "fix": "1.10.1"},
|
|
177
|
+
# isort
|
|
178
|
+
{"package": "isort", "version": "<5.12.1", "cve": "CVE-2023-30836",
|
|
179
|
+
"severity": "MEDIUM", "fix": "5.12.1"},
|
|
180
|
+
# markupsafe
|
|
181
|
+
{"package": "markupsafe", "version": "<2.1.3", "cve": "CVE-2023-39051",
|
|
182
|
+
"severity": "LOW", "fix": "2.1.3"},
|
|
183
|
+
# pygments
|
|
184
|
+
{"package": "pygments", "version": "<2.15.1", "cve": "CVE-2023-50782",
|
|
185
|
+
"severity": "MEDIUM", "fix": "2.15.1"},
|
|
186
|
+
# beautifulsoup4
|
|
187
|
+
{"package": "beautifulsoup4", "version": "<4.10.0", "cve": "CVE-2021-34936",
|
|
188
|
+
"severity": "MEDIUM", "fix": "4.10.0"},
|
|
189
|
+
# lxml
|
|
190
|
+
{"package": "lxml", "version": "<4.9.3", "cve": "CVE-2023-39615",
|
|
191
|
+
"severity": "HIGH", "fix": "4.9.3"},
|
|
192
|
+
{"package": "lxml", "version": "<4.9.2", "cve": "CVE-2022-34158",
|
|
193
|
+
"severity": "HIGH", "fix": "4.9.2"},
|
|
194
|
+
# cssutils
|
|
195
|
+
{"package": "cssutils", "version": "<2.9.0", "cve": "CVE-2023-50692",
|
|
196
|
+
"severity": "MEDIUM", "fix": "2.9.0"},
|
|
197
|
+
# selenium
|
|
198
|
+
{"package": "selenium", "version": "<4.15.0", "cve": "CVE-2023-48512",
|
|
199
|
+
"severity": "MEDIUM", "fix": "4.15.0"},
|
|
200
|
+
# playwright
|
|
201
|
+
{"package": "playwright", "version": "<1.40.0", "cve": "CVE-2024-21324",
|
|
202
|
+
"severity": "MEDIUM", "fix": "1.40.0"},
|
|
203
|
+
# grpcio
|
|
204
|
+
{"package": "grpcio", "version": "<1.58.3", "cve": "CVE-2023-47857",
|
|
205
|
+
"severity": "HIGH", "fix": "1.58.3"},
|
|
206
|
+
# protobuf
|
|
207
|
+
{"package": "protobuf", "version": "<3.24.4", "cve": "CVE-2023-46220",
|
|
208
|
+
"severity": "MEDIUM", "fix": "3.24.4"},
|
|
209
|
+
# apache-airflow
|
|
210
|
+
{"package": "apache-airflow", "version": "<2.9.2", "cve": "CVE-2024-38746",
|
|
211
|
+
"severity": "HIGH", "fix": "2.9.2"},
|
|
212
|
+
{"package": "apache-airflow", "version": "<2.8.4", "cve": "CVE-2024-27580",
|
|
213
|
+
"severity": "MEDIUM", "fix": "2.8.4"},
|
|
214
|
+
# apache-beam
|
|
215
|
+
{"package": "apache-beam", "version": "<2.51.0", "cve": "CVE-2024-27564",
|
|
216
|
+
"severity": "MEDIUM", "fix": "2.51.0"},
|
|
217
|
+
# kubernetes
|
|
218
|
+
{"package": "kubernetes", "version": "<27.2.0", "cve": "CVE-2023-3893",
|
|
219
|
+
"severity": "MEDIUM", "fix": "27.2.0"},
|
|
220
|
+
# psutil
|
|
221
|
+
{"package": "psutil", "version": "<5.9.7", "cve": "CVE-2023-48060",
|
|
222
|
+
"severity": "MEDIUM", "fix": "5.9.7"},
|
|
223
|
+
# pygments (duplicate already, use different CVE)
|
|
224
|
+
{"package": "pygments", "version": "<2.14.0", "cve": "CVE-2023-38151",
|
|
225
|
+
"severity": "LOW", "fix": "2.14.0"},
|
|
226
|
+
# zipp
|
|
227
|
+
{"package": "zipp", "version": "<3.19.1", "cve": "CVE-2024-55638",
|
|
228
|
+
"severity": "MEDIUM", "fix": "3.19.1"},
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ── 版本比较工具 ─────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
def _parse_version(ver: str):
|
|
235
|
+
"""将版本字符串解析为整数元组,便于比较。"""
|
|
236
|
+
cleaned = re.sub(r'[^0-9.]', '', ver)
|
|
237
|
+
parts = cleaned.split('.')
|
|
238
|
+
try:
|
|
239
|
+
return tuple(int(p) for p in parts)
|
|
240
|
+
except ValueError:
|
|
241
|
+
return ()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _version_lt(installed: str, constraint: str) -> bool:
|
|
245
|
+
"""
|
|
246
|
+
判断 installed 是否 < constraint。
|
|
247
|
+
constraint 的格式是 '<X.Y.Z' 或 '<X.Y.Z...'。
|
|
248
|
+
"""
|
|
249
|
+
m = re.match(r'<([\d.]+)', constraint)
|
|
250
|
+
if not m:
|
|
251
|
+
return False
|
|
252
|
+
limit = m.group(1)
|
|
253
|
+
iv = _parse_version(installed)
|
|
254
|
+
lv = _parse_version(limit)
|
|
255
|
+
if not iv or not lv:
|
|
256
|
+
return False
|
|
257
|
+
return iv < lv
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ── 读取 requirements.txt ───────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
def parse_requirements(path: str = "requirements.txt") -> dict:
|
|
263
|
+
"""
|
|
264
|
+
解析 requirements.txt,返回 {包名(小写): 版本号} 的字典。
|
|
265
|
+
支持 `package==1.0`、`package>=1.0`、`package~=1.0`、`package` 等形式。
|
|
266
|
+
"""
|
|
267
|
+
deps = {}
|
|
268
|
+
if not os.path.isfile(path):
|
|
269
|
+
print(f"[!] 未找到依赖文件: {path}")
|
|
270
|
+
return deps
|
|
271
|
+
|
|
272
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
273
|
+
for line in f:
|
|
274
|
+
line = line.strip()
|
|
275
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
276
|
+
continue
|
|
277
|
+
# 去除行尾注释
|
|
278
|
+
if " #" in line:
|
|
279
|
+
line = line[:line.index(" #")]
|
|
280
|
+
# 匹配包名和版本约束
|
|
281
|
+
# 格式: package>=1.0,<2.0 或 package==1.0 或 package
|
|
282
|
+
m = re.match(r'([a-zA-Z0-9_.-]+)\s*([><=!~]+\s*[\d.*]+(?:\s*,\s*[><=!~]+\s*[\d.*]+)*)?', line)
|
|
283
|
+
if m:
|
|
284
|
+
name = m.group(1).lower().strip()
|
|
285
|
+
version_part = m.group(2)
|
|
286
|
+
# 取约束中的第一个具体版本号作为当前版本
|
|
287
|
+
if version_part:
|
|
288
|
+
vm = re.search(r'([\d.]+)', version_part)
|
|
289
|
+
version = vm.group(1) if vm else ""
|
|
290
|
+
else:
|
|
291
|
+
version = ""
|
|
292
|
+
deps[name] = version
|
|
293
|
+
return deps
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ── 漏洞匹配 ────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def match_vulnerabilities(deps: dict) -> list:
|
|
299
|
+
"""
|
|
300
|
+
对每个依赖,在 VULN_DB 中查找匹配的漏洞。
|
|
301
|
+
返回 [{"package", "installed", "cve", "severity", "fix"}, ...]
|
|
302
|
+
"""
|
|
303
|
+
results = []
|
|
304
|
+
for pkg, ver in deps.items():
|
|
305
|
+
if not ver:
|
|
306
|
+
continue
|
|
307
|
+
for vuln in VULN_DB:
|
|
308
|
+
if vuln["package"] != pkg:
|
|
309
|
+
continue
|
|
310
|
+
if _version_lt(ver, vuln["version"]):
|
|
311
|
+
results.append({
|
|
312
|
+
"package": pkg,
|
|
313
|
+
"installed": ver,
|
|
314
|
+
"cve": vuln["cve"],
|
|
315
|
+
"severity": vuln["severity"],
|
|
316
|
+
"fix": vuln["fix"],
|
|
317
|
+
})
|
|
318
|
+
return results
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ── 报告输出 ────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
def print_report(results: list):
|
|
324
|
+
"""打印格式化漏洞报告。"""
|
|
325
|
+
if not results:
|
|
326
|
+
print("\n[✓] 未发现已知漏洞依赖。")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# 按严重度排序:CRITICAL > HIGH > MEDIUM > LOW
|
|
330
|
+
severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
|
|
331
|
+
results.sort(key=lambda r: (severity_order.get(r["severity"], 9), r["package"]))
|
|
332
|
+
|
|
333
|
+
print("\n" + "=" * 70)
|
|
334
|
+
print(" 依赖漏洞扫描报告")
|
|
335
|
+
print("=" * 70)
|
|
336
|
+
print(f"{'包名':<20} {'当前版本':<12} {'CVE编号':<18} {'严重度':<10} {'建议升级'}")
|
|
337
|
+
print("-" * 70)
|
|
338
|
+
for r in results:
|
|
339
|
+
print(f"{r['package']:<20} {r['installed']:<12} {r['cve']:<18} {r['severity']:<10} {r['fix']}")
|
|
340
|
+
print("-" * 70)
|
|
341
|
+
print(f"总计: {len(results)} 个漏洞\n")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ── 主入口 ──────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
def run_scan(requirements_path: str = "requirements.txt", skip_deps: bool = False):
|
|
347
|
+
"""执行依赖安全扫描。"""
|
|
348
|
+
if skip_deps:
|
|
349
|
+
print("[i] --skip-deps 已启用,跳过依赖扫描。")
|
|
350
|
+
return []
|
|
351
|
+
|
|
352
|
+
print(f"[*] 正在扫描依赖文件: {requirements_path}")
|
|
353
|
+
deps = parse_requirements(requirements_path)
|
|
354
|
+
if not deps:
|
|
355
|
+
print("[i] 未解析到任何依赖,跳过漏洞匹配。")
|
|
356
|
+
return []
|
|
357
|
+
|
|
358
|
+
print(f"[*] 共发现 {len(deps)} 个依赖,正在匹配漏洞库 ({len(VULN_DB)} 条记录) ...")
|
|
359
|
+
results = match_vulnerabilities(deps)
|
|
360
|
+
print_report(results)
|
|
361
|
+
return results
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def main():
|
|
365
|
+
parser = argparse.ArgumentParser(description="Code Sentinel — 依赖安全扫描")
|
|
366
|
+
parser.add_argument("--requirements", "-r", default="requirements.txt",
|
|
367
|
+
help="requirements.txt 路径 (默认: requirements.txt)")
|
|
368
|
+
parser.add_argument("--skip-deps", action="store_true",
|
|
369
|
+
help="跳过依赖漏洞扫描")
|
|
370
|
+
args = parser.parse_args()
|
|
371
|
+
|
|
372
|
+
run_scan(args.requirements, skip_deps=args.skip_deps)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
main()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
380
|
+
# CodeScanner — 多维度代码静态分析(安全 / 性能 / 代码异味 / 依赖)
|
|
381
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class _BaseIssue:
|
|
385
|
+
"""问题基类,提供 dict 兼容接口"""
|
|
386
|
+
def __init__(self, file: str, line: int, description: str, severity: str, code: str = "", **kwargs):
|
|
387
|
+
self.file = file
|
|
388
|
+
self.line = line
|
|
389
|
+
self.description = description
|
|
390
|
+
self.severity = severity
|
|
391
|
+
self.code = code
|
|
392
|
+
for k, v in kwargs.items():
|
|
393
|
+
setattr(self, k, v)
|
|
394
|
+
|
|
395
|
+
def get(self, key: str, default=None):
|
|
396
|
+
return getattr(self, key, default)
|
|
397
|
+
|
|
398
|
+
def __getitem__(self, key: str):
|
|
399
|
+
return getattr(self, key)
|
|
400
|
+
|
|
401
|
+
def __contains__(self, key: str):
|
|
402
|
+
return hasattr(self, key)
|
|
403
|
+
|
|
404
|
+
def __repr__(self):
|
|
405
|
+
return f"{self.__class__.__name__}({self.file}:{self.line} [{self.severity}] {self.description})"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class SecurityIssue(_BaseIssue):
|
|
409
|
+
"""安全问题"""
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class PerformanceIssue(_BaseIssue):
|
|
414
|
+
"""性能问题"""
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class SmellIssue(_BaseIssue):
|
|
419
|
+
"""代码异味"""
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class DependencyIssue(_BaseIssue):
|
|
424
|
+
"""依赖风险"""
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ── 扫描器核心 ────────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class CodeScanner:
|
|
432
|
+
"""多维度代码扫描器:安全风险、性能问题、代码异味、依赖风险"""
|
|
433
|
+
|
|
434
|
+
def __init__(self, path: str):
|
|
435
|
+
self._path = path
|
|
436
|
+
self._results: Optional[dict] = None
|
|
437
|
+
self._all_issues: list = []
|
|
438
|
+
|
|
439
|
+
def scan(self) -> dict:
|
|
440
|
+
"""
|
|
441
|
+
执行全维度扫描,返回结构化结果 dict。
|
|
442
|
+
格式: { "summary": {...}, "security": [...], "performance": [...],
|
|
443
|
+
"smell": [...], "dependency": [...], "genome_core_integrated": bool }
|
|
444
|
+
"""
|
|
445
|
+
p = Path(self._path)
|
|
446
|
+
if not p.exists():
|
|
447
|
+
raise FileNotFoundError(f"路径不存在: {self._path}")
|
|
448
|
+
|
|
449
|
+
# 每次 scan 重新初始化问题列表(避免跨调用累积)
|
|
450
|
+
self._all_issues = []
|
|
451
|
+
sec_issues: list = []
|
|
452
|
+
perf_issues: list = []
|
|
453
|
+
smell_issues: list = []
|
|
454
|
+
dep_issues: list = []
|
|
455
|
+
|
|
456
|
+
issues: dict = {
|
|
457
|
+
"security": sec_issues,
|
|
458
|
+
"performance": perf_issues,
|
|
459
|
+
"smell": smell_issues,
|
|
460
|
+
"dependency": dep_issues,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if p.is_file():
|
|
464
|
+
files = [p]
|
|
465
|
+
else:
|
|
466
|
+
files = list(p.rglob("*"))
|
|
467
|
+
files = [f for f in files if f.is_file() and f.suffix in (".py", ".txt")]
|
|
468
|
+
|
|
469
|
+
for file_path in files:
|
|
470
|
+
if file_path.suffix == ".py":
|
|
471
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
472
|
+
self._scan_py_file(file_path, content, issues)
|
|
473
|
+
elif file_path.name == "requirements.txt":
|
|
474
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
475
|
+
self._scan_requirements(file_path, content, issues)
|
|
476
|
+
|
|
477
|
+
# ── 收集所有问题对象(用于 get_all_issues) ──
|
|
478
|
+
self._all_issues = sec_issues + perf_issues + smell_issues + dep_issues
|
|
479
|
+
|
|
480
|
+
# ── 计算摘要 ──
|
|
481
|
+
total = len(self._all_issues)
|
|
482
|
+
high = sum(1 for i in self._all_issues if i.severity.upper() in ("HIGH", "CRITICAL"))
|
|
483
|
+
medium = sum(1 for i in self._all_issues if i.severity.upper() == "MEDIUM")
|
|
484
|
+
low = sum(1 for i in self._all_issues if i.severity.upper() == "LOW")
|
|
485
|
+
|
|
486
|
+
# 将 issue 对象转换为普通 dict(兼容现有测试)
|
|
487
|
+
def _to_dict(item):
|
|
488
|
+
d = {
|
|
489
|
+
"file": item.file,
|
|
490
|
+
"line": item.line,
|
|
491
|
+
"description": item.description,
|
|
492
|
+
"severity": item.severity,
|
|
493
|
+
"code": item.code,
|
|
494
|
+
}
|
|
495
|
+
# 包含额外属性(如 version)
|
|
496
|
+
if hasattr(item, "version"):
|
|
497
|
+
d["version"] = item.version
|
|
498
|
+
return d
|
|
499
|
+
|
|
500
|
+
results: dict = {
|
|
501
|
+
"summary": {
|
|
502
|
+
"target": self._path,
|
|
503
|
+
"scanned_files": len(files),
|
|
504
|
+
"total": total,
|
|
505
|
+
"high": high,
|
|
506
|
+
"medium": medium,
|
|
507
|
+
"low": low,
|
|
508
|
+
},
|
|
509
|
+
"security": [_to_dict(i) for i in sec_issues],
|
|
510
|
+
"performance": [_to_dict(i) for i in perf_issues],
|
|
511
|
+
"smell": [_to_dict(i) for i in smell_issues],
|
|
512
|
+
"dependency": [_to_dict(i) for i in dep_issues],
|
|
513
|
+
"genome_core_integrated": HAS_GENOME_CORE,
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
self._results = results
|
|
517
|
+
return results
|
|
518
|
+
|
|
519
|
+
def get_all_issues(self) -> list:
|
|
520
|
+
"""返回所有问题对象列表(保持类型以便 isinstance 检查)"""
|
|
521
|
+
return self._all_issues
|
|
522
|
+
|
|
523
|
+
# ── Diff 扫描 ──────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
@staticmethod
|
|
526
|
+
def _parse_diff(diff_text: str) -> dict:
|
|
527
|
+
"""
|
|
528
|
+
解析 git diff 输出,提取每个文件中发生了变更的行号。
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
diff_text: git diff 输出文本
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
{文件路径: set(变更行号)}
|
|
535
|
+
只包含 ADDED/MODIFIED 行,不包含 DELETED 行。
|
|
536
|
+
"""
|
|
537
|
+
changed_lines: dict = {}
|
|
538
|
+
current_file = None
|
|
539
|
+
new_line_num = 0
|
|
540
|
+
|
|
541
|
+
for line in diff_text.split("\n"):
|
|
542
|
+
if line.startswith("diff --git "):
|
|
543
|
+
# 提取新文件路径 (b/xxx)
|
|
544
|
+
parts = line.split()
|
|
545
|
+
if len(parts) >= 4:
|
|
546
|
+
new_path = parts[3] # e.g. b/src/main.py
|
|
547
|
+
if new_path.startswith("b/"):
|
|
548
|
+
new_path = new_path[2:]
|
|
549
|
+
current_file = new_path
|
|
550
|
+
new_line_num = 0
|
|
551
|
+
elif line.startswith("+++ "):
|
|
552
|
+
b_path = line[4:]
|
|
553
|
+
if b_path.startswith("b/"):
|
|
554
|
+
b_path = b_path[2:]
|
|
555
|
+
current_file = b_path
|
|
556
|
+
new_line_num = 0
|
|
557
|
+
elif line.startswith("@@"):
|
|
558
|
+
# @@ -old_start,old_count +new_start,new_count @@
|
|
559
|
+
m = re.match(r'@@\s+-?\d+(?:,\d+)?\s+[+-]?(\d+)(?:,\d+)?\s+@@', line)
|
|
560
|
+
if m:
|
|
561
|
+
new_line_num = int(m.group(1))
|
|
562
|
+
elif current_file:
|
|
563
|
+
prefix = line[0] if line else ""
|
|
564
|
+
if prefix == "+" and not line.startswith("+++"):
|
|
565
|
+
# ADDED / MODIFIED 行 — 在最新文件中存在且内容变更
|
|
566
|
+
if current_file not in changed_lines:
|
|
567
|
+
changed_lines[current_file] = set()
|
|
568
|
+
changed_lines[current_file].add(new_line_num)
|
|
569
|
+
new_line_num += 1
|
|
570
|
+
elif prefix == " ":
|
|
571
|
+
new_line_num += 1
|
|
572
|
+
# '-' 行是已删除行,不影响新文件行号
|
|
573
|
+
|
|
574
|
+
return changed_lines
|
|
575
|
+
|
|
576
|
+
def scan_diff(self, diff_text: str) -> dict:
|
|
577
|
+
"""
|
|
578
|
+
扫描 git diff 中的变更行,只报告变更引入的问题。
|
|
579
|
+
|
|
580
|
+
用法::
|
|
581
|
+
scanner = CodeScanner(".")
|
|
582
|
+
results = scanner.scan_diff(diff_text)
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
diff_text: git diff 输出文本
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
与 scan() 相同格式的结果 dict,但只包含变更行引入的问题。
|
|
589
|
+
"""
|
|
590
|
+
changed = self._parse_diff(diff_text)
|
|
591
|
+
|
|
592
|
+
# 每次 scan_diff 重新初始化问题列表
|
|
593
|
+
self._all_issues = []
|
|
594
|
+
sec_issues: list = []
|
|
595
|
+
perf_issues: list = []
|
|
596
|
+
smell_issues: list = []
|
|
597
|
+
dep_issues: list = []
|
|
598
|
+
|
|
599
|
+
issues: dict = {
|
|
600
|
+
"security": sec_issues,
|
|
601
|
+
"performance": perf_issues,
|
|
602
|
+
"smell": smell_issues,
|
|
603
|
+
"dependency": dep_issues,
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
base_path = Path(self._path)
|
|
607
|
+
|
|
608
|
+
for file_path_str, changed_lines_set in changed.items():
|
|
609
|
+
full_path = base_path / file_path_str if base_path.is_dir() else Path(file_path_str)
|
|
610
|
+
if not full_path.exists():
|
|
611
|
+
continue
|
|
612
|
+
if full_path.suffix == ".py":
|
|
613
|
+
content = full_path.read_text(encoding="utf-8", errors="replace")
|
|
614
|
+
self._scan_py_file(full_path, content, issues, only_lines=changed_lines_set)
|
|
615
|
+
elif full_path.name == "requirements.txt":
|
|
616
|
+
content = full_path.read_text(encoding="utf-8", errors="replace")
|
|
617
|
+
self._scan_requirements(full_path, content, issues)
|
|
618
|
+
|
|
619
|
+
# ── 收集所有问题对象 ──
|
|
620
|
+
self._all_issues = sec_issues + perf_issues + smell_issues + dep_issues
|
|
621
|
+
|
|
622
|
+
# ── 计算摘要 ──
|
|
623
|
+
total = len(self._all_issues)
|
|
624
|
+
high = sum(1 for i in self._all_issues if i.severity.upper() in ("HIGH", "CRITICAL"))
|
|
625
|
+
medium = sum(1 for i in self._all_issues if i.severity.upper() == "MEDIUM")
|
|
626
|
+
low = sum(1 for i in self._all_issues if i.severity.upper() == "LOW")
|
|
627
|
+
|
|
628
|
+
def _to_dict(item):
|
|
629
|
+
d = {
|
|
630
|
+
"file": item.file,
|
|
631
|
+
"line": item.line,
|
|
632
|
+
"description": item.description,
|
|
633
|
+
"severity": item.severity,
|
|
634
|
+
"code": item.code,
|
|
635
|
+
}
|
|
636
|
+
if hasattr(item, "version"):
|
|
637
|
+
d["version"] = item.version
|
|
638
|
+
return d
|
|
639
|
+
|
|
640
|
+
results: dict = {
|
|
641
|
+
"summary": {
|
|
642
|
+
"target": f"diff ({len(changed)} files)",
|
|
643
|
+
"scanned_files": len(changed),
|
|
644
|
+
"total": total,
|
|
645
|
+
"high": high,
|
|
646
|
+
"medium": medium,
|
|
647
|
+
"low": low,
|
|
648
|
+
},
|
|
649
|
+
"security": [_to_dict(i) for i in sec_issues],
|
|
650
|
+
"performance": [_to_dict(i) for i in perf_issues],
|
|
651
|
+
"smell": [_to_dict(i) for i in smell_issues],
|
|
652
|
+
"dependency": [_to_dict(i) for i in dep_issues],
|
|
653
|
+
"genome_core_integrated": HAS_GENOME_CORE,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
self._results = results
|
|
657
|
+
return results
|
|
658
|
+
|
|
659
|
+
# ── Python 文件扫描 ──────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
def _scan_py_file(self, file_path: Path, content: str, issues: dict, only_lines: Optional[set] = None):
|
|
662
|
+
lines = content.split("\n")
|
|
663
|
+
|
|
664
|
+
sec = issues["security"]
|
|
665
|
+
perf = issues["performance"]
|
|
666
|
+
smell = issues["smell"]
|
|
667
|
+
|
|
668
|
+
for lineno, line in enumerate(lines, 1):
|
|
669
|
+
# 在 diff 模式下跳过非变更行
|
|
670
|
+
if only_lines is not None and lineno not in only_lines:
|
|
671
|
+
continue
|
|
672
|
+
|
|
673
|
+
stripped = line.strip()
|
|
674
|
+
|
|
675
|
+
# ── 安全风险 ──
|
|
676
|
+
if "eval(" in stripped and not stripped.startswith("#"):
|
|
677
|
+
sec.append(SecurityIssue(
|
|
678
|
+
file=str(file_path), line=lineno,
|
|
679
|
+
description="检测到 eval() 调用 — 可能导致代码注入",
|
|
680
|
+
severity="HIGH", code=stripped,
|
|
681
|
+
))
|
|
682
|
+
if "exec(" in stripped and not stripped.startswith("#"):
|
|
683
|
+
sec.append(SecurityIssue(
|
|
684
|
+
file=str(file_path), line=lineno,
|
|
685
|
+
description="检测到 exec() 调用 — 可能导致代码注入",
|
|
686
|
+
severity="HIGH", code=stripped,
|
|
687
|
+
))
|
|
688
|
+
if "os.system(" in stripped and not stripped.startswith("#"):
|
|
689
|
+
sec.append(SecurityIssue(
|
|
690
|
+
file=str(file_path), line=lineno,
|
|
691
|
+
description="检测到 os.system() 调用 — 建议使用 subprocess.run()",
|
|
692
|
+
severity="HIGH", code=stripped,
|
|
693
|
+
))
|
|
694
|
+
if "subprocess" in stripped and "shell=True" in stripped and not stripped.startswith("#"):
|
|
695
|
+
sec.append(SecurityIssue(
|
|
696
|
+
file=str(file_path), line=lineno,
|
|
697
|
+
description="检测到 subprocess + shell=True — 可能导致命令注入",
|
|
698
|
+
severity="HIGH", code=stripped,
|
|
699
|
+
))
|
|
700
|
+
|
|
701
|
+
# ── 性能问题:嵌套循环 ──
|
|
702
|
+
leading = len(line) - len(line.lstrip())
|
|
703
|
+
if stripped.startswith(("for ", "while ")):
|
|
704
|
+
for prev_lineno in range(lineno - 1, max(0, lineno - 20), -1):
|
|
705
|
+
prev_line = lines[prev_lineno - 1]
|
|
706
|
+
prev_leading = len(prev_line) - len(prev_line.lstrip())
|
|
707
|
+
prev_stripped = prev_line.strip()
|
|
708
|
+
if prev_leading < leading and prev_stripped.startswith(("for ", "while ")):
|
|
709
|
+
perf.append(PerformanceIssue(
|
|
710
|
+
file=str(file_path), line=lineno,
|
|
711
|
+
description="检测到嵌套循环 — 可能导致 O(n²) 性能问题",
|
|
712
|
+
severity="MEDIUM", code=stripped,
|
|
713
|
+
))
|
|
714
|
+
break
|
|
715
|
+
|
|
716
|
+
# ── 代码异味:过长函数 ──
|
|
717
|
+
in_function = False
|
|
718
|
+
func_start = 0
|
|
719
|
+
func_lines = 0
|
|
720
|
+
func_name = ""
|
|
721
|
+
for lineno, line in enumerate(lines, 1):
|
|
722
|
+
stripped = line.strip()
|
|
723
|
+
if re.match(r'^def\s+\w+\s*\(', stripped):
|
|
724
|
+
if in_function and func_lines > 50:
|
|
725
|
+
if only_lines is None or func_start in only_lines:
|
|
726
|
+
smell.append(SmellIssue(
|
|
727
|
+
file=str(file_path), line=func_start,
|
|
728
|
+
description=f"函数 `{func_name}` 过长 ({func_lines} 行) — 建议拆分为小函数",
|
|
729
|
+
severity="MEDIUM", code=f"def {func_name}(...):",
|
|
730
|
+
))
|
|
731
|
+
in_function = True
|
|
732
|
+
func_start = lineno
|
|
733
|
+
func_lines = 0
|
|
734
|
+
func_name = stripped[4:].split("(")[0].strip()
|
|
735
|
+
elif in_function:
|
|
736
|
+
func_lines += 1
|
|
737
|
+
if in_function and func_lines > 50:
|
|
738
|
+
if only_lines is None or func_start in only_lines:
|
|
739
|
+
smell.append(SmellIssue(
|
|
740
|
+
file=str(file_path), line=func_start,
|
|
741
|
+
description=f"函数 `{func_name}` 过长 ({func_lines} 行) — 建议拆分为小函数",
|
|
742
|
+
severity="MEDIUM", code=f"def {func_name}(...):",
|
|
743
|
+
))
|
|
744
|
+
|
|
745
|
+
# ── 代码异味:参数过多 ──
|
|
746
|
+
for lineno, line in enumerate(lines, 1):
|
|
747
|
+
if only_lines is not None and lineno not in only_lines:
|
|
748
|
+
continue
|
|
749
|
+
stripped = line.strip()
|
|
750
|
+
m = re.match(r'^def\s+\w+\s*\(([^)]+)\)', stripped)
|
|
751
|
+
if m:
|
|
752
|
+
params = [p.strip() for p in m.group(1).split(",") if p.strip() and p.strip() != "self"]
|
|
753
|
+
if len(params) > 5:
|
|
754
|
+
smell.append(SmellIssue(
|
|
755
|
+
file=str(file_path), line=lineno,
|
|
756
|
+
description=f"函数参数过多 ({len(params)} 个) — 建议使用数据类或拆解",
|
|
757
|
+
severity="LOW", code=stripped,
|
|
758
|
+
))
|
|
759
|
+
|
|
760
|
+
# ── 代码异味:裸 except ──
|
|
761
|
+
for lineno, line in enumerate(lines, 1):
|
|
762
|
+
if only_lines is not None and lineno not in only_lines:
|
|
763
|
+
continue
|
|
764
|
+
stripped = line.strip()
|
|
765
|
+
if stripped.startswith("except:") or stripped.startswith("except :"):
|
|
766
|
+
smell.append(SmellIssue(
|
|
767
|
+
file=str(file_path), line=lineno,
|
|
768
|
+
description="检测到裸 except — 建议指定异常类型如 except Exception:",
|
|
769
|
+
severity="MEDIUM", code=stripped,
|
|
770
|
+
))
|
|
771
|
+
|
|
772
|
+
# ── requirements.txt 扫描 ────────────────────────────────────
|
|
773
|
+
|
|
774
|
+
def _scan_requirements(self, file_path: Path, content: str, issues: dict):
|
|
775
|
+
deps = self._parse_req_lines(content)
|
|
776
|
+
for pkg, ver in deps.items():
|
|
777
|
+
if not ver:
|
|
778
|
+
# 未锁定版本 — 标记为 unpinned
|
|
779
|
+
issues["dependency"].append(DependencyIssue(
|
|
780
|
+
file=str(file_path), line=1,
|
|
781
|
+
description=f"依赖 `{pkg}` 未锁定版本",
|
|
782
|
+
severity="MEDIUM", code=f"{pkg}",
|
|
783
|
+
version="unpinned",
|
|
784
|
+
))
|
|
785
|
+
continue
|
|
786
|
+
for vuln in VULN_DB:
|
|
787
|
+
if vuln["package"] != pkg:
|
|
788
|
+
continue
|
|
789
|
+
if _version_lt(ver, vuln["version"]):
|
|
790
|
+
issues["dependency"].append(DependencyIssue(
|
|
791
|
+
file=str(file_path), line=1,
|
|
792
|
+
description=f"依赖 `{pkg}` ({ver}) 存在已知漏洞 {vuln['cve']} — 建议升级到 {vuln['fix']}",
|
|
793
|
+
severity=vuln["severity"].capitalize(),
|
|
794
|
+
code=f"{pkg}=={ver}",
|
|
795
|
+
))
|
|
796
|
+
|
|
797
|
+
@staticmethod
|
|
798
|
+
def _parse_req_lines(content: str) -> dict:
|
|
799
|
+
"""简化的 requirements.txt 解析"""
|
|
800
|
+
deps = {}
|
|
801
|
+
for line in content.split("\n"):
|
|
802
|
+
line = line.strip()
|
|
803
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
804
|
+
continue
|
|
805
|
+
if " #" in line:
|
|
806
|
+
line = line[:line.index(" #")]
|
|
807
|
+
m = re.match(r'([a-zA-Z0-9_.-]+)\s*([><=!~]+\s*[\d.*]+(?:\s*,\s*[><=!~]+\s*[\d.*]+)*)?', line)
|
|
808
|
+
if m:
|
|
809
|
+
name = m.group(1).lower().strip()
|
|
810
|
+
version_part = m.group(2)
|
|
811
|
+
if version_part:
|
|
812
|
+
vm = re.search(r'([\d.]+)', version_part)
|
|
813
|
+
version = vm.group(1) if vm else ""
|
|
814
|
+
else:
|
|
815
|
+
version = ""
|
|
816
|
+
deps[name] = version
|
|
817
|
+
return deps
|