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
JavaScript
;
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;