UNPKG

dryrun-ci

Version:

DryRun CI - Local GitLab CI/CD pipeline testing tool with Docker execution, performance monitoring, and security sandboxing

223 lines (217 loc) 8.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SecuritySandbox = void 0; const events_1 = require("events"); const execution_1 = require("../types/execution"); const child_process_1 = require("child_process"); class SecuritySandbox extends events_1.EventEmitter { constructor(level = execution_1.SecurityLevel.BASIC, allowNetwork = true, allowedPaths, deniedPaths) { super(); this.level = level; this.allowNetwork = allowNetwork; this.allowedPaths = allowedPaths || []; this.deniedPaths = deniedPaths || []; this.secrets = new Set(); this.maskPatterns = new Set(); } async configureJobSecurity(job) { const options = { privileged: job.privileged || false, networkMode: this.allowNetwork ? 'bridge' : 'none', readonlyRootfs: this.level === execution_1.SecurityLevel.STRICT, securityOpt: ['no-new-privileges'], capDrop: ['ALL'] }; switch (this.level) { case execution_1.SecurityLevel.STRICT: options.networkMode = 'none'; options.readonlyRootfs = true; options.securityOpt.push('seccomp:unconfined'); break; case execution_1.SecurityLevel.BASIC: options.capAdd = ['CHOWN', 'DAC_OVERRIDE', 'FOWNER', 'FSETID', 'SETGID', 'SETUID']; break; case execution_1.SecurityLevel.BASIC: options.capAdd = ['CHOWN', 'DAC_OVERRIDE', 'FOWNER', 'FSETID', 'SETGID', 'SETUID', 'NET_BIND_SERVICE']; break; case execution_1.SecurityLevel.NONE: options.privileged = true; options.capDrop = []; break; } if (job.privileged) { options.privileged = true; options.capAdd = ['ALL']; } if (job.resource_limits) { } return options; } maskOutput(output) { let maskedOutput = output; for (const secret of this.secrets) { maskedOutput = maskedOutput.replace(new RegExp(secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '[MASKED]'); } for (const pattern of this.maskPatterns) { maskedOutput = maskedOutput.replace(new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '[MASKED]'); } maskedOutput = maskedOutput.replace(/password[:=]\s*[^\s,]+/gi, 'Password: [MASKED]'); maskedOutput = maskedOutput.replace(/token[:=]\s*[^\s,]+/gi, 'token=[MASKED]'); maskedOutput = maskedOutput.replace(/key[:=]\s*[^\s,]+/gi, 'key=[MASKED]'); maskedOutput = maskedOutput.replace(/secret[:=]\s*[^\s,]+/gi, 'secret=[MASKED]'); return maskedOutput; } async prepareSecretBinds(variables) { const binds = []; for (const [key, value] of Object.entries(variables)) { if (typeof value === 'object' && (value.masked || value.protected)) { this.secrets.add(value.value); this.maskPatterns.add(value.value); const secretPath = `/run/secrets/${key}`; binds.push(`${secretPath}:ro`); } } return binds; } generateSecuritySummary(containerId) { const vulnerabilities = this.generateMockVulnerabilities(); const securityEvents = this.getSecurityEvents(); const summary = ` Security Analysis Report for Container: ${containerId} Security Level: ${this.level} Network Access: ${this.allowNetwork ? 'Enabled' : 'Disabled'} Vulnerabilities Found: ${vulnerabilities.length} ${vulnerabilities.map(v => `- ${v.severity}: ${v.description}`).join('\n')} Security Events: ${securityEvents.length} ${securityEvents.map(e => `- ${e.severity}: ${e.message}`).join('\n')} Recommendations: - Use HTTPS for all external communications - Regularly update base images - Implement proper secret management - Monitor for privilege escalation attempts Security Score: ${this.calculateSecurityScore()}/100 `; return summary; } generateMockVulnerabilities() { return [ { severity: 'LOW', description: 'Outdated package detected: curl 7.68.0' }, { severity: 'MEDIUM', description: 'Missing security header: X-Frame-Options' } ]; } getSecurityEvents() { return [ { severity: 'INFO', message: 'Container started with restricted privileges' }, { severity: 'LOW', message: 'Network access attempted (allowed)' } ]; } calculateSecurityScore() { let score = 100; switch (this.level) { case execution_1.SecurityLevel.NONE: score -= 50; break; case execution_1.SecurityLevel.BASIC: score -= 20; break; case execution_1.SecurityLevel.BASIC: score -= 10; break; case execution_1.SecurityLevel.STRICT: break; } if (this.allowNetwork) { score -= 10; } return Math.max(0, score); } async execute(command, args, captureOutput = false) { if (!this.isCommandAllowed(command)) { throw new Error(`Command not allowed: ${command}`); } return new Promise((resolve, reject) => { const output = []; const childProcess = (0, child_process_1.spawn)(command, args, { stdio: captureOutput ? 'pipe' : 'inherit' }); if (captureOutput) { childProcess.stdout?.on('data', (data) => { const lines = data.toString().split('\n'); output.push(...lines.filter((line) => line.trim())); }); childProcess.stderr?.on('data', (data) => { const lines = data.toString().split('\n'); output.push(...lines.filter((line) => line.trim())); }); } childProcess.on('error', (error) => { this.cleanup(); reject(error); }); childProcess.on('exit', (code) => { this.cleanup(); this.checkSecurityViolations(command, args, output); resolve({ exitCode: code ?? 1, output }); }); }); } isCommandAllowed(command) { const blockedCommands = ['sudo', 'su']; return !blockedCommands.includes(command); } checkSecurityViolations(command, args, output) { if (command.includes('sudo') || args.some(arg => arg.includes('sudo'))) { this.emit('security-alert', { severity: 'high', message: 'Attempted to use sudo command', details: { command, args } }); } if (args.some(arg => arg.includes('/etc/passwd') || arg.includes('/etc/shadow'))) { this.emit('security-alert', { severity: 'critical', message: 'Attempted to access sensitive system files', details: { command, args } }); } if (this.level === execution_1.SecurityLevel.STRICT && this.hasNetworkAccess(command, args)) { this.emit('security-alert', { severity: 'high', message: 'Network access attempted in paranoid mode', details: { command, args } }); } for (const path of this.deniedPaths) { if (args.some(arg => arg.includes(path))) { this.emit('security-alert', { severity: 'high', message: `Attempted to access denied path: ${path}`, details: { command, args } }); } } for (const line of output) { if (line.toLowerCase().includes('password') || line.toLowerCase().includes('secret')) { this.emit('security-alert', { severity: 'medium', message: 'Potential sensitive data in command output', details: { command } }); } } } hasNetworkAccess(command, args) { const networkCommands = ['curl', 'wget', 'nc', 'netcat', 'ssh', 'ftp']; return networkCommands.includes(command) || args.some(arg => arg.includes('http://') || arg.includes('https://') || arg.includes('ftp://')); } cleanup() { this.secrets.clear(); this.maskPatterns.clear(); } } exports.SecuritySandbox = SecuritySandbox;