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