using System.Buffers.Binary; using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Options; using Tiku.Application.Assets; using Tiku.Infrastructure.Assets; namespace Tiku.UnitTests.Assets; public sealed class ClamAvAssetSecurityScannerTests { [Theory] [InlineData("stream: OK", AssetSecurityScanVerdict.Clean, null)] [InlineData("stream: Eicar-Signature FOUND", AssetSecurityScanVerdict.Infected, "Eicar-Signature")] public async Task Instream_protocol_parses_clean_and_infected_responses( string response, AssetSecurityScanVerdict expectedVerdict, string? expectedSignature) { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; var server = ServeScanAsync(listener, response); var scanner = CreateScanner(port); var payload = Encoding.UTF8.GetBytes("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"); var result = await scanner.ScanAsync(new MemoryStream(payload), payload.Length); await server; Assert.Equal(expectedVerdict, result.Verdict); Assert.Equal(expectedSignature, result.Signature); Assert.Equal(payload.Length, result.BytesScanned); } [Fact] public async Task Ping_requires_pong() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; var server = Task.Run(async () => { using var client = await listener.AcceptTcpClientAsync(); await using var stream = client.GetStream(); Assert.Equal("zPING\0", Encoding.ASCII.GetString(await ReadExactAsync(stream, 6))); await stream.WriteAsync("PONG\0"u8.ToArray()); listener.Stop(); }); Assert.True(await CreateScanner(port).CheckHealthAsync()); await server; } [Fact] public async Task Oversized_stream_is_rejected_before_connecting() { var scanner = CreateScanner(1, 4); var exception = await Assert.ThrowsAsync(() => scanner.ScanAsync(new MemoryStream(new byte[5]), 5)); Assert.Equal("clamav_stream_too_large", exception.Code); } [Fact] public async Task Unavailable_daemon_is_reported_as_transient_scanner_failure() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); var exception = await Assert.ThrowsAsync(() => CreateScanner(port).ScanAsync(new MemoryStream([1]), 1)); Assert.Equal("clamav_unavailable", exception.Code); } private static ClamAvAssetSecurityScanner CreateScanner(int port, long streamMaxLength = 1024 * 1024) { return new ClamAvAssetSecurityScanner(Options.Create(new ClamAvOptions { Host = IPAddress.Loopback.ToString(), Port = port, TimeoutSeconds = 2, ChunkBytes = 1024, StreamMaxLength = streamMaxLength })); } private static Task ServeScanAsync(TcpListener listener, string response) { return Task.Run(async () => { using var client = await listener.AcceptTcpClientAsync(); await using var stream = client.GetStream(); Assert.Equal("zINSTREAM\0", Encoding.ASCII.GetString(await ReadExactAsync(stream, 10))); while (true) { var lengthBytes = await ReadExactAsync(stream, 4); var length = BinaryPrimitives.ReadUInt32BigEndian(lengthBytes); if (length == 0) break; _ = await ReadExactAsync(stream, checked((int)length)); } await stream.WriteAsync(Encoding.UTF8.GetBytes(response + "\0")); listener.Stop(); }); } private static async Task ReadExactAsync(Stream stream, int length) { var buffer = new byte[length]; await stream.ReadExactlyAsync(buffer); return buffer; } }