Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4f8f826
feat: BepInEx 5.* Support
ifBars Jan 24, 2026
78b8f21
fix: Migrate from System.Text.Json to Newtonsoft.Json
ifBars Jan 24, 2026
83ab177
Update README.md
ifBars Jan 24, 2026
065534d
feat: BepInEx 6.* Support
ifBars Jan 30, 2026
0a45086
Add MLVScan.Core project and expand build configs
ifBars Jan 30, 2026
7590f5d
Merge branch 'master' into bepinex-support
ifBars Jan 30, 2026
e207206
Update BepInEx/5/BepInEx5Patcher.cs
ifBars Jan 30, 2026
e0f263c
Update Services/DeveloperReportGenerator.cs
ifBars Jan 30, 2026
168ca7d
Update Services/DeveloperReportGenerator.cs
ifBars Jan 30, 2026
d6dcf96
Update BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
ifBars Jan 30, 2026
a35f822
feat: Build Workflow
ifBars Jan 30, 2026
2a24166
fix: Inaccurate logging
ifBars Jan 30, 2026
51c3601
Update README.md
ifBars Jan 30, 2026
ad0ca67
Update Services/PluginScannerBase.cs
ifBars Jan 30, 2026
2fef523
Update MLVScan.sln
ifBars Jan 30, 2026
29b669b
Update BepInEx/5/BepInEx5Patcher.cs
ifBars Jan 30, 2026
e5e0fc8
Update build config, fix typo, and clarify README
ifBars Jan 30, 2026
033d9de
Update MelonLoader/MelonEnvironment.cs
ifBars Jan 30, 2026
3f94267
Update MelonLoaderServiceFactory.cs
ifBars Jan 30, 2026
147b6e2
Update MelonPluginScanner.cs
ifBars Jan 30, 2026
3d24b8e
Create local.build.props.example
ifBars Jan 30, 2026
bdb590b
Update DeveloperReportGenerator.cs
ifBars Jan 30, 2026
bcfca00
Update BepInExEnvironment.cs
ifBars Jan 30, 2026
617b47d
fix: Use MLVScan.Core from NuGet
ifBars Jan 30, 2026
5eceddd
Update MLVScan.csproj
ifBars Jan 30, 2026
7987c23
fix: CI
ifBars Jan 30, 2026
98ab2b2
Update build.yml
ifBars Jan 30, 2026
2ab298d
Update MLVScan.csproj
ifBars Jan 30, 2026
1a0eaf5
fix: CI Repack
ifBars Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Build MLVScan

on:
push:
branches: [ "master", "main" ]
pull_request:
branches: [ "master", "main" ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Install Mono (for ILRepack)
run: |
sudo apt-get update
sudo apt-get install -y mono-complete

# MelonLoader
- name: Restore MelonLoader dependencies
run: dotnet restore MLVScan.csproj -p:Configuration=MelonLoader

- name: Build MelonLoader
run: dotnet build MLVScan.csproj -c MelonLoader --no-restore

- name: Upload MelonLoader Artifact
uses: actions/upload-artifact@v4
with:
name: MLVScan.MelonLoader
path: bin/MelonLoader/**/MLVScan.MelonLoader.dll

# BepInEx 5
- name: Restore BepInEx dependencies
run: dotnet restore MLVScan.csproj -p:Configuration=BepInEx

- name: Build BepInEx
run: dotnet build MLVScan.csproj -c BepInEx --no-restore

- name: Upload BepInEx Artifact
uses: actions/upload-artifact@v4
with:
name: MLVScan.BepInEx
path: bin/BepInEx/**/MLVScan.BepInEx.dll

# BepInEx 6.x Mono
- name: Restore BepInEx6Mono dependencies
run: dotnet restore MLVScan.csproj -p:Configuration=BepInEx6Mono

- name: Build BepInEx6Mono
run: dotnet build MLVScan.csproj -c BepInEx6Mono --no-restore

- name: Upload BepInEx6Mono Artifact
uses: actions/upload-artifact@v4
with:
name: MLVScan.BepInEx6.Mono
path: bin/BepInEx6Mono/**/MLVScan.BepInEx6.Mono.dll

# BepInEx 6.x IL2CPP
- name: Restore BepInEx6IL2CPP dependencies
run: dotnet restore MLVScan.csproj -p:Configuration=BepInEx6IL2CPP

- name: Build BepInEx6IL2CPP
run: dotnet build MLVScan.csproj -c BepInEx6IL2CPP --no-restore

- name: Upload BepInEx6IL2CPP Artifact
uses: actions/upload-artifact@v4
with:
name: MLVScan.BepInEx6.IL2CPP
path: bin/BepInEx6IL2CPP/**/MLVScan.BepInEx6.IL2CPP.dll
42 changes: 42 additions & 0 deletions Abstractions/IConfigManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using MLVScan.Models;

namespace MLVScan.Abstractions
{
/// <summary>
/// Abstraction for configuration management across different mod platforms.
/// MelonLoader uses MelonPreferences (INI-based), BepInEx uses JSON files.
/// </summary>
public interface IConfigManager
{
/// <summary>
/// Gets the current configuration.
/// </summary>
ScanConfig Config { get; }

/// <summary>
/// Loads configuration from persistent storage.
/// Creates default configuration if none exists.
/// </summary>
ScanConfig LoadConfig();

/// <summary>
/// Saves configuration to persistent storage.
/// </summary>
void SaveConfig(ScanConfig config);

/// <summary>
/// Checks if a file hash is in the whitelist.
/// </summary>
bool IsHashWhitelisted(string hash);

/// <summary>
/// Gets all whitelisted hashes.
/// </summary>
string[] GetWhitelistedHashes();

/// <summary>
/// Sets the whitelisted hashes (normalizes and deduplicates).
/// </summary>
void SetWhitelistedHashes(string[] hashes);
}
}
46 changes: 46 additions & 0 deletions Abstractions/IPlatformEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace MLVScan.Abstractions
{
/// <summary>
/// Abstraction for platform-specific paths and environment info.
/// MelonLoader uses MelonEnvironment, BepInEx uses BepInEx.Paths.
/// </summary>
public interface IPlatformEnvironment
{
/// <summary>
/// Gets the game's root directory.
/// </summary>
string GameRootDirectory { get; }

/// <summary>
/// Gets the directory where plugins/mods are stored.
/// MelonLoader: Mods/ and Plugins/
/// BepInEx: BepInEx/plugins/
/// </summary>
string[] PluginDirectories { get; }

/// <summary>
/// Gets the directory for MLVScan's own data (reports, disabled info, etc.).
/// </summary>
string DataDirectory { get; }

/// <summary>
/// Gets the directory for scan reports.
/// </summary>
string ReportsDirectory { get; }

/// <summary>
/// Gets the managed assemblies directory (Unity DLLs, game code).
/// </summary>
string ManagedDirectory { get; }

/// <summary>
/// Gets the path to the MLVScan assembly itself (for self-exclusion).
/// </summary>
string SelfAssemblyPath { get; }

/// <summary>
/// Gets the platform name for display/logging.
/// </summary>
string PlatformName { get; }
}
}
125 changes: 125 additions & 0 deletions BepInEx/5/BepInEx5Patcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using BepInEx;
using BepInEx.Logging;
using Mono.Cecil;
using MLVScan.BepInEx;
using MLVScan.BepInEx.Adapters;

namespace MLVScan.BepInEx5
{
/// <summary>
/// BepInEx 5.x preloader patcher that scans plugins for malicious patterns
/// before the chainloader initializes them.
/// </summary>
public static class BepInEx5Patcher
{
private static ManualLogSource _logger;

/// <summary>
/// Default whitelist for known-safe BepInEx ecosystem plugins.
/// </summary>
private static readonly string[] DefaultWhitelistedHashes =
[
// BepInEx ecosystem - known safe plugins
"8c0735f521d0fa785bf81b2e627a93042362b736ebc2c4c7ac425276b49fa692",
"9f86b196ffc845bdbc85192054e2876388ce1294b5a880459c93cbed7de2ae9d",
"bc67dab59532d0daca129e574c87d43b24a0b63ccb7312ccd25e0d7c4887784c",
"f1f3ff967bdb8f63a4bfd878255890f6393af37d3cc357babb6b504d9473ee06",
"d034d0e941deb47ea6b5ee8ca288bdb1d0bb25475dfba02cb61f6eadf0fa448e",
"e28b71abefdb5c2e90ea2d9e3c79bdff95f8173d08022732f62f35d2c328895d",
"bd5ec0343880b528ef190afe91778d172a239a625929dc176492eddc5c66cc31",
"503f851721ffacc7839e42d7c6c8a7c39fa2cea6e70a480b8bad822064d65aa0",
"184386c0f5f5bae6b63c96b73e312d3f39eba0d0ca81de3e3bd574ef389d1e29"
];

/// <summary>
/// Required: Declares which assemblies to patch.
/// Empty = we don't patch game assemblies, just use Initialize() as entry point.
/// </summary>
public static IEnumerable<string> TargetDLLs { get; } = Array.Empty<string>();

/// <summary>
/// Required: Patching method (no-op - we don't modify game code).
/// </summary>
public static void Patch(AssemblyDefinition assembly) { }

/// <summary>
/// Called before patching - our main entry point.
/// Runs BEFORE the chainloader loads any plugins.
/// </summary>
public static void Initialize()
{
_logger = Logger.CreateLogSource("MLVScan");

try
{
_logger.LogInfo("MLVScan preloader patcher initializing...");
_logger.LogInfo($"Plugin directory: {Paths.PluginPath}");

// Create platform environment
var environment = new BepInExPlatformEnvironment();

// Load or create configuration
var configManager = new BepInExConfigManager(_logger, DefaultWhitelistedHashes);
var config = configManager.LoadConfig();

// Create adapters
var scanLogger = new BepInExScanLogger(_logger);
var resolverProvider = new BepInExAssemblyResolverProvider();

// Create scanner and disabler
var pluginScanner = new BepInExPluginScanner(
scanLogger,
resolverProvider,
config,
configManager,
environment);

var pluginDisabler = new BepInExPluginDisabler(scanLogger, config);
var reportGenerator = new BepInExReportGenerator(_logger, config);

// Scan all plugins
var scanResults = pluginScanner.ScanAllPlugins();

if (scanResults.Count > 0)
{
// Disable suspicious plugins
var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);

// Generate reports for disabled plugins
if (disabledPlugins.Count > 0)
{
reportGenerator.GenerateReports(disabledPlugins, scanResults);

_logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
_logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
}
}
else if (!config.EnableAutoScan)
{
_logger.LogInfo("Automatic scanning is disabled in configuration.");
}
else
{
_logger.LogInfo("No suspicious plugins detected.");
}
Comment on lines +82 to +106
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent EnableAutoScan behavior compared to BepInEx6MonoPatcher.

This patcher calls ScanAllPlugins() unconditionally (line 83), then checks config.EnableAutoScan in the else-if branch (line 99). However, BepInEx6MonoPatcher checks EnableAutoScan before scanning (see relevant snippet lines 70-74).

Current behavior here: scanning happens even when EnableAutoScan is false, wasting resources.

🔧 Suggested fix to match BepInEx6MonoPatcher pattern
-                // Scan all plugins
-                var scanResults = pluginScanner.ScanAllPlugins();
-
-                if (scanResults.Count > 0)
+                if (!config.EnableAutoScan)
+                {
+                    _logger.LogInfo("Auto-scan is disabled. Skipping plugin scan.");
+                }
+                else
                 {
-                    // Disable suspicious plugins
-                    var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
+                    var scanResults = pluginScanner.ScanAllPlugins();
 
-                    // Generate reports for disabled plugins
-                    if (disabledPlugins.Count > 0)
+                    if (scanResults.Count > 0)
                     {
-                        reportGenerator.GenerateReports(disabledPlugins, scanResults);
+                        var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
 
-                        _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
-                        _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
+                        if (disabledPlugins.Count > 0)
+                        {
+                            reportGenerator.GenerateReports(disabledPlugins, scanResults);
+
+                            _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
+                            _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
+                        }
+                    }
+                    else
+                    {
+                        _logger.LogInfo("No suspicious plugins detected.");
                     }
                 }
-                else if (!config.EnableAutoScan)
-                {
-                    _logger.LogInfo("Automatic scanning is disabled in configuration.");
-                }
-                else
-                {
-                    _logger.LogInfo("No suspicious plugins detected.");
-                }
🤖 Prompt for AI Agents
In `@BepInEx/5/BepInEx5Patcher.cs` around lines 82 - 106, The code currently calls
pluginScanner.ScanAllPlugins() unconditionally; change the control flow to check
config.EnableAutoScan before invoking ScanAllPlugins so scanning is skipped when
auto-scan is disabled. Concretely, move the config.EnableAutoScan check above
the pluginScanner.ScanAllPlugins() call (or return/skip early when false), so
ScanAllPlugins(), pluginDisabler.DisableSuspiciousPlugins(scanResults) and
reportGenerator.GenerateReports(disabledPlugins, scanResults) only run when
config.EnableAutoScan is true; keep the existing logging paths
(_logger.LogInfo/_logger.LogWarning) but ensure the "Automatic scanning is
disabled" message is emitted when EnableAutoScan is false.


_logger.LogInfo("MLVScan preloader scan complete.");
}
catch (Exception ex)
{
_logger?.LogError($"MLVScan initialization failed: {ex}");
}
}

/// <summary>
/// Called after all patching and assembly loading is complete.
/// </summary>
public static void Finish()
{
// Optional: cleanup, final summary logging
_logger?.LogDebug("MLVScan patcher finished.");
}
}
}
Loading