From 5d07cc93eb8701ccf6d3ae55b805537ebff588b6 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 7 Mar 2022 13:38:28 +0000 Subject: [PATCH] Add virus scanner --- VoidCat/Model/VirusScanResult.cs | 10 ++++ VoidCat/Model/VoidFile.cs | 5 ++ VoidCat/Model/VoidSettings.cs | 7 +++ .../Services/Abstractions/IFileInfoManager.cs | 4 ++ .../Services/Abstractions/IVirusScanStore.cs | 7 +++ .../Services/Abstractions/IVirusScanner.cs | 8 +++ .../Background/VirusScannerService.cs | 56 +++++++++++++++++++ VoidCat/Services/Files/FileInfoManager.cs | 11 +++- .../Services/VirusScanner/ClamAvScanner.cs | 38 +++++++++++++ .../Services/VirusScanner/VirusScanStore.cs | 14 +++++ .../VirusScanner/VirusScannerStartup.cs | 27 +++++++++ VoidCat/VoidCat.csproj | 1 + 12 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 VoidCat/Model/VirusScanResult.cs create mode 100644 VoidCat/Services/Abstractions/IVirusScanStore.cs create mode 100644 VoidCat/Services/Abstractions/IVirusScanner.cs create mode 100644 VoidCat/Services/Background/VirusScannerService.cs create mode 100644 VoidCat/Services/VirusScanner/ClamAvScanner.cs create mode 100644 VoidCat/Services/VirusScanner/VirusScanStore.cs create mode 100644 VoidCat/Services/VirusScanner/VirusScannerStartup.cs diff --git a/VoidCat/Model/VirusScanResult.cs b/VoidCat/Model/VirusScanResult.cs new file mode 100644 index 0000000..f609c23 --- /dev/null +++ b/VoidCat/Model/VirusScanResult.cs @@ -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 VirusNames { get; init; } = new(); +} \ No newline at end of file diff --git a/VoidCat/Model/VoidFile.cs b/VoidCat/Model/VoidFile.cs index c62729c..c7f0b09 100644 --- a/VoidCat/Model/VoidFile.cs +++ b/VoidCat/Model/VoidFile.cs @@ -30,6 +30,11 @@ namespace VoidCat.Model /// Traffic stats for this file /// public Bandwidth? Bandwidth { get; init; } + + /// + /// Virus scanner results + /// + public VirusScanResult? VirusScan { get; init; } } public sealed record PublicVoidFile : VoidFile diff --git a/VoidCat/Model/VoidSettings.cs b/VoidCat/Model/VoidSettings.cs index 7a40e48..49c2c81 100644 --- a/VoidCat/Model/VoidSettings.cs +++ b/VoidCat/Model/VoidSettings.cs @@ -19,6 +19,8 @@ namespace VoidCat.Model public List 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; } + } } diff --git a/VoidCat/Services/Abstractions/IFileInfoManager.cs b/VoidCat/Services/Abstractions/IFileInfoManager.cs index 868aa49..e2f5be1 100644 --- a/VoidCat/Services/Abstractions/IFileInfoManager.cs +++ b/VoidCat/Services/Abstractions/IFileInfoManager.cs @@ -2,6 +2,10 @@ using VoidCat.Model; namespace VoidCat.Services.Abstractions; +/// +/// Main interface for getting file info to serve to clients. +/// This interface should wrap all stores and return the combined result +/// public interface IFileInfoManager { ValueTask Get(Guid id); diff --git a/VoidCat/Services/Abstractions/IVirusScanStore.cs b/VoidCat/Services/Abstractions/IVirusScanStore.cs new file mode 100644 index 0000000..f08cdae --- /dev/null +++ b/VoidCat/Services/Abstractions/IVirusScanStore.cs @@ -0,0 +1,7 @@ +using VoidCat.Model; + +namespace VoidCat.Services.Abstractions; + +public interface IVirusScanStore : IBasicStore +{ +} \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IVirusScanner.cs b/VoidCat/Services/Abstractions/IVirusScanner.cs new file mode 100644 index 0000000..2d4ca05 --- /dev/null +++ b/VoidCat/Services/Abstractions/IVirusScanner.cs @@ -0,0 +1,8 @@ +using VoidCat.Model; + +namespace VoidCat.Services.Abstractions; + +public interface IVirusScanner +{ + ValueTask ScanFile(Guid id, CancellationToken cts); +} \ No newline at end of file diff --git a/VoidCat/Services/Background/VirusScannerService.cs b/VoidCat/Services/Background/VirusScannerService.cs new file mode 100644 index 0000000..c9d0a6a --- /dev/null +++ b/VoidCat/Services/Background/VirusScannerService.cs @@ -0,0 +1,56 @@ +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.Background; + +public class VirusScannerService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IVirusScanner _scanner; + private readonly IFileStore _fileStore; + private readonly IVirusScanStore _scanStore; + + public VirusScannerService(ILogger 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); + } + } +} \ No newline at end of file diff --git a/VoidCat/Services/Files/FileInfoManager.cs b/VoidCat/Services/Files/FileInfoManager.cs index 50411b7..67858a3 100644 --- a/VoidCat/Services/Files/FileInfoManager.cs +++ b/VoidCat/Services/Files/FileInfoManager.cs @@ -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 Get(Guid id) @@ -24,7 +26,8 @@ public class FileInfoManager : IFileInfoManager var meta = _metadataStore.Get(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); } } diff --git a/VoidCat/Services/VirusScanner/ClamAvScanner.cs b/VoidCat/Services/VirusScanner/ClamAvScanner.cs new file mode 100644 index 0000000..7b78bbb --- /dev/null +++ b/VoidCat/Services/VirusScanner/ClamAvScanner.cs @@ -0,0 +1,38 @@ +using nClam; +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.VirusScanner; + +public class ClamAvScanner : IVirusScanner +{ + private readonly ILogger _logger; + private readonly IClamClient _clam; + private readonly IFileStore _store; + + public ClamAvScanner(ILogger logger, IClamClient clam, IFileStore store) + { + _logger = logger; + _clam = clam; + _store = store; + } + + public async ValueTask ScanFile(Guid id, CancellationToken cts) + { + _logger.LogInformation("Starting scan of {Filename}", id); + + await using var fs = await _store.Open(new(id, Enumerable.Empty()), 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() + }; + } +} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusScanStore.cs b/VoidCat/Services/VirusScanner/VirusScanStore.cs new file mode 100644 index 0000000..f230bd6 --- /dev/null +++ b/VoidCat/Services/VirusScanner/VirusScanStore.cs @@ -0,0 +1,14 @@ +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.VirusScanner; + +public class VirusScanStore : BasicCacheStore, IVirusScanStore +{ + public VirusScanStore(ICache cache) : base(cache) + { + } + + public override string MapKey(Guid id) + => $"virus-scan:{id}"; +} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusScannerStartup.cs b/VoidCat/Services/VirusScanner/VirusScannerStartup.cs new file mode 100644 index 0000000..9b80923 --- /dev/null +++ b/VoidCat/Services/VirusScanner/VirusScannerStartup.cs @@ -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(); + + var avSettings = settings.VirusScanner; + if (avSettings != default) + { + services.AddHostedService(); + + // load ClamAV scanner + if (avSettings.ClamAV != default) + { + services.AddTransient((_) => + new ClamClient(avSettings.ClamAV.Host, avSettings.ClamAV.Port)); + services.AddTransient(); + } + } + } +} \ No newline at end of file diff --git a/VoidCat/VoidCat.csproj b/VoidCat/VoidCat.csproj index 67876dc..55c85af 100644 --- a/VoidCat/VoidCat.csproj +++ b/VoidCat/VoidCat.csproj @@ -17,6 +17,7 @@ +