Files
register-referentie/services/domain/Big.Infrastructure/ClamdDocumentScanner.cs
T
notandClaude Opus 5.5 d2c035679b feat(domain): scan uploads with clamd over INSTREAM; 422 refused, 503 scanner down (refs #192)
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>
2026-10-02 09:53:23 +02:00

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: &lt;signature&gt; 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;
}
}