Improved malware scan. Added server and user purge
This commit is contained in:
parent
95ba81eab4
commit
6972c2bb32
|
@ -9,7 +9,7 @@ public class FakePlayerPluginScan : MalwareScan
|
|||
public override string Name => "Fake player plugin scan";
|
||||
public override string Description => "This scan is a simple fake player plugin scan provided by moonlight";
|
||||
|
||||
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
|
||||
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||
{
|
||||
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||
var access = await serverService.CreateFileAccess(server, null!);
|
||||
|
@ -24,19 +24,16 @@ public class FakePlayerPluginScan : MalwareScan
|
|||
{
|
||||
if (fileElement.Name.ToLower().Contains("fakeplayer"))
|
||||
{
|
||||
return new[]
|
||||
return new()
|
||||
{
|
||||
new MalwareScanResult
|
||||
{
|
||||
Title = "Fake player plugin",
|
||||
Description = $"Suspicious plugin file: {fileElement.Name}",
|
||||
Author = "Marcel Baumgartner"
|
||||
}
|
||||
Title = "Fake player plugin",
|
||||
Description = $"Suspicious plugin file: {fileElement.Name}",
|
||||
Author = "Marcel Baumgartner"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<MalwareScanResult>();
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ public class MinerJarScan : MalwareScan
|
|||
public override string Name => "Miner jar scan";
|
||||
public override string Description => "This scan is a simple miner jar scan provided by moonlight";
|
||||
|
||||
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
|
||||
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||
{
|
||||
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||
var access = await serverService.CreateFileAccess(server, null!);
|
||||
|
@ -23,18 +23,15 @@ public class MinerJarScan : MalwareScan
|
|||
|
||||
if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile))
|
||||
{
|
||||
return new[]
|
||||
return new()
|
||||
{
|
||||
new MalwareScanResult
|
||||
{
|
||||
Title = "Found Miner",
|
||||
Description = "Detected suspicious library directory which may contain a script for miners",
|
||||
Author = "Marcel Baumgartner"
|
||||
}
|
||||
Title = "Found Miner",
|
||||
Description = "Detected suspicious library directory which may contain a script for miners",
|
||||
Author = "Marcel Baumgartner"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<MalwareScanResult>();
|
||||
return null;
|
||||
}
|
||||
}
|
35
Moonlight/App/MalwareScans/MinerScan.cs
Normal file
35
Moonlight/App/MalwareScans/MinerScan.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Models.Misc;
|
||||
using Moonlight.App.Services;
|
||||
|
||||
namespace Moonlight.App.MalwareScans;
|
||||
|
||||
public class MinerScan : MalwareScan
|
||||
{
|
||||
public override string Name => "Miner (NEZHA)";
|
||||
public override string Description => "Probably a miner";
|
||||
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||
{
|
||||
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||
|
||||
var access = await serverService.CreateFileAccess(server, null!);
|
||||
var files = await access.Ls();
|
||||
|
||||
foreach (var file in files.Where(x => x.IsFile && x.Name.EndsWith(".sh")))
|
||||
{
|
||||
var content = await access.Read(file);
|
||||
|
||||
if (content.ToLower().Contains("nezha"))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Title = "Miner",
|
||||
Description = "Miner start script (NEZHA)",
|
||||
Author = "Marcel Baumgartner"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ public class SelfBotCodeScan : MalwareScan
|
|||
public override string Name => "Selfbot code scan";
|
||||
public override string Description => "This scan is a simple selfbot code scan provided by moonlight";
|
||||
|
||||
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
|
||||
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||
{
|
||||
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||
var access = await serverService.CreateFileAccess(server, null!);
|
||||
|
@ -21,18 +21,15 @@ public class SelfBotCodeScan : MalwareScan
|
|||
|
||||
if (rawScript.Contains("https://discord.com/api") && !rawScript.Contains("https://discord.com/api/oauth2") && !rawScript.Contains("https://discord.com/api/webhook") || rawScript.Contains("https://rblxwild.com")) //TODO: Export to plugins, add regex for checking
|
||||
{
|
||||
return new[]
|
||||
return new MalwareScanResult
|
||||
{
|
||||
new MalwareScanResult
|
||||
{
|
||||
Title = "Potential selfbot",
|
||||
Description = $"Suspicious script file: {script.Name}",
|
||||
Author = "Marcel Baumgartner"
|
||||
}
|
||||
Title = "Potential selfbot",
|
||||
Description = $"Suspicious script file: {script.Name}",
|
||||
Author = "Marcel Baumgartner"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<MalwareScanResult>();
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ public class SelfBotScan : MalwareScan
|
|||
public override string Name => "Selfbot Scan";
|
||||
public override string Description => "This scan is a simple selfbot scan provided by moonlight";
|
||||
|
||||
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
|
||||
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||
{
|
||||
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||
var access = await serverService.CreateFileAccess(server, null!);
|
||||
|
@ -17,17 +17,14 @@ public class SelfBotScan : MalwareScan
|
|||
|
||||
if (fileElements.Any(x => x.Name == "tokens.txt"))
|
||||
{
|
||||
return new[]
|
||||
return new MalwareScanResult
|
||||
{
|
||||
new MalwareScanResult
|
||||
{
|
||||
Title = "Found SelfBot",
|
||||
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
|
||||
Author = "Marcel Baumgartner"
|
||||
}
|
||||
Title = "Found SelfBot",
|
||||
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
|
||||
Author = "Marcel Baumgartner"
|
||||
};
|
||||
}
|
||||
|
||||
return Array.Empty<MalwareScanResult>();
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -6,5 +6,5 @@ public abstract class MalwareScan
|
|||
{
|
||||
public abstract string Name { get; }
|
||||
public abstract string Description { get; }
|
||||
public abstract Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider);
|
||||
public abstract Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider);
|
||||
}
|
|
@ -15,7 +15,7 @@ public class MalwareBackgroundScanService
|
|||
|
||||
public bool IsRunning => !ScanTask?.IsCompleted ?? false;
|
||||
public bool ScanAllServers { get; set; }
|
||||
public readonly Dictionary<Server, MalwareScanResult[]> ScanResults;
|
||||
public readonly Dictionary<Server, MalwareScanResult> ScanResults;
|
||||
public string Status { get; private set; } = "N/A";
|
||||
|
||||
private Task? ScanTask;
|
||||
|
@ -70,16 +70,16 @@ public class MalwareBackgroundScanService
|
|||
Status = $"[{i} / {servers.Length}] Scanning server {server.Name}";
|
||||
await Event.Emit("malwareScan.status", IsRunning);
|
||||
|
||||
var results = await malwareScanService.Perform(server);
|
||||
var result = await malwareScanService.Perform(server);
|
||||
|
||||
if (results.Any())
|
||||
if (result != null)
|
||||
{
|
||||
lock (ScanResults)
|
||||
{
|
||||
ScanResults.Add(server, results);
|
||||
ScanResults.Add(server, result);
|
||||
}
|
||||
|
||||
await Event.Emit("malwareScan.result");
|
||||
await Event.Emit("malwareScan.result", server);
|
||||
}
|
||||
|
||||
i++;
|
||||
|
|
|
@ -5,7 +5,7 @@ using Moonlight.App.Services.Plugins;
|
|||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class MalwareScanService //TODO: Make this moddable using plugins
|
||||
public class MalwareScanService
|
||||
{
|
||||
private readonly PluginService PluginService;
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
@ -16,34 +16,29 @@ public class MalwareScanService //TODO: Make this moddable using plugins
|
|||
ServiceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task<MalwareScanResult[]> Perform(Server server)
|
||||
public async Task<MalwareScanResult?> Perform(Server server)
|
||||
{
|
||||
var defaultScans = new List<MalwareScan>
|
||||
{
|
||||
new SelfBotScan(),
|
||||
new MinerJarScan(),
|
||||
new SelfBotCodeScan(),
|
||||
new FakePlayerPluginScan()
|
||||
new FakePlayerPluginScan(),
|
||||
new MinerScan()
|
||||
};
|
||||
|
||||
var scans = await PluginService.BuildMalwareScans(defaultScans.ToArray());
|
||||
|
||||
var results = new List<MalwareScanResult>();
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
foreach (var scan in scans)
|
||||
{
|
||||
var result = await scan.Scan(server, scope.ServiceProvider);
|
||||
|
||||
if (result.Any())
|
||||
{
|
||||
foreach (var scanResult in result)
|
||||
{
|
||||
results.Add(scanResult);
|
||||
}
|
||||
}
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -113,19 +113,17 @@ public class ServerService
|
|||
if (ConfigService.Get().Moonlight.Security.MalwareCheckOnStart && signal == PowerSignal.Start ||
|
||||
signal == PowerSignal.Restart)
|
||||
{
|
||||
var results = await new MalwareScanService(
|
||||
var result = await new MalwareScanService(
|
||||
PluginService,
|
||||
ServiceScopeFactory
|
||||
).Perform(server);
|
||||
|
||||
if (results.Any())
|
||||
if (result != null)
|
||||
{
|
||||
var resultText = string.Join(" ", results.Select(x => x.Title));
|
||||
|
||||
Logger.Warn($"Found malware on server {server.Uuid}. Results: " + resultText);
|
||||
Logger.Warn($"Found malware on server {server.Uuid}. Result: " + result.Title);
|
||||
|
||||
throw new DisplayException(
|
||||
$"Unable to start server. Found following malware on this server: {resultText}. Please contact the support if you think this detection is a false positive",
|
||||
$"Unable to start server. Found following malware on this server: {result.Title}. Please contact the support if you think this detection is a false positive",
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,23 @@
|
|||
@using Moonlight.App.Services.Background
|
||||
@using Moonlight.App.Services
|
||||
@using BlazorTable
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.ApiClients.Wings
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Events
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Models.Misc
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services.Interop
|
||||
@using Moonlight.App.Services.Sessions
|
||||
|
||||
@inject MalwareBackgroundScanService MalwareBackgroundScanService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject ServerService ServerService
|
||||
@inject ToastService ToastService
|
||||
@inject SessionServerService SessionServerService
|
||||
@inject Repository<Server> ServerRepository
|
||||
@inject Repository<User> UserRepository
|
||||
@inject EventSystem Event
|
||||
|
||||
@implements IDisposable
|
||||
|
@ -38,7 +49,7 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-3">
|
||||
<div class="mb-5">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="scanAllServers" @bind="MalwareBackgroundScanService.ScanAllServers">
|
||||
<label class="form-check-label" for="scanAllServers">
|
||||
|
@ -48,9 +59,14 @@
|
|||
</div>
|
||||
|
||||
<WButton Text="@(SmartTranslateService.Translate("Start scan"))"
|
||||
CssClasses="btn-success"
|
||||
CssClasses="btn-success me-3"
|
||||
OnClick="MalwareBackgroundScanService.Start">
|
||||
</WButton>
|
||||
|
||||
<WButton Text="@(SmartTranslateService.Translate("Purge page"))"
|
||||
CssClasses="btn-danger"
|
||||
OnClick="PurgeSelected">
|
||||
</WButton>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,39 +81,14 @@
|
|||
<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">
|
||||
<Table @ref="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>
|
||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Title"))" Field="@(x => ScanResults[x].Title)" Sortable="false" Filterable="true" />
|
||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Description"))" Field="@(x => ScanResults[x].Description)" Sortable="false" Filterable="true" />
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||
</Table>
|
||||
</div>
|
||||
|
@ -109,7 +100,9 @@
|
|||
|
||||
@code
|
||||
{
|
||||
private readonly Dictionary<Server, MalwareScanResult[]> ScanResults = new();
|
||||
private readonly Dictionary<Server, MalwareScanResult> ScanResults = new();
|
||||
|
||||
private Table<Server> Table;
|
||||
|
||||
private LazyLoader LazyLoaderResults;
|
||||
|
||||
|
@ -117,7 +110,15 @@
|
|||
{
|
||||
await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); });
|
||||
|
||||
await Event.On<Object>("malwareScan.result", this, async o => { await LazyLoaderResults.Reload(); });
|
||||
await Event.On<Server>("malwareScan.result", this, async server =>
|
||||
{
|
||||
lock (MalwareBackgroundScanService.ScanResults)
|
||||
{
|
||||
ScanResults.Add(server, MalwareBackgroundScanService.ScanResults[server]);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
private Task LoadResults(LazyLoader arg)
|
||||
|
@ -140,4 +141,69 @@
|
|||
await Event.Off("malwareScan.status", this);
|
||||
await Event.Off("malwareScan.result", this);
|
||||
}
|
||||
|
||||
private async Task PurgeSelected()
|
||||
{
|
||||
int users = 0;
|
||||
int servers = 0;
|
||||
|
||||
int allServersCount = Table.FilteredItems.Count();
|
||||
int position = 0;
|
||||
|
||||
await ToastService.CreateProcessToast("purgeProcess", "Purging");
|
||||
|
||||
foreach (var item in Table.FilteredItems)
|
||||
{
|
||||
position++;
|
||||
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var server = ServerRepository.Get()
|
||||
.Include(x => x.Owner)
|
||||
.FirstOrDefault(x => x.Id == item.Id);
|
||||
|
||||
if(server == null)
|
||||
continue;
|
||||
|
||||
await ToastService.UpdateProcessToast("purgeProcess", $"[{position}/{allServersCount}] {server.Name}");
|
||||
|
||||
ScanResults.Remove(item);
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// Owner
|
||||
|
||||
server.Owner.Status = UserStatus.Banned;
|
||||
UserRepository.Update(server.Owner);
|
||||
users++;
|
||||
|
||||
try
|
||||
{
|
||||
await SessionServerService.ReloadUserSessions(server.Owner);
|
||||
}
|
||||
catch (Exception) {/* Ignored */}
|
||||
|
||||
// Server itself
|
||||
|
||||
await ServerService.SetPowerState(server, PowerSignal.Kill);
|
||||
await ServerService.Delete(server);
|
||||
servers++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn($"Error purging server: {item.Uuid}");
|
||||
Logger.Warn(e);
|
||||
|
||||
await ToastService.Error(
|
||||
$"Failed to purge server '{item.Name}': {e.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await ToastService.RemoveProcessToast("purgeProcess");
|
||||
await ToastService.Success($"Successfully purged {servers} servers by {users} users");
|
||||
}
|
||||
}
|
|
@ -190,7 +190,7 @@
|
|||
.Include(x => x.Variables)
|
||||
.Include(x => x.MainAllocation)
|
||||
.Include(x => x.Owner)
|
||||
.First(x => x.Uuid == uuid);
|
||||
.FirstOrDefault(x => x.Uuid == uuid);
|
||||
|
||||
if (CurrentServer != null)
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue