import { exec, spawn, net, promisify, EventEmitter, execAsync } from './plugins.js'; export interface ClamAVLogEvent { timestamp: string; message: string; type: 'update' | 'scan' | 'system' | 'error'; } export class ClamAVManager extends EventEmitter { private containerId: string | null = null; private containerName = 'clamav-daemon'; private imageTag = 'clamav/clamav:latest'; private port = 3310; private logs: ClamAVLogEvent[] = []; constructor() { super(); } public getLogs(): ClamAVLogEvent[] { return this.logs; } /** * Start the ClamAV container if it's not already running */ public async startContainer(): Promise { try { console.log('[ClamAV] Starting container initialization...'); // Check if container is already running const { stdout: psOutput } = await execAsync('docker ps --filter name=' + this.containerName); if (psOutput.includes(this.containerName)) { console.log('[ClamAV] Container is already running'); this.containerId = (await execAsync(`docker ps -q --filter name=${this.containerName}`)).stdout.trim(); console.log('[ClamAV] Container ID:', this.containerId); this.attachLogWatcher(); await this.waitForInitialization(); return; } // Check if container exists but is stopped const { stdout: psaOutput } = await execAsync('docker ps -a --filter name=' + this.containerName); if (psaOutput.includes(this.containerName)) { console.log('[ClamAV] Found stopped container, starting it...'); await execAsync(`docker start ${this.containerName}`); this.containerId = (await execAsync(`docker ps -q --filter name=${this.containerName}`)).stdout.trim(); console.log('[ClamAV] Started existing container, ID:', this.containerId); } else { // Create and start new container console.log('[ClamAV] Creating new container...'); const { stdout } = await execAsync( `docker run -d --name ${this.containerName} -p ${this.port}:3310 ${this.imageTag}` ); this.containerId = stdout.trim(); console.log('[ClamAV] Created new container, ID:', this.containerId); } this.attachLogWatcher(); console.log('[ClamAV] Waiting for initialization...'); await this.waitForInitialization(); console.log('[ClamAV] Container successfully initialized'); } catch (error) { console.error('[ClamAV] Error starting container:', error); throw error; } } /** * Stop the ClamAV container */ public async stopContainer(): Promise { if (!this.containerId) { console.log('No ClamAV container is running'); return; } try { await execAsync(`docker stop ${this.containerId}`); console.log('Stopped ClamAV container'); } catch (error) { console.error('Error stopping ClamAV container:', error); throw error; } } /** * Manually trigger a database update */ public async updateDatabase(): Promise { if (!this.containerId) { throw new Error('ClamAV container is not running'); } try { // First check if freshclam is already running const { stdout: psOutput } = await execAsync(`docker exec ${this.containerId} ps aux | grep freshclam`); if (psOutput.includes('/usr/local/sbin/freshclam -d')) { console.log('Freshclam daemon is already running'); // Wait a bit to ensure database is updated await new Promise(resolve => setTimeout(resolve, 2000)); return; } // If not running as daemon, try to update manually const { stdout, stderr } = await execAsync(`docker exec ${this.containerId} freshclam --no-warnings`); console.log('Database update output:', stdout); if (stderr) { console.error('Database update errors:', stderr); } } catch (error) { // Check if the error is due to freshclam already running if (error.stderr?.includes('ERROR: Problem with internal logger') || error.stdout?.includes('Resource temporarily unavailable')) { console.log('Freshclam is already running, skipping manual update'); return; } console.error('Error updating ClamAV database:', error); throw error; } } /** * Get the current database version information */ public async getDatabaseInfo(): Promise { if (!this.containerId) { throw new Error('ClamAV container is not running'); } try { // Try both .cld and .cvd files since ClamAV can use either format try { const { stdout } = await execAsync(`docker exec ${this.containerId} sigtool --info /var/lib/clamav/daily.cld`); return stdout; } catch { const { stdout } = await execAsync(`docker exec ${this.containerId} sigtool --info /var/lib/clamav/daily.cvd`); return stdout; } } catch (error) { console.error('Error getting database info:', error); throw error; } } /** * Watch container logs and emit events for different types of log messages */ private attachLogWatcher(): void { if (!this.containerId) return; const logProcess = spawn('docker', ['logs', '-f', this.containerId]); logProcess.stdout.on('data', (data) => { const lines = data.toString().split('\n'); lines.forEach(line => { if (!line.trim()) return; const event: ClamAVLogEvent = { timestamp: new Date().toISOString(), message: line, type: this.determineLogType(line) }; this.logs.push(event); this.emit('log', event); console.log(`[ClamAV ${event.type}] ${event.message}`); }); }); logProcess.stderr.on('data', (data) => { const event: ClamAVLogEvent = { timestamp: new Date().toISOString(), message: data.toString(), type: 'error' }; this.logs.push(event); this.emit('log', event); console.error(`[ClamAV error] ${event.message}`); }); logProcess.on('error', (error) => { console.error('Error in log watcher:', error); }); } /** * Determine the type of log message */ private determineLogType(logMessage: string): ClamAVLogEvent['type'] { const lowerMessage = logMessage.toLowerCase(); if (lowerMessage.includes('update') || lowerMessage.includes('freshclam')) { return 'update'; } else if (lowerMessage.includes('scan') || lowerMessage.includes('found')) { return 'scan'; } else if (lowerMessage.includes('error') || lowerMessage.includes('warning')) { return 'error'; } return 'system'; } /** * Wait for ClamAV to initialize by checking both logs and service readiness */ private async waitForInitialization(): Promise { return new Promise((resolve, reject) => { if (!this.containerId) { reject(new Error('Container ID not set')); return; } let timeout: NodeJS.Timeout; let checkCount = 0; const maxChecks = 60; // Check for 60 seconds const startTime = Date.now(); // Check service readiness const checkService = async () => { try { const elapsedTime = Math.round((Date.now() - startTime) / 1000); console.log(`[ClamAV] Checking service readiness (attempt ${checkCount + 1}, ${elapsedTime}s elapsed)...`); // First check if the service is accepting connections const client = new net.Socket(); await new Promise((resolveConn, rejectConn) => { const connectTimeout = setTimeout(() => { client.destroy(); rejectConn(new Error('Connection timeout')); }, 1000); client.connect(this.port, 'localhost', () => { clearTimeout(connectTimeout); client.end(); resolveConn(); }); client.on('error', (err) => { clearTimeout(connectTimeout); rejectConn(err); }); }); // Verify the service is responding to commands const { stdout } = await execAsync(`echo PING | nc localhost ${this.port}`); if (!stdout.includes('PONG')) { throw new Error('Service not responding to commands'); } // If we can connect and get a PONG, the service is ready console.log('[ClamAV] Service is accepting connections and responding to commands'); cleanup(); resolve(); } catch (error) { // Service not ready yet, will retry if (checkCount >= maxChecks) { cleanup(); reject(new Error(`ClamAV initialization timed out after ${maxChecks} seconds. Last error: ${error.message}`)); return; } checkCount++; } }; const cleanup = () => { clearTimeout(timeout); clearInterval(serviceCheck); }; const serviceCheck = setInterval(checkService, 1000); timeout = setTimeout(() => { cleanup(); reject(new Error('ClamAV initialization timed out after 60 seconds')); }, 60000); // Start initial service check checkService(); }); } }