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.

426 lines (366 loc) 10.6 kB
const crypto = require('crypto'); /** * Rate Limiter for Security Operations * * Implements rate limiting for: * - SSH connection attempts * - Vault unlock attempts * - Command execution * - General API operations */ class RateLimiter { constructor(options = {}) { this.defaultWindow = options.defaultWindow || 60 * 1000; // 1 minute this.maxAttempts = options.maxAttempts || 5; this.blockDuration = options.blockDuration || 15 * 60 * 1000; // 15 minutes // Storage for tracking attempts this.attempts = new Map(); this.blocked = new Map(); // Cleanup interval - only create in production to avoid test interference if (process.env.NODE_ENV !== 'test') { this.cleanupInterval = setInterval(() => { this.cleanup(); }, 5 * 60 * 1000); // Clean up every 5 minutes } } /** * Check if an operation is allowed */ isAllowed(identifier, operation = 'default', options = {}) { const key = this.getKey(identifier, operation); const now = Date.now(); // Check if currently blocked if (this.isBlocked(key, now)) { return { allowed: false, reason: 'rate_limited', remainingTime: this.getRemainingBlockTime(key, now), retryAfter: new Date(now + this.getRemainingBlockTime(key, now)) }; } // Get current attempts const attempts = this.attempts.get(key) || []; const window = options.window || this.defaultWindow; const maxAttempts = options.maxAttempts || this.maxAttempts; // Filter attempts within current window const recentAttempts = attempts.filter(timestamp => now - timestamp < window ); // Check if limit exceeded if (recentAttempts.length >= maxAttempts) { // Block this identifier this.block(key, now, options.blockDuration || this.blockDuration); return { allowed: false, reason: 'limit_exceeded', remainingTime: options.blockDuration || this.blockDuration, retryAfter: new Date(now + (options.blockDuration || this.blockDuration)) }; } return { allowed: true, remainingAttempts: maxAttempts - recentAttempts.length, resetTime: new Date(now + window) }; } /** * Record an attempt */ recordAttempt(identifier, operation = 'default', success = false) { const key = this.getKey(identifier, operation); const now = Date.now(); // Get or create attempts array if (!this.attempts.has(key)) { this.attempts.set(key, []); } const attempts = this.attempts.get(key); // Add current attempt attempts.push(now); // If successful, clear failed attempts if (success) { this.clearAttempts(key); } // Clean up old attempts this.cleanupAttempts(key, now); } /** * Record a successful operation */ recordSuccess(identifier, operation = 'default') { this.recordAttempt(identifier, operation, true); } /** * Record a failed operation */ recordFailure(identifier, operation = 'default') { this.recordAttempt(identifier, operation, false); } /** * Check if an identifier is currently blocked */ isBlocked(key, now = Date.now()) { const blockedInfo = this.blocked.get(key); if (!blockedInfo) { return false; } // Check if block has expired if (now >= blockedInfo.until) { this.blocked.delete(key); return false; } return true; } /** * Block an identifier */ block(key, now = Date.now(), duration = null) { const blockDuration = duration || this.blockDuration; this.blocked.set(key, { until: now + blockDuration, blockedAt: now, duration: blockDuration }); } /** * Unblock an identifier */ unblock(identifier, operation = 'default') { const key = this.getKey(identifier, operation); this.blocked.delete(key); this.clearAttempts(key); } /** * Get remaining block time */ getRemainingBlockTime(key, now = Date.now()) { const blockedInfo = this.blocked.get(key); if (!blockedInfo) { return 0; } return Math.max(0, blockedInfo.until - now); } /** * Clear attempts for an identifier */ clearAttempts(key) { this.attempts.delete(key); } /** * Clean up old attempts */ cleanupAttempts(key, now = Date.now()) { const attempts = this.attempts.get(key); if (!attempts) { return; } // Remove attempts older than 1 hour const cutoff = now - (60 * 60 * 1000); const recentAttempts = attempts.filter(timestamp => timestamp > cutoff); if (recentAttempts.length === 0) { this.attempts.delete(key); } else { this.attempts.set(key, recentAttempts); } } /** * Clean up expired blocks and old attempts */ cleanup() { const now = Date.now(); // Clean up expired blocks for (const [key, blockedInfo] of this.blocked.entries()) { if (now >= blockedInfo.until) { this.blocked.delete(key); } } // Clean up old attempts for (const key of this.attempts.keys()) { this.cleanupAttempts(key, now); } } /** * Generate a unique key for an identifier and operation */ getKey(identifier, operation) { return `${operation}:${identifier}`; } /** * Get statistics for an identifier */ getStats(identifier, operation = 'default') { const key = this.getKey(identifier, operation); const now = Date.now(); const attempts = this.attempts.get(key) || []; const blockedInfo = this.blocked.get(key); const recentAttempts = attempts.filter(timestamp => now - timestamp < this.defaultWindow ); return { identifier, operation, totalAttempts: attempts.length, recentAttempts: recentAttempts.length, isBlocked: this.isBlocked(key, now), remainingBlockTime: this.getRemainingBlockTime(key, now), resetTime: new Date(now + this.defaultWindow), remainingAttempts: Math.max(0, this.maxAttempts - recentAttempts.length) }; } /** * Get all statistics */ getAllStats() { const stats = []; // Get unique identifiers const identifiers = new Set(); for (const key of this.attempts.keys()) { const [, identifier] = key.split(':', 2); identifiers.add(identifier); } for (const key of this.blocked.keys()) { const [, identifier] = key.split(':', 2); identifiers.add(identifier); } // Get stats for each identifier for (const identifier of identifiers) { stats.push(this.getStats(identifier)); } return stats; } /** * Reset all rate limiting for an identifier */ reset(identifier, operation = 'default') { const key = this.getKey(identifier, operation); this.blocked.delete(key); this.clearAttempts(key); } /** * Reset all rate limiting */ resetAll() { this.attempts.clear(); this.blocked.clear(); } /** * Update configuration */ updateConfig(newConfig) { if (newConfig.defaultWindow) { this.defaultWindow = newConfig.defaultWindow; } if (newConfig.maxAttempts) { this.maxAttempts = newConfig.maxAttempts; } if (newConfig.blockDuration) { this.blockDuration = newConfig.blockDuration; } } /** * Get current configuration */ getConfig() { return { defaultWindow: this.defaultWindow, maxAttempts: this.maxAttempts, blockDuration: this.blockDuration, totalTracked: this.attempts.size + this.blocked.size }; } /** * Destroy the rate limiter and cleanup resources */ destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.resetAll(); } } /** * Specialized rate limiter for SSH operations */ class SSHRateLimiter extends RateLimiter { constructor(options = {}) { super({ defaultWindow: 5 * 60 * 1000, // 5 minutes for SSH maxAttempts: 3, // 3 attempts per 5 minutes blockDuration: 30 * 60 * 1000, // 30 minutes block ...options }); this.sshOperations = ['connect', 'auth', 'command']; } /** * Check if SSH connection is allowed */ isSSHAllowed(hostname, username, operation = 'connect') { const identifier = `${username}@${hostname}`; return this.isAllowed(identifier, `ssh_${operation}`, { window: 5 * 60 * 1000, // 5 minutes maxAttempts: 3, // 3 attempts blockDuration: 30 * 60 * 1000 // 30 minutes block }); } /** * Record SSH connection attempt */ recordSSHAttempt(hostname, username, operation = 'connect', success = false) { const identifier = `${username}@${hostname}`; this.recordAttempt(identifier, `ssh_${operation}`, success); } /** * Record successful SSH connection */ recordSSHSuccess(hostname, username, operation = 'connect') { this.recordSSHAttempt(hostname, username, operation, true); } /** * Record failed SSH connection */ recordSSHFailure(hostname, username, operation = 'connect') { this.recordSSHAttempt(hostname, username, operation, false); } } /** * Specialized rate limiter for vault operations */ class VaultRateLimiter extends RateLimiter { constructor(options = {}) { super({ defaultWindow: 15 * 60 * 1000, // 15 minutes for vault maxAttempts: 5, // 5 attempts per 15 minutes blockDuration: 60 * 60 * 1000, // 1 hour block ...options }); } /** * Check if vault unlock is allowed */ isVaultUnlockAllowed(identifier = 'default') { return this.isAllowed(identifier, 'vault_unlock', { window: 15 * 60 * 1000, // 15 minutes maxAttempts: 5, // 5 attempts blockDuration: 60 * 60 * 1000 // 1 hour block }); } /** * Record vault unlock attempt */ recordVaultUnlockAttempt(identifier = 'default', success = false) { this.recordAttempt(identifier, 'vault_unlock', success); } /** * Record successful vault unlock */ recordVaultUnlockSuccess(identifier = 'default') { this.recordVaultUnlockAttempt(identifier, true); } /** * Record failed vault unlock */ recordVaultUnlockFailure(identifier = 'default') { this.recordVaultUnlockAttempt(identifier, false); } } module.exports = { RateLimiter, SSHRateLimiter, VaultRateLimiter };