Added maleware scan

This commit is contained in:
Marcel Baumgartner 2023-06-22 20:36:33 +02:00
parent b8bfdb7729
commit 0379afd831
5 changed files with 342 additions and 2 deletions

View file

@ -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; } = "";
}

View file

@ -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<Server> ServerRepository;
private Repository<Node> NodeRepository;
private NodeService NodeService;
private ServerService ServerService;
private readonly EventSystem Event;
private readonly IServiceScopeFactory ServiceScopeFactory;
public bool IsRunning { get; private set; }
public readonly Dictionary<Server, MalwareScanResult[]> 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<Repository<Node>>();
ServerRepository = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
NodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
ServerService = scope.ServiceProvider.GetRequiredService<ServerService>();
var nodes = NodeRepository.Get().ToArray();
var containers = new List<Container>();
// 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<Server, Container>();
// 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<Server, MalwareScanResult[]>();
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<MalwareScanResult[]> PerformScanOnServer(Server server, Container container)
{
var results = new List<MalwareScanResult>();
// 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();
}
}

View file

@ -170,6 +170,7 @@ namespace Moonlight
builder.Services.AddSingleton<StatisticsCaptureService>();
builder.Services.AddSingleton<DiscordNotificationService>();
builder.Services.AddSingleton<CleanupService>();
builder.Services.AddSingleton<MalwareScanService>();
// Other
builder.Services.AddSingleton<MoonlightService>();
@ -208,6 +209,7 @@ namespace Moonlight
_ = app.Services.GetRequiredService<DiscordBotService>();
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
_ = app.Services.GetRequiredService<DiscordNotificationService>();
_ = app.Services.GetRequiredService<MalwareScanService>();
_ = app.Services.GetRequiredService<MoonlightService>();

View file

@ -15,8 +15,8 @@
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/system/auditlog">
<TL>AuditLog</TL>
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/system/malware">
<TL>Malware</TL>
</a>
</li>
<li class="nav-item mt-2">

View file

@ -0,0 +1,134 @@
@page "/admin/system/malware"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services.Background
@using Moonlight.App.Services
@using BlazorTable
@using Moonlight.App.Database.Entities
@using Moonlight.App.Events
@using Moonlight.App.Models.Misc
@inject MalwareScanService MalwareScanService
@inject SmartTranslateService SmartTranslateService
@inject EventSystem Event
@implements IDisposable
<OnlyAdmin>
<AdminSystemNavigation Index="2"/>
<div class="row">
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-body">
@if (MalwareScanService.IsRunning)
{
<span class="fs-3 spinner-border align-middle me-3"></span>
}
<span class="fs-3">@(MalwareScanService.Status)</span>
</div>
<div class="card-footer">
@if (MalwareScanService.IsRunning)
{
<button class="btn btn-success disabled">
<TL>Scan in progress</TL>
</button>
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Start scan"))"
CssClasses="btn-success"
OnClick="MalwareScanService.Start">
</WButton>
}
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Results</TL>
</span>
</div>
<div class="card-body">
<LazyLoader @ref="LazyLoaderResults" Load="LoadResults">
<div class="table-responsive">
<Table TableItem="Server" Items="ScanResults.Keys" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Server"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<a href="/server/@(context.Uuid)">@(context.Name)</a>
</Template>
</Column>
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Results"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<div class="row">
@foreach (var result in ScanResults[context])
{
<div class="col-12 col-md-6 p-3">
<div class="accordion" id="scanResult@(result.GetHashCode())">
<div class="accordion-item">
<h2 class="accordion-header" id="scanResult-header@(result.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#scanResult-body@(result.GetHashCode())" aria-expanded="false" aria-controls="scanResult-body@(result.GetHashCode())">
<span>@(result.Title)</span>
</button>
</h2>
<div id="scanResult-body@(result.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="scanResult-header@(result.GetHashCode())" data-bs-parent="#scanResult">
<div class="accordion-body">
<p>
@(result.Description)
</p>
</div>
</div>
</div>
</div>
</div>
}
</div>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</LazyLoader>
</div>
</div>
</div>
</div>
</OnlyAdmin>
@code
{
private readonly Dictionary<Server, MalwareScanResult[]> ScanResults = new();
private LazyLoader LazyLoaderResults;
protected override async Task OnInitializedAsync()
{
await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); });
await Event.On<Object>("malwareScan.result", this, async o => { await LazyLoaderResults.Reload(); });
}
private Task LoadResults(LazyLoader arg)
{
ScanResults.Clear();
lock (MalwareScanService.ScanResults)
{
foreach (var result in MalwareScanService.ScanResults)
{
ScanResults.Add(result.Key, result.Value);
}
}
return Task.CompletedTask;
}
public async void Dispose()
{
await Event.Off("malwareScan.status", this);
await Event.Off("malwareScan.result", this);
}
}