super-dev 2.0.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.
Files changed (61) hide show
  1. super_dev/__init__.py +11 -0
  2. super_dev/analyzer/__init__.py +34 -0
  3. super_dev/analyzer/analyzer.py +440 -0
  4. super_dev/analyzer/detectors.py +511 -0
  5. super_dev/analyzer/models.py +285 -0
  6. super_dev/cli.py +3257 -0
  7. super_dev/config/__init__.py +11 -0
  8. super_dev/config/frontend.py +557 -0
  9. super_dev/config/manager.py +281 -0
  10. super_dev/creators/__init__.py +26 -0
  11. super_dev/creators/creator.py +134 -0
  12. super_dev/creators/document_generator.py +2473 -0
  13. super_dev/creators/frontend_builder.py +371 -0
  14. super_dev/creators/implementation_builder.py +789 -0
  15. super_dev/creators/prompt_generator.py +289 -0
  16. super_dev/creators/requirement_parser.py +354 -0
  17. super_dev/creators/spec_builder.py +195 -0
  18. super_dev/deployers/__init__.py +20 -0
  19. super_dev/deployers/cicd.py +1269 -0
  20. super_dev/deployers/delivery.py +229 -0
  21. super_dev/deployers/migration.py +1032 -0
  22. super_dev/design/__init__.py +74 -0
  23. super_dev/design/aesthetics.py +530 -0
  24. super_dev/design/charts.py +396 -0
  25. super_dev/design/codegen.py +379 -0
  26. super_dev/design/engine.py +528 -0
  27. super_dev/design/generator.py +395 -0
  28. super_dev/design/landing.py +422 -0
  29. super_dev/design/tech_stack.py +524 -0
  30. super_dev/design/tokens.py +269 -0
  31. super_dev/design/ux_guide.py +391 -0
  32. super_dev/exceptions.py +119 -0
  33. super_dev/experts/__init__.py +19 -0
  34. super_dev/experts/service.py +161 -0
  35. super_dev/integrations/__init__.py +7 -0
  36. super_dev/integrations/manager.py +264 -0
  37. super_dev/orchestrator/__init__.py +12 -0
  38. super_dev/orchestrator/engine.py +958 -0
  39. super_dev/orchestrator/experts.py +423 -0
  40. super_dev/orchestrator/knowledge.py +352 -0
  41. super_dev/orchestrator/quality.py +356 -0
  42. super_dev/reviewers/__init__.py +17 -0
  43. super_dev/reviewers/code_review.py +471 -0
  44. super_dev/reviewers/quality_gate.py +964 -0
  45. super_dev/reviewers/redteam.py +881 -0
  46. super_dev/skills/__init__.py +7 -0
  47. super_dev/skills/manager.py +307 -0
  48. super_dev/specs/__init__.py +44 -0
  49. super_dev/specs/generator.py +264 -0
  50. super_dev/specs/manager.py +428 -0
  51. super_dev/specs/models.py +348 -0
  52. super_dev/specs/validator.py +415 -0
  53. super_dev/utils/__init__.py +11 -0
  54. super_dev/utils/logger.py +133 -0
  55. super_dev/web/api.py +1402 -0
  56. super_dev-2.0.0.dist-info/METADATA +252 -0
  57. super_dev-2.0.0.dist-info/RECORD +61 -0
  58. super_dev-2.0.0.dist-info/WHEEL +5 -0
  59. super_dev-2.0.0.dist-info/entry_points.txt +2 -0
  60. super_dev-2.0.0.dist-info/licenses/LICENSE +21 -0
  61. super_dev-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,881 @@
1
+ """
2
+ 红队审查器 - 安全、性能、架构审查
3
+
4
+ 开发:Excellent(11964948@qq.com)
5
+ 功能:模拟红队视角,全面审查项目安全性、性能和架构
6
+ 作用:在开发前发现问题,确保质量
7
+ 创建时间:2025-12-30
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import shutil
14
+ import subprocess # nosec B404
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+
18
+
19
+ @dataclass
20
+ class SecurityIssue:
21
+ """安全问题"""
22
+ severity: str # critical, high, medium, low
23
+ category: str # injection, auth, xss, csrf, etc.
24
+ description: str
25
+ recommendation: str
26
+ cwe: str | None = None
27
+ file_path: str | None = None
28
+ line: int | None = None
29
+
30
+
31
+ @dataclass
32
+ class PerformanceIssue:
33
+ """性能问题"""
34
+ severity: str # critical, high, medium, low
35
+ category: str # database, api, frontend, infrastructure
36
+ description: str
37
+ recommendation: str
38
+ impact: str = ""
39
+ file_path: str | None = None
40
+ line: int | None = None
41
+
42
+
43
+ @dataclass
44
+ class ArchitectureIssue:
45
+ """架构问题"""
46
+ severity: str # critical, high, medium, low
47
+ category: str # scalability, maintainability, reliability
48
+ description: str
49
+ recommendation: str
50
+ adr_needed: bool = False
51
+ file_path: str | None = None
52
+ line: int | None = None
53
+
54
+
55
+ @dataclass
56
+ class RedTeamReport:
57
+ """红队审查报告"""
58
+ project_name: str
59
+ security_issues: list[SecurityIssue] = field(default_factory=list)
60
+ performance_issues: list[PerformanceIssue] = field(default_factory=list)
61
+ architecture_issues: list[ArchitectureIssue] = field(default_factory=list)
62
+ pass_threshold: int = 70
63
+
64
+ @property
65
+ def critical_count(self) -> int:
66
+ return (
67
+ sum(1 for i in self.security_issues if i.severity == "critical") +
68
+ sum(1 for i in self.performance_issues if i.severity == "critical") +
69
+ sum(1 for i in self.architecture_issues if i.severity == "critical")
70
+ )
71
+
72
+ @property
73
+ def high_count(self) -> int:
74
+ return (
75
+ sum(1 for i in self.security_issues if i.severity == "high") +
76
+ sum(1 for i in self.performance_issues if i.severity == "high") +
77
+ sum(1 for i in self.architecture_issues if i.severity == "high")
78
+ )
79
+
80
+ @property
81
+ def total_score(self) -> int:
82
+ """计算总分 (0-100)"""
83
+ base_score = 100
84
+
85
+ # 扣分标准
86
+ for security_issue in self.security_issues:
87
+ if security_issue.severity == "critical":
88
+ base_score -= 20
89
+ elif security_issue.severity == "high":
90
+ base_score -= 10
91
+ elif security_issue.severity == "medium":
92
+ base_score -= 5
93
+ else:
94
+ base_score -= 2
95
+
96
+ for performance_issue in self.performance_issues:
97
+ if performance_issue.severity == "critical":
98
+ base_score -= 15
99
+ elif performance_issue.severity == "high":
100
+ base_score -= 8
101
+ elif performance_issue.severity == "medium":
102
+ base_score -= 4
103
+ else:
104
+ base_score -= 1
105
+
106
+ for architecture_issue in self.architecture_issues:
107
+ if architecture_issue.severity == "critical":
108
+ base_score -= 15
109
+ elif architecture_issue.severity == "high":
110
+ base_score -= 8
111
+ elif architecture_issue.severity == "medium":
112
+ base_score -= 4
113
+ else:
114
+ base_score -= 1
115
+
116
+ return max(0, base_score)
117
+
118
+ @property
119
+ def passed(self) -> bool:
120
+ """红队是否通过(无 critical 且得分达到阈值)"""
121
+ return self.critical_count == 0 and self.total_score >= self.pass_threshold
122
+
123
+ @property
124
+ def blocking_reasons(self) -> list[str]:
125
+ reasons: list[str] = []
126
+ if self.critical_count > 0:
127
+ reasons.append(f"存在 {self.critical_count} 个 critical 问题")
128
+ if self.total_score < self.pass_threshold:
129
+ reasons.append(f"红队评分 {self.total_score} 低于阈值 {self.pass_threshold}")
130
+ return reasons
131
+
132
+ def to_markdown(self) -> str:
133
+ """生成 Markdown 报告"""
134
+ lines = [
135
+ f"# {self.project_name} - 红队审查报告",
136
+ "",
137
+ "> **审查时间**: 自动生成",
138
+ f"> **总分**: {self.total_score}/100",
139
+ f"> **通过阈值**: {self.pass_threshold}",
140
+ "",
141
+ "---",
142
+ "",
143
+ "## 执行摘要",
144
+ "",
145
+ f"- **Critical 问题**: {self.critical_count}",
146
+ f"- **High 问题**: {self.high_count}",
147
+ f"- **总分**: {self.total_score}/100",
148
+ "",
149
+ ]
150
+
151
+ if not self.passed:
152
+ lines.append("**状态**: 未通过质量门禁 - 需要修复关键问题后重新审查")
153
+ elif self.total_score < 80:
154
+ lines.append("**状态**: 有条件通过 - 建议修复 High 级别问题")
155
+ else:
156
+ lines.append("**状态**: 通过 - 质量良好")
157
+
158
+ lines.extend(["", "---", ""])
159
+
160
+ # 安全审查
161
+ lines.extend([
162
+ "## 1. 安全审查",
163
+ "",
164
+ ])
165
+
166
+ if not self.security_issues:
167
+ lines.append("未发现明显的安全问题。")
168
+ else:
169
+ lines.append("| 严重性 | 类别 | 描述 | 建议 |")
170
+ lines.append("|:---|:---|:---|:---|")
171
+ for issue in self.security_issues:
172
+ cwe_ref = f" ({issue.cwe})" if issue.cwe else ""
173
+ lines.append(
174
+ f"| {issue.severity} | {issue.category}{cwe_ref} | {issue.description} | {issue.recommendation} |"
175
+ )
176
+
177
+ lines.extend(["", "---", ""])
178
+
179
+ # 性能审查
180
+ lines.extend([
181
+ "## 2. 性能审查",
182
+ "",
183
+ ])
184
+
185
+ if not self.performance_issues:
186
+ lines.append("未发现明显的性能问题。")
187
+ else:
188
+ lines.append("| 严重性 | 类别 | 描述 | 影响 | 建议 |")
189
+ lines.append("|:---|:---|:---|:---|:---|")
190
+ for performance_issue in self.performance_issues:
191
+ lines.append(
192
+ f"| {performance_issue.severity} | {performance_issue.category} | {performance_issue.description} | {performance_issue.impact} | {performance_issue.recommendation} |"
193
+ )
194
+
195
+ lines.extend(["", "---", ""])
196
+
197
+ # 架构审查
198
+ lines.extend([
199
+ "## 3. 架构审查",
200
+ "",
201
+ ])
202
+
203
+ if not self.architecture_issues:
204
+ lines.append("未发现明显的架构问题。")
205
+ else:
206
+ lines.append("| 严重性 | 类别 | 描述 | 需要 ADR | 建议 |")
207
+ lines.append("|:---|:---|:---|:---:|:---|")
208
+ for architecture_issue in self.architecture_issues:
209
+ adr = "是" if architecture_issue.adr_needed else "否"
210
+ lines.append(
211
+ f"| {architecture_issue.severity} | {architecture_issue.category} | {architecture_issue.description} | {adr} | {architecture_issue.recommendation} |"
212
+ )
213
+
214
+ lines.extend(["", "---", ""])
215
+
216
+ # 改进建议
217
+ lines.extend([
218
+ "## 4. 改进建议",
219
+ "",
220
+ "### 优先级 P0 (立即修复)",
221
+ "",
222
+ ])
223
+
224
+ p0_issues: list[SecurityIssue | PerformanceIssue | ArchitectureIssue] = [
225
+ i for i in self.security_issues + self.performance_issues + self.architecture_issues
226
+ if i.severity in ("critical", "high")
227
+ ]
228
+
229
+ if not p0_issues:
230
+ lines.append("无 P0 级别问题。")
231
+ else:
232
+ for idx, issue_item in enumerate(p0_issues, 1):
233
+ issue_type = "安全" if issue_item in self.security_issues else "性能" if issue_item in self.performance_issues else "架构"
234
+ lines.append(f"{idx}. [{issue_type}] {issue_item.description}")
235
+ lines.append(f" - 建议: {issue_item.recommendation}")
236
+ lines.append("")
237
+
238
+ lines.extend([
239
+ "### 优先级 P1 (尽快修复)",
240
+ "",
241
+ ])
242
+
243
+ p1_issues: list[SecurityIssue | PerformanceIssue | ArchitectureIssue] = [
244
+ i for i in self.security_issues + self.performance_issues + self.architecture_issues
245
+ if i.severity == "medium"
246
+ ]
247
+
248
+ if not p1_issues:
249
+ lines.append("无 P1 级别问题。")
250
+ else:
251
+ for idx, issue_item in enumerate(p1_issues, 1):
252
+ issue_type = "安全" if issue_item in self.security_issues else "性能" if issue_item in self.performance_issues else "架构"
253
+ lines.append(f"{idx}. [{issue_type}] {issue_item.description}")
254
+ lines.append(f" - 建议: {issue_item.recommendation}")
255
+ lines.append("")
256
+
257
+ return "\n".join(lines)
258
+
259
+
260
+ class RedTeamReviewer:
261
+ """红队审查器"""
262
+
263
+ _CODE_EXTENSIONS = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".java", ".sql"}
264
+ _SKIP_DIRS = {
265
+ ".git", ".idea", ".vscode", "node_modules", "__pycache__", ".pytest_cache",
266
+ ".mypy_cache", ".ruff_cache", "dist", "build", "output", ".super-dev", "logs",
267
+ ".venv", "venv", ".tox", ".cache", "coverage", "htmlcov", ".next", ".nuxt",
268
+ }
269
+ _PREFERRED_SCAN_DIRS = (
270
+ "backend", "frontend", "src", "app", "server", "api", "services", "lib", "super_dev"
271
+ )
272
+ _MAX_SCAN_FILES = 220
273
+ _MAX_FILE_SIZE = 300_000
274
+
275
+ def __init__(self, project_dir: Path, name: str, tech_stack: dict):
276
+ self.project_dir = Path(project_dir).resolve()
277
+ self.name = name
278
+ self.tech_stack = tech_stack
279
+ self.platform = tech_stack.get("platform", "web")
280
+ self.frontend = tech_stack.get("frontend", "react")
281
+ self.backend = tech_stack.get("backend", "node")
282
+ self.domain = tech_stack.get("domain", "")
283
+ self._source_file_cache: list[tuple[Path, str]] | None = None
284
+ self.enable_tool_scans = os.getenv("SUPER_DEV_ENABLE_TOOL_SCANS", "1").strip().lower() not in {
285
+ "0",
286
+ "false",
287
+ "no",
288
+ }
289
+
290
+ def review(self) -> RedTeamReport:
291
+ """执行完整红队审查"""
292
+ report = RedTeamReport(project_name=self.name)
293
+
294
+ # 安全审查
295
+ report.security_issues = self._review_security()
296
+
297
+ # 性能审查
298
+ report.performance_issues = self._review_performance()
299
+
300
+ # 架构审查
301
+ report.architecture_issues = self._review_architecture()
302
+
303
+ return report
304
+
305
+ def _review_security(self) -> list[SecurityIssue]:
306
+ """安全审查"""
307
+ issues: list[SecurityIssue] = []
308
+ issue_keys: set[tuple[str, str]] = set()
309
+
310
+ # 1) 扫描代码中的高风险模式(真实信号)
311
+ secret_pattern = re.compile(
312
+ r'(?i)\b(api[_-]?key|secret|token|password)\b\s*[:=]\s*["\']([^"\']{6,})["\']'
313
+ )
314
+ dangerous_rules = [
315
+ ("命令执行", re.compile(r"\bsubprocess\.(run|call|Popen)\([^)]*shell\s*=\s*True"), "CWE-78",
316
+ "避免 shell=True,改为参数数组执行并进行输入白名单校验"),
317
+ ("动态执行", re.compile(r"\b(eval|exec)\s*\("), "CWE-95",
318
+ "避免 eval/exec,使用安全解析器或受限表达式引擎"),
319
+ ("动态命令", re.compile(r"child_process\.exec\s*\("), "CWE-78",
320
+ "改用 execFile/spawn 并限定允许命令集合"),
321
+ ("SQL 注入", re.compile(r'(?i)(select|insert|update|delete)[^\n]{0,120}\+'), "CWE-89",
322
+ "避免字符串拼接 SQL,统一使用参数化查询/ORM"),
323
+ ]
324
+
325
+ for file_path, content in self._iter_source_files_with_content():
326
+ if self._is_yaml_file(file_path):
327
+ # 对配置文件只做轻量提示,不做高危判定
328
+ continue
329
+
330
+ # 硬编码凭据
331
+ for match in secret_pattern.finditer(content):
332
+ value = match.group(2).strip()
333
+ if self._looks_like_placeholder(value):
334
+ continue
335
+ line_no = self._line_number_from_offset(content, match.start())
336
+ issue_key = (str(file_path), "硬编码凭据")
337
+ if issue_key in issue_keys:
338
+ continue
339
+ issue_keys.add(issue_key)
340
+ issues.append(SecurityIssue(
341
+ severity="high",
342
+ category="硬编码凭据",
343
+ description=f"检测到疑似硬编码敏感信息: {file_path.name}:{line_no}",
344
+ recommendation="将密钥迁移到环境变量或密钥管理服务(Vault/KMS)",
345
+ cwe="CWE-798",
346
+ file_path=str(file_path),
347
+ line=line_no,
348
+ ))
349
+ break
350
+
351
+ for category, pattern, cwe, recommendation in dangerous_rules:
352
+ rule_match = pattern.search(content)
353
+ if not rule_match:
354
+ continue
355
+ issue_key = (str(file_path), category)
356
+ if issue_key in issue_keys:
357
+ continue
358
+ issue_keys.add(issue_key)
359
+ line_no = self._line_number_from_offset(content, rule_match.start())
360
+ issues.append(SecurityIssue(
361
+ severity="high",
362
+ category=category,
363
+ description=f"检测到高风险代码模式: {file_path.name}:{line_no}",
364
+ recommendation=recommendation,
365
+ cwe=cwe,
366
+ file_path=str(file_path),
367
+ line=line_no,
368
+ ))
369
+
370
+ # 2) 框架级最低安全基线(中低风险建议)
371
+ if self.backend != "none":
372
+ issues.append(SecurityIssue(
373
+ severity="medium",
374
+ category="认证",
375
+ description="建议统一鉴权中间件并对关键接口做细粒度权限控制",
376
+ recommendation="采用 JWT/Session + RBAC/ABAC,补充关键操作审计日志",
377
+ cwe="CWE-287",
378
+ ))
379
+ issues.append(SecurityIssue(
380
+ severity="medium",
381
+ category="速率限制",
382
+ description="建议对登录、注册、重置密码等敏感接口启用限流",
383
+ recommendation="采用令牌桶/滑动窗口算法并记录触发日志",
384
+ cwe="CWE-770",
385
+ ))
386
+
387
+ # 领域特定安全
388
+ if self.domain == "fintech":
389
+ issues.extend([
390
+ SecurityIssue(
391
+ severity="high",
392
+ category="PCI-DSS",
393
+ description="金融场景需完成支付数据合规评估与密钥分级管理",
394
+ recommendation="补充 PCI-DSS 控制项自检并输出合规矩阵",
395
+ cwe="CWE-320"
396
+ ),
397
+ SecurityIssue(
398
+ severity="high",
399
+ category="审计",
400
+ description="金融核心流程建议实施不可篡改审计链路",
401
+ recommendation="关键交易日志上链或做 WORM 存储并定期核验",
402
+ cwe="CWE-778"
403
+ ),
404
+ ])
405
+ elif self.domain == "medical":
406
+ issues.extend([
407
+ SecurityIssue(
408
+ severity="high",
409
+ category="HIPAA",
410
+ description="医疗数据必须符合 HIPAA 标准",
411
+ recommendation="实施数据加密、访问控制、审计日志",
412
+ cwe="CWE-200"
413
+ ),
414
+ ])
415
+
416
+ if self.enable_tool_scans:
417
+ issues.extend(self._run_optional_security_tool_scans())
418
+
419
+ return issues
420
+
421
+ def _review_performance(self) -> list[PerformanceIssue]:
422
+ """性能审查"""
423
+ issues: list[PerformanceIssue] = []
424
+ issue_keys: set[tuple[str, str]] = set()
425
+
426
+ async_requests_pattern = re.compile(r"async\s+def[\s\S]{0,300}requests\.(get|post|put|delete)\(")
427
+ n_plus_one_pattern = re.compile(r"for\s+.+:\s*[\r\n]+\s*.+\.(find|get|query|select)\(")
428
+
429
+ for file_path, content in self._iter_source_files_with_content():
430
+ line_count = content.count("\n") + 1
431
+
432
+ async_match = async_requests_pattern.search(content)
433
+ if async_match and (str(file_path), "API") not in issue_keys:
434
+ issue_keys.add((str(file_path), "API"))
435
+ line_no = self._line_number_from_offset(content, async_match.start())
436
+ issues.append(PerformanceIssue(
437
+ severity="high",
438
+ category="API",
439
+ description=f"异步上下文中检测到同步 HTTP 调用: {file_path.name}:{line_no}",
440
+ recommendation="在 async 流程中使用异步 HTTP 客户端(httpx.AsyncClient/aiohttp)",
441
+ impact="阻塞事件循环,增加高并发下请求延迟",
442
+ file_path=str(file_path),
443
+ line=line_no,
444
+ ))
445
+
446
+ n_plus_one_match = n_plus_one_pattern.search(content)
447
+ if n_plus_one_match and (str(file_path), "数据库") not in issue_keys:
448
+ issue_keys.add((str(file_path), "数据库"))
449
+ line_no = self._line_number_from_offset(content, n_plus_one_match.start())
450
+ issues.append(PerformanceIssue(
451
+ severity="medium",
452
+ category="数据库",
453
+ description=f"疑似 N+1 查询模式: {file_path.name}:{line_no}",
454
+ recommendation="批量查询或预加载关联数据,减少循环内 DB 调用",
455
+ impact="高数据量场景响应时间线性恶化",
456
+ file_path=str(file_path),
457
+ line=line_no,
458
+ ))
459
+
460
+ if line_count > 1200:
461
+ issues.append(PerformanceIssue(
462
+ severity="medium",
463
+ category="代码结构",
464
+ description=f"超大文件可能影响维护与性能优化: {file_path.name} ({line_count} 行)",
465
+ recommendation="拆分模块并隔离热点路径,便于单点性能调优",
466
+ impact="性能问题定位与重构成本升高",
467
+ ))
468
+
469
+ # 基线建议
470
+ if self.backend != "none":
471
+ issues.append(PerformanceIssue(
472
+ severity="medium",
473
+ category="数据库",
474
+ description="建议关键查询路径补齐索引与慢查询观测",
475
+ recommendation="建立慢查询阈值与索引基线,持续回归",
476
+ impact="降低接口尾延迟并提升吞吐稳定性",
477
+ ))
478
+ if self.frontend != "none":
479
+ issues.append(PerformanceIssue(
480
+ severity="medium",
481
+ category="前端",
482
+ description="建议实施代码分割与静态资源缓存策略",
483
+ recommendation="按路由拆包并配置长期缓存 + 指纹文件名",
484
+ impact="首屏加载与重复访问体验提升",
485
+ ))
486
+
487
+ return issues
488
+
489
+ def _review_architecture(self) -> list[ArchitectureIssue]:
490
+ """架构审查"""
491
+ issues: list[ArchitectureIssue] = []
492
+ source_files = self._iter_source_files_with_content()
493
+
494
+ if not self._has_test_assets(source_files):
495
+ issues.append(ArchitectureIssue(
496
+ severity="high",
497
+ category="可维护性",
498
+ description="未检测到 tests 目录,回归保障不足",
499
+ recommendation="建立单元/集成测试目录并纳入 CI 必跑策略",
500
+ adr_needed=False,
501
+ ))
502
+
503
+ has_health_endpoint = False
504
+ for _file_path, content in source_files:
505
+ if re.search(r"/health|health_check|healthcheck", content, re.IGNORECASE):
506
+ has_health_endpoint = True
507
+ break
508
+
509
+ if self.backend != "none" and not has_health_endpoint:
510
+ issues.append(ArchitectureIssue(
511
+ severity="medium",
512
+ category="可靠性",
513
+ description="未检测到健康检查端点标记",
514
+ recommendation="增加 /health 与 /ready 端点并接入部署探针",
515
+ adr_needed=False,
516
+ ))
517
+
518
+ ci_files = [
519
+ self.project_dir / ".github" / "workflows" / "ci.yml",
520
+ self.project_dir / ".gitlab-ci.yml",
521
+ self.project_dir / "Jenkinsfile",
522
+ self.project_dir / ".azure-pipelines.yml",
523
+ self.project_dir / "bitbucket-pipelines.yml",
524
+ ]
525
+ if not any(p.exists() for p in ci_files):
526
+ issues.append(ArchitectureIssue(
527
+ severity="medium",
528
+ category="工程化",
529
+ description="未检测到 CI/CD 主流程配置",
530
+ recommendation="补齐至少一套 CI 流水线并将质量门禁前置",
531
+ adr_needed=True,
532
+ ))
533
+
534
+ largest_file = None
535
+ largest_lines = 0
536
+ for file_path, content in source_files:
537
+ line_count = content.count("\n") + 1
538
+ if line_count > largest_lines:
539
+ largest_file = file_path
540
+ largest_lines = line_count
541
+ if largest_file and largest_lines > 2000:
542
+ issues.append(ArchitectureIssue(
543
+ severity="high",
544
+ category="可维护性",
545
+ description=f"检测到超大单体文件: {largest_file.name} ({largest_lines} 行)",
546
+ recommendation="按业务边界拆分模块并定义明确接口契约",
547
+ adr_needed=True,
548
+ file_path=str(largest_file),
549
+ line=1,
550
+ ))
551
+ elif largest_file and largest_lines > 1200:
552
+ issues.append(ArchitectureIssue(
553
+ severity="medium",
554
+ category="可维护性",
555
+ description=f"检测到大文件: {largest_file.name} ({largest_lines} 行)",
556
+ recommendation="逐步拆分高复杂度模块并补充针对性测试",
557
+ adr_needed=True,
558
+ file_path=str(largest_file),
559
+ line=1,
560
+ ))
561
+
562
+ return issues
563
+
564
+ def _iter_source_files_with_content(self) -> list[tuple[Path, str]]:
565
+ if self._source_file_cache is not None:
566
+ return self._source_file_cache
567
+
568
+ files: list[tuple[Path, str]] = []
569
+ seen: set[Path] = set()
570
+
571
+ # 优先扫描常见源码目录,保证信噪比
572
+ for root_name in self._PREFERRED_SCAN_DIRS:
573
+ root = self.project_dir / root_name
574
+ if not root.exists() or not root.is_dir():
575
+ continue
576
+ self._collect_scannable_files(root, files, seen)
577
+ if len(files) >= self._MAX_SCAN_FILES:
578
+ self._source_file_cache = files
579
+ return files
580
+
581
+ # 回退到全项目扫描,避免目录命名不标准导致漏检
582
+ if len(files) < self._MAX_SCAN_FILES:
583
+ self._collect_scannable_files(self.project_dir, files, seen)
584
+
585
+ self._source_file_cache = files
586
+ return files
587
+
588
+ def _collect_scannable_files(
589
+ self, root: Path, files: list[tuple[Path, str]], seen: set[Path]
590
+ ) -> None:
591
+ for dirpath, dirnames, filenames in os.walk(root):
592
+ # 预剪枝,避免进入大型依赖目录
593
+ dirnames[:] = [d for d in dirnames if not self._should_skip_dir(d)]
594
+
595
+ for filename in filenames:
596
+ if len(files) >= self._MAX_SCAN_FILES:
597
+ return
598
+
599
+ path = Path(dirpath) / filename
600
+ if not self._is_scannable_file(path):
601
+ continue
602
+
603
+ try:
604
+ resolved = path.resolve()
605
+ except Exception:
606
+ resolved = path
607
+ if resolved in seen:
608
+ continue
609
+ seen.add(resolved)
610
+
611
+ try:
612
+ if path.stat().st_size > self._MAX_FILE_SIZE:
613
+ continue
614
+ except OSError:
615
+ continue
616
+
617
+ try:
618
+ content = path.read_text(encoding="utf-8", errors="ignore")
619
+ except OSError:
620
+ continue
621
+
622
+ files.append((path, content))
623
+
624
+ def _should_skip_dir(self, dirname: str) -> bool:
625
+ if dirname in self._SKIP_DIRS:
626
+ return True
627
+ return dirname.startswith(".") and dirname not in {".github"}
628
+
629
+ def _is_scannable_file(self, path: Path) -> bool:
630
+ suffix = path.suffix.lower()
631
+ return suffix in self._CODE_EXTENSIONS or self._is_yaml_file(path)
632
+
633
+ def _is_yaml_file(self, path: Path) -> bool:
634
+ return path.suffix.lower() in {".yml", ".yaml"}
635
+
636
+ def _looks_like_placeholder(self, value: str) -> bool:
637
+ lowered = value.lower()
638
+ placeholder_markers = (
639
+ "your-", "example", "placeholder", "changeme", "<value>", "*****", "dummy"
640
+ )
641
+ if any(marker in lowered for marker in placeholder_markers):
642
+ return True
643
+ if lowered in {"password", "secret", "token", "api_key"}:
644
+ return True
645
+ return False
646
+
647
+ def _line_number_from_offset(self, content: str, start: int) -> int:
648
+ return content.count("\n", 0, start) + 1
649
+
650
+ def _has_test_assets(self, source_files: list[tuple[Path, str]]) -> bool:
651
+ if (self.project_dir / "tests").exists():
652
+ return True
653
+ test_name_patterns = [
654
+ re.compile(r"^test_.*\.py$"),
655
+ re.compile(r".*_test\.py$"),
656
+ re.compile(r".*\.test\.(js|ts|jsx|tsx)$"),
657
+ re.compile(r".*\.spec\.(js|ts|jsx|tsx)$"),
658
+ ]
659
+ for file_path, _ in source_files:
660
+ name = file_path.name
661
+ if any(pattern.match(name) for pattern in test_name_patterns):
662
+ return True
663
+ return False
664
+
665
+ def _run_optional_security_tool_scans(self) -> list[SecurityIssue]:
666
+ issues: list[SecurityIssue] = []
667
+ issues.extend(self._scan_with_bandit())
668
+ issues.extend(self._scan_with_semgrep())
669
+ issues.extend(self._scan_with_npm_audit())
670
+ return issues
671
+
672
+ def _scan_with_bandit(self) -> list[SecurityIssue]:
673
+ if self.backend != "python":
674
+ return []
675
+
676
+ bandit_exec = shutil.which("bandit")
677
+ if not bandit_exec:
678
+ return []
679
+
680
+ targets = self._discover_bandit_targets()
681
+ if not targets:
682
+ return []
683
+
684
+ result = self._run_command(
685
+ [bandit_exec, "-r", *targets, "-f", "json", "-q"],
686
+ timeout=180,
687
+ )
688
+ if result["timed_out"]:
689
+ return [
690
+ SecurityIssue(
691
+ severity="medium",
692
+ category="Bandit",
693
+ description="Bandit 扫描超时,建议拆分扫描范围或增加超时时间",
694
+ recommendation="将 Bandit 拆分为增量扫描并在 CI 中并行执行",
695
+ cwe="CWE-693",
696
+ )
697
+ ]
698
+
699
+ stdout = str(result["stdout"] or "")
700
+ if not stdout.strip():
701
+ return []
702
+
703
+ try:
704
+ payload = json.loads(stdout)
705
+ except Exception:
706
+ return []
707
+
708
+ findings = payload.get("results", [])
709
+ issues: list[SecurityIssue] = []
710
+ for finding in findings[:10]:
711
+ severity = self._map_tool_severity(str(finding.get("issue_severity", "medium")))
712
+ file_name = str(finding.get("filename", "unknown"))
713
+ line_no = finding.get("line_number")
714
+ issue_text = str(finding.get("issue_text", "Bandit 发现潜在安全风险"))
715
+ cwe_data = finding.get("issue_cwe", {})
716
+ cwe_id = None
717
+ if isinstance(cwe_data, dict) and cwe_data.get("id"):
718
+ cwe_id = f"CWE-{cwe_data.get('id')}"
719
+ issues.append(
720
+ SecurityIssue(
721
+ severity=severity,
722
+ category="Bandit",
723
+ description=f"Bandit: {issue_text} ({Path(file_name).name}:{line_no})",
724
+ recommendation="参考 Bandit 建议修复并补充安全回归测试",
725
+ cwe=cwe_id,
726
+ file_path=file_name,
727
+ line=int(line_no) if isinstance(line_no, int) else None,
728
+ )
729
+ )
730
+ return issues
731
+
732
+ def _scan_with_semgrep(self) -> list[SecurityIssue]:
733
+ semgrep_exec = shutil.which("semgrep")
734
+ if not semgrep_exec:
735
+ return []
736
+
737
+ result = self._run_command(
738
+ [
739
+ semgrep_exec,
740
+ "--config",
741
+ "auto",
742
+ "--json",
743
+ "--quiet",
744
+ "--metrics=off",
745
+ ".",
746
+ ],
747
+ timeout=240,
748
+ )
749
+ if result["timed_out"]:
750
+ return []
751
+
752
+ stdout = str(result["stdout"] or "")
753
+ if not stdout.strip():
754
+ return []
755
+
756
+ try:
757
+ payload = json.loads(stdout)
758
+ except Exception:
759
+ return []
760
+
761
+ matches = payload.get("results", [])
762
+ issues: list[SecurityIssue] = []
763
+ for finding in matches[:12]:
764
+ extra = finding.get("extra", {}) if isinstance(finding.get("extra"), dict) else {}
765
+ severity = self._map_tool_severity(str(extra.get("severity", "medium")))
766
+ message = str(extra.get("message", "Semgrep 检测到潜在风险"))
767
+ path = str(finding.get("path", "unknown"))
768
+ start = finding.get("start", {})
769
+ line_no = start.get("line") if isinstance(start, dict) else None
770
+ issue_type = str(finding.get("check_id", "semgrep.rule"))
771
+ issues.append(
772
+ SecurityIssue(
773
+ severity=severity,
774
+ category="Semgrep",
775
+ description=f"Semgrep({issue_type}): {message}",
776
+ recommendation="按规则建议修复并补充针对性测试",
777
+ cwe=None,
778
+ file_path=path,
779
+ line=int(line_no) if isinstance(line_no, int) else None,
780
+ )
781
+ )
782
+ return issues
783
+
784
+ def _scan_with_npm_audit(self) -> list[SecurityIssue]:
785
+ npm_exec = shutil.which("npm")
786
+ if not npm_exec:
787
+ return []
788
+
789
+ issues: list[SecurityIssue] = []
790
+ for target in (self.project_dir / "frontend", self.project_dir / "backend"):
791
+ package_json = target / "package.json"
792
+ if not package_json.exists():
793
+ continue
794
+
795
+ result = self._run_command(
796
+ [npm_exec, "--prefix", str(target), "audit", "--json"],
797
+ timeout=240,
798
+ )
799
+ if result["timed_out"]:
800
+ continue
801
+
802
+ stdout = str(result["stdout"] or "")
803
+ if not stdout.strip():
804
+ continue
805
+
806
+ try:
807
+ payload = json.loads(stdout)
808
+ except Exception:
809
+ continue
810
+
811
+ metadata = payload.get("metadata", {}) if isinstance(payload.get("metadata"), dict) else {}
812
+ vuln = metadata.get("vulnerabilities", {}) if isinstance(metadata.get("vulnerabilities"), dict) else {}
813
+ critical = int(vuln.get("critical", 0) or 0)
814
+ high = int(vuln.get("high", 0) or 0)
815
+ if critical <= 0 and high <= 0:
816
+ continue
817
+
818
+ severity = "critical" if critical > 0 else "high"
819
+ issues.append(
820
+ SecurityIssue(
821
+ severity=severity,
822
+ category="依赖漏洞",
823
+ description=f"{target.name} 依赖存在漏洞: critical={critical}, high={high}",
824
+ recommendation="执行 npm audit fix,并升级高危依赖版本后回归验证",
825
+ cwe="CWE-1104",
826
+ file_path=str(package_json),
827
+ line=None,
828
+ )
829
+ )
830
+
831
+ return issues
832
+
833
+ def _discover_bandit_targets(self) -> list[str]:
834
+ candidates = ["super_dev", "backend", "src", "app", "server", "api", "services"]
835
+ targets: list[str] = []
836
+ for item in candidates:
837
+ path = self.project_dir / item
838
+ if path.exists() and path.is_dir():
839
+ targets.append(str(path))
840
+ return targets
841
+
842
+ def _map_tool_severity(self, raw: str) -> str:
843
+ value = raw.strip().lower()
844
+ if value in {"error", "critical"}:
845
+ return "critical"
846
+ if value in {"warning", "high"}:
847
+ return "high"
848
+ if value in {"info", "low"}:
849
+ return "low"
850
+ return "medium"
851
+
852
+ def _run_command(self, cmd: list[str], timeout: int = 120) -> dict[str, object]:
853
+ try:
854
+ completed = subprocess.run(
855
+ cmd,
856
+ cwd=str(self.project_dir),
857
+ capture_output=True,
858
+ text=True,
859
+ timeout=timeout,
860
+ check=False,
861
+ ) # nosec B603
862
+ return {
863
+ "returncode": completed.returncode,
864
+ "stdout": completed.stdout or "",
865
+ "stderr": completed.stderr or "",
866
+ "timed_out": False,
867
+ }
868
+ except subprocess.TimeoutExpired as e:
869
+ return {
870
+ "returncode": -1,
871
+ "stdout": (e.stdout or ""),
872
+ "stderr": (e.stderr or ""),
873
+ "timed_out": True,
874
+ }
875
+ except Exception as e:
876
+ return {
877
+ "returncode": -1,
878
+ "stdout": "",
879
+ "stderr": str(e),
880
+ "timed_out": False,
881
+ }