feat(ClamAvService): Add ClamAV Manager with Docker container management capabilities.
This commit is contained in:
		@@ -1,5 +1,13 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 2025-02-03 - 1.1.0 - feat(ClamAvService)
 | 
			
		||||
Add ClamAV Manager with Docker container management capabilities.
 | 
			
		||||
 | 
			
		||||
- Introduced ClamAVManager class to manage ClamAV Docker containers.
 | 
			
		||||
- Implemented startContainer and stopContainer methods in ClamAVManager.
 | 
			
		||||
- Integrated ClamAVManager into ClamAvService for managing container lifecycle.
 | 
			
		||||
- Added ClamAVManager test setups and helpers in test suite.
 | 
			
		||||
 | 
			
		||||
## 2025-01-10 - 1.0.4 - fix(documentation)
 | 
			
		||||
Removed redundant conclusion section in readme.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										90
									
								
								test/helpers/clamav.helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								test/helpers/clamav.helper.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
import { ClamAVManager } from '../../ts/classes.clamav.manager.js';
 | 
			
		||||
import { execAsync } from '../../ts/plugins.js';
 | 
			
		||||
 | 
			
		||||
let clamManager: ClamAVManager | null = null;
 | 
			
		||||
let isCleaningUp = false;
 | 
			
		||||
 | 
			
		||||
export async function getManager(): Promise<ClamAVManager> {
 | 
			
		||||
  if (!clamManager) {
 | 
			
		||||
    throw new Error('ClamAV manager not initialized');
 | 
			
		||||
  }
 | 
			
		||||
  return clamManager;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function setupClamAV(): Promise<ClamAVManager> {
 | 
			
		||||
  console.log('[Helper] Setting up ClamAV...');
 | 
			
		||||
  
 | 
			
		||||
  // First cleanup any existing containers
 | 
			
		||||
  await forceCleanupContainer();
 | 
			
		||||
  
 | 
			
		||||
  if (!clamManager) {
 | 
			
		||||
    console.log('[Helper] Creating new ClamAV manager instance');
 | 
			
		||||
    clamManager = new ClamAVManager();
 | 
			
		||||
    await clamManager.startContainer();
 | 
			
		||||
    console.log('[Helper] ClamAV manager initialized');
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log('[Helper] Using existing ClamAV manager instance');
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return clamManager;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function cleanupClamAV(): Promise<void> {
 | 
			
		||||
  if (isCleaningUp) {
 | 
			
		||||
    console.log('[Helper] Cleanup already in progress, skipping');
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  isCleaningUp = true;
 | 
			
		||||
  console.log('[Helper] Cleaning up ClamAV...');
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    if (clamManager) {
 | 
			
		||||
      await clamManager.stopContainer();
 | 
			
		||||
      console.log('[Helper] ClamAV container stopped');
 | 
			
		||||
    }
 | 
			
		||||
    await forceCleanupContainer();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('[Helper] Error during cleanup:', error);
 | 
			
		||||
    throw error;
 | 
			
		||||
  } finally {
 | 
			
		||||
    clamManager = null;
 | 
			
		||||
    isCleaningUp = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function forceCleanupContainer(): Promise<void> {
 | 
			
		||||
  try {
 | 
			
		||||
    // Stop any existing container
 | 
			
		||||
    await execAsync('docker stop clamav-daemon').catch(() => {});
 | 
			
		||||
    // Remove any existing container
 | 
			
		||||
    await execAsync('docker rm -f clamav-daemon').catch(() => {});
 | 
			
		||||
    console.log('[Helper] Forced cleanup of existing containers complete');
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    // Ignore errors as the container might not exist
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handle interrupts
 | 
			
		||||
process.on('SIGINT', async () => {
 | 
			
		||||
  console.log('\n[Helper] Received SIGINT. Cleaning up...');
 | 
			
		||||
  try {
 | 
			
		||||
    await cleanupClamAV();
 | 
			
		||||
    process.exit(0);
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error('[Helper] Error during cleanup:', err);
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Ensure cleanup on process exit
 | 
			
		||||
process.on('exit', () => {
 | 
			
		||||
  if (clamManager && !isCleaningUp) {
 | 
			
		||||
    console.log('[Helper] Process exit detected, attempting cleanup');
 | 
			
		||||
    // We can't use async functions in exit handler, so we do our best
 | 
			
		||||
    try {
 | 
			
		||||
      execAsync('docker stop clamav-daemon').catch(() => {});
 | 
			
		||||
      execAsync('docker rm -f clamav-daemon').catch(() => {});
 | 
			
		||||
    } catch {}
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										55
									
								
								test/test.clamav.manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								test/test.clamav.manager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import { expect, tap } from '../ts/plugins.js';
 | 
			
		||||
import { type ClamAVLogEvent, ClamAVManager } from '../ts/classes.clamav.manager.js';
 | 
			
		||||
import { setupClamAV, cleanupClamAV, getManager } from './helpers/clamav.helper.js';
 | 
			
		||||
 | 
			
		||||
let manager: ClamAVManager;
 | 
			
		||||
 | 
			
		||||
tap.test('setup', async () => {
 | 
			
		||||
  manager = await setupClamAV();
 | 
			
		||||
  expect(manager).toBeTruthy();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('should have initialized container and receive logs', async () => {
 | 
			
		||||
  let logReceived = false;
 | 
			
		||||
  
 | 
			
		||||
  // Add event listener for logs
 | 
			
		||||
  manager.on('log', (event: ClamAVLogEvent) => {
 | 
			
		||||
    console.log(`[Test] Received log event: ${event.type} - ${event.message}`);
 | 
			
		||||
    logReceived = true;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Wait for logs
 | 
			
		||||
  const maxWaitTime = 5000;
 | 
			
		||||
  const startTime = Date.now();
 | 
			
		||||
  
 | 
			
		||||
  while (!logReceived && Date.now() - startTime < maxWaitTime) {
 | 
			
		||||
    await new Promise(resolve => setTimeout(resolve, 100));
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  expect(logReceived).toBeTruthy('No logs received within timeout period');
 | 
			
		||||
 | 
			
		||||
  // Verify container is running by checking if we can get database info
 | 
			
		||||
  try {
 | 
			
		||||
    const dbInfo = await manager.getDatabaseInfo();
 | 
			
		||||
    expect(dbInfo).toBeTruthy('Container should be running and able to get database info');
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error getting database info:', error);
 | 
			
		||||
    expect.fail('Failed to get database info - container may not be fully initialized');
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('should get database info', async () => {
 | 
			
		||||
  const dbInfo = await manager.getDatabaseInfo();
 | 
			
		||||
  console.log('Database Info:', dbInfo);
 | 
			
		||||
  expect(dbInfo).toBeTruthy();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('should update database', async () => {
 | 
			
		||||
  await manager.updateDatabase();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup', async () => {
 | 
			
		||||
  await cleanupClamAV();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										49
									
								
								test/test.ts
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								test/test.ts
									
									
									
									
									
								
							@@ -1,35 +1,40 @@
 | 
			
		||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
 | 
			
		||||
import { expect, tap } from '../ts/plugins.js';
 | 
			
		||||
import * as smartantivirus from '../ts/index.js';
 | 
			
		||||
import { setupClamAV, cleanupClamAV } from './helpers/clamav.helper.js';
 | 
			
		||||
 | 
			
		||||
const EICAR_TEST_STRING = 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*';
 | 
			
		||||
let clamService: smartantivirus.ClamAvService;
 | 
			
		||||
 | 
			
		||||
tap.test('should create a ClamAvService instance', async () => {
 | 
			
		||||
  clamService = new smartantivirus.ClamAvService();
 | 
			
		||||
  expect(clamService).toBeDefined();
 | 
			
		||||
tap.test('setup', async () => {
 | 
			
		||||
  await setupClamAV();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('should scan a string', async () => {
 | 
			
		||||
  const scanResult = await clamService.scanString('X5O!P%@AP[4\PZX54(P^)7CC)7}' + '$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*');
 | 
			
		||||
tap.test('should create a ClamAvService instance and initialize ClamAV', async () => {
 | 
			
		||||
  clamService = new smartantivirus.ClamAvService();
 | 
			
		||||
  expect(clamService).toBeTruthy();
 | 
			
		||||
  // The manager will start the container and wait for initialization
 | 
			
		||||
  await clamService.verifyConnection();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('should detect EICAR test string', async () => {
 | 
			
		||||
  const scanResult = await clamService.scanString(EICAR_TEST_STRING);
 | 
			
		||||
  console.log('Scan Result:', scanResult);
 | 
			
		||||
  // expect(scanResult).toEqual({ isInfected: true, reason: 'FOUND' });
 | 
			
		||||
  expect(scanResult.isInfected).toEqual(true);
 | 
			
		||||
  expect(scanResult.reason).toBeTruthy();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('should not detect clean string', async () => {
 | 
			
		||||
  const scanResult = await clamService.scanString('This is a clean string with no virus signature');
 | 
			
		||||
  console.log('Clean Scan Result:', scanResult);
 | 
			
		||||
  expect(scanResult.isInfected).toEqual(false);
 | 
			
		||||
  expect(scanResult.reason).toBeUndefined();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup', async () => {
 | 
			
		||||
  await cleanupClamAV();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* (async () => {
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    
 | 
			
		||||
    await clamService.updateVirusDefinitions(); // Step 2: Update definitions
 | 
			
		||||
    await clamService.startClamDaemon(); // Step 3: Start daemon
 | 
			
		||||
 | 
			
		||||
    const scanResult = await clamService.scanString('EICAR test string...');
 | 
			
		||||
    console.log('Scan Result:', scanResult);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error:', error);
 | 
			
		||||
  }
 | 
			
		||||
})(); */
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  name: '@push.rocks/smartantivirus',
 | 
			
		||||
  version: '1.0.4',
 | 
			
		||||
  version: '1.1.0',
 | 
			
		||||
  description: 'A Node.js package for integrating antivirus scanning capabilities using ClamAV, allowing in-memory file and data scanning.'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										274
									
								
								ts/classes.clamav.manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								ts/classes.clamav.manager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,274 @@
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Start the ClamAV container if it's not already running
 | 
			
		||||
   */
 | 
			
		||||
  public async startContainer(): Promise<void> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
    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<string> {
 | 
			
		||||
    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.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.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<void> {
 | 
			
		||||
    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<void>((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();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +1,35 @@
 | 
			
		||||
import * as plugins from './plugins.js';
 | 
			
		||||
import * as paths from './paths.js';
 | 
			
		||||
 | 
			
		||||
import { exec } from 'child_process';
 | 
			
		||||
import net from 'net';
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
 | 
			
		||||
const execAsync = promisify(exec);
 | 
			
		||||
import { net } from './plugins.js';
 | 
			
		||||
import { ClamAVManager } from './classes.clamav.manager.js';
 | 
			
		||||
 | 
			
		||||
export class ClamAvService {
 | 
			
		||||
  private host: string;
 | 
			
		||||
  private port: number;
 | 
			
		||||
  private manager: ClamAVManager;
 | 
			
		||||
 | 
			
		||||
  constructor(host: string = '127.0.0.1', port: number = 3310) {
 | 
			
		||||
    this.host = host;
 | 
			
		||||
    this.port = port;
 | 
			
		||||
    this.manager = new ClamAVManager();
 | 
			
		||||
 | 
			
		||||
    // Listen to ClamAV logs
 | 
			
		||||
    this.manager.on('log', (event) => {
 | 
			
		||||
      if (event.type === 'scan') {
 | 
			
		||||
        console.log(`[ClamAV Scan] ${event.message}`);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async ensureContainerStarted(): Promise<void> {
 | 
			
		||||
    await this.manager.startContainer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Scans an in-memory Buffer using ClamAV daemon's INSTREAM command.
 | 
			
		||||
   */
 | 
			
		||||
  public async scanBuffer(buffer: Buffer): Promise<{ isInfected: boolean; reason?: string }> {
 | 
			
		||||
    await this.ensureContainerStarted();
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      const client = new net.Socket();
 | 
			
		||||
  
 | 
			
		||||
@@ -85,6 +95,7 @@ export class ClamAvService {
 | 
			
		||||
   * Verifies the ClamAV daemon is reachable.
 | 
			
		||||
   */
 | 
			
		||||
  public async verifyConnection(): Promise<boolean> {
 | 
			
		||||
    await this.ensureContainerStarted();
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      const client = new net.Socket();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,2 @@
 | 
			
		||||
export * from './classes.smartantivirus.js';
 | 
			
		||||
export * from './classes.smartantivirus.js';
 | 
			
		||||
export * from './classes.clamav.manager.js';
 | 
			
		||||
@@ -1,24 +1,39 @@
 | 
			
		||||
// node native scope
 | 
			
		||||
// Node.js built-in modules
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import { exec, spawn } from 'child_process';
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
import { EventEmitter } from 'events';
 | 
			
		||||
import net from 'net';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  fs,
 | 
			
		||||
  path,
 | 
			
		||||
}
 | 
			
		||||
  exec,
 | 
			
		||||
  spawn,
 | 
			
		||||
  promisify,
 | 
			
		||||
  EventEmitter,
 | 
			
		||||
  net
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// @push.rocks scope
 | 
			
		||||
import * as smartpath from '@push.rocks/smartpath';
 | 
			
		||||
import * as smartfile from '@push.rocks/smartfile';
 | 
			
		||||
import { expect, tap } from '@push.rocks/tapbundle';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  smartpath,
 | 
			
		||||
  smartfile,
 | 
			
		||||
}
 | 
			
		||||
  expect,
 | 
			
		||||
  tap
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// third party scope
 | 
			
		||||
// Third party scope
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  axios,
 | 
			
		||||
}
 | 
			
		||||
  axios
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Common utilities
 | 
			
		||||
export const execAsync = promisify(exec);
 | 
			
		||||
		Reference in New Issue
	
	Block a user