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