Add virus scanner

This commit is contained in:
Kieran 2022-03-07 13:38:28 +00:00
parent 000d7bac92
commit 5d07cc93eb
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
12 changed files with 185 additions and 3 deletions

View File

@ -0,0 +1,10 @@
namespace VoidCat.Model;
public sealed class VirusScanResult
{
public DateTimeOffset ScanTime { get; init; } = DateTimeOffset.UtcNow;
public bool IsVirus { get; init; }
public List<string> VirusNames { get; init; } = new();
}

View File

@ -30,6 +30,11 @@ namespace VoidCat.Model
/// Traffic stats for this file
/// </summary>
public Bandwidth? Bandwidth { get; init; }
/// <summary>
/// Virus scanner results
/// </summary>
public VirusScanResult? VirusScan { get; init; }
}
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>

View File

@ -19,6 +19,8 @@ namespace VoidCat.Model
public List<Uri> CorsOrigins { get; init; } = new();
public CloudStorageSettings? CloudStorage { get; init; }
public VirusScannerSettings? VirusScanner { get; init; }
}
public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword);
@ -46,4 +48,9 @@ namespace VoidCat.Model
public string? Region { get; init; }
public string? BucketName { get; init; } = "void-cat";
}
public sealed record VirusScannerSettings
{
public Uri? ClamAV { get; init; }
}
}

View File

@ -2,6 +2,10 @@ using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Main interface for getting file info to serve to clients.
/// This interface should wrap all stores and return the combined result
/// </summary>
public interface IFileInfoManager
{
ValueTask<PublicVoidFile?> Get(Guid id);

View File

@ -0,0 +1,7 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
public interface IVirusScanStore : IBasicStore<VirusScanResult>
{
}

View File

@ -0,0 +1,8 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
public interface IVirusScanner
{
ValueTask<VirusScanResult> ScanFile(Guid id, CancellationToken cts);
}

View File

@ -0,0 +1,56 @@
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Background;
public class VirusScannerService : BackgroundService
{
private readonly ILogger<VirusScannerService> _logger;
private readonly IVirusScanner _scanner;
private readonly IFileStore _fileStore;
private readonly IVirusScanStore _scanStore;
public VirusScannerService(ILogger<VirusScannerService> logger, IVirusScanner scanner, IVirusScanStore scanStore,
IFileStore fileStore)
{
_scanner = scanner;
_logger = logger;
_scanStore = scanStore;
_fileStore = fileStore;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Virus scanner background service starting..");
while (!stoppingToken.IsCancellationRequested)
{
var page = 0;
while (true)
{
var files = await _fileStore.ListFiles(new(page, 10));
if (files.Pages < page) break;
page++;
await foreach (var file in files.Results.WithCancellation(stoppingToken))
{
// check for scans
var scan = await _scanStore.Get(file.Id);
if (scan == default)
{
try
{
var result = await _scanner.ScanFile(file.Id, stoppingToken);
await _scanStore.Set(file.Id, result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to scan file {Id} error={Message}", file.Id, ex.Message);
}
}
}
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}

View File

@ -9,14 +9,16 @@ public class FileInfoManager : IFileInfoManager
private readonly IPaywallStore _paywallStore;
private readonly IStatsReporter _statsReporter;
private readonly IUserStore _userStore;
private readonly IVirusScanStore _virusScanStore;
public FileInfoManager(IFileMetadataStore metadataStore, IPaywallStore paywallStore, IStatsReporter statsReporter,
IUserStore userStore)
IUserStore userStore, IVirusScanStore virusScanStore)
{
_metadataStore = metadataStore;
_paywallStore = paywallStore;
_statsReporter = statsReporter;
_userStore = userStore;
_virusScanStore = virusScanStore;
}
public async ValueTask<PublicVoidFile?> Get(Guid id)
@ -24,7 +26,8 @@ public class FileInfoManager : IFileInfoManager
var meta = _metadataStore.Get<VoidFileMeta>(id);
var paywall = _paywallStore.Get(id);
var bandwidth = _statsReporter.GetBandwidth(id);
await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask());
var virusScan = _virusScanStore.Get(id);
await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask(), virusScan.AsTask());
if (meta.Result == default) return default;
@ -37,7 +40,8 @@ public class FileInfoManager : IFileInfoManager
Metadata = meta.Result,
Paywall = paywall.Result,
Bandwidth = bandwidth.Result,
Uploader = user?.Flags.HasFlag(VoidUserFlags.PublicProfile) == true ? user : null
Uploader = user?.Flags.HasFlag(VoidUserFlags.PublicProfile) == true ? user : null,
VirusScan = virusScan.Result
};
}
@ -46,5 +50,6 @@ public class FileInfoManager : IFileInfoManager
await _metadataStore.Delete(id);
await _paywallStore.Delete(id);
await _statsReporter.Delete(id);
await _virusScanStore.Delete(id);
}
}

View File

@ -0,0 +1,38 @@
using nClam;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.VirusScanner;
public class ClamAvScanner : IVirusScanner
{
private readonly ILogger<ClamAvScanner> _logger;
private readonly IClamClient _clam;
private readonly IFileStore _store;
public ClamAvScanner(ILogger<ClamAvScanner> logger, IClamClient clam, IFileStore store)
{
_logger = logger;
_clam = clam;
_store = store;
}
public async ValueTask<VirusScanResult> ScanFile(Guid id, CancellationToken cts)
{
_logger.LogInformation("Starting scan of {Filename}", id);
await using var fs = await _store.Open(new(id, Enumerable.Empty<RangeRequest>()), cts);
var result = await _clam.SendAndScanFileAsync(fs, cts);
if (result.Result == ClamScanResults.Error)
{
_logger.LogError("Failed to scan file {File}", id);
}
return new()
{
IsVirus = result.Result == ClamScanResults.VirusDetected,
VirusNames = result.InfectedFiles?.Select(a => a.VirusName.Trim()).ToList() ?? new()
};
}
}

View File

@ -0,0 +1,14 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.VirusScanner;
public class VirusScanStore : BasicCacheStore<VirusScanResult>, IVirusScanStore
{
public VirusScanStore(ICache cache) : base(cache)
{
}
public override string MapKey(Guid id)
=> $"virus-scan:{id}";
}

View File

@ -0,0 +1,27 @@
using nClam;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.VirusScanner;
public static class VirusScannerStartup
{
public static void AddVirusScanner(this IServiceCollection services, VoidSettings settings)
{
services.AddTransient<IVirusScanStore, VirusScanStore>();
var avSettings = settings.VirusScanner;
if (avSettings != default)
{
services.AddHostedService<Background.VirusScannerService>();
// load ClamAV scanner
if (avSettings.ClamAV != default)
{
services.AddTransient<IClamClient>((_) =>
new ClamClient(avSettings.ClamAV.Host, avSettings.ClamAV.Port));
services.AddTransient<IVirusScanner, ClamAvScanner>();
}
}
}
}

View File

@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="NBitcoin" Version="6.0.19" />
<PackageReference Include="nClam" Version="7.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" />
<PackageReference Include="Seq.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.5.27-prerelease" />