Verified against a real clamd 1.4.6: clean → Clean, EICAR → Infected, closed port → Unavailable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
49 lines
2.1 KiB
C#
49 lines
2.1 KiB
C#
using System.Buffers.Binary;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using Big.Application;
|
|
|
|
namespace Big.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// Scans a document with clamd over its INSTREAM protocol (S-29, ADR-0036): <c>zINSTREAM\0</c>, the
|
|
/// document as one big-endian length-prefixed chunk, a zero-length terminator, then one reply —
|
|
/// <c>stream: OK</c> or <c>stream: <signature> FOUND</c>. Anything else (an ERROR reply, a refused
|
|
/// connection, a timeout) is <see cref="ScanVerdict.Unavailable"/>, so the caller fails closed.
|
|
/// </summary>
|
|
public sealed class ClamdDocumentScanner(ClamAvOptions options) : IDocumentScanner
|
|
{
|
|
public async Task<ScanVerdict> ScanAsync(byte[] content, CancellationToken ct = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
timeout.CancelAfter(options.Timeout);
|
|
|
|
string reply;
|
|
try
|
|
{
|
|
using var client = new TcpClient();
|
|
await client.ConnectAsync(options.Host, options.Port, timeout.Token);
|
|
var stream = client.GetStream();
|
|
|
|
var length = new byte[4];
|
|
BinaryPrimitives.WriteInt32BigEndian(length, content.Length);
|
|
await stream.WriteAsync("zINSTREAM\0"u8.ToArray(), timeout.Token);
|
|
await stream.WriteAsync(length, timeout.Token);
|
|
await stream.WriteAsync(content, timeout.Token);
|
|
await stream.WriteAsync(new byte[4], timeout.Token);
|
|
|
|
using var reader = new StreamReader(stream, Encoding.ASCII);
|
|
reply = (await reader.ReadToEndAsync(timeout.Token)).TrimEnd('\0', '\n');
|
|
}
|
|
catch (Exception e) when (e is SocketException or IOException
|
|
|| (e is OperationCanceledException && !ct.IsCancellationRequested))
|
|
{
|
|
return ScanVerdict.Unavailable;
|
|
}
|
|
|
|
if (reply == "stream: OK") return ScanVerdict.Clean;
|
|
return reply.EndsWith(" FOUND", StringComparison.Ordinal) ? ScanVerdict.Infected : ScanVerdict.Unavailable;
|
|
}
|
|
}
|