nClam is a lightweight .NET library for communicating with a ClamAV server (clamd) to perform virus scanning. It provides a simple, async API that encapsulates the communication protocol and parses scan results.
- ✅ Full async/await support
- ✅ Multiple scanning methods (server-side, stream-based, multithreaded, all-match)
- ✅ Comprehensive error handling and input validation
- ✅ Supports .NET 8.0, .NET 6.0, .NET Standard 2.0, and .NET Framework 4.5
- ✅ Well-documented API with XML documentation
- ✅ Thread-safe operations
- ✅ Server statistics and health monitoring
- ✅ Database reload without server restart
- ✅ Configurable connection and read/write timeouts
- ✅ Memory-efficient streaming with ArrayPool
- ✅ IDisposable and IAsyncDisposable support for proper resource management
- ✅ Enhanced error handling with specific exception types
- ✅ Optimized parsing with modern C# features
- ✅ Connection error handling with detailed exceptions
- ClamAV Server (clamd): A running ClamAV daemon server
- Windows: ClamAV Win32 ports
- Linux: Available through package managers (
apt-get install clamav-daemonoryum install clamav) - macOS:
brew install clamav
Install the package via NuGet:
Install-Package nClamOr via .NET CLI:
dotnet add package nClamusing System;
using System.Linq;
using System.Threading.Tasks;
using nClam;
class Program
{
static async Task Main(string[] args)
{
var client = new ClamClient("localhost", 3310);
// Check server connectivity
var isConnected = await client.PingAsync();
if (!isConnected)
{
Console.WriteLine("Failed to connect to ClamAV server");
return;
}
// Scan a file
var result = await client.SendAndScanFileAsync("C:\\test.txt");
switch (result.Result)
{
case ClamScanResults.Clean:
Console.WriteLine("File is clean!");
break;
case ClamScanResults.VirusDetected:
Console.WriteLine("Virus detected!");
foreach (var infectedFile in result.InfectedFiles)
{
Console.WriteLine($"File: {infectedFile.FileName}");
Console.WriteLine($"Virus: {infectedFile.VirusName}");
}
break;
case ClamScanResults.Error:
Console.WriteLine($"Error: {result.RawResult}");
break;
}
}
}// Default port is 3310
var client = new ClamClient("localhost");
// Or specify a custom port
var client = new ClamClient("clamav.example.com", 3310);// Configure chunk size for streaming (default: 128KB)
client.MaxChunkSize = 131072;
// Configure maximum stream size (default: 25MB)
client.MaxStreamSize = 26214400;
// Configure timeouts (default: 30s connection, 5min read/write)
client.ConnectionTimeout = 30000; // 30 seconds
client.ReadWriteTimeout = 300000; // 5 minutes// Check if server is reachable
bool isAlive = await client.PingAsync();
// Get ClamAV server version
string version = await client.GetVersionAsync();Scans a file that already exists on the ClamAV server's filesystem:
var result = await client.ScanFileOnServerAsync("/path/to/file.txt");Sends file data to the server for scanning (recommended for remote files):
// From file path
var result = await client.SendAndScanFileAsync("C:\\test.txt");
// From byte array
byte[] fileData = File.ReadAllBytes("test.txt");
var result = await client.SendAndScanFileAsync(fileData);
// From stream
using (var stream = File.OpenRead("test.txt"))
{
var result = await client.SendAndScanFileAsync(stream);
}Uses multiple threads on the server for faster scanning of large directories:
var result = await client.ScanFileOnServerMultithreadedAsync("/path/to/directory");Continues scanning after the first match is found:
var result = await client.ContinueScanFileOnServerAsync("/path/to/directory");Scans and reports all matches found in a file:
var result = await client.AllMatchScanFileOnServerAsync("/path/to/file.zip");
// Returns all viruses found, not just the first oneRetrieve comprehensive statistics from the ClamAV server:
var stats = await client.GetStatsAsync();
Console.WriteLine($"Threads: {stats.Threads}/{stats.MaxThreads}");
Console.WriteLine($"Scanned: {stats.ScannedItems}, Found: {stats.FoundItems}");
Console.WriteLine($"Virus Signatures: {stats.VirusSignatures}");
Console.WriteLine($"Memory Usage: {stats.MemoryUsage} bytes");Reload the virus database without restarting the server:
var success = await client.ReloadDatabaseAsync();
if (success)
{
Console.WriteLine("Database reloaded successfully");
}All methods support cancellation tokens:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var result = await client.SendAndScanFileAsync(
"largefile.zip",
cts.Token
);var result = await client.SendAndScanFileAsync("file.txt");
// Check scan status
if (result.Result == ClamScanResults.Clean)
{
Console.WriteLine("File is safe");
}
else if (result.Result == ClamScanResults.VirusDetected)
{
// Access infected files
foreach (var infected in result.InfectedFiles)
{
Console.WriteLine($"Virus: {infected.VirusName} in {infected.FileName}");
}
}
else if (result.Result == ClamScanResults.Error)
{
// Check raw error message
Console.WriteLine($"Error: {result.RawResult}");
}
// Access raw response
string rawResponse = result.RawResult;using System;
using System.IO;
using System.Threading.Tasks;
using nClam;
class VirusScanner
{
private readonly ClamClient _client;
public VirusScanner(string server, int port = 3310)
{
_client = new ClamClient(server, port);
}
public async Task<bool> ScanFileAsync(string filePath)
{
try
{
// Verify server is available
if (!await _client.PingAsync())
{
Console.WriteLine("ClamAV server is not available");
return false;
}
// Scan the file
var result = await _client.SendAndScanFileAsync(filePath);
if (result.Result == ClamScanResults.VirusDetected)
{
Console.WriteLine($"⚠️ Virus detected in {filePath}");
foreach (var infected in result.InfectedFiles)
{
Console.WriteLine($" - {infected.VirusName}");
}
return false;
}
else if (result.Result == ClamScanResults.Clean)
{
Console.WriteLine($"✓ File is clean: {filePath}");
return true;
}
else
{
Console.WriteLine($"✗ Scan error: {result.RawResult}");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error scanning file: {ex.Message}");
return false;
}
}
}
// Usage
class Program
{
static async Task Main(string[] args)
{
var scanner = new VirusScanner("localhost", 3310);
await scanner.ScanFileAsync("test.txt");
}
}The library provides comprehensive error handling with specific exception types:
try
{
var client = new ClamClient(null, 3310); // Throws ArgumentException
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
try
{
await client.SendAndScanFileAsync((byte[])null); // Throws ArgumentNullException
}
catch (ArgumentNullException ex)
{
Console.WriteLine(ex.Message);
}try
{
var result = await client.ScanFileOnServerAsync("/path/to/file");
}
catch (ClamConnectionException ex)
{
Console.WriteLine($"Failed to connect to {ex.Server}:{ex.Port}");
Console.WriteLine($"Inner exception: {ex.InnerException?.Message}");
}
catch (TimeoutException ex)
{
Console.WriteLine($"Connection timed out: {ex.Message}");
}try
{
await client.SendAndScanFileAsync(largeStream);
}
catch (MaxStreamSizeExceededException ex)
{
Console.WriteLine($"File exceeds maximum size: {ex.Message}");
}ClamException- Base exception for all ClamAV-related errorsClamConnectionException- Thrown when connection to server failsMaxStreamSizeExceededException- Thrown when file exceeds maximum stream sizeTimeoutException- Thrown when connection or operation times out
- .NET 8.0: Latest LTS version with best performance
- .NET 6.0: LTS version with modern features
- .NET Standard 2.0: Compatible with .NET Core 2.0+, .NET Framework 4.6.1+, and more
- .NET Framework 4.5: For legacy applications
Licensed under the Apache License 2.0. See LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request. We appreciate contributions that:
- Fix bugs
- Add new features
- Improve documentation
- Add tests
- Improve code quality