Added maleware scan
This commit is contained in:
parent
b8bfdb7729
commit
0379afd831
5 changed files with 342 additions and 2 deletions
8
Moonlight/App/Models/Misc/MalwareScanResult.cs
Normal file
8
Moonlight/App/Models/Misc/MalwareScanResult.cs
Normal 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; } = "";
|
||||
}
|
196
Moonlight/App/Services/Background/MalwareScanService.cs
Normal file
196
Moonlight/App/Services/Background/MalwareScanService.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
134
Moonlight/Shared/Views/Admin/Sys/Malware.razor
Normal file
134
Moonlight/Shared/Views/Admin/Sys/Malware.razor
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue