fix(clamav.manager): Improve log handling and add timeout for log reception in ClamAV manager tests

This commit is contained in:
Philipp Kunz 2025-02-03 13:55:15 +01:00
parent e31e7cca44
commit 4446f265cb
7 changed files with 175 additions and 29 deletions

View File

@ -1,5 +1,12 @@
# Changelog
## 2025-02-03 - 1.1.1 - fix(clamav.manager)
Improve log handling and add timeout for log reception in ClamAV manager tests
- Refined the log receiving mechanism in ClamAV manager tests to use promises for better control over log receipt timing.
- Introduced a timeout mechanism in the log receiving test case to avoid indefinite waiting.
- Fixed the test case setup to accurately reflect log receipt and database information verification.
## 2025-02-03 - 1.1.0 - feat(ClamAvService)
Add ClamAV Manager with Docker container management capabilities.

View File

@ -19,7 +19,8 @@
"@git.zone/tsrun": "^1.2.46",
"@git.zone/tstest": "^1.0.44",
"@push.rocks/tapbundle": "^5.0.15",
"@types/node": "^20.8.7"
"@types/node": "^20.8.7",
"typescript": "^5.7.3"
},
"dependencies": {
"@push.rocks/smartfile": "^11.1.5",

10
pnpm-lock.yaml generated
View File

@ -39,6 +39,9 @@ importers:
'@types/node':
specifier: ^20.8.7
version: 20.17.12
typescript:
specifier: ^5.7.3
version: 5.7.3
packages:
@ -3935,6 +3938,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
typescript@5.7.3:
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
engines: {node: '>=14.17'}
hasBin: true
uglify-js@3.19.3:
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
engines: {node: '>=0.8.0'}
@ -9602,6 +9610,8 @@ snapshots:
typescript@5.6.3: {}
typescript@5.7.3: {}
uglify-js@3.19.3: {}
uint8array-extras@1.4.0: {}

125
readme.md
View File

@ -1,28 +1,72 @@
# @push.rocks/smartantivirus
A package for performing antivirus testing, especially suitable for use with ClamAV.
A package for performing antivirus testing with ClamAV, featuring both direct daemon communication and Docker container management.
## Features
- **Docker Integration**: Automatically manages ClamAV containers for easy setup and testing
- **Real-time Logging**: Captures and processes ClamAV logs with type-safe event handling
- **Database Management**: Supports automatic database updates and version tracking
- **Flexible Scanning**: Scan strings, buffers, and files for malware
- **Health Monitoring**: Built-in service readiness checks and connection verification
## Install
Installing `@push.rocks/smartantivirus` is straightforward. You'll need Node.js and npm installed on your machine to get started. Once they are ready, you can add the `@push.rocks/smartantivirus` package to your project by running the following command:
Installing `@push.rocks/smartantivirus` is straightforward. You'll need Node.js and npm installed on your machine:
```bash
npm install @push.rocks/smartantivirus
```
This will add the package to your project's dependencies and allow you to integrate antivirus scanning capabilities directly into your application.
### Prerequisites
- Node.js and npm
- Docker (for container-based usage)
- ClamAV daemon (for direct daemon usage)
## Usage
The `@push.rocks/smartantivirus` package provides tools to easily integrate antivirus scanning capabilities into your Node.js application by interfacing with the ClamAV daemon. Below is a comprehensive guide on how to use the features of this library.
The package provides two main ways to use ClamAV:
### Setting Up the ClamAV Daemon
1. **Docker-based Usage** (Recommended): Uses `ClamAVManager` to automatically handle container lifecycle
2. **Direct Daemon Usage**: Uses `ClamAvService` to communicate with an existing ClamAV daemon
Before using this package, make sure you have ClamAV installed and running on your system. You can find installation instructions for various operating systems on the [ClamAV official website](https://www.clamav.net/documents/installing-clamav).
Below is a comprehensive guide on how to use both approaches.
After installing ClamAV, start the ClamAV daemon (`clamd`). Make sure it is configured to listen on a port accessible to your Node.js application. You can configure this in the `clamd.conf` file, typically located in `/etc/clamav/clamd.conf`.
### Docker-based Usage with ClamAVManager
### Basic Usage
The `ClamAVManager` class provides a high-level interface for managing ClamAV in Docker containers:
```typescript
import { ClamAVManager } from '@push.rocks/smartantivirus';
async function main() {
// Create a new manager instance
const manager = new ClamAVManager();
// Start the ClamAV container
await manager.startContainer();
// Listen for log events
manager.on('log', (event) => {
console.log(`[ClamAV ${event.type}] ${event.message}`);
});
// Get database information
const dbInfo = await manager.getDatabaseInfo();
console.log('Database Info:', dbInfo);
// Update virus definitions
await manager.updateDatabase();
// When done, stop the container
await manager.stopContainer();
}
main().catch(console.error);
```
### Direct Daemon Usage with ClamAvService
The primary interface provided by the package is the `ClamAvService` class. It allows you to scan data in memory or verify the connection to the ClamAV daemon.
@ -89,11 +133,24 @@ scanBufferExample();
### Error Handling and Debugging
The methods of `ClamAvService` throw errors if there are issues with communication or processing data. Wrap your code in try-catch blocks and use appropriate logging to handle errors gracefully.
Both `ClamAVManager` and `ClamAvService` provide comprehensive error handling:
```typescript
try {
const scanResult = await clamService.scanString('Some suspicious string...');
// Using ClamAVManager
const manager = new ClamAVManager();
await manager.startContainer();
// Listen for errors in logs
manager.on('log', (event) => {
if (event.type === 'error') {
console.error(`ClamAV Error: ${event.message}`);
}
});
// Using ClamAvService
const service = new ClamAvService();
const scanResult = await service.scanString('Some suspicious string...');
console.log(`Infection Status: ${scanResult.isInfected ? 'Infected' : 'Clean'}`);
if (scanResult.isInfected) {
console.log(`Reason: ${scanResult.reason}`);
@ -115,6 +172,52 @@ The tests include creating and utilizing a `ClamAvService` instance and attempts
### Advanced Usage and Integration
Beyond scanning strings and buffers, you can implement additional advanced use cases based on your specific application needs, such as integrating into web services or automating file scans in cloud environments. Consider building upon provided functionalities and adapting them to meet the requirements of your application architecture.
#### Container Configuration
The `ClamAVManager` supports customizing the Docker container:
```typescript
const manager = new ClamAVManager();
// Container properties are configurable
console.log(manager.containerName); // 'clamav-daemon'
console.log(manager.port); // 3310
```
#### Log Management
Access and process ClamAV logs:
```typescript
const manager = new ClamAVManager();
// Get all logs
const logs = manager.getLogs();
// Filter logs by type
const errorLogs = logs.filter(log => log.type === 'error');
const updateLogs = logs.filter(log => log.type === 'update');
```
#### Health Checks
Monitor ClamAV service health:
```typescript
const manager = new ClamAVManager();
// Service automatically checks readiness during initialization
await manager.startContainer(); // Includes readiness checks
// Get database status
const dbInfo = await manager.getDatabaseInfo();
console.log('Database Version:', dbInfo);
```
You can build upon these functionalities to implement advanced use cases such as:
- Automated virus scanning in CI/CD pipelines
- Real-time file monitoring in web applications
- Cloud-based malware detection services
- Integration with security information and event management (SIEM) systems
With the help of Node.js worker threads or external task queues like RabbitMQ, you can distribute scanning tasks efficiently within high-traffic environments.

View File

@ -1,7 +1,9 @@
import { expect, tap } from '../ts/plugins.js';
import { type ClamAVLogEvent, ClamAVManager } from '../ts/classes.clamav.manager.js';
import type { ClamAVLogEvent } from '../ts/classes.clamav.manager.js';
import { setupClamAV, cleanupClamAV, getManager } from './helpers/clamav.helper.js';
type ClamAVManager = Awaited<ReturnType<typeof setupClamAV>>;
let manager: ClamAVManager;
tap.test('setup', async () => {
@ -10,31 +12,47 @@ tap.test('setup', async () => {
});
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));
// Create a promise that resolves when we receive a log
const logPromise = new Promise<void>((resolve) => {
// First check if we already have logs
const existingLogs = manager.getLogs();
if (existingLogs.length > 0) {
console.log('[Test] Found existing logs:', existingLogs.map(log => `${log.type}: ${log.message}`).join('\n'));
resolve();
return;
}
expect(logReceived).toBeTruthy('No logs received within timeout period');
// If no existing logs, wait for new ones
const handler = (event: ClamAVLogEvent) => {
console.log(`[Test] Received log event: ${event.type} - ${event.message}`);
manager.removeListener('log', handler);
resolve();
};
manager.on('log', handler);
});
// Wait for logs with timeout
const timeoutPromise = new Promise<void>((_, reject) => {
setTimeout(() => reject(new Error('Timeout waiting for logs')), 30000);
});
try {
await Promise.race([logPromise, timeoutPromise]);
} catch (error) {
console.error('Error waiting for logs:', error);
throw error;
}
console.log('Log received check passed');
// 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');
expect(dbInfo).toBeTruthy();
console.log('Database info check passed');
} catch (error) {
console.error('Error getting database info:', error);
expect.fail('Failed to get database info - container may not be fully initialized');
throw new Error('Failed to get database info - container may not be fully initialized');
}
});

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartantivirus',
version: '1.1.0',
version: '1.1.1',
description: 'A Node.js package for integrating antivirus scanning capabilities using ClamAV, allowing in-memory file and data scanning.'
}

View File

@ -11,11 +11,16 @@ export class ClamAVManager extends EventEmitter {
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
*/
@ -157,6 +162,7 @@ export class ClamAVManager extends EventEmitter {
type: this.determineLogType(line)
};
this.logs.push(event);
this.emit('log', event);
console.log(`[ClamAV ${event.type}] ${event.message}`);
});
@ -169,6 +175,7 @@ export class ClamAVManager extends EventEmitter {
type: 'error'
};
this.logs.push(event);
this.emit('log', event);
console.error(`[ClamAV error] ${event.message}`);
});