diff --git a/.dockerignore b/.dockerignore index 217331b..c377dad 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ .git +*.Dockerfile Dockerfile bin obj \ No newline at end of file diff --git a/.idea/.idea.MalwareMultiScan/.idea/runConfigurations/MalwareMultiScan_Worker_Dockerfile.xml b/.idea/.idea.MalwareMultiScan/.idea/runConfigurations/MalwareMultiScan_Scanner_Dockerfile.xml similarity index 72% rename from .idea/.idea.MalwareMultiScan/.idea/runConfigurations/MalwareMultiScan_Worker_Dockerfile.xml rename to .idea/.idea.MalwareMultiScan/.idea/runConfigurations/MalwareMultiScan_Scanner_Dockerfile.xml index 5c2be23..5a37aed 100644 --- a/.idea/.idea.MalwareMultiScan/.idea/runConfigurations/MalwareMultiScan_Worker_Dockerfile.xml +++ b/.idea/.idea.MalwareMultiScan/.idea/runConfigurations/MalwareMultiScan_Scanner_Dockerfile.xml @@ -1,5 +1,5 @@ - + diff --git a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs new file mode 100644 index 0000000..cda488e --- /dev/null +++ b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace MalwareMultiScan.Api.Attributes +{ + /// + /// Validate URI to be an absolute http(s) URL. + /// + internal 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") + return new ValidationResult("Only absolute http(s) URLs are supported"); + + return ValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Attributes/MaxFileSizeAttribute.cs b/MalwareMultiScan.Api/Attributes/MaxFileSizeAttribute.cs new file mode 100644 index 0000000..56baa38 --- /dev/null +++ b/MalwareMultiScan.Api/Attributes/MaxFileSizeAttribute.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace MalwareMultiScan.Api.Attributes +{ + /// + /// Validate uploaded file size for the max file size defined in settings. + /// + public class MaxFileSizeAttribute : ValidationAttribute + { + /// + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + 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; + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Controllers/DownloadController.cs b/MalwareMultiScan.Api/Controllers/DownloadController.cs new file mode 100644 index 0000000..af088da --- /dev/null +++ b/MalwareMultiScan.Api/Controllers/DownloadController.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using MalwareMultiScan.Api.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace MalwareMultiScan.Api.Controllers +{ + [ApiController] + [Route("download")] + [Produces("application/octet-stream")] + public class DownloadController : Controller + { + private readonly ScanResultService _scanResultService; + + public DownloadController(ScanResultService scanResultService) + { + _scanResultService = scanResultService; + } + + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Index(string id) + { + var fileStream = await _scanResultService.ObtainFile(id); + + if (fileStream == null) + return NotFound(); + + return File(fileStream, "application/octet-stream"); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Controllers/QueueController.cs b/MalwareMultiScan.Api/Controllers/QueueController.cs new file mode 100644 index 0000000..b27e0db --- /dev/null +++ b/MalwareMultiScan.Api/Controllers/QueueController.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Attributes; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace MalwareMultiScan.Api.Controllers +{ + [ApiController] + [Route("queue")] + [Produces("application/json")] + public class QueueController : Controller + { + private readonly ScanResultService _scanResultService; + + public QueueController(ScanResultService scanResultService) + { + _scanResultService = scanResultService; + } + + [HttpPost("file")] + [ProducesResponseType(typeof(ScanResult), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task ScanFile( + [Required, MaxFileSize] IFormFile file) + { + var result = await _scanResultService.CreateScanResult(); + + string storedFileId; + + await using (var uploadFileStream = file.OpenReadStream()) + storedFileId = await _scanResultService.StoreFile(file.Name, uploadFileStream); + + await _scanResultService.QueueUrlScan(result, Url.Action("Index", "Download", new {id = storedFileId}, + Request.Scheme, Request.Host.Value)); + + return CreatedAtAction("Index", "ScanResults", new {id = result.Id}, result); + } + + [HttpPost("url")] + [ProducesResponseType(typeof(ScanResult), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task ScanUrl( + [FromForm, Required, IsHttpUrl] string url) + { + var result = await _scanResultService.CreateScanResult(); + + await _scanResultService.QueueUrlScan(result, url); + + return CreatedAtAction("Index", "ScanResults", new {id = result.Id}, result); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs new file mode 100644 index 0000000..661a873 --- /dev/null +++ b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace MalwareMultiScan.Api.Controllers +{ + [ApiController] + [Route("results")] + [Produces("application/json")] + public class ScanResultsController : Controller + { + private readonly ScanResultService _scanResultService; + + public ScanResultsController(ScanResultService scanResultService) + { + _scanResultService = scanResultService; + } + + [HttpGet("{id}")] + [ProducesResponseType(typeof(ScanResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Index(string id) + { + var scanResult = await _scanResultService.GetScanResult(id); + + if (scanResult == null) + return NotFound(); + + return Ok(scanResult); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs b/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs new file mode 100644 index 0000000..55f369c --- /dev/null +++ b/MalwareMultiScan.Api/Data/Configuration/ScanBackend.cs @@ -0,0 +1,8 @@ +namespace MalwareMultiScan.Api.Data.Configuration +{ + public class ScanBackend + { + public string Id { get; set; } + 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 new file mode 100644 index 0000000..76315c8 --- /dev/null +++ b/MalwareMultiScan.Api/Data/Models/ScanResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace MalwareMultiScan.Api.Data.Models +{ + public class ScanResult + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public Dictionary Results { get; set; } = + new Dictionary(); + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs b/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs new file mode 100644 index 0000000..a566c3d --- /dev/null +++ b/MalwareMultiScan.Api/Data/Models/ScanResultEntry.cs @@ -0,0 +1,9 @@ +namespace MalwareMultiScan.Api.Data.Models +{ + public class ScanResultEntry + { + public bool Completed { get; set; } + public bool? Succeeded { get; set; } + public string[] Threats { get; set; } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Dockerfile b/MalwareMultiScan.Api/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..feb6a21 --- /dev/null +++ b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +using System.Net; +using EasyNetQ; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; + +namespace MalwareMultiScan.Api.Extensions +{ + internal static class ServiceCollectionExtensions + { + public static void AddMongoDb(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton( + serviceProvider => + { + var db = new MongoClient(configuration.GetConnectionString("Mongo")); + + return db.GetDatabase( + configuration.GetValue("DatabaseName")); + }); + } + + public static void AddRabbitMq(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(x => + RabbitHutch.CreateBus(configuration.GetConnectionString("RabbitMQ"))); + } + + public static void AddDockerForwardedHeadersOptions(this IServiceCollection services) + { + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("::ffff:10.0.0.0"), 104)); + options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("::ffff:192.168.0.0"), 112)); + options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("::ffff:172.16.0.0"), 108)); + }); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/MalwareMultiScan.Api.csproj b/MalwareMultiScan.Api/MalwareMultiScan.Api.csproj new file mode 100644 index 0000000..312f84f --- /dev/null +++ b/MalwareMultiScan.Api/MalwareMultiScan.Api.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.1 + + + + + Always + + + + + + + + + + + + + + diff --git a/MalwareMultiScan.Worker/Program.cs b/MalwareMultiScan.Api/Program.cs similarity index 75% rename from MalwareMultiScan.Worker/Program.cs rename to MalwareMultiScan.Api/Program.cs index afcc239..acaf861 100644 --- a/MalwareMultiScan.Worker/Program.cs +++ b/MalwareMultiScan.Api/Program.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace MalwareMultiScan.Worker +namespace MalwareMultiScan.Api { public static class Program { @@ -13,7 +13,7 @@ namespace MalwareMultiScan.Worker private static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(builder => { builder.UseStartup(); }); + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); } } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/ReceiverHostedService.cs b/MalwareMultiScan.Api/Services/ReceiverHostedService.cs new file mode 100644 index 0000000..9ead132 --- /dev/null +++ b/MalwareMultiScan.Api/Services/ReceiverHostedService.cs @@ -0,0 +1,69 @@ +using System.Threading; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Backends.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MalwareMultiScan.Api.Services +{ + /// + /// Receiver hosted service. + /// + public class ReceiverHostedService : IHostedService + { + private readonly IBus _bus; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly ScanResultService _scanResultService; + + /// + /// Create receiver hosted service. + /// + /// Service bus. + /// Configuration. + /// Scan result service. + /// Logger. + public ReceiverHostedService(IBus bus, IConfiguration configuration, ScanResultService scanResultService, + ILogger logger) + { + _bus = bus; + _configuration = configuration; + _scanResultService = scanResultService; + _logger = logger; + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + _bus.Receive(_configuration.GetValue("ResultsSubscriptionId"), async message => + { + _logger.LogInformation( + $"Received a result from {message.Backend} for {message.Id} " + + $"with threats {string.Join(",", message.Threats)}"); + + await _scanResultService.UpdateScanResultForBackend( + message.Id, message.Backend, true, + message.Succeeded, message.Threats ?? new string[] { }); + }); + + _logger.LogInformation( + "Started hosted service for receiving scan results"); + + return Task.CompletedTask; + } + + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _bus.Dispose(); + + _logger.LogInformation( + "Stopped hosted service for receiving scan results"); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/ScanBackendService.cs b/MalwareMultiScan.Api/Services/ScanBackendService.cs new file mode 100644 index 0000000..71ca347 --- /dev/null +++ b/MalwareMultiScan.Api/Services/ScanBackendService.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Api.Data.Configuration; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Backends.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace MalwareMultiScan.Api.Services +{ + /// + /// Scan backends service. + /// + public class ScanBackendService + { + private readonly IBus _bus; + private readonly ILogger _logger; + + /// + /// Create scan backend service. + /// + /// Configuration. + /// Service bus. + /// Logger. + /// Missing BackendsConfiguration YAML file. + public ScanBackendService(IConfiguration configuration, IBus bus, ILogger logger) + { + _bus = bus; + _logger = logger; + + var configurationPath = configuration.GetValue("BackendsConfiguration"); + + if (!File.Exists(configurationPath)) + throw new FileNotFoundException("Missing BackendsConfiguration YAML file", configurationPath); + + var configurationContent = File.ReadAllText(configurationPath); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + 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( + $"Queueing scan for {result.Id} on {backend.Id} at {fileUrl}"); + + await _bus.SendAsync(backend.Id, new ScanRequestMessage + { + Id = result.Id, + Uri = new Uri(fileUrl) + }); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/ScanResultService.cs b/MalwareMultiScan.Api/Services/ScanResultService.cs new file mode 100644 index 0000000..231c88e --- /dev/null +++ b/MalwareMultiScan.Api/Services/ScanResultService.cs @@ -0,0 +1,128 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Models; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; + +namespace MalwareMultiScan.Api.Services +{ + /// + /// Scan results service. + /// + public class ScanResultService + { + private const string CollectionName = "ScanResults"; + + private readonly GridFSBucket _bucket; + private readonly IMongoCollection _collection; + private readonly ScanBackendService _scanBackendService; + + /// + /// Create scan result service. + /// + /// Mongo database. + /// Scan backend service. + public ScanResultService(IMongoDatabase db, ScanBackendService scanBackendService) + { + _scanBackendService = scanBackendService; + + _bucket = new GridFSBucket(db); + _collection = db.GetCollection(CollectionName); + } + + /// + /// Create scan result. + /// + /// Scan result. + public async Task CreateScanResult() + { + var scanResult = new ScanResult + { + Results = _scanBackendService.List + .Where(b => b.Enabled) + .ToDictionary(k => k.Id, v => new ScanResultEntry()) + }; + + await _collection.InsertOneAsync(scanResult); + + return scanResult; + } + + /// + /// Get scan result. + /// + /// Scan result id. + /// Scan result. + public async Task GetScanResult(string id) + { + var result = await _collection.FindAsync( + Builders.Filter.Where(r => r.Id == id)); + + return await result.FirstOrDefaultAsync(); + } + + /// + /// Update scan status for the backend. + /// + /// Result id. + /// Backend id. + /// If the scan has been completed. + /// If the scan has been succeeded. + /// List of found threats. + public async Task UpdateScanResultForBackend(string resultId, string backendId, + bool completed = false, bool succeeded = false, string[] threats = null) + { + await _collection.UpdateOneAsync( + Builders.Filter.Where(r => r.Id == resultId), + Builders.Update.Set(r => r.Results[backendId], new ScanResultEntry + { + Completed = completed, + Succeeded = succeeded, + Threats = threats ?? new string[] { } + })); + } + + /// + /// Queue URL scan. + /// + /// Scan result instance. + /// File URL. + public async Task QueueUrlScan(ScanResult result, string fileUrl) + { + foreach (var backend in _scanBackendService.List.Where(b => b.Enabled)) + await _scanBackendService.QueueUrlScan(result, backend, fileUrl); + } + + /// + /// Store file. + /// + /// File name. + /// File stream. + /// Stored file id. + public async Task StoreFile(string fileName, Stream fileStream) + { + var objectId = await _bucket.UploadFromStreamAsync( + fileName, fileStream); + + 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 + { + Seekable = true + }); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Startup.cs b/MalwareMultiScan.Api/Startup.cs new file mode 100644 index 0000000..5039640 --- /dev/null +++ b/MalwareMultiScan.Api/Startup.cs @@ -0,0 +1,40 @@ +using MalwareMultiScan.Api.Extensions; +using MalwareMultiScan.Api.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace MalwareMultiScan.Api +{ + public class Startup + { + private readonly IConfiguration _configuration; + + public Startup(IConfiguration configuration) + { + _configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddDockerForwardedHeadersOptions(); + + services.AddMongoDb(_configuration); + services.AddRabbitMq(_configuration); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddControllers(); + + services.AddHostedService(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseForwardedHeaders(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/appsettings.json b/MalwareMultiScan.Api/appsettings.json new file mode 100644 index 0000000..c2e1ca1 --- /dev/null +++ b/MalwareMultiScan.Api/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + + "ConnectionStrings": { + "Mongo": "mongodb://localhost:27017", + "RabbitMQ": "host=localhost" + }, + + "DatabaseName": "MalwareMultiScan", + "ResultsSubscriptionId": "mms.results", + "MaxFileSize": 1048576, + "BackendsConfiguration": "backends.yaml" +} diff --git a/MalwareMultiScan.Api/backends.yaml b/MalwareMultiScan.Api/backends.yaml new file mode 100644 index 0000000..586e21a --- /dev/null +++ b/MalwareMultiScan.Api/backends.yaml @@ -0,0 +1,2 @@ +- id: dummy + enabled: true \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs index f46606b..802750c 100644 --- a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractLocalProcessScanBackend.cs @@ -18,15 +18,12 @@ namespace MalwareMultiScan.Backends.Backends.Abstracts _logger = logger; } - protected virtual Regex MatchRegex { get; } - protected virtual string BackendPath { get; } - protected virtual bool ParseStdErr { get; } + protected abstract Regex MatchRegex { get; } + protected abstract string BackendPath { get; } + protected virtual bool ParseStdErr { get; } = false; protected virtual bool ThrowOnNonZeroExitCode { get; } = true; - protected virtual string GetBackendArguments(string path) - { - throw new NotImplementedException(); - } + protected abstract string GetBackendArguments(string path); public override async Task ScanAsync(string path, CancellationToken cancellationToken) { @@ -56,13 +53,13 @@ namespace MalwareMultiScan.Backends.Backends.Abstracts 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}"); diff --git a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs index da66eb7..6229b1f 100644 --- a/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Abstracts/AbstractScanBackend.cs @@ -3,37 +3,25 @@ using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MalwareMultiScan.Shared.Interfaces; -using Microsoft.AspNetCore.Http; +using MalwareMultiScan.Backends.Interfaces; namespace MalwareMultiScan.Backends.Backends.Abstracts { public abstract class AbstractScanBackend : IScanBackend { - public virtual string Id => throw new NotImplementedException(); + public abstract string Id { get; } - public virtual DateTime DatabaseLastUpdate => throw new NotImplementedException(); - - public virtual Task ScanAsync(string path, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public abstract Task ScanAsync(string path, CancellationToken cancellationToken); public async Task ScanAsync(Uri uri, CancellationToken cancellationToken) { using var httpClient = new HttpClient(); + await using var uriStream = await httpClient.GetStreamAsync(uri); return await ScanAsync(uriStream, cancellationToken); } - public async Task ScanAsync(IFormFile file, CancellationToken cancellationToken) - { - await using var fileStream = file.OpenReadStream(); - - return await ScanAsync(fileStream, 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 2aa6e69..8798d8c 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/ClamavScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/ClamavScanBackend.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; using Microsoft.Extensions.Logging; @@ -11,17 +9,16 @@ namespace MalwareMultiScan.Backends.Backends.Implementations public ClamavScanBackend(ILogger logger) : base(logger) { } - + public override string Id { get; } = "clamav"; - - public override DateTime DatabaseLastUpdate => - File.GetLastWriteTime("/var/lib/clamav/daily.cvd"); protected override string BackendPath { get; } = "/usr/bin/clamscan"; - + 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 $"--no-summary {path}"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs index 2d40818..e222d3e 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/ComodoScanBackend.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; using Microsoft.Extensions.Logging; @@ -11,16 +9,13 @@ namespace MalwareMultiScan.Backends.Backends.Implementations public ComodoScanBackend(ILogger logger) : base(logger) { } - + public override string Id { get; } = "comodo"; - - public override DateTime DatabaseLastUpdate => - File.GetLastWriteTime("/opt/COMODO/scanners/bases.cav"); protected override string BackendPath { get; } = "/opt/COMODO/cmdscan"; - + protected override Regex MatchRegex { get; } = - new Regex(@".* ---> Found Virus, Malware Name is (?.*)", + new Regex(@".* ---> Found Virus, Malware Name is (?.*)", RegexOptions.Compiled | RegexOptions.Multiline); protected override string GetBackendArguments(string path) diff --git a/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs index ba8d11b..a98dcb6 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/DrWebScanBackend.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; using Microsoft.Extensions.Logging; @@ -11,14 +9,11 @@ namespace MalwareMultiScan.Backends.Backends.Implementations public DrWebScanBackend(ILogger logger) : base(logger) { } - + public override string Id { get; } = "drweb"; - - public override DateTime DatabaseLastUpdate => - File.GetLastWriteTime("/var/opt/drweb.com/version/version.ini"); protected override string BackendPath { get; } = "/usr/bin/drweb-ctl"; - + protected override Regex MatchRegex { get; } = new Regex(@".* - infected with (?[\S ]+)", RegexOptions.Compiled | RegexOptions.Multiline); diff --git a/MalwareMultiScan.Backends/Backends/Implementations/DummyScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/DummyScanBackend.cs new file mode 100644 index 0000000..b835515 --- /dev/null +++ b/MalwareMultiScan.Backends/Backends/Implementations/DummyScanBackend.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +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(); + } + + private static Task Scan() + { + return Task.FromResult(new[] {"Malware.Dummy.Result"}); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs index 89b9b3b..2448351 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/KesScanBackend.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; using Microsoft.Extensions.Logging; @@ -11,14 +9,11 @@ namespace MalwareMultiScan.Backends.Backends.Implementations public KesScanBackend(ILogger logger) : base(logger) { } - + public override string Id { get; } = "kes"; - - public override DateTime DatabaseLastUpdate => - File.GetLastWriteTime("/var/opt/kaspersky/kesl/common/updates/avbases/klsrl.dat"); protected override string BackendPath { get; } = "/bin/bash"; - + protected override Regex MatchRegex { get; } = new Regex(@"[ +]DetectName.*: (?.*)", RegexOptions.Compiled | RegexOptions.Multiline); diff --git a/MalwareMultiScan.Backends/Backends/Implementations/McAffeeScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/McAffeeScanBackend.cs index 1970951..9c4d76a 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/McAffeeScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/McAffeeScanBackend.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; using Microsoft.Extensions.Logging; @@ -11,14 +9,11 @@ namespace MalwareMultiScan.Backends.Backends.Implementations public McAfeeScanBackend(ILogger logger) : base(logger) { } - + public override string Id { get; } = "mcafeee"; - - public override DateTime DatabaseLastUpdate => - File.GetLastWriteTime("/usr/local/uvscan/avvscan.dat"); protected override string BackendPath { get; } = "/usr/local/uvscan/uvscan"; - + protected override bool ThrowOnNonZeroExitCode { get; } = false; protected override Regex MatchRegex { get; } = diff --git a/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs index d363a77..9b8cecd 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/SophosScanBackend.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; using Microsoft.Extensions.Logging; @@ -11,11 +9,8 @@ namespace MalwareMultiScan.Backends.Backends.Implementations public SophosScanBackend(ILogger logger) : base(logger) { } - + public override string Id { get; } = "sophos"; - - public override DateTime DatabaseLastUpdate => - File.GetLastWriteTime("/opt/sophos-av/lib/sav/vdlsync.upd"); protected override string BackendPath { get; } = "/opt/sophos-av/bin/savscan"; diff --git a/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs b/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs index 2dd4129..8eb69b0 100644 --- a/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs +++ b/MalwareMultiScan.Backends/Backends/Implementations/WindowsDefenderScanBackend.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text.RegularExpressions; using MalwareMultiScan.Backends.Backends.Abstracts; using Microsoft.Extensions.Logging; @@ -14,9 +12,6 @@ namespace MalwareMultiScan.Backends.Backends.Implementations public override string Id { get; } = "windows-defender"; - public override DateTime DatabaseLastUpdate => - File.GetLastWriteTime("/opt/engine/mpavbase.vdm"); - protected override string BackendPath { get; } = "/opt/mpclient"; protected override Regex MatchRegex { get; } = diff --git a/MalwareMultiScan.Backends/Dockerfiles/Clamav.Dockerfile b/MalwareMultiScan.Backends/Dockerfiles/Clamav.Dockerfile index 310618e..dd34136 100644 --- a/MalwareMultiScan.Backends/Dockerfiles/Clamav.Dockerfile +++ b/MalwareMultiScan.Backends/Dockerfiles/Clamav.Dockerfile @@ -1,4 +1,4 @@ -FROM mindcollapse/malware-multi-scan-worker:latest +FROM mindcollapse/malware-multi-scan-scanner:latest ENV DEBIAN_FRONTEND noninteractive diff --git a/MalwareMultiScan.Backends/Dockerfiles/Comodo.Dockerfile b/MalwareMultiScan.Backends/Dockerfiles/Comodo.Dockerfile index 7ebead2..8e10c16 100644 --- a/MalwareMultiScan.Backends/Dockerfiles/Comodo.Dockerfile +++ b/MalwareMultiScan.Backends/Dockerfiles/Comodo.Dockerfile @@ -1,4 +1,4 @@ -FROM mindcollapse/malware-multi-scan-worker:latest +FROM mindcollapse/malware-multi-scan-scanner:latest RUN apt-get update && apt-get install wget -y diff --git a/MalwareMultiScan.Backends/Dockerfiles/DrWeb.Dockerfile b/MalwareMultiScan.Backends/Dockerfiles/DrWeb.Dockerfile index 168156f..c6073db 100644 --- a/MalwareMultiScan.Backends/Dockerfiles/DrWeb.Dockerfile +++ b/MalwareMultiScan.Backends/Dockerfiles/DrWeb.Dockerfile @@ -1,4 +1,4 @@ -FROM mindcollapse/malware-multi-scan-worker:latest +FROM mindcollapse/malware-multi-scan-scanner:latest ARG DRWEB_KEY ENV DRWEB_KEY=$DRWEB_KEY diff --git a/MalwareMultiScan.Backends/Dockerfiles/KES.Dockerfile b/MalwareMultiScan.Backends/Dockerfiles/KES.Dockerfile index 5a7fbd6..80cff17 100644 --- a/MalwareMultiScan.Backends/Dockerfiles/KES.Dockerfile +++ b/MalwareMultiScan.Backends/Dockerfiles/KES.Dockerfile @@ -1,4 +1,4 @@ -FROM mindcollapse/malware-multi-scan-worker:latest +FROM mindcollapse/malware-multi-scan-scanner:latest ARG KES_KEY ENV KES_KEY=$KES_KEY diff --git a/MalwareMultiScan.Backends/Dockerfiles/McAfee.Dockerfile b/MalwareMultiScan.Backends/Dockerfiles/McAfee.Dockerfile index b8dd8d4..8dadd87 100644 --- a/MalwareMultiScan.Backends/Dockerfiles/McAfee.Dockerfile +++ b/MalwareMultiScan.Backends/Dockerfiles/McAfee.Dockerfile @@ -1,4 +1,4 @@ -FROM mindcollapse/malware-multi-scan-worker:latest +FROM mindcollapse/malware-multi-scan-scanner:latest RUN apt-get update && apt-get install unzip wget -y diff --git a/MalwareMultiScan.Backends/Dockerfiles/Sophos.Dockerfile b/MalwareMultiScan.Backends/Dockerfiles/Sophos.Dockerfile index ce57e8b..814309d 100644 --- a/MalwareMultiScan.Backends/Dockerfiles/Sophos.Dockerfile +++ b/MalwareMultiScan.Backends/Dockerfiles/Sophos.Dockerfile @@ -1,4 +1,4 @@ -FROM mindcollapse/malware-multi-scan-worker:latest +FROM mindcollapse/malware-multi-scan-scanner:latest RUN apt-get update && apt-get install wget -y diff --git a/MalwareMultiScan.Backends/Dockerfiles/WindowsDefender.Dockerfile b/MalwareMultiScan.Backends/Dockerfiles/WindowsDefender.Dockerfile index 6444031..b97083f 100644 --- a/MalwareMultiScan.Backends/Dockerfiles/WindowsDefender.Dockerfile +++ b/MalwareMultiScan.Backends/Dockerfiles/WindowsDefender.Dockerfile @@ -11,7 +11,7 @@ WORKDIR /opt/loadlibrary/engine RUN curl -L "https://go.microsoft.com/fwlink/?LinkID=121721&arch=x86" --output mpan-fe.exe RUN cabextract mpan-fe.exe && rm mpan-fe.exe -FROM mindcollapse/malware-multi-scan-worker:latest +FROM mindcollapse/malware-multi-scan-scanner:latest RUN apt-get update && apt-get install -y libc6-i386 diff --git a/MalwareMultiScan.Shared/Data/Enums/BackendType.cs b/MalwareMultiScan.Backends/Enums/BackendType.cs similarity index 72% rename from MalwareMultiScan.Shared/Data/Enums/BackendType.cs rename to MalwareMultiScan.Backends/Enums/BackendType.cs index 31e4fea..cc935b3 100644 --- a/MalwareMultiScan.Shared/Data/Enums/BackendType.cs +++ b/MalwareMultiScan.Backends/Enums/BackendType.cs @@ -1,7 +1,8 @@ -namespace MalwareMultiScan.Shared.Data.Enums +namespace MalwareMultiScan.Backends.Enums { public enum BackendType { + Dummy, Defender, Clamav, DrWeb, diff --git a/MalwareMultiScan.Backends/Extensions/ServiceCollectionExtensions.cs b/MalwareMultiScan.Backends/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a7005c8 --- /dev/null +++ b/MalwareMultiScan.Backends/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +using System; +using MalwareMultiScan.Backends.Backends.Implementations; +using MalwareMultiScan.Backends.Enums; +using MalwareMultiScan.Backends.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace MalwareMultiScan.Backends.Extensions +{ + public static class ServiceCollectionExtensions + { + public static void AddScanningBackend(this IServiceCollection services, BackendType type) + { + using var provider = services.BuildServiceProvider(); + + var logger = provider.GetService>(); + + services.AddSingleton(type switch + { + 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() + }); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Shared/Interfaces/IScanBackend.cs b/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs similarity index 66% rename from MalwareMultiScan.Shared/Interfaces/IScanBackend.cs rename to MalwareMultiScan.Backends/Interfaces/IScanBackend.cs index e10d3c5..ea872f1 100644 --- a/MalwareMultiScan.Shared/Interfaces/IScanBackend.cs +++ b/MalwareMultiScan.Backends/Interfaces/IScanBackend.cs @@ -2,19 +2,14 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -namespace MalwareMultiScan.Shared.Interfaces +namespace MalwareMultiScan.Backends.Interfaces { public interface IScanBackend { public string Id { get; } - - public DateTime DatabaseLastUpdate { get; } - public Task ScanAsync(string path, CancellationToken cancellationToken); public Task ScanAsync(Uri uri, CancellationToken cancellationToken); - public Task ScanAsync(IFormFile file, CancellationToken cancellationToken); 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 fb7bbed..ae69f21 100644 --- a/MalwareMultiScan.Backends/MalwareMultiScan.Backends.csproj +++ b/MalwareMultiScan.Backends/MalwareMultiScan.Backends.csproj @@ -3,9 +3,10 @@ netcoreapp3.1 - + - + + diff --git a/MalwareMultiScan.Backends/Messages/ScanRequestMessage.cs b/MalwareMultiScan.Backends/Messages/ScanRequestMessage.cs new file mode 100644 index 0000000..2ba674c --- /dev/null +++ b/MalwareMultiScan.Backends/Messages/ScanRequestMessage.cs @@ -0,0 +1,11 @@ +using System; + +namespace MalwareMultiScan.Backends.Messages +{ + public class ScanRequestMessage + { + public string Id { get; set; } + + 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 new file mode 100644 index 0000000..21807b7 --- /dev/null +++ b/MalwareMultiScan.Backends/Messages/ScanResultMessage.cs @@ -0,0 +1,11 @@ +namespace MalwareMultiScan.Backends.Messages +{ + public class ScanResultMessage + { + public string Id { get; set; } + public string Backend { get; set; } + + public bool Succeeded { get; set; } + public string[] Threats { get; set; } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Worker/Dockerfile b/MalwareMultiScan.Scanner/Dockerfile similarity index 51% rename from MalwareMultiScan.Worker/Dockerfile rename to MalwareMultiScan.Scanner/Dockerfile index 1791d67..bab5501 100644 --- a/MalwareMultiScan.Worker/Dockerfile +++ b/MalwareMultiScan.Scanner/Dockerfile @@ -2,11 +2,10 @@ FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS builder WORKDIR /src -COPY MalwareMultiScan.Worker /src/MalwareMultiScan.Worker -COPY MalwareMultiScan.Shared /src/MalwareMultiScan.Shared +COPY MalwareMultiScan.Worker /src/MalwareMultiScan.Scanner COPY MalwareMultiScan.Backends /src/MalwareMultiScan.Backends -RUN dotnet publish -c Release -r linux-x64 -o ./publish MalwareMultiScan.Worker/MalwareMultiScan.Worker.csproj +RUN dotnet publish -c Release -r linux-x64 -o ./publish MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 @@ -14,7 +13,4 @@ WORKDIR /worker COPY --from=builder /src/publish /worker -ENV ASPNETCORE_ENVIRONMENT=Production -ENV ASPNETCORE_URLS=http://+:9901 - -ENTRYPOINT ["/worker/MalwareMultiScan.Worker"] \ No newline at end of file +ENTRYPOINT ["/worker/MalwareMultiScan.Scanner"] \ No newline at end of file diff --git a/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj b/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj new file mode 100644 index 0000000..2d98265 --- /dev/null +++ b/MalwareMultiScan.Scanner/MalwareMultiScan.Scanner.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + + + + + + + + + + diff --git a/MalwareMultiScan.Scanner/Program.cs b/MalwareMultiScan.Scanner/Program.cs new file mode 100644 index 0000000..eba4512 --- /dev/null +++ b/MalwareMultiScan.Scanner/Program.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using MalwareMultiScan.Backends.Enums; +using MalwareMultiScan.Backends.Extensions; +using MalwareMultiScan.Scanner.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace MalwareMultiScan.Scanner +{ + public static class Program + { + public static async Task Main(string[] args) + { + await Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(configure => + { + configure.AddJsonFile("appsettings.json"); + configure.AddEnvironmentVariables(); + }) + .ConfigureServices((context, services) => + { + services.AddLogging(); + + services.AddScanningBackend( + context.Configuration.GetValue("BackendType")); + + services.AddHostedService(); + }).RunConsoleAsync(); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Scanner/Services/ScanHostedService.cs b/MalwareMultiScan.Scanner/Services/ScanHostedService.cs new file mode 100644 index 0000000..f9dc666 --- /dev/null +++ b/MalwareMultiScan.Scanner/Services/ScanHostedService.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Backends.Interfaces; +using MalwareMultiScan.Backends.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MalwareMultiScan.Scanner.Services +{ + internal class ScanHostedService : IHostedService + { + private readonly IScanBackend _backend; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + private IBus _bus; + + public ScanHostedService(ILogger logger, IConfiguration configuration, IScanBackend backend) + { + _logger = logger; + _configuration = configuration; + _backend = backend; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _bus = RabbitHutch.CreateBus( + _configuration.GetConnectionString("RabbitMQ")); + + _bus.Receive(_backend.Id, Scan); + + _logger.LogInformation( + $"Started scan hosting service for the backend {_backend.Id}"); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _bus.Dispose(); + + _logger.LogInformation( + $"Stopped scan hosting service for the backend {_backend.Id}"); + + 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 + }; + + 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 + { + await _bus.SendAsync( + _configuration.GetValue("ResultsSubscriptionId"), result); + } + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Scanner/appsettings.json b/MalwareMultiScan.Scanner/appsettings.json new file mode 100644 index 0000000..3c086e9 --- /dev/null +++ b/MalwareMultiScan.Scanner/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + + "BackendType": "Dummy", + + "MaxScanningTime": 60, + "ResultsSubscriptionId": "mms.results", + + "ConnectionStrings": { + "RabbitMQ": "host=localhost;prefetchcount=1" + } +} diff --git a/MalwareMultiScan.Shared/Attributes/UrlValidationAttribute.cs b/MalwareMultiScan.Shared/Attributes/UrlValidationAttribute.cs deleted file mode 100644 index ba6f232..0000000 --- a/MalwareMultiScan.Shared/Attributes/UrlValidationAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace MalwareMultiScan.Shared.Attributes -{ - public class UrlValidationAttribute : ValidationAttribute - { - protected override ValidationResult IsValid(object value, ValidationContext validationContext) - { - var uri = (Uri) value; - - if (uri == null || uri.Scheme != "http" && uri.Scheme != "https") - return new ValidationResult("Only http(s) URLs are supported"); - - return ValidationResult.Success; - } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Shared/Data/Requests/BasicRequest.cs b/MalwareMultiScan.Shared/Data/Requests/BasicRequest.cs deleted file mode 100644 index 0870b99..0000000 --- a/MalwareMultiScan.Shared/Data/Requests/BasicRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using MalwareMultiScan.Shared.Attributes; - -namespace MalwareMultiScan.Shared.Data.Requests -{ - public abstract class BasicRequest - { - [Required] - [UrlValidation] - public Uri CallbackUrl { get; set; } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Shared/Data/Requests/FileRequest.cs b/MalwareMultiScan.Shared/Data/Requests/FileRequest.cs deleted file mode 100644 index 612bd18..0000000 --- a/MalwareMultiScan.Shared/Data/Requests/FileRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Http; - -namespace MalwareMultiScan.Shared.Data.Requests -{ - public class FileRequest : BasicRequest - { - [Required] - public IFormFile InputFile { get; set; } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Shared/Data/Requests/UrlRequest.cs b/MalwareMultiScan.Shared/Data/Requests/UrlRequest.cs deleted file mode 100644 index e9a0478..0000000 --- a/MalwareMultiScan.Shared/Data/Requests/UrlRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using MalwareMultiScan.Shared.Attributes; - -namespace MalwareMultiScan.Shared.Data.Requests -{ - public class UrlRequest : BasicRequest - { - [Required] - [UrlValidation] - public Uri InputUrl { get; set; } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Shared/Data/Responses/ResultResponse.cs b/MalwareMultiScan.Shared/Data/Responses/ResultResponse.cs deleted file mode 100644 index 0d33c63..0000000 --- a/MalwareMultiScan.Shared/Data/Responses/ResultResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Linq; - -namespace MalwareMultiScan.Shared.Data.Responses -{ - public class ResultResponse - { - public string Backend { get; set; } - - public bool Success { get; set; } - - public DateTime? DatabaseLastUpdate { get; set; } - - public bool Detected => Threats?.Any() == true; - - public string[] Threats { get; set; } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Shared/MalwareMultiScan.Shared.csproj b/MalwareMultiScan.Shared/MalwareMultiScan.Shared.csproj deleted file mode 100644 index 5d2c164..0000000 --- a/MalwareMultiScan.Shared/MalwareMultiScan.Shared.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - diff --git a/MalwareMultiScan.Worker/Controllers/ScanController.cs b/MalwareMultiScan.Worker/Controllers/ScanController.cs deleted file mode 100644 index c41dadd..0000000 --- a/MalwareMultiScan.Worker/Controllers/ScanController.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using Hangfire; -using MalwareMultiScan.Shared.Data.Requests; -using MalwareMultiScan.Worker.Jobs; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace MalwareMultiScan.Worker.Controllers -{ - [ApiController] - [Produces("application/json")] - public class ScanController : ControllerBase - { - [HttpPost] - [ProducesResponseType(StatusCodes.Status202Accepted)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [Route("/scan/file")] - public async Task ScanFile([FromForm] FileRequest request) - { - var temporaryFile = Path.GetTempFileName(); - - await using (var temporaryFileSteam = System.IO.File.OpenWrite(temporaryFile)) - { - await request.InputFile.CopyToAsync(temporaryFileSteam); - } - - BackgroundJob.Enqueue( - x => x.ScanFile(temporaryFile, request.CallbackUrl)); - - return Accepted(request.CallbackUrl); - } - - [HttpPost] - [ProducesResponseType(StatusCodes.Status202Accepted)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [Route("/scan/url")] - public IActionResult ScanUrl([FromForm] UrlRequest request) - { - BackgroundJob.Enqueue( - x => x.ScanUrl(request.InputUrl, request.CallbackUrl)); - - return Accepted(request.CallbackUrl); - } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Worker/Jobs/ScanJob.cs b/MalwareMultiScan.Worker/Jobs/ScanJob.cs deleted file mode 100644 index 9fa7c05..0000000 --- a/MalwareMultiScan.Worker/Jobs/ScanJob.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Hangfire; -using MalwareMultiScan.Backends.Backends.Implementations; -using MalwareMultiScan.Shared.Data.Enums; -using MalwareMultiScan.Shared.Data.Responses; -using MalwareMultiScan.Shared.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace MalwareMultiScan.Worker.Jobs -{ - public class ScanJob - { - private readonly IScanBackend _backend; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - private readonly int _scanTimeout; - - public ScanJob(IConfiguration configuration, ILogger logger, - IHttpClientFactory httpClientFactory) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - _scanTimeout = configuration.GetValue("ScanTimeout"); - - _backend = configuration.GetValue("BackendType") switch - { - 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() - }; - } - - private async Task PostResult(ResultResponse response, Uri callbackUrl, CancellationToken cancellationToken) - { - var serializedResponse = JsonSerializer.Serialize( - response, typeof(ResultResponse), new JsonSerializerOptions - { - WriteIndented = true - }); - - _logger.LogInformation( - $"Sending following payload to {callbackUrl}: {serializedResponse}"); - - using var httpClient = _httpClientFactory.CreateClient(); - - var callbackResponse = await httpClient.PostAsync(callbackUrl, - new StringContent(serializedResponse, Encoding.UTF8, "application/json"), cancellationToken); - - _logger.LogInformation($"Callback URL {callbackUrl} returned a status {callbackResponse.StatusCode}"); - } - - private async Task Scan(Func> scanMethod, Uri callbackUrl) - { - var cancellationTokenSource = new CancellationTokenSource(); - - cancellationTokenSource.CancelAfter(_scanTimeout * 1000); - - var cancellationToken = cancellationTokenSource.Token; - - var response = new ResultResponse - { - Backend = _backend.Id - }; - - try - { - response.Success = true; - response.Threats = await scanMethod(cancellationToken); - response.DatabaseLastUpdate = _backend.DatabaseLastUpdate; - } - catch (Exception exception) - { - response.Success = false; - - _logger.LogError(exception, "Scanning failed with exception"); - } - - await PostResult(response, callbackUrl, cancellationToken); - } - - [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ScanUrl(Uri url, Uri callbackUrl) - { - await Scan(async t => await _backend.ScanAsync(url, t), callbackUrl); - } - - [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ScanFile(string file, Uri callbackUrl) - { - try - { - await Scan(async t => await _backend.ScanAsync(file, t), callbackUrl); - } - finally - { - File.Delete(file); - } - } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Worker/MalwareMultiScan.Worker.csproj b/MalwareMultiScan.Worker/MalwareMultiScan.Worker.csproj deleted file mode 100644 index 3f7bdca..0000000 --- a/MalwareMultiScan.Worker/MalwareMultiScan.Worker.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - - - - - - diff --git a/MalwareMultiScan.Worker/Properties/launchSettings.json b/MalwareMultiScan.Worker/Properties/launchSettings.json deleted file mode 100644 index 2c9b6ab..0000000 --- a/MalwareMultiScan.Worker/Properties/launchSettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:53549", - "sslPort": 44380 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "MalwareMultiScan.Worker": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/MalwareMultiScan.Worker/Startup.cs b/MalwareMultiScan.Worker/Startup.cs deleted file mode 100644 index bd3ba6f..0000000 --- a/MalwareMultiScan.Worker/Startup.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Hangfire; -using Hangfire.MemoryStorage; -using MalwareMultiScan.Worker.Jobs; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; - -namespace MalwareMultiScan.Worker -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddLogging(); - services.AddControllers(); - services.AddHttpClient(); - - services.AddSingleton(); - - services.AddHangfire( - configuration => configuration.UseMemoryStorage()); - - services.AddHangfireServer(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseRouting(); - - app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); - - app.UseHangfireServer(); - } - } -} \ No newline at end of file diff --git a/MalwareMultiScan.Worker/appsettings.Development.json b/MalwareMultiScan.Worker/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/MalwareMultiScan.Worker/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/MalwareMultiScan.Worker/appsettings.json b/MalwareMultiScan.Worker/appsettings.json deleted file mode 100644 index 1c64e84..0000000 --- a/MalwareMultiScan.Worker/appsettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning", - "MalwareMultiScan": "Debug" - } - }, - - "AllowedHosts": "*", - - "BackendType": "", - "ScanTimeout": 300 -} diff --git a/MalwareMultiScan.sln b/MalwareMultiScan.sln index 4d3aab6..20591bc 100644 --- a/MalwareMultiScan.sln +++ b/MalwareMultiScan.sln @@ -1,28 +1,28 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Worker", "MalwareMultiScan.Worker\MalwareMultiScan.Worker.csproj", "{5D515E0A-B2C6-4C1D-88F6-C296F73409FA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Shared", "MalwareMultiScan.Shared\MalwareMultiScan.Shared.csproj", "{9E0A0B50-741F-4A49-97A2-0B337374347F}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Backends", "MalwareMultiScan.Backends\MalwareMultiScan.Backends.csproj", "{382B49AC-0FFA-44FC-875D-9D4692DDC05D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Api", "MalwareMultiScan.Api\MalwareMultiScan.Api.csproj", "{7B63B897-D390-4617-821F-F96799CBA2F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Scanner", "MalwareMultiScan.Scanner\MalwareMultiScan.Scanner.csproj", "{8A16A3C4-2AE3-4F63-8280-635FF7878080}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5D515E0A-B2C6-4C1D-88F6-C296F73409FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5D515E0A-B2C6-4C1D-88F6-C296F73409FA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5D515E0A-B2C6-4C1D-88F6-C296F73409FA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5D515E0A-B2C6-4C1D-88F6-C296F73409FA}.Release|Any CPU.Build.0 = Release|Any CPU - {9E0A0B50-741F-4A49-97A2-0B337374347F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E0A0B50-741F-4A49-97A2-0B337374347F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E0A0B50-741F-4A49-97A2-0B337374347F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E0A0B50-741F-4A49-97A2-0B337374347F}.Release|Any CPU.Build.0 = Release|Any CPU {382B49AC-0FFA-44FC-875D-9D4692DDC05D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {382B49AC-0FFA-44FC-875D-9D4692DDC05D}.Debug|Any CPU.Build.0 = Debug|Any CPU {382B49AC-0FFA-44FC-875D-9D4692DDC05D}.Release|Any CPU.ActiveCfg = Release|Any CPU {382B49AC-0FFA-44FC-875D-9D4692DDC05D}.Release|Any CPU.Build.0 = Release|Any CPU + {7B63B897-D390-4617-821F-F96799CBA2F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B63B897-D390-4617-821F-F96799CBA2F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B63B897-D390-4617-821F-F96799CBA2F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B63B897-D390-4617-821F-F96799CBA2F4}.Release|Any CPU.Build.0 = Release|Any CPU + {8A16A3C4-2AE3-4F63-8280-635FF7878080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection EndGlobal