From 4f8f826381e4de0f317c8d2b39f55d9b14e1288b Mon Sep 17 00:00:00 2001 From: ifBars Date: Sat, 24 Jan 2026 03:04:54 -0800 Subject: [PATCH 01/28] feat: BepInEx 5.* Support --- Abstractions/IConfigManager.cs | 42 ++ Abstractions/IPlatformEnvironment.cs | 46 ++ .../BepInExAssemblyResolverProvider.cs | 45 ++ BepInEx/Adapters/BepInExScanLogger.cs | 25 + BepInEx/BepInExConfigManager.cs | 142 ++++ BepInEx/BepInExEnvironment.cs | 69 ++ BepInEx/BepInExPluginDisabler.cs | 30 + BepInEx/BepInExPluginScanner.cs | 57 ++ BepInEx/BepInExReportGenerator.cs | 214 ++++++ BepInEx/MLVScanPatcher.cs | 120 ++++ Core.cs | 19 +- MLVScan.csproj | 679 ++++-------------- .../Adapters}/GameAssemblyResolverProvider.cs | 0 .../Adapters}/MelonScanLogger.cs | 0 .../MelonConfigManager.cs | 18 +- MelonLoader/MelonEnvironment.cs | 88 +++ MelonLoader/MelonPluginDisabler.cs | 31 + MelonLoader/MelonPluginScanner.cs | 107 +++ Models/DisabledModInfo.cs | 16 - PlatformConstants.cs | 17 +- Services/DeveloperReportGenerator.cs | 61 +- Services/HashUtility.cs | 65 ++ Services/IlDumpService.cs | 64 +- Services/ModDisabler.cs | 68 -- Services/ModScanner.cs | 169 ----- Services/PluginDisablerBase.cs | 139 ++++ Services/PluginScannerBase.cs | 139 ++++ Services/PromptGeneratorService.cs | 5 +- Services/ServiceFactory.cs | 43 +- 29 files changed, 1639 insertions(+), 879 deletions(-) create mode 100644 Abstractions/IConfigManager.cs create mode 100644 Abstractions/IPlatformEnvironment.cs create mode 100644 BepInEx/Adapters/BepInExAssemblyResolverProvider.cs create mode 100644 BepInEx/Adapters/BepInExScanLogger.cs create mode 100644 BepInEx/BepInExConfigManager.cs create mode 100644 BepInEx/BepInExEnvironment.cs create mode 100644 BepInEx/BepInExPluginDisabler.cs create mode 100644 BepInEx/BepInExPluginScanner.cs create mode 100644 BepInEx/BepInExReportGenerator.cs create mode 100644 BepInEx/MLVScanPatcher.cs rename {Adapters => MelonLoader/Adapters}/GameAssemblyResolverProvider.cs (100%) rename {Adapters => MelonLoader/Adapters}/MelonScanLogger.cs (100%) rename Services/ConfigManager.cs => MelonLoader/MelonConfigManager.cs (94%) create mode 100644 MelonLoader/MelonEnvironment.cs create mode 100644 MelonLoader/MelonPluginDisabler.cs create mode 100644 MelonLoader/MelonPluginScanner.cs delete mode 100644 Models/DisabledModInfo.cs create mode 100644 Services/HashUtility.cs delete mode 100644 Services/ModDisabler.cs delete mode 100644 Services/ModScanner.cs create mode 100644 Services/PluginDisablerBase.cs create mode 100644 Services/PluginScannerBase.cs 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/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..913aedf --- /dev/null +++ b/BepInEx/BepInExConfigManager.cs @@ -0,0 +1,142 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using BepInEx; +using BepInEx.Logging; +using MLVScan.Abstractions; +using MLVScan.Models; + +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 options + private static readonly JsonSerializerOptions JsonOptions = new() + { + 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..c91b6f4 --- /dev/null +++ b/BepInEx/BepInExEnvironment.cs @@ -0,0 +1,69 @@ +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; + + public BepInExPlatformEnvironment() + { + _dataDir = Path.Combine(Paths.BepInExRootPath, "MLVScan"); + _reportsDir = Path.Combine(_dataDir, "Reports"); + } + + public string GameRootDirectory => Paths.GameRootPath; + + public string[] PluginDirectories => new[] + { + Paths.PluginPath + }; + + 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..da77b07 --- /dev/null +++ b/BepInEx/BepInExReportGenerator.cs @@ -0,0 +1,214 @@ +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.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 (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 (!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/BepInEx/MLVScanPatcher.cs b/BepInEx/MLVScanPatcher.cs new file mode 100644 index 0000000..e5933d7 --- /dev/null +++ b/BepInEx/MLVScanPatcher.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using BepInEx; +using BepInEx.Logging; +using Mono.Cecil; +using MLVScan.BepInEx.Adapters; + +namespace MLVScan.BepInEx +{ + /// + /// BepInEx preloader patcher that scans plugins for malicious patterns + /// before the chainloader initializes them. + /// + public static class MLVScanPatcher + { + 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 + { + _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/Core.cs b/Core.cs index 2f8a125..e497758 100644 --- a/Core.cs +++ b/Core.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using MelonLoader; +using MLVScan.MelonLoader; using MLVScan.Models; using MLVScan.Services; @@ -15,9 +16,10 @@ namespace MLVScan public class Core : MelonPlugin { private ServiceFactory _serviceFactory; - private ConfigManager _configManager; - private ModScanner _modScanner; - private ModDisabler _modDisabler; + private MelonConfigManager _configManager; + private MelonPlatformEnvironment _environment; + private MelonPluginScanner _pluginScanner; + private MelonPluginDisabler _pluginDisabler; private IlDumpService _ilDumpService; private DeveloperReportGenerator _developerReportGenerator; private bool _initialized = false; @@ -40,11 +42,12 @@ public override void OnEarlyInitializeMelon() _serviceFactory = new ServiceFactory(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 +106,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 +114,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 +138,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; diff --git a/MLVScan.csproj b/MLVScan.csproj index 651e68c..fb6aefc 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -1,565 +1,178 @@ + + + netstandard2.1 + latest enable disable MLVScan - default false 1.6.1 1.6.1 en-US + True + false + MelonLoader;BepInEx + + + + + + + MELONLOADER MLVScan.MelonLoader - latest - enable + + + + + + BEPINEX + MLVScan.BepInEx + + + + + + + + + - - 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MelonLoaderPath)\MelonLoader.dll false - - D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ContentLoadModule.dll + + $(MelonLoaderPath)\0Harmony.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 + $(GameManagedPath)\UnityEngine.CoreModule.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 - false - - - D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\VisualDesignCafe.ShaderX.dll - false - - - D:\SteamLibrary\steamapps\common\Schedule I_alternate\MelonLoader\net35\MelonLoader.dll - false - - - D:\SteamLibrary\steamapps\common\Schedule I_alternate\MelonLoader\net35\0Harmony.dll - false + + + + + + + + + + + $(BepInExCorePath)\BepInEx.dll + + + $(BepInExCorePath)\0Harmony.dll - - True - false - + + + + - - - - - - - - - - - - + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + 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..34e4bdc --- /dev/null +++ b/MelonLoader/MelonEnvironment.cs @@ -0,0 +1,88 @@ +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 + { + // Unity managed assemblies location + var managedPath = Path.Combine(_gameRoot, "Schedule I_Data", "Managed"); + if (Directory.Exists(managedPath)) + return managedPath; + + // 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/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..cad1ce2 --- /dev/null +++ b/MelonLoader/MelonPluginScanner.cs @@ -0,0 +1,107 @@ +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() + { + // Configured directories relative to game root + foreach (var scanDir in Config.ScanDirectories) + { + yield return Path.Combine(MelonEnvironment.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..a974ade 100644 --- a/PlatformConstants.cs +++ b/PlatformConstants.cs @@ -1,20 +1,33 @@ 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 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/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs index e54f15c..1bba7be 100644 --- a/Services/DeveloperReportGenerator.cs +++ b/Services/DeveloperReportGenerator.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; -using MelonLoader; +using MLVScan.Abstractions; using MLVScan.Models; namespace MLVScan.Services @@ -10,9 +13,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 +28,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 +45,57 @@ 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 (count > 3) { - _logger.Msg($" ... and {count - 3} more"); + _logger.Info($" ... and {count - 3} more"); } - _logger.Msg(""); - _logger.Msg("--------------------------------------"); + _logger.Info(""); + _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 +154,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(""); 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..f778f4f --- /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.Add(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 index 26a78ad..7e03b1c 100644 --- a/Services/ServiceFactory.cs +++ b/Services/ServiceFactory.cs @@ -1,6 +1,8 @@ +using System; using MelonLoader; using MLVScan.Abstractions; using MLVScan.Adapters; +using MLVScan.MelonLoader; using MLVScan.Models; using MLVScan.Services; @@ -11,35 +13,42 @@ namespace MLVScan /// public class ServiceFactory { - private readonly MelonLogger.Instance _logger; + private readonly MelonLogger.Instance _melonLogger; private readonly IScanLogger _scanLogger; private readonly IAssemblyResolverProvider _resolverProvider; - private readonly ConfigManager _configManager; + private readonly MelonConfigManager _configManager; + private readonly MelonPlatformEnvironment _environment; private readonly ScanConfig _fallbackConfig; public ServiceFactory(MelonLogger.Instance logger) { - _logger = logger; + _melonLogger = logger ?? throw new ArgumentNullException(nameof(logger)); _scanLogger = new MelonScanLogger(logger); _resolverProvider = new GameAssemblyResolverProvider(); + _environment = new MelonPlatformEnvironment(); _fallbackConfig = new ScanConfig(); try { - _configManager = new ConfigManager(logger); + _configManager = new MelonConfigManager(logger); } catch (Exception ex) { - _logger.Error($"Failed to create ConfigManager: {ex.Message}"); - _logger.Msg("Using default configuration values"); + _melonLogger.Error($"Failed to create ConfigManager: {ex.Message}"); + _melonLogger.Msg("Using default configuration values"); } } - public ConfigManager CreateConfigManager() + public MelonConfigManager CreateConfigManager() { return _configManager; } + public MelonPlatformEnvironment CreateEnvironment() + { + return _environment; + } + public AssemblyScanner CreateAssemblyScanner() { var config = _configManager?.Config ?? _fallbackConfig; @@ -48,33 +57,37 @@ public AssemblyScanner CreateAssemblyScanner() return new AssemblyScanner(rules, config, _resolverProvider); } - public ModScanner CreateModScanner() + public MelonPluginScanner CreatePluginScanner() { - var assemblyScanner = CreateAssemblyScanner(); var config = _configManager?.Config ?? _fallbackConfig; - return new ModScanner(assemblyScanner, _logger, config, _configManager); + return new MelonPluginScanner( + _scanLogger, + _resolverProvider, + config, + _configManager, + _environment); } - public ModDisabler CreateModDisabler() + public MelonPluginDisabler CreatePluginDisabler() { var config = _configManager?.Config ?? _fallbackConfig; - return new ModDisabler(_logger, config); + return new MelonPluginDisabler(_scanLogger, config); } public PromptGeneratorService CreatePromptGenerator() { var config = _configManager?.Config ?? _fallbackConfig; - return new PromptGeneratorService(config, _logger); + return new PromptGeneratorService(config, _scanLogger); } public IlDumpService CreateIlDumpService() { - return new IlDumpService(_logger); + return new IlDumpService(_scanLogger, _environment); } public DeveloperReportGenerator CreateDeveloperReportGenerator() { - return new DeveloperReportGenerator(_logger); + return new DeveloperReportGenerator(_scanLogger); } } } From 78b8f2130087af01cc1cb34d1f5d5cd12d31146a Mon Sep 17 00:00:00 2001 From: ifBars Date: Sat, 24 Jan 2026 12:42:18 -0800 Subject: [PATCH 02/28] fix: Migrate from System.Text.Json to Newtonsoft.Json --- BepInEx/BepInExConfigManager.cs | 17 ++++++++--------- MLVScan.csproj | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/BepInEx/BepInExConfigManager.cs b/BepInEx/BepInExConfigManager.cs index 913aedf..bc65bb3 100644 --- a/BepInEx/BepInExConfigManager.cs +++ b/BepInEx/BepInExConfigManager.cs @@ -1,12 +1,12 @@ using System; using System.IO; using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; using BepInEx; using BepInEx.Logging; using MLVScan.Abstractions; using MLVScan.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace MLVScan.BepInEx { @@ -21,12 +21,11 @@ public class BepInExConfigManager : IConfigManager private readonly string _configPath; private ScanConfig _config; - // JSON serialization options - private static readonly JsonSerializerOptions JsonOptions = new() + // JSON serialization settings + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } + Formatting = Formatting.Indented, + Converters = { new StringEnumConverter() } }; public BepInExConfigManager(ManualLogSource logger, string[] defaultWhitelistedHashes = null) @@ -48,7 +47,7 @@ public ScanConfig LoadConfig() if (File.Exists(_configPath)) { var json = File.ReadAllText(_configPath); - var loaded = JsonSerializer.Deserialize(json, JsonOptions); + var loaded = JsonConvert.DeserializeObject(json, JsonSettings); if (loaded != null) { @@ -97,7 +96,7 @@ public void SaveConfig(ScanConfig config) Directory.CreateDirectory(configDir); } - var json = JsonSerializer.Serialize(config, JsonOptions); + var json = JsonConvert.SerializeObject(config, JsonSettings); File.WriteAllText(_configPath, json); _config = config; diff --git a/MLVScan.csproj b/MLVScan.csproj index fb6aefc..85659ac 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -133,7 +133,7 @@ - + From 83ab1773b0565c3491984c9f875448bad3f898db Mon Sep 17 00:00:00 2001 From: ifBars Date: Sat, 24 Jan 2026 13:00:17 -0800 Subject: [PATCH 03/28] Update README.md --- README.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dc64b97..063fee5 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,46 @@ MLVScan Icon

-**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** and **BepInEx 5.x**. ![MLVScan Example](example.png) ## ⚡ 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:** +- Runs as a preloader patcher (scans before chainloader) +- Configuration stored in `BepInEx/config/MLVScan.json` +- Reports saved to `BepInEx/MLVScan/Reports/` + ## 🛡️ Powered by MLVScan.Core MLVScan is built on **[MLVScan.Core](https://github.com/ifBars/MLVScan.Core)**, a cross-platform malware detection engine. From 065534d92ef25b9c3f8ad753fcab6dacd500bd6a Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 18:39:49 -0800 Subject: [PATCH 04/28] feat: BepInEx 6.* Support --- .../BepInEx5Patcher.cs} | 7 +- BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs | 109 ++++++++++++++++++ BepInEx/6/Mono/BepInEx6MonoPatcher.cs | 109 ++++++++++++++++++ BepInEx/BepInExConfigManager.cs | 15 +-- BepInEx/BepInExReportGenerator.cs | 62 ++++++++++ MLVScan.csproj | 109 +++++++++++++++--- MLVScan.sln | 20 ++-- Core.cs => MelonLoader/MelonLoaderPlugin.cs | 91 +++++++++++++-- .../MelonLoaderServiceFactory.cs | 7 +- PlatformConstants.cs | 10 ++ Services/DeveloperReportGenerator.cs | 97 +++++++++++++++- 11 files changed, 586 insertions(+), 50 deletions(-) rename BepInEx/{MLVScanPatcher.cs => 5/BepInEx5Patcher.cs} (96%) create mode 100644 BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs create mode 100644 BepInEx/6/Mono/BepInEx6MonoPatcher.cs rename Core.cs => MelonLoader/MelonLoaderPlugin.cs (79%) rename Services/ServiceFactory.cs => MelonLoader/MelonLoaderServiceFactory.cs (95%) diff --git a/BepInEx/MLVScanPatcher.cs b/BepInEx/5/BepInEx5Patcher.cs similarity index 96% rename from BepInEx/MLVScanPatcher.cs rename to BepInEx/5/BepInEx5Patcher.cs index e5933d7..2d83ff0 100644 --- a/BepInEx/MLVScanPatcher.cs +++ b/BepInEx/5/BepInEx5Patcher.cs @@ -3,15 +3,16 @@ using BepInEx; using BepInEx.Logging; using Mono.Cecil; +using MLVScan.BepInEx; using MLVScan.BepInEx.Adapters; -namespace MLVScan.BepInEx +namespace MLVScan.BepInEx5 { /// - /// BepInEx preloader patcher that scans plugins for malicious patterns + /// BepInEx 5.x preloader patcher that scans plugins for malicious patterns /// before the chainloader initializes them. /// - public static class MLVScanPatcher + public static class BepInEx5Patcher { private static ManualLogSource _logger; diff --git a/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs new file mode 100644 index 0000000..b789fdf --- /dev/null +++ b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs @@ -0,0 +1,109 @@ +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 + { + _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..6378a5d --- /dev/null +++ b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs @@ -0,0 +1,109 @@ +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); + + // 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 + { + _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/BepInExConfigManager.cs b/BepInEx/BepInExConfigManager.cs index bc65bb3..2040e58 100644 --- a/BepInEx/BepInExConfigManager.cs +++ b/BepInEx/BepInExConfigManager.cs @@ -5,8 +5,8 @@ using BepInEx.Logging; using MLVScan.Abstractions; using MLVScan.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json; +using System.Text.Json.Serialization; namespace MLVScan.BepInEx { @@ -22,10 +22,11 @@ public class BepInExConfigManager : IConfigManager private ScanConfig _config; // JSON serialization settings - private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { - Formatting = Formatting.Indented, - Converters = { new StringEnumConverter() } + WriteIndented = true, + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } }; public BepInExConfigManager(ManualLogSource logger, string[] defaultWhitelistedHashes = null) @@ -47,7 +48,7 @@ public ScanConfig LoadConfig() if (File.Exists(_configPath)) { var json = File.ReadAllText(_configPath); - var loaded = JsonConvert.DeserializeObject(json, JsonSettings); + var loaded = JsonSerializer.Deserialize(json, JsonOptions); if (loaded != null) { @@ -96,7 +97,7 @@ public void SaveConfig(ScanConfig config) Directory.CreateDirectory(configDir); } - var json = JsonConvert.SerializeObject(config, JsonSettings); + var json = JsonSerializer.Serialize(config, JsonOptions); File.WriteAllText(_configPath, json); _config = config; diff --git a/BepInEx/BepInExReportGenerator.cs b/BepInEx/BepInExReportGenerator.cs index da77b07..3a52399 100644 --- a/BepInEx/BepInExReportGenerator.cs +++ b/BepInEx/BepInExReportGenerator.cs @@ -6,6 +6,7 @@ using BepInEx; using BepInEx.Logging; using MLVScan.Models; +using MLVScan.Models.Rules; using MLVScan.Services; namespace MLVScan.BepInEx @@ -74,6 +75,35 @@ private void LogConsoleReport(string pluginName, string hash, List { _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) @@ -159,6 +189,38 @@ private void GenerateFileReport( 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)) diff --git a/MLVScan.csproj b/MLVScan.csproj index 85659ac..dd42e2a 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -14,7 +14,7 @@ en-US True false - MelonLoader;BepInEx + MelonLoader;BepInEx;BepInEx6Mono;BepInEx6IL2CPP @@ -23,6 +23,7 @@ MELONLOADER MLVScan.MelonLoader + $(NoWarn);MSB3277 @@ -33,6 +34,23 @@ MLVScan.BepInEx + + + + + BEPINEX;BEPINEX6;BEPINEX6_MONO + MLVScan.BepInEx6.Mono + + + + + + + net6.0 + BEPINEX;BEPINEX6;BEPINEX6_IL2CPP + MLVScan.BepInEx6.IL2CPP + + @@ -62,21 +80,49 @@ - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -110,7 +156,7 @@ - @@ -121,19 +167,38 @@ + + + + + + + + + + + + + + + + + + + - - + + - + - + @@ -142,20 +207,30 @@ - - + + + + + + + + + - - - - + + "$(PkgILRepack)\tools\ILRepack.exe" /parallel /union + /lib:"@(ILRepackLib, '" /lib:"')" + $(ILRepackArgs) $(ILRepackLibArgs) + $(ILRepackArgs) /out:"$(OutputPath)$(AssemblyName).merged.dll" + $(ILRepackArgs) "@(InputAssemblies, '" "')" + + + diff --git a/MLVScan.sln b/MLVScan.sln index efa2737..03c91d8 100644 --- a/MLVScan.sln +++ b/MLVScan.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.13.35931.197 d17.13 @@ -7,14 +7,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan", "MLVScan.csproj", EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + MelonLoader|Any CPU = MelonLoader|Any CPU + BepInEx|Any CPU = BepInEx|Any CPU + BepInEx6Mono|Any CPU = BepInEx6Mono|Any CPU + BepInEx6IL2CPP|Any CPU = BepInEx6IL2CPP|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {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}.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}.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}.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}.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}.BepInEx6IL2CPP|Any CPU.ActiveCfg = BepInEx6IL2CPP|Any CPU + {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|Any CPU.Build.0 = BepInEx6IL2CPP|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Core.cs b/MelonLoader/MelonLoaderPlugin.cs similarity index 79% rename from Core.cs rename to MelonLoader/MelonLoaderPlugin.cs index e497758..9db8d3b 100644 --- a/Core.cs +++ b/MelonLoader/MelonLoaderPlugin.cs @@ -3,19 +3,24 @@ using System.IO; using System.Linq; using MelonLoader; -using MLVScan.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 MelonLoaderServiceFactory _serviceFactory; private MelonConfigManager _configManager; private MelonPlatformEnvironment _environment; private MelonPluginScanner _pluginScanner; @@ -40,7 +45,7 @@ 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(); @@ -201,7 +206,7 @@ private void GenerateDetailedReports(List disabledMods, Dict { _developerReportGenerator.GenerateConsoleReport(modName, actualFindings); } - else + else { // Standard reporting LoggerInstance.Warning("Suspicious patterns found:"); @@ -217,9 +222,39 @@ private void GenerateDetailedReports(List disabledMods, Dict { 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}"); @@ -242,7 +277,7 @@ private void GenerateDetailedReports(List disabledMods, Dict 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); @@ -305,10 +340,45 @@ private void GenerateDetailedReports(List disabledMods, Dict 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):"); @@ -409,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/Services/ServiceFactory.cs b/MelonLoader/MelonLoaderServiceFactory.cs similarity index 95% rename from Services/ServiceFactory.cs rename to MelonLoader/MelonLoaderServiceFactory.cs index 7e03b1c..99ed812 100644 --- a/Services/ServiceFactory.cs +++ b/MelonLoader/MelonLoaderServiceFactory.cs @@ -2,16 +2,15 @@ using MelonLoader; using MLVScan.Abstractions; using MLVScan.Adapters; -using MLVScan.MelonLoader; using MLVScan.Models; using MLVScan.Services; -namespace MLVScan +namespace MLVScan.MelonLoader { /// /// Factory for creating MLVScan services in the MelonLoader context. /// - public class ServiceFactory + public class MelonLoaderServiceFactory { private readonly MelonLogger.Instance _melonLogger; private readonly IScanLogger _scanLogger; @@ -20,7 +19,7 @@ public class ServiceFactory private readonly MelonPlatformEnvironment _environment; private readonly ScanConfig _fallbackConfig; - public ServiceFactory(MelonLogger.Instance logger) + public MelonLoaderServiceFactory(MelonLogger.Instance logger) { _melonLogger = logger ?? throw new ArgumentNullException(nameof(logger)); _scanLogger = new MelonScanLogger(logger); diff --git a/PlatformConstants.cs b/PlatformConstants.cs index a974ade..468ff41 100644 --- a/PlatformConstants.cs +++ b/PlatformConstants.cs @@ -17,6 +17,16 @@ public static class PlatformConstants /// 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. diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs index 1bba7be..d766b75 100644 --- a/Services/DeveloperReportGenerator.cs +++ b/Services/DeveloperReportGenerator.cs @@ -4,6 +4,7 @@ using System.Text; using MLVScan.Abstractions; using MLVScan.Models; +using MLVScan.Models.Rules; namespace MLVScan.Services { @@ -83,13 +84,41 @@ public void GenerateConsoleReport(string modName, List findings) foreach (var finding in ruleGroup.Take(3)) { _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.Info($" ... and {count - 3} more"); } - _logger.Info(""); _logger.Info("--------------------------------------"); } @@ -170,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(); } } From 0a450867acc8c89eec81d3dfa1e3014cc5b35eee Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 18:50:26 -0800 Subject: [PATCH 05/28] Add MLVScan.Core project and expand build configs Added the MLVScan.Core project to the solution. --- MLVScan.sln | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/MLVScan.sln b/MLVScan.sln index 03c91d8..3adc09f 100644 --- a/MLVScan.sln +++ b/MLVScan.sln @@ -1,26 +1,106 @@ - + 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan.Core", "..\MLVScan.Core\MLVScan.Core.csproj", "{CBE47B78-2460-4C71-B9E6-CBEC26AAED73}" +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 + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From e207206b61107382e86b1b8df2ff4cf5db4d943b Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:22:34 -0800 Subject: [PATCH 06/28] Update BepInEx/5/BepInEx5Patcher.cs fix: Inaccurate logging Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- BepInEx/5/BepInEx5Patcher.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BepInEx/5/BepInEx5Patcher.cs b/BepInEx/5/BepInEx5Patcher.cs index 2d83ff0..9057151 100644 --- a/BepInEx/5/BepInEx5Patcher.cs +++ b/BepInEx/5/BepInEx5Patcher.cs @@ -96,10 +96,15 @@ public static void Initialize() _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."); } From e0f263cee558062966f4de77b32606055277577f Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:23:43 -0800 Subject: [PATCH 07/28] Update Services/DeveloperReportGenerator.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Services/DeveloperReportGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs index d766b75..7f63778 100644 --- a/Services/DeveloperReportGenerator.cs +++ b/Services/DeveloperReportGenerator.cs @@ -215,7 +215,7 @@ public string GenerateFileReport(string modName, string hash, List CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", - _ => "[???" + _ => "[???]" }; sb.AppendLine($" {prefix} {node.Location}"); if (!string.IsNullOrEmpty(node.Description)) From 168ca7dafbf96b45677fb3eba53566c08f62cb64 Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:24:04 -0800 Subject: [PATCH 08/28] Update Services/DeveloperReportGenerator.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Services/DeveloperReportGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs index 7f63778..101e16e 100644 --- a/Services/DeveloperReportGenerator.cs +++ b/Services/DeveloperReportGenerator.cs @@ -95,7 +95,7 @@ public void GenerateConsoleReport(string modName, List findings) CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", - _ => "[???" + _ => "[???]" }; _logger.Info($" {prefix} {node.Location}"); } From d6dcf96f6d3f43d796e779d7e5c90afd117ddd45 Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:24:31 -0800 Subject: [PATCH 09/28] Update BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs index b789fdf..77d38ca 100644 --- a/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs +++ b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs @@ -85,6 +85,10 @@ public override void Initialize() _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."); From a35f82200e875bf7c919dbc1f10b3f5252d1412e Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:27:37 -0800 Subject: [PATCH 10/28] feat: Build Workflow --- .github/workflows/build.yml | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..799355c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,62 @@ +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: Restore dependencies + run: dotnet restore MLVScan.sln + + # 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: 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 Mono + - 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 IL2CPP + - 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 From 2a24166ffb470b9292acbab97f5880330f12f044 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:27:46 -0800 Subject: [PATCH 11/28] fix: Inaccurate logging --- BepInEx/6/Mono/BepInEx6MonoPatcher.cs | 34 +++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/BepInEx/6/Mono/BepInEx6MonoPatcher.cs b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs index 6378a5d..cf24036 100644 --- a/BepInEx/6/Mono/BepInEx6MonoPatcher.cs +++ b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs @@ -68,26 +68,30 @@ public override void Initialize() var pluginDisabler = new BepInExPluginDisabler(scanLogger, config); var reportGenerator = new BepInExReportGenerator(_logger, config); - // Scan all plugins - var scanResults = pluginScanner.ScanAllPlugins(); - - if (scanResults.Count > 0) + if (!config.EnableAutoScan) { - // Disable suspicious plugins - var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults); + _logger.LogInfo("Auto-scan is disabled. Skipping plugin scan."); + } + else + { + var scanResults = pluginScanner.ScanAllPlugins(); - // Generate reports for disabled plugins - if (disabledPlugins.Count > 0) + if (scanResults.Count > 0) { - reportGenerator.GenerateReports(disabledPlugins, scanResults); + var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults); + + 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."); + _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s)."); + _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details."); + } + } + else + { + _logger.LogInfo("No suspicious plugins detected."); } - } - else - { - _logger.LogInfo("No suspicious plugins detected."); } _logger.LogInfo("MLVScan BepInEx 6 (Mono) preloader scan complete."); From 51c3601f193c5588a1b79d5262cc5f68dd2a990f Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:27:27 -0800 Subject: [PATCH 12/28] Update README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 063fee5..0ca14a9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **MLVScan** is a security-focused mod loader plugin that protects your game by scanning mods for malicious patterns *before* they execute. -Supports **MelonLoader** and **BepInEx 5.x**. +Supports **MelonLoader**, **BepInEx 5.x**, and **BepInEx 6.x** (IL2CPP/Mono). ![MLVScan Example](example.png) From ad0ca67e9d43dab74b2662d9546c2ca6a2b14137 Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:28:30 -0800 Subject: [PATCH 13/28] Update Services/PluginScannerBase.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Services/PluginScannerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/PluginScannerBase.cs b/Services/PluginScannerBase.cs index f778f4f..c9a7233 100644 --- a/Services/PluginScannerBase.cs +++ b/Services/PluginScannerBase.cs @@ -131,7 +131,7 @@ protected virtual void ScanSingleFile(string filePath, Dictionary= Config.SuspiciousThreshold) { - results.Add(filePath, actualFindings); + results[filePath] = actualFindings; Logger.Warning($"Found {actualFindings.Count} suspicious patterns in {fileName}"); } } From 2fef5233b130dbfa7a551c547d324a860421a9aa Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:31:00 -0800 Subject: [PATCH 14/28] Update MLVScan.sln --- MLVScan.sln | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/MLVScan.sln b/MLVScan.sln index 3adc09f..f13bf33 100644 --- a/MLVScan.sln +++ b/MLVScan.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.13.35931.197 @@ -65,30 +65,30 @@ Global {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 - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Debug|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Release|Any CPU + {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Release|Any CPU {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.Build.0 = Debug|Any CPU {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.ActiveCfg = Debug|Any CPU From 29b669b9dddea87ffa7b316c0d04fc56210c2245 Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:30:16 -0800 Subject: [PATCH 15/28] Update BepInEx/5/BepInEx5Patcher.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- BepInEx/5/BepInEx5Patcher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/BepInEx/5/BepInEx5Patcher.cs b/BepInEx/5/BepInEx5Patcher.cs index 9057151..16144b3 100644 --- a/BepInEx/5/BepInEx5Patcher.cs +++ b/BepInEx/5/BepInEx5Patcher.cs @@ -104,7 +104,6 @@ public static void Initialize() { _logger.LogInfo("No suspicious plugins detected."); } - } _logger.LogInfo("MLVScan preloader scan complete."); } From e5e0fc81cf9541f629aba4dd51bfe8ebb9a0b8d3 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:38:32 -0800 Subject: [PATCH 16/28] Update build config, fix typo, and clarify README Refactored MLVScan.csproj to improve ILRepack library handling and removed redundant build targets. Fixed a minor typo in MelonLoaderPlugin.cs. Expanded and clarified README instructions for BepInEx 5.x and 6.x usage and installation. --- MLVScan.csproj | 32 +++----------------------------- MelonLoader/MelonLoaderPlugin.cs | 4 ++-- README.md | 10 +++++++++- 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/MLVScan.csproj b/MLVScan.csproj index fd4e2b3..7b76117 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -212,8 +212,10 @@ + + - + @@ -250,32 +252,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MelonLoader/MelonLoaderPlugin.cs b/MelonLoader/MelonLoaderPlugin.cs index 9db8d3b..cf64e3d 100644 --- a/MelonLoader/MelonLoaderPlugin.cs +++ b/MelonLoader/MelonLoaderPlugin.cs @@ -233,7 +233,7 @@ private void GenerateDetailedReports(List disabledMods, Dict CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", - _ => "[???" + _ => "[???]" }; LoggerInstance.Msg($" {prefix} {node.Location}"); } @@ -357,7 +357,7 @@ private void GenerateDetailedReports(List disabledMods, Dict CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", - _ => "[???" + _ => "[???]" }; writer.WriteLine($" {prefix} {node.Location}"); if (!string.IsNullOrEmpty(node.Description)) diff --git a/README.md b/README.md index 0ca14a9..f69b2f0 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,18 @@ Detailed documentation is available in the **[MLVScan Wiki](https://github.com/i - Configuration stored in `MelonPreferences.cfg` - Reports saved to `UserData/MLVScan/Reports/` -**BepInEx:** +**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 From 033d9de632da33bbe10d6e68c4c8069d9ff77797 Mon Sep 17 00:00:00 2001 From: ifBars <114284668+ifBars@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:37:21 -0800 Subject: [PATCH 17/28] Update MelonLoader/MelonEnvironment.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- MelonLoader/MelonEnvironment.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/MelonLoader/MelonEnvironment.cs b/MelonLoader/MelonEnvironment.cs index 34e4bdc..a9a64fa 100644 --- a/MelonLoader/MelonEnvironment.cs +++ b/MelonLoader/MelonEnvironment.cs @@ -54,10 +54,18 @@ public string ManagedDirectory { get { - // Unity managed assemblies location - var managedPath = Path.Combine(_gameRoot, "Schedule I_Data", "Managed"); - if (Directory.Exists(managedPath)) - return managedPath; + // 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"); From 3f94267bf31bd3fc6d4bd462c43414121831c1f5 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:40:29 -0800 Subject: [PATCH 18/28] Update MelonLoaderServiceFactory.cs --- MelonLoader/MelonLoaderServiceFactory.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MelonLoader/MelonLoaderServiceFactory.cs b/MelonLoader/MelonLoaderServiceFactory.cs index 99ed812..9da899c 100644 --- a/MelonLoader/MelonLoaderServiceFactory.cs +++ b/MelonLoader/MelonLoaderServiceFactory.cs @@ -38,8 +38,17 @@ public MelonLoaderServiceFactory(MelonLogger.Instance logger) } } + /// + /// 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; } From 147b6e25b08f8654fc2192e70d6bf25c0632e352 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:43:49 -0800 Subject: [PATCH 19/28] Update MelonPluginScanner.cs --- MelonLoader/MelonPluginScanner.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MelonLoader/MelonPluginScanner.cs b/MelonLoader/MelonPluginScanner.cs index cad1ce2..888322b 100644 --- a/MelonLoader/MelonPluginScanner.cs +++ b/MelonLoader/MelonPluginScanner.cs @@ -29,10 +29,9 @@ public MelonPluginScanner( protected override IEnumerable GetScanDirectories() { - // Configured directories relative to game root foreach (var scanDir in Config.ScanDirectories) { - yield return Path.Combine(MelonEnvironment.GameRootDirectory, scanDir); + yield return Path.Combine(_environment.GameRootDirectory, scanDir); } } From 3d24b8e1c94e10c13f2f48377c1fc8142b4e1013 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:43:51 -0800 Subject: [PATCH 20/28] Create local.build.props.example --- local.build.props.example | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 local.build.props.example 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bdb590b3378dcc1e883e5d7d55c8c0ca987e9776 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:47:08 -0800 Subject: [PATCH 21/28] Update DeveloperReportGenerator.cs --- Services/DeveloperReportGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs index 101e16e..6dc7160 100644 --- a/Services/DeveloperReportGenerator.cs +++ b/Services/DeveloperReportGenerator.cs @@ -256,7 +256,7 @@ public string GenerateFileReport(string modName, string hash, List 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}"); From bcfca00df5cc1b81ba0f5ddef692ad3e29befa62 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:47:09 -0800 Subject: [PATCH 22/28] Update BepInExEnvironment.cs --- BepInEx/BepInExEnvironment.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/BepInEx/BepInExEnvironment.cs b/BepInEx/BepInExEnvironment.cs index c91b6f4..54c703f 100644 --- a/BepInEx/BepInExEnvironment.cs +++ b/BepInEx/BepInExEnvironment.cs @@ -13,19 +13,18 @@ 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 => new[] - { - Paths.PluginPath - }; + public string[] PluginDirectories => _pluginDirectories; public string DataDirectory { From 617b47defd74d1d8be7d13c6f3424bc895414218 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:51:53 -0800 Subject: [PATCH 23/28] fix: Use MLVScan.Core from NuGet --- MLVScan.csproj | 10 ++-------- MLVScan.sln | 40 ++-------------------------------------- 2 files changed, 4 insertions(+), 46 deletions(-) diff --git a/MLVScan.csproj b/MLVScan.csproj index 7b76117..bc966cd 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -190,11 +190,8 @@ - - - - + @@ -207,10 +204,7 @@ - - - - + diff --git a/MLVScan.sln b/MLVScan.sln index f13bf33..928fada 100644 --- a/MLVScan.sln +++ b/MLVScan.sln @@ -5,8 +5,7 @@ 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 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan.Core", "..\MLVScan.Core\MLVScan.Core.csproj", "{CBE47B78-2460-4C71-B9E6-CBEC26AAED73}" -EndProject + Global GlobalSection(SolutionConfigurationPlatforms) = preSolution MelonLoader|Any CPU = MelonLoader|Any CPU @@ -65,42 +64,7 @@ Global {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 - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.ActiveCfg = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.Build.0 = Debug|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.Build.0 = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.ActiveCfg = Release|Any CPU - {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 5ecedddb05764814e223a2a37e16df0d56a61a1e Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 19:53:59 -0800 Subject: [PATCH 24/28] Update MLVScan.csproj --- MLVScan.csproj | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/MLVScan.csproj b/MLVScan.csproj index bc966cd..c45fe96 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -136,16 +136,21 @@ - + + + + + + + $(MelonLoaderPath)\MelonLoader.dll false - + $(MelonLoaderPath)\0Harmony.dll false - - + $(GameManagedPath)\UnityEngine.CoreModule.dll false From 7987c23fa0a7f59657125a3a3af2ea9a2a5e9c1c Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 20:07:12 -0800 Subject: [PATCH 25/28] fix: CI --- .github/workflows/build.yml | 4 ++-- MLVScan.csproj | 47 ++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 799355c..d751d44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,7 +41,7 @@ jobs: name: MLVScan.BepInEx path: bin/BepInEx/**/MLVScan.BepInEx.dll - # BepInEx 6 Mono + # BepInEx 6.x Mono - name: Build BepInEx6Mono run: dotnet build MLVScan.csproj -c BepInEx6Mono --no-restore @@ -51,7 +51,7 @@ jobs: name: MLVScan.BepInEx6.Mono path: bin/BepInEx6Mono/**/MLVScan.BepInEx6.Mono.dll - # BepInEx 6 IL2CPP + # BepInEx 6.x IL2CPP - name: Build BepInEx6IL2CPP run: dotnet build MLVScan.csproj -c BepInEx6IL2CPP --no-restore diff --git a/MLVScan.csproj b/MLVScan.csproj index c45fe96..9752f1c 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -17,6 +17,16 @@ MelonLoader;BepInEx;BepInEx6Mono;BepInEx6IL2CPP + + + + + + https://api.nuget.org/v3/index.json; + https://nuget.bepinex.dev/v3/index.json + + + @@ -137,8 +147,8 @@ - - + + @@ -157,11 +167,13 @@ - + - - + + @@ -176,19 +188,21 @@ - - - - + + + + + - - - + + + + @@ -211,14 +225,15 @@ + - - - - + + + + "$(PkgILRepack)\tools\ILRepack.exe" /parallel /union From 98ab2b202ad6db8b8a4c9b6d17c5f8215e480385 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 20:09:34 -0800 Subject: [PATCH 26/28] Update build.yml --- .github/workflows/build.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d751d44..31d6cd0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,10 +18,10 @@ jobs: with: dotnet-version: '8.0.x' - - name: Restore dependencies - run: dotnet restore MLVScan.sln - # 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 @@ -32,6 +32,9 @@ jobs: 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 @@ -42,6 +45,9 @@ jobs: 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 @@ -52,6 +58,9 @@ jobs: 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 From 2ab298d877247a331a35bf93668e6ffa997e89b4 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 20:10:41 -0800 Subject: [PATCH 27/28] Update MLVScan.csproj --- MLVScan.csproj | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/MLVScan.csproj b/MLVScan.csproj index 9752f1c..e9a7b56 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -212,7 +212,7 @@ - + @@ -236,14 +236,15 @@ - "$(PkgILRepack)\tools\ILRepack.exe" /parallel /union - /lib:"@(ILRepackLib, '" /lib:"')" - $(ILRepackArgs) $(ILRepackLibArgs) - $(ILRepackArgs) /out:"$(OutputPath)$(AssemblyName).merged.dll" - $(ILRepackArgs) "@(InputAssemblies, '" "')" + $(OutputPath)$(AssemblyName).merged.dll - - + + From 1a0eaf50851d686759c0c17ad3b86ba912bfdfa3 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 29 Jan 2026 20:13:54 -0800 Subject: [PATCH 28/28] fix: CI Repack --- .github/workflows/build.yml | 5 +++++ MLVScan.csproj | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31d6cd0..e1601fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,11 @@ jobs: 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 diff --git a/MLVScan.csproj b/MLVScan.csproj index e9a7b56..f26b150 100644 --- a/MLVScan.csproj +++ b/MLVScan.csproj @@ -212,7 +212,7 @@ - + @@ -220,7 +220,7 @@ - + @@ -237,13 +237,13 @@ $(OutputPath)$(AssemblyName).merged.dll + $(PkgILRepack)\tools\ILRepack.exe + + "$(ILRepackExePath)" + mono "$(ILRepackExePath)" + /lib:"@(ILRepackLib, '" /lib:"')" - + @@ -251,7 +251,7 @@ - + @@ -259,7 +259,7 @@ - +