create-dwy 0.5.1 → 0.6.0

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 +1 -1
  2. package/package.json +2 -2
  3. package/src/create.js +61 -37
  4. package/src/sync.js +212 -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,217 @@
1
+ """阶段 1:信息收集 — DNS 解析、WHOIS、技术栈指纹识别(并发实现)。
2
+
3
+ 用法:
4
+ uv run pentest_recon.py --target <host> --mode dns|tech --output-dir <dir>
5
+ """
6
+
7
+ import argparse
8
+ import asyncio
9
+ import json
10
+ import socket
11
+ import sys
12
+ import time
13
+ from concurrent.futures import ThreadPoolExecutor, as_completed
14
+ from pathlib import Path
15
+
16
+ import dns.resolver
17
+ import httpx
18
+ import whois
19
+ from rich.console import Console
20
+
21
+ from lib.executor import AuditLog, run_command, save_json_result
22
+ from lib.validator import validate_target
23
+
24
+ console = Console()
25
+
26
+ DNS_RECORD_TYPES = ["A", "AAAA", "MX", "NS", "TXT", "CNAME", "SOA"]
27
+ SENSITIVE_PATHS = ["/robots.txt", "/sitemap.xml", "/.env", "/.git/HEAD", "/wp-admin/"]
28
+ INTERESTING_HEADERS = ["Server", "X-Powered-By", "X-Generator", "X-AspNet-Version"]
29
+
30
+
31
+ def _query_dns_record(host: str, rtype: str) -> tuple[str, list[str]]:
32
+ """单个 DNS 记录类型查询,供线程池调用。"""
33
+ try:
34
+ answers = dns.resolver.resolve(host, rtype)
35
+ return rtype, [str(r) for r in answers]
36
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
37
+ return rtype, []
38
+ except Exception:
39
+ return rtype, []
40
+
41
+
42
+ def _resolve_addresses(host: str) -> list[str]:
43
+ try:
44
+ addrs = socket.getaddrinfo(host, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
45
+ return list({addr[4][0] for addr in addrs})
46
+ except socket.gaierror:
47
+ return []
48
+
49
+
50
+ def _whois_query(host: str) -> dict:
51
+ try:
52
+ w = whois.whois(host)
53
+ return {
54
+ "registrar": w.registrar,
55
+ "creation_date": str(w.creation_date),
56
+ "expiration_date": str(w.expiration_date),
57
+ "name_servers": w.name_servers if isinstance(w.name_servers, list) else [w.name_servers] if w.name_servers else [],
58
+ "org": w.org,
59
+ "country": w.country,
60
+ }
61
+ except Exception:
62
+ return {}
63
+
64
+
65
+ def recon_dns(host: str) -> dict:
66
+ """DNS 解析 + 记录枚举 + WHOIS(DNS、WHOIS、地址解析三路并发)。"""
67
+ results = {
68
+ "dns_records": {},
69
+ "ip_addresses": [],
70
+ "whois": {},
71
+ }
72
+
73
+ console.print(f"[bold]DNS 解析 + WHOIS 并发查询: {host}[/bold]")
74
+
75
+ # 一次性派发:地址解析 + WHOIS + 7 个 DNS 记录类型,全部并发
76
+ with ThreadPoolExecutor(max_workers=10) as pool:
77
+ addr_future = pool.submit(_resolve_addresses, host)
78
+ whois_future = pool.submit(_whois_query, host)
79
+ record_futures = {
80
+ pool.submit(_query_dns_record, host, rtype): rtype for rtype in DNS_RECORD_TYPES
81
+ }
82
+
83
+ results["ip_addresses"] = addr_future.result()
84
+ if results["ip_addresses"]:
85
+ console.print(f" IP 地址: {', '.join(results['ip_addresses'])}")
86
+
87
+ for fut in as_completed(record_futures):
88
+ rtype, records = fut.result()
89
+ if records:
90
+ results["dns_records"][rtype] = records
91
+ console.print(f" {rtype}: {', '.join(records[:3])}")
92
+
93
+ results["whois"] = whois_future.result()
94
+ if results["whois"].get("registrar"):
95
+ console.print(f" 注册商: {results['whois']['registrar']}")
96
+
97
+ return results
98
+
99
+
100
+ async def _async_probe_path(client: httpx.AsyncClient, base_url: str, path: str) -> dict | None:
101
+ try:
102
+ resp = await client.get(f"{base_url}{path}", timeout=5, follow_redirects=False)
103
+ if resp.status_code == 200:
104
+ return {"path": path, "status": resp.status_code, "size": len(resp.content)}
105
+ except Exception:
106
+ pass
107
+ return None
108
+
109
+
110
+ async def _async_collect_http(url: str) -> tuple[dict, list[dict]]:
111
+ """并发收集 HTTP 头 + 敏感文件探测。"""
112
+ http_headers: dict = {}
113
+ interesting_files: list[dict] = []
114
+
115
+ async with httpx.AsyncClient(verify=False, timeout=10) as client:
116
+ head_task = client.get(url, follow_redirects=True)
117
+ path_tasks = [_async_probe_path(client, url, p) for p in SENSITIVE_PATHS]
118
+ results = await asyncio.gather(head_task, *path_tasks, return_exceptions=True)
119
+
120
+ head_resp = results[0]
121
+ if isinstance(head_resp, httpx.Response):
122
+ for header in INTERESTING_HEADERS:
123
+ value = head_resp.headers.get(header)
124
+ if value:
125
+ http_headers[header] = value
126
+
127
+ for r in results[1:]:
128
+ if isinstance(r, dict):
129
+ interesting_files.append(r)
130
+
131
+ return http_headers, interesting_files
132
+
133
+
134
+ def recon_tech(host: str, audit: AuditLog) -> dict:
135
+ """技术栈指纹识别(whatweb + HTTP 头分析 + 敏感文件并发探测)。"""
136
+ results = {
137
+ "http_headers": {},
138
+ "technologies": [],
139
+ "interesting_files": [],
140
+ }
141
+
142
+ console.print(f"[bold]技术栈指纹: {host}[/bold]")
143
+ url = f"http://{host}" if not host.startswith(("http://", "https://")) else host
144
+
145
+ # whatweb 与 HTTP 收集并发:whatweb 是子进程,HTTP 是 asyncio,放线程池里跑 asyncio
146
+ with ThreadPoolExecutor(max_workers=2) as pool:
147
+ whatweb_future = pool.submit(
148
+ run_command,
149
+ ["whatweb", "--color=never", "-a", "3", "--log-json=-", url],
150
+ audit=audit,
151
+ )
152
+ http_future = pool.submit(asyncio.run, _async_collect_http(url))
153
+
154
+ whatweb_result = whatweb_future.result()
155
+ if whatweb_result.returncode == 0 and whatweb_result.stdout.strip():
156
+ try:
157
+ whatweb_data = json.loads(whatweb_result.stdout.strip().split("\n")[0])
158
+ if isinstance(whatweb_data, list) and whatweb_data:
159
+ plugins = whatweb_data[0].get("plugins", {})
160
+ for name, info in plugins.items():
161
+ if name not in ("IP", "Country", "UncommonHeaders"):
162
+ version = info.get("version", [""])[0] if isinstance(info.get("version"), list) else ""
163
+ tech = f"{name} {version}".strip()
164
+ results["technologies"].append(tech)
165
+ console.print(f" 发现技术: {', '.join(results['technologies'][:10])}")
166
+ except (json.JSONDecodeError, IndexError, KeyError):
167
+ pass
168
+
169
+ http_headers, interesting_files = http_future.result()
170
+ results["http_headers"] = http_headers
171
+ results["interesting_files"] = interesting_files
172
+
173
+ for header, value in http_headers.items():
174
+ console.print(f" {header}: {value}")
175
+ for f in interesting_files:
176
+ console.print(f" [yellow]发现敏感文件: {f['path']} ({f['status']})[/yellow]")
177
+
178
+ return results
179
+
180
+
181
+ def main():
182
+ parser = argparse.ArgumentParser(description="渗透测试 - 信息收集(并发)")
183
+ parser.add_argument("--target", required=True, help="目标 IP/域名/URL")
184
+ parser.add_argument("--mode", required=True, choices=["dns", "tech"], help="扫描模式")
185
+ parser.add_argument("--output-dir", required=True, help="输出目录")
186
+ args = parser.parse_args()
187
+
188
+ target, warning = validate_target(args.target)
189
+ if target is None:
190
+ console.print(f"[red]{warning}[/red]")
191
+ sys.exit(1)
192
+ if warning:
193
+ console.print(f"[yellow]{warning}[/yellow]")
194
+
195
+ output_dir = Path(args.output_dir)
196
+ output_dir.mkdir(parents=True, exist_ok=True)
197
+ audit = AuditLog(output_dir / "audit.log")
198
+
199
+ result = {
200
+ "target": target.host,
201
+ "mode": args.mode,
202
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
203
+ "findings": {},
204
+ }
205
+
206
+ if args.mode == "dns":
207
+ result["findings"] = recon_dns(target.host)
208
+ elif args.mode == "tech":
209
+ result["findings"] = recon_tech(target.host, audit)
210
+
211
+ output_file = output_dir / f"recon_{args.mode}.json"
212
+ save_json_result(result, output_file)
213
+ console.print(f"\n[green]结果已保存: {output_file}[/green]")
214
+
215
+
216
+ if __name__ == "__main__":
217
+ main()
@@ -0,0 +1,343 @@
1
+ """报告生成器:汇总所有阶段 JSON 结果 → 渲染 Markdown 报告。
2
+
3
+ 用法:
4
+ uv run pentest_report.py --input-dir <dir> --output <report.md>
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ import time
10
+ from pathlib import Path
11
+
12
+ from jinja2 import Template
13
+ from rich.console import Console
14
+
15
+ console = Console()
16
+
17
+
18
+ def load_json_files(input_dir: Path) -> dict[str, dict]:
19
+ """加载输出目录下所有 JSON 结果。"""
20
+ results = {}
21
+ for json_file in sorted(input_dir.glob("*.json")):
22
+ if json_file.name == "audit.log":
23
+ continue
24
+ try:
25
+ with open(json_file) as f:
26
+ results[json_file.stem] = json.load(f)
27
+ except (json.JSONDecodeError, OSError) as e:
28
+ console.print(f"[yellow]跳过 {json_file.name}: {e}[/yellow]")
29
+ return results
30
+
31
+
32
+ def collect_all_findings(results: dict[str, dict]) -> list[dict]:
33
+ """从所有结果中收集漏洞发现。"""
34
+ findings = []
35
+ for name, data in results.items():
36
+ if "findings" in data:
37
+ if isinstance(data["findings"], list):
38
+ for f in data["findings"]:
39
+ if isinstance(f, dict):
40
+ f.setdefault("source", name)
41
+ findings.append(f)
42
+ elif isinstance(data["findings"], dict):
43
+ # recon 结果是 dict 结构,跳过
44
+ pass
45
+ return findings
46
+
47
+
48
+ def sort_findings(findings: list[dict]) -> list[dict]:
49
+ """按 CVSS 评分降序排序。"""
50
+ severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Info": 4}
51
+ return sorted(
52
+ findings,
53
+ key=lambda f: (severity_order.get(f.get("severity", "Info"), 5), -f.get("cvss", 0)),
54
+ )
55
+
56
+
57
+ def count_by_severity(findings: list[dict]) -> dict[str, int]:
58
+ """按严重性统计。"""
59
+ counts = {"Critical": 0, "High": 0, "Medium": 0, "Low": 0, "Info": 0}
60
+ for f in findings:
61
+ s = f.get("severity", "Info")
62
+ counts[s] = counts.get(s, 0) + 1
63
+ return counts
64
+
65
+
66
+ def determine_risk_level(counts: dict[str, int]) -> str:
67
+ """根据漏洞分布确定整体风险等级。"""
68
+ if counts.get("Critical", 0) > 0:
69
+ return "Critical"
70
+ if counts.get("High", 0) > 0:
71
+ return "High"
72
+ if counts.get("Medium", 0) > 0:
73
+ return "Medium"
74
+ if counts.get("Low", 0) > 0:
75
+ return "Low"
76
+ return "Info"
77
+
78
+
79
+ REPORT_TEMPLATE = """# 渗透测试报告
80
+
81
+ ## 基本信息
82
+
83
+ | 项目 | 内容 |
84
+ |------|------|
85
+ | 目标 | {{ target }} |
86
+ | 测试时间 | {{ timestamp }} |
87
+ | 报告生成 | {{ report_time }} |
88
+
89
+ ## 执行摘要
90
+
91
+ 本次渗透测试发现 **{{ total_findings }}** 个安全问题:
92
+
93
+ | 严重性 | 数量 |
94
+ |--------|------|
95
+ | Critical (9.0-10.0) | {{ counts.Critical }} |
96
+ | High (7.0-8.9) | {{ counts.High }} |
97
+ | Medium (4.0-6.9) | {{ counts.Medium }} |
98
+ | Low (0.1-3.9) | {{ counts.Low }} |
99
+ | Info | {{ counts.Info }} |
100
+
101
+ **整体风险评级:{{ risk_level }}**
102
+
103
+ ---
104
+
105
+ {% if recon_dns %}
106
+ ## 目标概况
107
+
108
+ ### DNS 信息
109
+ {% if recon_dns.ip_addresses %}
110
+ - IP 地址: {{ recon_dns.ip_addresses | join(', ') }}
111
+ {% endif %}
112
+ {% if recon_dns.dns_records %}
113
+ {% for rtype, records in recon_dns.dns_records.items() %}
114
+ - {{ rtype }}: {{ records | join(', ') }}
115
+ {% endfor %}
116
+ {% endif %}
117
+ {% if recon_dns.whois.registrar %}
118
+ - 注册商: {{ recon_dns.whois.registrar }}
119
+ {% endif %}
120
+ {% endif %}
121
+
122
+ {% if recon_tech %}
123
+ ### 技术栈
124
+ {% if recon_tech.technologies %}
125
+ {{ recon_tech.technologies | join(', ') }}
126
+ {% endif %}
127
+ {% if recon_tech.http_headers %}
128
+ | 响应头 | 值 |
129
+ |--------|-----|
130
+ {% for header, value in recon_tech.http_headers.items() %}
131
+ | {{ header }} | {{ value }} |
132
+ {% endfor %}
133
+ {% endif %}
134
+ {% if recon_tech.interesting_files %}
135
+
136
+ ### 发现的敏感文件
137
+ | 路径 | 状态码 |
138
+ |------|--------|
139
+ {% for f in recon_tech.interesting_files %}
140
+ | {{ f.path }} | {{ f.status }} |
141
+ {% endfor %}
142
+ {% endif %}
143
+ {% endif %}
144
+
145
+ {% if scan_ports %}
146
+ ## 端口扫描结果
147
+
148
+ | 端口 | 协议 | 服务 | 版本 |
149
+ |------|------|------|------|
150
+ {% for p in scan_ports.tcp.ports %}
151
+ | {{ p.port }} | {{ p.protocol }} | {{ p.service }} | {{ p.product }} {{ p.version }} |
152
+ {% endfor %}
153
+ {% if scan_ports.tcp.os_detection %}
154
+
155
+ ### OS 检测
156
+ {% for os in scan_ports.tcp.os_detection %}
157
+ - {{ os.name }} ({{ os.accuracy }}%)
158
+ {% endfor %}
159
+ {% endif %}
160
+ {% endif %}
161
+
162
+ ---
163
+
164
+ ## 漏洞详情
165
+
166
+ {% for finding in findings %}
167
+ ### VULN-{{ loop.index }}: {{ finding.title or finding.type or finding.id or 'Unknown' }}
168
+
169
+ | 属性 | 值 |
170
+ |------|-----|
171
+ | 严重性 | {{ finding.severity }} |
172
+ | CVSS 评分 | {{ finding.cvss | default('N/A') }} |
173
+ {% if finding.cve_id %}| CVE ID | {{ finding.cve_id }} |{% endif %}
174
+ | 影响范围 | {{ finding.affected_service or finding.service or finding.url or target }} |
175
+ | 发现工具 | {{ finding.discovery_tool or finding.source or 'N/A' }} |
176
+
177
+ {% if finding.description or finding.detail or finding.finding %}
178
+ **描述:** {{ finding.description or finding.detail or finding.finding }}
179
+ {% endif %}
180
+
181
+ {% if finding.evidence %}
182
+ **证据:**
183
+ ```
184
+ {{ finding.evidence }}
185
+ ```
186
+ {% endif %}
187
+
188
+ {% if finding.remediation %}
189
+ **修复建议:** {{ finding.remediation }}
190
+ {% endif %}
191
+
192
+ {% if finding.references %}
193
+ **参考链接:**
194
+ {% for ref in finding.references %}
195
+ - {{ ref }}
196
+ {% endfor %}
197
+ {% endif %}
198
+
199
+ ---
200
+
201
+ {% endfor %}
202
+
203
+ {% if ssl_findings %}
204
+ ## SSL/TLS 审计结果
205
+
206
+ {% if ssl_cert %}
207
+ ### 证书信息
208
+ | 属性 | 值 |
209
+ |------|-----|
210
+ | 有效 | {{ '是' if ssl_cert.valid else '否' }} |
211
+ | 颁发者 | {{ ssl_cert.issuer }} |
212
+ | 剩余天数 | {{ ssl_cert.days_remaining }} |
213
+ | 自签名 | {{ '是' if ssl_cert.self_signed else '否' }} |
214
+ | 已过期 | {{ '是' if ssl_cert.expired else '否' }} |
215
+ {% endif %}
216
+
217
+ {% if ssl_findings %}
218
+ | 问题 | 严重性 | 详情 |
219
+ |------|--------|------|
220
+ {% for f in ssl_findings %}
221
+ | {{ f.type or f.id }} | {{ f.severity }} | {{ (f.detail or f.finding)[:80] }} |
222
+ {% endfor %}
223
+ {% endif %}
224
+ {% endif %}
225
+
226
+ ## 修复优先级建议
227
+
228
+ {% set critical_findings = findings | selectattr('severity', 'equalto', 'Critical') | list %}
229
+ {% set high_findings = findings | selectattr('severity', 'equalto', 'High') | list %}
230
+ {% set medium_findings = findings | selectattr('severity', 'equalto', 'Medium') | list %}
231
+
232
+ {% if critical_findings %}
233
+ ### 1. 立即修复(24 小时内)
234
+ {% for f in critical_findings %}
235
+ - {{ f.title or f.type or f.id }}: {{ f.description or f.detail or '' }}
236
+ {% endfor %}
237
+ {% endif %}
238
+
239
+ {% if high_findings %}
240
+ ### 2. 尽快修复(1 周内)
241
+ {% for f in high_findings %}
242
+ - {{ f.title or f.type or f.id }}: {{ f.description or f.detail or '' }}
243
+ {% endfor %}
244
+ {% endif %}
245
+
246
+ {% if medium_findings %}
247
+ ### 3. 计划修复(1 个月内)
248
+ {% for f in medium_findings %}
249
+ - {{ f.title or f.type or f.id }}: {{ f.description or f.detail or '' }}
250
+ {% endfor %}
251
+ {% endif %}
252
+
253
+ ---
254
+
255
+ ## 免责声明
256
+
257
+ 本报告仅记录经授权的安全评估结果。所有测试均在授权范围内进行,
258
+ 未执行任何破坏性操作。发现的漏洞信息应妥善保管,仅用于安全加固。
259
+ """
260
+
261
+
262
+ def generate_report(results: dict[str, dict], output_path: Path):
263
+ """生成渲染后的 Markdown 报告。"""
264
+ all_findings = collect_all_findings(results)
265
+ sorted_findings = sort_findings(all_findings)
266
+ counts = count_by_severity(sorted_findings)
267
+ risk_level = determine_risk_level(counts)
268
+
269
+ # 提取各阶段数据
270
+ recon_dns = results.get("recon_dns", {}).get("findings", {})
271
+ recon_tech = results.get("recon_tech", {}).get("findings", {})
272
+ scan_ports = results.get("scan_ports", {})
273
+ ssl_data = results.get("ssl_audit", {})
274
+ ssl_cert = ssl_data.get("certificate", {})
275
+ ssl_findings = ssl_data.get("findings", [])
276
+
277
+ # 确定目标
278
+ target = "Unknown"
279
+ for data in results.values():
280
+ if "target" in data:
281
+ target = data["target"]
282
+ break
283
+
284
+ # 确定时间
285
+ timestamps = [data.get("timestamp", "") for data in results.values() if "timestamp" in data]
286
+ timestamp = min(timestamps) if timestamps else time.strftime("%Y-%m-%d %H:%M:%S")
287
+
288
+ template = Template(REPORT_TEMPLATE)
289
+ report = template.render(
290
+ target=target,
291
+ timestamp=timestamp,
292
+ report_time=time.strftime("%Y-%m-%d %H:%M:%S"),
293
+ total_findings=len(sorted_findings),
294
+ counts=counts,
295
+ risk_level=risk_level,
296
+ recon_dns=recon_dns,
297
+ recon_tech=recon_tech,
298
+ scan_ports=scan_ports,
299
+ findings=sorted_findings,
300
+ ssl_cert=ssl_cert,
301
+ ssl_findings=ssl_findings,
302
+ )
303
+
304
+ output_path.parent.mkdir(parents=True, exist_ok=True)
305
+ output_path.write_text(report)
306
+ console.print(f"[green bold]报告已生成: {output_path}[/green bold]")
307
+
308
+ # 打印摘要
309
+ console.print(f"\n{'='*60}")
310
+ console.print(f" 渗透测试报告摘要")
311
+ console.print(f"{'='*60}")
312
+ console.print(f" 目标: {target}")
313
+ console.print(f" 总发现: {len(sorted_findings)} 个安全问题")
314
+ console.print(f" 风险等级: {risk_level}")
315
+ if counts["Critical"] > 0:
316
+ console.print(f" [red bold]!! {counts['Critical']} 个严重漏洞 !![/red bold]")
317
+ if counts["High"] > 0:
318
+ console.print(f" [red]{counts['High']} 个高危漏洞[/red]")
319
+ console.print(f"{'='*60}")
320
+
321
+
322
+ def main():
323
+ parser = argparse.ArgumentParser(description="渗透测试 - 报告生成")
324
+ parser.add_argument("--input-dir", required=True, help="扫描结果目录")
325
+ parser.add_argument("--output", required=True, help="报告输出路径 (.md)")
326
+ args = parser.parse_args()
327
+
328
+ input_dir = Path(args.input_dir)
329
+ if not input_dir.exists():
330
+ console.print(f"[red]输入目录不存在: {input_dir}[/red]")
331
+ sys.exit(1)
332
+
333
+ results = load_json_files(input_dir)
334
+ if not results:
335
+ console.print(f"[yellow]未找到任何扫描结果 JSON 文件[/yellow]")
336
+ sys.exit(1)
337
+
338
+ console.print(f"加载了 {len(results)} 个结果文件: {', '.join(results.keys())}")
339
+ generate_report(results, Path(args.output))
340
+
341
+
342
+ if __name__ == "__main__":
343
+ main()