diff --git a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs index cda488e..e6be77f 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") + 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"); return ValidationResult.Success; 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 b2edfe3..17300fe 100644 --- a/MalwareMultiScan.Api/Controllers/DownloadController.cs +++ b/MalwareMultiScan.Api/Controllers/DownloadController.cs @@ -1,22 +1,33 @@ using System.Threading.Tasks; -using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace MalwareMultiScan.Api.Controllers { + /// + /// Downloads controller. + /// [ApiController] [Route("api/download")] [Produces("application/octet-stream")] public class DownloadController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public DownloadController(ScanResultService 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 5122797..4df3af3 100644 --- a/MalwareMultiScan.Api/Controllers/QueueController.cs +++ b/MalwareMultiScan.Api/Controllers/QueueController.cs @@ -2,48 +2,65 @@ 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; namespace MalwareMultiScan.Api.Controllers { + /// + /// Queue controller. + /// [ApiController] [Route("api/queue")] [Produces("application/json")] public class QueueController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public QueueController(ScanResultService 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.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); } + /// + /// 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 b67e1cf..a144f08 100644 --- a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs +++ b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs @@ -1,23 +1,34 @@ using System.Threading.Tasks; using MalwareMultiScan.Api.Data.Models; -using MalwareMultiScan.Api.Services; +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 ScanResultService _scanResultService; - - public ScanResultsController(ScanResultService scanResultService) + 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 feb6a21..5c18425 100644 --- a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs +++ b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,34 +1,28 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; -using EasyNetQ; using Microsoft.AspNetCore.Builder; 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) - { - 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/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 acaf861..d8993a8 100644 --- a/MalwareMultiScan.Api/Program.cs +++ b/MalwareMultiScan.Api/Program.cs @@ -1,9 +1,11 @@ +using EasyNetQ.LightInject; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace MalwareMultiScan.Api { - public static class Program + [ExcludeFromCodeCoverage] + internal 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 81% rename from MalwareMultiScan.Api/Services/ReceiverHostedService.cs rename to MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs index af4b2ed..86db217 100644 --- a/MalwareMultiScan.Api/Services/ReceiverHostedService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs @@ -1,31 +1,29 @@ 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; + private readonly IScanResultService _scanResultService; /// - /// Create receiver hosted service. + /// Initialize receiver hosted service. /// - /// Service bus. + /// EasyNetQ bus. /// Configuration. /// Scan result service. /// Logger. - public ReceiverHostedService(IBus bus, IConfiguration configuration, ScanResultService scanResultService, + public ReceiverHostedService(IBus bus, IConfiguration configuration, IScanResultService scanResultService, ILogger logger) { _bus = bus; @@ -34,13 +32,14 @@ namespace MalwareMultiScan.Api.Services _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)}"); @@ -56,11 +55,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 72% rename from MalwareMultiScan.Api/Services/ScanBackendService.cs rename to MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs index 71ca347..1e5cc33 100644 --- a/MalwareMultiScan.Api/Services/ScanBackendService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs @@ -4,29 +4,28 @@ 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. + /// Initialise scan backend service. /// /// Configuration. - /// Service bus. + /// EasyNetQ bus. /// Logger. - /// Missing BackendsConfiguration YAML file. + /// Missing backends.yaml configuration. public ScanBackendService(IConfiguration configuration, IBus bus, ILogger logger) { _bus = bus; @@ -46,17 +45,10 @@ 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 57% rename from MalwareMultiScan.Api/Services/ScanResultService.cs rename to MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs index 862b573..68aa580 100644 --- a/MalwareMultiScan.Api/Services/ScanResultService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs @@ -2,40 +2,37 @@ 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; + private readonly IScanBackendService _scanBackendService; /// - /// Create scan result service. + /// Initialize scan result service. /// /// Mongo database. + /// GridFS bucket. /// Scan backend service. - public ScanResultService(IMongoDatabase db, ScanBackendService 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 @@ -50,11 +47,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( @@ -63,15 +56,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) { @@ -86,23 +71,14 @@ namespace MalwareMultiScan.Api.Services })); } - /// - /// 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( @@ -111,20 +87,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..67ed8f0 --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..3cc6484 --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Configuration; +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 new file mode 100644 index 0000000..55cf404 --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs @@ -0,0 +1,59 @@ +using System.IO; +using System.Threading.Tasks; +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 1472e95..f23ba0c 100644 --- a/MalwareMultiScan.Api/Startup.cs +++ b/MalwareMultiScan.Api/Startup.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; using MalwareMultiScan.Api.Extensions; -using MalwareMultiScan.Api.Services; +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; @@ -7,7 +10,8 @@ using Microsoft.Extensions.DependencyInjection; namespace MalwareMultiScan.Api { - public class Startup + [ExcludeFromCodeCoverage] + internal class Startup { private readonly IConfiguration _configuration; @@ -19,7 +23,7 @@ namespace MalwareMultiScan.Api public void ConfigureServices(IServiceCollection services) { services.AddDockerForwardedHeadersOptions(); - + services.Configure(options => { options.Limits.MaxRequestBodySize = _configuration.GetValue("MaxFileSize"); @@ -28,8 +32,8 @@ namespace MalwareMultiScan.Api services.AddMongoDb(_configuration); services.AddRabbitMq(_configuration); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddControllers(); 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/Api/AttributesTests.cs b/MalwareMultiScan.Tests/Api/AttributesTests.cs new file mode 100644 index 0000000..86b2d4a --- /dev/null +++ b/MalwareMultiScan.Tests/Api/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.Api +{ + 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/Api/ControllersTests.cs b/MalwareMultiScan.Tests/Api/ControllersTests.cs new file mode 100644 index 0000000..8cdea7e --- /dev/null +++ b/MalwareMultiScan.Tests/Api/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.Api +{ + 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/Api/ReceiverHostedServiceTests.cs b/MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs new file mode 100644 index 0000000..4790955 --- /dev/null +++ b/MalwareMultiScan.Tests/Api/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.Api +{ + public class ReceiverHostedServiceTests + { + 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) => + { + 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/Api/ScanBackendServiceTests.cs b/MalwareMultiScan.Tests/Api/ScanBackendServiceTests.cs new file mode 100644 index 0000000..8e8fbf2 --- /dev/null +++ b/MalwareMultiScan.Tests/Api/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.Api +{ + public class ScanBackendServiceTests + { + private Mock _busMock; + private IScanBackendService _scanBackendService; + + [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/Api/ScanResultServiceTests.cs b/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs new file mode 100644 index 0000000..65c1bae --- /dev/null +++ b/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs @@ -0,0 +1,121 @@ +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.Api +{ + public class ScanResultServiceTest + { + 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} + }); + + _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.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 new file mode 100644 index 0000000..d58b0d8 --- /dev/null +++ b/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + 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 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