UNPKG

ssh-bridge-ai

Version:

One Command Magic SSH with Invisible Analytics - Connect to any server instantly with 'sshbridge user@server'. Zero setup, zero friction, pure magic. Industry-standard security with behind-the-scenes business intelligence.

465 lines (410 loc) 12.6 kB
const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); /** * Secure Logger with Automatic Sensitive Data Redaction * * Implements secure logging with: * - Automatic redaction of sensitive data * - Configurable log levels * - Audit trail for security events * - Tamper-evident logging */ class SecureLogger { constructor(options = {}) { this.logLevel = options.logLevel || process.env.SSHBRIDGE_LOG_LEVEL || 'info'; this.logFile = options.logFile || path.join(process.env.HOME || process.cwd(), '.sshbridge', 'logs', 'security.log'); this.maxLogSize = options.maxLogSize || 10 * 1024 * 1024; // 10MB this.maxLogFiles = options.maxLogFiles || 5; this.enableAuditLog = options.enableAuditLog !== false; this.enableFileLogging = options.enableFileLogging !== false; // Sensitive data patterns for automatic redaction this.sensitivePatterns = [ // SSH private keys { pattern: /-----BEGIN\s+.*PRIVATE\s+KEY-----[\s\S]*?-----END\s+.*PRIVATE\s+KEY-----/g, replacement: '[REDACTED_SSH_PRIVATE_KEY]' }, // SSH public keys { pattern: /ssh-rsa\s+[A-Za-z0-9+/]+[=]*\s+[^\s]+/g, replacement: '[REDACTED_SSH_PUBLIC_KEY]' }, // SSH fingerprints { pattern: /[0-9a-f]{2}(:[0-9a-f]{2}){15}/g, replacement: '[REDACTED_SSH_FINGERPRINT]' }, // MD5 hashes { pattern: /[a-f0-9]{32}/g, replacement: '[REDACTED_MD5_HASH]' }, // SHA hashes { pattern: /[a-f0-9]{40}|[a-f0-9]{64}/g, replacement: '[REDACTED_SHA_HASH]' }, // Email addresses { pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replacement: '[REDACTED_EMAIL]' }, // IP addresses { pattern: /(?:[0-9]{1,3}\.){3}[0-9]{1,3}/g, replacement: '[REDACTED_IP]' }, // IPv6 addresses { pattern: /(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::1|fe80:[0-9a-fA-F:]*|fc00:[0-9a-fA-F:]*|fd00:[0-9a-fA-F:]*/g, replacement: '[REDACTED_IPV6]' }, // Passwords in URLs { pattern: /(https?:\/\/[^:]+:)[^@]+@/g, replacement: '$1[REDACTED_PASSWORD]@' }, // API keys { pattern: /(api[_-]?key|token|secret|password|passwd|pwd)\s*[:=]\s*[^\s\n]+/gi, replacement: '$1: [REDACTED]' }, // JWT tokens { pattern: /eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*/g, replacement: '[REDACTED_JWT_TOKEN]' }, // Private network addresses { pattern: /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|127\.|169\.254\.)/g, replacement: '[REDACTED_PRIVATE_IP]' }, // File paths that might contain sensitive info { pattern: /(\/home\/|\/root\/|\/etc\/|\/var\/log\/|\/tmp\/)[^\s\n]+/g, replacement: '[REDACTED_FILE_PATH]' } ]; // Log levels and their numeric values this.logLevels = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 }; // Ensure log directory exists if (this.enableFileLogging) { this.ensureLogDirectory(); } } /** * Ensure log directory exists with secure permissions */ async ensureLogDirectory() { try { const logDir = path.dirname(this.logFile); if (!(await fs.access(logDir).catch(() => false))) { await fs.mkdir(logDir, { recursive: true, mode: 0o700 }); } } catch (error) { console.error('Failed to create log directory:', error.message); } } /** * Sanitize sensitive data from text */ sanitize(text) { if (typeof text !== 'string') { return text; } let sanitized = text; // Apply all sensitive patterns for (const { pattern, replacement } of this.sensitivePatterns) { sanitized = sanitized.replace(pattern, replacement); } return sanitized; } /** * Sanitize object recursively */ sanitizeObject(obj) { if (obj === null || obj === undefined) { return obj; } if (typeof obj === 'string') { return this.sanitize(obj); } if (Array.isArray(obj)) { return obj.map(item => this.sanitizeObject(item)); } if (typeof obj === 'object') { const sanitized = {}; for (const [key, value] of Object.entries(obj)) { // Skip logging of sensitive keys entirely if (this.isSensitiveKey(key)) { sanitized[key] = '[REDACTED_SENSITIVE_KEY]'; } else { sanitized[key] = this.sanitizeObject(value); } } return sanitized; } return obj; } /** * Check if a key name indicates sensitive data */ isSensitiveKey(key) { const sensitiveKeys = [ 'password', 'passwd', 'pwd', 'secret', 'key', 'token', 'auth', 'credential', 'private', 'ssh', 'rsa', 'dsa', 'ecdsa', 'ed25519', 'fingerprint', 'hash', 'salt', 'nonce', 'iv', 'cipher' ]; return sensitiveKeys.some(sensitiveKey => key.toLowerCase().includes(sensitiveKey.toLowerCase()) ); } /** * Log message with automatic sanitization */ log(level, message, data = {}, options = {}) { // Check log level if (this.logLevels[level] > this.logLevels[this.logLevel]) { return; } // Sanitize message and data const sanitizedMessage = this.sanitize(message); const sanitizedData = this.sanitizeObject(data); // Create log entry const timestamp = new Date().toISOString(); const logEntry = { timestamp, level: level.toUpperCase(), message: sanitizedMessage, data: sanitizedData, pid: process.pid, ...options }; // Console output this.outputToConsole(logEntry); // File logging if (this.enableFileLogging) { this.outputToFile(logEntry); } // Audit logging for security events if (this.enableAuditLog && this.isSecurityEvent(level, message)) { this.auditLog(logEntry); } } /** * Output to console with color coding */ outputToConsole(logEntry) { const { level, message, timestamp } = logEntry; // Color coding for different log levels const colors = { ERROR: '\x1b[31m', // Red WARN: '\x1b[33m', // Yellow INFO: '\x1b[36m', // Cyan DEBUG: '\x1b[35m', // Magenta TRACE: '\x1b[37m' // White }; const reset = '\x1b[0m'; const color = colors[level] || ''; console.log(`${color}[${level}]${reset} ${timestamp} ${message}`); // Log data if present and not empty if (logEntry.data && Object.keys(logEntry.data).length > 0) { console.log(JSON.stringify(logEntry.data, null, 2)); } } /** * Output to file */ async outputToFile(logEntry) { try { const logLine = JSON.stringify(logEntry) + '\n'; // Append to log file first await fs.appendFile(this.logFile, logLine, { mode: 0o600 }); // Check if we need to rotate logs after writing await this.rotateLogsIfNeeded(); } catch (error) { console.error('Failed to write to log file:', error.message); } } /** * Rotate logs if they exceed size limit */ async rotateLogsIfNeeded() { try { // Check if log file exists let stats; try { stats = await fs.stat(this.logFile); } catch (error) { // File doesn't exist, no rotation needed return; } if (stats.size > this.maxLogSize) { // Rotate existing logs for (let i = this.maxLogFiles - 1; i > 0; i--) { const oldFile = `${this.logFile}.${i}`; const newFile = `${this.logFile}.${i + 1}`; try { if (await fs.access(oldFile).catch(() => false)) { await fs.rename(oldFile, newFile); } } catch (error) { // Ignore errors during rotation } } // Rename current log file await fs.rename(this.logFile, `${this.logFile}.1`); // Create new log file await fs.writeFile(this.logFile, '', { mode: 0o600 }); } } catch (error) { // Ignore rotation errors } } /** * Check if this is a security event that should be audited */ isSecurityEvent(level, message) { const securityKeywords = [ 'login', 'logout', 'auth', 'authentication', 'authorization', 'ssh', 'key', 'vault', 'unlock', 'lock', 'password', 'secret', 'permission', 'access', 'denied', 'failed', 'success', 'attempt', 'brute', 'force', 'attack', 'vulnerability', 'exploit', 'malware' ]; const lowerMessage = message.toLowerCase(); return securityKeywords.some(keyword => lowerMessage.includes(keyword)); } /** * Audit logging for security events */ async auditLog(logEntry) { try { const auditFile = path.join(path.dirname(this.logFile), 'audit.log'); const auditEntry = { ...logEntry, audit: true, sessionId: this.getSessionId(), userAgent: process.env.USER_AGENT || 'unknown', sourceIp: process.env.SOURCE_IP || 'unknown' }; const auditLine = JSON.stringify(auditEntry) + '\n'; await fs.appendFile(auditFile, auditLine, { mode: 0o600 }); } catch (error) { console.error('Failed to write audit log:', error.message); } } /** * Generate or retrieve session ID */ getSessionId() { if (!this.sessionId) { this.sessionId = crypto.randomBytes(16).toString('hex'); } return this.sessionId; } /** * Convenience methods for different log levels */ error(message, data = {}, options = {}) { this.log('error', message, data, options); } warn(message, data = {}, options = {}) { this.log('warn', message, data, options); } info(message, data = {}, options = {}) { this.log('info', message, data, options); } debug(message, data = {}, options = {}) { this.log('debug', message, data, options); } trace(message, data = {}, options = {}) { this.log('trace', message, data, options); } /** * Log security event with additional context */ security(level, event, details = {}, options = {}) { const securityOptions = { ...options, securityEvent: true, eventType: event, timestamp: new Date().toISOString() }; this.log(level, `SECURITY: ${event}`, details, securityOptions); } /** * Log command execution for audit trail */ logCommand(command, hostname, user, exitCode, duration, options = {}) { const commandData = { command: this.sanitize(command), hostname: this.sanitize(hostname), user: this.sanitize(user), exitCode, duration, timestamp: new Date().toISOString(), ...options }; this.info('Command executed', commandData, { commandExecution: true }); } /** * Log SSH connection attempt */ logSSHConnection(hostname, user, keyPath, success, error = null, options = {}) { const connectionData = { hostname: this.sanitize(hostname), user: this.sanitize(user), keyPath: this.sanitize(keyPath), success, error: error ? this.sanitize(error.message) : null, timestamp: new Date().toISOString(), ...options }; this.info('SSH connection attempt', connectionData, { sshConnection: true }); } /** * Get logger configuration */ getConfig() { return { logLevel: this.logLevel, logFile: this.logFile, maxLogSize: this.maxLogSize, maxLogFiles: this.maxLogFiles, enableAuditLog: this.enableAuditLog, enableFileLogging: this.enableFileLogging, sensitivePatterns: this.sensitivePatterns.length }; } /** * Update logger configuration */ updateConfig(newConfig) { if (newConfig.logLevel) { this.logLevel = newConfig.logLevel; } if (newConfig.logFile) { this.logFile = newConfig.logFile; } if (newConfig.maxLogSize) { this.maxLogSize = newConfig.maxLogSize; } if (newConfig.maxLogFiles) { this.maxLogFiles = newConfig.maxLogFiles; } if (newConfig.enableAuditLog !== undefined) { this.enableAuditLog = newConfig.enableAuditLog; } if (newConfig.enableFileLogging !== undefined) { this.enableFileLogging = newConfig.enableFileLogging; } } } module.exports = { SecureLogger };