diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..e1601fa
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -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
diff --git a/Abstractions/IConfigManager.cs b/Abstractions/IConfigManager.cs
new file mode 100644
index 0000000..15d55ce
--- /dev/null
+++ b/Abstractions/IConfigManager.cs
@@ -0,0 +1,42 @@
+using MLVScan.Models;
+
+namespace MLVScan.Abstractions
+{
+ ///
+ /// Abstraction for configuration management across different mod platforms.
+ /// MelonLoader uses MelonPreferences (INI-based), BepInEx uses JSON files.
+ ///
+ public interface IConfigManager
+ {
+ ///
+ /// Gets the current configuration.
+ ///
+ ScanConfig Config { get; }
+
+ ///
+ /// Loads configuration from persistent storage.
+ /// Creates default configuration if none exists.
+ ///
+ ScanConfig LoadConfig();
+
+ ///
+ /// Saves configuration to persistent storage.
+ ///
+ void SaveConfig(ScanConfig config);
+
+ ///
+ /// Checks if a file hash is in the whitelist.
+ ///
+ bool IsHashWhitelisted(string hash);
+
+ ///
+ /// Gets all whitelisted hashes.
+ ///
+ string[] GetWhitelistedHashes();
+
+ ///
+ /// Sets the whitelisted hashes (normalizes and deduplicates).
+ ///
+ void SetWhitelistedHashes(string[] hashes);
+ }
+}
diff --git a/Abstractions/IPlatformEnvironment.cs b/Abstractions/IPlatformEnvironment.cs
new file mode 100644
index 0000000..bf86c04
--- /dev/null
+++ b/Abstractions/IPlatformEnvironment.cs
@@ -0,0 +1,46 @@
+namespace MLVScan.Abstractions
+{
+ ///
+ /// Abstraction for platform-specific paths and environment info.
+ /// MelonLoader uses MelonEnvironment, BepInEx uses BepInEx.Paths.
+ ///
+ public interface IPlatformEnvironment
+ {
+ ///
+ /// Gets the game's root directory.
+ ///
+ string GameRootDirectory { get; }
+
+ ///
+ /// Gets the directory where plugins/mods are stored.
+ /// MelonLoader: Mods/ and Plugins/
+ /// BepInEx: BepInEx/plugins/
+ ///
+ string[] PluginDirectories { get; }
+
+ ///
+ /// Gets the directory for MLVScan's own data (reports, disabled info, etc.).
+ ///
+ string DataDirectory { get; }
+
+ ///
+ /// Gets the directory for scan reports.
+ ///
+ string ReportsDirectory { get; }
+
+ ///
+ /// Gets the managed assemblies directory (Unity DLLs, game code).
+ ///
+ string ManagedDirectory { get; }
+
+ ///
+ /// Gets the path to the MLVScan assembly itself (for self-exclusion).
+ ///
+ string SelfAssemblyPath { get; }
+
+ ///
+ /// Gets the platform name for display/logging.
+ ///
+ string PlatformName { get; }
+ }
+}
diff --git a/BepInEx/5/BepInEx5Patcher.cs b/BepInEx/5/BepInEx5Patcher.cs
new file mode 100644
index 0000000..16144b3
--- /dev/null
+++ b/BepInEx/5/BepInEx5Patcher.cs
@@ -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
+{
+ ///
+ /// BepInEx 5.x preloader patcher that scans plugins for malicious patterns
+ /// before the chainloader initializes them.
+ ///
+ public static class BepInEx5Patcher
+ {
+ private static ManualLogSource _logger;
+
+ ///
+ /// Default whitelist for known-safe BepInEx ecosystem plugins.
+ ///
+ private static readonly string[] DefaultWhitelistedHashes =
+ [
+ // BepInEx ecosystem - known safe plugins
+ "8c0735f521d0fa785bf81b2e627a93042362b736ebc2c4c7ac425276b49fa692",
+ "9f86b196ffc845bdbc85192054e2876388ce1294b5a880459c93cbed7de2ae9d",
+ "bc67dab59532d0daca129e574c87d43b24a0b63ccb7312ccd25e0d7c4887784c",
+ "f1f3ff967bdb8f63a4bfd878255890f6393af37d3cc357babb6b504d9473ee06",
+ "d034d0e941deb47ea6b5ee8ca288bdb1d0bb25475dfba02cb61f6eadf0fa448e",
+ "e28b71abefdb5c2e90ea2d9e3c79bdff95f8173d08022732f62f35d2c328895d",
+ "bd5ec0343880b528ef190afe91778d172a239a625929dc176492eddc5c66cc31",
+ "503f851721ffacc7839e42d7c6c8a7c39fa2cea6e70a480b8bad822064d65aa0",
+ "184386c0f5f5bae6b63c96b73e312d3f39eba0d0ca81de3e3bd574ef389d1e29"
+ ];
+
+ ///
+ /// Required: Declares which assemblies to patch.
+ /// Empty = we don't patch game assemblies, just use Initialize() as entry point.
+ ///
+ public static IEnumerable TargetDLLs { get; } = Array.Empty();
+
+ ///
+ /// Required: Patching method (no-op - we don't modify game code).
+ ///
+ public static void Patch(AssemblyDefinition assembly) { }
+
+ ///
+ /// Called before patching - our main entry point.
+ /// Runs BEFORE the chainloader loads any plugins.
+ ///
+ 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.");
+ }
+
+ _logger.LogInfo("MLVScan preloader scan complete.");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError($"MLVScan initialization failed: {ex}");
+ }
+ }
+
+ ///
+ /// Called after all patching and assembly loading is complete.
+ ///
+ public static void Finish()
+ {
+ // Optional: cleanup, final summary logging
+ _logger?.LogDebug("MLVScan patcher finished.");
+ }
+ }
+}
diff --git a/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
new file mode 100644
index 0000000..77d38ca
--- /dev/null
+++ b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
@@ -0,0 +1,113 @@
+using System;
+using BepInEx;
+using BepInEx.Logging;
+using BepInEx.Preloader.Core.Patching;
+using MLVScan.BepInEx;
+using MLVScan.BepInEx.Adapters;
+
+namespace MLVScan.BepInEx6.IL2CPP
+{
+ ///
+ /// BepInEx 6.x (IL2CPP) preloader patcher that scans plugins for malicious patterns
+ /// before the chainloader initializes them.
+ ///
+ [PatcherPluginInfo("com.bars.mlvscan", "MLVScan", "1.6.1")]
+ public class BepInEx6IL2CppPatcher : BasePatcher
+ {
+ private ManualLogSource _logger;
+
+ ///
+ /// Default whitelist for known-safe BepInEx ecosystem plugins.
+ ///
+ private static readonly string[] DefaultWhitelistedHashes =
+ [
+ // BepInEx ecosystem - known safe plugins
+ "8c0735f521d0fa785bf81b2e627a93042362b736ebc2c4c7ac425276b49fa692",
+ "9f86b196ffc845bdbc85192054e2876388ce1294b5a880459c93cbed7de2ae9d",
+ "bc67dab59532d0daca129e574c87d43b24a0b63ccb7312ccd25e0d7c4887784c",
+ "f1f3ff967bdb8f63a4bfd878255890f6393af37d3cc357babb6b504d9473ee06",
+ "d034d0e941deb47ea6b5ee8ca288bdb1d0bb25475dfba02cb61f6eadf0fa448e",
+ "e28b71abefdb5c2e90ea2d9e3c79bdff95f8173d08022732f62f35d2c328895d",
+ "bd5ec0343880b528ef190afe91778d172a239a625929dc176492eddc5c66cc31",
+ "503f851721ffacc7839e42d7c6c8a7c39fa2cea6e70a480b8bad822064d65aa0",
+ "184386c0f5f5bae6b63c96b73e312d3f39eba0d0ca81de3e3bd574ef389d1e29"
+ ];
+
+ ///
+ /// Called when the patcher is initialized.
+ /// This is the main entry point for BepInEx 6.x patchers.
+ ///
+ public override void Initialize()
+ {
+ _logger = Logger.CreateLogSource("MLVScan");
+
+ try
+ {
+ _logger.LogInfo("MLVScan BepInEx 6 (IL2CPP) 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.");
+ }
+
+ _logger.LogInfo("MLVScan BepInEx 6 (IL2CPP) preloader scan complete.");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError($"MLVScan initialization failed: {ex}");
+ }
+ }
+
+ ///
+ /// Called when all patching is complete.
+ ///
+ public override void Finalizer()
+ {
+ _logger?.LogDebug("MLVScan patcher finished.");
+ }
+ }
+}
diff --git a/BepInEx/6/Mono/BepInEx6MonoPatcher.cs b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs
new file mode 100644
index 0000000..cf24036
--- /dev/null
+++ b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs
@@ -0,0 +1,113 @@
+using System;
+using BepInEx;
+using BepInEx.Logging;
+using BepInEx.Preloader.Core.Patching;
+using MLVScan.BepInEx;
+using MLVScan.BepInEx.Adapters;
+
+namespace MLVScan.BepInEx6.Mono
+{
+ ///
+ /// BepInEx 6.x (Mono) preloader patcher that scans plugins for malicious patterns
+ /// before the chainloader initializes them.
+ ///
+ [PatcherPluginInfo("com.bars.mlvscan", "MLVScan", "1.6.1")]
+ public class BepInEx6MonoPatcher : BasePatcher
+ {
+ private ManualLogSource _logger;
+
+ ///
+ /// Default whitelist for known-safe BepInEx ecosystem plugins.
+ ///
+ private static readonly string[] DefaultWhitelistedHashes =
+ [
+ // BepInEx ecosystem - known safe plugins
+ "8c0735f521d0fa785bf81b2e627a93042362b736ebc2c4c7ac425276b49fa692",
+ "9f86b196ffc845bdbc85192054e2876388ce1294b5a880459c93cbed7de2ae9d",
+ "bc67dab59532d0daca129e574c87d43b24a0b63ccb7312ccd25e0d7c4887784c",
+ "f1f3ff967bdb8f63a4bfd878255890f6393af37d3cc357babb6b504d9473ee06",
+ "d034d0e941deb47ea6b5ee8ca288bdb1d0bb25475dfba02cb61f6eadf0fa448e",
+ "e28b71abefdb5c2e90ea2d9e3c79bdff95f8173d08022732f62f35d2c328895d",
+ "bd5ec0343880b528ef190afe91778d172a239a625929dc176492eddc5c66cc31",
+ "503f851721ffacc7839e42d7c6c8a7c39fa2cea6e70a480b8bad822064d65aa0",
+ "184386c0f5f5bae6b63c96b73e312d3f39eba0d0ca81de3e3bd574ef389d1e29"
+ ];
+
+ ///
+ /// Called when the patcher is initialized.
+ /// This is the main entry point for BepInEx 6.x patchers.
+ ///
+ public override void Initialize()
+ {
+ _logger = Logger.CreateLogSource("MLVScan");
+
+ try
+ {
+ _logger.LogInfo("MLVScan BepInEx 6 (Mono) 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);
+
+ if (!config.EnableAutoScan)
+ {
+ _logger.LogInfo("Auto-scan is disabled. Skipping plugin scan.");
+ }
+ else
+ {
+ var scanResults = pluginScanner.ScanAllPlugins();
+
+ if (scanResults.Count > 0)
+ {
+ var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
+
+ 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.");
+ }
+ }
+
+ _logger.LogInfo("MLVScan BepInEx 6 (Mono) preloader scan complete.");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError($"MLVScan initialization failed: {ex}");
+ }
+ }
+
+ ///
+ /// Called when all patching is complete.
+ ///
+ public override void Finalizer()
+ {
+ _logger?.LogDebug("MLVScan patcher finished.");
+ }
+ }
+}
diff --git a/BepInEx/Adapters/BepInExAssemblyResolverProvider.cs b/BepInEx/Adapters/BepInExAssemblyResolverProvider.cs
new file mode 100644
index 0000000..a686764
--- /dev/null
+++ b/BepInEx/Adapters/BepInExAssemblyResolverProvider.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using BepInEx;
+using MLVScan.Abstractions;
+using Mono.Cecil;
+
+namespace MLVScan.BepInEx.Adapters
+{
+ ///
+ /// Provides assembly resolution for scanning in BepInEx context.
+ /// Adds all relevant BepInEx and game directories to search paths.
+ ///
+ public class BepInExAssemblyResolverProvider : IAssemblyResolverProvider
+ {
+ public IAssemblyResolver CreateResolver()
+ {
+ var resolver = new DefaultAssemblyResolver();
+
+ try
+ {
+ // Game's managed assemblies (Unity DLLs, game code)
+ if (Directory.Exists(Paths.ManagedPath))
+ resolver.AddSearchDirectory(Paths.ManagedPath);
+
+ // BepInEx core assemblies
+ if (Directory.Exists(Paths.BepInExAssemblyDirectory))
+ resolver.AddSearchDirectory(Paths.BepInExAssemblyDirectory);
+
+ // Plugin directory (for plugin-to-plugin references)
+ if (Directory.Exists(Paths.PluginPath))
+ resolver.AddSearchDirectory(Paths.PluginPath);
+
+ // Patcher directory (where we are running from)
+ if (Directory.Exists(Paths.PatcherPluginPath))
+ resolver.AddSearchDirectory(Paths.PatcherPluginPath);
+ }
+ catch (Exception)
+ {
+ // If path resolution fails, use default resolver behavior
+ }
+
+ return resolver;
+ }
+ }
+}
diff --git a/BepInEx/Adapters/BepInExScanLogger.cs b/BepInEx/Adapters/BepInExScanLogger.cs
new file mode 100644
index 0000000..0e21bfd
--- /dev/null
+++ b/BepInEx/Adapters/BepInExScanLogger.cs
@@ -0,0 +1,25 @@
+using System;
+using BepInEx.Logging;
+using MLVScan.Abstractions;
+
+namespace MLVScan.BepInEx.Adapters
+{
+ ///
+ /// Adapter that wraps BepInEx's logging system to implement IScanLogger.
+ ///
+ public class BepInExScanLogger : IScanLogger
+ {
+ private readonly ManualLogSource _logger;
+
+ public BepInExScanLogger(ManualLogSource logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public void Debug(string message) => _logger.LogDebug(message);
+ public void Info(string message) => _logger.LogInfo(message);
+ public void Warning(string message) => _logger.LogWarning(message);
+ public void Error(string message) => _logger.LogError(message);
+ public void Error(string message, Exception exception) => _logger.LogError($"{message}: {exception}");
+ }
+}
diff --git a/BepInEx/BepInExConfigManager.cs b/BepInEx/BepInExConfigManager.cs
new file mode 100644
index 0000000..2040e58
--- /dev/null
+++ b/BepInEx/BepInExConfigManager.cs
@@ -0,0 +1,142 @@
+using System;
+using System.IO;
+using System.Linq;
+using BepInEx;
+using BepInEx.Logging;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of IConfigManager using JSON file storage.
+ /// Required because BepInEx's ConfigFile isn't available at preload time.
+ ///
+ public class BepInExConfigManager : IConfigManager
+ {
+ private readonly ManualLogSource _logger;
+ private readonly string[] _defaultWhitelistedHashes;
+ private readonly string _configPath;
+ private ScanConfig _config;
+
+ // JSON serialization settings
+ private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNameCaseInsensitive = true,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ public BepInExConfigManager(ManualLogSource logger, string[] defaultWhitelistedHashes = null)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _defaultWhitelistedHashes = defaultWhitelistedHashes ?? Array.Empty();
+
+ // Config stored alongside other BepInEx configs
+ _configPath = Path.Combine(Paths.ConfigPath, "MLVScan.json");
+ _config = new ScanConfig();
+ }
+
+ public ScanConfig Config => _config;
+
+ public ScanConfig LoadConfig()
+ {
+ try
+ {
+ if (File.Exists(_configPath))
+ {
+ var json = File.ReadAllText(_configPath);
+ var loaded = JsonSerializer.Deserialize(json, JsonOptions);
+
+ if (loaded != null)
+ {
+ _config = loaded;
+ _logger.LogInfo("Configuration loaded from MLVScan.json");
+ return _config;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Failed to load config, using defaults: {ex.Message}");
+ }
+
+ // Create default config
+ _config = CreateDefaultConfig();
+ SaveConfig(_config);
+ _logger.LogInfo("Created default MLVScan.json configuration");
+
+ return _config;
+ }
+
+ private ScanConfig CreateDefaultConfig()
+ {
+ return new ScanConfig
+ {
+ EnableAutoScan = true,
+ EnableAutoDisable = true,
+ MinSeverityForDisable = Severity.Medium,
+ ScanDirectories = new[] { "plugins" },
+ SuspiciousThreshold = 1,
+ WhitelistedHashes = _defaultWhitelistedHashes,
+ DumpFullIlReports = false,
+ DeveloperMode = false
+ };
+ }
+
+ public void SaveConfig(ScanConfig config)
+ {
+ try
+ {
+ // Ensure config directory exists
+ var configDir = Path.GetDirectoryName(_configPath);
+ if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
+ {
+ Directory.CreateDirectory(configDir);
+ }
+
+ var json = JsonSerializer.Serialize(config, JsonOptions);
+
+ File.WriteAllText(_configPath, json);
+ _config = config;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to save config: {ex.Message}");
+ }
+ }
+
+ public bool IsHashWhitelisted(string hash)
+ {
+ if (string.IsNullOrWhiteSpace(hash))
+ return false;
+
+ return _config.WhitelistedHashes.Contains(
+ hash.ToLowerInvariant(),
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ public string[] GetWhitelistedHashes()
+ {
+ return _config.WhitelistedHashes;
+ }
+
+ public void SetWhitelistedHashes(string[] hashes)
+ {
+ if (hashes == null)
+ return;
+
+ var normalizedHashes = hashes
+ .Where(h => !string.IsNullOrWhiteSpace(h))
+ .Select(h => h.ToLowerInvariant())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ _config.WhitelistedHashes = normalizedHashes;
+ SaveConfig(_config);
+ _logger.LogInfo($"Updated whitelist with {normalizedHashes.Length} hash(es)");
+ }
+ }
+}
diff --git a/BepInEx/BepInExEnvironment.cs b/BepInEx/BepInExEnvironment.cs
new file mode 100644
index 0000000..54c703f
--- /dev/null
+++ b/BepInEx/BepInExEnvironment.cs
@@ -0,0 +1,68 @@
+using System;
+using System.IO;
+using BepInEx;
+using MLVScan.Abstractions;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of IPlatformEnvironment.
+ /// Uses BepInEx.Paths for path resolution.
+ ///
+ public class BepInExPlatformEnvironment : IPlatformEnvironment
+ {
+ private readonly string _dataDir;
+ private readonly string _reportsDir;
+ private readonly string[] _pluginDirectories;
+
+ public BepInExPlatformEnvironment()
+ {
+ _dataDir = Path.Combine(Paths.BepInExRootPath, "MLVScan");
+ _reportsDir = Path.Combine(_dataDir, "Reports");
+ _pluginDirectories = new[] { Paths.PluginPath };
+ }
+
+ public string GameRootDirectory => Paths.GameRootPath;
+
+ public string[] PluginDirectories => _pluginDirectories;
+
+ public string DataDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_dataDir))
+ Directory.CreateDirectory(_dataDir);
+ return _dataDir;
+ }
+ }
+
+ public string ReportsDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_reportsDir))
+ Directory.CreateDirectory(_reportsDir);
+ return _reportsDir;
+ }
+ }
+
+ public string ManagedDirectory => Paths.ManagedPath;
+
+ public string SelfAssemblyPath
+ {
+ get
+ {
+ try
+ {
+ return typeof(BepInExPlatformEnvironment).Assembly.Location;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+ }
+
+ public string PlatformName => "BepInEx";
+ }
+}
diff --git a/BepInEx/BepInExPluginDisabler.cs b/BepInEx/BepInExPluginDisabler.cs
new file mode 100644
index 0000000..0539ad9
--- /dev/null
+++ b/BepInEx/BepInExPluginDisabler.cs
@@ -0,0 +1,30 @@
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of plugin disabler.
+ /// Uses ".blocked" extension (BepInEx convention).
+ ///
+ public class BepInExPluginDisabler : PluginDisablerBase
+ {
+ private const string BepInExBlockedExtension = ".blocked";
+
+ public BepInExPluginDisabler(IScanLogger logger, ScanConfig config)
+ : base(logger, config)
+ {
+ }
+
+ protected override string DisabledExtension => BepInExBlockedExtension;
+
+ ///
+ /// BepInEx uses append style (plugin.dll -> plugin.dll.blocked).
+ ///
+ protected override string GetDisabledPath(string originalPath)
+ {
+ return originalPath + BepInExBlockedExtension;
+ }
+ }
+}
diff --git a/BepInEx/BepInExPluginScanner.cs b/BepInEx/BepInExPluginScanner.cs
new file mode 100644
index 0000000..63e4739
--- /dev/null
+++ b/BepInEx/BepInExPluginScanner.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using BepInEx;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of plugin scanner.
+ /// Scans BepInEx/plugins/ directory.
+ ///
+ public class BepInExPluginScanner : PluginScannerBase
+ {
+ private readonly BepInExPlatformEnvironment _environment;
+
+ public BepInExPluginScanner(
+ IScanLogger logger,
+ IAssemblyResolverProvider resolverProvider,
+ ScanConfig config,
+ IConfigManager configManager,
+ BepInExPlatformEnvironment environment)
+ : base(logger, resolverProvider, config, configManager)
+ {
+ _environment = environment ?? throw new ArgumentNullException(nameof(environment));
+ }
+
+ protected override IEnumerable GetScanDirectories()
+ {
+ // BepInEx plugins directory
+ if (Directory.Exists(Paths.PluginPath))
+ {
+ yield return Paths.PluginPath;
+ }
+ }
+
+ protected override bool IsSelfAssembly(string filePath)
+ {
+ try
+ {
+ var selfPath = _environment.SelfAssemblyPath;
+ if (string.IsNullOrEmpty(selfPath))
+ return false;
+
+ return Path.GetFullPath(filePath).Equals(
+ Path.GetFullPath(selfPath),
+ StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/BepInEx/BepInExReportGenerator.cs b/BepInEx/BepInExReportGenerator.cs
new file mode 100644
index 0000000..3a52399
--- /dev/null
+++ b/BepInEx/BepInExReportGenerator.cs
@@ -0,0 +1,276 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using BepInEx;
+using BepInEx.Logging;
+using MLVScan.Models;
+using MLVScan.Models.Rules;
+using MLVScan.Services;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// Generates detailed reports for blocked plugins.
+ ///
+ public class BepInExReportGenerator
+ {
+ private readonly ManualLogSource _logger;
+ private readonly ScanConfig _config;
+ private readonly string _reportDirectory;
+
+ public BepInExReportGenerator(ManualLogSource logger, ScanConfig config)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+
+ // Reports go to BepInEx/MLVScan/Reports/
+ _reportDirectory = Path.Combine(Paths.BepInExRootPath, "MLVScan", "Reports");
+ }
+
+ public void GenerateReports(
+ List disabledPlugins,
+ Dictionary> scanResults)
+ {
+ EnsureReportDirectoryExists();
+
+ foreach (var pluginInfo in disabledPlugins)
+ {
+ if (!scanResults.TryGetValue(pluginInfo.OriginalPath, out var findings))
+ continue;
+
+ var pluginName = Path.GetFileName(pluginInfo.OriginalPath);
+
+ // Log to console
+ LogConsoleReport(pluginName, pluginInfo.FileHash, findings);
+
+ // Generate file report
+ GenerateFileReport(pluginName, pluginInfo, findings);
+ }
+ }
+
+ private void LogConsoleReport(string pluginName, string hash, List findings)
+ {
+ _logger.LogWarning(new string('=', 50));
+ _logger.LogWarning($"BLOCKED PLUGIN: {pluginName}");
+ _logger.LogInfo($"SHA256: {hash}");
+ _logger.LogInfo($"Suspicious patterns: {findings.Count}");
+
+ var grouped = findings
+ .GroupBy(f => f.Severity)
+ .OrderByDescending(g => (int)g.Key);
+
+ foreach (var group in grouped)
+ {
+ _logger.LogInfo($" {group.Key}: {group.Count()} issue(s)");
+ }
+
+ // Show top 3 findings
+ var topFindings = findings
+ .OrderByDescending(f => f.Severity)
+ .Take(3);
+
+ foreach (var finding in topFindings)
+ {
+ _logger.LogWarning($"[{finding.Severity}] {finding.Description}");
+ _logger.LogInfo($" Location: {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ _logger.LogInfo(" Call Chain:");
+ foreach (var node in finding.CallChain.Nodes.Take(5))
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ _logger.LogInfo($" {prefix} {node.Location}");
+ }
+ if (finding.CallChain.Nodes.Count > 5)
+ {
+ _logger.LogInfo($" ... and {finding.CallChain.Nodes.Count - 5} more");
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ _logger.LogInfo($" Data Flow: {finding.DataFlowChain.Pattern} ({finding.DataFlowChain.Confidence * 100:F0}% confidence)");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ _logger.LogInfo($" Cross-method: {finding.DataFlowChain.InvolvedMethods.Count} methods involved");
+ }
+ }
+ }
+
+ if (findings.Count > 3)
+ {
+ _logger.LogInfo($" ... and {findings.Count - 3} more findings");
+ }
+
+ DisplaySecurityNotice(pluginName);
+ }
+
+ private void DisplaySecurityNotice(string pluginName)
+ {
+ _logger.LogWarning("--- SECURITY NOTICE ---");
+ _logger.LogInfo($"MLVScan blocked {pluginName} before it could execute.");
+ _logger.LogInfo("If this is your first time with this plugin, you are likely safe.");
+ _logger.LogInfo("If you've used it before, consider running a malware scan.");
+ _logger.LogInfo("");
+ _logger.LogInfo("To whitelist a false positive:");
+ _logger.LogInfo(" Add the SHA256 hash to BepInEx/config/MLVScan.json");
+ _logger.LogInfo("");
+ _logger.LogInfo("Resources:");
+ _logger.LogInfo(" Malwarebytes: https://www.malwarebytes.com/");
+ _logger.LogInfo(" Community: https://discord.gg/UD4K4chKak");
+ }
+
+ private void GenerateFileReport(
+ string pluginName,
+ DisabledPluginInfo pluginInfo,
+ List findings)
+ {
+ try
+ {
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+ var reportPath = Path.Combine(_reportDirectory, $"{pluginName}_{timestamp}.report.txt");
+
+ var sb = new StringBuilder();
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("MLVScan Security Report (BepInEx)");
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine($"Generated: {DateTime.Now}");
+ sb.AppendLine($"Plugin: {pluginName}");
+ sb.AppendLine($"SHA256: {pluginInfo.FileHash}");
+ sb.AppendLine($"Original Path: {pluginInfo.OriginalPath}");
+ sb.AppendLine($"Blocked Path: {pluginInfo.DisabledPath}");
+ sb.AppendLine($"Total Findings: {findings.Count}");
+ sb.AppendLine();
+
+ // Severity breakdown
+ sb.AppendLine("Severity Breakdown:");
+ foreach (var group in findings.GroupBy(f => f.Severity).OrderByDescending(g => (int)g.Key))
+ {
+ sb.AppendLine($" {group.Key}: {group.Count()}");
+ }
+ sb.AppendLine();
+
+ // Detailed findings
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("DETAILED FINDINGS");
+ sb.AppendLine(new string('=', 60));
+
+ var groupedByDescription = findings.GroupBy(f => f.Description);
+ foreach (var group in groupedByDescription)
+ {
+ var first = group.First();
+ sb.AppendLine();
+ sb.AppendLine($"[{first.Severity}] {first.Description}");
+ sb.AppendLine($"Occurrences: {group.Count()}");
+
+ if (_config.DeveloperMode && first.DeveloperGuidance != null)
+ {
+ sb.AppendLine();
+ sb.AppendLine("Developer Guidance:");
+ sb.AppendLine($" {first.DeveloperGuidance.Remediation}");
+
+ if (first.DeveloperGuidance.AlternativeApis?.Length > 0)
+ {
+ sb.AppendLine($" Alternatives: {string.Join(", ", first.DeveloperGuidance.AlternativeApis)}");
+ }
+ }
+
+ sb.AppendLine();
+ sb.AppendLine("Locations:");
+ foreach (var finding in group.Take(10))
+ {
+ sb.AppendLine($" - {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ sb.AppendLine(" Call Chain Analysis:");
+ foreach (var node in finding.CallChain.Nodes)
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ sb.AppendLine($" {prefix} {node.Location}");
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ sb.AppendLine($" {node.Description}");
+ }
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ sb.AppendLine(" Data Flow Analysis:");
+ sb.AppendLine($" Pattern: {finding.DataFlowChain.Pattern}");
+ sb.AppendLine($" Confidence: {finding.DataFlowChain.Confidence * 100:F0}%");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ sb.AppendLine($" Cross-method flow through {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
+
+ if (!string.IsNullOrEmpty(finding.CodeSnippet))
+ {
+ foreach (var line in finding.CodeSnippet.Split('\n').Take(5))
+ {
+ sb.AppendLine($" {line.Trim()}");
+ }
+ }
+ }
+
+ if (group.Count() > 10)
+ {
+ sb.AppendLine($" ... and {group.Count() - 10} more");
+ }
+ }
+
+ // Security notice
+ sb.AppendLine();
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("SECURITY RECOMMENDATIONS");
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("1. Verify with the modding community if this is a known mod");
+ sb.AppendLine("2. Run a full system scan with Malwarebytes or similar");
+ sb.AppendLine("3. Check the Discord for guidance: https://discord.gg/UD4K4chKak");
+ sb.AppendLine();
+ sb.AppendLine("To whitelist (if false positive):");
+ sb.AppendLine($" Add this hash to BepInEx/config/MLVScan.json:");
+ sb.AppendLine($" \"{pluginInfo.FileHash}\"");
+
+ File.WriteAllText(reportPath, sb.ToString());
+ _logger.LogInfo($"Report saved: {reportPath}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to generate report: {ex.Message}");
+ }
+ }
+
+ private void EnsureReportDirectoryExists()
+ {
+ try
+ {
+ if (!Directory.Exists(_reportDirectory))
+ {
+ Directory.CreateDirectory(_reportDirectory);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to create report directory: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/MLVScan.csproj b/MLVScan.csproj
index 1e0c99e..f26b150 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -1,565 +1,270 @@
+
+
+
netstandard2.1
+ latest
enable
disable
MLVScan
- default
false
1.6.1
1.6.1
en-US
+ True
+ false
+ MelonLoader;BepInEx;BepInEx6Mono;BepInEx6IL2CPP
+
+
+
+
+
+
+
+ https://api.nuget.org/v3/index.json;
+ https://nuget.bepinex.dev/v3/index.json
+
+
+
+
+
+
+
+ MELONLOADER
MLVScan.MelonLoader
- latest
- enable
+ $(NoWarn);MSB3277
+
+
+
+
+
+ BEPINEX
+ MLVScan.BepInEx
+
+
+
+
+
+
+ BEPINEX;BEPINEX6;BEPINEX6_MONO
+ MLVScan.BepInEx6.Mono
+
+
+
+
+
+
+ net6.0
+ BEPINEX;BEPINEX6;BEPINEX6_IL2CPP
+ MLVScan.BepInEx6.IL2CPP
+
+
+
+
+
+
+
+
+
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Accessibility.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\AstarPathfindingProject.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Autodesk.Fbx.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\com.rlabrecque.steamworks.net.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\HSVPicker.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\LokoSolo.PinchableScrollRect.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Mono.Data.Sqlite.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Mono.Posix.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Mono.WebBrowser.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Newtonsoft.Json.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\RuntimePreviewGenerator.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\StylizedWaterForURP.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.AI.Navigation.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Burst.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Burst.Unsafe.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Collections.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Collections.LowLevel.ILSupport.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Formats.Fbx.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.InputSystem.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.InputSystem.ForUI.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.InputSystem.RebindingUI.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Mathematics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.MemoryProfiler.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Postprocessing.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.ProGrids.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipeline.Universal.ShaderLibrary.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Core.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Core.ShaderLibrary.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Universal.Config.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Universal.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Universal.Shaders.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Analytics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.CloudDiagnostics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Analytics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Configuration.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Device.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Environments.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Environments.Internal.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Internal.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Networking.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Registration.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Scheduler.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Telemetry.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Threading.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.TerrainTools.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.TextMeshPro.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Timeline.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AccessibilityModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AndroidJNIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AnimationModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ARModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AssetBundleModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AudioModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ClothModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ClusterInputModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ClusterRendererModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ContentLoadModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.CoreModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.CrashReportingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.DirectorModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.DSPGraphModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.GameCenterModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.GIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.GridModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.HotReloadModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ImageConversionModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.IMGUIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.InputLegacyModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.InputModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.JSONSerializeModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.LocalizationModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.NVIDIAModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ParticleSystemModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.PerformanceReportingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.Physics2DModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.PhysicsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ProfilerModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.PropertiesModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ScreenCaptureModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SharedInternalsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SpriteMaskModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SpriteShapeModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.StreamingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SubstanceModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SubsystemsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TerrainModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TerrainPhysicsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TextCoreFontEngineModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TextCoreTextEngineModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TextRenderingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TilemapModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TLSModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UI.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UIElementsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UmbraModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityAnalyticsCommonModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityAnalyticsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityConnectModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityCurlModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityTestProtocolModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestAssetBundleModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestAudioModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestTextureModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestWWWModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VehiclesModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VFXModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VideoModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VirtualTexturingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VRModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.WindModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.XRModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\VisualDesignCafe.Nature.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\VisualDesignCafe.Packages.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(MelonLoaderPath)\MelonLoader.dll
false
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\VisualDesignCafe.ShaderX.dll
+
+ $(MelonLoaderPath)\0Harmony.dll
false
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\MelonLoader\net35\MelonLoader.dll
+
+ $(GameManagedPath)\UnityEngine.CoreModule.dll
false
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\MelonLoader\net35\0Harmony.dll
- false
+
+
+
+
+
+
+
+
+
+
+
+ $(BepInExCorePath)\BepInEx.dll
+
+
+ $(BepInExCorePath)\0Harmony.dll
-
- True
- false
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(OutputPath)$(AssemblyName).merged.dll
+ $(PkgILRepack)\tools\ILRepack.exe
+
+ "$(ILRepackExePath)"
+ mono "$(ILRepackExePath)"
+ /lib:"@(ILRepackLib, '" /lib:"')"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/MLVScan.sln b/MLVScan.sln
index efa2737..928fada 100644
--- a/MLVScan.sln
+++ b/MLVScan.sln
@@ -1,20 +1,70 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.13.35931.197 d17.13
+VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan", "MLVScan.csproj", "{219261F3-C447-4B11-80CA-B4149CECF3BE}"
EndProject
+
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ MelonLoader|Any CPU = MelonLoader|Any CPU
+ MelonLoader|x64 = MelonLoader|x64
+ MelonLoader|x86 = MelonLoader|x86
+ BepInEx|Any CPU = BepInEx|Any CPU
+ BepInEx|x64 = BepInEx|x64
+ BepInEx|x86 = BepInEx|x86
+ BepInEx6Mono|Any CPU = BepInEx6Mono|Any CPU
+ BepInEx6Mono|x64 = BepInEx6Mono|x64
+ BepInEx6Mono|x86 = BepInEx6Mono|x86
+ BepInEx6IL2CPP|Any CPU = BepInEx6IL2CPP|Any CPU
+ BepInEx6IL2CPP|x64 = BepInEx6IL2CPP|x64
+ BepInEx6IL2CPP|x86 = BepInEx6IL2CPP|x86
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|Any CPU.ActiveCfg = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|Any CPU.Build.0 = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x64.ActiveCfg = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x64.Build.0 = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x86.ActiveCfg = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x86.Build.0 = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|Any CPU.ActiveCfg = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|Any CPU.Build.0 = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x64.ActiveCfg = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x64.Build.0 = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x86.ActiveCfg = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x86.Build.0 = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|Any CPU.ActiveCfg = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|Any CPU.Build.0 = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x64.ActiveCfg = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x64.Build.0 = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x86.ActiveCfg = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x86.Build.0 = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|Any CPU.ActiveCfg = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|Any CPU.Build.0 = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x64.ActiveCfg = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x64.Build.0 = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x86.ActiveCfg = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x86.Build.0 = BepInEx6IL2CPP|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x64.Build.0 = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x86.Build.0 = Debug|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x64.ActiveCfg = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x64.Build.0 = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.ActiveCfg = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.Build.0 = Release|Any CPU
+
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Adapters/GameAssemblyResolverProvider.cs b/MelonLoader/Adapters/GameAssemblyResolverProvider.cs
similarity index 100%
rename from Adapters/GameAssemblyResolverProvider.cs
rename to MelonLoader/Adapters/GameAssemblyResolverProvider.cs
diff --git a/Adapters/MelonScanLogger.cs b/MelonLoader/Adapters/MelonScanLogger.cs
similarity index 100%
rename from Adapters/MelonScanLogger.cs
rename to MelonLoader/Adapters/MelonScanLogger.cs
diff --git a/Services/ConfigManager.cs b/MelonLoader/MelonConfigManager.cs
similarity index 94%
rename from Services/ConfigManager.cs
rename to MelonLoader/MelonConfigManager.cs
index c4887f1..cbb864d 100644
--- a/Services/ConfigManager.cs
+++ b/MelonLoader/MelonConfigManager.cs
@@ -1,9 +1,15 @@
+using System;
+using System.Linq;
using MelonLoader;
+using MLVScan.Abstractions;
using MLVScan.Models;
-namespace MLVScan.Services
+namespace MLVScan.MelonLoader
{
- public class ConfigManager
+ ///
+ /// MelonLoader implementation of IConfigManager using MelonPreferences.
+ ///
+ public class MelonConfigManager : IConfigManager
{
private readonly MelonLogger.Instance _logger;
private readonly MelonPreferences_Category _category;
@@ -17,7 +23,7 @@ public class ConfigManager
private readonly MelonPreferences_Entry _dumpFullIlReports;
private readonly MelonPreferences_Entry _developerMode;
- public ConfigManager(MelonLogger.Instance logger)
+ public MelonConfigManager(MelonLogger.Instance logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -72,6 +78,12 @@ public ConfigManager(MelonLogger.Instance logger)
public ScanConfig Config { get; private set; }
+ public ScanConfig LoadConfig()
+ {
+ UpdateConfigFromPreferences();
+ return Config;
+ }
+
private void OnConfigChanged(T oldValue, T newValue)
{
UpdateConfigFromPreferences();
diff --git a/MelonLoader/MelonEnvironment.cs b/MelonLoader/MelonEnvironment.cs
new file mode 100644
index 0000000..a9a64fa
--- /dev/null
+++ b/MelonLoader/MelonEnvironment.cs
@@ -0,0 +1,96 @@
+using System;
+using System.IO;
+using MelonLoader.Utils;
+using MLVScan.Abstractions;
+
+namespace MLVScan.MelonLoader
+{
+ ///
+ /// MelonLoader implementation of IPlatformEnvironment.
+ /// Uses MelonEnvironment for path resolution.
+ ///
+ public class MelonPlatformEnvironment : IPlatformEnvironment
+ {
+ private readonly string _gameRoot;
+ private readonly string _dataDir;
+ private readonly string _reportsDir;
+
+ public MelonPlatformEnvironment()
+ {
+ _gameRoot = MelonEnvironment.GameRootDirectory;
+ _dataDir = Path.Combine(_gameRoot, "MLVScan");
+ _reportsDir = Path.Combine(_dataDir, "Reports");
+ }
+
+ public string GameRootDirectory => _gameRoot;
+
+ public string[] PluginDirectories => new[]
+ {
+ Path.Combine(_gameRoot, "Mods"),
+ Path.Combine(_gameRoot, "Plugins")
+ };
+
+ public string DataDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_dataDir))
+ Directory.CreateDirectory(_dataDir);
+ return _dataDir;
+ }
+ }
+
+ public string ReportsDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_reportsDir))
+ Directory.CreateDirectory(_reportsDir);
+ return _reportsDir;
+ }
+ }
+
+ public string ManagedDirectory
+ {
+ get
+ {
+ // Find Unity data folder dynamically (pattern: *_Data)
+ try
+ {
+ var dataFolders = Directory.GetDirectories(_gameRoot, "*_Data");
+ foreach (var dataFolder in dataFolders)
+ {
+ var managedPath = Path.Combine(dataFolder, "Managed");
+ if (Directory.Exists(managedPath))
+ return managedPath;
+ }
+ }
+ catch { /* Ignore enumeration errors */ }
+
+ // Fallback for Il2Cpp games
+ var il2cppPath = Path.Combine(_gameRoot, "MelonLoader", "Managed");
+ if (Directory.Exists(il2cppPath))
+ return il2cppPath;
+
+ return string.Empty;
+ }
+ }
+
+ public string SelfAssemblyPath
+ {
+ get
+ {
+ try
+ {
+ return typeof(MelonPlatformEnvironment).Assembly.Location;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+ }
+
+ public string PlatformName => "MelonLoader";
+ }
+}
diff --git a/Core.cs b/MelonLoader/MelonLoaderPlugin.cs
similarity index 76%
rename from Core.cs
rename to MelonLoader/MelonLoaderPlugin.cs
index 2f8a125..cf64e3d 100644
--- a/Core.cs
+++ b/MelonLoader/MelonLoaderPlugin.cs
@@ -3,21 +3,28 @@
using System.IO;
using System.Linq;
using MelonLoader;
+using MelonLoader.Utils;
using MLVScan.Models;
+using MLVScan.Models.Rules;
using MLVScan.Services;
-[assembly: MelonInfo(typeof(MLVScan.Core), "MLVScan", MLVScan.PlatformConstants.PlatformVersion, "Bars")]
+[assembly: MelonInfo(typeof(MLVScan.MelonLoader.MelonLoaderPlugin), "MLVScan", MLVScan.PlatformConstants.PlatformVersion, "Bars")]
[assembly: MelonPriority(Int32.MinValue)]
[assembly: MelonColor(255, 139, 0, 0)]
-namespace MLVScan
+namespace MLVScan.MelonLoader
{
- public class Core : MelonPlugin
+ ///
+ /// MelonLoader plugin entry point for MLVScan.
+ /// Sets up services, initializes the default whitelist, and orchestrates scanning, disabling, and reporting.
+ ///
+ public class MelonLoaderPlugin : MelonPlugin
{
- private ServiceFactory _serviceFactory;
- private ConfigManager _configManager;
- private ModScanner _modScanner;
- private ModDisabler _modDisabler;
+ private MelonLoaderServiceFactory _serviceFactory;
+ private MelonConfigManager _configManager;
+ private MelonPlatformEnvironment _environment;
+ private MelonPluginScanner _pluginScanner;
+ private MelonPluginDisabler _pluginDisabler;
private IlDumpService _ilDumpService;
private DeveloperReportGenerator _developerReportGenerator;
private bool _initialized = false;
@@ -38,13 +45,14 @@ public override void OnEarlyInitializeMelon()
{
LoggerInstance.Msg("Pre-scanning for malicious mods...");
- _serviceFactory = new ServiceFactory(LoggerInstance);
+ _serviceFactory = new MelonLoaderServiceFactory(LoggerInstance);
_configManager = _serviceFactory.CreateConfigManager();
+ _environment = _serviceFactory.CreateEnvironment();
InitializeDefaultWhitelist();
- _modScanner = _serviceFactory.CreateModScanner();
- _modDisabler = _serviceFactory.CreateModDisabler();
+ _pluginScanner = _serviceFactory.CreatePluginScanner();
+ _pluginDisabler = _serviceFactory.CreatePluginDisabler();
_ilDumpService = _serviceFactory.CreateIlDumpService();
_developerReportGenerator = _serviceFactory.CreateDeveloperReportGenerator();
@@ -103,7 +111,7 @@ public Dictionary> ScanAndDisableMods(bool force = fal
}
LoggerInstance.Msg("Scanning for suspicious mods...");
- var scanResults = _modScanner.ScanAllMods(force);
+ var scanResults = _pluginScanner.ScanAllPlugins(force);
var filteredResults = scanResults
.Where(kv => kv.Value.Count > 0 && kv.Value.Any(f => f.Location != "Assembly scanning"))
@@ -111,7 +119,7 @@ public Dictionary> ScanAndDisableMods(bool force = fal
if (filteredResults.Count > 0)
{
- var disabledMods = _modDisabler.DisableSuspiciousMods(filteredResults, force);
+ var disabledMods = _pluginDisabler.DisableSuspiciousPlugins(filteredResults, force);
var disabledCount = disabledMods.Count;
LoggerInstance.Msg($"Disabled {disabledCount} suspicious mods");
@@ -135,7 +143,7 @@ public Dictionary> ScanAndDisableMods(bool force = fal
}
}
- private void GenerateDetailedReports(List disabledMods, Dictionary> scanResults)
+ private void GenerateDetailedReports(List disabledMods, Dictionary> scanResults)
{
var isDeveloperMode = _configManager?.Config?.DeveloperMode ?? false;
@@ -198,7 +206,7 @@ private void GenerateDetailedReports(List disabledMods, Diction
{
_developerReportGenerator.GenerateConsoleReport(modName, actualFindings);
}
- else
+ else
{
// Standard reporting
LoggerInstance.Warning("Suspicious patterns found:");
@@ -214,9 +222,39 @@ private void GenerateDetailedReports(List disabledMods, Diction
{
var finding = categoryFindings[i];
LoggerInstance.Msg($" * At: {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ LoggerInstance.Msg(" Call Chain Analysis:");
+ foreach (var node in finding.CallChain.Nodes.Take(4))
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???]"
+ };
+ LoggerInstance.Msg($" {prefix} {node.Location}");
+ }
+ if (finding.CallChain.Nodes.Count > 4)
+ {
+ LoggerInstance.Msg($" ... and {finding.CallChain.Nodes.Count - 4} more");
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ LoggerInstance.Msg($" Data Flow: {finding.DataFlowChain.Pattern} ({finding.DataFlowChain.Confidence * 100:F0}% confidence)");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ LoggerInstance.Msg($" Cross-method flow through {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
+
if (!string.IsNullOrEmpty(finding.CodeSnippet))
{
- LoggerInstance.Msg($" Code Snippet (IL):");
+ LoggerInstance.Msg(" Code Snippet (IL):");
foreach (var line in finding.CodeSnippet.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
LoggerInstance.Msg($" {line}");
@@ -239,7 +277,7 @@ private void GenerateDetailedReports(List disabledMods, Diction
try
{
// Generate report and prompt files
- var reportDirectory = Path.Combine(MelonLoader.Utils.MelonEnvironment.UserDataDirectory, "MLVScan", "Reports");
+ var reportDirectory = Path.Combine(MelonEnvironment.UserDataDirectory, "MLVScan", "Reports");
if (!Directory.Exists(reportDirectory))
{
Directory.CreateDirectory(reportDirectory);
@@ -302,10 +340,45 @@ private void GenerateDetailedReports(List disabledMods, Diction
writer.WriteLine($"\n== {group.Key} ==");
writer.WriteLine($"Severity: {group.Value[0].Severity}");
writer.WriteLine($"Instances: {group.Value.Count}");
- writer.WriteLine("\nLocations & Snippets:");
+ writer.WriteLine("\nLocations & Analysis:");
foreach (var finding in group.Value)
{
writer.WriteLine($"- {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ writer.WriteLine(" Call Chain Analysis:");
+ writer.WriteLine($" {finding.CallChain.Summary}");
+ writer.WriteLine(" Attack Path:");
+ foreach (var node in finding.CallChain.Nodes)
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???]"
+ };
+ writer.WriteLine($" {prefix} {node.Location}");
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ writer.WriteLine($" {node.Description}");
+ }
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ writer.WriteLine(" Data Flow Analysis:");
+ writer.WriteLine($" Pattern: {finding.DataFlowChain.Pattern}");
+ writer.WriteLine($" Confidence: {finding.DataFlowChain.Confidence * 100:F0}%");
+ writer.WriteLine($" {finding.DataFlowChain.Summary}");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ writer.WriteLine($" Cross-method flow through {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
+
if (!string.IsNullOrEmpty(finding.CodeSnippet))
{
writer.WriteLine(" Code Snippet (IL):");
@@ -406,6 +479,7 @@ private static void WriteSecurityNoticeToReport(StreamWriter writer)
writer.WriteLine("\nDETAILED MALWARE REMOVAL GUIDES:");
writer.WriteLine("- Malwarebytes Guide: https://www.malwarebytes.com/cybersecurity/basics/how-to-remove-virus-from-computer");
writer.WriteLine("- Microsoft Safety Scanner: https://learn.microsoft.com/en-us/defender-endpoint/safety-scanner-download");
+ writer.WriteLine("- XWorm (Common Modding Malware) Removal Guide: https://www.pcrisk.com/removal-guides/27436-xworm-rat");
writer.WriteLine("\n=============================================");
}
}
diff --git a/MelonLoader/MelonLoaderServiceFactory.cs b/MelonLoader/MelonLoaderServiceFactory.cs
new file mode 100644
index 0000000..9da899c
--- /dev/null
+++ b/MelonLoader/MelonLoaderServiceFactory.cs
@@ -0,0 +1,101 @@
+using System;
+using MelonLoader;
+using MLVScan.Abstractions;
+using MLVScan.Adapters;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.MelonLoader
+{
+ ///
+ /// Factory for creating MLVScan services in the MelonLoader context.
+ ///
+ public class MelonLoaderServiceFactory
+ {
+ private readonly MelonLogger.Instance _melonLogger;
+ private readonly IScanLogger _scanLogger;
+ private readonly IAssemblyResolverProvider _resolverProvider;
+ private readonly MelonConfigManager _configManager;
+ private readonly MelonPlatformEnvironment _environment;
+ private readonly ScanConfig _fallbackConfig;
+
+ public MelonLoaderServiceFactory(MelonLogger.Instance logger)
+ {
+ _melonLogger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _scanLogger = new MelonScanLogger(logger);
+ _resolverProvider = new GameAssemblyResolverProvider();
+ _environment = new MelonPlatformEnvironment();
+ _fallbackConfig = new ScanConfig();
+
+ try
+ {
+ _configManager = new MelonConfigManager(logger);
+ }
+ catch (Exception ex)
+ {
+ _melonLogger.Error($"Failed to create ConfigManager: {ex.Message}");
+ _melonLogger.Msg("Using default configuration values");
+ }
+ }
+
+ ///
+ /// Creates the configuration manager.
+ ///
+ /// The MelonConfigManager instance.
+ /// Thrown when the configuration manager is unavailable due to initialization failure.
+ public MelonConfigManager CreateConfigManager()
+ {
+ if (_configManager == null)
+ {
+ throw new InvalidOperationException("Configuration manager unavailable: failed to initialize during factory construction.");
+ }
+ return _configManager;
+ }
+
+ public MelonPlatformEnvironment CreateEnvironment()
+ {
+ return _environment;
+ }
+
+ public AssemblyScanner CreateAssemblyScanner()
+ {
+ var config = _configManager?.Config ?? _fallbackConfig;
+ var rules = RuleFactory.CreateDefaultRules();
+
+ return new AssemblyScanner(rules, config, _resolverProvider);
+ }
+
+ public MelonPluginScanner CreatePluginScanner()
+ {
+ var config = _configManager?.Config ?? _fallbackConfig;
+ return new MelonPluginScanner(
+ _scanLogger,
+ _resolverProvider,
+ config,
+ _configManager,
+ _environment);
+ }
+
+ public MelonPluginDisabler CreatePluginDisabler()
+ {
+ var config = _configManager?.Config ?? _fallbackConfig;
+ return new MelonPluginDisabler(_scanLogger, config);
+ }
+
+ public PromptGeneratorService CreatePromptGenerator()
+ {
+ var config = _configManager?.Config ?? _fallbackConfig;
+ return new PromptGeneratorService(config, _scanLogger);
+ }
+
+ public IlDumpService CreateIlDumpService()
+ {
+ return new IlDumpService(_scanLogger, _environment);
+ }
+
+ public DeveloperReportGenerator CreateDeveloperReportGenerator()
+ {
+ return new DeveloperReportGenerator(_scanLogger);
+ }
+ }
+}
diff --git a/MelonLoader/MelonPluginDisabler.cs b/MelonLoader/MelonPluginDisabler.cs
new file mode 100644
index 0000000..0ad3498
--- /dev/null
+++ b/MelonLoader/MelonPluginDisabler.cs
@@ -0,0 +1,31 @@
+using System.IO;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.MelonLoader
+{
+ ///
+ /// MelonLoader implementation of plugin disabler.
+ /// Uses ".di" extension (MelonLoader convention).
+ ///
+ public class MelonPluginDisabler : PluginDisablerBase
+ {
+ private const string MelonDisabledExtension = ".di";
+
+ public MelonPluginDisabler(IScanLogger logger, ScanConfig config)
+ : base(logger, config)
+ {
+ }
+
+ protected override string DisabledExtension => MelonDisabledExtension;
+
+ ///
+ /// MelonLoader uses extension replacement style (plugin.dll -> plugin.di).
+ ///
+ protected override string GetDisabledPath(string originalPath)
+ {
+ return Path.ChangeExtension(originalPath, MelonDisabledExtension);
+ }
+ }
+}
diff --git a/MelonLoader/MelonPluginScanner.cs b/MelonLoader/MelonPluginScanner.cs
new file mode 100644
index 0000000..888322b
--- /dev/null
+++ b/MelonLoader/MelonPluginScanner.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using MelonLoader.Utils;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.MelonLoader
+{
+ ///
+ /// MelonLoader implementation of plugin scanner.
+ /// Scans Mods/, Plugins/, and Thunderstore directories.
+ ///
+ public class MelonPluginScanner : PluginScannerBase
+ {
+ private readonly MelonPlatformEnvironment _environment;
+
+ public MelonPluginScanner(
+ IScanLogger logger,
+ IAssemblyResolverProvider resolverProvider,
+ ScanConfig config,
+ IConfigManager configManager,
+ MelonPlatformEnvironment environment)
+ : base(logger, resolverProvider, config, configManager)
+ {
+ _environment = environment ?? throw new ArgumentNullException(nameof(environment));
+ }
+
+ protected override IEnumerable GetScanDirectories()
+ {
+ foreach (var scanDir in Config.ScanDirectories)
+ {
+ yield return Path.Combine(_environment.GameRootDirectory, scanDir);
+ }
+ }
+
+ protected override bool IsSelfAssembly(string filePath)
+ {
+ try
+ {
+ var selfPath = _environment.SelfAssemblyPath;
+ if (string.IsNullOrEmpty(selfPath))
+ return false;
+
+ return Path.GetFullPath(filePath).Equals(
+ Path.GetFullPath(selfPath),
+ StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ protected override void OnScanComplete(Dictionary> results)
+ {
+ // Also scan Thunderstore Mod Manager directories
+ ScanThunderstoreModManager(results);
+ }
+
+ private void ScanThunderstoreModManager(Dictionary> results)
+ {
+ try
+ {
+ string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ string thunderstoreBasePath = Path.Combine(appDataPath, "Thunderstore Mod Manager", "DataFolder");
+
+ if (!Directory.Exists(thunderstoreBasePath))
+ return;
+
+ // Find game folders
+ foreach (var gameFolder in Directory.GetDirectories(thunderstoreBasePath))
+ {
+ // Scan profiles
+ string profilesPath = Path.Combine(gameFolder, "profiles");
+ if (!Directory.Exists(profilesPath))
+ continue;
+
+ foreach (var profileFolder in Directory.GetDirectories(profilesPath))
+ {
+ // Scan Mods directory
+ string modsPath = Path.Combine(profileFolder, "Mods");
+ if (Directory.Exists(modsPath))
+ {
+ Logger.Info($"Scanning Thunderstore profile mods: {modsPath}");
+ ScanDirectory(modsPath, results);
+ }
+
+ // Scan Plugins directory
+ string pluginsPath = Path.Combine(profileFolder, "Plugins");
+ if (Directory.Exists(pluginsPath))
+ {
+ Logger.Info($"Scanning Thunderstore profile plugins: {pluginsPath}");
+ ScanDirectory(pluginsPath, results);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Error scanning Thunderstore Mod Manager directories: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/Models/DisabledModInfo.cs b/Models/DisabledModInfo.cs
deleted file mode 100644
index 70c5923..0000000
--- a/Models/DisabledModInfo.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace MLVScan.Models
-{
- public class DisabledModInfo
- {
- public string OriginalPath { get; }
- public string DisabledPath { get; }
- public string FileHash { get; }
-
- public DisabledModInfo(string originalPath, string disabledPath, string fileHash)
- {
- OriginalPath = originalPath;
- DisabledPath = disabledPath;
- FileHash = fileHash;
- }
- }
-}
diff --git a/PlatformConstants.cs b/PlatformConstants.cs
index 9873b2c..468ff41 100644
--- a/PlatformConstants.cs
+++ b/PlatformConstants.cs
@@ -1,20 +1,43 @@
namespace MLVScan
{
///
- /// Version and build constants for MLVScan.MelonLoader platform.
+ /// Version and build constants for MLVScan platform.
/// Update this file when releasing new versions.
+ /// Uses conditional compilation for platform-specific values.
///
public static class PlatformConstants
{
///
- /// Platform-specific version (MelonLoader implementation).
+ /// Platform-specific version.
///
public const string PlatformVersion = "1.6.1";
+#if MELONLOADER
///
/// Platform name identifier.
///
public const string PlatformName = "MLVScan.MelonLoader";
+#elif BEPINEX6_IL2CPP
+ ///
+ /// Platform name identifier.
+ ///
+ public const string PlatformName = "MLVScan.BepInEx6.IL2CPP";
+#elif BEPINEX6_MONO
+ ///
+ /// Platform name identifier.
+ ///
+ public const string PlatformName = "MLVScan.BepInEx6.Mono";
+#elif BEPINEX
+ ///
+ /// Platform name identifier.
+ ///
+ public const string PlatformName = "MLVScan.BepInEx";
+#else
+ ///
+ /// Platform name identifier (fallback for IDE).
+ ///
+ public const string PlatformName = "MLVScan";
+#endif
///
/// Gets the full platform version string.
diff --git a/README.md b/README.md
index dc64b97..f69b2f0 100644
--- a/README.md
+++ b/README.md
@@ -4,26 +4,54 @@
-**MLVScan** is a security-focused MelonLoader plugin that protects your game by scanning mods for malicious patterns *before* they execute.
+**MLVScan** is a security-focused mod loader plugin that protects your game by scanning mods for malicious patterns *before* they execute.
+
+Supports **MelonLoader**, **BepInEx 5.x**, and **BepInEx 6.x** (IL2CPP/Mono).

## ⚡ Quick Start
-1. **Download** the latest `MLVScan.dll` from [Releases](../../releases).
+### For MelonLoader
+1. **Download** the latest `MLVScan.MelonLoader.dll` from [Releases](../../releases).
2. **Install** by dropping it into your game's `Plugins` folder.
3. **Play!** MLVScan automatically scans mods on startup.
+### For BepInEx 5.x
+1. **Download** the latest `MLVScan.BepInEx.dll` from [Releases](../../releases).
+2. **Install** by dropping it into your game's `BepInEx/patchers` folder.
+3. **Play!** MLVScan automatically scans plugins before they load.
+
## 📚 Documentation
Detailed documentation is available in the **[MLVScan Wiki](https://github.com/ifBars/MLVScan/wiki)**:
-* **[Getting Started](https://github.com/ifBars/MLVScan/wiki/Getting-Started)** - Full installation and setup guide.
+* **[Getting Started](https://github.com/ifBars/MLVScan/wiki/Getting-Started)** - Full installation and setup guide for both MelonLoader and BepInEx.
* **[Whitelisting](https://github.com/ifBars/MLVScan/wiki/Whitelisting)** - How to use the SHA256 security whitelist.
* **[Understanding Reports](https://github.com/ifBars/MLVScan/wiki/Scan-Reports)** - Interpret warnings and security levels.
* **[Architecture](https://github.com/ifBars/MLVScan/wiki/Architecture)** - How the ecosystem works.
* **[FAQ](https://github.com/ifBars/MLVScan/wiki/FAQ)** - Common questions and troubleshooting.
+### Key Differences
+
+**MelonLoader:**
+- Runs as a plugin during the mod loading phase
+- Configuration stored in `MelonPreferences.cfg`
+- Reports saved to `UserData/MLVScan/Reports/`
+
+**BepInEx 5.x:**
+- Runs as a preloader patcher (scans before chainloader)
+- Configuration stored in `BepInEx/config/MLVScan.json`
+- Reports saved to `BepInEx/MLVScan/Reports/`
+- Install via `BepInEx/patchers` folder
+
+**BepInEx 6.x (IL2CPP / Mono):**
+- Runs as a preloader patcher (scans before chainloader)
+- Configuration stored in `BepInEx/config/MLVScan.json` (same as 5.x)
+- Reports saved to `BepInEx/MLVScan/Reports/` (same as 5.x)
+- Uses `[PatcherPlugin]` attribute-based packaging instead of patchers folder
+- Plugin compatibility: BepInEx 5.x plugins may require updating for 6.x API changes
+
## 🛡️ Powered by MLVScan.Core
MLVScan is built on **[MLVScan.Core](https://github.com/ifBars/MLVScan.Core)**, a cross-platform malware detection engine.
diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs
index e54f15c..6dc7160 100644
--- a/Services/DeveloperReportGenerator.cs
+++ b/Services/DeveloperReportGenerator.cs
@@ -1,6 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text;
-using MelonLoader;
+using MLVScan.Abstractions;
using MLVScan.Models;
+using MLVScan.Models.Rules;
namespace MLVScan.Services
{
@@ -10,9 +14,9 @@ namespace MLVScan.Services
///
public class DeveloperReportGenerator
{
- private readonly MelonLogger.Instance _logger;
+ private readonly IScanLogger _logger;
- public DeveloperReportGenerator(MelonLogger.Instance logger)
+ public DeveloperReportGenerator(IScanLogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -25,12 +29,12 @@ public void GenerateConsoleReport(string modName, List findings)
if (findings == null || findings.Count == 0)
return;
- _logger.Msg("======= DEVELOPER SCAN REPORT =======");
- _logger.Msg(PlatformConstants.GetFullVersionInfo());
- _logger.Msg($"Mod: {modName}");
- _logger.Msg("--------------------------------------");
- _logger.Msg($"Total findings: {findings.Count}");
- _logger.Msg("");
+ _logger.Info("======= DEVELOPER SCAN REPORT =======");
+ _logger.Info(PlatformConstants.GetFullVersionInfo());
+ _logger.Info($"Mod: {modName}");
+ _logger.Info("--------------------------------------");
+ _logger.Info($"Total findings: {findings.Count}");
+ _logger.Info("");
var groupedByRule = findings
.Where(f => f.RuleId != null)
@@ -42,57 +46,85 @@ public void GenerateConsoleReport(string modName, List findings)
var firstFinding = ruleGroup.First();
var count = ruleGroup.Count();
- _logger.Msg($"[{firstFinding.Severity}] {firstFinding.Description}");
- _logger.Msg($" Rule: {firstFinding.RuleId}");
- _logger.Msg($" Occurrences: {count}");
+ _logger.Info($"[{firstFinding.Severity}] {firstFinding.Description}");
+ _logger.Info($" Rule: {firstFinding.RuleId}");
+ _logger.Info($" Occurrences: {count}");
// Show developer guidance if available
if (firstFinding.DeveloperGuidance != null)
{
- _logger.Msg("");
- _logger.Msg(" Developer Guidance:");
- _logger.Msg($" {WrapText(firstFinding.DeveloperGuidance.Remediation, 2)}");
+ _logger.Info("");
+ _logger.Info(" Developer Guidance:");
+ _logger.Info($" {WrapText(firstFinding.DeveloperGuidance.Remediation, 2)}");
if (!string.IsNullOrEmpty(firstFinding.DeveloperGuidance.DocumentationUrl))
{
- _logger.Msg($" Documentation: {firstFinding.DeveloperGuidance.DocumentationUrl}");
+ _logger.Info($" Documentation: {firstFinding.DeveloperGuidance.DocumentationUrl}");
}
if (firstFinding.DeveloperGuidance.AlternativeApis != null &&
firstFinding.DeveloperGuidance.AlternativeApis.Length > 0)
{
- _logger.Msg($" Suggested APIs: {string.Join(", ", firstFinding.DeveloperGuidance.AlternativeApis)}");
+ _logger.Info($" Suggested APIs: {string.Join(", ", firstFinding.DeveloperGuidance.AlternativeApis)}");
}
if (!firstFinding.DeveloperGuidance.IsRemediable)
{
- _logger.Warning(" ⚠ No safe alternative - this pattern should not be used in MelonLoader mods.");
+ _logger.Warning(" No safe alternative - this pattern should not be used in mods.");
}
}
else
{
- _logger.Msg(" (No developer guidance available for this rule)");
+ _logger.Info(" (No developer guidance available for this rule)");
}
// Show sample locations
- _logger.Msg("");
- _logger.Msg(" Sample locations:");
+ _logger.Info("");
+ _logger.Info(" Sample locations:");
foreach (var finding in ruleGroup.Take(3))
{
- _logger.Msg($" - {finding.Location}");
+ _logger.Info($" - {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ _logger.Info(" Call Chain:");
+ foreach (var node in finding.CallChain.Nodes.Take(3))
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???]"
+ };
+ _logger.Info($" {prefix} {node.Location}");
+ }
+ if (finding.CallChain.Nodes.Count > 3)
+ {
+ _logger.Info($" ... and {finding.CallChain.Nodes.Count - 3} more");
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ _logger.Info($" Data Flow: {finding.DataFlowChain.Pattern} ({finding.DataFlowChain.Confidence * 100:F0}%)");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ _logger.Info($" Cross-method: {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
}
if (count > 3)
{
- _logger.Msg($" ... and {count - 3} more");
+ _logger.Info($" ... and {count - 3} more");
}
- _logger.Msg("");
- _logger.Msg("--------------------------------------");
+ _logger.Info("--------------------------------------");
}
- _logger.Msg("");
- _logger.Msg("For more information, visit: https://discord.gg/UD4K4chKak");
- _logger.Msg("=====================================");
+ _logger.Info("");
+ _logger.Info("For more information, visit: https://discord.gg/UD4K4chKak");
+ _logger.Info("=====================================");
}
///
@@ -151,7 +183,7 @@ public string GenerateFileReport(string modName, string hash, List
if (!firstFinding.DeveloperGuidance.IsRemediable)
{
sb.AppendLine("");
- sb.AppendLine("WARNING: This pattern has no safe alternative and should not be used in MelonLoader mods.");
+ sb.AppendLine("WARNING: This pattern has no safe alternative and should not be used in mods.");
}
sb.AppendLine("");
@@ -167,12 +199,76 @@ public string GenerateFileReport(string modName, string hash, List
foreach (var finding in ruleGroup)
{
sb.AppendLine($"Location: {finding.Location}");
+
+ // Show call chain if available
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ sb.AppendLine();
+ sb.AppendLine("--- CALL CHAIN ANALYSIS ---");
+ sb.AppendLine(finding.CallChain.Summary);
+ sb.AppendLine();
+ sb.AppendLine("Attack Path:");
+ foreach (var node in finding.CallChain.Nodes)
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???]"
+ };
+ sb.AppendLine($" {prefix} {node.Location}");
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ sb.AppendLine($" {node.Description}");
+ }
+ }
+ }
+
+ // Show data flow chain if available
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ sb.AppendLine();
+ sb.AppendLine("--- DATA FLOW ANALYSIS ---");
+ sb.AppendLine($"Pattern: {finding.DataFlowChain.Pattern}");
+ sb.AppendLine($"Confidence: {finding.DataFlowChain.Confidence * 100:F0}%");
+ sb.AppendLine(finding.DataFlowChain.Summary);
+
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ sb.AppendLine();
+ sb.AppendLine("Cross-Method Flow:");
+ foreach (var method in finding.DataFlowChain.InvolvedMethods)
+ {
+ sb.AppendLine($" - {method}");
+ }
+ }
+
+ sb.AppendLine();
+ sb.AppendLine("Data Flow Path:");
+ for (int i = 0; i < finding.DataFlowChain.Nodes.Count; i++)
+ {
+ var node = finding.DataFlowChain.Nodes[i];
+ var arrow = i > 0 ? " -> " : " ";
+ var prefix = node.NodeType switch
+ {
+ DataFlowNodeType.Source => "[SOURCE]",
+ DataFlowNodeType.Transform => "[TRANSFORM]",
+ DataFlowNodeType.Sink => "[SINK]",
+ DataFlowNodeType.Intermediate => "[PASS]",
+ _ => "[???]"
+ };
+ sb.AppendLine($"{arrow}{prefix} {node.Operation} ({node.DataDescription})");
+ sb.AppendLine($"{new string(' ', arrow.Length)} Location: {node.Location}");
+ }
+ }
+
if (!string.IsNullOrEmpty(finding.CodeSnippet))
{
sb.AppendLine("Code Snippet:");
sb.AppendLine(finding.CodeSnippet);
}
- sb.AppendLine("");
+ sb.AppendLine();
}
}
diff --git a/Services/HashUtility.cs b/Services/HashUtility.cs
new file mode 100644
index 0000000..f559752
--- /dev/null
+++ b/Services/HashUtility.cs
@@ -0,0 +1,65 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+
+namespace MLVScan.Services
+{
+ ///
+ /// Utility class for computing file hashes.
+ /// Used for whitelisting and tracking disabled mods.
+ ///
+ public static class HashUtility
+ {
+ ///
+ /// Calculates the SHA256 hash of a file.
+ ///
+ /// Path to the file to hash.
+ /// Lowercase hex string of the hash, or error message on failure.
+ public static string CalculateFileHash(string filePath)
+ {
+ try
+ {
+ if (!File.Exists(filePath))
+ return $"File not found: {filePath}";
+
+ using var sha256 = SHA256.Create();
+ using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ var hash = sha256.ComputeHash(stream);
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return "Access denied";
+ }
+ catch (IOException ex)
+ {
+ return $"IO Error: {ex.Message}";
+ }
+ catch (Exception ex)
+ {
+ return $"Error: {ex.Message}";
+ }
+ }
+
+ ///
+ /// Validates that a string looks like a valid SHA256 hash.
+ ///
+ public static bool IsValidHash(string hash)
+ {
+ if (string.IsNullOrWhiteSpace(hash))
+ return false;
+
+ // SHA256 produces 64 hex characters
+ if (hash.Length != 64)
+ return false;
+
+ foreach (char c in hash)
+ {
+ if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Services/IlDumpService.cs b/Services/IlDumpService.cs
index 6590344..919423d 100644
--- a/Services/IlDumpService.cs
+++ b/Services/IlDumpService.cs
@@ -1,21 +1,26 @@
using System;
using System.IO;
using System.Linq;
-using MelonLoader;
-using MelonLoader.Utils;
+using MLVScan.Abstractions;
using Mono.Cecil;
using Mono.Cecil.Cil;
namespace MLVScan.Services
{
+ ///
+ /// Service for dumping IL code from assemblies.
+ /// Uses platform environment for assembly resolution.
+ ///
public class IlDumpService
{
- private readonly MelonLogger.Instance _logger;
+ private readonly IScanLogger _logger;
+ private readonly IPlatformEnvironment _environment;
private readonly DefaultAssemblyResolver _assemblyResolver;
- public IlDumpService(MelonLogger.Instance logger)
+ public IlDumpService(IScanLogger logger, IPlatformEnvironment environment)
{
- _logger = logger;
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _environment = environment ?? throw new ArgumentNullException(nameof(environment));
_assemblyResolver = BuildResolver();
}
@@ -42,7 +47,8 @@ public bool TryDumpAssembly(string assemblyPath, string outputPath)
using var writer = new StreamWriter(outputPath);
writer.WriteLine($"; Full IL dump for {Path.GetFileName(assemblyPath)}");
- writer.WriteLine($"; Generated: {System.DateTime.Now}");
+ writer.WriteLine($"; Generated: {DateTime.Now}");
+ writer.WriteLine($"; Platform: {_environment.PlatformName}");
writer.WriteLine();
foreach (var module in assembly.Modules)
@@ -56,12 +62,12 @@ public bool TryDumpAssembly(string assemblyPath, string outputPath)
}
}
- _logger?.Msg($"Saved IL dump to: {outputPath}");
+ _logger.Info($"Saved IL dump to: {outputPath}");
return true;
}
catch (Exception ex)
{
- _logger?.Error($"Failed to dump IL for {Path.GetFileName(assemblyPath)}: {ex.Message}");
+ _logger.Error($"Failed to dump IL for {Path.GetFileName(assemblyPath)}: {ex.Message}");
return false;
}
}
@@ -70,37 +76,27 @@ private DefaultAssemblyResolver BuildResolver()
{
var resolver = new DefaultAssemblyResolver();
- var gameDir = MelonEnvironment.GameRootDirectory;
- var melonDir = Path.Combine(gameDir, "MelonLoader");
-
- resolver.AddSearchDirectory(gameDir);
-
- if (Directory.Exists(melonDir))
+ // Add game root
+ var gameDir = _environment.GameRootDirectory;
+ if (Directory.Exists(gameDir))
{
- resolver.AddSearchDirectory(melonDir);
-
- var managedDir = Path.Combine(melonDir, "Managed");
- if (Directory.Exists(managedDir))
- {
- resolver.AddSearchDirectory(managedDir);
- }
-
- var dependenciesDir = Path.Combine(melonDir, "Dependencies");
- if (Directory.Exists(dependenciesDir))
- {
- resolver.AddSearchDirectory(dependenciesDir);
+ resolver.AddSearchDirectory(gameDir);
+ }
- foreach (var dir in Directory.GetDirectories(dependenciesDir, "*", SearchOption.AllDirectories))
- {
- resolver.AddSearchDirectory(dir);
- }
- }
+ // Add managed assemblies directory
+ var managedDir = _environment.ManagedDirectory;
+ if (!string.IsNullOrEmpty(managedDir) && Directory.Exists(managedDir))
+ {
+ resolver.AddSearchDirectory(managedDir);
}
- var gameManagedDir = Path.Combine(gameDir, "Schedule I_Data", "Managed");
- if (Directory.Exists(gameManagedDir))
+ // Add plugin directories
+ foreach (var pluginDir in _environment.PluginDirectories)
{
- resolver.AddSearchDirectory(gameManagedDir);
+ if (Directory.Exists(pluginDir))
+ {
+ resolver.AddSearchDirectory(pluginDir);
+ }
}
return resolver;
diff --git a/Services/ModDisabler.cs b/Services/ModDisabler.cs
deleted file mode 100644
index da1b895..0000000
--- a/Services/ModDisabler.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MelonLoader;
-using MLVScan.Models;
-
-namespace MLVScan.Services
-{
- public class ModDisabler(MelonLogger.Instance logger, ScanConfig config)
- {
- private readonly MelonLogger.Instance _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- private readonly ScanConfig _config = config ?? throw new ArgumentNullException(nameof(config));
- private const string DisabledExtension = ".di";
-
- public List DisableSuspiciousMods(Dictionary> scanResults, bool forceDisable = false)
- {
- if (!forceDisable && !_config.EnableAutoDisable)
- {
- _logger.Msg("Automatic disabling is turned off in configuration");
- return [];
- }
-
- var disabledMods = new List();
-
- foreach (var (modFilePath, findings) in scanResults)
- {
- var severeFindings = findings.Where(f =>
- (int)f.Severity >= (int)_config.MinSeverityForDisable)
- .ToList();
-
- // Skip if no findings meet the minimum severity threshold
- if (severeFindings.Count == 0)
- {
- _logger.Msg($"Mod {Path.GetFileName(modFilePath)} has findings but none meet minimum severity threshold ({_config.MinSeverityForDisable} - If this is set to Medium, the mod is likely not malicious).");
- continue;
- }
-
- if (!forceDisable && severeFindings.Count < _config.SuspiciousThreshold)
- {
- _logger.Msg($"Mod {Path.GetFileName(modFilePath)} has suspicious patterns but below threshold");
- continue;
- }
-
- try
- {
- var fileHash = ModScanner.CalculateFileHash(modFilePath);
- var newFilePath = Path.ChangeExtension(modFilePath, DisabledExtension);
-
- if (File.Exists(newFilePath))
- {
- File.Delete(newFilePath);
- }
-
- File.Move(modFilePath, newFilePath);
- _logger.Warning($"Disabled potentially malicious mod: {Path.GetFileName(modFilePath)}");
- disabledMods.Add(new DisabledModInfo(modFilePath, newFilePath, fileHash));
- }
- catch (Exception ex)
- {
- _logger.Error($"Failed to disable mod {Path.GetFileName(modFilePath)}: {ex.Message}");
- }
- }
-
- return disabledMods;
- }
- }
-}
diff --git a/Services/ModScanner.cs b/Services/ModScanner.cs
deleted file mode 100644
index de67508..0000000
--- a/Services/ModScanner.cs
+++ /dev/null
@@ -1,169 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using MelonLoader;
-using MelonLoader.Utils;
-using MLVScan.Models;
-
-namespace MLVScan.Services
-{
- public class ModScanner(
- AssemblyScanner assemblyScanner,
- MelonLogger.Instance logger,
- ScanConfig config,
- ConfigManager configManager)
- {
- private readonly AssemblyScanner _assemblyScanner = assemblyScanner ?? throw new ArgumentNullException(nameof(assemblyScanner));
- private readonly MelonLogger.Instance _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- private readonly ScanConfig _config = config ?? throw new ArgumentNullException(nameof(config));
- private readonly ConfigManager _configManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
-
- public Dictionary> ScanAllMods(bool forceScanning = false)
- {
- var results = new Dictionary>();
-
- if (!forceScanning && !_config.EnableAutoScan)
- {
- _logger.Msg("Automatic scanning is disabled in configuration");
- return results;
- }
-
- // Scan configured directories
- foreach (var scanDir in _config.ScanDirectories)
- {
- var directoryPath = Path.Combine(MelonEnvironment.GameRootDirectory, scanDir);
-
- if (!Directory.Exists(directoryPath))
- {
- _logger.Warning($"Directory not found: {directoryPath}");
- continue;
- }
-
- ScanDirectory(directoryPath, results);
- }
-
- // Scan Thunderstore Mod Manager directories
- ScanThunderstoreModManager(results);
-
- return results;
- }
-
- private void ScanDirectory(string directoryPath, Dictionary> results)
- {
- var modFiles = Directory.GetFiles(directoryPath, "*.dll", SearchOption.AllDirectories);
- _logger.Msg($"Found {modFiles.Length} potential mod files in {directoryPath}");
-
- foreach (var modFile in modFiles)
- {
- try
- {
- var modFileName = Path.GetFileName(modFile);
- var hash = CalculateFileHash(modFile);
-
- if (Path.GetFullPath(modFile).Equals(Path.GetFullPath(typeof(Core).Assembly.Location), StringComparison.OrdinalIgnoreCase))
- {
- _logger.Msg($"Skipping self: {modFileName}");
- continue;
- }
-
- if (_configManager.IsHashWhitelisted(hash))
- {
- _logger.Msg($"Skipping whitelisted mod: {modFileName} [Hash: {hash}]");
- continue;
- }
-
- var findings = _assemblyScanner.Scan(modFile).ToList();
- if (findings.Count < _config.SuspiciousThreshold)
- continue;
- results.Add(modFile, findings);
- _logger.Warning($"Found {findings.Count} suspicious patterns in {Path.GetFileName(modFile)}");
- }
- catch (Exception ex)
- {
- _logger.Error($"Error scanning {Path.GetFileName(modFile)}: {ex.Message}");
- }
- }
- }
-
- private void ScanThunderstoreModManager(Dictionary> results)
- {
- try
- {
- string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
- string thunderstoreBasePath = Path.Combine(appDataPath, "Thunderstore Mod Manager", "DataFolder");
-
- if (!Directory.Exists(thunderstoreBasePath))
- {
- return;
- }
-
- // Find game folders (like ScheduleI in the example)
- foreach (var gameFolder in Directory.GetDirectories(thunderstoreBasePath))
- {
- // Scan profiles
- string profilesPath = Path.Combine(gameFolder, "profiles");
- if (Directory.Exists(profilesPath))
- {
- foreach (var profileFolder in Directory.GetDirectories(profilesPath))
- {
- // Scan Mods directory
- string modsPath = Path.Combine(profileFolder, "Mods");
- if (Directory.Exists(modsPath))
- {
- _logger.Msg($"Scanning Thunderstore profile mods: {modsPath}");
- ScanDirectory(modsPath, results);
- }
-
- // Scan Plugins directory
- string pluginsPath = Path.Combine(profileFolder, "Plugins");
- if (Directory.Exists(pluginsPath))
- {
- _logger.Msg($"Scanning Thunderstore profile plugins: {pluginsPath}");
- ScanDirectory(pluginsPath, results);
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- _logger.Error($"Error scanning Thunderstore Mod Manager directories: {ex.Message}");
- }
- }
-
- public static string CalculateFileHash(string filePath)
- {
- try
- {
- // Check if file exists first
- if (!File.Exists(filePath))
- {
- return $"File not found: {filePath}";
- }
-
- using (var sha256 = SHA256.Create())
- {
- using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- {
- var hash = sha256.ComputeHash(stream);
- return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
- }
- }
- }
- catch (UnauthorizedAccessException)
- {
- return "Access denied";
- }
- catch (IOException ex)
- {
- return $"IO Error: {ex.Message}";
- }
- catch (Exception ex)
- {
- return $"Error: {ex.Message}";
- }
- }
- }
-}
diff --git a/Services/PluginDisablerBase.cs b/Services/PluginDisablerBase.cs
new file mode 100644
index 0000000..4196020
--- /dev/null
+++ b/Services/PluginDisablerBase.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+
+namespace MLVScan.Services
+{
+ ///
+ /// Result info for a disabled plugin.
+ ///
+ public class DisabledPluginInfo
+ {
+ public string OriginalPath { get; }
+ public string DisabledPath { get; }
+ public string FileHash { get; }
+
+ public DisabledPluginInfo(string originalPath, string disabledPath, string fileHash)
+ {
+ OriginalPath = originalPath;
+ DisabledPath = disabledPath;
+ FileHash = fileHash;
+ }
+ }
+
+ ///
+ /// Abstract base class for plugin/mod disabling across platforms.
+ /// Contains shared disabling logic, with platform-specific details
+ /// delegated to derived classes.
+ ///
+ public abstract class PluginDisablerBase
+ {
+ protected readonly IScanLogger Logger;
+ protected readonly ScanConfig Config;
+
+ ///
+ /// Extension used to disable plugins (platform-specific).
+ /// MelonLoader uses ".di", BepInEx uses ".blocked".
+ ///
+ protected abstract string DisabledExtension { get; }
+
+ protected PluginDisablerBase(IScanLogger logger, ScanConfig config)
+ {
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ Config = config ?? throw new ArgumentNullException(nameof(config));
+ }
+
+ ///
+ /// Gets the disabled file path for a given plugin.
+ ///
+ protected virtual string GetDisabledPath(string originalPath)
+ {
+ // BepInEx style: append extension (plugin.dll.blocked)
+ // MelonLoader style: replace extension (plugin.di)
+ // Default to append style
+ return originalPath + DisabledExtension;
+ }
+
+ ///
+ /// Called after a plugin is disabled successfully.
+ ///
+ protected virtual void OnPluginDisabled(string originalPath, string disabledPath, string hash) { }
+
+ ///
+ /// Disables plugins that meet severity and threshold criteria.
+ ///
+ /// Dictionary of file paths to their findings.
+ /// If true, disables even if auto-disable is off.
+ public List DisableSuspiciousPlugins(
+ Dictionary> scanResults,
+ bool forceDisable = false)
+ {
+ if (!forceDisable && !Config.EnableAutoDisable)
+ {
+ Logger.Info("Automatic disabling is turned off in configuration");
+ return new List();
+ }
+
+ var disabledPlugins = new List();
+
+ foreach (var (pluginPath, findings) in scanResults)
+ {
+ var severeFindings = findings
+ .Where(f => (int)f.Severity >= (int)Config.MinSeverityForDisable)
+ .ToList();
+
+ if (severeFindings.Count == 0)
+ {
+ Logger.Info($"Plugin {Path.GetFileName(pluginPath)} has findings but none meet severity threshold ({Config.MinSeverityForDisable})");
+ continue;
+ }
+
+ if (!forceDisable && severeFindings.Count < Config.SuspiciousThreshold)
+ {
+ Logger.Info($"Plugin {Path.GetFileName(pluginPath)} below suspicious threshold");
+ continue;
+ }
+
+ try
+ {
+ var info = DisablePlugin(pluginPath);
+ if (info != null)
+ {
+ disabledPlugins.Add(info);
+ OnPluginDisabled(info.OriginalPath, info.DisabledPath, info.FileHash);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Failed to disable {Path.GetFileName(pluginPath)}: {ex.Message}");
+ }
+ }
+
+ return disabledPlugins;
+ }
+
+ ///
+ /// Disables a single plugin by renaming it.
+ ///
+ protected virtual DisabledPluginInfo DisablePlugin(string pluginPath)
+ {
+ var fileHash = HashUtility.CalculateFileHash(pluginPath);
+ var disabledPath = GetDisabledPath(pluginPath);
+
+ // Remove existing disabled file if present
+ if (File.Exists(disabledPath))
+ {
+ File.Delete(disabledPath);
+ }
+
+ // Rename to disable
+ File.Move(pluginPath, disabledPath);
+
+ Logger.Warning($"BLOCKED: {Path.GetFileName(pluginPath)}");
+ return new DisabledPluginInfo(pluginPath, disabledPath, fileHash);
+ }
+ }
+}
diff --git a/Services/PluginScannerBase.cs b/Services/PluginScannerBase.cs
new file mode 100644
index 0000000..c9a7233
--- /dev/null
+++ b/Services/PluginScannerBase.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+
+namespace MLVScan.Services
+{
+ ///
+ /// Abstract base class for plugin/mod scanning across platforms.
+ /// Contains shared scanning logic, with platform-specific details
+ /// delegated to derived classes.
+ ///
+ public abstract class PluginScannerBase
+ {
+ protected readonly IScanLogger Logger;
+ protected readonly IAssemblyResolverProvider ResolverProvider;
+ protected readonly ScanConfig Config;
+ protected readonly IConfigManager ConfigManager;
+ protected readonly AssemblyScanner AssemblyScanner;
+
+ protected PluginScannerBase(
+ IScanLogger logger,
+ IAssemblyResolverProvider resolverProvider,
+ ScanConfig config,
+ IConfigManager configManager)
+ {
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ ResolverProvider = resolverProvider ?? throw new ArgumentNullException(nameof(resolverProvider));
+ Config = config ?? throw new ArgumentNullException(nameof(config));
+ ConfigManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
+
+ var rules = RuleFactory.CreateDefaultRules();
+ AssemblyScanner = new AssemblyScanner(rules, Config, ResolverProvider);
+ }
+
+ ///
+ /// Gets the directories to scan for plugins.
+ ///
+ protected abstract IEnumerable GetScanDirectories();
+
+ ///
+ /// Checks if a file path is this scanner's own assembly.
+ ///
+ protected abstract bool IsSelfAssembly(string filePath);
+
+ ///
+ /// Performs any platform-specific post-scan processing.
+ ///
+ protected virtual void OnScanComplete(Dictionary> results) { }
+
+ ///
+ /// Scans all plugins in configured directories.
+ ///
+ /// If true, scans even if auto-scan is disabled.
+ public Dictionary> ScanAllPlugins(bool forceScanning = false)
+ {
+ var results = new Dictionary>();
+
+ if (!forceScanning && !Config.EnableAutoScan)
+ {
+ Logger.Info("Automatic scanning is disabled in configuration");
+ return results;
+ }
+
+ foreach (var directory in GetScanDirectories())
+ {
+ if (!Directory.Exists(directory))
+ {
+ Logger.Warning($"Directory not found: {directory}");
+ continue;
+ }
+
+ ScanDirectory(directory, results);
+ }
+
+ OnScanComplete(results);
+ return results;
+ }
+
+ ///
+ /// Scans a single directory for malicious plugins.
+ ///
+ protected virtual void ScanDirectory(string directoryPath, Dictionary> results)
+ {
+ var pluginFiles = Directory.GetFiles(directoryPath, "*.dll", SearchOption.AllDirectories);
+ Logger.Info($"Found {pluginFiles.Length} plugin files in {directoryPath}");
+
+ foreach (var pluginFile in pluginFiles)
+ {
+ try
+ {
+ ScanSingleFile(pluginFile, results);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Error scanning {Path.GetFileName(pluginFile)}: {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Scans a single file and adds results if suspicious.
+ ///
+ protected virtual void ScanSingleFile(string filePath, Dictionary> results)
+ {
+ var fileName = Path.GetFileName(filePath);
+ var hash = HashUtility.CalculateFileHash(filePath);
+
+ // Skip ourselves
+ if (IsSelfAssembly(filePath))
+ {
+ Logger.Debug($"Skipping self: {fileName}");
+ return;
+ }
+
+ // Skip whitelisted plugins
+ if (ConfigManager.IsHashWhitelisted(hash))
+ {
+ Logger.Debug($"Skipping whitelisted: {fileName}");
+ return;
+ }
+
+ var findings = AssemblyScanner.Scan(filePath).ToList();
+
+ // Filter out placeholder findings
+ var actualFindings = findings
+ .Where(f => f.Location != "Assembly scanning")
+ .ToList();
+
+ if (actualFindings.Count >= Config.SuspiciousThreshold)
+ {
+ results[filePath] = actualFindings;
+ Logger.Warning($"Found {actualFindings.Count} suspicious patterns in {fileName}");
+ }
+ }
+ }
+}
diff --git a/Services/PromptGeneratorService.cs b/Services/PromptGeneratorService.cs
index ea4cd08..944bfee 100644
--- a/Services/PromptGeneratorService.cs
+++ b/Services/PromptGeneratorService.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Text;
+using MLVScan.Abstractions;
using MLVScan.Models;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -13,9 +14,9 @@ namespace MLVScan.Services
public class PromptGeneratorService
{
private readonly ScanConfig _config;
- private readonly MelonLoader.MelonLogger.Instance _logger;
+ private readonly IScanLogger _logger;
- public PromptGeneratorService(ScanConfig config, MelonLoader.MelonLogger.Instance logger)
+ public PromptGeneratorService(ScanConfig config, IScanLogger logger)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
diff --git a/Services/ServiceFactory.cs b/Services/ServiceFactory.cs
deleted file mode 100644
index 26a78ad..0000000
--- a/Services/ServiceFactory.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using MelonLoader;
-using MLVScan.Abstractions;
-using MLVScan.Adapters;
-using MLVScan.Models;
-using MLVScan.Services;
-
-namespace MLVScan
-{
- ///
- /// Factory for creating MLVScan services in the MelonLoader context.
- ///
- public class ServiceFactory
- {
- private readonly MelonLogger.Instance _logger;
- private readonly IScanLogger _scanLogger;
- private readonly IAssemblyResolverProvider _resolverProvider;
- private readonly ConfigManager _configManager;
- private readonly ScanConfig _fallbackConfig;
-
- public ServiceFactory(MelonLogger.Instance logger)
- {
- _logger = logger;
- _scanLogger = new MelonScanLogger(logger);
- _resolverProvider = new GameAssemblyResolverProvider();
- _fallbackConfig = new ScanConfig();
-
- try
- {
- _configManager = new ConfigManager(logger);
- }
- catch (Exception ex)
- {
- _logger.Error($"Failed to create ConfigManager: {ex.Message}");
- _logger.Msg("Using default configuration values");
- }
- }
-
- public ConfigManager CreateConfigManager()
- {
- return _configManager;
- }
-
- public AssemblyScanner CreateAssemblyScanner()
- {
- var config = _configManager?.Config ?? _fallbackConfig;
- var rules = RuleFactory.CreateDefaultRules();
-
- return new AssemblyScanner(rules, config, _resolverProvider);
- }
-
- public ModScanner CreateModScanner()
- {
- var assemblyScanner = CreateAssemblyScanner();
- var config = _configManager?.Config ?? _fallbackConfig;
- return new ModScanner(assemblyScanner, _logger, config, _configManager);
- }
-
- public ModDisabler CreateModDisabler()
- {
- var config = _configManager?.Config ?? _fallbackConfig;
- return new ModDisabler(_logger, config);
- }
-
- public PromptGeneratorService CreatePromptGenerator()
- {
- var config = _configManager?.Config ?? _fallbackConfig;
- return new PromptGeneratorService(config, _logger);
- }
-
- public IlDumpService CreateIlDumpService()
- {
- return new IlDumpService(_logger);
- }
-
- public DeveloperReportGenerator CreateDeveloperReportGenerator()
- {
- return new DeveloperReportGenerator(_logger);
- }
- }
-}
diff --git a/local.build.props.example b/local.build.props.example
new file mode 100644
index 0000000..baba87c
--- /dev/null
+++ b/local.build.props.example
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+