aiptx 2.0.2__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.

Potentially problematic release.


This version of aiptx might be problematic. Click here for more details.

Files changed (165) hide show
  1. aipt_v2/__init__.py +110 -0
  2. aipt_v2/__main__.py +24 -0
  3. aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
  4. aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
  5. aipt_v2/agents/__init__.py +24 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/ptt.py +406 -0
  8. aipt_v2/agents/state.py +168 -0
  9. aipt_v2/app.py +960 -0
  10. aipt_v2/browser/__init__.py +31 -0
  11. aipt_v2/browser/automation.py +458 -0
  12. aipt_v2/browser/crawler.py +453 -0
  13. aipt_v2/cli.py +321 -0
  14. aipt_v2/compliance/__init__.py +71 -0
  15. aipt_v2/compliance/compliance_report.py +449 -0
  16. aipt_v2/compliance/framework_mapper.py +424 -0
  17. aipt_v2/compliance/nist_mapping.py +345 -0
  18. aipt_v2/compliance/owasp_mapping.py +330 -0
  19. aipt_v2/compliance/pci_mapping.py +297 -0
  20. aipt_v2/config.py +288 -0
  21. aipt_v2/core/__init__.py +43 -0
  22. aipt_v2/core/agent.py +630 -0
  23. aipt_v2/core/llm.py +395 -0
  24. aipt_v2/core/memory.py +305 -0
  25. aipt_v2/core/ptt.py +329 -0
  26. aipt_v2/database/__init__.py +14 -0
  27. aipt_v2/database/models.py +232 -0
  28. aipt_v2/database/repository.py +384 -0
  29. aipt_v2/docker/__init__.py +23 -0
  30. aipt_v2/docker/builder.py +260 -0
  31. aipt_v2/docker/manager.py +222 -0
  32. aipt_v2/docker/sandbox.py +371 -0
  33. aipt_v2/evasion/__init__.py +58 -0
  34. aipt_v2/evasion/request_obfuscator.py +272 -0
  35. aipt_v2/evasion/tls_fingerprint.py +285 -0
  36. aipt_v2/evasion/ua_rotator.py +301 -0
  37. aipt_v2/evasion/waf_bypass.py +439 -0
  38. aipt_v2/execution/__init__.py +23 -0
  39. aipt_v2/execution/executor.py +302 -0
  40. aipt_v2/execution/parser.py +544 -0
  41. aipt_v2/execution/terminal.py +337 -0
  42. aipt_v2/health.py +437 -0
  43. aipt_v2/intelligence/__init__.py +85 -0
  44. aipt_v2/intelligence/auth.py +520 -0
  45. aipt_v2/intelligence/chaining.py +775 -0
  46. aipt_v2/intelligence/cve_aipt.py +334 -0
  47. aipt_v2/intelligence/cve_info.py +1111 -0
  48. aipt_v2/intelligence/rag.py +239 -0
  49. aipt_v2/intelligence/scope.py +442 -0
  50. aipt_v2/intelligence/searchers/__init__.py +5 -0
  51. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  52. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  53. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  54. aipt_v2/intelligence/tools.json +443 -0
  55. aipt_v2/intelligence/triage.py +670 -0
  56. aipt_v2/interface/__init__.py +5 -0
  57. aipt_v2/interface/cli.py +230 -0
  58. aipt_v2/interface/main.py +501 -0
  59. aipt_v2/interface/tui.py +1276 -0
  60. aipt_v2/interface/utils.py +583 -0
  61. aipt_v2/llm/__init__.py +39 -0
  62. aipt_v2/llm/config.py +26 -0
  63. aipt_v2/llm/llm.py +514 -0
  64. aipt_v2/llm/memory.py +214 -0
  65. aipt_v2/llm/request_queue.py +89 -0
  66. aipt_v2/llm/utils.py +89 -0
  67. aipt_v2/models/__init__.py +15 -0
  68. aipt_v2/models/findings.py +295 -0
  69. aipt_v2/models/phase_result.py +224 -0
  70. aipt_v2/models/scan_config.py +207 -0
  71. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  72. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  73. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  74. aipt_v2/monitoring/prometheus.yml +60 -0
  75. aipt_v2/orchestration/__init__.py +52 -0
  76. aipt_v2/orchestration/pipeline.py +398 -0
  77. aipt_v2/orchestration/progress.py +300 -0
  78. aipt_v2/orchestration/scheduler.py +296 -0
  79. aipt_v2/orchestrator.py +2284 -0
  80. aipt_v2/payloads/__init__.py +27 -0
  81. aipt_v2/payloads/cmdi.py +150 -0
  82. aipt_v2/payloads/sqli.py +263 -0
  83. aipt_v2/payloads/ssrf.py +204 -0
  84. aipt_v2/payloads/templates.py +222 -0
  85. aipt_v2/payloads/traversal.py +166 -0
  86. aipt_v2/payloads/xss.py +204 -0
  87. aipt_v2/prompts/__init__.py +60 -0
  88. aipt_v2/proxy/__init__.py +29 -0
  89. aipt_v2/proxy/history.py +352 -0
  90. aipt_v2/proxy/interceptor.py +452 -0
  91. aipt_v2/recon/__init__.py +44 -0
  92. aipt_v2/recon/dns.py +241 -0
  93. aipt_v2/recon/osint.py +367 -0
  94. aipt_v2/recon/subdomain.py +372 -0
  95. aipt_v2/recon/tech_detect.py +311 -0
  96. aipt_v2/reports/__init__.py +17 -0
  97. aipt_v2/reports/generator.py +313 -0
  98. aipt_v2/reports/html_report.py +378 -0
  99. aipt_v2/runtime/__init__.py +44 -0
  100. aipt_v2/runtime/base.py +30 -0
  101. aipt_v2/runtime/docker.py +401 -0
  102. aipt_v2/runtime/local.py +346 -0
  103. aipt_v2/runtime/tool_server.py +205 -0
  104. aipt_v2/scanners/__init__.py +28 -0
  105. aipt_v2/scanners/base.py +273 -0
  106. aipt_v2/scanners/nikto.py +244 -0
  107. aipt_v2/scanners/nmap.py +402 -0
  108. aipt_v2/scanners/nuclei.py +273 -0
  109. aipt_v2/scanners/web.py +454 -0
  110. aipt_v2/scripts/security_audit.py +366 -0
  111. aipt_v2/telemetry/__init__.py +7 -0
  112. aipt_v2/telemetry/tracer.py +347 -0
  113. aipt_v2/terminal/__init__.py +28 -0
  114. aipt_v2/terminal/executor.py +400 -0
  115. aipt_v2/terminal/sandbox.py +350 -0
  116. aipt_v2/tools/__init__.py +44 -0
  117. aipt_v2/tools/active_directory/__init__.py +78 -0
  118. aipt_v2/tools/active_directory/ad_config.py +238 -0
  119. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  120. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  121. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  122. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  123. aipt_v2/tools/agents_graph/__init__.py +19 -0
  124. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  125. aipt_v2/tools/api_security/__init__.py +76 -0
  126. aipt_v2/tools/api_security/api_discovery.py +608 -0
  127. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  128. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  129. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  130. aipt_v2/tools/browser/__init__.py +5 -0
  131. aipt_v2/tools/browser/browser_actions.py +238 -0
  132. aipt_v2/tools/browser/browser_instance.py +535 -0
  133. aipt_v2/tools/browser/tab_manager.py +344 -0
  134. aipt_v2/tools/cloud/__init__.py +70 -0
  135. aipt_v2/tools/cloud/cloud_config.py +273 -0
  136. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  137. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  138. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  139. aipt_v2/tools/executor.py +307 -0
  140. aipt_v2/tools/parser.py +408 -0
  141. aipt_v2/tools/proxy/__init__.py +5 -0
  142. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  143. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  144. aipt_v2/tools/registry.py +196 -0
  145. aipt_v2/tools/scanners/__init__.py +343 -0
  146. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  147. aipt_v2/tools/scanners/burp_tool.py +631 -0
  148. aipt_v2/tools/scanners/config.py +156 -0
  149. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  150. aipt_v2/tools/scanners/zap_tool.py +612 -0
  151. aipt_v2/tools/terminal/__init__.py +5 -0
  152. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  153. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  154. aipt_v2/tools/terminal/terminal_session.py +449 -0
  155. aipt_v2/tools/tool_processing.py +108 -0
  156. aipt_v2/utils/__init__.py +17 -0
  157. aipt_v2/utils/logging.py +201 -0
  158. aipt_v2/utils/model_manager.py +187 -0
  159. aipt_v2/utils/searchers/__init__.py +269 -0
  160. aiptx-2.0.2.dist-info/METADATA +324 -0
  161. aiptx-2.0.2.dist-info/RECORD +165 -0
  162. aiptx-2.0.2.dist-info/WHEEL +5 -0
  163. aiptx-2.0.2.dist-info/entry_points.txt +7 -0
  164. aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
  165. aiptx-2.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,44 @@
1
+ """
2
+ AIPT Runtime Module - Docker sandbox and execution environment
3
+ """
4
+
5
+ from aipt_v2.runtime.base import AbstractRuntime, SandboxInfo
6
+
7
+ # Lazy import for DockerRuntime to avoid Docker dependency issues
8
+ _runtime = None
9
+
10
+
11
+ def __getattr__(name):
12
+ """Lazy import for optional Docker dependency"""
13
+ if name == "DockerRuntime":
14
+ from aipt_v2.runtime.docker import DockerRuntime
15
+ return DockerRuntime
16
+ raise AttributeError(f"module 'aipt_v2.runtime' has no attribute '{name}'")
17
+
18
+
19
+ def get_runtime():
20
+ """Get or create the global runtime instance"""
21
+ global _runtime
22
+ if _runtime is None:
23
+ from aipt_v2.runtime.docker import DockerRuntime
24
+ _runtime = DockerRuntime()
25
+ return _runtime
26
+
27
+
28
+ def set_runtime(runtime: AbstractRuntime) -> None:
29
+ """Set the global runtime instance"""
30
+ global _runtime
31
+ _runtime = runtime
32
+
33
+
34
+ # Alias for backwards compatibility
35
+ BaseRuntime = AbstractRuntime
36
+
37
+
38
+ __all__ = [
39
+ "AbstractRuntime",
40
+ "BaseRuntime",
41
+ "SandboxInfo",
42
+ "get_runtime",
43
+ "set_runtime",
44
+ ]
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from typing import TypedDict, Optional, List, Dict
4
+
5
+
6
+ class SandboxInfo(TypedDict):
7
+ workspace_id: str
8
+ api_url: str
9
+ auth_token: Optional[str]
10
+ tool_server_port: int
11
+ agent_id: str
12
+
13
+
14
+ class AbstractRuntime(ABC):
15
+ @abstractmethod
16
+ async def create_sandbox(
17
+ self,
18
+ agent_id: str,
19
+ existing_token: Optional[str] = None,
20
+ local_sources: Optional[List[Dict[str, str]]] = None,
21
+ ) -> SandboxInfo:
22
+ raise NotImplementedError
23
+
24
+ @abstractmethod
25
+ async def get_sandbox_url(self, container_id: str, port: int) -> str:
26
+ raise NotImplementedError
27
+
28
+ @abstractmethod
29
+ async def destroy_sandbox(self, container_id: str) -> None:
30
+ raise NotImplementedError
@@ -0,0 +1,401 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import logging
5
+ import os
6
+ import secrets
7
+ import socket
8
+ import time
9
+ from pathlib import Path
10
+ from typing import cast
11
+
12
+ import docker
13
+ from docker.errors import DockerException, ImageNotFound, NotFound
14
+ from docker.models.containers import Container
15
+
16
+ from .runtime import AbstractRuntime, SandboxInfo
17
+
18
+
19
+ AIPT_IMAGE = os.getenv("AIPT_IMAGE", "ghcr.io/aipt/aipt-sandbox:0.1.10")
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class DockerRuntime(AbstractRuntime):
24
+ def __init__(self) -> None:
25
+ try:
26
+ self.client = docker.from_env()
27
+ except DockerException as e:
28
+ logger.exception("Failed to connect to Docker daemon")
29
+ raise RuntimeError("Docker is not available or not configured correctly.") from e
30
+
31
+ self._scan_container: Container | None = None
32
+ self._tool_server_port: int | None = None
33
+ self._tool_server_token: str | None = None
34
+
35
+ def _generate_sandbox_token(self) -> str:
36
+ return secrets.token_urlsafe(32)
37
+
38
+ def _find_available_port(self) -> int:
39
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
40
+ s.bind(("", 0))
41
+ return cast("int", s.getsockname()[1])
42
+
43
+ def _get_scan_id(self, agent_id: str) -> str:
44
+ try:
45
+ from aipt_v2.telemetry.tracer import get_global_tracer
46
+
47
+ tracer = get_global_tracer()
48
+ if tracer and tracer.scan_config:
49
+ return str(tracer.scan_config.get("scan_id", "default-scan"))
50
+ except ImportError:
51
+ logger.debug("Failed to import tracer, using fallback scan ID")
52
+ except AttributeError:
53
+ logger.debug("Tracer missing scan_config, using fallback scan ID")
54
+
55
+ return f"scan-{agent_id.split('-')[0]}"
56
+
57
+ def _verify_image_available(self, image_name: str, max_retries: int = 3) -> None:
58
+ def _validate_image(image: docker.models.images.Image) -> None:
59
+ if not image.id or not image.attrs:
60
+ raise ImageNotFound(f"Image {image_name} metadata incomplete")
61
+
62
+ for attempt in range(max_retries):
63
+ try:
64
+ image = self.client.images.get(image_name)
65
+ _validate_image(image)
66
+ except ImageNotFound:
67
+ if attempt == max_retries - 1:
68
+ logger.exception(f"Image {image_name} not found after {max_retries} attempts")
69
+ raise
70
+ logger.warning(f"Image {image_name} not ready, attempt {attempt + 1}/{max_retries}")
71
+ time.sleep(2**attempt)
72
+ except DockerException:
73
+ if attempt == max_retries - 1:
74
+ logger.exception(f"Failed to verify image {image_name}")
75
+ raise
76
+ logger.warning(f"Docker error verifying image, attempt {attempt + 1}/{max_retries}")
77
+ time.sleep(2**attempt)
78
+ else:
79
+ logger.debug(f"Image {image_name} verified as available")
80
+ return
81
+
82
+ def _create_container_with_retry(self, scan_id: str, max_retries: int = 3) -> Container:
83
+ last_exception = None
84
+ container_name = f"aipt-scan-{scan_id}"
85
+
86
+ for attempt in range(max_retries):
87
+ try:
88
+ self._verify_image_available(AIPT_IMAGE)
89
+
90
+ try:
91
+ existing_container = self.client.containers.get(container_name)
92
+ logger.warning(f"Container {container_name} already exists, removing it")
93
+ with contextlib.suppress(Exception):
94
+ existing_container.stop(timeout=5)
95
+ existing_container.remove(force=True)
96
+ time.sleep(1)
97
+ except NotFound:
98
+ pass
99
+ except DockerException as e:
100
+ logger.warning(f"Error checking/removing existing container: {e}")
101
+
102
+ caido_port = self._find_available_port()
103
+ tool_server_port = self._find_available_port()
104
+ tool_server_token = self._generate_sandbox_token()
105
+
106
+ self._tool_server_port = tool_server_port
107
+ self._tool_server_token = tool_server_token
108
+
109
+ container = self.client.containers.run(
110
+ AIPT_IMAGE,
111
+ command="sleep infinity",
112
+ detach=True,
113
+ name=container_name,
114
+ hostname=f"aipt-scan-{scan_id}",
115
+ ports={
116
+ f"{caido_port}/tcp": caido_port,
117
+ f"{tool_server_port}/tcp": tool_server_port,
118
+ },
119
+ cap_add=["NET_ADMIN", "NET_RAW"],
120
+ labels={"aipt-scan-id": scan_id},
121
+ environment={
122
+ "PYTHONUNBUFFERED": "1",
123
+ "CAIDO_PORT": str(caido_port),
124
+ "TOOL_SERVER_PORT": str(tool_server_port),
125
+ "TOOL_SERVER_TOKEN": tool_server_token,
126
+ },
127
+ tty=True,
128
+ )
129
+
130
+ self._scan_container = container
131
+ logger.info("Created container %s for scan %s", container.id, scan_id)
132
+
133
+ self._initialize_container(
134
+ container, caido_port, tool_server_port, tool_server_token
135
+ )
136
+ except DockerException as e:
137
+ last_exception = e
138
+ if attempt == max_retries - 1:
139
+ logger.exception(f"Failed to create container after {max_retries} attempts")
140
+ break
141
+
142
+ logger.warning(f"Container creation attempt {attempt + 1}/{max_retries} failed")
143
+
144
+ self._tool_server_port = None
145
+ self._tool_server_token = None
146
+
147
+ sleep_time = (2**attempt) + (0.1 * attempt)
148
+ time.sleep(sleep_time)
149
+ else:
150
+ return container
151
+
152
+ raise RuntimeError(
153
+ f"Failed to create Docker container after {max_retries} attempts: {last_exception}"
154
+ ) from last_exception
155
+
156
+ def _get_or_create_scan_container(self, scan_id: str) -> Container: # noqa: PLR0912
157
+ container_name = f"aipt-scan-{scan_id}"
158
+
159
+ if self._scan_container:
160
+ try:
161
+ self._scan_container.reload()
162
+ if self._scan_container.status == "running":
163
+ return self._scan_container
164
+ except NotFound:
165
+ self._scan_container = None
166
+ self._tool_server_port = None
167
+ self._tool_server_token = None
168
+
169
+ try:
170
+ container = self.client.containers.get(container_name)
171
+ container.reload()
172
+
173
+ if (
174
+ "aipt-scan-id" not in container.labels
175
+ or container.labels["aipt-scan-id"] != scan_id
176
+ ):
177
+ logger.warning(
178
+ f"Container {container_name} exists but missing/wrong label, updating"
179
+ )
180
+
181
+ if container.status != "running":
182
+ logger.info(f"Starting existing container {container_name}")
183
+ container.start()
184
+ time.sleep(2)
185
+
186
+ self._scan_container = container
187
+
188
+ for env_var in container.attrs["Config"]["Env"]:
189
+ if env_var.startswith("TOOL_SERVER_PORT="):
190
+ self._tool_server_port = int(env_var.split("=")[1])
191
+ elif env_var.startswith("TOOL_SERVER_TOKEN="):
192
+ self._tool_server_token = env_var.split("=")[1]
193
+
194
+ logger.info(f"Reusing existing container {container_name}")
195
+
196
+ except NotFound:
197
+ pass
198
+ except DockerException as e:
199
+ logger.warning(f"Failed to get container by name {container_name}: {e}")
200
+ else:
201
+ return container
202
+
203
+ try:
204
+ containers = self.client.containers.list(
205
+ all=True, filters={"label": f"aipt-scan-id={scan_id}"}
206
+ )
207
+ if containers:
208
+ container = cast("Container", containers[0])
209
+ if container.status != "running":
210
+ container.start()
211
+ time.sleep(2)
212
+ self._scan_container = container
213
+
214
+ for env_var in container.attrs["Config"]["Env"]:
215
+ if env_var.startswith("TOOL_SERVER_PORT="):
216
+ self._tool_server_port = int(env_var.split("=")[1])
217
+ elif env_var.startswith("TOOL_SERVER_TOKEN="):
218
+ self._tool_server_token = env_var.split("=")[1]
219
+
220
+ logger.info(f"Found existing container by label for scan {scan_id}")
221
+ return container
222
+ except DockerException as e:
223
+ logger.warning("Failed to find existing container by label for scan %s: %s", scan_id, e)
224
+
225
+ logger.info("Creating new Docker container for scan %s", scan_id)
226
+ return self._create_container_with_retry(scan_id)
227
+
228
+ def _initialize_container(
229
+ self, container: Container, caido_port: int, tool_server_port: int, tool_server_token: str
230
+ ) -> None:
231
+ logger.info("Initializing Caido proxy on port %s", caido_port)
232
+ result = container.exec_run(
233
+ f"bash -c 'export CAIDO_PORT={caido_port} && /usr/local/bin/docker-entrypoint.sh true'",
234
+ detach=False,
235
+ )
236
+
237
+ time.sleep(5)
238
+
239
+ result = container.exec_run(
240
+ "bash -c 'source /etc/profile.d/proxy.sh && echo $CAIDO_API_TOKEN'", user="pentester"
241
+ )
242
+ caido_token = result.output.decode().strip() if result.exit_code == 0 else ""
243
+
244
+ container.exec_run(
245
+ f"bash -c 'source /etc/profile.d/proxy.sh && cd /app && "
246
+ f"AIPT_SANDBOX_MODE=true CAIDO_API_TOKEN={caido_token} CAIDO_PORT={caido_port} "
247
+ f"poetry run python aipt_v2/runtime/tool_server.py --token {tool_server_token} "
248
+ f"--host 0.0.0.0 --port {tool_server_port} &'",
249
+ detach=True,
250
+ user="pentester",
251
+ )
252
+
253
+ time.sleep(5)
254
+
255
+ def _copy_local_directory_to_container(
256
+ self, container: Container, local_path: str, target_name: str | None = None
257
+ ) -> None:
258
+ import tarfile
259
+ from io import BytesIO
260
+
261
+ try:
262
+ local_path_obj = Path(local_path).resolve()
263
+ if not local_path_obj.exists() or not local_path_obj.is_dir():
264
+ logger.warning(f"Local path does not exist or is not directory: {local_path_obj}")
265
+ return
266
+
267
+ if target_name:
268
+ logger.info(
269
+ f"Copying local directory {local_path_obj} to container at "
270
+ f"/workspace/{target_name}"
271
+ )
272
+ else:
273
+ logger.info(f"Copying local directory {local_path_obj} to container")
274
+
275
+ tar_buffer = BytesIO()
276
+ with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
277
+ for item in local_path_obj.rglob("*"):
278
+ if item.is_file():
279
+ rel_path = item.relative_to(local_path_obj)
280
+ arcname = Path(target_name) / rel_path if target_name else rel_path
281
+ tar.add(item, arcname=arcname)
282
+
283
+ tar_buffer.seek(0)
284
+ container.put_archive("/workspace", tar_buffer.getvalue())
285
+
286
+ container.exec_run(
287
+ "chown -R pentester:pentester /workspace && chmod -R 755 /workspace",
288
+ user="root",
289
+ )
290
+
291
+ logger.info("Successfully copied local directory to /workspace")
292
+
293
+ except (OSError, DockerException):
294
+ logger.exception("Failed to copy local directory to container")
295
+
296
+ async def create_sandbox(
297
+ self,
298
+ agent_id: str,
299
+ existing_token: str | None = None,
300
+ local_sources: list[dict[str, str]] | None = None,
301
+ ) -> SandboxInfo:
302
+ scan_id = self._get_scan_id(agent_id)
303
+ container = self._get_or_create_scan_container(scan_id)
304
+
305
+ source_copied_key = f"_source_copied_{scan_id}"
306
+ if local_sources and not hasattr(self, source_copied_key):
307
+ for index, source in enumerate(local_sources, start=1):
308
+ source_path = source.get("source_path")
309
+ if not source_path:
310
+ continue
311
+
312
+ target_name = source.get("workspace_subdir")
313
+ if not target_name:
314
+ target_name = Path(source_path).name or f"target_{index}"
315
+
316
+ self._copy_local_directory_to_container(container, source_path, target_name)
317
+ setattr(self, source_copied_key, True)
318
+
319
+ container_id = container.id
320
+ if container_id is None:
321
+ raise RuntimeError("Docker container ID is unexpectedly None")
322
+
323
+ token = existing_token if existing_token is not None else self._tool_server_token
324
+
325
+ if self._tool_server_port is None or token is None:
326
+ raise RuntimeError("Tool server not initialized or no token available")
327
+
328
+ api_url = await self.get_sandbox_url(container_id, self._tool_server_port)
329
+
330
+ await self._register_agent_with_tool_server(api_url, agent_id, token)
331
+
332
+ return {
333
+ "workspace_id": container_id,
334
+ "api_url": api_url,
335
+ "auth_token": token,
336
+ "tool_server_port": self._tool_server_port,
337
+ "agent_id": agent_id,
338
+ }
339
+
340
+ async def _register_agent_with_tool_server(
341
+ self, api_url: str, agent_id: str, token: str
342
+ ) -> None:
343
+ import httpx
344
+
345
+ try:
346
+ async with httpx.AsyncClient(trust_env=False) as client:
347
+ response = await client.post(
348
+ f"{api_url}/register_agent",
349
+ params={"agent_id": agent_id},
350
+ headers={"Authorization": f"Bearer {token}"},
351
+ timeout=30,
352
+ )
353
+ response.raise_for_status()
354
+ logger.info(f"Registered agent {agent_id} with tool server")
355
+ except (httpx.RequestError, httpx.HTTPStatusError) as e:
356
+ logger.warning(f"Failed to register agent {agent_id}: {e}")
357
+
358
+ async def get_sandbox_url(self, container_id: str, port: int) -> str:
359
+ try:
360
+ container = self.client.containers.get(container_id)
361
+ container.reload()
362
+
363
+ host = self._resolve_docker_host()
364
+
365
+ except NotFound:
366
+ raise ValueError(f"Container {container_id} not found.") from None
367
+ except DockerException as e:
368
+ raise RuntimeError(f"Failed to get container URL for {container_id}: {e}") from e
369
+ else:
370
+ return f"http://{host}:{port}"
371
+
372
+ def _resolve_docker_host(self) -> str:
373
+ docker_host = os.getenv("DOCKER_HOST", "")
374
+ if not docker_host:
375
+ return "127.0.0.1"
376
+
377
+ from urllib.parse import urlparse
378
+
379
+ parsed = urlparse(docker_host)
380
+
381
+ if parsed.scheme in ("tcp", "http", "https") and parsed.hostname:
382
+ return parsed.hostname
383
+
384
+ return "127.0.0.1"
385
+
386
+ async def destroy_sandbox(self, container_id: str) -> None:
387
+ logger.info("Destroying scan container %s", container_id)
388
+ try:
389
+ container = self.client.containers.get(container_id)
390
+ container.stop()
391
+ container.remove()
392
+ logger.info("Successfully destroyed container %s", container_id)
393
+
394
+ self._scan_container = None
395
+ self._tool_server_port = None
396
+ self._tool_server_token = None
397
+
398
+ except NotFound:
399
+ logger.warning("Container %s not found for destruction.", container_id)
400
+ except DockerException as e:
401
+ logger.warning("Failed to destroy container %s: %s", container_id, e)