aiptx 2.0.7__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.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +46 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/exploit_agent.py +688 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +957 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +2933 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +341 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +194 -0
- aipt_v2/intelligence/adaptation.py +474 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/correlation.py +536 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/knowledge_graph.py +590 -0
- aipt_v2/intelligence/learning.py +626 -0
- aipt_v2/intelligence/llm_analyzer.py +502 -0
- aipt_v2/intelligence/llm_tool_selector.py +518 -0
- aipt_v2/intelligence/payload_generator.py +562 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interactive_shell.py +559 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/local_tool_installer.py +1467 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2427 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +53 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/runtime/vps.py +830 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/setup_wizard.py +941 -0
- aipt_v2/skills/__init__.py +80 -0
- aipt_v2/skills/agents/__init__.py +14 -0
- aipt_v2/skills/agents/api_tester.py +706 -0
- aipt_v2/skills/agents/base.py +477 -0
- aipt_v2/skills/agents/code_review.py +459 -0
- aipt_v2/skills/agents/security_agent.py +336 -0
- aipt_v2/skills/agents/web_pentest.py +818 -0
- aipt_v2/skills/prompts/__init__.py +647 -0
- aipt_v2/system_detector.py +539 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +202 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aipt_v2/verify_install.py +793 -0
- aiptx-2.0.7.dist-info/METADATA +345 -0
- aiptx-2.0.7.dist-info/RECORD +187 -0
- aiptx-2.0.7.dist-info/WHEEL +5 -0
- aiptx-2.0.7.dist-info/entry_points.txt +7 -0
- aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Runtime Module - Docker sandbox, VPS, and local execution environments
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aipt_v2.runtime.base import AbstractRuntime, SandboxInfo
|
|
6
|
+
|
|
7
|
+
# Lazy import for DockerRuntime and VPSRuntime to avoid dependency issues
|
|
8
|
+
_runtime = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name):
|
|
12
|
+
"""Lazy import for optional dependencies"""
|
|
13
|
+
if name == "DockerRuntime":
|
|
14
|
+
from aipt_v2.runtime.docker import DockerRuntime
|
|
15
|
+
return DockerRuntime
|
|
16
|
+
elif name == "VPSRuntime":
|
|
17
|
+
from aipt_v2.runtime.vps import VPSRuntime
|
|
18
|
+
return VPSRuntime
|
|
19
|
+
elif name == "LocalRuntime":
|
|
20
|
+
from aipt_v2.runtime.local import LocalRuntime
|
|
21
|
+
return LocalRuntime
|
|
22
|
+
elif name == "generate_vps_setup_script":
|
|
23
|
+
from aipt_v2.runtime.vps import generate_vps_setup_script
|
|
24
|
+
return generate_vps_setup_script
|
|
25
|
+
raise AttributeError(f"module 'aipt_v2.runtime' has no attribute '{name}'")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_runtime():
|
|
29
|
+
"""Get or create the global runtime instance"""
|
|
30
|
+
global _runtime
|
|
31
|
+
if _runtime is None:
|
|
32
|
+
from aipt_v2.runtime.docker import DockerRuntime
|
|
33
|
+
_runtime = DockerRuntime()
|
|
34
|
+
return _runtime
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def set_runtime(runtime: AbstractRuntime) -> None:
|
|
38
|
+
"""Set the global runtime instance"""
|
|
39
|
+
global _runtime
|
|
40
|
+
_runtime = runtime
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Alias for backwards compatibility
|
|
44
|
+
BaseRuntime = AbstractRuntime
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"AbstractRuntime",
|
|
49
|
+
"BaseRuntime",
|
|
50
|
+
"SandboxInfo",
|
|
51
|
+
"get_runtime",
|
|
52
|
+
"set_runtime",
|
|
53
|
+
]
|
aipt_v2/runtime/base.py
ADDED
|
@@ -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)
|