From 67f911d9fd4bbf47a0385f87442bc9056b168ef2 Mon Sep 17 00:00:00 2001 From: yatchiya Date: Mon, 9 Sep 2024 16:23:40 +0200 Subject: [PATCH] v1 - sandbox node --- .../example/nodejs-project/Dockerfile | 5 + .../example/nodejs-project/docker-compose.yml | 8 + .../example/nodejs-project/index.js | 9 ++ .../example/nodejs-project/output/output.txt | 1 + .../example/nodejs-project/package.json | 5 + .../example/python-flask/Dockerfile | 7 + .../example/python-flask/app.py | 12 ++ .../example/python-flask/docker-compose.yml | 10 ++ .../example/python-flask/requirements.txt | 2 + .../example/python-project/Dockerfile | 9 ++ .../example/python-project/app.py | 10 ++ .../example/python-project/docker-compose.yml | 10 ++ .../example/python-project/output/output.txt | 1 + .../example/python-project/requirements.txt | 0 packages/sandbox-docker/package.json | 55 +++++++ packages/sandbox-docker/src/core/enclave.ts | 110 +++++++++++++ .../sandbox-docker/src/core/file-manager.ts | 38 +++++ .../src/core/package-manager.ts | 135 ++++++++++++++++ .../sandbox-docker/src/core/virtual-fs.ts | 39 +++++ .../src/security/docker-sandbox.ts | 152 ++++++++++++++++++ .../src/security/resource-limiter.ts | 73 +++++++++ packages/sandbox-docker/src/test.ts | 51 ++++++ packages/sandbox-docker/src/types.ts | 47 ++++++ .../sandbox-docker/src/utils/error-handler.ts | 21 +++ packages/sandbox-docker/src/utils/logger.ts | 41 +++++ packages/sandbox-docker/tsconfig.json | 14 ++ 26 files changed, 865 insertions(+) create mode 100644 packages/sandbox-docker/example/nodejs-project/Dockerfile create mode 100644 packages/sandbox-docker/example/nodejs-project/docker-compose.yml create mode 100644 packages/sandbox-docker/example/nodejs-project/index.js create mode 100644 packages/sandbox-docker/example/nodejs-project/output/output.txt create mode 100644 packages/sandbox-docker/example/nodejs-project/package.json create mode 100644 packages/sandbox-docker/example/python-flask/Dockerfile create mode 100644 packages/sandbox-docker/example/python-flask/app.py create mode 100644 packages/sandbox-docker/example/python-flask/docker-compose.yml create mode 100644 packages/sandbox-docker/example/python-flask/requirements.txt create mode 100644 packages/sandbox-docker/example/python-project/Dockerfile create mode 100644 packages/sandbox-docker/example/python-project/app.py create mode 100644 packages/sandbox-docker/example/python-project/docker-compose.yml create mode 100644 packages/sandbox-docker/example/python-project/output/output.txt create mode 100644 packages/sandbox-docker/example/python-project/requirements.txt create mode 100644 packages/sandbox-docker/package.json create mode 100644 packages/sandbox-docker/src/core/enclave.ts create mode 100644 packages/sandbox-docker/src/core/file-manager.ts create mode 100644 packages/sandbox-docker/src/core/package-manager.ts create mode 100644 packages/sandbox-docker/src/core/virtual-fs.ts create mode 100644 packages/sandbox-docker/src/security/docker-sandbox.ts create mode 100644 packages/sandbox-docker/src/security/resource-limiter.ts create mode 100644 packages/sandbox-docker/src/test.ts create mode 100644 packages/sandbox-docker/src/types.ts create mode 100644 packages/sandbox-docker/src/utils/error-handler.ts create mode 100644 packages/sandbox-docker/src/utils/logger.ts create mode 100644 packages/sandbox-docker/tsconfig.json diff --git a/packages/sandbox-docker/example/nodejs-project/Dockerfile b/packages/sandbox-docker/example/nodejs-project/Dockerfile new file mode 100644 index 0000000..d17c153 --- /dev/null +++ b/packages/sandbox-docker/example/nodejs-project/Dockerfile @@ -0,0 +1,5 @@ +FROM node:20 +WORKDIR /app +COPY . . +RUN npm install +CMD ["node", "index.js"] \ No newline at end of file diff --git a/packages/sandbox-docker/example/nodejs-project/docker-compose.yml b/packages/sandbox-docker/example/nodejs-project/docker-compose.yml new file mode 100644 index 0000000..ee0819f --- /dev/null +++ b/packages/sandbox-docker/example/nodejs-project/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + app: + build: . + environment: + - INPUT_VALUE=${INPUT_VALUE} + volumes: + - ./output:/app/output \ No newline at end of file diff --git a/packages/sandbox-docker/example/nodejs-project/index.js b/packages/sandbox-docker/example/nodejs-project/index.js new file mode 100644 index 0000000..afe56dc --- /dev/null +++ b/packages/sandbox-docker/example/nodejs-project/index.js @@ -0,0 +1,9 @@ +const fs = require('fs'); + +const inputValue = process.env.INPUT_VALUE || 'Default Value'; +console.log(`Received input: ${inputValue}`); + +const output = `Processed: ${inputValue.toUpperCase()}`; +fs.writeFileSync('/app/output/output.txt', output); + +console.log('Processing complete'); \ No newline at end of file diff --git a/packages/sandbox-docker/example/nodejs-project/output/output.txt b/packages/sandbox-docker/example/nodejs-project/output/output.txt new file mode 100644 index 0000000..340569a --- /dev/null +++ b/packages/sandbox-docker/example/nodejs-project/output/output.txt @@ -0,0 +1 @@ +Processed: HELLO, FROM DOCKER! \ No newline at end of file diff --git a/packages/sandbox-docker/example/nodejs-project/package.json b/packages/sandbox-docker/example/nodejs-project/package.json new file mode 100644 index 0000000..f6950f8 --- /dev/null +++ b/packages/sandbox-docker/example/nodejs-project/package.json @@ -0,0 +1,5 @@ +{ + "name": "your-project-name", + "version": "1.0.0", + "dependencies": {} + } \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-flask/Dockerfile b/packages/sandbox-docker/example/python-flask/Dockerfile new file mode 100644 index 0000000..5ddeb7a --- /dev/null +++ b/packages/sandbox-docker/example/python-flask/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.9-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 8081 +CMD ["python", "app.py"] \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-flask/app.py b/packages/sandbox-docker/example/python-flask/app.py new file mode 100644 index 0000000..c88d544 --- /dev/null +++ b/packages/sandbox-docker/example/python-flask/app.py @@ -0,0 +1,12 @@ +from flask import Flask +import os + +app = Flask(__name__) + +@app.route('/') +def hello(): + input_value = os.environ.get('INPUT_VALUE', 'Default Value') + return f"Hello docker file from Flask! Input: {input_value}" + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8081) \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-flask/docker-compose.yml b/packages/sandbox-docker/example/python-flask/docker-compose.yml new file mode 100644 index 0000000..e7e6189 --- /dev/null +++ b/packages/sandbox-docker/example/python-flask/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3' +services: + web: + build: . + ports: + - "8081:8081" + environment: + - INPUT_VALUE=${INPUT_VALUE} + volumes: + - ./output:/app/output \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-flask/requirements.txt b/packages/sandbox-docker/example/python-flask/requirements.txt new file mode 100644 index 0000000..e0f2ec9 --- /dev/null +++ b/packages/sandbox-docker/example/python-flask/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.0.1 +Werkzeug==2.0.1 \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-project/Dockerfile b/packages/sandbox-docker/example/python-project/Dockerfile new file mode 100644 index 0000000..3c45d23 --- /dev/null +++ b/packages/sandbox-docker/example/python-project/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +RUN mkdir -p /app/output && chown -R 1000:1000 /app/output +RUN useradd -m -u 1000 appuser +USER appuser +CMD ["python", "app.py"] \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-project/app.py b/packages/sandbox-docker/example/python-project/app.py new file mode 100644 index 0000000..884c44c --- /dev/null +++ b/packages/sandbox-docker/example/python-project/app.py @@ -0,0 +1,10 @@ +import os + +input_value = os.environ.get('INPUT_VALUE', 'default') +result = f"Processed input: {input_value.upper()}" + +os.makedirs('/app/output', exist_ok=True) +with open('/app/output/output.txt', 'w') as f: + f.write(result) + +print(result) \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-project/docker-compose.yml b/packages/sandbox-docker/example/python-project/docker-compose.yml new file mode 100644 index 0000000..8d79bb3 --- /dev/null +++ b/packages/sandbox-docker/example/python-project/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3' +services: + web: + build: . + ports: + - "5001:5000" + environment: + - INPUT_VALUE=${INPUT_VALUE} + volumes: + - ./output:/app/output \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-project/output/output.txt b/packages/sandbox-docker/example/python-project/output/output.txt new file mode 100644 index 0000000..3859fe6 --- /dev/null +++ b/packages/sandbox-docker/example/python-project/output/output.txt @@ -0,0 +1 @@ +Processed input: HELLO DUDE ! \ No newline at end of file diff --git a/packages/sandbox-docker/example/python-project/requirements.txt b/packages/sandbox-docker/example/python-project/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/packages/sandbox-docker/package.json b/packages/sandbox-docker/package.json new file mode 100644 index 0000000..b7019d5 --- /dev/null +++ b/packages/sandbox-docker/package.json @@ -0,0 +1,55 @@ +{ + "name": "secure-nodejs-code-enclave", + "version": "1.0.0", + "description": "A secure execution environment for untrusted JavaScript/TypeScript code", + "main": "dist/test.js", + "scripts": { + "build": "tsc", + "start": "node dist/test.js", + "start_example": "node dist/test.js", + "test": "jest --detectOpenHandles", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "start:server": "node dist/test.js --server" + }, + "dependencies": { + "@types/js-yaml": "^4.0.9", + "@vitalets/google-translate-api": "^9.2.0", + "axios": "^1.7.5", + "body-parser": "^1.20.2", + "dotenv": "^16.4.5", + "escodegen": "^2.1.0", + "esprima": "^4.0.1", + "express": "^4.19.2", + "js-yaml": "^4.1.0", + "natural": "^8.0.1", + "openai": "^4.56.1", + "qllm-lib": "^3.2.1", + "uuid": "^8.3.2", + "vm2": "^3.9.11", + "yaml": "^2.5.0", + "yargs": "^17.5.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/body-parser": "^1.19.5", + "@types/escodegen": "^0.0.10", + "@types/esprima": "^4.0.6", + "@types/express": "^4.17.21", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.106", + "@types/uuid": "^8.3.4", + "@types/yargs": "^17.0.33", + "jest": "^27.5.1", + "ts-jest": "^27.1.5", + "typescript": "^4.9.5" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "testMatch": [ + "**/__tests__/**/*.ts", + "**/?(*.)+(spec|test).ts" + ] + } +} diff --git a/packages/sandbox-docker/src/core/enclave.ts b/packages/sandbox-docker/src/core/enclave.ts new file mode 100644 index 0000000..348a88b --- /dev/null +++ b/packages/sandbox-docker/src/core/enclave.ts @@ -0,0 +1,110 @@ +import { v4 as uuidv4 } from 'uuid'; +import { PackageManager } from './package-manager'; +import { FileManager } from './file-manager'; +import { VirtualFileSystem } from './virtual-fs'; +import { ResourceLimiter } from '../security/resource-limiter'; +import { Logger } from '../utils/logger'; +import { ErrorHandler } from '../utils/error-handler'; +import { EnclaveConfig, FileInput, EnclaveStatus } from '../types'; +import { DockerSandbox } from '../security/docker-sandbox'; + +/** + * Enclave class represents a secure execution environment for Node.js projects. + */ +export class Enclave { + private readonly id: string; + private readonly tempDir: string; + private readonly packageManager: PackageManager; + private readonly fileManager: FileManager; + private readonly virtualFs: VirtualFileSystem; + private readonly resourceLimiter: ResourceLimiter; + private readonly logger: Logger; + private readonly errorHandler: ErrorHandler; + private readonly dockerSandbox: DockerSandbox; + private status: EnclaveStatus; + + /** + * Creates a new Enclave instance. + * @param config - Configuration options for the enclave. + */ + constructor(config: EnclaveConfig) { + this.id = uuidv4(); + this.tempDir = `/tmp/secure-nodejs-enclave-${this.id}`; + this.packageManager = new PackageManager(this.tempDir, config.cacheDir); + this.fileManager = new FileManager(this.tempDir); + this.virtualFs = new VirtualFileSystem(this.tempDir); + this.resourceLimiter = new ResourceLimiter(config.resourceLimits); + this.logger = new Logger(config.loggerConfig); + this.errorHandler = new ErrorHandler(); + this.dockerSandbox = new DockerSandbox(config.dockerConfig); + this.status = 'initialized'; + } + + /** + * Executes a Docker container with the specified project and parameters. + * @param projectPath - Path to the project to be executed. + * @param params - Key-value pairs of parameters for the execution. + * @returns An object containing the output and logs from the execution. + * @throws Error if the execution fails. + */ + async executeDocker(projectPath: string, params: Record): Promise<{ output: string; logs: string }> { + this.status = 'executing'; + this.logger.info(`Starting Docker execution for project: ${projectPath}`); + this.logger.debug(`Params: ${JSON.stringify(params)}`); + + try { + const result = await this.dockerSandbox.run(projectPath, params); + this.logger.info('Docker execution completed successfully'); + this.status = 'completed'; + return result; + } catch (error) { + this.status = 'error'; + this.logger.error('Docker execution failed', error as Error); + throw this.errorHandler.handleError(error as Error); + } + } + + /** + * Prepares the enclave by writing files and installing packages. + * @param files - Array of file inputs to be written. + * @param packages - Array of package names to be installed. + * @throws Error if preparation fails. + */ + async prepare(files: FileInput[], packages: string[]): Promise { + this.status = 'preparing'; + try { + await Promise.all([ + this.fileManager.writeFiles(files) + ]); + this.status = 'prepared'; + } catch (error) { + this.status = 'error'; + throw this.errorHandler.handleError(error as Error); + } + } + + /** + * Cleans up resources used by the enclave. + */ + async cleanup(): Promise { + this.status = 'cleaning'; + try { + await Promise.all([ + this.fileManager.cleanup(), + this.packageManager.cleanup() + ]); + this.status = 'cleaned'; + } catch (error) { + this.status = 'error'; + this.logger.error('Cleanup failed', error as Error); + } + } + + /** + * Gets the current status of the enclave. + * @returns The current status of the enclave. + */ + getStatus(): EnclaveStatus { + return this.status; + } +} \ No newline at end of file diff --git a/packages/sandbox-docker/src/core/file-manager.ts b/packages/sandbox-docker/src/core/file-manager.ts new file mode 100644 index 0000000..757d208 --- /dev/null +++ b/packages/sandbox-docker/src/core/file-manager.ts @@ -0,0 +1,38 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { FileInput } from '../types'; + +export class FileManager { + constructor(private tempDir: string) {} + + async writeFiles(files: FileInput[]): Promise { + for (const file of files) { + if (!this.validateFileName(file.name)) { + throw new Error(`Invalid file name: ${file.name}`); + } + const filePath = path.join(this.tempDir, file.name); + await fs.writeFile(filePath, file.content); + } + } + + async readFile(fileName: string): Promise { + if (!this.validateFileName(fileName)) { + throw new Error(`Invalid file name: ${fileName}`); + } + const filePath = path.join(this.tempDir, fileName); + return fs.readFile(filePath, 'utf-8'); + } + + validateFileName(fileName: string): boolean { + const normalizedPath = path.normalize(fileName); + return !normalizedPath.startsWith('..') && !path.isAbsolute(normalizedPath); + } + + async listFiles(): Promise { + return fs.readdir(this.tempDir); + } + + async cleanup(): Promise { + await fs.rm(this.tempDir, { recursive: true, force: true }); + } +} diff --git a/packages/sandbox-docker/src/core/package-manager.ts b/packages/sandbox-docker/src/core/package-manager.ts new file mode 100644 index 0000000..bd1b222 --- /dev/null +++ b/packages/sandbox-docker/src/core/package-manager.ts @@ -0,0 +1,135 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; + +type PackageContent = Record; + +/** + * PackageManager class for managing package files and caching + */ +export class PackageManager { + /** + * Creates a new PackageManager instance + * @param {string} packagesDir - Directory containing package files + * @param {string} cacheDir - Directory for caching package files + */ + constructor(private readonly packagesDir: string, private readonly cacheDir: string) {} + + /** + * Reads and returns a list of package files + * @returns {Promise} Array of package file names + */ + async readPackages(): Promise { + try { + const files = await fs.readdir(this.packagesDir); + return files.filter(file => file.endsWith('.yml') || file.endsWith('.yaml') || file.endsWith('.json')); + } catch (error) { + console.error('Failed to read packages:', error); + return []; + } + } + + /** + * Retrieves the content of a specific package + * @param {string} packageName - Name of the package file + * @returns {Promise} Content of the package + */ + async getPackageContent(packageName: string): Promise { + try { + const filePath = path.join(this.packagesDir, packageName); + const content = await fs.readFile(filePath, 'utf-8'); + return this.parseFileContent(packageName, content); + } catch (error) { + console.error(`Failed to read package ${packageName}:`, error); + throw error; + } + } + + /** + * Caches a package's content + * @param {string} packageName - Name of the package file + * @param {PackageContent} content - Content to be cached + */ + async cachePackage(packageName: string, content: PackageContent): Promise { + try { + const cachePath = path.join(this.cacheDir, packageName); + const fileContent = this.stringifyContent(packageName, content); + await fs.writeFile(cachePath, fileContent); + } catch (error) { + console.error(`Failed to cache package ${packageName}:`, error); + throw error; + } + } + + /** + * Retrieves a list of cached package files + * @returns {Promise} Array of cached package file names + */ + async getCachedPackages(): Promise { + try { + const files = await fs.readdir(this.cacheDir); + return files.filter(file => file.endsWith('.yml') || file.endsWith('.yaml') || file.endsWith('.json')); + } catch (error) { + console.error('Failed to read cached packages:', error); + return []; + } + } + + /** + * Retrieves the content of a specific cached package + * @param {string} packageName - Name of the cached package file + * @returns {Promise} Content of the cached package + */ + async getCachedPackageContent(packageName: string): Promise { + try { + const filePath = path.join(this.cacheDir, packageName); + const content = await fs.readFile(filePath, 'utf-8'); + return this.parseFileContent(packageName, content); + } catch (error) { + console.error(`Failed to read cached package ${packageName}:`, error); + throw error; + } + } + + /** + * Cleans up the cache directory + */ + async cleanup(): Promise { + try { + await fs.rm(this.cacheDir, { recursive: true, force: true }); + await fs.mkdir(this.cacheDir, { recursive: true }); + } catch (error) { + console.error('Error during PackageManager cleanup:', error); + } + } + + /** + * Parses the content of a file based on its extension + * @param {string} fileName - Name of the file + * @param {string} content - Content of the file + * @returns {PackageContent} Parsed content + */ + private parseFileContent(fileName: string, content: string): PackageContent { + if (fileName.endsWith('.json')) { + return JSON.parse(content); + } else if (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) { + return yaml.load(content) as PackageContent; + } + throw new Error(`Unsupported file type: ${fileName}`); + } + + /** + * Stringifies content based on the file extension + * @param {string} fileName - Name of the file + * @param {PackageContent} content - Content to stringify + * @returns {string} Stringified content + */ + private stringifyContent(fileName: string, content: PackageContent): string { + if (fileName.endsWith('.json')) { + return JSON.stringify(content, null, 2); + } else if (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) { + return yaml.dump(content); + } + throw new Error(`Unsupported file type: ${fileName}`); + } +} diff --git a/packages/sandbox-docker/src/core/virtual-fs.ts b/packages/sandbox-docker/src/core/virtual-fs.ts new file mode 100644 index 0000000..3a0a379 --- /dev/null +++ b/packages/sandbox-docker/src/core/virtual-fs.ts @@ -0,0 +1,39 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; + +export class VirtualFileSystem { + constructor(private rootDir: string) {} + + async readFile(filePath: string): Promise { + const fullPath = this.resolvePath(filePath); + return fs.readFile(fullPath); + } + + async writeFile(filePath: string, data: Buffer): Promise { + const fullPath = this.resolvePath(filePath); + await fs.writeFile(fullPath, data); + } + + async exists(filePath: string): Promise { + const fullPath = this.resolvePath(filePath); + try { + await fs.access(fullPath); + return true; + } catch { + return false; + } + } + + async mkdir(dirPath: string): Promise { + const fullPath = this.resolvePath(dirPath); + await fs.mkdir(fullPath, { recursive: true }); + } + + private resolvePath(filePath: string): string { + const normalizedPath = path.normalize(filePath); + if (normalizedPath.startsWith('..') || path.isAbsolute(normalizedPath)) { + throw new Error('Access denied: Attempting to access outside of virtual file system'); + } + return path.join(this.rootDir, normalizedPath); + } +} diff --git a/packages/sandbox-docker/src/security/docker-sandbox.ts b/packages/sandbox-docker/src/security/docker-sandbox.ts new file mode 100644 index 0000000..ae4063b --- /dev/null +++ b/packages/sandbox-docker/src/security/docker-sandbox.ts @@ -0,0 +1,152 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { DockerConfig } from '../types'; +import { Logger } from '../utils/logger'; + +const execAsync = promisify(exec); + +/** + * DockerSandbox class manages Docker container operations for project execution. + */ +export class DockerSandbox { + private readonly logger: Logger; + + /** + * Creates a new DockerSandbox instance. + * @param config - Docker configuration options. + */ + constructor(private readonly config: DockerConfig) { + this.logger = new Logger({ debugMode: true }); + } + + /** + * Runs a Docker project with specified parameters. + * @param projectPath - Path to the Docker project. + * @param params - Key-value pairs of environment variables. + * @returns An object containing the output and logs from the execution. + * @throws Error if the project is invalid or execution fails. + */ + async run(projectPath: string, params: Record): Promise<{ output: string; logs: string }> { + this.logger.info(`Validating Docker project: ${projectPath}`); + await this.validateProject(projectPath); + + this.logger.info('Updating .env file with parameters'); + await this.updateEnvFile(projectPath, params); + + this.logger.info('Building and running Docker container'); + const logs = await this.buildAndRun(projectPath); + + const isRunning = await this.checkContainerStatus(projectPath); + this.logger.info(isRunning ? 'Container is still running. You can access the Flask app.' : 'Container has stopped.'); + + this.logger.info('Retrieving output'); + const output = await this.getOutput(projectPath); + + return { output, logs }; + } + + /** + * Builds and runs the Docker container. + * @param projectPath - Path to the Docker project. + * @returns Logs from the Docker execution. + * @throws Error if Docker execution fails. + */ + private async buildAndRun(projectPath: string): Promise { + try { + // Attempt to clean up existing containers and networks + await this.cleanupDocker(); + + this.logger.info('Executing docker-compose up --build'); + const { stdout, stderr } = await execAsync('docker-compose up --build -d', { + cwd: projectPath, + timeout: this.config.timeout, + }); + + const logs = stdout + stderr; + this.logger.debug(`Docker execution output: ${logs}`); + return logs; + } catch (error) { + this.logger.error('Docker execution failed', error as Error); + throw new Error(`Docker execution failed: ${(error as Error).message}`); + } + } + + /** + * Attempts to clean up Docker containers and networks. + * Continues execution even if cleanup fails. + */ + private async cleanupDocker(): Promise { + try { + await execAsync('docker rm -f $(docker ps -aq)'); + this.logger.info('Successfully removed all Docker containers'); + } catch (error) { + this.logger.warn('Failed to remove Docker containers, continuing execution'); + } + + try { + await execAsync('docker network prune -f'); + this.logger.info('Successfully pruned Docker networks'); + } catch (error) { + this.logger.warn('Failed to prune Docker networks, continuing execution'); + } + } + + /** + * Checks if the Docker container is still running. + * @param projectPath - Path to the Docker project. + * @returns True if the container is running, false otherwise. + */ + private async checkContainerStatus(projectPath: string): Promise { + try { + const { stdout } = await execAsync('docker-compose ps -q', { cwd: projectPath }); + return stdout.trim() !== ''; + } catch (error) { + this.logger.error('Failed to check container status', error as Error); + return false; + } + } + + /** + * Validates the Docker project structure. + * @param projectPath - Path to the Docker project. + * @throws Error if docker-compose.yml is not found. + */ + private async validateProject(projectPath: string): Promise { + const dockerComposePath = path.join(projectPath, 'docker-compose.yml'); + try { + await fs.access(dockerComposePath); + } catch { + throw new Error('Invalid Docker project: docker-compose.yml not found'); + } + } + + /** + * Updates the .env file with provided parameters. + * @param projectPath - Path to the Docker project. + * @param params - Key-value pairs of environment variables. + */ + private async updateEnvFile(projectPath: string, params: Record): Promise { + const envPath = path.join(projectPath, '.env'); + const envContent = Object.entries(params) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + await fs.writeFile(envPath, envContent); + } + + /** + * Retrieves the output from the Docker execution. + * @param projectPath - Path to the Docker project. + * @returns Content of the output file or a default message if not available. + */ + private async getOutput(projectPath: string): Promise { + const outputPath = path.join(projectPath, 'output', 'output.txt'); + try { + return await fs.readFile(outputPath, 'utf-8'); + } catch (error) { + this.logger.error('Failed to read output file'); + return 'No output available'; + } + } +} \ No newline at end of file diff --git a/packages/sandbox-docker/src/security/resource-limiter.ts b/packages/sandbox-docker/src/security/resource-limiter.ts new file mode 100644 index 0000000..50ff914 --- /dev/null +++ b/packages/sandbox-docker/src/security/resource-limiter.ts @@ -0,0 +1,73 @@ +import { ResourceLimits, ResourceUsageStats } from '../types'; + +/** + * Class for managing and enforcing resource limits during execution + */ +export class ResourceLimiter { + private startTime: number; + private maxMemory: number; + private memoryCheckInterval: NodeJS.Timeout | null = null; + + /** + * Creates a new ResourceLimiter instance + * @param {ResourceLimits} limits - The resource limits to enforce + */ + constructor(private limits: ResourceLimits) { + this.startTime = Date.now(); + this.maxMemory = 0; + } + + /** + * Enforces the specified resource limits + * @returns {NodeJS.Timeout | null} A timeout object if execution time limit is set, null otherwise + */ + enforceLimits(): NodeJS.Timeout | null { + // Enforce execution time limit + if (this.limits.maxExecutionTime) { + return setTimeout(() => { + throw new Error('Execution time limit exceeded'); + }, this.limits.maxExecutionTime); + } + + // Enforce memory usage limit + if (this.limits.maxMemory) { + this.memoryCheckInterval = setInterval(() => { + const memoryUsage = process.memoryUsage().heapUsed; + if (this.limits.maxMemory && memoryUsage > this.limits.maxMemory) { + this.clearLimits(); + throw new Error('Memory limit exceeded'); + } + this.maxMemory = Math.max(this.maxMemory, memoryUsage); + }, 100); // Check every 100ms + } + + return null; + } + + /** + * Clears any active resource limit checks + */ + clearLimits(): void { + if (this.memoryCheckInterval) { + clearInterval(this.memoryCheckInterval); + } + } + + /** + * Monitors resource usage (placeholder for implementation) + */ + monitorUsage(): void { + // Implementation depends on specific monitoring requirements + } + + /** + * Retrieves current resource usage statistics + * @returns {ResourceUsageStats} Object containing execution time and max memory usage + */ + getUsageStats(): ResourceUsageStats { + return { + executionTime: Date.now() - this.startTime, + maxMemoryUsage: this.maxMemory, + }; + } +} diff --git a/packages/sandbox-docker/src/test.ts b/packages/sandbox-docker/src/test.ts new file mode 100644 index 0000000..b0cb5c6 --- /dev/null +++ b/packages/sandbox-docker/src/test.ts @@ -0,0 +1,51 @@ +import { Enclave } from './core/enclave'; +import { Logger } from './utils/logger'; + +async function runDockerProject(projectPath: string, inputValue: string) { + const logger = new Logger({ debugMode: true }); + logger.info(`Starting Docker project execution: ${projectPath}`); + + const enclave = new Enclave({ + cacheDir: './cache', + sandboxConfig: { rootDir: './sandbox' }, + resourceLimits: { + maxExecutionTime: 30000, + maxMemory: 100 * 1024 * 1024, + }, + loggerConfig: { debugMode: true }, + dockerConfig: { timeout: 300000 }, // 5 minutes, + }); + + const params = { + INPUT_VALUE: inputValue, + }; + + try { + logger.info(`Executing Docker project: ${projectPath}`); + const { output, logs } = await enclave.executeDocker(projectPath, params); + + logger.info('Build and execution logs:'); + console.log(logs); + + logger.info('Execution result:'); + console.log(output); + + // Extract the port number from the logs (if applicable) + const portMatch = logs.match(/Running on http:\/\/0\.0\.0\.0:(\d+)/); + if (portMatch) { + const port = portMatch[1]; + logger.info(`Application is running on port ${port}`); + } + } catch (error) { + logger.error('Execution failed:', error as Error); + } finally { + logger.info('Cleaning up enclave'); + await enclave.cleanup(); + } +} + +// Usage +runDockerProject('./example/nodejs-project', 'Hello, From docker!'); +// python-project +// nodejs-project +// python-flask diff --git a/packages/sandbox-docker/src/types.ts b/packages/sandbox-docker/src/types.ts new file mode 100644 index 0000000..839a449 --- /dev/null +++ b/packages/sandbox-docker/src/types.ts @@ -0,0 +1,47 @@ + +export type ProjectType = 'python_flask' | 'nodejs_express'; + +export interface DockerConfig { + // Add any Docker-specific configuration options here + timeout: number; +} + +export interface EnclaveConfig { + cacheDir: string; + sandboxConfig: SandboxConfig; + resourceLimits: ResourceLimits; + loggerConfig: LoggerConfig; + dockerConfig: DockerConfig; + } + + export interface FileInput { + name: string; + content: string; + } + + export type EnclaveStatus = 'initialized' | 'preparing' | 'prepared' | 'executing' | 'completed' | 'cleaning' | 'cleaned' | 'error'; + + export interface SandboxConfig { + rootDir: string; + } + + export interface ResourceLimits { + maxExecutionTime?: number; + maxMemory?: number; + } + + export interface ResourceUsageStats { + executionTime: number; + maxMemoryUsage: number; + } + + export interface LoggerConfig { + debugMode: boolean; + } + + export interface DetailedError { + message: string; + stack?: string; + type: string; + code?: string; + } \ No newline at end of file diff --git a/packages/sandbox-docker/src/utils/error-handler.ts b/packages/sandbox-docker/src/utils/error-handler.ts new file mode 100644 index 0000000..a5de40f --- /dev/null +++ b/packages/sandbox-docker/src/utils/error-handler.ts @@ -0,0 +1,21 @@ +import { DetailedError } from '../types'; + +export class ErrorHandler { + handleError(error: Error): DetailedError { + const detailedError: DetailedError = { + message: error.message, + stack: error.stack, + type: error.constructor.name, + }; + + if (this.isEnclaveError(error)) { + detailedError.code = (error as any).code; + } + + return detailedError; + } + + isEnclaveError(error: Error): boolean { + return error instanceof Error && 'code' in error; + } +} \ No newline at end of file diff --git a/packages/sandbox-docker/src/utils/logger.ts b/packages/sandbox-docker/src/utils/logger.ts new file mode 100644 index 0000000..6fa6cca --- /dev/null +++ b/packages/sandbox-docker/src/utils/logger.ts @@ -0,0 +1,41 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export class Logger { + private logFile: string; + + constructor(private config: { debugMode: boolean }) { + this.logFile = path.join(process.cwd(), 'enclave.log'); + } + + private log(level: string, message: string, error?: Error) { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${level}: ${message}`; + + console.log(logMessage); + fs.appendFileSync(this.logFile, logMessage + '\n'); + + if (error && this.config.debugMode) { + console.error(error); + fs.appendFileSync(this.logFile, `${error.stack}\n`); + } + } + + info(message: string) { + this.log('INFO', message); + } + + warn(message: string) { + this.log('WARN', message); + } + + error(message: string, error?: Error) { + this.log('ERROR', message, error); + } + + debug(message: string) { + if (this.config.debugMode) { + this.log('DEBUG', message); + } + } +} \ No newline at end of file diff --git a/packages/sandbox-docker/tsconfig.json b/packages/sandbox-docker/tsconfig.json new file mode 100644 index 0000000..826882d --- /dev/null +++ b/packages/sandbox-docker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] + } \ No newline at end of file