109 lines
4.1 KiB
C#
109 lines
4.1 KiB
C#
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, streamMaxLength: 4);
|
|
var exception = await Assert.ThrowsAsync<AssetSecurityScannerException>(() =>
|
|
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<AssetSecurityScannerException>(() =>
|
|
CreateScanner(port).ScanAsync(new MemoryStream([1]), 1));
|
|
Assert.Equal("clamav_unavailable", exception.Code);
|
|
}
|
|
|
|
private static ClamAvAssetSecurityScanner CreateScanner(int port, long streamMaxLength = 1024 * 1024) =>
|
|
new(Options.Create(new ClamAvOptions
|
|
{
|
|
Host = IPAddress.Loopback.ToString(),
|
|
Port = port,
|
|
TimeoutSeconds = 2,
|
|
ChunkBytes = 1024,
|
|
StreamMaxLength = streamMaxLength
|
|
}));
|
|
|
|
private static Task ServeScanAsync(TcpListener listener, string response) => 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<byte[]> ReadExactAsync(Stream stream, int length)
|
|
{
|
|
var buffer = new byte[length];
|
|
await stream.ReadExactlyAsync(buffer);
|
|
return buffer;
|
|
}
|
|
}
|