diff --git a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs
index e6be77f..5c49f61 100644
--- a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs
+++ b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs
@@ -8,9 +8,23 @@ namespace MalwareMultiScan.Api.Attributes
///
public class IsHttpUrlAttribute : ValidationAttribute
{
+ private readonly bool _failOnNull;
+
+ ///
+ /// Initialize http URL validation attribute.
+ ///
+ /// True if URL is mandatory, false otherwise.
+ public IsHttpUrlAttribute(bool failOnNull = true)
+ {
+ _failOnNull = failOnNull;
+ }
+
///
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
+ if (!_failOnNull && string.IsNullOrEmpty(value.ToString()))
+ return ValidationResult.Success;
+
if (!Uri.TryCreate((string) value, UriKind.Absolute, out var uri)
|| !uri.IsAbsoluteUri
|| uri.Scheme.ToLowerInvariant() != "http"
diff --git a/MalwareMultiScan.Api/Controllers/QueueController.cs b/MalwareMultiScan.Api/Controllers/QueueController.cs
index 4df3af3..bbee862 100644
--- a/MalwareMultiScan.Api/Controllers/QueueController.cs
+++ b/MalwareMultiScan.Api/Controllers/QueueController.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using MalwareMultiScan.Api.Attributes;
@@ -31,13 +32,16 @@ namespace MalwareMultiScan.Api.Controllers
/// Queue file for scanning.
///
/// File from form data.
+ /// Optional callback URL.
[HttpPost("file")]
[ProducesResponseType(typeof(ScanResult), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task ScanFile(
- [Required] [MaxFileSize] IFormFile file)
+ [Required] [MaxFileSize] IFormFile file,
+ [FromForm] [IsHttpUrl(false)] string callbackUrl = null)
{
- var result = await _scanResultService.CreateScanResult();
+ var result = await _scanResultService.CreateScanResult(
+ string.IsNullOrEmpty(callbackUrl) ? null : new Uri(callbackUrl));
string storedFileId;
@@ -56,13 +60,16 @@ namespace MalwareMultiScan.Api.Controllers
/// Queue URL for scanning.
///
/// URL from form data.
+ /// Optional callback URL.
[HttpPost("url")]
[ProducesResponseType(typeof(ScanResult), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task ScanUrl(
- [FromForm] [Required] [IsHttpUrl] string url)
+ [FromForm] [Required] [IsHttpUrl] string url,
+ [FromForm] [IsHttpUrl(false)] string callbackUrl = null)
{
- var result = await _scanResultService.CreateScanResult();
+ var result = await _scanResultService.CreateScanResult(
+ string.IsNullOrEmpty(callbackUrl) ? null : new Uri(callbackUrl));
await _scanResultService.QueueUrlScan(result, url);
diff --git a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs
index a144f08..c75dc4f 100644
--- a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs
+++ b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs
@@ -15,7 +15,7 @@ namespace MalwareMultiScan.Api.Controllers
public class ScanResultsController : Controller
{
private readonly IScanResultService _scanResultService;
-
+
///
/// Initialize scan results controller.
///
diff --git a/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs b/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs
index 75911a2..f75111c 100644
--- a/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs
+++ b/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs
@@ -9,7 +9,7 @@ namespace MalwareMultiScan.Api.Data.Configuration
/// Backend id.
///
public string Id { get; set; }
-
+
///
/// Backend state.
///
diff --git a/MalwareMultiScan.Api/Data/Models/ScanResult.cs b/MalwareMultiScan.Api/Data/Models/ScanResult.cs
index ce41477..4a9ddbe 100644
--- a/MalwareMultiScan.Api/Data/Models/ScanResult.cs
+++ b/MalwareMultiScan.Api/Data/Models/ScanResult.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
@@ -17,7 +18,12 @@ namespace MalwareMultiScan.Api.Data.Models
public string Id { get; set; }
///
- /// Result entries where key is backend id and value is .
+ /// Optional callback URL.
+ ///
+ public Uri CallbackUrl { 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 48af9ff..eca493d 100644
--- a/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs
+++ b/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs
@@ -9,17 +9,17 @@ namespace MalwareMultiScan.Api.Data.Models
/// 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.
///
diff --git a/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs
index 86db217..02b3452 100644
--- a/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs
+++ b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Net.Http;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using EasyNetQ;
@@ -5,6 +8,7 @@ using MalwareMultiScan.Api.Services.Interfaces;
using MalwareMultiScan.Backends.Messages;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
namespace MalwareMultiScan.Api.Services.Implementations
{
@@ -13,6 +17,7 @@ namespace MalwareMultiScan.Api.Services.Implementations
{
private readonly IBus _bus;
private readonly IConfiguration _configuration;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
private readonly IScanResultService _scanResultService;
@@ -23,31 +28,23 @@ namespace MalwareMultiScan.Api.Services.Implementations
/// Configuration.
/// Scan result service.
/// Logger.
+ /// HTTP client factory.
public ReceiverHostedService(IBus bus, IConfiguration configuration, IScanResultService scanResultService,
- ILogger logger)
+ ILogger logger, IHttpClientFactory httpClientFactory)
{
_bus = bus;
_configuration = configuration;
_scanResultService = scanResultService;
_logger = logger;
+ _httpClientFactory = httpClientFactory;
}
///
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)}");
-
- await _scanResultService.UpdateScanResultForBackend(
- message.Id, message.Backend, message.Duration, true,
- message.Succeeded, message.Threats);
- });
+ _bus.Receive(
+ _configuration.GetValue("ResultsSubscriptionId"), StoreScanResult);
_logger.LogInformation(
"Started hosted service for receiving scan results");
@@ -65,5 +62,42 @@ namespace MalwareMultiScan.Api.Services.Implementations
return Task.CompletedTask;
}
+
+ private async Task StoreScanResult(ScanResultMessage message)
+ {
+ message.Threats ??= new string[] { };
+
+ _logger.LogInformation(
+ $"Received a result from {message.Backend} for {message.Id} " +
+ $"with threats {string.Join(",", message.Threats)}");
+
+ await _scanResultService.UpdateScanResultForBackend(
+ message.Id, message.Backend, message.Duration, true,
+ message.Succeeded, message.Threats);
+
+ var result = await _scanResultService.GetScanResult(message.Id);
+
+ if (result?.CallbackUrl == null)
+ return;
+
+ var cancellationTokenSource = new CancellationTokenSource(
+ TimeSpan.FromSeconds(3));
+
+ using var httpClient = _httpClientFactory.CreateClient();
+
+ try
+ {
+ var response = await httpClient.PostAsync(
+ result.CallbackUrl,
+ new StringContent(JsonConvert.SerializeObject(result), Encoding.UTF8, "application/json"),
+ cancellationTokenSource.Token);
+
+ response.EnsureSuccessStatusCode();
+ }
+ catch (Exception exception)
+ {
+ _logger.LogError(exception, $"Failed to POST to callback URL {result.CallbackUrl}");
+ }
+ }
}
}
\ No newline at end of file
diff --git a/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs
index 68aa580..51b1812 100644
--- a/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs
+++ b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs
@@ -1,3 +1,4 @@
+using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -31,12 +32,14 @@ namespace MalwareMultiScan.Api.Services.Implementations
_collection = db.GetCollection(CollectionName);
}
-
+
///
- public async Task CreateScanResult()
+ public async Task CreateScanResult(Uri callbackUrl)
{
var scanResult = new ScanResult
{
+ CallbackUrl = callbackUrl,
+
Results = _scanBackendService.List
.Where(b => b.Enabled)
.ToDictionary(k => k.Id, v => new ScanResultEntry())
diff --git a/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs
index 55cf404..9f0b6d9 100644
--- a/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs
+++ b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs
@@ -1,3 +1,4 @@
+using System;
using System.IO;
using System.Threading.Tasks;
using MalwareMultiScan.Api.Data.Models;
@@ -12,8 +13,9 @@ namespace MalwareMultiScan.Api.Services.Interfaces
///
/// Create scan result.
///
+ /// Optional callback URL.
/// Scan result entry with id.
- Task CreateScanResult();
+ Task CreateScanResult(Uri callbackUrl);
///
/// Get scan result.
diff --git a/MalwareMultiScan.Api/Startup.cs b/MalwareMultiScan.Api/Startup.cs
index f23ba0c..3e8624c 100644
--- a/MalwareMultiScan.Api/Startup.cs
+++ b/MalwareMultiScan.Api/Startup.cs
@@ -38,6 +38,8 @@ namespace MalwareMultiScan.Api
services.AddControllers();
services.AddHostedService();
+
+ services.AddHttpClient();
}
public void Configure(IApplicationBuilder app)
diff --git a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs
index c52a616..034609c 100644
--- a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs
+++ b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs
@@ -22,17 +22,17 @@ namespace MalwareMultiScan.Backends.Backends.Abstracts
/// 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.
///
@@ -44,7 +44,7 @@ namespace MalwareMultiScan.Backends.Backends.Abstracts
/// Path to the temporary file.
/// Formatted string with parameters and path.
protected abstract string GetBackendArguments(string path);
-
+
///
public override Task ScanAsync(string path, CancellationToken cancellationToken)
{
diff --git a/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs
index 45b9f2f..4db9613 100644
--- a/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs
+++ b/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs
@@ -11,7 +11,7 @@ namespace MalwareMultiScan.Backends.Backends.Implementations
public WindowsDefenderScanBackend(IProcessRunner processRunner) : base(processRunner)
{
}
-
+
///
public override string Id { get; } = "windows-defender";
@@ -22,7 +22,7 @@ namespace MalwareMultiScan.Backends.Backends.Implementations
protected override Regex MatchRegex { get; } =
new Regex(@"EngineScanCallback\(\): Threat (?[\S]+) identified",
RegexOptions.Compiled | RegexOptions.Multiline);
-
+
///
protected override bool ParseStdErr { get; } = true;
diff --git a/MalwareMultiScan.Backends/Enums/BackendType.cs b/MalwareMultiScan.Backends/Enums/BackendType.cs
index e858ce2..5dda416 100644
--- a/MalwareMultiScan.Backends/Enums/BackendType.cs
+++ b/MalwareMultiScan.Backends/Enums/BackendType.cs
@@ -9,37 +9,37 @@ namespace MalwareMultiScan.Backends.Enums
/// Dummy
///
Dummy,
-
+
///
/// Windows Defender.
///
Defender,
-
+
///
/// ClamAV.
///
Clamav,
-
+
///
/// DrWeb.
///
DrWeb,
-
+
///
/// KES.
///
Kes,
-
+
///
/// Comodo.
///
Comodo,
-
+
///
/// Sophos.
///
Sophos,
-
+
///
/// McAfee.
///
diff --git a/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs b/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs
index 64f42be..b352ea0 100644
--- a/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs
+++ b/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs
@@ -14,7 +14,7 @@ namespace MalwareMultiScan.Backends.Interfaces
/// Unique backend id.
///
public string Id { get; }
-
+
///
/// Scan file.
///
@@ -22,7 +22,7 @@ namespace MalwareMultiScan.Backends.Interfaces
/// Cancellation token.
/// List of detected threats.
public Task ScanAsync(string path, CancellationToken cancellationToken);
-
+
///
/// Scan URL.
///
@@ -30,7 +30,7 @@ namespace MalwareMultiScan.Backends.Interfaces
/// Cancellation token.
/// List of detected threats.
public Task ScanAsync(Uri uri, CancellationToken cancellationToken);
-
+
///
/// Scan stream.
///
diff --git a/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs b/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs
index 515d64c..01686ff 100644
--- a/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs
+++ b/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs
@@ -9,7 +9,7 @@ namespace MalwareMultiScan.Backends.Messages
/// Result id.
///
public string Id { get; set; }
-
+
///
/// Backend.
///
@@ -19,7 +19,7 @@ namespace MalwareMultiScan.Backends.Messages
/// Status.
///
public bool Succeeded { get; set; }
-
+
///
/// List of detected threats.
///
diff --git a/MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs b/MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs
index 1805e34..2e4c6f3 100644
--- a/MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs
+++ b/MalwareMultiScan.Backends/Services/Implementations/ProcessRunner.cs
@@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging;
namespace MalwareMultiScan.Backends.Services.Implementations
{
///
- ///
///
[ExcludeFromCodeCoverage]
public class ProcessRunner : IProcessRunner
@@ -26,7 +25,6 @@ namespace MalwareMultiScan.Backends.Services.Implementations
}
///
- ///
///
///
///
diff --git a/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj b/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj
index 3fc1786..b904dcc 100644
--- a/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj
+++ b/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj
@@ -10,6 +10,8 @@
+
+
diff --git a/MalwareMultiScan.Scanner/Program.cs b/MalwareMultiScan.Scanner/Program.cs
index 94c5afc..8fc6e45 100644
--- a/MalwareMultiScan.Scanner/Program.cs
+++ b/MalwareMultiScan.Scanner/Program.cs
@@ -1,7 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
+using Hangfire;
+using Hangfire.MemoryStorage;
using MalwareMultiScan.Backends.Extensions;
using MalwareMultiScan.Scanner.Services.Implementations;
+using MalwareMultiScan.Scanner.Services.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -26,7 +29,16 @@ namespace MalwareMultiScan.Scanner
services.AddRabbitMq(context.Configuration);
services.AddScanningBackend(context.Configuration);
+ services.AddSingleton();
services.AddHostedService();
+
+ services.AddHangfire(
+ configuration => configuration.UseMemoryStorage());
+
+ services.AddHangfireServer(options =>
+ {
+ options.WorkerCount = context.Configuration.GetValue("WorkerCount");
+ });
}).RunConsoleAsync();
}
}
diff --git a/MalwareMultiScan.Scanner/Services/Implementations/ScanBackgroundJob.cs b/MalwareMultiScan.Scanner/Services/Implementations/ScanBackgroundJob.cs
new file mode 100644
index 0000000..0804298
--- /dev/null
+++ b/MalwareMultiScan.Scanner/Services/Implementations/ScanBackgroundJob.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+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.Logging;
+
+namespace MalwareMultiScan.Scanner.Services.Implementations
+{
+ ///
+ public class ScanBackgroundJob : IScanBackgroundJob
+ {
+ private readonly IScanBackend _backend;
+ private readonly IBus _bus;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initialize scan background job.
+ ///
+ /// Configuration.
+ /// Bus.
+ /// Logger.
+ /// Scan backend.
+ public ScanBackgroundJob(IConfiguration configuration, IBus bus,
+ ILogger logger, IScanBackend backend)
+ {
+ _configuration = configuration;
+ _bus = bus;
+ _logger = logger;
+ _backend = backend;
+ }
+
+ ///
+ public async Task Process(ScanRequestMessage message)
+ {
+ _logger.LogInformation(
+ $"Starting scan of {message.Uri} via backend {_backend.Id} from {message.Id}");
+
+ var cancellationTokenSource = new CancellationTokenSource(
+ TimeSpan.FromSeconds(_configuration.GetValue("MaxScanningTime")));
+
+ var result = new ScanResultMessage
+ {
+ Id = message.Id,
+ Backend = _backend.Id
+ };
+
+ var stopwatch = new Stopwatch();
+
+ stopwatch.Start();
+
+ try
+ {
+ result.Threats = await _backend.ScanAsync(
+ message.Uri, cancellationTokenSource.Token);
+
+ result.Succeeded = true;
+
+ _logger.LogInformation(
+ $"Backend {_backend.Id} completed a scan of {message.Id} " +
+ $"with result '{string.Join(", ", result.Threats)}'");
+ }
+ catch (Exception exception)
+ {
+ result.Succeeded = false;
+
+ _logger.LogError(
+ exception, "Scanning failed with exception");
+ }
+ finally
+ {
+ stopwatch.Stop();
+ }
+
+ result.Duration = stopwatch.ElapsedMilliseconds / 1000;
+
+ _logger.LogInformation(
+ $"Sending scan results with status {result.Succeeded}");
+
+ await _bus.SendAsync(
+ _configuration.GetValue("ResultsSubscriptionId"), result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/MalwareMultiScan.Scanner/Services/Implementations/ScanHostedService.cs b/MalwareMultiScan.Scanner/Services/Implementations/ScanHostedService.cs
index 9e6c2f2..180249a 100644
--- a/MalwareMultiScan.Scanner/Services/Implementations/ScanHostedService.cs
+++ b/MalwareMultiScan.Scanner/Services/Implementations/ScanHostedService.cs
@@ -1,12 +1,10 @@
-using System;
-using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using EasyNetQ;
+using Hangfire;
using MalwareMultiScan.Backends.Interfaces;
using MalwareMultiScan.Backends.Messages;
using MalwareMultiScan.Scanner.Services.Interfaces;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace MalwareMultiScan.Scanner.Services.Implementations
@@ -16,7 +14,6 @@ namespace MalwareMultiScan.Scanner.Services.Implementations
{
private readonly IScanBackend _backend;
private readonly IBus _bus;
- private readonly IConfiguration _configuration;
private readonly ILogger _logger;
///
@@ -25,22 +22,20 @@ namespace MalwareMultiScan.Scanner.Services.Implementations
/// Logger.
/// Scan backend.
/// EasyNetQ bus.
- /// Configuration.
public ScanHostedService(
ILogger logger,
- IScanBackend backend, IBus bus,
- IConfiguration configuration)
+ IScanBackend backend, IBus bus)
{
_logger = logger;
_bus = bus;
- _configuration = configuration;
_backend = backend;
}
///
public Task StartAsync(CancellationToken cancellationToken)
{
- _bus.Receive(_backend.Id, Scan);
+ _bus.Receive(_backend.Id, message =>
+ BackgroundJob.Enqueue(j => j.Process(message)));
_logger.LogInformation(
$"Started scan hosting service for the backend {_backend.Id}");
@@ -58,55 +53,5 @@ namespace MalwareMultiScan.Scanner.Services.Implementations
return Task.CompletedTask;
}
-
- private async Task Scan(ScanRequestMessage message)
- {
- _logger.LogInformation(
- $"Starting scan of {message.Uri} via backend {_backend.Id} from {message.Id}");
-
- var cancellationTokenSource = new CancellationTokenSource(
- TimeSpan.FromSeconds(_configuration.GetValue("MaxScanningTime")));
-
- var result = new ScanResultMessage
- {
- Id = message.Id,
- Backend = _backend.Id
- };
-
- var stopwatch = new Stopwatch();
-
- stopwatch.Start();
-
- try
- {
- result.Threats = await _backend.ScanAsync(
- message.Uri, cancellationTokenSource.Token);
-
- result.Succeeded = true;
-
- _logger.LogInformation(
- $"Backend {_backend.Id} completed a scan of {message.Id} " +
- $"with result '{string.Join(", ", result.Threats)}'");
- }
- catch (Exception exception)
- {
- result.Succeeded = false;
-
- _logger.LogError(
- exception, "Scanning failed with exception");
- }
- finally
- {
- stopwatch.Stop();
- }
-
- result.Duration = stopwatch.ElapsedMilliseconds / 1000;
-
- _logger.LogInformation(
- $"Sending scan results with status {result.Succeeded}");
-
- await _bus.SendAsync(
- _configuration.GetValue("ResultsSubscriptionId"), result);
- }
}
}
\ No newline at end of file
diff --git a/MalwareMultiScan.Scanner/Services/Interfaces/IScanBackgroundJob.cs b/MalwareMultiScan.Scanner/Services/Interfaces/IScanBackgroundJob.cs
new file mode 100644
index 0000000..e688dc9
--- /dev/null
+++ b/MalwareMultiScan.Scanner/Services/Interfaces/IScanBackgroundJob.cs
@@ -0,0 +1,17 @@
+using System.Threading.Tasks;
+using MalwareMultiScan.Backends.Messages;
+
+namespace MalwareMultiScan.Scanner.Services.Interfaces
+{
+ ///
+ /// Background scanning job.
+ ///
+ public interface IScanBackgroundJob
+ {
+ ///
+ /// Process scan request in the background worker.
+ ///
+ /// Request message.
+ Task Process(ScanRequestMessage requestMessage);
+ }
+}
\ No newline at end of file
diff --git a/MalwareMultiScan.Scanner/appsettings.json b/MalwareMultiScan.Scanner/appsettings.json
index 3c086e9..55e4b45 100644
--- a/MalwareMultiScan.Scanner/appsettings.json
+++ b/MalwareMultiScan.Scanner/appsettings.json
@@ -11,6 +11,7 @@
"MaxScanningTime": 60,
"ResultsSubscriptionId": "mms.results",
+ "WorkerCount": 4,
"ConnectionStrings": {
"RabbitMQ": "host=localhost;prefetchcount=1"
diff --git a/MalwareMultiScan.Tests/Api/ControllersTests.cs b/MalwareMultiScan.Tests/Api/ControllersTests.cs
index 8cdea7e..7383af2 100644
--- a/MalwareMultiScan.Tests/Api/ControllersTests.cs
+++ b/MalwareMultiScan.Tests/Api/ControllersTests.cs
@@ -46,7 +46,7 @@ namespace MalwareMultiScan.Tests.Api
.Returns(Task.FromResult((Stream) null));
_scanResultServiceMock
- .Setup(x => x.CreateScanResult())
+ .Setup(x => x.CreateScanResult(null))
.Returns(Task.FromResult(new ScanResult()));
_downloadController = new DownloadController(_scanResultServiceMock.Object);
diff --git a/MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs b/MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs
index 4790955..a92f897 100644
--- a/MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs
+++ b/MalwareMultiScan.Tests/Api/ReceiverHostedServiceTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading.Tasks;
using EasyNetQ;
using MalwareMultiScan.Api.Services.Implementations;
@@ -50,11 +51,14 @@ namespace MalwareMultiScan.Tests.Api
["ResultsSubscriptionId"] = "mms.results"
};
+ var httpClientFactoryMock = new Mock();
+
_receiverHostedService = new ReceiverHostedService(
_busMock.Object,
configuration,
_scanResultServiceMock.Object,
- Mock.Of>());
+ Mock.Of>(),
+ httpClientFactoryMock.Object);
}
[Test]
diff --git a/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs b/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs
index 60b8d58..e85cdbb 100644
--- a/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs
+++ b/MalwareMultiScan.Tests/Api/ScanResultServiceTests.cs
@@ -82,7 +82,7 @@ namespace MalwareMultiScan.Tests.Api
[Test]
public async Task TestScanResultCreateGet()
{
- var result = await _resultService.CreateScanResult();
+ var result = await _resultService.CreateScanResult(null);
Assert.NotNull(result.Id);
Assert.That(result.Results, Contains.Key("dummy"));
@@ -107,7 +107,7 @@ namespace MalwareMultiScan.Tests.Api
[Test]
public async Task TestQueueUrlScan()
{
- var result = await _resultService.CreateScanResult();
+ var result = await _resultService.CreateScanResult(null);
await _resultService.QueueUrlScan(
result, "http://url.com");
diff --git a/MalwareMultiScan.Tests/Scanner/ScanBackgroundJobTests.cs b/MalwareMultiScan.Tests/Scanner/ScanBackgroundJobTests.cs
new file mode 100644
index 0000000..d255d03
--- /dev/null
+++ b/MalwareMultiScan.Tests/Scanner/ScanBackgroundJobTests.cs
@@ -0,0 +1,68 @@
+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 ScanBackgroundJobTests
+ {
+ private Mock _busMock;
+ private Mock _scanBackendMock;
+ private IScanBackgroundJob _scanBackgroundJob;
+
+ [SetUp]
+ public void SetUp()
+ {
+ var configuration = new ConfigurationRoot(new List
+ {
+ new MemoryConfigurationProvider(new MemoryConfigurationSource())
+ })
+ {
+ ["ResultsSubscriptionId"] = "mms.results"
+ };
+
+ _busMock = new Mock();
+
+ _scanBackendMock = new Mock();
+
+ _scanBackendMock
+ .SetupGet(x => x.Id)
+ .Returns("dummy");
+
+ _scanBackendMock
+ .Setup(x => x.ScanAsync(It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(new[] {"Test"}));
+
+ _scanBackgroundJob = new ScanBackgroundJob(configuration, _busMock.Object,
+ Mock.Of>(), _scanBackendMock.Object);
+ }
+
+ [Test]
+ public async Task TestMessageProcessing()
+ {
+ await _scanBackgroundJob.Process(new ScanRequestMessage
+ {
+ Id = "test",
+ Uri = new Uri("http://test.com")
+ });
+
+ _scanBackendMock.Verify(
+ x => x.ScanAsync(It.IsAny(), It.IsAny()));
+
+ _busMock.Verify(x => x.SendAsync("mms.results", It.Is(m =>
+ m.Succeeded && m.Backend == "dummy" && m.Id == "test" && m.Threats.Contains("Test"))));
+ }
+ }
+}
\ No newline at end of file
diff --git a/MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs b/MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs
index 9f8c484..a7a2a19 100644
--- a/MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs
+++ b/MalwareMultiScan.Tests/Scanner/ScanHostedServiceTests.cs
@@ -1,15 +1,10 @@
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;
@@ -25,35 +20,10 @@ namespace MalwareMultiScan.Tests.Scanner
[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");
@@ -61,8 +31,7 @@ namespace MalwareMultiScan.Tests.Scanner
_scanHostedService = new ScanHostedService(
Mock.Of>(),
_scanBackendMock.Object,
- _busMock.Object,
- configuration);
+ _busMock.Object);
}
[Test]
@@ -70,9 +39,8 @@ namespace MalwareMultiScan.Tests.Scanner
{
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")
- )));
+ _busMock.Verify(
+ x => x.Receive("dummy", It.IsAny>()));
}
[Test]
diff --git a/MalwareMultiScan.Ui/pages/index.vue b/MalwareMultiScan.Ui/pages/index.vue
index e0769a2..a0bedb7 100644
--- a/MalwareMultiScan.Ui/pages/index.vue
+++ b/MalwareMultiScan.Ui/pages/index.vue
@@ -9,6 +9,9 @@
+
+
@@ -19,6 +22,9 @@
Scan
+
+
@@ -37,6 +43,7 @@ export default Vue.extend({
file: null as File | null,
url: '' as string,
+ callbackUrl: '' as string
}
},
@@ -53,6 +60,9 @@ export default Vue.extend({
data.append('file', this.file, this.file.name);
+ if (this.isUrl(this.callbackUrl))
+ data.append('callbackUrl', this.callbackUrl);
+
try {
this.uploading = true;
@@ -80,6 +90,9 @@ export default Vue.extend({
data.append('url', this.url);
+ if (this.isUrl(this.callbackUrl))
+ data.append('callbackUrl', this.callbackUrl);
+
const result = await this.$axios.$post('queue/url', data);
await this.$router.push({name: 'id', params: {id: result.id}});
diff --git a/MalwareMultiScan.sln b/MalwareMultiScan.sln
index be584ea..2c0b362 100644
--- a/MalwareMultiScan.sln
+++ b/MalwareMultiScan.sln
@@ -11,6 +11,8 @@ ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
.gitignore = .gitignore
docker-compose.yaml = docker-compose.yaml
+ README.md = README.md
+ LICENSE = LICENSE
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Tests", "MalwareMultiScan.Tests\MalwareMultiScan.Tests.csproj", "{9896162D-8FC7-4911-933F-A78C94128923}"
diff --git a/README.md b/README.md
index 8d8e0fb..5e9913d 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,12 @@ In the end, it's nothing but the set of Docker containers running the agent. Tha
**IMPORTANT**: MalwareMultiScan is not intended as a publicly-facing API / UI. It has (intentionally) no authorization, authentication, rate-limiting, or logging. Therefore, it should be used only as an internal / private API or behind the restrictive API gateway.
+Whole solution can be started with `docker-compose up` executed in a root folder of repository.
+
+It can be also deployed to the Docker Swarm cluster by using the command `docker stack deploy malware-multi-scan --compose-file docker-compose.yaml`.
+
+After the start the Demo Web UI will become available under http://localhost:8888.
+
See [components](#components) chapter below and the [docker-compose.yaml](docker-compose.yaml) file.
### Configuration
@@ -48,18 +54,22 @@ Configuration of API and Scanners is performed by passing the environment variab
* `MaxScanningTime=60` - Scan time limit. It is used not just for actual scanning but also for getting the file.
+* `WorkerCount=4` - Number of workers for parallel scanning.
+
#### MalwareMultiScan.Ui
* `API_URL=http://localhost:5000` - Absolute URL incl. port number for the running instance of MalwareMultiScan.Api.
### API Endpoints
-* POST `/api/queue/url` with a `url` parameter passed via the form data. Returns `201 Accepted` response with a [ScanResult](MalwareMultiScan.Api/Data/Models/ScanResult.cs) or `400 Bad Request` error.
+* POST `/api/queue/url` with a `url` parameter passed via the form data.. Returns `201 Accepted` response with a [ScanResult](MalwareMultiScan.Api/Data/Models/ScanResult.cs) or `400 Bad Request` error.
- * POST `/api/queue/file` with a `file` parameter passed via the form data. Returns `201 Accepted` response with a [ScanResult](MalwareMultiScan.Api/Data/Models/ScanResult.cs) or `400 Bad Request` error.
+* POST `/api/queue/file` with a `file` parameter passed via the form data. Returns `201 Accepted` response with a [ScanResult](MalwareMultiScan.Api/Data/Models/ScanResult.cs) or `400 Bad Request` error.
* GET `/api/results/{result-id}` where `{result-id}` corresponds to the id value of a [ScanResult](MalwareMultiScan.Api/Data/Models/ScanResult.cs). Returns `200 OK` response with a [ScanResult](MalwareMultiScan.Api/Data/Models/ScanResult.cs) or `404 Not Found` error.
+Both `/api/queue/url` and `/api/queue/file` also accept an optional `callbackUrl` parameter with the http(s) URL in it. This URL will be requested by the POST method with JSON serialized [ScanResult](MalwareMultiScan.Api/Data/Models/ScanResult.cs) in a body on every update from scan backends.
+
## Supported Scan Engines
| Name | Dockerfile | Enabled | Comments |
@@ -85,7 +95,7 @@ More scan backends can be added in the future. Some of the popular ones do not h
* **Docker** and **docker-compose** running under Windows (in Linux containers mode), Linux, or OSX. Docker Compose is needed only for test / local deployments.
-* **Optional**: DockerSwarm / Kubernetes cluster for scaling up the scanning capacities. A simultaneous scan by a single node is not supported, yet load-balancing is possible by the built-in orchestrators' round-robin routing.
+* **Optional**: DockerSwarm / Kubernetes cluster for scaling up the scanning capacities.
### Parts
@@ -99,4 +109,4 @@ More scan backends can be added in the future. Some of the popular ones do not h
## Plans
- See [issues](https://github.com/mindcollapse/MalwareMultiScan/issues) for the list of planned features, bug-fixes, and improvements.
+ See [issues](https://github.com/mindcollapse/MalwareMultiScan/issues) for the list of planned features, bug-fixes, and improvements.
\ No newline at end of file