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.
@@ -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