Skip to content

Commit 6c43e6a

Browse files
Merge pull request #187 from Moonlight-Panel/AddMalwareScan
Added maleware scan
2 parents b8bfdb7 + 0379afd commit 6c43e6a

File tree

5 files changed

+342
-2
lines changed

5 files changed

+342
-2
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Moonlight.App.Models.Misc;
2+
3+
public class MalwareScanResult
4+
{
5+
public string Title { get; set; } = "";
6+
public string Description { get; set; } = "";
7+
public string Author { get; set; } = "";
8+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
using Moonlight.App.ApiClients.Daemon.Resources;
2+
using Moonlight.App.Database.Entities;
3+
using Moonlight.App.Events;
4+
using Moonlight.App.Exceptions;
5+
using Moonlight.App.Helpers;
6+
using Moonlight.App.Models.Misc;
7+
using Moonlight.App.Repositories;
8+
9+
namespace Moonlight.App.Services.Background;
10+
11+
public class MalwareScanService
12+
{
13+
private Repository<Server> ServerRepository;
14+
private Repository<Node> NodeRepository;
15+
private NodeService NodeService;
16+
private ServerService ServerService;
17+
18+
private readonly EventSystem Event;
19+
private readonly IServiceScopeFactory ServiceScopeFactory;
20+
21+
public bool IsRunning { get; private set; }
22+
public readonly Dictionary<Server, MalwareScanResult[]> ScanResults;
23+
public string Status { get; private set; } = "N/A";
24+
25+
public MalwareScanService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
26+
{
27+
ServiceScopeFactory = serviceScopeFactory;
28+
Event = eventSystem;
29+
30+
ScanResults = new();
31+
}
32+
33+
public Task Start()
34+
{
35+
if (IsRunning)
36+
throw new DisplayException("Malware scan is already running");
37+
38+
Task.Run(Run);
39+
40+
return Task.CompletedTask;
41+
}
42+
43+
private async Task Run()
44+
{
45+
IsRunning = true;
46+
Status = "Clearing last results";
47+
await Event.Emit("malwareScan.status", IsRunning);
48+
49+
lock (ScanResults)
50+
{
51+
ScanResults.Clear();
52+
}
53+
54+
await Event.Emit("malwareScan.result");
55+
56+
using var scope = ServiceScopeFactory.CreateScope();
57+
58+
// Load services from di scope
59+
NodeRepository = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
60+
ServerRepository = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
61+
NodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
62+
ServerService = scope.ServiceProvider.GetRequiredService<ServerService>();
63+
64+
var nodes = NodeRepository.Get().ToArray();
65+
var containers = new List<Container>();
66+
67+
// Fetch and summarize all running containers from all nodes
68+
Logger.Verbose("Fetching and summarizing all running containers from all nodes");
69+
70+
Status = "Fetching and summarizing all running containers from all nodes";
71+
await Event.Emit("malwareScan.status", IsRunning);
72+
73+
foreach (var node in nodes)
74+
{
75+
var metrics = await NodeService.GetDockerMetrics(node);
76+
77+
foreach (var container in metrics.Containers)
78+
{
79+
containers.Add(container);
80+
}
81+
}
82+
83+
var containerServerMapped = new Dictionary<Server, Container>();
84+
85+
// Map all the containers to their corresponding server if existing
86+
Logger.Verbose("Mapping all the containers to their corresponding server if existing");
87+
88+
Status = "Mapping all the containers to their corresponding server if existing";
89+
await Event.Emit("malwareScan.status", IsRunning);
90+
91+
foreach (var container in containers)
92+
{
93+
if (Guid.TryParse(container.Name, out Guid uuid))
94+
{
95+
var server = ServerRepository
96+
.Get()
97+
.FirstOrDefault(x => x.Uuid == uuid);
98+
99+
if(server == null)
100+
continue;
101+
102+
containerServerMapped.Add(server, container);
103+
}
104+
}
105+
106+
// Perform scan
107+
var resultsMapped = new Dictionary<Server, MalwareScanResult[]>();
108+
foreach (var mapping in containerServerMapped)
109+
{
110+
Logger.Verbose($"Scanning server {mapping.Key.Name} for malware");
111+
112+
Status = $"Scanning server {mapping.Key.Name} for malware";
113+
await Event.Emit("malwareScan.status", IsRunning);
114+
115+
var results = await PerformScanOnServer(mapping.Key, mapping.Value);
116+
117+
if (results.Any())
118+
{
119+
resultsMapped.Add(mapping.Key, results);
120+
Logger.Verbose($"{results.Length} findings on server {mapping.Key.Name}");
121+
}
122+
}
123+
124+
Logger.Verbose($"Scan complete. Detected {resultsMapped.Count} servers with findings");
125+
126+
IsRunning = false;
127+
Status = $"Scan complete. Detected {resultsMapped.Count} servers with findings";
128+
await Event.Emit("malwareScan.status", IsRunning);
129+
130+
lock (ScanResults)
131+
{
132+
foreach (var mapping in resultsMapped)
133+
{
134+
ScanResults.Add(mapping.Key, mapping.Value);
135+
}
136+
}
137+
138+
await Event.Emit("malwareScan.result");
139+
}
140+
141+
private async Task<MalwareScanResult[]> PerformScanOnServer(Server server, Container container)
142+
{
143+
var results = new List<MalwareScanResult>();
144+
145+
// TODO: Move scans to an universal format / api
146+
147+
// Define scans here
148+
149+
async Task ScanSelfBot()
150+
{
151+
var access = await ServerService.CreateFileAccess(server, null!);
152+
var fileElements = await access.Ls();
153+
154+
if (fileElements.Any(x => x.Name == "tokens.txt"))
155+
{
156+
results.Add(new ()
157+
{
158+
Title = "Found SelfBot",
159+
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
160+
Author = "Marcel Baumgartner"
161+
});
162+
}
163+
}
164+
165+
async Task ScanFakePlayerPlugins()
166+
{
167+
var access = await ServerService.CreateFileAccess(server, null!);
168+
var fileElements = await access.Ls();
169+
170+
if (fileElements.Any(x => !x.IsFile && x.Name == "plugins")) // Check for plugins folder
171+
{
172+
await access.Cd("plugins");
173+
fileElements = await access.Ls();
174+
175+
foreach (var fileElement in fileElements)
176+
{
177+
if (fileElement.Name.ToLower().Contains("fake"))
178+
{
179+
results.Add(new()
180+
{
181+
Title = "Fake player plugin",
182+
Description = $"Suspicious plugin file: {fileElement.Name}",
183+
Author = "Marcel Baumgartner"
184+
});
185+
}
186+
}
187+
}
188+
}
189+
190+
// Execute scans
191+
await ScanSelfBot();
192+
await ScanFakePlayerPlugins();
193+
194+
return results.ToArray();
195+
}
196+
}

Moonlight/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ public static async Task Main(string[] args)
170170
builder.Services.AddSingleton<StatisticsCaptureService>();
171171
builder.Services.AddSingleton<DiscordNotificationService>();
172172
builder.Services.AddSingleton<CleanupService>();
173+
builder.Services.AddSingleton<MalwareScanService>();
173174

174175
// Other
175176
builder.Services.AddSingleton<MoonlightService>();
@@ -208,6 +209,7 @@ public static async Task Main(string[] args)
208209
_ = app.Services.GetRequiredService<DiscordBotService>();
209210
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
210211
_ = app.Services.GetRequiredService<DiscordNotificationService>();
212+
_ = app.Services.GetRequiredService<MalwareScanService>();
211213

212214
_ = app.Services.GetRequiredService<MoonlightService>();
213215

Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
</a>
1616
</li>
1717
<li class="nav-item mt-2">
18-
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/system/auditlog">
19-
<TL>AuditLog</TL>
18+
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/system/malware">
19+
<TL>Malware</TL>
2020
</a>
2121
</li>
2222
<li class="nav-item mt-2">
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
@page "/admin/system/malware"
2+
3+
@using Moonlight.Shared.Components.Navigations
4+
@using Moonlight.App.Services.Background
5+
@using Moonlight.App.Services
6+
@using BlazorTable
7+
@using Moonlight.App.Database.Entities
8+
@using Moonlight.App.Events
9+
@using Moonlight.App.Models.Misc
10+
11+
@inject MalwareScanService MalwareScanService
12+
@inject SmartTranslateService SmartTranslateService
13+
@inject EventSystem Event
14+
15+
@implements IDisposable
16+
17+
<OnlyAdmin>
18+
<AdminSystemNavigation Index="2"/>
19+
20+
<div class="row">
21+
<div class="col-12 col-lg-6">
22+
<div class="card">
23+
<div class="card-body">
24+
@if (MalwareScanService.IsRunning)
25+
{
26+
<span class="fs-3 spinner-border align-middle me-3"></span>
27+
}
28+
29+
<span class="fs-3">@(MalwareScanService.Status)</span>
30+
</div>
31+
<div class="card-footer">
32+
@if (MalwareScanService.IsRunning)
33+
{
34+
<button class="btn btn-success disabled">
35+
<TL>Scan in progress</TL>
36+
</button>
37+
}
38+
else
39+
{
40+
<WButton Text="@(SmartTranslateService.Translate("Start scan"))"
41+
CssClasses="btn-success"
42+
OnClick="MalwareScanService.Start">
43+
</WButton>
44+
}
45+
</div>
46+
</div>
47+
</div>
48+
<div class="col-12 col-lg-6">
49+
<div class="card">
50+
<div class="card-header">
51+
<span class="card-title">
52+
<TL>Results</TL>
53+
</span>
54+
</div>
55+
<div class="card-body">
56+
<LazyLoader @ref="LazyLoaderResults" Load="LoadResults">
57+
<div class="table-responsive">
58+
<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">
59+
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Server"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
60+
<Template>
61+
<a href="/server/@(context.Uuid)">@(context.Name)</a>
62+
</Template>
63+
</Column>
64+
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Results"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
65+
<Template>
66+
<div class="row">
67+
@foreach (var result in ScanResults[context])
68+
{
69+
<div class="col-12 col-md-6 p-3">
70+
<div class="accordion" id="scanResult@(result.GetHashCode())">
71+
<div class="accordion-item">
72+
<h2 class="accordion-header" id="scanResult-header@(result.GetHashCode())">
73+
<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())">
74+
<span>@(result.Title)</span>
75+
</button>
76+
</h2>
77+
<div id="scanResult-body@(result.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="scanResult-header@(result.GetHashCode())" data-bs-parent="#scanResult">
78+
<div class="accordion-body">
79+
<p>
80+
@(result.Description)
81+
</p>
82+
</div>
83+
</div>
84+
</div>
85+
</div>
86+
</div>
87+
}
88+
</div>
89+
</Template>
90+
</Column>
91+
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
92+
</Table>
93+
</div>
94+
</LazyLoader>
95+
</div>
96+
</div>
97+
</div>
98+
</div>
99+
</OnlyAdmin>
100+
101+
@code
102+
{
103+
private readonly Dictionary<Server, MalwareScanResult[]> ScanResults = new();
104+
105+
private LazyLoader LazyLoaderResults;
106+
107+
protected override async Task OnInitializedAsync()
108+
{
109+
await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); });
110+
111+
await Event.On<Object>("malwareScan.result", this, async o => { await LazyLoaderResults.Reload(); });
112+
}
113+
114+
private Task LoadResults(LazyLoader arg)
115+
{
116+
ScanResults.Clear();
117+
118+
lock (MalwareScanService.ScanResults)
119+
{
120+
foreach (var result in MalwareScanService.ScanResults)
121+
{
122+
ScanResults.Add(result.Key, result.Value);
123+
}
124+
}
125+
126+
return Task.CompletedTask;
127+
}
128+
129+
public async void Dispose()
130+
{
131+
await Event.Off("malwareScan.status", this);
132+
await Event.Off("malwareScan.result", this);
133+
}
134+
}

0 commit comments

Comments
 (0)