forked from JoasASantos/NeuroSploit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontainer_pool.py
More file actions
executable file
·209 lines (179 loc) · 6.98 KB
/
Copy pathcontainer_pool.py
File metadata and controls
executable file
·209 lines (179 loc) · 6.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""
NeuroSploit v3 - Container Pool
Global coordinator for per-scan Kali Linux containers.
Tracks all running sandbox containers, enforces max concurrent limits,
handles lifecycle management and orphan cleanup.
"""
import asyncio
import json
import logging
import threading
from datetime import datetime, timedelta
from typing import Dict, Optional
logger = logging.getLogger(__name__)
try:
import docker
from docker.errors import NotFound
HAS_DOCKER = True
except ImportError:
HAS_DOCKER = False
from core.kali_sandbox import KaliSandbox
class ContainerPool:
"""Global pool managing per-scan KaliSandbox instances.
Thread-safe. One pool per process. Enforces resource limits.
"""
def __init__(
self,
image: str = "neurosploit-kali:latest",
max_concurrent: int = 5,
memory_limit: str = "2g",
cpu_limit: float = 2.0,
container_ttl_minutes: int = 60,
):
self.image = image
self.max_concurrent = max_concurrent
self.memory_limit = memory_limit
self.cpu_limit = cpu_limit
self.container_ttl = timedelta(minutes=container_ttl_minutes)
self._sandboxes: Dict[str, KaliSandbox] = {}
self._lock = asyncio.Lock()
@classmethod
def from_config(cls) -> "ContainerPool":
"""Create pool from config/config.json sandbox section."""
try:
with open("config/config.json") as f:
cfg = json.load(f)
sandbox_cfg = cfg.get("sandbox", {})
kali_cfg = sandbox_cfg.get("kali", {})
resources = sandbox_cfg.get("resources", {})
return cls(
image=kali_cfg.get("image", "neurosploit-kali:latest"),
max_concurrent=kali_cfg.get("max_concurrent", 5),
memory_limit=resources.get("memory_limit", "2g"),
cpu_limit=resources.get("cpu_limit", 2.0),
container_ttl_minutes=kali_cfg.get("container_ttl_minutes", 60),
)
except Exception as e:
logger.warning(f"Could not load pool config, using defaults: {e}")
return cls()
async def get_or_create(
self, scan_id: str, enable_vpn: bool = False,
) -> KaliSandbox:
"""Get existing sandbox for scan_id, or create a new one.
Raises RuntimeError if max_concurrent limit reached.
"""
async with self._lock:
# Return existing
if scan_id in self._sandboxes:
sb = self._sandboxes[scan_id]
if sb.is_available:
return sb
else:
del self._sandboxes[scan_id]
# Check limit
active = sum(1 for sb in self._sandboxes.values() if sb.is_available)
if active >= self.max_concurrent:
raise RuntimeError(
f"Max concurrent containers ({self.max_concurrent}) reached. "
f"Active scans: {list(self._sandboxes.keys())}"
)
# Create new
sb = KaliSandbox(
scan_id=scan_id,
image=self.image,
memory_limit=self.memory_limit,
cpu_limit=self.cpu_limit,
enable_vpn=enable_vpn,
)
ok, msg = await sb.initialize()
if not ok:
raise RuntimeError(f"Failed to create Kali sandbox: {msg}")
self._sandboxes[scan_id] = sb
logger.info(
f"Pool: created container for scan {scan_id} "
f"({active + 1}/{self.max_concurrent} active)"
)
return sb
async def destroy(self, scan_id: str):
"""Stop and remove the container for a specific scan."""
async with self._lock:
sb = self._sandboxes.pop(scan_id, None)
if sb:
await sb.stop()
logger.info(f"Pool: destroyed container for scan {scan_id}")
async def cleanup_all(self):
"""Destroy all managed containers (shutdown hook)."""
async with self._lock:
scan_ids = list(self._sandboxes.keys())
for sid in scan_ids:
await self.destroy(sid)
logger.info("Pool: all containers destroyed")
async def cleanup_orphans(self):
"""Find and remove neurosploit-* containers not tracked by this pool."""
if not HAS_DOCKER:
return
try:
client = docker.from_env()
containers = client.containers.list(
all=True,
filters={"label": "neurosploit.type=kali-sandbox"},
)
async with self._lock:
tracked = set(self._sandboxes.keys())
removed = 0
for c in containers:
scan_id = c.labels.get("neurosploit.scan_id", "")
if scan_id not in tracked:
try:
c.stop(timeout=5)
except Exception:
pass
try:
c.remove(force=True)
removed += 1
logger.info(f"Pool: removed orphan container {c.name}")
except Exception:
pass
if removed:
logger.info(f"Pool: cleaned up {removed} orphan containers")
except Exception as e:
logger.warning(f"Pool: orphan cleanup failed: {e}")
async def cleanup_expired(self):
"""Remove containers that have exceeded their TTL."""
now = datetime.utcnow()
async with self._lock:
expired = [
sid for sid, sb in self._sandboxes.items()
if sb._created_at and (now - sb._created_at) > self.container_ttl
]
for sid in expired:
logger.warning(f"Pool: container for scan {sid} exceeded TTL, destroying")
await self.destroy(sid)
def list_sandboxes(self) -> Dict[str, Dict]:
"""List all tracked sandboxes with status."""
result = {}
for sid, sb in self._sandboxes.items():
result[sid] = {
"scan_id": sid,
"container_name": sb.container_name,
"available": sb.is_available,
"installed_tools": sorted(sb._installed_tools),
"created_at": sb._created_at.isoformat() if sb._created_at else None,
}
return result
@property
def active_count(self) -> int:
return sum(1 for sb in self._sandboxes.values() if sb.is_available)
# ---------------------------------------------------------------------------
# Global singleton pool
# ---------------------------------------------------------------------------
_pool: Optional[ContainerPool] = None
_pool_lock = threading.Lock()
def get_pool() -> ContainerPool:
"""Get or create the global container pool."""
global _pool
if _pool is None:
with _pool_lock:
if _pool is None:
_pool = ContainerPool.from_config()
return _pool