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