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.
- package/bin/index.js +1 -1
- package/package.json +2 -2
- package/src/create.js +61 -37
- package/src/sync.js +212 -61
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-commit-convention.md +0 -106
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /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()
|