qyro 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.
- qyro/__init__.py +17 -0
- qyro/adapters/__init__.py +4 -0
- qyro/adapters/language_adapters/__init__.py +4 -0
- qyro/adapters/language_adapters/c/__init__.py +4 -0
- qyro/adapters/language_adapters/python/__init__.py +4 -0
- qyro/adapters/language_adapters/python/python_adapter.py +584 -0
- qyro/cli/__init__.py +8 -0
- qyro/cli/__main__.py +5 -0
- qyro/cli/cli.py +392 -0
- qyro/cli/interactive.py +297 -0
- qyro/common/__init__.py +37 -0
- qyro/common/animation.py +82 -0
- qyro/common/builder.py +434 -0
- qyro/common/compiler.py +895 -0
- qyro/common/config.py +93 -0
- qyro/common/constants.py +99 -0
- qyro/common/errors.py +176 -0
- qyro/common/frontend.py +74 -0
- qyro/common/health.py +358 -0
- qyro/common/kafka_manager.py +192 -0
- qyro/common/logging.py +149 -0
- qyro/common/memory.py +147 -0
- qyro/common/metrics.py +301 -0
- qyro/common/monitoring.py +468 -0
- qyro/common/parser.py +91 -0
- qyro/common/platform.py +609 -0
- qyro/common/redis_memory.py +1108 -0
- qyro/common/rpc.py +287 -0
- qyro/common/sandbox.py +191 -0
- qyro/common/schema_loader.py +33 -0
- qyro/common/secure_sandbox.py +490 -0
- qyro/common/toolchain_validator.py +617 -0
- qyro/common/type_generator.py +176 -0
- qyro/common/validation.py +401 -0
- qyro/common/validator.py +204 -0
- qyro/gateway/__init__.py +8 -0
- qyro/gateway/gateway.py +303 -0
- qyro/orchestrator/__init__.py +8 -0
- qyro/orchestrator/orchestrator.py +1223 -0
- qyro-2.0.0.dist-info/METADATA +244 -0
- qyro-2.0.0.dist-info/RECORD +45 -0
- qyro-2.0.0.dist-info/WHEEL +5 -0
- qyro-2.0.0.dist-info/entry_points.txt +2 -0
- qyro-2.0.0.dist-info/licenses/LICENSE +21 -0
- qyro-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nexus Secure Sandbox System
|
|
3
|
+
Production-grade containerized execution for untrusted code using Docker isolation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import docker
|
|
7
|
+
import tempfile
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, Optional, List
|
|
12
|
+
import time
|
|
13
|
+
import signal
|
|
14
|
+
import subprocess
|
|
15
|
+
import hashlib
|
|
16
|
+
import secrets
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SecureSandbox:
|
|
24
|
+
"""
|
|
25
|
+
Production-grade secure execution environment for untrusted code using Docker containers.
|
|
26
|
+
|
|
27
|
+
Features:
|
|
28
|
+
- Isolated container execution with minimal privileges
|
|
29
|
+
- Strict resource limits (CPU, memory, disk)
|
|
30
|
+
- Complete network isolation
|
|
31
|
+
- File system restrictions with read-only mounts
|
|
32
|
+
- Execution timeout enforcement
|
|
33
|
+
- Comprehensive security policies
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, max_memory: str = "128m", max_cpu: float = 0.5, timeout: int = 30):
|
|
37
|
+
try:
|
|
38
|
+
self.client = docker.from_env()
|
|
39
|
+
# Test connection
|
|
40
|
+
self.client.ping()
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise RuntimeError(f"Docker is required for sandboxing: {e}")
|
|
43
|
+
|
|
44
|
+
self.max_memory = max_memory
|
|
45
|
+
self.max_cpu = max_cpu
|
|
46
|
+
self.timeout = timeout
|
|
47
|
+
|
|
48
|
+
# Define supported languages and their security configurations
|
|
49
|
+
self.language_configs = {
|
|
50
|
+
'python': {
|
|
51
|
+
'image': 'python:3.11-alpine',
|
|
52
|
+
'entrypoint': ['python'],
|
|
53
|
+
'security_opts': ['no-new-privileges:true'],
|
|
54
|
+
'readonly_rootfs': True
|
|
55
|
+
},
|
|
56
|
+
'javascript': {
|
|
57
|
+
'image': 'node:18-alpine',
|
|
58
|
+
'entrypoint': ['node'],
|
|
59
|
+
'security_opts': ['no-new-privileges:true'],
|
|
60
|
+
'readonly_rootfs': True
|
|
61
|
+
},
|
|
62
|
+
'go': {
|
|
63
|
+
'image': 'golang:1.21-alpine',
|
|
64
|
+
'entrypoint': ['sh', '-c'],
|
|
65
|
+
'security_opts': ['no-new-privileges:true'],
|
|
66
|
+
'readonly_rootfs': True
|
|
67
|
+
},
|
|
68
|
+
'rust': {
|
|
69
|
+
'image': 'rust:1.75-alpine',
|
|
70
|
+
'entrypoint': ['sh', '-c'],
|
|
71
|
+
'security_opts': ['no-new-privileges:true'],
|
|
72
|
+
'readonly_rootfs': True
|
|
73
|
+
},
|
|
74
|
+
'java': {
|
|
75
|
+
'image': 'openjdk:17-jdk-alpine',
|
|
76
|
+
'entrypoint': ['sh', '-c'],
|
|
77
|
+
'security_opts': ['no-new-privileges:true'],
|
|
78
|
+
'readonly_rootfs': True
|
|
79
|
+
},
|
|
80
|
+
'c': {
|
|
81
|
+
'image': 'gcc:11.4-alpine',
|
|
82
|
+
'entrypoint': ['sh', '-c'],
|
|
83
|
+
'security_opts': ['no-new-privileges:true'],
|
|
84
|
+
'readonly_rootfs': True
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def _validate_code(self, code: str, language: str) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Validate code for dangerous patterns before execution.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
code: Source code to validate
|
|
94
|
+
language: Programming language
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if code is safe, False otherwise
|
|
98
|
+
"""
|
|
99
|
+
# Dangerous patterns to detect
|
|
100
|
+
dangerous_patterns = [
|
|
101
|
+
r'\b(import|from)\s+os\b', # OS module imports
|
|
102
|
+
r'\b(import|from)\s+subprocess\b', # Subprocess imports
|
|
103
|
+
r'\b(import|from)\s+sys\b', # Sys module (can affect interpreter)
|
|
104
|
+
r'\bexec\b', # Dynamic code execution
|
|
105
|
+
r'\beval\b', # Dynamic code execution
|
|
106
|
+
r'\bcompile\b', # Dynamic code compilation
|
|
107
|
+
r'\bglobals\b', # Access to global namespace
|
|
108
|
+
r'\blocals\b', # Access to local namespace
|
|
109
|
+
r'\bopen\b', # File operations
|
|
110
|
+
r'\bfile\b', # File operations
|
|
111
|
+
r'\binput\b', # Interactive input
|
|
112
|
+
r'\braw_input\b', # Interactive input (Python 2)
|
|
113
|
+
r'\bprint\b', # Output operations (we'll redirect this)
|
|
114
|
+
r'\bsystem\b', # System calls
|
|
115
|
+
r'\bpopen\b', # Process creation
|
|
116
|
+
r'\bcall\b', # Process creation
|
|
117
|
+
r'\bcheck_call\b', # Process creation
|
|
118
|
+
r'\bcheck_output\b', # Process creation
|
|
119
|
+
r'\bsocket\b', # Network operations
|
|
120
|
+
r'\bconnect\b', # Network operations
|
|
121
|
+
r'\bbind\b', # Network operations
|
|
122
|
+
r'\blisten\b', # Network operations
|
|
123
|
+
r'\baccept\b', # Network operations
|
|
124
|
+
r'\bgetaddrinfo\b', # Network operations
|
|
125
|
+
r'\bgethostbyname\b', # Network operations
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# Language-specific patterns
|
|
129
|
+
if language == 'python':
|
|
130
|
+
dangerous_patterns.extend([
|
|
131
|
+
r'\b__import__\b', # Dynamic imports
|
|
132
|
+
r'\bgetattr\b', # Attribute access
|
|
133
|
+
r'\bsetattr\b', # Attribute modification
|
|
134
|
+
r'\bdelattr\b', # Attribute deletion
|
|
135
|
+
r'\bhasattr\b', # Attribute checking
|
|
136
|
+
])
|
|
137
|
+
elif language == 'javascript':
|
|
138
|
+
dangerous_patterns.extend([
|
|
139
|
+
r'\brequire\(', # Module imports
|
|
140
|
+
r'\bprocess\.', # Process manipulation
|
|
141
|
+
r'\bchild_process\.', # Child process creation
|
|
142
|
+
r'\bfs\.', # File system operations
|
|
143
|
+
r'\brequire\(\s*["\']child_process["\']\s*\)', # Child process module
|
|
144
|
+
r'\brequire\(\s*["\']fs["\']\s*\)', # File system module
|
|
145
|
+
r'\brequire\(\s*["\']net["\']\s*\)', # Network module
|
|
146
|
+
r'\brequire\(\s*["\']dns["\']\s*\)', # DNS module
|
|
147
|
+
])
|
|
148
|
+
elif language == 'java':
|
|
149
|
+
dangerous_patterns.extend([
|
|
150
|
+
r'Runtime\.getRuntime\(\)',
|
|
151
|
+
r'System\.exec\(',
|
|
152
|
+
r'ProcessBuilder\(',
|
|
153
|
+
r'new\s+File\(',
|
|
154
|
+
r'FileReader\(',
|
|
155
|
+
r'FileWriter\(',
|
|
156
|
+
r'RandomAccessFile\(',
|
|
157
|
+
r'new\s+Socket\(',
|
|
158
|
+
r'new\s+ServerSocket\(',
|
|
159
|
+
r'URL\(',
|
|
160
|
+
r'URLConnection\(',
|
|
161
|
+
])
|
|
162
|
+
elif language == 'c':
|
|
163
|
+
dangerous_patterns.extend([
|
|
164
|
+
r'\bsystem\s*\(', # System calls
|
|
165
|
+
r'\bpopen\s*\(', # Process creation
|
|
166
|
+
r'\bexecve\s*\(', # Process execution
|
|
167
|
+
r'\bfork\s*\(', # Process creation
|
|
168
|
+
r'\bvfork\s*\(', # Process creation
|
|
169
|
+
r'\bopen\s*\(', # File operations
|
|
170
|
+
r'\bcreat\s*\(', # File creation
|
|
171
|
+
r'\baccess\s*\(', # File access checks
|
|
172
|
+
r'\bchmod\s*\(', # File permission changes
|
|
173
|
+
r'\bchown\s*\(', # File ownership changes
|
|
174
|
+
r'\bsocket\s*\(', # Socket creation
|
|
175
|
+
r'\bconnect\s*\(', # Socket connection
|
|
176
|
+
r'\bbind\s*\(', # Socket binding
|
|
177
|
+
r'\blisten\s*\(', # Socket listening
|
|
178
|
+
r'\baccept\s*\(', # Socket acceptance
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
# Check for dangerous patterns
|
|
182
|
+
for pattern in dangerous_patterns:
|
|
183
|
+
import re
|
|
184
|
+
if re.search(pattern, code, re.IGNORECASE):
|
|
185
|
+
logger.warning(f"Dangerous pattern detected in {language} code: {pattern}")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
def _sanitize_code(self, code: str, language: str) -> str:
|
|
191
|
+
"""
|
|
192
|
+
Sanitize code to remove or neutralize potentially harmful constructs.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
code: Source code to sanitize
|
|
196
|
+
language: Programming language
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Sanitized code
|
|
200
|
+
"""
|
|
201
|
+
# For Python, wrap code in a restricted environment
|
|
202
|
+
if language == 'python':
|
|
203
|
+
# Create a safe execution wrapper
|
|
204
|
+
wrapper = '''
|
|
205
|
+
import sys
|
|
206
|
+
import io
|
|
207
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
208
|
+
|
|
209
|
+
# Restrict built-ins
|
|
210
|
+
safe_builtins = {{
|
|
211
|
+
'abs', 'all', 'any', 'bool', 'chr', 'complex', 'dict', 'dir', 'divmod',
|
|
212
|
+
'enumerate', 'filter', 'float', 'format', 'frozenset', 'hash', 'hex',
|
|
213
|
+
'int', 'isinstance', 'issubclass', 'iter', 'len', 'list', 'map', 'max',
|
|
214
|
+
'min', 'next', 'object', 'oct', 'ord', 'pow', 'range', 'repr', 'reversed',
|
|
215
|
+
'round', 'set', 'slice', 'sorted', 'str', 'sum', 'super', 'tuple', 'type',
|
|
216
|
+
'zip', 'True', 'False', 'None'
|
|
217
|
+
}}
|
|
218
|
+
|
|
219
|
+
# Create restricted globals
|
|
220
|
+
restricted_globals = {{
|
|
221
|
+
'__builtins__': {{k: __builtins__[k] for k in safe_builtins if k in __builtins__}},
|
|
222
|
+
'__name__': '__main__',
|
|
223
|
+
'__doc__': None,
|
|
224
|
+
}}
|
|
225
|
+
|
|
226
|
+
# Capture output
|
|
227
|
+
output_buffer = io.StringIO()
|
|
228
|
+
error_buffer = io.StringIO()
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
|
|
232
|
+
exec("""{code}""", restricted_globals)
|
|
233
|
+
|
|
234
|
+
print("===OUTPUT===")
|
|
235
|
+
print(output_buffer.getvalue())
|
|
236
|
+
print("===ERRORS===")
|
|
237
|
+
print(error_buffer.getvalue())
|
|
238
|
+
except Exception as e:
|
|
239
|
+
print("===ERRORS===")
|
|
240
|
+
print(f"Execution error: {{e}}")
|
|
241
|
+
'''.format(code=code.replace('"', '\\"').replace('\n', '\\n'))
|
|
242
|
+
return wrapper
|
|
243
|
+
|
|
244
|
+
return code
|
|
245
|
+
|
|
246
|
+
@contextmanager
|
|
247
|
+
def _create_secure_temp_dir(self):
|
|
248
|
+
"""Create a temporary directory with secure permissions."""
|
|
249
|
+
temp_dir = tempfile.mkdtemp(prefix="nexus_sandbox_")
|
|
250
|
+
try:
|
|
251
|
+
# Set restrictive permissions
|
|
252
|
+
os.chmod(temp_dir, 0o700) # Only owner can read/write/execute
|
|
253
|
+
yield Path(temp_dir)
|
|
254
|
+
finally:
|
|
255
|
+
# Clean up
|
|
256
|
+
import shutil
|
|
257
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
258
|
+
|
|
259
|
+
def execute_untrusted_code(
|
|
260
|
+
self,
|
|
261
|
+
code: str,
|
|
262
|
+
language: str,
|
|
263
|
+
timeout: Optional[int] = None,
|
|
264
|
+
memory_limit: Optional[str] = None,
|
|
265
|
+
cpu_quota: Optional[float] = None
|
|
266
|
+
) -> Dict[str, Any]:
|
|
267
|
+
"""
|
|
268
|
+
Execute untrusted code in a secure Docker container.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
code: Source code to execute
|
|
272
|
+
language: Programming language ('python', 'javascript', 'go', etc.)
|
|
273
|
+
timeout: Execution timeout in seconds (overrides default)
|
|
274
|
+
memory_limit: Memory limit (e.g., '128m', '1g') (overrides default)
|
|
275
|
+
cpu_quota: CPU quota as fraction of one CPU (overrides default)
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Dictionary with execution results
|
|
279
|
+
"""
|
|
280
|
+
# Validate inputs
|
|
281
|
+
language = language.lower()
|
|
282
|
+
if language not in self.language_configs:
|
|
283
|
+
return {
|
|
284
|
+
'success': False,
|
|
285
|
+
'stdout': '',
|
|
286
|
+
'stderr': '',
|
|
287
|
+
'exit_code': -1,
|
|
288
|
+
'error': f"Unsupported language for sandbox: {language}"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Use provided values or defaults
|
|
292
|
+
timeout = timeout or self.timeout
|
|
293
|
+
memory_limit = memory_limit or self.max_memory
|
|
294
|
+
cpu_quota = cpu_quota or self.max_cpu
|
|
295
|
+
|
|
296
|
+
# Validate code for dangerous patterns
|
|
297
|
+
if not self._validate_code(code, language):
|
|
298
|
+
return {
|
|
299
|
+
'success': False,
|
|
300
|
+
'stdout': '',
|
|
301
|
+
'stderr': '',
|
|
302
|
+
'exit_code': -1,
|
|
303
|
+
'error': 'Code contains dangerous patterns and was rejected'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Sanitize code
|
|
307
|
+
sanitized_code = self._sanitize_code(code, language)
|
|
308
|
+
|
|
309
|
+
# Create temporary directory for code
|
|
310
|
+
with self._create_secure_temp_dir() as temp_dir:
|
|
311
|
+
temp_path = Path(temp_dir)
|
|
312
|
+
|
|
313
|
+
# Write code to temporary file based on language
|
|
314
|
+
file_extensions = {
|
|
315
|
+
'python': '.py',
|
|
316
|
+
'javascript': '.js',
|
|
317
|
+
'go': '.go',
|
|
318
|
+
'rust': '.rs',
|
|
319
|
+
'java': '.java',
|
|
320
|
+
'c': '.c',
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
ext = file_extensions[language]
|
|
324
|
+
code_file = temp_path / f"code{ext}"
|
|
325
|
+
|
|
326
|
+
# Write the sanitized code
|
|
327
|
+
with open(code_file, 'w', encoding='utf-8') as f:
|
|
328
|
+
f.write(sanitized_code)
|
|
329
|
+
|
|
330
|
+
# Determine execution command based on language
|
|
331
|
+
config = self.language_configs[language]
|
|
332
|
+
image = config['image']
|
|
333
|
+
|
|
334
|
+
# Create a unique container name to avoid conflicts
|
|
335
|
+
container_name = f"nexus_sandbox_{secrets.token_hex(8)}"
|
|
336
|
+
|
|
337
|
+
# Calculate CPU period and quota
|
|
338
|
+
cpu_period = 100000 # 100ms in microseconds
|
|
339
|
+
cpu_quota = int(cpu_quota * cpu_period) # Convert fraction to quota
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
# Run container with security restrictions
|
|
343
|
+
container = self.client.containers.run(
|
|
344
|
+
image=image,
|
|
345
|
+
command=[code_file.name] if language != 'go' else ['sh', '-c', f'cd /code && go run {code_file.name}'],
|
|
346
|
+
volumes={str(temp_path): {'bind': '/code', 'mode': 'ro'}}, # Read-only mount
|
|
347
|
+
network_mode='none', # Complete network isolation
|
|
348
|
+
mem_limit=memory_limit,
|
|
349
|
+
cpu_period=cpu_period,
|
|
350
|
+
cpu_quota=cpu_quota,
|
|
351
|
+
security_opt=config['security_opts'],
|
|
352
|
+
readonly_rootfs=config['readonly_rootfs'],
|
|
353
|
+
environment={
|
|
354
|
+
'HOME': '/tmp',
|
|
355
|
+
'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
|
356
|
+
'PYTHONUNBUFFERED': '1', # For Python
|
|
357
|
+
},
|
|
358
|
+
working_dir='/code',
|
|
359
|
+
remove=True, # Auto-remove when done
|
|
360
|
+
stdout=True,
|
|
361
|
+
stderr=True,
|
|
362
|
+
detach=False,
|
|
363
|
+
timeout=timeout,
|
|
364
|
+
user='nobody:nobody' if self._container_supports_user(image) else None, # Run as unprivileged user
|
|
365
|
+
cap_drop=['ALL'], # Drop all capabilities
|
|
366
|
+
privileged=False, # Never run privileged
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Parse output
|
|
370
|
+
stdout_raw = container[0] if container[0] else b""
|
|
371
|
+
stderr_raw = container[1] if container[1] else b""
|
|
372
|
+
|
|
373
|
+
stdout = stdout_raw.decode('utf-8', errors='replace') if stdout_raw else ""
|
|
374
|
+
stderr = stderr_raw.decode('utf-8', errors='replace') if stderr_raw else ""
|
|
375
|
+
|
|
376
|
+
# Extract actual output if wrapped (for Python)
|
|
377
|
+
if language == 'python':
|
|
378
|
+
# Extract output from our wrapper
|
|
379
|
+
if '===OUTPUT===' in stdout:
|
|
380
|
+
output_part = stdout.split('===OUTPUT===')[1]
|
|
381
|
+
if '===ERRORS===' in output_part:
|
|
382
|
+
actual_output = output_part.split('===ERRORS===')[0].strip()
|
|
383
|
+
else:
|
|
384
|
+
actual_output = output_part.strip()
|
|
385
|
+
stdout = actual_output
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
'success': True,
|
|
389
|
+
'stdout': stdout,
|
|
390
|
+
'stderr': stderr,
|
|
391
|
+
'exit_code': 0 # Docker run returns combined output
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
except docker.errors.ContainerError as e:
|
|
395
|
+
return {
|
|
396
|
+
'success': False,
|
|
397
|
+
'stdout': e.stdout.decode('utf-8', errors='replace') if e.stdout else "",
|
|
398
|
+
'stderr': e.stderr.decode('utf-8', errors='replace') if e.stderr else "",
|
|
399
|
+
'exit_code': e.exit_code,
|
|
400
|
+
'error': f"Container execution failed: {e}"
|
|
401
|
+
}
|
|
402
|
+
except docker.errors.ImageNotFound:
|
|
403
|
+
return {
|
|
404
|
+
'success': False,
|
|
405
|
+
'stdout': '',
|
|
406
|
+
'stderr': '',
|
|
407
|
+
'exit_code': -1,
|
|
408
|
+
'error': f"Docker image not found: {image}"
|
|
409
|
+
}
|
|
410
|
+
except docker.errors.APIError as e:
|
|
411
|
+
return {
|
|
412
|
+
'success': False,
|
|
413
|
+
'stdout': '',
|
|
414
|
+
'stderr': '',
|
|
415
|
+
'exit_code': -1,
|
|
416
|
+
'error': f"Docker API error: {e}"
|
|
417
|
+
}
|
|
418
|
+
except Exception as e:
|
|
419
|
+
return {
|
|
420
|
+
'success': False,
|
|
421
|
+
'stdout': '',
|
|
422
|
+
'stderr': '',
|
|
423
|
+
'exit_code': -1,
|
|
424
|
+
'error': str(e)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
def _container_supports_user(self, image: str) -> bool:
|
|
428
|
+
"""
|
|
429
|
+
Check if the container image supports running as a specific user.
|
|
430
|
+
"""
|
|
431
|
+
try:
|
|
432
|
+
# Try to inspect the image to see if it has a default user
|
|
433
|
+
img = self.client.images.get(image)
|
|
434
|
+
# Check if image has USER instruction
|
|
435
|
+
return True # For simplicity, assume most images support user switching
|
|
436
|
+
except:
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
def is_available(self) -> bool:
|
|
440
|
+
"""Check if Docker is available for sandboxing."""
|
|
441
|
+
try:
|
|
442
|
+
self.client.ping()
|
|
443
|
+
return True
|
|
444
|
+
except:
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
def get_supported_languages(self) -> List[str]:
|
|
448
|
+
"""Get list of supported languages for sandboxing."""
|
|
449
|
+
return list(self.language_configs.keys())
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# Singleton instance
|
|
453
|
+
_secure_sandbox = None
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def get_secure_sandbox(max_memory: str = "128m", max_cpu: float = 0.5, timeout: int = 30) -> Optional[SecureSandbox]:
|
|
457
|
+
"""Get the global secure sandbox instance."""
|
|
458
|
+
global _secure_sandbox
|
|
459
|
+
if _secure_sandbox is None:
|
|
460
|
+
try:
|
|
461
|
+
_secure_sandbox = SecureSandbox(max_memory, max_cpu, timeout)
|
|
462
|
+
except RuntimeError:
|
|
463
|
+
# Docker not available
|
|
464
|
+
return None
|
|
465
|
+
return _secure_sandbox
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def execute_secure_code(code: str, language: str, **kwargs) -> Dict[str, Any]:
|
|
469
|
+
"""
|
|
470
|
+
Convenience function to execute code securely.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
code: Source code to execute
|
|
474
|
+
language: Programming language
|
|
475
|
+
**kwargs: Additional options (timeout, memory_limit, cpu_quota)
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Execution results
|
|
479
|
+
"""
|
|
480
|
+
sandbox = get_secure_sandbox()
|
|
481
|
+
if not sandbox:
|
|
482
|
+
return {
|
|
483
|
+
'success': False,
|
|
484
|
+
'stdout': '',
|
|
485
|
+
'stderr': '',
|
|
486
|
+
'exit_code': -1,
|
|
487
|
+
'error': 'Secure sandbox not available (Docker required)'
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return sandbox.execute_untrusted_code(code, language, **kwargs)
|