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