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
core/rate_limiter.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate Limiting & Circuit Breaker
|
|
3
|
+
Prevents API abuse and handles backend failures gracefully
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import hashlib
|
|
8
|
+
import logging
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from functools import wraps
|
|
14
|
+
from typing import Any, Callable, Dict, Optional
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import aioredis
|
|
18
|
+
|
|
19
|
+
REDIS_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
REDIS_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CircuitState(Enum):
|
|
27
|
+
CLOSED = "closed" # Normal operation
|
|
28
|
+
OPEN = "open" # Failing, reject requests
|
|
29
|
+
HALF_OPEN = "half_open" # Testing if service recovered
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RateLimitConfig:
|
|
34
|
+
"""Rate limiting configuration"""
|
|
35
|
+
|
|
36
|
+
requests_per_second: float = 1.0
|
|
37
|
+
burst_size: int = 5
|
|
38
|
+
max_retries: int = 3
|
|
39
|
+
base_delay: float = 1.0
|
|
40
|
+
max_delay: float = 60.0
|
|
41
|
+
exponential_base: float = 2.0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class CircuitBreakerConfig:
|
|
46
|
+
"""Circuit breaker configuration"""
|
|
47
|
+
|
|
48
|
+
failure_threshold: int = 5
|
|
49
|
+
recovery_timeout: float = 30.0
|
|
50
|
+
half_open_max_calls: int = 3
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TokenBucket:
|
|
54
|
+
"""
|
|
55
|
+
Token bucket rate limiter.
|
|
56
|
+
Allows burst traffic while maintaining average rate.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, rate: float, capacity: int):
|
|
60
|
+
self.rate = rate
|
|
61
|
+
self.capacity = capacity
|
|
62
|
+
self.tokens = capacity
|
|
63
|
+
self.last_update = time.monotonic()
|
|
64
|
+
self._lock = asyncio.Lock()
|
|
65
|
+
|
|
66
|
+
async def acquire(self, tokens: int = 1) -> float:
|
|
67
|
+
"""
|
|
68
|
+
Acquire tokens. Returns wait time if not enough tokens.
|
|
69
|
+
"""
|
|
70
|
+
async with self._lock:
|
|
71
|
+
now = time.monotonic()
|
|
72
|
+
elapsed = now - self.last_update
|
|
73
|
+
|
|
74
|
+
# Add tokens based on elapsed time
|
|
75
|
+
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
|
|
76
|
+
self.last_update = now
|
|
77
|
+
|
|
78
|
+
if self.tokens >= tokens:
|
|
79
|
+
self.tokens -= tokens
|
|
80
|
+
return 0.0
|
|
81
|
+
|
|
82
|
+
# Calculate wait time
|
|
83
|
+
needed = tokens - self.tokens
|
|
84
|
+
wait_time = needed / self.rate
|
|
85
|
+
|
|
86
|
+
return wait_time
|
|
87
|
+
|
|
88
|
+
async def wait(self, tokens: int = 1):
|
|
89
|
+
"""Wait until tokens are available"""
|
|
90
|
+
wait_time = await self.acquire(tokens)
|
|
91
|
+
if wait_time > 0:
|
|
92
|
+
await asyncio.sleep(wait_time)
|
|
93
|
+
async with self._lock:
|
|
94
|
+
self.tokens -= tokens
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ExponentialBackoff:
|
|
98
|
+
"""
|
|
99
|
+
Exponential backoff with jitter for retries.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
base_delay: float = 1.0,
|
|
105
|
+
max_delay: float = 60.0,
|
|
106
|
+
exponential_base: float = 2.0,
|
|
107
|
+
jitter: bool = True,
|
|
108
|
+
):
|
|
109
|
+
self.base_delay = base_delay
|
|
110
|
+
self.max_delay = max_delay
|
|
111
|
+
self.exponential_base = exponential_base
|
|
112
|
+
self.jitter = jitter
|
|
113
|
+
self.attempt = 0
|
|
114
|
+
|
|
115
|
+
def next_delay(self) -> float:
|
|
116
|
+
"""Calculate next delay with exponential backoff"""
|
|
117
|
+
delay = min(
|
|
118
|
+
self.base_delay * (self.exponential_base**self.attempt), self.max_delay
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if self.jitter:
|
|
122
|
+
# Add random jitter (±25%)
|
|
123
|
+
delay *= random.uniform(0.75, 1.25)
|
|
124
|
+
|
|
125
|
+
self.attempt += 1
|
|
126
|
+
return delay
|
|
127
|
+
|
|
128
|
+
def reset(self):
|
|
129
|
+
"""Reset attempt counter"""
|
|
130
|
+
self.attempt = 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CircuitBreaker:
|
|
134
|
+
"""
|
|
135
|
+
Circuit breaker pattern implementation.
|
|
136
|
+
Prevents cascading failures when backends are down.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, name: str, config: CircuitBreakerConfig = None):
|
|
140
|
+
self.name = name
|
|
141
|
+
self.config = config or CircuitBreakerConfig()
|
|
142
|
+
self.state = CircuitState.CLOSED
|
|
143
|
+
self.failures = 0
|
|
144
|
+
self.last_failure_time: Optional[float] = None
|
|
145
|
+
self.half_open_calls = 0
|
|
146
|
+
self._lock = asyncio.Lock()
|
|
147
|
+
|
|
148
|
+
async def call(self, func: Callable, *args, **kwargs) -> Any:
|
|
149
|
+
"""
|
|
150
|
+
Execute function with circuit breaker protection.
|
|
151
|
+
"""
|
|
152
|
+
async with self._lock:
|
|
153
|
+
if self.state == CircuitState.OPEN:
|
|
154
|
+
if self._should_attempt_reset():
|
|
155
|
+
self.state = CircuitState.HALF_OPEN
|
|
156
|
+
self.half_open_calls = 0
|
|
157
|
+
logger.info(f"Circuit {self.name}: Entering HALF_OPEN state")
|
|
158
|
+
else:
|
|
159
|
+
raise CircuitBreakerOpen(f"Circuit {self.name} is OPEN")
|
|
160
|
+
|
|
161
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
162
|
+
if self.half_open_calls >= self.config.half_open_max_calls:
|
|
163
|
+
raise CircuitBreakerOpen(
|
|
164
|
+
f"Circuit {self.name} HALF_OPEN limit reached"
|
|
165
|
+
)
|
|
166
|
+
self.half_open_calls += 1
|
|
167
|
+
|
|
168
|
+
# Execute outside lock
|
|
169
|
+
try:
|
|
170
|
+
result = await func(*args, **kwargs)
|
|
171
|
+
await self._on_success()
|
|
172
|
+
return result
|
|
173
|
+
except Exception as e:
|
|
174
|
+
await self._on_failure()
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
def _should_attempt_reset(self) -> bool:
|
|
178
|
+
"""Check if enough time passed to try recovery"""
|
|
179
|
+
if self.last_failure_time is None:
|
|
180
|
+
return True
|
|
181
|
+
return (
|
|
182
|
+
time.monotonic() - self.last_failure_time
|
|
183
|
+
) >= self.config.recovery_timeout
|
|
184
|
+
|
|
185
|
+
async def _on_success(self):
|
|
186
|
+
"""Handle successful call"""
|
|
187
|
+
async with self._lock:
|
|
188
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
189
|
+
self.state = CircuitState.CLOSED
|
|
190
|
+
self.failures = 0
|
|
191
|
+
self.half_open_calls = 0
|
|
192
|
+
logger.info(f"Circuit {self.name}: CLOSED (recovered)")
|
|
193
|
+
|
|
194
|
+
async def _on_failure(self):
|
|
195
|
+
"""Handle failed call"""
|
|
196
|
+
async with self._lock:
|
|
197
|
+
self.failures += 1
|
|
198
|
+
self.last_failure_time = time.monotonic()
|
|
199
|
+
|
|
200
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
201
|
+
self.state = CircuitState.OPEN
|
|
202
|
+
logger.warning(f"Circuit {self.name}: OPEN (recovery failed)")
|
|
203
|
+
elif self.failures >= self.config.failure_threshold:
|
|
204
|
+
self.state = CircuitState.OPEN
|
|
205
|
+
logger.warning(f"Circuit {self.name}: OPEN ({self.failures} failures)")
|
|
206
|
+
|
|
207
|
+
def get_state(self) -> CircuitState:
|
|
208
|
+
"""Get current circuit state"""
|
|
209
|
+
return self.state
|
|
210
|
+
|
|
211
|
+
async def force_reset(self):
|
|
212
|
+
"""Force circuit to CLOSED state"""
|
|
213
|
+
async with self._lock:
|
|
214
|
+
self.state = CircuitState.CLOSED
|
|
215
|
+
self.failures = 0
|
|
216
|
+
self.half_open_calls = 0
|
|
217
|
+
logger.info(f"Circuit {self.name}: Force reset to CLOSED")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class CircuitBreakerOpen(Exception):
|
|
221
|
+
"""Exception raised when circuit breaker is open"""
|
|
222
|
+
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class RateLimitedClient:
|
|
227
|
+
"""
|
|
228
|
+
HTTP client with rate limiting, retries, and circuit breaker.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(
|
|
232
|
+
self,
|
|
233
|
+
name: str,
|
|
234
|
+
rate_config: RateLimitConfig = None,
|
|
235
|
+
circuit_config: CircuitBreakerConfig = None,
|
|
236
|
+
):
|
|
237
|
+
self.name = name
|
|
238
|
+
self.rate_config = rate_config or RateLimitConfig()
|
|
239
|
+
self.circuit = CircuitBreaker(name, circuit_config)
|
|
240
|
+
|
|
241
|
+
# Token bucket for rate limiting
|
|
242
|
+
self.bucket = TokenBucket(
|
|
243
|
+
rate=self.rate_config.requests_per_second,
|
|
244
|
+
capacity=self.rate_config.burst_size,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self._session: Optional[Any] = None
|
|
248
|
+
|
|
249
|
+
async def request(
|
|
250
|
+
self, method: str, url: str, retries: int = None, **kwargs
|
|
251
|
+
) -> Any:
|
|
252
|
+
"""
|
|
253
|
+
Make rate-limited request with circuit breaker.
|
|
254
|
+
"""
|
|
255
|
+
retries = retries or self.rate_config.max_retries
|
|
256
|
+
backoff = ExponentialBackoff(
|
|
257
|
+
base_delay=self.rate_config.base_delay, max_delay=self.rate_config.max_delay
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
last_error = None
|
|
261
|
+
|
|
262
|
+
for attempt in range(retries + 1):
|
|
263
|
+
# Wait for rate limit
|
|
264
|
+
await self.bucket.wait()
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
return await self.circuit.call(self._do_request, method, url, **kwargs)
|
|
268
|
+
except CircuitBreakerOpen:
|
|
269
|
+
raise
|
|
270
|
+
except Exception as e:
|
|
271
|
+
last_error = e
|
|
272
|
+
logger.warning(
|
|
273
|
+
f"{self.name} request failed (attempt {attempt + 1}): {e}"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if attempt < retries:
|
|
277
|
+
delay = backoff.next_delay()
|
|
278
|
+
logger.info(f"Retrying in {delay:.2f}s...")
|
|
279
|
+
await asyncio.sleep(delay)
|
|
280
|
+
|
|
281
|
+
raise last_error or Exception("Max retries exceeded")
|
|
282
|
+
|
|
283
|
+
async def _do_request(self, method: str, url: str, **kwargs) -> Any:
|
|
284
|
+
"""Actual request implementation - override in subclass"""
|
|
285
|
+
raise NotImplementedError("Subclasses must implement _do_request")
|
|
286
|
+
|
|
287
|
+
async def close(self):
|
|
288
|
+
"""Close client resources"""
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class SmartRouter:
|
|
293
|
+
"""
|
|
294
|
+
Intelligent backend routing with health checks and circuit breakers.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def __init__(self):
|
|
298
|
+
self.backends: Dict[str, Any] = {}
|
|
299
|
+
self.circuit_breakers: Dict[str, CircuitBreaker] = {}
|
|
300
|
+
self.health_status: Dict[str, bool] = {}
|
|
301
|
+
self._lock = asyncio.Lock()
|
|
302
|
+
|
|
303
|
+
def register_backend(
|
|
304
|
+
self,
|
|
305
|
+
name: str,
|
|
306
|
+
backend: Any,
|
|
307
|
+
priority: int = 0,
|
|
308
|
+
circuit_config: CircuitBreakerConfig = None,
|
|
309
|
+
):
|
|
310
|
+
"""Register a backend with circuit breaker"""
|
|
311
|
+
self.backends[name] = {"instance": backend, "priority": priority, "name": name}
|
|
312
|
+
self.circuit_breakers[name] = CircuitBreaker(name, circuit_config)
|
|
313
|
+
self.health_status[name] = True
|
|
314
|
+
|
|
315
|
+
async def get_healthy_backend(self, min_priority: int = 0) -> Optional[Any]:
|
|
316
|
+
"""
|
|
317
|
+
Get a healthy backend based on priority and circuit state.
|
|
318
|
+
"""
|
|
319
|
+
# Sort by priority (highest first)
|
|
320
|
+
candidates = sorted(
|
|
321
|
+
self.backends.values(), key=lambda b: b["priority"], reverse=True
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
for backend_info in candidates:
|
|
325
|
+
name = backend_info["name"]
|
|
326
|
+
|
|
327
|
+
# Check circuit breaker state
|
|
328
|
+
circuit = self.circuit_breakers[name]
|
|
329
|
+
if circuit.state == CircuitState.OPEN:
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
# Check health status
|
|
333
|
+
if await self._is_healthy(name):
|
|
334
|
+
return backend_info["instance"]
|
|
335
|
+
|
|
336
|
+
# Fallback: try any backend even if circuit is half-open
|
|
337
|
+
for backend_info in candidates:
|
|
338
|
+
name = backend_info["name"]
|
|
339
|
+
circuit = self.circuit_breakers[name]
|
|
340
|
+
if circuit.state == CircuitState.HALF_OPEN:
|
|
341
|
+
return backend_info["instance"]
|
|
342
|
+
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
async def _is_healthy(self, name: str) -> bool:
|
|
346
|
+
"""Check if backend is healthy"""
|
|
347
|
+
# Could implement actual health check here
|
|
348
|
+
return self.health_status.get(name, False)
|
|
349
|
+
|
|
350
|
+
async def execute(self, name: str, func: Callable, *args, **kwargs) -> Any:
|
|
351
|
+
"""Execute function with circuit breaker protection"""
|
|
352
|
+
if name not in self.circuit_breakers:
|
|
353
|
+
raise ValueError(f"Unknown backend: {name}")
|
|
354
|
+
|
|
355
|
+
circuit = self.circuit_breakers[name]
|
|
356
|
+
return await circuit.call(func, *args, **kwargs)
|
|
357
|
+
|
|
358
|
+
def get_status(self) -> Dict[str, Any]:
|
|
359
|
+
"""Get status of all backends"""
|
|
360
|
+
return {
|
|
361
|
+
name: {
|
|
362
|
+
"circuit_state": cb.state.value,
|
|
363
|
+
"failures": cb.failures,
|
|
364
|
+
"healthy": self.health_status.get(name, False),
|
|
365
|
+
}
|
|
366
|
+
for name, cb in self.circuit_breakers.items()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def rate_limited(
|
|
371
|
+
requests_per_second: float = 1.0, burst_size: int = 5, max_retries: int = 3
|
|
372
|
+
):
|
|
373
|
+
"""
|
|
374
|
+
Decorator for rate limiting async functions.
|
|
375
|
+
"""
|
|
376
|
+
bucket = TokenBucket(rate=requests_per_second, capacity=burst_size)
|
|
377
|
+
|
|
378
|
+
def decorator(func: Callable) -> Callable:
|
|
379
|
+
@wraps(func)
|
|
380
|
+
async def wrapper(*args, **kwargs):
|
|
381
|
+
await bucket.wait()
|
|
382
|
+
|
|
383
|
+
backoff = ExponentialBackoff()
|
|
384
|
+
last_error = None
|
|
385
|
+
|
|
386
|
+
for attempt in range(max_retries + 1):
|
|
387
|
+
try:
|
|
388
|
+
return await func(*args, **kwargs)
|
|
389
|
+
except Exception as e:
|
|
390
|
+
last_error = e
|
|
391
|
+
if attempt < max_retries:
|
|
392
|
+
delay = backoff.next_delay()
|
|
393
|
+
await asyncio.sleep(delay)
|
|
394
|
+
|
|
395
|
+
raise last_error
|
|
396
|
+
|
|
397
|
+
return wrapper
|
|
398
|
+
|
|
399
|
+
return decorator
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def cached_key(*args, **kwargs) -> str:
|
|
403
|
+
"""Generate cache key from function arguments"""
|
|
404
|
+
key_data = str(args) + str(sorted(kwargs.items()))
|
|
405
|
+
return hashlib.md5(key_data.encode()).hexdigest()
|