From b2902c128a2a15bca34ab1ee61200eebfe83d61a Mon Sep 17 00:00:00 2001 From: Volodymyr Smirnov Date: Thu, 29 Oct 2020 12:17:09 +0200 Subject: [PATCH 1/2] unit tests for API (99% coverage) --- .../Attributes/IsHttpUrlAttribute.cs | 5 +- .../Controllers/DownloadController.cs | 6 +- .../Controllers/QueueController.cs | 10 +- .../Controllers/ScanResultsController.cs | 6 +- .../Extensions/ServiceCollectionExtensions.cs | 21 ++-- MalwareMultiScan.Api/Program.cs | 2 + .../ReceiverHostedService.cs | 31 ++--- .../ScanBackendService.cs | 28 +---- .../ScanResultService.cs | 84 ++++--------- .../Interfaces/IReceiverHostedService.cs | 9 ++ .../Interfaces/IScanBackendService.cs | 13 ++ .../Services/Interfaces/IScanResultService.cs | 22 ++++ MalwareMultiScan.Api/Startup.cs | 11 +- MalwareMultiScan.Tests/AttributesTests.cs | 57 +++++++++ MalwareMultiScan.Tests/ControllersTests.cs | 103 +++++++++++++++ .../MalwareMultiScan.Tests.csproj | 22 ++++ .../ReceiverHostedServiceTests.cs | 77 ++++++++++++ .../ScanBackendServiceTests.cs | 63 ++++++++++ .../ScanResultServiceTests.cs | 119 ++++++++++++++++++ MalwareMultiScan.sln | 6 + 20 files changed, 565 insertions(+), 130 deletions(-) rename MalwareMultiScan.Api/Services/{ => Implementations}/ReceiverHostedService.cs (70%) rename MalwareMultiScan.Api/Services/{ => Implementations}/ScanBackendService.cs (66%) rename MalwareMultiScan.Api/Services/{ => Implementations}/ScanResultService.cs (53%) create mode 100644 MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs create mode 100644 MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs create mode 100644 MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs create mode 100644 MalwareMultiScan.Tests/AttributesTests.cs create mode 100644 MalwareMultiScan.Tests/ControllersTests.cs create mode 100644 MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj create mode 100644 MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs create mode 100644 MalwareMultiScan.Tests/ScanBackendServiceTests.cs create mode 100644 MalwareMultiScan.Tests/ScanResultServiceTests.cs diff --git a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs index cda488e..58e357b 100644 --- a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs +++ b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs @@ -6,14 +6,15 @@ namespace MalwareMultiScan.Api.Attributes /// /// Validate URI to be an absolute http(s) URL. /// - internal class IsHttpUrlAttribute : ValidationAttribute + public class IsHttpUrlAttribute : ValidationAttribute { /// protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (!Uri.TryCreate((string)value, UriKind.Absolute, out var uri) || !uri.IsAbsoluteUri - || uri.Scheme != "http" && uri.Scheme != "https") + || uri.Scheme.ToLowerInvariant() != "http" + && uri.Scheme.ToLowerInvariant() != "https") return new ValidationResult("Only absolute http(s) URLs are supported"); return ValidationResult.Success; diff --git a/MalwareMultiScan.Api/Controllers/DownloadController.cs b/MalwareMultiScan.Api/Controllers/DownloadController.cs index b2edfe3..212b357 100644 --- a/MalwareMultiScan.Api/Controllers/DownloadController.cs +++ b/MalwareMultiScan.Api/Controllers/DownloadController.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -10,9 +10,9 @@ namespace MalwareMultiScan.Api.Controllers [Produces("application/octet-stream")] public class DownloadController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public DownloadController(ScanResultService scanResultService) + public DownloadController(IScanResultService scanResultService) { _scanResultService = scanResultService; } diff --git a/MalwareMultiScan.Api/Controllers/QueueController.cs b/MalwareMultiScan.Api/Controllers/QueueController.cs index 5122797..a68aa7c 100644 --- a/MalwareMultiScan.Api/Controllers/QueueController.cs +++ b/MalwareMultiScan.Api/Controllers/QueueController.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using MalwareMultiScan.Api.Attributes; using MalwareMultiScan.Api.Data.Models; -using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -13,9 +13,9 @@ namespace MalwareMultiScan.Api.Controllers [Produces("application/json")] public class QueueController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public QueueController(ScanResultService scanResultService) + public QueueController(IScanResultService scanResultService) { _scanResultService = scanResultService; } @@ -31,10 +31,10 @@ namespace MalwareMultiScan.Api.Controllers string storedFileId; await using (var uploadFileStream = file.OpenReadStream()) - storedFileId = await _scanResultService.StoreFile(file.Name, uploadFileStream); + storedFileId = await _scanResultService.StoreFile(file.FileName, uploadFileStream); await _scanResultService.QueueUrlScan(result, Url.Action("Index", "Download", new {id = storedFileId}, - Request.Scheme, Request.Host.Value)); + Request?.Scheme, Request?.Host.Value)); return CreatedAtAction("Index", "ScanResults", new {id = result.Id}, result); } diff --git a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs index b67e1cf..3752062 100644 --- a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs +++ b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using MalwareMultiScan.Api.Data.Models; using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -11,9 +13,9 @@ namespace MalwareMultiScan.Api.Controllers [Produces("application/json")] public class ScanResultsController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public ScanResultsController(ScanResultService scanResultService) + public ScanResultsController(IScanResultService scanResultService) { _scanResultService = scanResultService; } diff --git a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs index feb6a21..4e58e0c 100644 --- a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs +++ b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; using EasyNetQ; using Microsoft.AspNetCore.Builder; @@ -5,30 +6,30 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; +using MongoDB.Driver.GridFS; namespace MalwareMultiScan.Api.Extensions { + [ExcludeFromCodeCoverage] internal static class ServiceCollectionExtensions { - public static void AddMongoDb(this IServiceCollection services, IConfiguration configuration) + internal static void AddMongoDb(this IServiceCollection services, IConfiguration configuration) { - services.AddSingleton( - serviceProvider => - { - var db = new MongoClient(configuration.GetConnectionString("Mongo")); + var client = new MongoClient(configuration.GetConnectionString("Mongo")); + var db = client.GetDatabase(configuration.GetValue("DatabaseName")); - return db.GetDatabase( - configuration.GetValue("DatabaseName")); - }); + services.AddSingleton(client); + services.AddSingleton(db); + services.AddSingleton(new GridFSBucket(db)); } - public static void AddRabbitMq(this IServiceCollection services, IConfiguration configuration) + internal static void AddRabbitMq(this IServiceCollection services, IConfiguration configuration) { services.AddSingleton(x => RabbitHutch.CreateBus(configuration.GetConnectionString("RabbitMQ"))); } - public static void AddDockerForwardedHeadersOptions(this IServiceCollection services) + internal static void AddDockerForwardedHeadersOptions(this IServiceCollection services) { services.Configure(options => { diff --git a/MalwareMultiScan.Api/Program.cs b/MalwareMultiScan.Api/Program.cs index acaf861..5211d45 100644 --- a/MalwareMultiScan.Api/Program.cs +++ b/MalwareMultiScan.Api/Program.cs @@ -1,8 +1,10 @@ +using EasyNetQ.LightInject; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace MalwareMultiScan.Api { + [ExcludeFromCodeCoverage] public static class Program { public static void Main(string[] args) diff --git a/MalwareMultiScan.Api/Services/ReceiverHostedService.cs b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs similarity index 70% rename from MalwareMultiScan.Api/Services/ReceiverHostedService.cs rename to MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs index af4b2ed..1fa43c0 100644 --- a/MalwareMultiScan.Api/Services/ReceiverHostedService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs @@ -1,31 +1,21 @@ using System.Threading; using System.Threading.Tasks; using EasyNetQ; +using MalwareMultiScan.Api.Services.Interfaces; using MalwareMultiScan.Backends.Messages; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace MalwareMultiScan.Api.Services +namespace MalwareMultiScan.Api.Services.Implementations { - /// - /// Receiver hosted service. - /// - public class ReceiverHostedService : IHostedService + public class ReceiverHostedService : IReceiverHostedService { private readonly IBus _bus; private readonly IConfiguration _configuration; private readonly ILogger _logger; - private readonly ScanResultService _scanResultService; - - /// - /// Create receiver hosted service. - /// - /// Service bus. - /// Configuration. - /// Scan result service. - /// Logger. - public ReceiverHostedService(IBus bus, IConfiguration configuration, ScanResultService scanResultService, + private readonly IScanResultService _scanResultService; + + public ReceiverHostedService(IBus bus, IConfiguration configuration, IScanResultService scanResultService, ILogger logger) { _bus = bus; @@ -33,8 +23,7 @@ namespace MalwareMultiScan.Api.Services _scanResultService = scanResultService; _logger = logger; } - - /// + public Task StartAsync(CancellationToken cancellationToken) { _bus.Receive(_configuration.GetValue("ResultsSubscriptionId"), async message => @@ -55,12 +44,10 @@ namespace MalwareMultiScan.Api.Services return Task.CompletedTask; } - - - /// + public Task StopAsync(CancellationToken cancellationToken) { - _bus.Dispose(); + _bus?.Dispose(); _logger.LogInformation( "Stopped hosted service for receiving scan results"); diff --git a/MalwareMultiScan.Api/Services/ScanBackendService.cs b/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs similarity index 66% rename from MalwareMultiScan.Api/Services/ScanBackendService.cs rename to MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs index 71ca347..83dc419 100644 --- a/MalwareMultiScan.Api/Services/ScanBackendService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs @@ -4,29 +4,20 @@ using System.Threading.Tasks; using EasyNetQ; using MalwareMultiScan.Api.Data.Configuration; using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Interfaces; using MalwareMultiScan.Backends.Messages; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace MalwareMultiScan.Api.Services +namespace MalwareMultiScan.Api.Services.Implementations { - /// - /// Scan backends service. - /// - public class ScanBackendService + public class ScanBackendService : IScanBackendService { private readonly IBus _bus; private readonly ILogger _logger; - /// - /// Create scan backend service. - /// - /// Configuration. - /// Service bus. - /// Logger. - /// Missing BackendsConfiguration YAML file. public ScanBackendService(IConfiguration configuration, IBus bus, ILogger logger) { _bus = bus; @@ -45,18 +36,9 @@ namespace MalwareMultiScan.Api.Services List = deserializer.Deserialize(configurationContent); } - - /// - /// List of available scan backends. - /// + public ScanBackend[] List { get; } - - /// - /// Queue URL scan. - /// - /// Scan result instance. - /// Scan backend. - /// File download URL. + public async Task QueueUrlScan(ScanResult result, ScanBackend backend, string fileUrl) { _logger.LogInformation( diff --git a/MalwareMultiScan.Api/Services/ScanResultService.cs b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs similarity index 53% rename from MalwareMultiScan.Api/Services/ScanResultService.cs rename to MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs index 862b573..50df75e 100644 --- a/MalwareMultiScan.Api/Services/ScanResultService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs @@ -2,40 +2,29 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Interfaces; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.GridFS; -namespace MalwareMultiScan.Api.Services +namespace MalwareMultiScan.Api.Services.Implementations { - /// - /// Scan results service. - /// - public class ScanResultService + public class ScanResultService : IScanResultService { private const string CollectionName = "ScanResults"; - private readonly GridFSBucket _bucket; + private readonly IGridFSBucket _bucket; private readonly IMongoCollection _collection; - private readonly ScanBackendService _scanBackendService; - - /// - /// Create scan result service. - /// - /// Mongo database. - /// Scan backend service. - public ScanResultService(IMongoDatabase db, ScanBackendService scanBackendService) + private readonly IScanBackendService _scanBackendService; + + public ScanResultService(IMongoDatabase db, IGridFSBucket bucket, IScanBackendService scanBackendService) { + _bucket = bucket; _scanBackendService = scanBackendService; - - _bucket = new GridFSBucket(db); + _collection = db.GetCollection(CollectionName); } - - /// - /// Create scan result. - /// - /// Scan result. + public async Task CreateScanResult() { var scanResult = new ScanResult @@ -49,12 +38,7 @@ namespace MalwareMultiScan.Api.Services return scanResult; } - - /// - /// Get scan result. - /// - /// Scan result id. - /// Scan result. + public async Task GetScanResult(string id) { var result = await _collection.FindAsync( @@ -62,16 +46,7 @@ namespace MalwareMultiScan.Api.Services return await result.FirstOrDefaultAsync(); } - - /// - /// Update scan status for the backend. - /// - /// Result id. - /// Backend id. - /// Duration of scan. - /// If the scan has been completed. - /// If the scan has been succeeded. - /// List of found threats. + public async Task UpdateScanResultForBackend(string resultId, string backendId, long duration, bool completed = false, bool succeeded = false, string[] threats = null) { @@ -85,24 +60,13 @@ namespace MalwareMultiScan.Api.Services Threats = threats ?? new string[] { } })); } - - /// - /// Queue URL scan. - /// - /// Scan result instance. - /// File URL. + public async Task QueueUrlScan(ScanResult result, string fileUrl) { foreach (var backend in _scanBackendService.List.Where(b => b.Enabled)) await _scanBackendService.QueueUrlScan(result, backend, fileUrl); } - - /// - /// Store file. - /// - /// File name. - /// File stream. - /// Stored file id. + public async Task StoreFile(string fileName, Stream fileStream) { var objectId = await _bucket.UploadFromStreamAsync( @@ -110,21 +74,23 @@ namespace MalwareMultiScan.Api.Services return objectId.ToString(); } - - /// - /// Obtain stored file stream. - /// - /// File id. - /// File seekable stream. + public async Task ObtainFile(string id) { if (!ObjectId.TryParse(id, out var objectId)) return null; - return await _bucket.OpenDownloadStreamAsync(objectId, new GridFSDownloadOptions + try { - Seekable = true - }); + return await _bucket.OpenDownloadStreamAsync(objectId, new GridFSDownloadOptions + { + Seekable = true + }); + } + catch (GridFSFileNotFoundException) + { + return null; + } } } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs b/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs new file mode 100644 index 0000000..7a8f52b --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Hosting; + +namespace MalwareMultiScan.Api.Services.Interfaces +{ + public interface IReceiverHostedService : IHostedService + { + + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs b/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs new file mode 100644 index 0000000..66ac044 --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Configuration; +using MalwareMultiScan.Api.Data.Models; + +namespace MalwareMultiScan.Api.Services.Interfaces +{ + public interface IScanBackendService + { + ScanBackend[] List { get; } + + Task QueueUrlScan(ScanResult result, ScanBackend backend, string fileUrl); + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs new file mode 100644 index 0000000..172ab87 --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Models; + +namespace MalwareMultiScan.Api.Services.Interfaces +{ + public interface IScanResultService + { + Task CreateScanResult(); + + Task GetScanResult(string id); + + Task UpdateScanResultForBackend(string resultId, string backendId, long duration, + bool completed = false, bool succeeded = false, string[] threats = null); + + Task QueueUrlScan(ScanResult result, string fileUrl); + + Task StoreFile(string fileName, Stream fileStream); + + Task ObtainFile(string id); + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Startup.cs b/MalwareMultiScan.Api/Startup.cs index 1472e95..d3a6905 100644 --- a/MalwareMultiScan.Api/Startup.cs +++ b/MalwareMultiScan.Api/Startup.cs @@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using MalwareMultiScan.Api.Extensions; -using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; @@ -7,7 +9,8 @@ using Microsoft.Extensions.DependencyInjection; namespace MalwareMultiScan.Api { - public class Startup + [ExcludeFromCodeCoverage] + internal class Startup { private readonly IConfiguration _configuration; @@ -28,8 +31,8 @@ namespace MalwareMultiScan.Api services.AddMongoDb(_configuration); services.AddRabbitMq(_configuration); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddControllers(); diff --git a/MalwareMultiScan.Tests/AttributesTests.cs b/MalwareMultiScan.Tests/AttributesTests.cs new file mode 100644 index 0000000..e44675f --- /dev/null +++ b/MalwareMultiScan.Tests/AttributesTests.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using MalwareMultiScan.Api.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class AttributesTests + { + [Test] + public void TestIsHttpUrlAttribute() + { + var attribute = new IsHttpUrlAttribute(); + + Assert.True(attribute.IsValid("http://test.com/file.zip")); + Assert.True(attribute.IsValid("https://test.com/file.zip")); + Assert.True(attribute.IsValid("HTTPS://test.com")); + Assert.False(attribute.IsValid(null)); + Assert.False(attribute.IsValid(string.Empty)); + Assert.False(attribute.IsValid("test")); + Assert.False(attribute.IsValid("ftp://test.com")); + } + + [Test] + public void TestMaxFileSizeAttribute() + { + var attribute = new MaxFileSizeAttribute(); + + var configuration = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }) + { + ["MaxFileSize"] = "100" + }; + + var context = new ValidationContext( + string.Empty, Mock.Of(x => + x.GetService(typeof(IConfiguration)) == configuration + ), null); + + Assert.True(attribute.GetValidationResult( + Mock.Of(x => x.Length == 10), context) == ValidationResult.Success); + + Assert.True(attribute.GetValidationResult( + Mock.Of(x => x.Length == 100), context) == ValidationResult.Success); + + Assert.False(attribute.GetValidationResult( + Mock.Of(x => x.Length == 101), context) == ValidationResult.Success); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/ControllersTests.cs b/MalwareMultiScan.Tests/ControllersTests.cs new file mode 100644 index 0000000..685ec08 --- /dev/null +++ b/MalwareMultiScan.Tests/ControllersTests.cs @@ -0,0 +1,103 @@ +using System.IO; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Controllers; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Interfaces; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ControllersTests + { + private DownloadController _downloadController; + private QueueController _queueController; + private ScanResultsController _scanResultsController; + private Mock _scanResultServiceMock; + + [SetUp] + public void SetUp() + { + _scanResultServiceMock = new Mock(); + + _scanResultServiceMock + .Setup(x => x.ObtainFile("existing")) + .Returns(Task.FromResult(new MemoryStream() as Stream)); + + _scanResultServiceMock + .Setup(x => x.ObtainFile("non-existing")) + .Returns(Task.FromResult((Stream) null)); + + _scanResultServiceMock + .Setup(x => x.GetScanResult("existing")) + .Returns(Task.FromResult(new ScanResult + { + Id = "existing" + })); + + _scanResultServiceMock + .Setup(x => x.GetScanResult("non-existing")) + .Returns(Task.FromResult((ScanResult) null)); + + _scanResultServiceMock + .Setup(x => x.ObtainFile("non-existing")) + .Returns(Task.FromResult((Stream) null)); + + _scanResultServiceMock + .Setup(x => x.CreateScanResult()) + .Returns(Task.FromResult(new ScanResult())); + + _downloadController = new DownloadController(_scanResultServiceMock.Object); + _scanResultsController = new ScanResultsController(_scanResultServiceMock.Object); + + _queueController = new QueueController(_scanResultServiceMock.Object) + { + Url = Mock.Of() + }; + } + + [Test] + public async Task TestFileDownload() + { + Assert.True(await _downloadController.Index("existing") is FileStreamResult); + Assert.True(await _downloadController.Index("non-existing") is NotFoundResult); + } + + [Test] + public async Task TestScanResults() + { + Assert.True( + await _scanResultsController.Index("existing") is OkObjectResult okObjectResult && + okObjectResult.Value is ScanResult scanResult && scanResult.Id == "existing"); + + Assert.True(await _scanResultsController.Index("non-existing") is NotFoundResult); + } + + [Test] + public async Task TestFileQueue() + { + Assert.True(await _queueController.ScanFile( + Mock.Of(x => + x.FileName == "test.exe" && + x.OpenReadStream() == new MemoryStream()) + ) is CreatedAtActionResult); + + _scanResultServiceMock.Verify( + x => x.StoreFile("test.exe", It.IsAny()), Times.Once); + + _scanResultServiceMock.Verify( + x => x.QueueUrlScan(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task TestUrlQueue() + { + Assert.True(await _queueController.ScanUrl("http://test.com") is CreatedAtActionResult); + + _scanResultServiceMock.Verify( + x => x.QueueUrlScan(It.IsAny(), "http://test.com"), Times.Once); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj b/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj new file mode 100644 index 0000000..ac5f5d8 --- /dev/null +++ b/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + diff --git a/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs b/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs new file mode 100644 index 0000000..9cb976e --- /dev/null +++ b/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; +using MalwareMultiScan.Backends.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ReceiverHostedServiceTests + { + private IReceiverHostedService _receiverHostedService; + private Mock _busMock; + private Mock _scanResultServiceMock; + + [SetUp] + public void SetUp() + { + _busMock = new Mock(); + + _busMock + .Setup(x => x.Receive("mms.results", It.IsAny>())) + .Callback>((s, func) => + { + var task = func.Invoke(new ScanResultMessage + { + Id = "test", + Backend = "dummy", + Duration = 100, + Succeeded = true, + Threats = new[] {"Test"} + }); + + task.Wait(); + }); + + _scanResultServiceMock = new Mock(); + + var configuration = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }) + { + ["ResultsSubscriptionId"] = "mms.results" + }; + + _receiverHostedService = new ReceiverHostedService( + _busMock.Object, + configuration, + _scanResultServiceMock.Object, + Mock.Of>()); + } + + [Test] + public async Task TestBusReceiveScanResultMessage() + { + await _receiverHostedService.StartAsync(default); + + _scanResultServiceMock.Verify(x => x.UpdateScanResultForBackend( + "test", "dummy", 100, true, true, new[] {"Test"}), Times.Once); + } + + [Test] + public async Task TestBusIsDisposedOnStop() + { + await _receiverHostedService.StopAsync(default); + + _busMock.Verify(x => x.Dispose(), Times.Once); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/ScanBackendServiceTests.cs b/MalwareMultiScan.Tests/ScanBackendServiceTests.cs new file mode 100644 index 0000000..8af896b --- /dev/null +++ b/MalwareMultiScan.Tests/ScanBackendServiceTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Api.Data.Configuration; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; +using MalwareMultiScan.Backends.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ScanBackendServiceTests + { + private IScanBackendService _scanBackendService; + private Mock _busMock; + + [SetUp] + public void SetUp() + { + var configuration = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }) + { + ["BackendsConfiguration"] = "backends.yaml" + }; + + _busMock = new Mock(); + + _scanBackendService = new ScanBackendService( + configuration, + _busMock.Object, + Mock.Of>()); + } + + [Test] + public void TestBackendsAreNotEmpty() + { + Assert.IsNotEmpty(_scanBackendService.List); + } + + [Test] + public async Task TestQueueUrlScan() + { + await _scanBackendService.QueueUrlScan( + new ScanResult { Id = "test"}, + new ScanBackend { Id = "dummy"}, + "http://test.com" + ); + + _busMock.Verify(x => x.SendAsync( + "dummy", It.Is(r => + r.Id == "test" && + r.Uri == new Uri("http://test.com"))), Times.Once); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/ScanResultServiceTests.cs b/MalwareMultiScan.Tests/ScanResultServiceTests.cs new file mode 100644 index 0000000..48cf5f0 --- /dev/null +++ b/MalwareMultiScan.Tests/ScanResultServiceTests.cs @@ -0,0 +1,119 @@ +using System.IO; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Configuration; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; +using Mongo2Go; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ScanResultServiceTest + { + private IScanResultService _resultService; + private MongoDbRunner _mongoDbRunner; + private Mock _scanBackendService; + + [SetUp] + public void SetUp() + { + _mongoDbRunner = MongoDbRunner.Start(); + + var connection = new MongoClient(_mongoDbRunner.ConnectionString); + var database = connection.GetDatabase("Test"); + var gridFsBucket = new GridFSBucket(database); + + _scanBackendService = new Mock(); + + _scanBackendService + .SetupGet(x => x.List) + .Returns(() => new[] + { + new ScanBackend {Id = "dummy", Enabled = true}, + new ScanBackend {Id = "clamav", Enabled = true}, + new ScanBackend {Id = "disabled", Enabled = false}, + }); + + _resultService = new ScanResultService( + database, gridFsBucket, _scanBackendService.Object); + } + + [TearDown] + public void TearDown() + { + _mongoDbRunner.Dispose(); + } + + [Test] + public async Task TestFileStorageStoreObtain() + { + var originalData = new byte[] {0xDE, 0xAD, 0xBE, 0xEF}; + + string fileId; + + await using (var dataStream = new MemoryStream(originalData)) + fileId = await _resultService.StoreFile("test.txt", dataStream); + + Assert.NotNull(fileId); + + Assert.IsNull(await _resultService.ObtainFile("wrong-id")); + Assert.IsNull(await _resultService.ObtainFile(ObjectId.GenerateNewId().ToString())); + + await using var fileSteam = await _resultService.ObtainFile(fileId); + + var readData = new[] + { + (byte)fileSteam.ReadByte(), + (byte)fileSteam.ReadByte(), + (byte)fileSteam.ReadByte(), + (byte)fileSteam.ReadByte() + }; + + Assert.That(readData, Is.EquivalentTo(originalData)); + } + + [Test] + public async Task TestScanResultCreateGet() + { + var result = await _resultService.CreateScanResult(); + + Assert.NotNull(result.Id); + Assert.That(result.Results, Contains.Key("dummy")); + + await _resultService.UpdateScanResultForBackend( + result.Id, "dummy", 100, true, true, new[] {"Test"}); + + result = await _resultService.GetScanResult(result.Id); + + Assert.NotNull(result.Id); + Assert.That(result.Results, Contains.Key("dummy")); + + var dummyResult = result.Results["dummy"]; + + Assert.IsTrue(dummyResult.Completed); + Assert.IsTrue(dummyResult.Succeeded); + Assert.AreEqual(dummyResult.Duration, 100); + Assert.IsNotEmpty(dummyResult.Threats); + Assert.That(dummyResult.Threats, Is.EquivalentTo(new[] {"Test"})); + } + + [Test] + public async Task TestQueueUrlScan() + { + var result = await _resultService.CreateScanResult(); + + await _resultService.QueueUrlScan( + result, "http://url.com"); + + _scanBackendService.Verify(x => x.QueueUrlScan( + It.IsAny(), + It.IsAny(), + "http://url.com"), Times.Exactly(2)); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.sln b/MalwareMultiScan.sln index 7136b4f..be584ea 100644 --- a/MalwareMultiScan.sln +++ b/MalwareMultiScan.sln @@ -13,6 +13,8 @@ ProjectSection(SolutionItems) = preProject docker-compose.yaml = docker-compose.yaml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Tests", "MalwareMultiScan.Tests\MalwareMultiScan.Tests.csproj", "{9896162D-8FC7-4911-933F-A78C94128923}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,5 +33,9 @@ Global {8A16A3C4-2AE3-4F63-8280-635FF7878080}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A16A3C4-2AE3-4F63-8280-635FF7878080}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A16A3C4-2AE3-4F63-8280-635FF7878080}.Release|Any CPU.Build.0 = Release|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From b68c285ce5baf24f597339cd83e18598b3aae1d8 Mon Sep 17 00:00:00 2001 From: Volodymyr Smirnov Date: Thu, 29 Oct 2020 16:09:56 +0200 Subject: [PATCH 2/2] finished unit tests and docstrings --- .../Attributes/IsHttpUrlAttribute.cs | 6 +- .../Attributes/MaxFileSizeAttribute.cs | 6 +- .../Controllers/DownloadController.cs | 11 ++ .../Controllers/QueueController.cs | 21 ++- .../Controllers/ScanResultsController.cs | 15 +- .../Data/Configuration/ScanBackend.cs | 10 ++ .../Data/Models/ScanResult.cs | 9 + .../Data/Models/ScanResultEntry.cs | 20 ++- .../Extensions/ServiceCollectionExtensions.cs | 7 - .../MalwareMultiScan.Api.csproj | 6 +- MalwareMultiScan.Api/Program.cs | 2 +- .../Implementations/ReceiverHostedService.cs | 19 +- .../Implementations/ScanBackendService.cs | 14 +- .../Implementations/ScanResultService.cs | 27 ++- .../Interfaces/IReceiverHostedService.cs | 4 +- .../Interfaces/IScanBackendService.cs | 12 ++ .../Services/Interfaces/IScanResultService.cs | 41 ++++- MalwareMultiScan.Api/Startup.cs | 3 +- .../AbstractLocalProcessScanBackend.cs | 83 ++++----- .../Backends/Abstracts/AbstractScanBackend.cs | 7 + .../Implementations/ClamavScanBackend.cs | 11 +- .../Implementations/ComodoScanBackend.cs | 10 +- .../Implementations/DrWebScanBackend.cs | 10 +- .../Implementations/DummyScanBackend.cs | 7 +- .../Implementations/KesScanBackend.cs | 10 +- .../Implementations/McAfeeScanBackend.cs | 11 +- .../Implementations/SophosScanBackend.cs | 11 +- .../WindowsDefenderScanBackend.cs | 15 +- .../Enums/BackendType.cs | 34 ++++ .../Extensions/ServiceCollectionExtensions.cs | 70 ++++++-- .../Interfaces/IScanBackend.cs | 27 +++ .../MalwareMultiScan.Backends.csproj | 8 + .../Messages/ScanRequestMessage.cs | 9 + .../Messages/ScanResultMessage.cs | 24 ++- .../Services/Implementations/ProcessRunner.cs | 81 +++++++++ .../Services/Interfaces/IProcessRunner.cs | 22 +++ .../MalwareMultiScan.Scanner.csproj | 8 +- MalwareMultiScan.Scanner/Program.cs | 11 +- .../ScanHostedService.cs | 36 ++-- .../Services/Interfaces/IScanHostedService.cs | 11 ++ .../{ => Api}/AttributesTests.cs | 10 +- .../{ => Api}/ControllersTests.cs | 22 +-- .../{ => Api}/ReceiverHostedServiceTests.cs | 20 +-- .../{ => Api}/ScanBackendServiceTests.cs | 16 +- .../{ => Api}/ScanResultServiceTests.cs | 38 ++-- .../Backends/BackendsTests.cs | 162 ++++++++++++++++++ .../MalwareMultiScan.Tests.csproj | 1 + .../Scanner/ScanHostedServiceTests.cs | 86 ++++++++++ 48 files changed, 910 insertions(+), 194 deletions(-) create mode 100644 MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs create mode 100644 MalwareMultiScan.Backends/Services/Interfaces/IProcessRunner.cs rename MalwareMultiScan.Scanner/Services/{ => Implementations}/ScanHostedService.cs (77%) create mode 100644 MalwareMultiScan.Scanner/Services/Interfaces/IScanHostedService.cs rename MalwareMultiScan.Tests/{ => Api}/AttributesTests.cs (95%) rename MalwareMultiScan.Tests/{ => Api}/ControllersTests.cs (95%) rename MalwareMultiScan.Tests/{ => Api}/ReceiverHostedServiceTests.cs (91%) rename MalwareMultiScan.Tests/{ => Api}/ScanBackendServiceTests.cs (88%) rename MalwareMultiScan.Tests/{ => Api}/ScanResultServiceTests.cs (90%) create mode 100644 MalwareMultiScan.Tests/Backends/BackendsTests.cs create mode 100644 MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs diff --git a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs index 58e357b..e6be77f 100644 --- a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs +++ b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs @@ -11,9 +11,9 @@ namespace MalwareMultiScan.Api.Attributes /// protected override ValidationResult IsValid(object value, ValidationContext validationContext) { - if (!Uri.TryCreate((string)value, UriKind.Absolute, out var uri) - || !uri.IsAbsoluteUri - || uri.Scheme.ToLowerInvariant() != "http" + if (!Uri.TryCreate((string) value, UriKind.Absolute, out var uri) + || !uri.IsAbsoluteUri + || uri.Scheme.ToLowerInvariant() != "http" && uri.Scheme.ToLowerInvariant() != "https") return new ValidationResult("Only absolute http(s) URLs are supported"); diff --git a/MalwareMultiScan.Api/Attributes/MaxFileSizeAttribute.cs b/MalwareMultiScan.Api/Attributes/MaxFileSizeAttribute.cs index 56baa38..5aaa873 100644 --- a/MalwareMultiScan.Api/Attributes/MaxFileSizeAttribute.cs +++ b/MalwareMultiScan.Api/Attributes/MaxFileSizeAttribute.cs @@ -16,12 +16,12 @@ namespace MalwareMultiScan.Api.Attributes var maxSize = validationContext .GetRequiredService() .GetValue("MaxFileSize"); - + var formFile = (IFormFile) value; - + if (formFile == null || formFile.Length > maxSize) return new ValidationResult($"File exceeds the maximum size of {maxSize} bytes"); - + return ValidationResult.Success; } } diff --git a/MalwareMultiScan.Api/Controllers/DownloadController.cs b/MalwareMultiScan.Api/Controllers/DownloadController.cs index 212b357..17300fe 100644 --- a/MalwareMultiScan.Api/Controllers/DownloadController.cs +++ b/MalwareMultiScan.Api/Controllers/DownloadController.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Mvc; namespace MalwareMultiScan.Api.Controllers { + /// + /// Downloads controller. + /// [ApiController] [Route("api/download")] [Produces("application/octet-stream")] @@ -12,11 +15,19 @@ namespace MalwareMultiScan.Api.Controllers { private readonly IScanResultService _scanResultService; + /// + /// Initialize downloads controller. + /// + /// Scan result service. public DownloadController(IScanResultService scanResultService) { _scanResultService = scanResultService; } + /// + /// Download file by id. + /// + /// File id. [HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/MalwareMultiScan.Api/Controllers/QueueController.cs b/MalwareMultiScan.Api/Controllers/QueueController.cs index a68aa7c..4df3af3 100644 --- a/MalwareMultiScan.Api/Controllers/QueueController.cs +++ b/MalwareMultiScan.Api/Controllers/QueueController.cs @@ -8,6 +8,9 @@ using Microsoft.AspNetCore.Mvc; namespace MalwareMultiScan.Api.Controllers { + /// + /// Queue controller. + /// [ApiController] [Route("api/queue")] [Produces("application/json")] @@ -15,23 +18,33 @@ namespace MalwareMultiScan.Api.Controllers { private readonly IScanResultService _scanResultService; + /// + /// Initialize queue controller. + /// + /// Scan result service. public QueueController(IScanResultService scanResultService) { _scanResultService = scanResultService; } + /// + /// Queue file for scanning. + /// + /// File from form data. [HttpPost("file")] [ProducesResponseType(typeof(ScanResult), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task ScanFile( - [Required, MaxFileSize] IFormFile file) + [Required] [MaxFileSize] IFormFile file) { var result = await _scanResultService.CreateScanResult(); string storedFileId; await using (var uploadFileStream = file.OpenReadStream()) + { storedFileId = await _scanResultService.StoreFile(file.FileName, uploadFileStream); + } await _scanResultService.QueueUrlScan(result, Url.Action("Index", "Download", new {id = storedFileId}, Request?.Scheme, Request?.Host.Value)); @@ -39,11 +52,15 @@ namespace MalwareMultiScan.Api.Controllers return CreatedAtAction("Index", "ScanResults", new {id = result.Id}, result); } + /// + /// Queue URL for scanning. + /// + /// URL from form data. [HttpPost("url")] [ProducesResponseType(typeof(ScanResult), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task ScanUrl( - [FromForm, Required, IsHttpUrl] string url) + [FromForm] [Required] [IsHttpUrl] string url) { var result = await _scanResultService.CreateScanResult(); diff --git a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs index 3752062..a144f08 100644 --- a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs +++ b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs @@ -1,25 +1,34 @@ using System.Threading.Tasks; using MalwareMultiScan.Api.Data.Models; -using MalwareMultiScan.Api.Services; -using MalwareMultiScan.Api.Services.Implementations; using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace MalwareMultiScan.Api.Controllers { + /// + /// Scan results controller. + /// [ApiController] [Route("api/results")] [Produces("application/json")] public class ScanResultsController : Controller { private readonly IScanResultService _scanResultService; - + + /// + /// Initialize scan results controller. + /// + /// Scan result service. public ScanResultsController(IScanResultService scanResultService) { _scanResultService = scanResultService; } + /// + /// Get scan result by id. + /// + /// Scan result id. [HttpGet("{id}")] [ProducesResponseType(typeof(ScanResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs b/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs index 55f369c..75911a2 100644 --- a/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs +++ b/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs @@ -1,8 +1,18 @@ namespace MalwareMultiScan.Api.Data.Configuration { + /// + /// Scan backend. + /// public class ScanBackend { + /// + /// Backend id. + /// public string Id { get; set; } + + /// + /// Backend state. + /// public bool Enabled { get; set; } } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Data/Models/ScanResult.cs b/MalwareMultiScan.Api/Data/Models/ScanResult.cs index 76315c8..ce41477 100644 --- a/MalwareMultiScan.Api/Data/Models/ScanResult.cs +++ b/MalwareMultiScan.Api/Data/Models/ScanResult.cs @@ -4,12 +4,21 @@ using MongoDB.Bson.Serialization.Attributes; namespace MalwareMultiScan.Api.Data.Models { + /// + /// Scan result. + /// public class ScanResult { + /// + /// Result id. + /// [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } + /// + /// Result entries where key is backend id and value is . + /// public Dictionary Results { get; set; } = new Dictionary(); } diff --git a/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs b/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs index b024c3d..48af9ff 100644 --- a/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs +++ b/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs @@ -1,12 +1,28 @@ -using System; - namespace MalwareMultiScan.Api.Data.Models { + /// + /// Scan result entry. + /// public class ScanResultEntry { + /// + /// Completion status. + /// public bool Completed { get; set; } + + /// + /// Indicates that scanning completed without error. + /// public bool? Succeeded { get; set; } + + /// + /// Scanning duration in seconds. + /// public long Duration { get; set; } + + /// + /// Detected names of threats. + /// public string[] Threats { get; set; } } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs index 4e58e0c..5c18425 100644 --- a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs +++ b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using EasyNetQ; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; @@ -23,12 +22,6 @@ namespace MalwareMultiScan.Api.Extensions services.AddSingleton(new GridFSBucket(db)); } - internal static void AddRabbitMq(this IServiceCollection services, IConfiguration configuration) - { - services.AddSingleton(x => - RabbitHutch.CreateBus(configuration.GetConnectionString("RabbitMQ"))); - } - internal static void AddDockerForwardedHeadersOptions(this IServiceCollection services) { services.Configure(options => diff --git a/MalwareMultiScan.Api/MalwareMultiScan.Api.csproj b/MalwareMultiScan.Api/MalwareMultiScan.Api.csproj index 312f84f..9f0cb3e 100644 --- a/MalwareMultiScan.Api/MalwareMultiScan.Api.csproj +++ b/MalwareMultiScan.Api/MalwareMultiScan.Api.csproj @@ -2,6 +2,11 @@ netcoreapp3.1 + Volodymyr Smirnov + MalwareMultiScan Api + 1.0.0 + 1.0.0 + true @@ -11,7 +16,6 @@ - diff --git a/MalwareMultiScan.Api/Program.cs b/MalwareMultiScan.Api/Program.cs index 5211d45..d8993a8 100644 --- a/MalwareMultiScan.Api/Program.cs +++ b/MalwareMultiScan.Api/Program.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting; namespace MalwareMultiScan.Api { [ExcludeFromCodeCoverage] - public static class Program + internal static class Program { public static void Main(string[] args) { diff --git a/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs index 1fa43c0..86db217 100644 --- a/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs @@ -8,13 +8,21 @@ using Microsoft.Extensions.Logging; namespace MalwareMultiScan.Api.Services.Implementations { + /// public class ReceiverHostedService : IReceiverHostedService { private readonly IBus _bus; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly IScanResultService _scanResultService; - + + /// + /// Initialize receiver hosted service. + /// + /// EasyNetQ bus. + /// Configuration. + /// Scan result service. + /// Logger. public ReceiverHostedService(IBus bus, IConfiguration configuration, IScanResultService scanResultService, ILogger logger) { @@ -23,13 +31,15 @@ namespace MalwareMultiScan.Api.Services.Implementations _scanResultService = scanResultService; _logger = logger; } - + + + /// public Task StartAsync(CancellationToken cancellationToken) { _bus.Receive(_configuration.GetValue("ResultsSubscriptionId"), async message => { message.Threats ??= new string[] { }; - + _logger.LogInformation( $"Received a result from {message.Backend} for {message.Id} " + $"with threats {string.Join(",", message.Threats)}"); @@ -44,7 +54,8 @@ namespace MalwareMultiScan.Api.Services.Implementations return Task.CompletedTask; } - + + /// public Task StopAsync(CancellationToken cancellationToken) { _bus?.Dispose(); diff --git a/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs b/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs index 83dc419..1e5cc33 100644 --- a/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs @@ -13,11 +13,19 @@ using YamlDotNet.Serialization.NamingConventions; namespace MalwareMultiScan.Api.Services.Implementations { + /// public class ScanBackendService : IScanBackendService { private readonly IBus _bus; private readonly ILogger _logger; + /// + /// Initialise scan backend service. + /// + /// Configuration. + /// EasyNetQ bus. + /// Logger. + /// Missing backends.yaml configuration. public ScanBackendService(IConfiguration configuration, IBus bus, ILogger logger) { _bus = bus; @@ -36,9 +44,11 @@ namespace MalwareMultiScan.Api.Services.Implementations List = deserializer.Deserialize(configurationContent); } - + + /// public ScanBackend[] List { get; } - + + /// public async Task QueueUrlScan(ScanResult result, ScanBackend backend, string fileUrl) { _logger.LogInformation( diff --git a/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs index 50df75e..68aa580 100644 --- a/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs @@ -9,6 +9,7 @@ using MongoDB.Driver.GridFS; namespace MalwareMultiScan.Api.Services.Implementations { + /// public class ScanResultService : IScanResultService { private const string CollectionName = "ScanResults"; @@ -16,15 +17,22 @@ namespace MalwareMultiScan.Api.Services.Implementations private readonly IGridFSBucket _bucket; private readonly IMongoCollection _collection; private readonly IScanBackendService _scanBackendService; - + + /// + /// Initialize scan result service. + /// + /// Mongo database. + /// GridFS bucket. + /// Scan backend service. public ScanResultService(IMongoDatabase db, IGridFSBucket bucket, IScanBackendService scanBackendService) { _bucket = bucket; _scanBackendService = scanBackendService; - + _collection = db.GetCollection(CollectionName); } + /// public async Task CreateScanResult() { var scanResult = new ScanResult @@ -38,7 +46,8 @@ namespace MalwareMultiScan.Api.Services.Implementations return scanResult; } - + + /// public async Task GetScanResult(string id) { var result = await _collection.FindAsync( @@ -46,7 +55,8 @@ namespace MalwareMultiScan.Api.Services.Implementations return await result.FirstOrDefaultAsync(); } - + + /// public async Task UpdateScanResultForBackend(string resultId, string backendId, long duration, bool completed = false, bool succeeded = false, string[] threats = null) { @@ -60,13 +70,15 @@ namespace MalwareMultiScan.Api.Services.Implementations Threats = threats ?? new string[] { } })); } - + + /// public async Task QueueUrlScan(ScanResult result, string fileUrl) { foreach (var backend in _scanBackendService.List.Where(b => b.Enabled)) await _scanBackendService.QueueUrlScan(result, backend, fileUrl); } - + + /// public async Task StoreFile(string fileName, Stream fileStream) { var objectId = await _bucket.UploadFromStreamAsync( @@ -74,7 +86,8 @@ namespace MalwareMultiScan.Api.Services.Implementations return objectId.ToString(); } - + + /// public async Task ObtainFile(string id) { if (!ObjectId.TryParse(id, out var objectId)) diff --git a/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs b/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs index 7a8f52b..67ed8f0 100644 --- a/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs +++ b/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs @@ -2,8 +2,10 @@ using Microsoft.Extensions.Hosting; namespace MalwareMultiScan.Api.Services.Interfaces { + /// + /// Receiver hosted service. + /// public interface IReceiverHostedService : IHostedService { - } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs b/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs index 66ac044..3cc6484 100644 --- a/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs @@ -4,10 +4,22 @@ using MalwareMultiScan.Api.Data.Models; namespace MalwareMultiScan.Api.Services.Interfaces { + /// + /// Scan backend service. + /// public interface IScanBackendService { + /// + /// Get list of parsed backends. + /// ScanBackend[] List { get; } + /// + /// Queue URL for scan. + /// + /// Result entry. + /// Backend entry. + /// Remote URL. Task QueueUrlScan(ScanResult result, ScanBackend backend, string fileUrl); } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs index 172ab87..55cf404 100644 --- a/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs @@ -4,19 +4,56 @@ using MalwareMultiScan.Api.Data.Models; namespace MalwareMultiScan.Api.Services.Interfaces { + /// + /// Scan result service. + /// public interface IScanResultService - { + { + /// + /// Create scan result. + /// + /// Scan result entry with id. Task CreateScanResult(); - + + /// + /// Get scan result. + /// + /// Result id. + /// Scan result entry with id. Task GetScanResult(string id); + /// + /// Update scan result entry. + /// + /// Result id. + /// Backend id. + /// Duration. + /// Completion status. + /// Indicates that scanning completed without error. + /// Detected names of threats. Task UpdateScanResultForBackend(string resultId, string backendId, long duration, bool completed = false, bool succeeded = false, string[] threats = null); + /// + /// Queue URL for scanning. + /// + /// Result entry. + /// Remote URL. Task QueueUrlScan(ScanResult result, string fileUrl); + /// + /// Store file. + /// + /// File name. + /// File stream. + /// Unique file id. Task StoreFile(string fileName, Stream fileStream); + /// + /// Obtain file. + /// + /// Unique file id. + /// Seekable file stream opened for reading. Task ObtainFile(string id); } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Startup.cs b/MalwareMultiScan.Api/Startup.cs index d3a6905..f23ba0c 100644 --- a/MalwareMultiScan.Api/Startup.cs +++ b/MalwareMultiScan.Api/Startup.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using MalwareMultiScan.Api.Extensions; using MalwareMultiScan.Api.Services.Implementations; using MalwareMultiScan.Api.Services.Interfaces; +using MalwareMultiScan.Backends.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; @@ -22,7 +23,7 @@ namespace MalwareMultiScan.Api public void ConfigureServices(IServiceCollection services) { services.AddDockerForwardedHeadersOptions(); - + services.Configure(options => { options.Limits.MaxRequestBodySize = _configuration.GetValue("MaxFileSize"); diff --git a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs index 6274e60..c52a616 100644 --- a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs @@ -1,77 +1,64 @@ using System; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Abstracts { + /// public abstract class AbstractLocalProcessScanBackend : AbstractScanBackend { - private readonly ILogger _logger; + private readonly IProcessRunner _processRunner; - protected AbstractLocalProcessScanBackend(ILogger logger) + /// + protected AbstractLocalProcessScanBackend(IProcessRunner processRunner) { - _logger = logger; + _processRunner = processRunner; } + /// + /// Regex to extract names of threats. + /// protected abstract Regex MatchRegex { get; } + + /// + /// Path to the backend. + /// protected abstract string BackendPath { get; } + + /// + /// Parse StdErr instead of StdOut. + /// protected virtual bool ParseStdErr { get; } = false; + + /// + /// Throw on non-zero exit code. + /// protected virtual bool ThrowOnNonZeroExitCode { get; } = true; + /// + /// Get backend process parameters. + /// + /// Path to the temporary file. + /// Formatted string with parameters and path. protected abstract string GetBackendArguments(string path); - - public override async Task ScanAsync(string path, CancellationToken cancellationToken) + + /// + public override Task ScanAsync(string path, CancellationToken cancellationToken) { - var process = new Process - { - StartInfo = new ProcessStartInfo(BackendPath, GetBackendArguments(path)) - { - RedirectStandardError = true, - RedirectStandardOutput = true, - WorkingDirectory = Path.GetDirectoryName(BackendPath) ?? Directory.GetCurrentDirectory() - } - }; + var exitCode = _processRunner.RunTillCompletion(BackendPath, GetBackendArguments(path), cancellationToken, + out var standardOutput, out var standardError); - _logger.LogInformation( - $"Starting process {process.StartInfo.FileName} " + - $"with arguments {process.StartInfo.Arguments} " + - $"in working directory {process.StartInfo.WorkingDirectory}"); + if (ThrowOnNonZeroExitCode && exitCode != 0) + throw new ApplicationException($"Process has terminated with an exit code {exitCode}"); - process.Start(); - - cancellationToken.Register(() => - { - if (process.HasExited) - return; - - process.Kill(true); - - throw new TimeoutException("Scanning failed to complete within the timeout"); - }); - - process.WaitForExit(); - - _logger.LogInformation($"Process has exited with code {process.ExitCode}"); - - var standardOutput = await process.StandardOutput.ReadToEndAsync(); - var standardError = await process.StandardError.ReadToEndAsync(); - - _logger.LogDebug($"Process standard output: {standardOutput}"); - _logger.LogDebug($"Process standard error: {standardError}"); - - if (ThrowOnNonZeroExitCode && process.ExitCode != 0) - throw new ApplicationException($"Process has terminated with an exit code {process.ExitCode}"); - - return MatchRegex + return Task.FromResult(MatchRegex .Matches(ParseStdErr ? standardError : standardOutput) .Where(x => x.Success) .Select(x => x.Groups["threat"].Value) - .ToArray(); + .ToArray()); } } } \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs index 6229b1f..00a282a 100644 --- a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http; using System.Threading; @@ -7,12 +8,17 @@ using MalwareMultiScan.Backends.Interfaces; namespace MalwareMultiScan.Backends.Backends.Abstracts { + /// + [ExcludeFromCodeCoverage] public abstract class AbstractScanBackend : IScanBackend { + /// public abstract string Id { get; } + /// public abstract Task ScanAsync(string path, CancellationToken cancellationToken); + /// public async Task ScanAsync(Uri uri, CancellationToken cancellationToken) { using var httpClient = new HttpClient(); @@ -22,6 +28,7 @@ namespace MalwareMultiScan.Backends.Backends.Abstracts return await ScanAsync(uriStream, cancellationToken); } + /// public async Task ScanAsync(Stream stream, CancellationToken cancellationToken) { var tempFile = Path.GetTempFileName(); diff --git a/MalwareMultiScan.Backends/Backends/Implementations/ClamavScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/ClamavScanBackend.cs index 3125409..d80fc16 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/ClamavScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/ClamavScanBackend.cs @@ -1,24 +1,31 @@ using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class ClamavScanBackend : AbstractLocalProcessScanBackend { - public ClamavScanBackend(ILogger logger) : base(logger) + /// + public ClamavScanBackend(IProcessRunner processRunner) : base(processRunner) { } + /// public override string Id { get; } = "clamav"; + /// protected override string BackendPath { get; } = "/usr/bin/clamdscan"; + /// protected override Regex MatchRegex { get; } = new Regex(@"(\S+): (?[\S]+) FOUND", RegexOptions.Compiled | RegexOptions.Multiline); + /// protected override bool ThrowOnNonZeroExitCode { get; } = false; + /// protected override string GetBackendArguments(string path) { return $"-m --fdpass --no-summary {path}"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs index e222d3e..6daee74 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs @@ -1,23 +1,29 @@ using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class ComodoScanBackend : AbstractLocalProcessScanBackend { - public ComodoScanBackend(ILogger logger) : base(logger) + /// + public ComodoScanBackend(IProcessRunner processRunner) : base(processRunner) { } + /// public override string Id { get; } = "comodo"; + /// protected override string BackendPath { get; } = "/opt/COMODO/cmdscan"; + /// protected override Regex MatchRegex { get; } = new Regex(@".* ---> Found Virus, Malware Name is (?.*)", RegexOptions.Compiled | RegexOptions.Multiline); + /// protected override string GetBackendArguments(string path) { return $"-v -s {path}"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs index a98dcb6..7681449 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs @@ -1,22 +1,28 @@ using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class DrWebScanBackend : AbstractLocalProcessScanBackend { - public DrWebScanBackend(ILogger logger) : base(logger) + /// + public DrWebScanBackend(IProcessRunner processRunner) : base(processRunner) { } + /// public override string Id { get; } = "drweb"; + /// protected override string BackendPath { get; } = "/usr/bin/drweb-ctl"; + /// protected override Regex MatchRegex { get; } = new Regex(@".* - infected with (?[\S ]+)", RegexOptions.Compiled | RegexOptions.Multiline); + /// protected override string GetBackendArguments(string path) { return $"scan {path}"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/DummyScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/DummyScanBackend.cs index e1ff5d6..6701648 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/DummyScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/DummyScanBackend.cs @@ -6,20 +6,25 @@ using MalwareMultiScan.Backends.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class DummyScanBackend : IScanBackend { + /// public string Id { get; } = "dummy"; + /// public Task ScanAsync(string path, CancellationToken cancellationToken) { return Scan(); } + /// public Task ScanAsync(Uri uri, CancellationToken cancellationToken) { return Scan(); } + /// public Task ScanAsync(Stream stream, CancellationToken cancellationToken) { return Scan(); @@ -29,7 +34,7 @@ namespace MalwareMultiScan.Backends.Backends.Implementations { await Task.Delay( TimeSpan.FromSeconds(5)); - + return new[] {"Malware.Dummy.Result"}; } } diff --git a/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs index 2448351..d1141ec 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs @@ -1,22 +1,28 @@ using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class KesScanBackend : AbstractLocalProcessScanBackend { - public KesScanBackend(ILogger logger) : base(logger) + /// + public KesScanBackend(IProcessRunner processRunner) : base(processRunner) { } + /// public override string Id { get; } = "kes"; + /// protected override string BackendPath { get; } = "/bin/bash"; + /// protected override Regex MatchRegex { get; } = new Regex(@"[ +]DetectName.*: (?.*)", RegexOptions.Compiled | RegexOptions.Multiline); + /// protected override string GetBackendArguments(string path) { return $"/usr/bin/kesl-scan {path}"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/McAfeeScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/McAfeeScanBackend.cs index 8baa2c4..f15a66f 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/McAfeeScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/McAfeeScanBackend.cs @@ -1,24 +1,31 @@ using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class McAfeeScanBackend : AbstractLocalProcessScanBackend { - public McAfeeScanBackend(ILogger logger) : base(logger) + /// + public McAfeeScanBackend(IProcessRunner processRunner) : base(processRunner) { } + /// public override string Id { get; } = "mcafee"; + /// protected override string BackendPath { get; } = "/usr/local/uvscan/uvscan"; + /// protected override bool ThrowOnNonZeroExitCode { get; } = false; + /// protected override Regex MatchRegex { get; } = new Regex(@".* ... Found: (?.*).", RegexOptions.Compiled | RegexOptions.Multiline); + /// protected override string GetBackendArguments(string path) { return $"--SECURE {path}"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs index 9b8cecd..0479582 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs @@ -1,24 +1,31 @@ using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class SophosScanBackend : AbstractLocalProcessScanBackend { - public SophosScanBackend(ILogger logger) : base(logger) + /// + public SophosScanBackend(IProcessRunner processRunner) : base(processRunner) { } + /// public override string Id { get; } = "sophos"; + /// protected override string BackendPath { get; } = "/opt/sophos-av/bin/savscan"; + /// protected override bool ThrowOnNonZeroExitCode { get; } = false; + /// protected override Regex MatchRegex { get; } = new Regex(@">>> Virus '(?.*)' found in file .*", RegexOptions.Compiled | RegexOptions.Multiline); + /// protected override string GetBackendArguments(string path) { return $"-f -archive -ss {path}"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs index 8eb69b0..45b9f2f 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs @@ -1,25 +1,32 @@ using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; -using Microsoft.Extensions.Logging; +using MalwareMultiScan.Backends.Services.Interfaces; namespace MalwareMultiScan.Backends.Backends.Implementations { + /// public class WindowsDefenderScanBackend : AbstractLocalProcessScanBackend { - public WindowsDefenderScanBackend(ILogger logger) : base(logger) + /// + public WindowsDefenderScanBackend(IProcessRunner processRunner) : base(processRunner) { } - + + /// public override string Id { get; } = "windows-defender"; + /// protected override string BackendPath { get; } = "/opt/mpclient"; + /// protected override Regex MatchRegex { get; } = new Regex(@"EngineScanCallback\(\): Threat (?[\S]+) identified", RegexOptions.Compiled | RegexOptions.Multiline); - + + /// protected override bool ParseStdErr { get; } = true; + /// protected override string GetBackendArguments(string path) { return path; diff --git a/MalwareMultiScan.Backends/Enums/BackendType.cs b/MalwareMultiScan.Backends/Enums/BackendType.cs index cc935b3..e858ce2 100644 --- a/MalwareMultiScan.Backends/Enums/BackendType.cs +++ b/MalwareMultiScan.Backends/Enums/BackendType.cs @@ -1,14 +1,48 @@ namespace MalwareMultiScan.Backends.Enums { + /// + /// Backend type. + /// public enum BackendType { + /// + /// Dummy + /// Dummy, + + /// + /// Windows Defender. + /// Defender, + + /// + /// ClamAV. + /// Clamav, + + /// + /// DrWeb. + /// DrWeb, + + /// + /// KES. + /// Kes, + + /// + /// Comodo. + /// Comodo, + + /// + /// Sophos. + /// Sophos, + + /// + /// McAfee. + /// McAfee } } \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Extensions/ServiceCollectionExtensions.cs b/MalwareMultiScan.Backends/Extensions/ServiceCollectionExtensions.cs index a7005c8..0c2bf00 100644 --- a/MalwareMultiScan.Backends/Extensions/ServiceCollectionExtensions.cs +++ b/MalwareMultiScan.Backends/Extensions/ServiceCollectionExtensions.cs @@ -1,32 +1,72 @@ using System; +using System.Diagnostics.CodeAnalysis; +using EasyNetQ; using MalwareMultiScan.Backends.Backends.Implementations; using MalwareMultiScan.Backends.Enums; using MalwareMultiScan.Backends.Interfaces; +using MalwareMultiScan.Backends.Services.Implementations; +using MalwareMultiScan.Backends.Services.Interfaces; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace MalwareMultiScan.Backends.Extensions { + /// + /// Extensions for IServiceCollection. + /// + [ExcludeFromCodeCoverage] public static class ServiceCollectionExtensions { - public static void AddScanningBackend(this IServiceCollection services, BackendType type) + /// + /// Add RabbitMQ service. + /// + /// Service collection. + /// Configuration. + public static void AddRabbitMq(this IServiceCollection services, IConfiguration configuration) { - using var provider = services.BuildServiceProvider(); + services.AddSingleton(x => + RabbitHutch.CreateBus(configuration.GetConnectionString("RabbitMQ"))); + } - var logger = provider.GetService>(); + /// + /// Add scanning backend. + /// + /// Service collection. + /// Configuration. + /// Unknown backend. + public static void AddScanningBackend(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); - services.AddSingleton(type switch + switch (configuration.GetValue("BackendType")) { - BackendType.Dummy => new DummyScanBackend(), - BackendType.Defender => new WindowsDefenderScanBackend(logger), - BackendType.Clamav => new ClamavScanBackend(logger), - BackendType.DrWeb => new DrWebScanBackend(logger), - BackendType.Kes => new KesScanBackend(logger), - BackendType.Comodo => new ComodoScanBackend(logger), - BackendType.Sophos => new SophosScanBackend(logger), - BackendType.McAfee => new McAfeeScanBackend(logger), - _ => throw new NotImplementedException() - }); + case BackendType.Dummy: + services.AddSingleton(); + break; + case BackendType.Defender: + services.AddSingleton(); + break; + case BackendType.Clamav: + services.AddSingleton(); + break; + case BackendType.DrWeb: + services.AddSingleton(); + break; + case BackendType.Kes: + services.AddSingleton(); + break; + case BackendType.Comodo: + services.AddSingleton(); + break; + case BackendType.Sophos: + services.AddSingleton(); + break; + case BackendType.McAfee: + services.AddSingleton(); + break; + default: + throw new ArgumentOutOfRangeException(); + } } } } \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs b/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs index ea872f1..64f42be 100644 --- a/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs +++ b/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs @@ -5,11 +5,38 @@ using System.Threading.Tasks; namespace MalwareMultiScan.Backends.Interfaces { + /// + /// Scan backend. + /// public interface IScanBackend { + /// + /// Unique backend id. + /// public string Id { get; } + + /// + /// Scan file. + /// + /// File path. + /// Cancellation token. + /// List of detected threats. public Task ScanAsync(string path, CancellationToken cancellationToken); + + /// + /// Scan URL. + /// + /// URL. + /// Cancellation token. + /// List of detected threats. public Task ScanAsync(Uri uri, CancellationToken cancellationToken); + + /// + /// Scan stream. + /// + /// Stream. + /// Cancellation token. + /// List of detected threats. public Task ScanAsync(Stream stream, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/MalwareMultiScan.Backends/MalwareMultiScan.Backends.csproj b/MalwareMultiScan.Backends/MalwareMultiScan.Backends.csproj index ae69f21..7611bdd 100644 --- a/MalwareMultiScan.Backends/MalwareMultiScan.Backends.csproj +++ b/MalwareMultiScan.Backends/MalwareMultiScan.Backends.csproj @@ -2,11 +2,19 @@ netcoreapp3.1 + Volodymyr Smirnov + MalwareMultiScan Backends + 1.0.0 + 1.0.0 + true + + + diff --git a/MalwareMultiScan.Backends/Messages/ScanRequestMessage.cs b/MalwareMultiScan.Backends/Messages/ScanRequestMessage.cs index 2ba674c..011e49b 100644 --- a/MalwareMultiScan.Backends/Messages/ScanRequestMessage.cs +++ b/MalwareMultiScan.Backends/Messages/ScanRequestMessage.cs @@ -2,10 +2,19 @@ using System; namespace MalwareMultiScan.Backends.Messages { + /// + /// Scan request message. + /// public class ScanRequestMessage { + /// + /// Result id. + /// public string Id { get; set; } + /// + /// Remote URL. + /// public Uri Uri { get; set; } } } \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs b/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs index 1f9bdb6..515d64c 100644 --- a/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs +++ b/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs @@ -1,15 +1,33 @@ -using System; - namespace MalwareMultiScan.Backends.Messages { + /// + /// Scan result message. + /// public class ScanResultMessage { + /// + /// Result id. + /// public string Id { get; set; } + + /// + /// Backend. + /// public string Backend { get; set; } + /// + /// Status. + /// public bool Succeeded { get; set; } - public string[] Threats { get; set; } + /// + /// List of detected threats. + /// + public string[] Threats { get; set; } + + /// + /// Duration. + /// public long Duration { get; set; } } } \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs b/MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs new file mode 100644 index 0000000..1805e34 --- /dev/null +++ b/MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using MalwareMultiScan.Backends.Services.Interfaces; +using Microsoft.Extensions.Logging; + +namespace MalwareMultiScan.Backends.Services.Implementations +{ + /// + /// + /// + [ExcludeFromCodeCoverage] + public class ProcessRunner : IProcessRunner + { + private readonly ILogger _logger; + + /// + /// Initialize process runner. + /// + /// Logger. + public ProcessRunner(ILogger logger) + { + _logger = logger; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public int RunTillCompletion(string path, string arguments, CancellationToken cancellationToken, + out string standardOutput, out string standardError) + { + var process = new Process + { + StartInfo = new ProcessStartInfo(path, arguments) + { + RedirectStandardError = true, + RedirectStandardOutput = true, + WorkingDirectory = Path.GetDirectoryName(path) ?? Directory.GetCurrentDirectory() + } + }; + + _logger.LogInformation( + $"Starting process {process.StartInfo.FileName} " + + $"with arguments {process.StartInfo.Arguments} " + + $"in working directory {process.StartInfo.WorkingDirectory}"); + + process.Start(); + + cancellationToken.Register(() => + { + if (process.HasExited) + return; + + process.Kill(true); + + throw new TimeoutException("Scanning failed to complete within the timeout"); + }); + + process.WaitForExit(); + + _logger.LogInformation($"Process has exited with code {process.ExitCode}"); + + standardOutput = process.StandardOutput.ReadToEnd(); + standardError = process.StandardError.ReadToEnd(); + + _logger.LogDebug($"Process standard output: {standardOutput}"); + _logger.LogDebug($"Process standard error: {standardError}"); + + return process.ExitCode; + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Services/Interfaces/IProcessRunner.cs b/MalwareMultiScan.Backends/Services/Interfaces/IProcessRunner.cs new file mode 100644 index 0000000..028aafd --- /dev/null +++ b/MalwareMultiScan.Backends/Services/Interfaces/IProcessRunner.cs @@ -0,0 +1,22 @@ +using System.Threading; + +namespace MalwareMultiScan.Backends.Services.Interfaces +{ + /// + /// Process runner. + /// + public interface IProcessRunner + { + /// + /// Run process till completion. + /// + /// Path to binary. + /// Arguments. + /// Cancellation token. + /// Standard output of a process. + /// Standard error of a process. + /// Exit code. + int RunTillCompletion(string path, string arguments, CancellationToken token, + out string standardOutput, out string standardError); + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj b/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj index 2d98265..1771ce2 100644 --- a/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj +++ b/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj @@ -2,11 +2,15 @@ netcoreapp3.1 + Volodymyr Smirnov + MalwareMultiScan Scanner + 1.0.0 + 1.0.0 + true - - + diff --git a/MalwareMultiScan.Scanner/Program.cs b/MalwareMultiScan.Scanner/Program.cs index eba4512..94c5afc 100644 --- a/MalwareMultiScan.Scanner/Program.cs +++ b/MalwareMultiScan.Scanner/Program.cs @@ -1,14 +1,15 @@ +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; -using MalwareMultiScan.Backends.Enums; using MalwareMultiScan.Backends.Extensions; -using MalwareMultiScan.Scanner.Services; +using MalwareMultiScan.Scanner.Services.Implementations; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace MalwareMultiScan.Scanner { - public static class Program + [ExcludeFromCodeCoverage] + internal static class Program { public static async Task Main(string[] args) { @@ -22,8 +23,8 @@ namespace MalwareMultiScan.Scanner { services.AddLogging(); - services.AddScanningBackend( - context.Configuration.GetValue("BackendType")); + services.AddRabbitMq(context.Configuration); + services.AddScanningBackend(context.Configuration); services.AddHostedService(); }).RunConsoleAsync(); diff --git a/MalwareMultiScan.Scanner/Services/ScanHostedService.cs b/MalwareMultiScan.Scanner/Services/Implementations/ScanHostedService.cs similarity index 77% rename from MalwareMultiScan.Scanner/Services/ScanHostedService.cs rename to MalwareMultiScan.Scanner/Services/Implementations/ScanHostedService.cs index b85bc49..9e6c2f2 100644 --- a/MalwareMultiScan.Scanner/Services/ScanHostedService.cs +++ b/MalwareMultiScan.Scanner/Services/Implementations/ScanHostedService.cs @@ -5,32 +5,41 @@ using System.Threading.Tasks; using EasyNetQ; using MalwareMultiScan.Backends.Interfaces; using MalwareMultiScan.Backends.Messages; +using MalwareMultiScan.Scanner.Services.Interfaces; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace MalwareMultiScan.Scanner.Services +namespace MalwareMultiScan.Scanner.Services.Implementations { - internal class ScanHostedService : IHostedService + /// + public class ScanHostedService : IScanHostedService { private readonly IScanBackend _backend; + private readonly IBus _bus; private readonly IConfiguration _configuration; private readonly ILogger _logger; - private IBus _bus; - - public ScanHostedService(ILogger logger, IConfiguration configuration, IScanBackend backend) + /// + /// Initialise scan hosted service. + /// + /// Logger. + /// Scan backend. + /// EasyNetQ bus. + /// Configuration. + public ScanHostedService( + ILogger logger, + IScanBackend backend, IBus bus, + IConfiguration configuration) { _logger = logger; + _bus = bus; _configuration = configuration; _backend = backend; } + /// public Task StartAsync(CancellationToken cancellationToken) { - _bus = RabbitHutch.CreateBus( - _configuration.GetConnectionString("RabbitMQ")); - _bus.Receive(_backend.Id, Scan); _logger.LogInformation( @@ -39,6 +48,7 @@ namespace MalwareMultiScan.Scanner.Services return Task.CompletedTask; } + /// public Task StopAsync(CancellationToken cancellationToken) { _bus.Dispose(); @@ -62,9 +72,9 @@ namespace MalwareMultiScan.Scanner.Services Id = message.Id, Backend = _backend.Id }; - + var stopwatch = new Stopwatch(); - + stopwatch.Start(); try @@ -91,10 +101,10 @@ namespace MalwareMultiScan.Scanner.Services } result.Duration = stopwatch.ElapsedMilliseconds / 1000; - + _logger.LogInformation( $"Sending scan results with status {result.Succeeded}"); - + await _bus.SendAsync( _configuration.GetValue("ResultsSubscriptionId"), result); } diff --git a/MalwareMultiScan.Scanner/Services/Interfaces/IScanHostedService.cs b/MalwareMultiScan.Scanner/Services/Interfaces/IScanHostedService.cs new file mode 100644 index 0000000..8286292 --- /dev/null +++ b/MalwareMultiScan.Scanner/Services/Interfaces/IScanHostedService.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Hosting; + +namespace MalwareMultiScan.Scanner.Services.Interfaces +{ + /// + /// Scan hosted service. + /// + public interface IScanHostedService : IHostedService + { + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/AttributesTests.cs b/MalwareMultiScan.Tests/Api/AttributesTests.cs similarity index 95% rename from MalwareMultiScan.Tests/AttributesTests.cs rename to MalwareMultiScan.Tests/Api/AttributesTests.cs index e44675f..86b2d4a 100644 --- a/MalwareMultiScan.Tests/AttributesTests.cs +++ b/MalwareMultiScan.Tests/Api/AttributesTests.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Configuration.Memory; using Moq; using NUnit.Framework; -namespace MalwareMultiScan.Tests +namespace MalwareMultiScan.Tests.Api { public class AttributesTests { @@ -16,7 +16,7 @@ namespace MalwareMultiScan.Tests public void TestIsHttpUrlAttribute() { var attribute = new IsHttpUrlAttribute(); - + Assert.True(attribute.IsValid("http://test.com/file.zip")); Assert.True(attribute.IsValid("https://test.com/file.zip")); Assert.True(attribute.IsValid("HTTPS://test.com")); @@ -30,7 +30,7 @@ namespace MalwareMultiScan.Tests public void TestMaxFileSizeAttribute() { var attribute = new MaxFileSizeAttribute(); - + var configuration = new ConfigurationRoot(new List { new MemoryConfigurationProvider(new MemoryConfigurationSource()) @@ -46,10 +46,10 @@ namespace MalwareMultiScan.Tests Assert.True(attribute.GetValidationResult( Mock.Of(x => x.Length == 10), context) == ValidationResult.Success); - + Assert.True(attribute.GetValidationResult( Mock.Of(x => x.Length == 100), context) == ValidationResult.Success); - + Assert.False(attribute.GetValidationResult( Mock.Of(x => x.Length == 101), context) == ValidationResult.Success); } diff --git a/MalwareMultiScan.Tests/ControllersTests.cs b/MalwareMultiScan.Tests/Api/ControllersTests.cs similarity index 95% rename from MalwareMultiScan.Tests/ControllersTests.cs rename to MalwareMultiScan.Tests/Api/ControllersTests.cs index 685ec08..8cdea7e 100644 --- a/MalwareMultiScan.Tests/ControllersTests.cs +++ b/MalwareMultiScan.Tests/Api/ControllersTests.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc; using Moq; using NUnit.Framework; -namespace MalwareMultiScan.Tests +namespace MalwareMultiScan.Tests.Api { public class ControllersTests { @@ -16,7 +16,7 @@ namespace MalwareMultiScan.Tests private QueueController _queueController; private ScanResultsController _scanResultsController; private Mock _scanResultServiceMock; - + [SetUp] public void SetUp() { @@ -25,7 +25,7 @@ namespace MalwareMultiScan.Tests _scanResultServiceMock .Setup(x => x.ObtainFile("existing")) .Returns(Task.FromResult(new MemoryStream() as Stream)); - + _scanResultServiceMock .Setup(x => x.ObtainFile("non-existing")) .Returns(Task.FromResult((Stream) null)); @@ -36,22 +36,22 @@ namespace MalwareMultiScan.Tests { Id = "existing" })); - + _scanResultServiceMock .Setup(x => x.GetScanResult("non-existing")) .Returns(Task.FromResult((ScanResult) null)); - + _scanResultServiceMock .Setup(x => x.ObtainFile("non-existing")) .Returns(Task.FromResult((Stream) null)); - + _scanResultServiceMock .Setup(x => x.CreateScanResult()) .Returns(Task.FromResult(new ScanResult())); _downloadController = new DownloadController(_scanResultServiceMock.Object); _scanResultsController = new ScanResultsController(_scanResultServiceMock.Object); - + _queueController = new QueueController(_scanResultServiceMock.Object) { Url = Mock.Of() @@ -71,7 +71,7 @@ namespace MalwareMultiScan.Tests Assert.True( await _scanResultsController.Index("existing") is OkObjectResult okObjectResult && okObjectResult.Value is ScanResult scanResult && scanResult.Id == "existing"); - + Assert.True(await _scanResultsController.Index("non-existing") is NotFoundResult); } @@ -86,16 +86,16 @@ namespace MalwareMultiScan.Tests _scanResultServiceMock.Verify( x => x.StoreFile("test.exe", It.IsAny()), Times.Once); - + _scanResultServiceMock.Verify( x => x.QueueUrlScan(It.IsAny(), It.IsAny()), Times.Once); } - + [Test] public async Task TestUrlQueue() { Assert.True(await _queueController.ScanUrl("http://test.com") is CreatedAtActionResult); - + _scanResultServiceMock.Verify( x => x.QueueUrlScan(It.IsAny(), "http://test.com"), Times.Once); } diff --git a/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs b/MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs similarity index 91% rename from MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs rename to MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs index 9cb976e..4790955 100644 --- a/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs +++ b/MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs @@ -11,19 +11,19 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace MalwareMultiScan.Tests +namespace MalwareMultiScan.Tests.Api { public class ReceiverHostedServiceTests { - private IReceiverHostedService _receiverHostedService; private Mock _busMock; + private IReceiverHostedService _receiverHostedService; private Mock _scanResultServiceMock; - + [SetUp] public void SetUp() { _busMock = new Mock(); - + _busMock .Setup(x => x.Receive("mms.results", It.IsAny>())) .Callback>((s, func) => @@ -39,9 +39,9 @@ namespace MalwareMultiScan.Tests task.Wait(); }); - + _scanResultServiceMock = new Mock(); - + var configuration = new ConfigurationRoot(new List { new MemoryConfigurationProvider(new MemoryConfigurationSource()) @@ -49,11 +49,11 @@ namespace MalwareMultiScan.Tests { ["ResultsSubscriptionId"] = "mms.results" }; - + _receiverHostedService = new ReceiverHostedService( - _busMock.Object, - configuration, - _scanResultServiceMock.Object, + _busMock.Object, + configuration, + _scanResultServiceMock.Object, Mock.Of>()); } diff --git a/MalwareMultiScan.Tests/ScanBackendServiceTests.cs b/MalwareMultiScan.Tests/Api/ScanBackendServiceTests.cs similarity index 88% rename from MalwareMultiScan.Tests/ScanBackendServiceTests.cs rename to MalwareMultiScan.Tests/Api/ScanBackendServiceTests.cs index 8af896b..8e8fbf2 100644 --- a/MalwareMultiScan.Tests/ScanBackendServiceTests.cs +++ b/MalwareMultiScan.Tests/Api/ScanBackendServiceTests.cs @@ -13,12 +13,12 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace MalwareMultiScan.Tests +namespace MalwareMultiScan.Tests.Api { public class ScanBackendServiceTests { - private IScanBackendService _scanBackendService; private Mock _busMock; + private IScanBackendService _scanBackendService; [SetUp] public void SetUp() @@ -30,12 +30,12 @@ namespace MalwareMultiScan.Tests { ["BackendsConfiguration"] = "backends.yaml" }; - + _busMock = new Mock(); - + _scanBackendService = new ScanBackendService( - configuration, - _busMock.Object, + configuration, + _busMock.Object, Mock.Of>()); } @@ -49,8 +49,8 @@ namespace MalwareMultiScan.Tests public async Task TestQueueUrlScan() { await _scanBackendService.QueueUrlScan( - new ScanResult { Id = "test"}, - new ScanBackend { Id = "dummy"}, + new ScanResult {Id = "test"}, + new ScanBackend {Id = "dummy"}, "http://test.com" ); diff --git a/MalwareMultiScan.Tests/ScanResultServiceTests.cs b/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs similarity index 90% rename from MalwareMultiScan.Tests/ScanResultServiceTests.cs rename to MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs index 48cf5f0..65c1bae 100644 --- a/MalwareMultiScan.Tests/ScanResultServiceTests.cs +++ b/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs @@ -11,32 +11,32 @@ using MongoDB.Driver.GridFS; using Moq; using NUnit.Framework; -namespace MalwareMultiScan.Tests +namespace MalwareMultiScan.Tests.Api { public class ScanResultServiceTest { - private IScanResultService _resultService; private MongoDbRunner _mongoDbRunner; + private IScanResultService _resultService; private Mock _scanBackendService; [SetUp] public void SetUp() { _mongoDbRunner = MongoDbRunner.Start(); - + var connection = new MongoClient(_mongoDbRunner.ConnectionString); var database = connection.GetDatabase("Test"); var gridFsBucket = new GridFSBucket(database); _scanBackendService = new Mock(); - + _scanBackendService .SetupGet(x => x.List) .Returns(() => new[] { new ScanBackend {Id = "dummy", Enabled = true}, new ScanBackend {Id = "clamav", Enabled = true}, - new ScanBackend {Id = "disabled", Enabled = false}, + new ScanBackend {Id = "disabled", Enabled = false} }); _resultService = new ScanResultService( @@ -57,23 +57,25 @@ namespace MalwareMultiScan.Tests string fileId; await using (var dataStream = new MemoryStream(originalData)) + { fileId = await _resultService.StoreFile("test.txt", dataStream); - + } + Assert.NotNull(fileId); - + Assert.IsNull(await _resultService.ObtainFile("wrong-id")); Assert.IsNull(await _resultService.ObtainFile(ObjectId.GenerateNewId().ToString())); - + await using var fileSteam = await _resultService.ObtainFile(fileId); - + var readData = new[] { - (byte)fileSteam.ReadByte(), - (byte)fileSteam.ReadByte(), - (byte)fileSteam.ReadByte(), - (byte)fileSteam.ReadByte() + (byte) fileSteam.ReadByte(), + (byte) fileSteam.ReadByte(), + (byte) fileSteam.ReadByte(), + (byte) fileSteam.ReadByte() }; - + Assert.That(readData, Is.EquivalentTo(originalData)); } @@ -81,7 +83,7 @@ namespace MalwareMultiScan.Tests public async Task TestScanResultCreateGet() { var result = await _resultService.CreateScanResult(); - + Assert.NotNull(result.Id); Assert.That(result.Results, Contains.Key("dummy")); @@ -94,7 +96,7 @@ namespace MalwareMultiScan.Tests Assert.That(result.Results, Contains.Key("dummy")); var dummyResult = result.Results["dummy"]; - + Assert.IsTrue(dummyResult.Completed); Assert.IsTrue(dummyResult.Succeeded); Assert.AreEqual(dummyResult.Duration, 100); @@ -109,9 +111,9 @@ namespace MalwareMultiScan.Tests await _resultService.QueueUrlScan( result, "http://url.com"); - + _scanBackendService.Verify(x => x.QueueUrlScan( - It.IsAny(), + It.IsAny(), It.IsAny(), "http://url.com"), Times.Exactly(2)); } diff --git a/MalwareMultiScan.Tests/Backends/BackendsTests.cs b/MalwareMultiScan.Tests/Backends/BackendsTests.cs new file mode 100644 index 0000000..619da9a --- /dev/null +++ b/MalwareMultiScan.Tests/Backends/BackendsTests.cs @@ -0,0 +1,162 @@ +using System.Threading; +using System.Threading.Tasks; +using MalwareMultiScan.Backends.Backends.Implementations; +using MalwareMultiScan.Backends.Interfaces; +using MalwareMultiScan.Backends.Services.Interfaces; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests.Backends +{ + public class BackendsTests + { + private static IProcessRunner GetProcessRunner(int exitCode, string stdOutput, string stdError) + { + var processRunnerMock = new Mock(); + + processRunnerMock + .Setup(p => p.RunTillCompletion( + It.IsAny(), + It.IsAny(), + It.IsAny(), + out It.Ref.IsAny, + out It.Ref.IsAny + )) + .Callback(new RunTillCompletion((string path, string arguments, CancellationToken token, + out string pOut, out string pErr) => + { + pOut = stdOutput; + pErr = stdError; + })).Returns(exitCode); + + return processRunnerMock.Object; + } + + [Test] + public async Task TestDummy() + { + var backend = new DummyScanBackend(); + + Assert.Contains("Malware.Dummy.Result", await backend.ScanAsync("test.exe", default)); + } + + private static async Task TestVirusDetected(IScanBackend backend) + { + Assert.Contains("Malware-Test-Result", await backend.ScanAsync("test.exe", default)); + } + + private static async Task TestVirusNotDetected(IScanBackend backend) + { + Assert.IsEmpty(await backend.ScanAsync("test.exe", default)); + } + + [Test] + public async Task TestClamav() + { + await TestVirusDetected(new ClamavScanBackend( + GetProcessRunner(1, "/worker/test.exe: Malware-Test-Result FOUND\n", null))); + + await TestVirusNotDetected(new ClamavScanBackend( + GetProcessRunner(0, "/worker/test.exe: OK\n", ""))); + } + + [Test] + public async Task TestWindowsDefender() + { + await TestVirusDetected(new WindowsDefenderScanBackend( + GetProcessRunner(0, null, "main(): Scanning /worker/test.exe...\n" + + "EngineScanCallback(): Scanning input\n" + + "EngineScanCallback(): Threat Malware-Test-Result identified."))); + + await TestVirusNotDetected(new WindowsDefenderScanBackend( + GetProcessRunner(0, null, "main(): Scanning /worker/test.exe...\n" + + "EngineScanCallback(): Scanning input"))); + } + + [Test] + public async Task TestSophos() + { + await TestVirusDetected(new SophosScanBackend( + GetProcessRunner(3, ">>> Virus 'Malware-Test-Result' found in file /worker/test.exe\n", null))); + + await TestVirusNotDetected(new SophosScanBackend( + GetProcessRunner(0, "", null))); + } + + [Test] + public async Task TestMcAfee() + { + await TestVirusDetected(new McAfeeScanBackend( + GetProcessRunner(1, "McAfee VirusScan Command Line for Linux64 Version: 6.0.4.564\n" + + "Copyright (C) 2013 McAfee, Inc.\n" + + "(408) 988-3832 EVALUATION COPY - October 28 2020\n\n" + + "AV Engine version: 5600.1067 for Linux64\n" + + "Dat set version: 9787 created Oct 27 2020\n" + + "Scanning for 668682 viruses, trojans and variants.\n\n" + + "/worker/test.exe ... Found: Malware-Test-Result.\n\n" + + "Time: 00:00.00", null))); + + await TestVirusNotDetected(new McAfeeScanBackend( + GetProcessRunner(0, "McAfee VirusScan Command Line for Linux64 Version: 6.0.4.564\n" + + "Copyright (C) 2013 McAfee, Inc.\n" + + "(408) 988-3832 EVALUATION COPY - October 28 2020\n\n" + + "AV Engine version: 5600.1067 for Linux64\n" + + "Dat set version: 9787 created Oct 27 2020\n" + + "Scanning for 668682 viruses, trojans and variants.\n\n" + + "Time: 00:00.00", null))); + } + + [Test] + public async Task TestKes() + { + await TestVirusDetected(new KesScanBackend( + GetProcessRunner(0, "ObjectId: 22\n\t\t" + + " FileName : /worker/test.exe\n" + + " DangerLevel : High\n" + + " DetectType : Virware\n" + + " DetectName : Malware-Test-Result\n" + + " CompoundObject : No\n" + + " AddTime : 2020-10-29 13:05:20\n" + + " FileSize : 68\n", null))); + + await TestVirusNotDetected(new KesScanBackend( + GetProcessRunner(0, "No files in Storage for the query\n", null))); + } + + [Test] + public async Task TestDrWeb() + { + await TestVirusDetected(new DrWebScanBackend( + GetProcessRunner(0, + "/worker/test.exe - infected with Malware-Test-Result\n" + + "Scanned objects: 1, scan errors: 0, threats found: 1, threats neutralized: 0.\n" + + "Scanned 0.07 KB in 5.39 s with speed 0.01 KB/s.", null))); + + await TestVirusNotDetected(new DrWebScanBackend( + GetProcessRunner(0, + "/worker/test.exe - Ok\n" + + "Scanned objects: 1, scan errors: 0, threats found: 0, threats neutralized: 0.", null))); + } + + [Test] + public async Task TestComodo() + { + await TestVirusDetected(new ComodoScanBackend( + GetProcessRunner(0, "-----== Scan Start ==-----\n" + + "/worker/test.exe ---> Found Virus, Malware Name is Malware-Test-Result\n" + + "-----== Scan End ==-----\n" + + "Number of Scanned Files: 1\n" + + "Number of Found Viruses: 0", null))); + + await TestVirusNotDetected(new ComodoScanBackend( + GetProcessRunner(0, "-----== Scan Start ==-----\n" + + "/worker/test.exe ---> Not Virus\n" + + "-----== Scan End ==-----\n" + + "Number of Scanned Files: 1\n" + + "Number of Found Viruses: 0", null))); + } + + private delegate void RunTillCompletion(string path, string arguments, CancellationToken token, + out string stdOut, out string stdErr); + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj b/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj index ac5f5d8..d58b0d8 100644 --- a/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj +++ b/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs b/MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs new file mode 100644 index 0000000..9f8c484 --- /dev/null +++ b/MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Backends.Interfaces; +using MalwareMultiScan.Backends.Messages; +using MalwareMultiScan.Scanner.Services.Implementations; +using MalwareMultiScan.Scanner.Services.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests.Scanner +{ + public class ScanHostedServiceTests + { + private Mock _busMock; + private Mock _scanBackendMock; + private IScanHostedService _scanHostedService; + + [SetUp] + public void SetUp() + { + var configuration = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }) + { + ["ResultsSubscriptionId"] = "mms.results" + }; + + _busMock = new Mock(); + + _busMock + .Setup(x => x.Receive("dummy", It.IsAny>())) + .Callback>((s, func) => + { + var task = func.Invoke(new ScanRequestMessage + { + Id = "test", + Uri = new Uri("http://test.com") + }); + + task.Wait(); + }); + + _scanBackendMock = new Mock(); + + _scanBackendMock + .Setup(x => x.ScanAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new[] {"Test"})); + + _scanBackendMock + .SetupGet(x => x.Id) + .Returns("dummy"); + + _scanHostedService = new ScanHostedService( + Mock.Of>(), + _scanBackendMock.Object, + _busMock.Object, + configuration); + } + + [Test] + public async Task TestBusReceiveScanResultMessage() + { + await _scanHostedService.StartAsync(default); + + _busMock.Verify(x => x.SendAsync("mms.results", It.Is( + m => m.Succeeded && m.Backend == "dummy" && m.Id == "test" && m.Threats.Contains("Test") + ))); + } + + [Test] + public async Task TestBusIsDisposedOnStop() + { + await _scanHostedService.StopAsync(default); + + _busMock.Verify(x => x.Dispose(), Times.Once); + } + } +} \ No newline at end of file