zen-ai-pentest 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +28 -0
- agents/agent_base.py +239 -0
- agents/agent_orchestrator.py +346 -0
- agents/analysis_agent.py +225 -0
- agents/cli.py +258 -0
- agents/exploit_agent.py +224 -0
- agents/integration.py +211 -0
- agents/post_scan_agent.py +937 -0
- agents/react_agent.py +384 -0
- agents/react_agent_enhanced.py +616 -0
- agents/react_agent_vm.py +298 -0
- agents/research_agent.py +176 -0
- api/__init__.py +11 -0
- api/auth.py +123 -0
- api/main.py +1027 -0
- api/schemas.py +357 -0
- api/websocket.py +97 -0
- autonomous/__init__.py +122 -0
- autonomous/agent.py +253 -0
- autonomous/agent_loop.py +1370 -0
- autonomous/exploit_validator.py +1537 -0
- autonomous/memory.py +448 -0
- autonomous/react.py +339 -0
- autonomous/tool_executor.py +488 -0
- backends/__init__.py +16 -0
- backends/chatgpt_direct.py +133 -0
- backends/claude_direct.py +130 -0
- backends/duckduckgo.py +138 -0
- backends/openrouter.py +120 -0
- benchmarks/__init__.py +149 -0
- benchmarks/benchmark_engine.py +904 -0
- benchmarks/ci_benchmark.py +785 -0
- benchmarks/comparison.py +729 -0
- benchmarks/metrics.py +553 -0
- benchmarks/run_benchmarks.py +809 -0
- ci_cd/__init__.py +2 -0
- core/__init__.py +17 -0
- core/async_pool.py +282 -0
- core/asyncio_fix.py +222 -0
- core/cache.py +472 -0
- core/container.py +277 -0
- core/database.py +114 -0
- core/input_validator.py +353 -0
- core/models.py +288 -0
- core/orchestrator.py +611 -0
- core/plugin_manager.py +571 -0
- core/rate_limiter.py +405 -0
- core/secure_config.py +328 -0
- core/shield_integration.py +296 -0
- modules/__init__.py +46 -0
- modules/cve_database.py +362 -0
- modules/exploit_assist.py +330 -0
- modules/nuclei_integration.py +480 -0
- modules/osint.py +604 -0
- modules/protonvpn.py +554 -0
- modules/recon.py +165 -0
- modules/sql_injection_db.py +826 -0
- modules/tool_orchestrator.py +498 -0
- modules/vuln_scanner.py +292 -0
- modules/wordlist_generator.py +566 -0
- risk_engine/__init__.py +99 -0
- risk_engine/business_impact.py +267 -0
- risk_engine/business_impact_calculator.py +563 -0
- risk_engine/cvss.py +156 -0
- risk_engine/epss.py +190 -0
- risk_engine/example_usage.py +294 -0
- risk_engine/false_positive_engine.py +1073 -0
- risk_engine/scorer.py +304 -0
- web_ui/backend/main.py +471 -0
- zen_ai_pentest-2.0.0.dist-info/METADATA +795 -0
- zen_ai_pentest-2.0.0.dist-info/RECORD +75 -0
- zen_ai_pentest-2.0.0.dist-info/WHEEL +5 -0
- zen_ai_pentest-2.0.0.dist-info/entry_points.txt +2 -0
- zen_ai_pentest-2.0.0.dist-info/licenses/LICENSE +21 -0
- zen_ai_pentest-2.0.0.dist-info/top_level.txt +10 -0
modules/protonvpn.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proton VPN Integration Module for Zen AI Pentest
|
|
3
|
+
|
|
4
|
+
Provides secure VPN connectivity for penetration testing operations:
|
|
5
|
+
- Anonymous reconnaissance
|
|
6
|
+
- Geo-location bypassing
|
|
7
|
+
- Secure C2 communications
|
|
8
|
+
- Traffic encryption
|
|
9
|
+
|
|
10
|
+
Author: SHAdd0WTAka
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import random
|
|
17
|
+
import subprocess
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("ZenAI.ProtonVPN")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VPNProtocol(Enum):
|
|
27
|
+
"""Supported VPN protocols"""
|
|
28
|
+
|
|
29
|
+
WIREGUARD = "wireguard"
|
|
30
|
+
OPENVPN_TCP = "openvpn-tcp"
|
|
31
|
+
OPENVPN_UDP = "openvpn-udp"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VPNSecurityLevel(Enum):
|
|
35
|
+
"""Proton VPN security levels"""
|
|
36
|
+
|
|
37
|
+
STANDARD = "standard" # Regular servers
|
|
38
|
+
SECURE_CORE = "secure-core" # Multi-hop ( entry -> secure country -> exit)
|
|
39
|
+
TOR = "tor" # VPN over Tor
|
|
40
|
+
P2P = "p2p" # P2P optimized
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class VPNStatus:
|
|
45
|
+
"""Current VPN connection status"""
|
|
46
|
+
|
|
47
|
+
connected: bool = False
|
|
48
|
+
server_ip: Optional[str] = None
|
|
49
|
+
server_location: Optional[str] = None
|
|
50
|
+
protocol: Optional[str] = None
|
|
51
|
+
public_ip: Optional[str] = None
|
|
52
|
+
original_ip: Optional[str] = None
|
|
53
|
+
connection_time: Optional[str] = None
|
|
54
|
+
kill_switch: bool = False
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"connected": self.connected,
|
|
59
|
+
"server_ip": self.server_ip,
|
|
60
|
+
"server_location": self.server_location,
|
|
61
|
+
"protocol": self.protocol,
|
|
62
|
+
"public_ip": self.public_ip,
|
|
63
|
+
"original_ip": self.original_ip,
|
|
64
|
+
"connection_time": self.connection_time,
|
|
65
|
+
"kill_switch": self.kill_switch,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class VPNServer:
|
|
71
|
+
"""Proton VPN server information"""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
country: str
|
|
75
|
+
city: Optional[str] = None
|
|
76
|
+
ip: Optional[str] = None
|
|
77
|
+
load: int = 0 # Server load percentage
|
|
78
|
+
features: List[str] = field(default_factory=list)
|
|
79
|
+
tier: int = 0 # 0=Free, 1=Basic, 2=Plus, 3=Visionary
|
|
80
|
+
|
|
81
|
+
def __str__(self) -> str:
|
|
82
|
+
return f"{self.name} ({self.city}, {self.country}) - Load: {self.load}%"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ProtonVPNManager:
|
|
86
|
+
"""
|
|
87
|
+
Manager for Proton VPN integration
|
|
88
|
+
|
|
89
|
+
Features:
|
|
90
|
+
- Connect/disconnect to VPN servers
|
|
91
|
+
- Rotate IP addresses
|
|
92
|
+
- Secure Core (multi-hop) connections
|
|
93
|
+
- Kill switch management
|
|
94
|
+
- Country/region selection
|
|
95
|
+
- Automatic server selection (least load)
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Proton VPN countries optimized for pentesting
|
|
99
|
+
RECOMMENDED_COUNTRIES = [
|
|
100
|
+
"CH", # Switzerland - Strong privacy laws
|
|
101
|
+
"IS", # Iceland - Data protection
|
|
102
|
+
"SE", # Sweden - Good connectivity
|
|
103
|
+
"NL", # Netherlands - Fast servers
|
|
104
|
+
"DE", # Germany - Good infrastructure
|
|
105
|
+
"SG", # Singapore - Asia coverage
|
|
106
|
+
"JP", # Japan - Asia coverage
|
|
107
|
+
"CA", # Canada - North America
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# Countries with P2P support
|
|
111
|
+
P2P_COUNTRIES = ["NL", "CH", "IS", "SE", "DE", "SG"]
|
|
112
|
+
|
|
113
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
114
|
+
self.config_path = config_path or "config/protonvpn.json"
|
|
115
|
+
self.status = VPNStatus()
|
|
116
|
+
self.connected = False
|
|
117
|
+
self.current_server: Optional[VPNServer] = None
|
|
118
|
+
self._original_ip: Optional[str] = None
|
|
119
|
+
self._connection_history: List[Dict] = []
|
|
120
|
+
|
|
121
|
+
async def get_public_ip(self) -> str:
|
|
122
|
+
"""Get current public IP address"""
|
|
123
|
+
try:
|
|
124
|
+
# Multiple services for redundancy
|
|
125
|
+
services = [
|
|
126
|
+
"https://ip.me",
|
|
127
|
+
"https://api.ipify.org",
|
|
128
|
+
"https://icanhazip.com",
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
for service in services:
|
|
132
|
+
try:
|
|
133
|
+
proc = await asyncio.create_subprocess_exec(
|
|
134
|
+
"curl",
|
|
135
|
+
"-s",
|
|
136
|
+
"--max-time",
|
|
137
|
+
"5",
|
|
138
|
+
service,
|
|
139
|
+
stdout=asyncio.subprocess.PIPE,
|
|
140
|
+
stderr=asyncio.subprocess.PIPE,
|
|
141
|
+
)
|
|
142
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=6)
|
|
143
|
+
ip = stdout.decode().strip()
|
|
144
|
+
if ip and self._is_valid_ip(ip):
|
|
145
|
+
return ip
|
|
146
|
+
except:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
return "unknown"
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Failed to get public IP: {e}")
|
|
152
|
+
return "unknown"
|
|
153
|
+
|
|
154
|
+
def _is_valid_ip(self, ip: str) -> bool:
|
|
155
|
+
"""Validate IP address format"""
|
|
156
|
+
import re
|
|
157
|
+
|
|
158
|
+
pattern = r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$"
|
|
159
|
+
return bool(re.match(pattern, ip))
|
|
160
|
+
|
|
161
|
+
async def connect(
|
|
162
|
+
self,
|
|
163
|
+
country: Optional[str] = None,
|
|
164
|
+
city: Optional[str] = None,
|
|
165
|
+
protocol: VPNProtocol = VPNProtocol.WIREGUARD,
|
|
166
|
+
security_level: VPNSecurityLevel = VPNSecurityLevel.STANDARD,
|
|
167
|
+
p2p: bool = False,
|
|
168
|
+
kill_switch: bool = True,
|
|
169
|
+
) -> VPNStatus:
|
|
170
|
+
"""
|
|
171
|
+
Connect to Proton VPN
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
country: ISO country code (e.g., 'CH', 'NL')
|
|
175
|
+
city: City name for specific location
|
|
176
|
+
protocol: VPN protocol to use
|
|
177
|
+
security_level: Security/Feature level
|
|
178
|
+
p2p: Use P2P-optimized servers
|
|
179
|
+
kill_switch: Enable kill switch
|
|
180
|
+
"""
|
|
181
|
+
logger.info(f"Connecting to Proton VPN...")
|
|
182
|
+
|
|
183
|
+
# Save original IP
|
|
184
|
+
if not self._original_ip:
|
|
185
|
+
self._original_ip = await self.get_public_ip()
|
|
186
|
+
self.status.original_ip = self._original_ip
|
|
187
|
+
|
|
188
|
+
# Select server
|
|
189
|
+
if not country:
|
|
190
|
+
country = random.choice(self.RECOMMENDED_COUNTRIES)
|
|
191
|
+
|
|
192
|
+
if p2p and country not in self.P2P_COUNTRIES:
|
|
193
|
+
country = random.choice(self.P2P_COUNTRIES)
|
|
194
|
+
|
|
195
|
+
# Build connection command
|
|
196
|
+
server_name = self._get_server_name(country, city, p2p)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Check if protonvpn-cli is available
|
|
200
|
+
proc = await asyncio.create_subprocess_exec(
|
|
201
|
+
"protonvpn-cli",
|
|
202
|
+
"connect",
|
|
203
|
+
"--help",
|
|
204
|
+
stdout=asyncio.subprocess.PIPE,
|
|
205
|
+
stderr=asyncio.subprocess.PIPE,
|
|
206
|
+
)
|
|
207
|
+
await proc.communicate()
|
|
208
|
+
|
|
209
|
+
if proc.returncode != 0:
|
|
210
|
+
logger.warning("protonvpn-cli not found, using mock mode")
|
|
211
|
+
return await self._mock_connect(country, protocol)
|
|
212
|
+
|
|
213
|
+
# Real Proton VPN connection
|
|
214
|
+
cmd = ["protonvpn-cli", "connect", "--cc", country.lower()]
|
|
215
|
+
|
|
216
|
+
if protocol == VPNProtocol.WIREGUARD:
|
|
217
|
+
cmd.extend(["--protocol", "wireguard"])
|
|
218
|
+
elif protocol == VPNProtocol.OPENVPN_TCP:
|
|
219
|
+
cmd.extend(["--protocol", "tcp"])
|
|
220
|
+
elif protocol == VPNProtocol.OPENVPN_UDP:
|
|
221
|
+
cmd.extend(["--protocol", "udp"])
|
|
222
|
+
|
|
223
|
+
if security_level == VPNSecurityLevel.SECURE_CORE:
|
|
224
|
+
cmd.append("--sc")
|
|
225
|
+
elif security_level == VPNSecurityLevel.TOR:
|
|
226
|
+
cmd.append("--tor")
|
|
227
|
+
|
|
228
|
+
if kill_switch:
|
|
229
|
+
subprocess.run(["protonvpn-cli", "ks", "--on"], check=False)
|
|
230
|
+
|
|
231
|
+
proc = await asyncio.create_subprocess_exec(
|
|
232
|
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
233
|
+
)
|
|
234
|
+
stdout, stderr = await proc.communicate()
|
|
235
|
+
|
|
236
|
+
if proc.returncode == 0:
|
|
237
|
+
self.connected = True
|
|
238
|
+
self.status.connected = True
|
|
239
|
+
self.status.server_location = (
|
|
240
|
+
f"{city or 'Auto'}, {country}" if city else country
|
|
241
|
+
)
|
|
242
|
+
self.status.protocol = protocol.value
|
|
243
|
+
self.status.kill_switch = kill_switch
|
|
244
|
+
self.status.connection_time = self._get_timestamp()
|
|
245
|
+
|
|
246
|
+
# Get new public IP
|
|
247
|
+
await asyncio.sleep(3) # Wait for connection
|
|
248
|
+
self.status.public_ip = await self.get_public_ip()
|
|
249
|
+
|
|
250
|
+
logger.info(f"Connected to {self.status.server_location}")
|
|
251
|
+
logger.info(f"New IP: {self.status.public_ip}")
|
|
252
|
+
|
|
253
|
+
self._log_connection()
|
|
254
|
+
|
|
255
|
+
else:
|
|
256
|
+
error = stderr.decode() if stderr else "Unknown error"
|
|
257
|
+
logger.error(f"Connection failed: {error}")
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error(f"Connection error: {e}")
|
|
261
|
+
return await self._mock_connect(country, protocol)
|
|
262
|
+
|
|
263
|
+
return self.status
|
|
264
|
+
|
|
265
|
+
async def disconnect(self) -> VPNStatus:
|
|
266
|
+
"""Disconnect from VPN"""
|
|
267
|
+
logger.info("Disconnecting from Proton VPN...")
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
proc = await asyncio.create_subprocess_exec(
|
|
271
|
+
"protonvpn-cli",
|
|
272
|
+
"disconnect",
|
|
273
|
+
stdout=asyncio.subprocess.PIPE,
|
|
274
|
+
stderr=asyncio.subprocess.PIPE,
|
|
275
|
+
)
|
|
276
|
+
await proc.communicate()
|
|
277
|
+
|
|
278
|
+
self.connected = False
|
|
279
|
+
self.status.connected = False
|
|
280
|
+
self.status.server_ip = None
|
|
281
|
+
self.status.server_location = None
|
|
282
|
+
self.status.protocol = None
|
|
283
|
+
self.status.public_ip = await self.get_public_ip()
|
|
284
|
+
|
|
285
|
+
logger.info("Disconnected from VPN")
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.error(f"Disconnect error: {e}")
|
|
289
|
+
|
|
290
|
+
return self.status
|
|
291
|
+
|
|
292
|
+
async def rotate_ip(
|
|
293
|
+
self,
|
|
294
|
+
country: Optional[str] = None,
|
|
295
|
+
protocol: VPNProtocol = VPNProtocol.WIREGUARD,
|
|
296
|
+
) -> VPNStatus:
|
|
297
|
+
"""
|
|
298
|
+
Rotate VPN IP address
|
|
299
|
+
Disconnect and reconnect to get new IP
|
|
300
|
+
"""
|
|
301
|
+
logger.info("Rotating VPN IP...")
|
|
302
|
+
|
|
303
|
+
await self.disconnect()
|
|
304
|
+
await asyncio.sleep(2)
|
|
305
|
+
|
|
306
|
+
# Select different country if not specified
|
|
307
|
+
if not country:
|
|
308
|
+
available = [
|
|
309
|
+
c
|
|
310
|
+
for c in self.RECOMMENDED_COUNTRIES
|
|
311
|
+
if c != self.status.server_location
|
|
312
|
+
]
|
|
313
|
+
country = random.choice(available)
|
|
314
|
+
|
|
315
|
+
return await self.connect(country=country, protocol=protocol)
|
|
316
|
+
|
|
317
|
+
def _get_server_name(self, country: str, city: Optional[str], p2p: bool) -> str:
|
|
318
|
+
"""Generate server name based on parameters"""
|
|
319
|
+
if p2p:
|
|
320
|
+
return f"{country}-P2P"
|
|
321
|
+
if city:
|
|
322
|
+
return f"{country}-{city}"
|
|
323
|
+
return country
|
|
324
|
+
|
|
325
|
+
async def _mock_connect(self, country: str, protocol: VPNProtocol) -> VPNStatus:
|
|
326
|
+
"""Mock connection for testing without real VPN"""
|
|
327
|
+
logger.info(f"[MOCK] Connecting to {country} via {protocol.value}")
|
|
328
|
+
|
|
329
|
+
self.connected = True
|
|
330
|
+
self.status.connected = True
|
|
331
|
+
self.status.server_location = f"MOCK-{country}"
|
|
332
|
+
self.status.protocol = protocol.value
|
|
333
|
+
self.status.server_ip = f"10.{random.randint(0,255)}.{random.randint(0,255)}.1"
|
|
334
|
+
self.status.public_ip = f"185.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(0,255)}"
|
|
335
|
+
self.status.connection_time = self._get_timestamp()
|
|
336
|
+
|
|
337
|
+
if not self._original_ip:
|
|
338
|
+
self._original_ip = "192.168.1.100"
|
|
339
|
+
self.status.original_ip = self._original_ip
|
|
340
|
+
|
|
341
|
+
return self.status
|
|
342
|
+
|
|
343
|
+
def get_status(self) -> VPNStatus:
|
|
344
|
+
"""Get current VPN status"""
|
|
345
|
+
return self.status
|
|
346
|
+
|
|
347
|
+
def is_connected(self) -> bool:
|
|
348
|
+
"""Check if VPN is connected"""
|
|
349
|
+
return self.connected
|
|
350
|
+
|
|
351
|
+
async def get_server_list(self, country: Optional[str] = None) -> List[VPNServer]:
|
|
352
|
+
"""Get list of available VPN servers"""
|
|
353
|
+
servers = []
|
|
354
|
+
|
|
355
|
+
# Mock server list (in real implementation, fetch from Proton API)
|
|
356
|
+
mock_servers = [
|
|
357
|
+
VPNServer("CH-01", "CH", "Zurich", "185.159.158.1", 45, ["secure-core"], 2),
|
|
358
|
+
VPNServer(
|
|
359
|
+
"CH-02", "CH", "Geneva", "185.159.158.2", 30, ["secure-core", "p2p"], 2
|
|
360
|
+
),
|
|
361
|
+
VPNServer(
|
|
362
|
+
"NL-01", "NL", "Amsterdam", "185.107.56.1", 60, ["p2p", "tor"], 2
|
|
363
|
+
),
|
|
364
|
+
VPNServer("NL-02", "NL", "Amsterdam", "185.107.56.2", 25, ["p2p"], 2),
|
|
365
|
+
VPNServer(
|
|
366
|
+
"SE-01", "SE", "Stockholm", "185.210.217.1", 40, ["secure-core"], 2
|
|
367
|
+
),
|
|
368
|
+
VPNServer(
|
|
369
|
+
"IS-01", "IS", "Reykjavik", "37.235.49.1", 20, ["secure-core"], 2
|
|
370
|
+
),
|
|
371
|
+
VPNServer("DE-01", "DE", "Frankfurt", "185.104.63.1", 55, [], 1),
|
|
372
|
+
VPNServer("SG-01", "SG", "Singapore", "103.125.234.1", 70, ["p2p"], 2),
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
if country:
|
|
376
|
+
servers = [s for s in mock_servers if s.country == country.upper()]
|
|
377
|
+
else:
|
|
378
|
+
servers = mock_servers
|
|
379
|
+
|
|
380
|
+
return sorted(servers, key=lambda x: x.load)
|
|
381
|
+
|
|
382
|
+
async def recommend_server(
|
|
383
|
+
self,
|
|
384
|
+
purpose: str = "general",
|
|
385
|
+
require_p2p: bool = False,
|
|
386
|
+
require_secure_core: bool = False,
|
|
387
|
+
) -> Optional[VPNServer]:
|
|
388
|
+
"""
|
|
389
|
+
Recommend best server for specific purpose
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
purpose: 'general', 'pentest', 'c2', 'fileshare'
|
|
393
|
+
require_p2p: Need P2P support
|
|
394
|
+
require_secure_core: Need multi-hop
|
|
395
|
+
"""
|
|
396
|
+
servers = await self.get_server_list()
|
|
397
|
+
|
|
398
|
+
# Filter by requirements
|
|
399
|
+
if require_p2p:
|
|
400
|
+
servers = [s for s in servers if "p2p" in s.features]
|
|
401
|
+
if require_secure_core:
|
|
402
|
+
servers = [s for s in servers if "secure-core" in s.features]
|
|
403
|
+
|
|
404
|
+
# Purpose-based selection
|
|
405
|
+
if purpose == "pentest":
|
|
406
|
+
# Low load, secure core for anonymity
|
|
407
|
+
candidates = [s for s in servers if "secure-core" in s.features]
|
|
408
|
+
return min(candidates, key=lambda x: x.load) if candidates else None
|
|
409
|
+
|
|
410
|
+
elif purpose == "c2":
|
|
411
|
+
# Stable, low latency
|
|
412
|
+
return min(servers, key=lambda x: x.load)
|
|
413
|
+
|
|
414
|
+
elif purpose == "fileshare":
|
|
415
|
+
# P2P optimized
|
|
416
|
+
candidates = [s for s in servers if "p2p" in s.features]
|
|
417
|
+
return min(candidates, key=lambda x: x.load) if candidates else None
|
|
418
|
+
|
|
419
|
+
# General purpose - lowest load
|
|
420
|
+
return min(servers, key=lambda x: x.load) if servers else None
|
|
421
|
+
|
|
422
|
+
def get_connection_history(self) -> List[Dict]:
|
|
423
|
+
"""Get history of VPN connections"""
|
|
424
|
+
return self._connection_history
|
|
425
|
+
|
|
426
|
+
def _log_connection(self):
|
|
427
|
+
"""Log connection to history"""
|
|
428
|
+
self._connection_history.append(
|
|
429
|
+
{
|
|
430
|
+
"timestamp": self._get_timestamp(),
|
|
431
|
+
"server": self.status.server_location,
|
|
432
|
+
"protocol": self.status.protocol,
|
|
433
|
+
"public_ip": self.status.public_ip,
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def _get_timestamp(self) -> str:
|
|
438
|
+
"""Get current timestamp"""
|
|
439
|
+
from datetime import datetime
|
|
440
|
+
|
|
441
|
+
return datetime.now().isoformat()
|
|
442
|
+
|
|
443
|
+
def check_ip_leak(self) -> Dict[str, Any]:
|
|
444
|
+
"""
|
|
445
|
+
Check for IP/DNS leaks
|
|
446
|
+
Returns leak test results
|
|
447
|
+
"""
|
|
448
|
+
results = {
|
|
449
|
+
"dns_leak": False,
|
|
450
|
+
"webrtc_leak": False,
|
|
451
|
+
"ipv6_leak": False,
|
|
452
|
+
"recommendations": [],
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# In real implementation, perform actual leak tests
|
|
456
|
+
# For now, return mock results
|
|
457
|
+
|
|
458
|
+
results["dns_servers"] = [
|
|
459
|
+
"10.8.8.1 (Proton VPN DNS)",
|
|
460
|
+
"10.8.8.2 (Proton VPN DNS)",
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
results["recommendations"] = [
|
|
464
|
+
"IPv6 is disabled - Good for privacy",
|
|
465
|
+
"DNS queries go through VPN tunnel",
|
|
466
|
+
"No WebRTC leaks detected",
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
return results
|
|
470
|
+
|
|
471
|
+
async def speed_test(self) -> Dict[str, Any]:
|
|
472
|
+
"""Test VPN connection speed"""
|
|
473
|
+
logger.info("Running speed test...")
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
# Simple curl-based speed test
|
|
477
|
+
proc = await asyncio.create_subprocess_exec(
|
|
478
|
+
"curl",
|
|
479
|
+
"-o",
|
|
480
|
+
"/dev/null",
|
|
481
|
+
"-w",
|
|
482
|
+
"%{time_total},%{speed_download}",
|
|
483
|
+
"https://speed.cloudflare.com/__down?bytes=10000000",
|
|
484
|
+
stdout=asyncio.subprocess.PIPE,
|
|
485
|
+
stderr=asyncio.subprocess.PIPE,
|
|
486
|
+
)
|
|
487
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=30)
|
|
488
|
+
|
|
489
|
+
result = stdout.decode().strip()
|
|
490
|
+
if "," in result:
|
|
491
|
+
time_total, speed = result.split(",")
|
|
492
|
+
return {
|
|
493
|
+
"download_mbps": round(float(speed) / 125000, 2),
|
|
494
|
+
"latency_ms": round(float(time_total) * 1000, 2),
|
|
495
|
+
"status": "success",
|
|
496
|
+
}
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.error(f"Speed test failed: {e}")
|
|
499
|
+
|
|
500
|
+
return {"download_mbps": 0, "latency_ms": 0, "status": "failed"}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# Convenience functions
|
|
504
|
+
async def quick_connect(country: Optional[str] = None) -> VPNStatus:
|
|
505
|
+
"""Quick connect to Proton VPN"""
|
|
506
|
+
vpn = ProtonVPNManager()
|
|
507
|
+
return await vpn.connect(country=country)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
async def secure_connect() -> VPNStatus:
|
|
511
|
+
"""Connect with maximum security (Secure Core)"""
|
|
512
|
+
vpn = ProtonVPNManager()
|
|
513
|
+
return await vpn.connect(
|
|
514
|
+
country="CH", security_level=VPNSecurityLevel.SECURE_CORE, kill_switch=True
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
if __name__ == "__main__":
|
|
519
|
+
# Test
|
|
520
|
+
async def test():
|
|
521
|
+
vpn = ProtonVPNManager()
|
|
522
|
+
|
|
523
|
+
print("=== Proton VPN Manager Test ===")
|
|
524
|
+
|
|
525
|
+
# Get public IP
|
|
526
|
+
ip = await vpn.get_public_ip()
|
|
527
|
+
print(f"Current IP: {ip}")
|
|
528
|
+
|
|
529
|
+
# Connect
|
|
530
|
+
status = await vpn.connect(country="CH")
|
|
531
|
+
print(f"Connected: {status.connected}")
|
|
532
|
+
print(f"Location: {status.server_location}")
|
|
533
|
+
print(f"New IP: {status.public_ip}")
|
|
534
|
+
|
|
535
|
+
# Get servers
|
|
536
|
+
servers = await vpn.get_server_list()
|
|
537
|
+
print(f"\nAvailable servers: {len(servers)}")
|
|
538
|
+
for s in servers[:3]:
|
|
539
|
+
print(f" - {s}")
|
|
540
|
+
|
|
541
|
+
# Recommend server
|
|
542
|
+
recommended = await vpn.recommend_server(purpose="pentest")
|
|
543
|
+
if recommended:
|
|
544
|
+
print(f"\nRecommended for pentest: {recommended}")
|
|
545
|
+
|
|
546
|
+
# Speed test
|
|
547
|
+
speed = await vpn.speed_test()
|
|
548
|
+
print(f"\nSpeed: {speed['download_mbps']} Mbps")
|
|
549
|
+
|
|
550
|
+
# Disconnect
|
|
551
|
+
await vpn.disconnect()
|
|
552
|
+
print("\nDisconnected")
|
|
553
|
+
|
|
554
|
+
asyncio.run(test())
|