create-dwy 0.5.0 → 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 +213 -53
- 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} +314 -357
- 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} +116 -79
- 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} +361 -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/settings.json +20 -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-publish/SKILL.md +135 -0
- 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/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-auto-sync/SKILL.md +0 -52
- 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,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()
|