create-dwy 0.5.1 → 0.6.1

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 (156) hide show
  1. package/bin/index.js +6 -15
  2. package/package.json +3 -3
  3. package/src/create.js +61 -37
  4. package/src/sync.js +206 -61
  5. package/templates/claude-global/CLAUDE.md +92 -0
  6. package/templates/claude-global/rules/dwy-android-core.md +610 -0
  7. package/templates/claude-global/rules/dwy-api-design.md +110 -0
  8. package/templates/claude-global/rules/dwy-db-migration.md +46 -0
  9. package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
  10. package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
  11. package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
  12. package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
  13. package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
  14. package/templates/claude-global/rules/dwy-git-security.md +53 -0
  15. package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
  16. package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
  17. package/templates/claude-global/rules/dwy-project-structure.md +181 -0
  18. package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
  19. package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
  20. package/templates/claude-global/rules/dwy-python-orm.md +61 -0
  21. package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
  22. package/templates/claude-global/rules/dwy-python-testing.md +53 -0
  23. package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
  24. package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
  25. package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
  26. package/templates/claude-global/rules/dwy-vue-api.md +146 -0
  27. package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
  28. package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
  29. package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
  30. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  47. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  48. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  49. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  50. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  51. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  53. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  54. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  55. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  56. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  60. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  61. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  62. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  64. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  65. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  71. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  72. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  92. package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
  93. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  94. package/templates/project/backend/.dwy.hbs +5 -0
  95. package/templates/project/backend/.python-version +1 -0
  96. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  97. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  98. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  99. package/templates/project/backend/deploy.sh.hbs +18 -0
  100. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  101. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  102. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  103. package/templates/project/backend/src/app/database.py +10 -0
  104. package/templates/project/backend/src/app/dependencies.py +5 -0
  105. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  106. package/templates/project/backend/src/app/models/base.py +5 -0
  107. package/templates/project/web/CLAUDE.md.hbs +29 -36
  108. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  109. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  110. package/templates/project/web/apps/web/src/App.vue +9 -0
  111. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  112. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  113. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  114. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  115. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  116. package/templates/project/web/apps/web/src/style.css +2 -0
  117. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  118. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  119. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  120. package/templates/project/web/apps/web/tsconfig.json +6 -0
  121. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  122. package/templates/project/web/package.json.hbs +16 -0
  123. package/templates/project/web/packages/.gitkeep +0 -0
  124. package/templates/project/web/pnpm-workspace.yaml +3 -0
  125. package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
  126. package/templates/claude-global/rules/agent-team-execution.md +0 -138
  127. package/templates/claude-global/rules/agent-team-patterns.md +0 -157
  128. package/templates/claude-global/rules/git-commit-convention.md +0 -106
  129. package/templates/claude-global/rules/git-security.md +0 -144
  130. package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
  131. package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
  132. package/templates/project/web/.claude/commands/deploy.md +0 -4
  133. package/templates/project/web/backend/src/app/models/base.py +0 -5
  134. package/templates/project/web/deploy.sh.hbs +0 -113
  135. package/templates/project/web/frontend/src/App.vue +0 -3
  136. package/templates/project/web/frontend/src/api/client.ts +0 -13
  137. package/templates/project/web/frontend/src/style.css +0 -1
  138. package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
  139. package/templates/project/web/frontend/tsconfig.app.json +0 -14
  140. package/templates/project/web/frontend/tsconfig.json +0 -4
  141. /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
  142. /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
  143. /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
  144. /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
  145. /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
  146. /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
  147. /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
  148. /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
  149. /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
  150. /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
  151. /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
  152. /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
  153. /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
  154. /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
  155. /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
  156. /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
@@ -0,0 +1,205 @@
1
+ """阶段 2:端口扫描 + 服务枚举(python-nmap,TCP/UDP 并发)。
2
+
3
+ 用法:
4
+ uv run pentest_scan.py --target <host> --output-dir <dir> [--sudo]
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ import time
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from pathlib import Path
12
+
13
+ import nmap
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ from lib.executor import AuditLog, run_command, save_json_result
18
+ from lib.validator import validate_target
19
+
20
+ console = Console()
21
+
22
+
23
+ def scan_tcp_ports(host: str, sudo: bool = False, audit: AuditLog | None = None) -> dict:
24
+ """TCP 端口扫描 + 服务版本探测(阶段 1/2/3 必须串行,因 2 依赖 1 的端口列表)。"""
25
+ results = {
26
+ "scan_type": "tcp",
27
+ "ports": [],
28
+ "os_detection": [],
29
+ "scan_args": "",
30
+ }
31
+
32
+ nm = nmap.PortScanner()
33
+
34
+ console.print("[bold]TCP 阶段 1: 快速端口扫描 (top 1000)...[/bold]")
35
+ scan_args = "-sT -T3 --top-ports 1000 --open"
36
+ if sudo:
37
+ scan_args = "-sS -T3 --top-ports 1000 --open"
38
+
39
+ try:
40
+ nm.scan(hosts=host, arguments=scan_args)
41
+ except nmap.PortScannerError as e:
42
+ console.print(f"[red]nmap 扫描失败: {e}[/red]")
43
+ console.print("[yellow]提示: SYN 扫描需要 sudo 权限,尝试不加 --sudo 运行[/yellow]")
44
+ return results
45
+
46
+ results["scan_args"] = scan_args
47
+ open_ports = []
48
+
49
+ if host in nm.all_hosts():
50
+ for proto in nm[host].all_protocols():
51
+ for port in sorted(nm[host][proto].keys()):
52
+ state = nm[host][proto][port]["state"]
53
+ if state == "open":
54
+ open_ports.append(port)
55
+
56
+ if not open_ports:
57
+ console.print("[yellow]未发现开放端口[/yellow]")
58
+ return results
59
+
60
+ console.print(f" 发现 {len(open_ports)} 个开放端口: {', '.join(map(str, open_ports[:20]))}")
61
+
62
+ # 阶段 2/3 并发:版本探测和 OS 检测可以同时跑(都基于阶段 1 结果,互不依赖)
63
+ console.print("[bold]TCP 阶段 2+3 并发: 版本探测 + OS 指纹识别...[/bold]")
64
+ port_str = ",".join(map(str, open_ports))
65
+
66
+ def _version_scan() -> nmap.PortScanner | None:
67
+ scanner = nmap.PortScanner()
68
+ try:
69
+ scanner.scan(hosts=host, arguments=f"-sV -sC -p {port_str}")
70
+ return scanner
71
+ except nmap.PortScannerError as e:
72
+ console.print(f"[yellow]版本探测失败: {e}[/yellow]")
73
+ return None
74
+
75
+ def _os_scan() -> nmap.PortScanner | None:
76
+ if not sudo:
77
+ return None
78
+ scanner = nmap.PortScanner()
79
+ try:
80
+ scanner.scan(hosts=host, arguments="-O")
81
+ return scanner
82
+ except Exception:
83
+ console.print("[yellow] OS 检测需要 sudo 权限[/yellow]")
84
+ return None
85
+
86
+ with ThreadPoolExecutor(max_workers=2) as pool:
87
+ ver_future = pool.submit(_version_scan)
88
+ os_future = pool.submit(_os_scan)
89
+
90
+ ver_scanner = ver_future.result()
91
+ os_scanner = os_future.result()
92
+
93
+ if ver_scanner and host in ver_scanner.all_hosts():
94
+ for proto in ver_scanner[host].all_protocols():
95
+ for port in sorted(ver_scanner[host][proto].keys()):
96
+ info = ver_scanner[host][proto][port]
97
+ port_data = {
98
+ "port": port,
99
+ "protocol": proto,
100
+ "state": info.get("state", ""),
101
+ "service": info.get("name", ""),
102
+ "version": info.get("version", ""),
103
+ "product": info.get("product", ""),
104
+ "extrainfo": info.get("extrainfo", ""),
105
+ "scripts": dict(info["script"]) if "script" in info else {},
106
+ }
107
+ results["ports"].append(port_data)
108
+
109
+ if os_scanner and host in os_scanner.all_hosts() and "osmatch" in os_scanner[host]:
110
+ for os_match in os_scanner[host]["osmatch"][:3]:
111
+ results["os_detection"].append({
112
+ "name": os_match.get("name", ""),
113
+ "accuracy": os_match.get("accuracy", ""),
114
+ })
115
+ console.print(f" OS: {os_match['name']} ({os_match['accuracy']}%)")
116
+
117
+ table = Table(title=f"端口扫描结果 — {host}")
118
+ table.add_column("端口", style="cyan")
119
+ table.add_column("协议")
120
+ table.add_column("服务", style="green")
121
+ table.add_column("版本")
122
+ table.add_column("附加信息")
123
+
124
+ for p in results["ports"]:
125
+ version_str = f"{p['product']} {p['version']}".strip()
126
+ table.add_row(str(p["port"]), p["protocol"], p["service"], version_str, p["extrainfo"])
127
+
128
+ console.print(table)
129
+ return results
130
+
131
+
132
+ def scan_udp_top(host: str, audit: AuditLog | None = None) -> dict:
133
+ """UDP 关键端口扫描(top 20)。"""
134
+ results = {"scan_type": "udp", "ports": []}
135
+
136
+ console.print("[bold]UDP 关键端口扫描 (top 20)...[/bold]")
137
+ cmd = ["nmap", "-sU", "--top-ports", "20", "--open", "-T3", host]
138
+ exec_result = run_command(cmd, timeout=120, audit=audit)
139
+
140
+ if exec_result.returncode == 0:
141
+ for line in exec_result.stdout.split("\n"):
142
+ parts = line.strip().split()
143
+ if parts and "/" in parts[0] and "open" in line:
144
+ port_proto = parts[0].split("/")
145
+ results["ports"].append({
146
+ "port": int(port_proto[0]),
147
+ "protocol": port_proto[1] if len(port_proto) > 1 else "udp",
148
+ "state": parts[1] if len(parts) > 1 else "open",
149
+ "service": parts[2] if len(parts) > 2 else "",
150
+ })
151
+ else:
152
+ console.print(f"[yellow]UDP 扫描失败(可能需要 sudo): {exec_result.stderr[:200]}[/yellow]")
153
+
154
+ return results
155
+
156
+
157
+ def main():
158
+ parser = argparse.ArgumentParser(description="渗透测试 - 端口扫描(TCP/UDP 并发)")
159
+ parser.add_argument("--target", required=True, help="目标 IP/域名")
160
+ parser.add_argument("--output-dir", required=True, help="输出目录")
161
+ parser.add_argument("--sudo", action="store_true", help="使用 sudo 进行 SYN/OS 扫描")
162
+ args = parser.parse_args()
163
+
164
+ target, warning = validate_target(args.target)
165
+ if target is None:
166
+ console.print(f"[red]{warning}[/red]")
167
+ sys.exit(1)
168
+ if warning:
169
+ console.print(f"[yellow]{warning}[/yellow]")
170
+
171
+ output_dir = Path(args.output_dir)
172
+ output_dir.mkdir(parents=True, exist_ok=True)
173
+ audit = AuditLog(output_dir / "audit.log")
174
+
175
+ # TCP 与 UDP 全程并发(两者完全独立)
176
+ console.print("[bold cyan]TCP / UDP 扫描并发启动[/bold cyan]")
177
+ with ThreadPoolExecutor(max_workers=2) as pool:
178
+ tcp_future = pool.submit(scan_tcp_ports, target.host, args.sudo, audit)
179
+ udp_future = pool.submit(scan_udp_top, target.host, audit)
180
+ tcp_result = tcp_future.result()
181
+ udp_result = udp_future.result()
182
+
183
+ result = {
184
+ "target": target.host,
185
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
186
+ "tcp": tcp_result,
187
+ "udp": udp_result,
188
+ }
189
+
190
+ output_file = output_dir / "scan_ports.json"
191
+ save_json_result(result, output_file)
192
+ console.print(f"\n[green]结果已保存: {output_file}[/green]")
193
+
194
+ open_ports = [p["port"] for p in result["tcp"]["ports"] if p.get("state") == "open"]
195
+ http_ports = [p["port"] for p in result["tcp"]["ports"] if p.get("service") in ("http", "https", "http-proxy", "http-alt")]
196
+ ssl_ports = [p["port"] for p in result["tcp"]["ports"] if p.get("service") in ("https", "ssl", "imaps", "smtps", "pop3s")]
197
+
198
+ console.print(f"\n[bold]摘要:[/bold]")
199
+ console.print(f" 开放端口: {open_ports}")
200
+ console.print(f" HTTP 端口: {http_ports}")
201
+ console.print(f" SSL/TLS 端口: {ssl_ports}")
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
@@ -0,0 +1,208 @@
1
+ """阶段 5:SSL/TLS 审计 — testssl.sh 封装 + Python ssl 基础检测(并发执行)。
2
+
3
+ 用法:
4
+ uv run pentest_ssl.py --target <host> --output-dir <dir> [--port 443]
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ import socket
10
+ import ssl
11
+ import sys
12
+ import time
13
+ from concurrent.futures import ThreadPoolExecutor
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ from rich.console import Console
18
+ from rich.table import Table
19
+
20
+ from lib.executor import AuditLog, run_command, save_json_result
21
+ from lib.severity import Severity
22
+
23
+ console = Console()
24
+
25
+
26
+ def check_certificate(host: str, port: int = 443) -> dict:
27
+ """Python ssl 模块基础证书检查。"""
28
+ result = {
29
+ "valid": False,
30
+ "issuer": "",
31
+ "subject": "",
32
+ "not_before": "",
33
+ "not_after": "",
34
+ "days_remaining": 0,
35
+ "self_signed": False,
36
+ "expired": False,
37
+ "errors": [],
38
+ }
39
+
40
+ try:
41
+ ctx = ssl.create_default_context()
42
+ with socket.create_connection((host, port), timeout=10) as sock:
43
+ with ctx.wrap_socket(sock, server_hostname=host) as ssock:
44
+ cert = ssock.getpeercert()
45
+ result["valid"] = True
46
+ result["issuer"] = str(dict(x[0] for x in cert.get("issuer", [])))
47
+ result["subject"] = str(dict(x[0] for x in cert.get("subject", [])))
48
+ result["not_before"] = cert.get("notBefore", "")
49
+ result["not_after"] = cert.get("notAfter", "")
50
+
51
+ not_after_str = cert.get("notAfter", "")
52
+ if not_after_str:
53
+ expire_date = datetime.strptime(not_after_str, "%b %d %H:%M:%S %Y %Z")
54
+ expire_date = expire_date.replace(tzinfo=timezone.utc)
55
+ days_remaining = (expire_date - datetime.now(timezone.utc)).days
56
+ result["days_remaining"] = days_remaining
57
+ if days_remaining < 0:
58
+ result["expired"] = True
59
+ elif days_remaining < 30:
60
+ result["errors"].append(f"证书将在 {days_remaining} 天后过期")
61
+
62
+ issuer_cn = dict(x[0] for x in cert.get("issuer", [])).get("commonName", "")
63
+ subject_cn = dict(x[0] for x in cert.get("subject", [])).get("commonName", "")
64
+ if issuer_cn == subject_cn:
65
+ result["self_signed"] = True
66
+
67
+ except ssl.SSLCertVerificationError as e:
68
+ result["errors"].append(f"证书验证失败: {e}")
69
+ except ssl.SSLError as e:
70
+ result["errors"].append(f"SSL 错误: {e}")
71
+ except (socket.timeout, ConnectionRefusedError, OSError) as e:
72
+ result["errors"].append(f"连接失败: {e}")
73
+
74
+ return result
75
+
76
+
77
+ def scan_testssl(host: str, port: int, audit: AuditLog, output_dir: Path) -> list[dict]:
78
+ """testssl.sh 完整 SSL/TLS 审计。"""
79
+ findings = []
80
+
81
+ json_output = output_dir / "testssl_raw.json"
82
+ cmd = [
83
+ "testssl",
84
+ "--json", str(json_output),
85
+ "--quiet",
86
+ "--color", "0",
87
+ f"{host}:{port}",
88
+ ]
89
+
90
+ result = run_command(cmd, timeout=300, audit=audit)
91
+
92
+ if result.returncode == -3:
93
+ console.print("[yellow]testssl.sh 未安装,使用 Python ssl 基础检测[/yellow]")
94
+ return findings
95
+
96
+ if result.returncode != 0 and not json_output.exists():
97
+ console.print(f"[yellow]testssl.sh 异常: {result.stderr[:200]}[/yellow]")
98
+ return findings
99
+
100
+ if json_output.exists():
101
+ with open(json_output) as f:
102
+ try:
103
+ data = json.load(f)
104
+ for entry in data:
105
+ severity_str = entry.get("severity", "INFO")
106
+ finding_id = entry.get("id", "")
107
+ finding_text = entry.get("finding", "")
108
+
109
+ if severity_str.upper() in ("CRITICAL", "HIGH", "MEDIUM", "WARN"):
110
+ severity_map = {
111
+ "CRITICAL": Severity.CRITICAL,
112
+ "HIGH": Severity.HIGH,
113
+ "MEDIUM": Severity.MEDIUM,
114
+ "WARN": Severity.LOW,
115
+ }
116
+ cvss_map = {
117
+ "CRITICAL": 9.0,
118
+ "HIGH": 7.0,
119
+ "MEDIUM": 5.0,
120
+ "WARN": 3.0,
121
+ }
122
+ findings.append({
123
+ "id": finding_id,
124
+ "finding": finding_text,
125
+ "severity": severity_map.get(severity_str.upper(), Severity.INFO).value,
126
+ "cvss": cvss_map.get(severity_str.upper(), 0.0),
127
+ })
128
+ except json.JSONDecodeError:
129
+ console.print("[yellow]testssl.sh JSON 解析失败[/yellow]")
130
+
131
+ return findings
132
+
133
+
134
+ def main():
135
+ parser = argparse.ArgumentParser(description="渗透测试 - SSL/TLS 审计(证书检查与 testssl.sh 并发)")
136
+ parser.add_argument("--target", required=True, help="目标域名/IP")
137
+ parser.add_argument("--output-dir", required=True, help="输出目录")
138
+ parser.add_argument("--port", type=int, default=443, help="SSL 端口(默认 443)")
139
+ args = parser.parse_args()
140
+
141
+ target, warning = validate_target(args.target)
142
+ if target is None:
143
+ console.print(f"[red]{warning}[/red]")
144
+ sys.exit(1)
145
+ if warning:
146
+ console.print(f"[yellow]{warning}[/yellow]")
147
+
148
+ output_dir = Path(args.output_dir)
149
+ output_dir.mkdir(parents=True, exist_ok=True)
150
+ audit = AuditLog(output_dir / "audit.log")
151
+
152
+ # 证书检查 + testssl.sh 并发
153
+ console.print(f"[bold]证书检查 + testssl.sh 并发: {target.host}:{args.port}[/bold]")
154
+ with ThreadPoolExecutor(max_workers=2) as pool:
155
+ cert_future = pool.submit(check_certificate, target.host, args.port)
156
+ testssl_future = pool.submit(scan_testssl, target.host, args.port, audit, output_dir)
157
+ cert_info = cert_future.result()
158
+ testssl_findings = testssl_future.result()
159
+
160
+ # 证书结果分析
161
+ cert_findings = []
162
+ if cert_info["expired"]:
163
+ console.print(" [red]证书已过期![/red]")
164
+ cert_findings.append({"type": "expired_cert", "severity": "High", "cvss": 7.5, "detail": "SSL 证书已过期"})
165
+ elif cert_info["days_remaining"] < 30 and cert_info["valid"]:
166
+ console.print(f" [yellow]证书将在 {cert_info['days_remaining']} 天后过期[/yellow]")
167
+ cert_findings.append({"type": "expiring_cert", "severity": "Medium", "cvss": 4.0, "detail": f"证书将在 {cert_info['days_remaining']} 天后过期"})
168
+ if cert_info["self_signed"]:
169
+ console.print(" [yellow]自签名证书[/yellow]")
170
+ cert_findings.append({"type": "self_signed", "severity": "Medium", "cvss": 5.0, "detail": "使用自签名证书"})
171
+ if cert_info["errors"]:
172
+ for err in cert_info["errors"]:
173
+ console.print(f" [yellow]{err}[/yellow]")
174
+ if cert_info["valid"] and not cert_info["expired"] and not cert_info["self_signed"]:
175
+ console.print(f" [green]证书有效,剩余 {cert_info['days_remaining']} 天[/green]")
176
+
177
+ all_findings = cert_findings + testssl_findings
178
+ result = {
179
+ "target": target.host,
180
+ "port": args.port,
181
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
182
+ "certificate": cert_info,
183
+ "findings": all_findings,
184
+ "total": len(all_findings),
185
+ }
186
+
187
+ output_file = output_dir / "ssl_audit.json"
188
+ save_json_result(result, output_file)
189
+ console.print(f"\n[green]结果已保存: {output_file}[/green]")
190
+
191
+ if all_findings:
192
+ table = Table(title="SSL/TLS 审计发现")
193
+ table.add_column("类型")
194
+ table.add_column("严重性")
195
+ table.add_column("详情")
196
+ for f in all_findings[:20]:
197
+ table.add_row(
198
+ f.get("type", f.get("id", "")),
199
+ f.get("severity", ""),
200
+ f.get("detail", f.get("finding", ""))[:80],
201
+ )
202
+ console.print(table)
203
+ else:
204
+ console.print("[green]未发现 SSL/TLS 安全问题[/green]")
205
+
206
+
207
+ if __name__ == "__main__":
208
+ main()
@@ -0,0 +1,205 @@
1
+ """阶段 3:漏洞扫描 — Nuclei 模板扫描 + Nmap NSE 漏洞脚本。
2
+
3
+ 用法:
4
+ uv run pentest_vuln.py --target <host> --mode nuclei|nse --output-dir <dir> [--ports 80,443,8080]
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ import sys
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ from lib.config import NUCLEI_SEVERITY, NUCLEI_TAGS, SCAN_RATE_LIMIT
17
+ from lib.executor import AuditLog, run_command, save_json_result
18
+ from lib.severity import Finding, Severity, cvss_to_severity, nuclei_severity_to_cvss, nmap_vuln_to_finding
19
+ from lib.validator import validate_target
20
+
21
+ console = Console()
22
+
23
+
24
+ def scan_nuclei(target: str, audit: AuditLog, output_dir: Path) -> list[dict]:
25
+ """Nuclei 模板驱动漏洞扫描。"""
26
+ findings = []
27
+ console.print("[bold]Nuclei 漏洞扫描...[/bold]")
28
+
29
+ url = target if target.startswith(("http://", "https://")) else f"http://{target}"
30
+ nuclei_output = output_dir / "nuclei_raw.json"
31
+
32
+ cmd = [
33
+ "nuclei",
34
+ "-u", url,
35
+ "-severity", NUCLEI_SEVERITY,
36
+ "-rate-limit", str(SCAN_RATE_LIMIT),
37
+ "-jsonl",
38
+ "-o", str(nuclei_output),
39
+ "-silent",
40
+ ]
41
+
42
+ result = run_command(cmd, timeout=600, audit=audit)
43
+
44
+ if result.blocked:
45
+ console.print(f"[red]Nuclei 被安全策略拦截: {result.block_reason}[/red]")
46
+ return findings
47
+
48
+ if result.returncode != 0 and not nuclei_output.exists():
49
+ console.print(f"[yellow]Nuclei 扫描异常: {result.stderr[:300]}[/yellow]")
50
+ # nuclei 未安装时降级为仅 nmap
51
+ if "command not found" in result.stderr or result.returncode == -3:
52
+ console.print("[yellow]nuclei 未安装,跳过此阶段[/yellow]")
53
+ return findings
54
+
55
+ # 解析 nuclei JSON 输出(每行一个 JSON)
56
+ if nuclei_output.exists():
57
+ with open(nuclei_output) as f:
58
+ for line in f:
59
+ line = line.strip()
60
+ if not line:
61
+ continue
62
+ try:
63
+ entry = json.loads(line)
64
+ severity_str = entry.get("info", {}).get("severity", "info")
65
+ cvss = nuclei_severity_to_cvss(severity_str)
66
+ finding = Finding(
67
+ title=entry.get("info", {}).get("name", "Unknown"),
68
+ severity=cvss_to_severity(cvss),
69
+ cvss=cvss,
70
+ cve_id=", ".join(entry.get("info", {}).get("classification", {}).get("cve-id", [])),
71
+ description=entry.get("info", {}).get("description", ""),
72
+ evidence=entry.get("matched-at", ""),
73
+ affected_service=entry.get("host", target),
74
+ discovery_tool="nuclei",
75
+ remediation=entry.get("info", {}).get("remediation", ""),
76
+ references=entry.get("info", {}).get("reference", []),
77
+ )
78
+ findings.append(finding.to_dict())
79
+ severity_color = {
80
+ Severity.CRITICAL: "red bold",
81
+ Severity.HIGH: "red",
82
+ Severity.MEDIUM: "yellow",
83
+ Severity.LOW: "blue",
84
+ }.get(finding.severity, "white")
85
+ console.print(f" [{severity_color}][{finding.severity.value}][/{severity_color}] {finding.title} — {finding.cve_id or 'N/A'}")
86
+ except json.JSONDecodeError:
87
+ continue
88
+
89
+ console.print(f" Nuclei 发现 {len(findings)} 个漏洞")
90
+ return findings
91
+
92
+
93
+ def scan_nse(host: str, ports: str, audit: AuditLog) -> list[dict]:
94
+ """Nmap NSE 漏洞脚本扫描。"""
95
+ findings = []
96
+ console.print("[bold]Nmap NSE 漏洞脚本扫描...[/bold]")
97
+
98
+ if not ports:
99
+ console.print("[yellow]无开放端口,跳过 NSE 扫描[/yellow]")
100
+ return findings
101
+
102
+ cmd = [
103
+ "nmap",
104
+ "--script", "vuln",
105
+ "-p", ports,
106
+ host,
107
+ ]
108
+
109
+ result = run_command(cmd, timeout=300, audit=audit)
110
+
111
+ if result.returncode != 0:
112
+ console.print(f"[yellow]NSE 扫描异常: {result.stderr[:200]}[/yellow]")
113
+ return findings
114
+
115
+ # 解析 nmap 漏洞脚本输出
116
+ current_script = ""
117
+ current_output = []
118
+ in_script = False
119
+
120
+ for line in result.stdout.split("\n"):
121
+ stripped = line.strip()
122
+ if stripped.startswith("|_"):
123
+ # 脚本输出结束行
124
+ if current_script and in_script:
125
+ output_text = "\n".join(current_output)
126
+ if "VULNERABLE" in output_text.upper() or "vulnerable" in output_text.lower():
127
+ finding = nmap_vuln_to_finding(current_script, output_text)
128
+ findings.append(finding.to_dict())
129
+ console.print(f" [red][{finding.severity.value}][/red] {finding.title}")
130
+ current_output.append(stripped[2:].strip())
131
+ in_script = False
132
+ elif stripped.startswith("| "):
133
+ if in_script:
134
+ current_output.append(stripped[2:].strip())
135
+ elif ":" in stripped and stripped.startswith("|"):
136
+ # 新脚本开始
137
+ if current_script and in_script and current_output:
138
+ output_text = "\n".join(current_output)
139
+ if "VULNERABLE" in output_text.upper():
140
+ finding = nmap_vuln_to_finding(current_script, output_text)
141
+ findings.append(finding.to_dict())
142
+ script_name = stripped.lstrip("| ").split(":")[0].strip()
143
+ current_script = script_name
144
+ current_output = []
145
+ in_script = True
146
+
147
+ console.print(f" NSE 发现 {len(findings)} 个漏洞")
148
+ return findings
149
+
150
+
151
+ def main():
152
+ parser = argparse.ArgumentParser(description="渗透测试 - 漏洞扫描")
153
+ parser.add_argument("--target", required=True, help="目标 IP/域名/URL")
154
+ parser.add_argument("--mode", required=True, choices=["nuclei", "nse"], help="扫描模式")
155
+ parser.add_argument("--output-dir", required=True, help="输出目录")
156
+ parser.add_argument("--ports", default="", help="逗号分隔的端口列表(NSE 模式必需)")
157
+ args = parser.parse_args()
158
+
159
+ target, warning = validate_target(args.target)
160
+ if target is None:
161
+ console.print(f"[red]{warning}[/red]")
162
+ sys.exit(1)
163
+ if warning:
164
+ console.print(f"[yellow]{warning}[/yellow]")
165
+
166
+ output_dir = Path(args.output_dir)
167
+ output_dir.mkdir(parents=True, exist_ok=True)
168
+ audit = AuditLog(output_dir / "audit.log")
169
+
170
+ findings = []
171
+ if args.mode == "nuclei":
172
+ findings = scan_nuclei(target.raw, audit, output_dir)
173
+ elif args.mode == "nse":
174
+ findings = scan_nse(target.host, args.ports, audit)
175
+
176
+ result = {
177
+ "target": target.host,
178
+ "mode": args.mode,
179
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
180
+ "findings": findings,
181
+ "total": len(findings),
182
+ }
183
+
184
+ output_file = output_dir / f"vuln_{args.mode}.json"
185
+ save_json_result(result, output_file)
186
+ console.print(f"\n[green]结果已保存: {output_file}[/green]")
187
+
188
+ # 摘要
189
+ severity_counts = {}
190
+ for f in findings:
191
+ s = f["severity"]
192
+ severity_counts[s] = severity_counts.get(s, 0) + 1
193
+
194
+ if severity_counts:
195
+ table = Table(title="漏洞分布")
196
+ table.add_column("严重性")
197
+ table.add_column("数量")
198
+ for s in ["Critical", "High", "Medium", "Low", "Info"]:
199
+ if s in severity_counts:
200
+ table.add_row(s, str(severity_counts[s]))
201
+ console.print(table)
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()