diff --git a/Moonlight/App/Models/Misc/MalwareScanResult.cs b/Moonlight/App/Models/Misc/MalwareScanResult.cs new file mode 100644 index 0000000..41ba334 --- /dev/null +++ b/Moonlight/App/Models/Misc/MalwareScanResult.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Models.Misc; + +public class MalwareScanResult +{ + public string Title { get; set; } = ""; + public string Description { get; set; } = ""; + public string Author { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Services/Background/MalwareScanService.cs b/Moonlight/App/Services/Background/MalwareScanService.cs new file mode 100644 index 0000000..7db3166 --- /dev/null +++ b/Moonlight/App/Services/Background/MalwareScanService.cs @@ -0,0 +1,196 @@ +using Moonlight.App.ApiClients.Daemon.Resources; +using Moonlight.App.Database.Entities; +using Moonlight.App.Events; +using Moonlight.App.Exceptions; +using Moonlight.App.Helpers; +using Moonlight.App.Models.Misc; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Background; + +public class MalwareScanService +{ + private Repository ServerRepository; + private Repository NodeRepository; + private NodeService NodeService; + private ServerService ServerService; + + private readonly EventSystem Event; + private readonly IServiceScopeFactory ServiceScopeFactory; + + public bool IsRunning { get; private set; } + public readonly Dictionary ScanResults; + public string Status { get; private set; } = "N/A"; + + public MalwareScanService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem) + { + ServiceScopeFactory = serviceScopeFactory; + Event = eventSystem; + + ScanResults = new(); + } + + public Task Start() + { + if (IsRunning) + throw new DisplayException("Malware scan is already running"); + + Task.Run(Run); + + return Task.CompletedTask; + } + + private async Task Run() + { + IsRunning = true; + Status = "Clearing last results"; + await Event.Emit("malwareScan.status", IsRunning); + + lock (ScanResults) + { + ScanResults.Clear(); + } + + await Event.Emit("malwareScan.result"); + + using var scope = ServiceScopeFactory.CreateScope(); + + // Load services from di scope + NodeRepository = scope.ServiceProvider.GetRequiredService>(); + ServerRepository = scope.ServiceProvider.GetRequiredService>(); + NodeService = scope.ServiceProvider.GetRequiredService(); + ServerService = scope.ServiceProvider.GetRequiredService(); + + var nodes = NodeRepository.Get().ToArray(); + var containers = new List(); + + // Fetch and summarize all running containers from all nodes + Logger.Verbose("Fetching and summarizing all running containers from all nodes"); + + Status = "Fetching and summarizing all running containers from all nodes"; + await Event.Emit("malwareScan.status", IsRunning); + + foreach (var node in nodes) + { + var metrics = await NodeService.GetDockerMetrics(node); + + foreach (var container in metrics.Containers) + { + containers.Add(container); + } + } + + var containerServerMapped = new Dictionary(); + + // Map all the containers to their corresponding server if existing + Logger.Verbose("Mapping all the containers to their corresponding server if existing"); + + Status = "Mapping all the containers to their corresponding server if existing"; + await Event.Emit("malwareScan.status", IsRunning); + + foreach (var container in containers) + { + if (Guid.TryParse(container.Name, out Guid uuid)) + { + var server = ServerRepository + .Get() + .FirstOrDefault(x => x.Uuid == uuid); + + if(server == null) + continue; + + containerServerMapped.Add(server, container); + } + } + + // Perform scan + var resultsMapped = new Dictionary(); + foreach (var mapping in containerServerMapped) + { + Logger.Verbose($"Scanning server {mapping.Key.Name} for malware"); + + Status = $"Scanning server {mapping.Key.Name} for malware"; + await Event.Emit("malwareScan.status", IsRunning); + + var results = await PerformScanOnServer(mapping.Key, mapping.Value); + + if (results.Any()) + { + resultsMapped.Add(mapping.Key, results); + Logger.Verbose($"{results.Length} findings on server {mapping.Key.Name}"); + } + } + + Logger.Verbose($"Scan complete. Detected {resultsMapped.Count} servers with findings"); + + IsRunning = false; + Status = $"Scan complete. Detected {resultsMapped.Count} servers with findings"; + await Event.Emit("malwareScan.status", IsRunning); + + lock (ScanResults) + { + foreach (var mapping in resultsMapped) + { + ScanResults.Add(mapping.Key, mapping.Value); + } + } + + await Event.Emit("malwareScan.result"); + } + + private async Task PerformScanOnServer(Server server, Container container) + { + var results = new List(); + + // TODO: Move scans to an universal format / api + + // Define scans here + + async Task ScanSelfBot() + { + var access = await ServerService.CreateFileAccess(server, null!); + var fileElements = await access.Ls(); + + if (fileElements.Any(x => x.Name == "tokens.txt")) + { + results.Add(new () + { + Title = "Found SelfBot", + Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot", + Author = "Marcel Baumgartner" + }); + } + } + + async Task ScanFakePlayerPlugins() + { + var access = await ServerService.CreateFileAccess(server, null!); + var fileElements = await access.Ls(); + + if (fileElements.Any(x => !x.IsFile && x.Name == "plugins")) // Check for plugins folder + { + await access.Cd("plugins"); + fileElements = await access.Ls(); + + foreach (var fileElement in fileElements) + { + if (fileElement.Name.ToLower().Contains("fake")) + { + results.Add(new() + { + Title = "Fake player plugin", + Description = $"Suspicious plugin file: {fileElement.Name}", + Author = "Marcel Baumgartner" + }); + } + } + } + } + + // Execute scans + await ScanSelfBot(); + await ScanFakePlayerPlugins(); + + return results.ToArray(); + } +} \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 2dfae90..8743b96 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -170,6 +170,7 @@ namespace Moonlight builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Other builder.Services.AddSingleton(); @@ -208,6 +209,7 @@ namespace Moonlight _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); + _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); diff --git a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor index e9720e3..d282f34 100644 --- a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor @@ -15,8 +15,8 @@