Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of MLVScan v2.0.2
Plugins/MLVScan.MelonLoader.dll
Decompiled 3 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using MLVScan.Abstractions; using MLVScan.Adapters; using MLVScan.MelonLoader; using MLVScan.Models; using MLVScan.Models.CrossAssembly; using MLVScan.Models.DataFlow; using MLVScan.Models.Dto; using MLVScan.Models.Rules; using MLVScan.Models.Rules.Helpers; using MLVScan.Models.ThreatIntel; using MLVScan.Services; using MLVScan.Services.Caching; using MLVScan.Services.Configuration; using MLVScan.Services.DataFlow; using MLVScan.Services.Diagnostics; using MLVScan.Services.Helpers; using MLVScan.Services.Resolution; using MLVScan.Services.Scope; using MLVScan.Services.ThreatIntel; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using Microsoft.Win32.SafeHandles; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(MelonLoaderPlugin), "MLVScan", "2.0.2", "Bars", null)] [assembly: MelonPriority(int.MinValue)] [assembly: MelonColor(255, 139, 0, 0)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("MLVScan.MelonLoader")] [assembly: AssemblyConfiguration("MelonLoader")] [assembly: AssemblyFileVersion("2.0.2")] [assembly: AssemblyInformationalVersion("2.0.2+8f6dd31d0a1cb0fc97580de7b8295c61eb93fa4e")] [assembly: AssemblyProduct("MLVScan.MelonLoader")] [assembly: AssemblyTitle("MLVScan.MelonLoader")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("2.0.2.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace MLVScan { internal static class MLVScanBuildInfo { public const string PlatformVersion = "2.0.2"; } public static class PlatformConstants { public const string PlatformVersion = "2.0.2"; public const string PlatformName = "MLVScan.MelonLoader"; public static string GetVersionString() { return "MLVScan.MelonLoader v2.0.2"; } public static string GetFullVersionInfo() { return "Engine: MLVScan.Core v" + MLVScanVersions.CoreVersion + " Platform: " + GetVersionString(); } } } namespace MLVScan.Adapters { public class MelonScanLogger : IScanLogger { private readonly Instance _logger; public MelonScanLogger(Instance logger) { _logger = logger ?? throw new ArgumentNullException("logger"); } public void Debug(string message) { _logger.Msg("[DEBUG] " + message); } public void Info(string message) { _logger.Msg(message); } public void Warning(string message) { _logger.Warning(message); } public void Error(string message) { _logger.Error(message); } public void Error(string message, Exception exception) { _logger.Error($"{message}: {exception}"); } } public class GameAssemblyResolverProvider : CatalogingAssemblyResolverProviderBase { public GameAssemblyResolverProvider(LoaderScanTelemetryHub telemetry) : base(telemetry) { } protected override IEnumerable<ResolverRoot> GetStableRoots() { List<ResolverRoot> list = new List<ResolverRoot>(); try { string path = Path.Combine(MelonEnvironment.GameRootDirectory, Path.GetFileNameWithoutExtension(MelonEnvironment.GameExecutablePath) + "_Data", "Managed"); if (Directory.Exists(path)) { list.Add(new ResolverRoot(path, 0)); } string path2 = Path.Combine(MelonEnvironment.GameRootDirectory, "MelonLoader", "net35"); if (Directory.Exists(path2)) { list.Add(new ResolverRoot(path2, 5)); } string path3 = Path.Combine(MelonEnvironment.GameRootDirectory, "MelonLoader", "net6"); if (Directory.Exists(path3)) { list.Add(new ResolverRoot(path3, 6)); } } catch (Exception) { return list; } return list; } } } namespace MLVScan.MelonLoader { public class MelonLoaderPlugin : MelonPlugin { private MelonLoaderServiceFactory _serviceFactory; private MelonConfigManager _configManager; private MelonPlatformEnvironment _environment; private MelonPluginScanner _pluginScanner; private MelonPluginDisabler _pluginDisabler; private IlDumpService _ilDumpService; private DeveloperReportGenerator _developerReportGenerator; private ReportUploadService _reportUploadService; private bool _initialized = false; private bool _showUploadConsentPopup; private string _pendingUploadPath = string.Empty; private string _pendingUploadModName = string.Empty; private string _pendingUploadVerdictKind = string.Empty; private bool _pendingUploadWasBlocked = true; private List<ScanFinding> _pendingUploadFindings; public override void OnEarlyInitializeMelon() { try { ((MelonBase)this).LoggerInstance.Msg("Pre-scanning for malicious mods..."); _serviceFactory = new MelonLoaderServiceFactory(((MelonBase)this).LoggerInstance); _configManager = _serviceFactory.CreateConfigManager(); _environment = _serviceFactory.CreateEnvironment(); _pluginScanner = _serviceFactory.CreatePluginScanner(); _pluginDisabler = _serviceFactory.CreatePluginDisabler(); _ilDumpService = _serviceFactory.CreateIlDumpService(); _developerReportGenerator = _serviceFactory.CreateDeveloperReportGenerator(); _reportUploadService = _serviceFactory.CreateReportUploadService(); _initialized = true; ScanAndDisableMods(force: true); } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Error("Error in pre-mod scan: " + ex.Message); ((MelonBase)this).LoggerInstance.Error(ex.StackTrace); } } public override void OnInitializeMelon() { try { ((MelonBase)this).LoggerInstance.Msg("MLVScan initialization complete"); if (_configManager.Config.WhitelistedHashes.Length != 0) { ((MelonBase)this).LoggerInstance.Msg($"{_configManager.Config.WhitelistedHashes.Length} mod(s) are whitelisted and won't be scanned"); ((MelonBase)this).LoggerInstance.Msg("To manage whitelisted mods, edit MelonPreferences.cfg"); } } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Error("Error initializing MLVScan: " + ex.Message); ((MelonBase)this).LoggerInstance.Error(ex.StackTrace); } } public override void OnGUI() { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) if (_showUploadConsentPopup) { float num = Math.Min(620f, (float)Screen.width - 40f); float num2 = 280f; float num3 = ((float)Screen.width - num) / 2f; float num4 = ((float)Screen.height - num2) / 2f; GUI.Box(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), string.Empty); GUI.Box(new Rect(num3, num4, num, num2), "MLVScan Upload Consent"); GUI.Label(new Rect(num3 + 20f, num4 + 40f, num - 40f, 140f), ConsentMessageHelper.GetUploadConsentMessage(_pendingUploadModName, _pendingUploadVerdictKind, _pendingUploadWasBlocked) + "\n\nWould you like to upload this file to the MLVScan API for human review?\n\nYes: upload this mod now and enable automatic uploads for future detections.\nNo: do not upload and do not show this prompt again."); if (GUI.Button(new Rect(num3 + 20f, num4 + num2 - 60f, (num - 60f) / 2f, 36f), "Yes, upload")) { HandleUploadConsentDecision(approved: true); } if (GUI.Button(new Rect(num3 + 40f + (num - 60f) / 2f, num4 + num2 - 60f, (num - 60f) / 2f, 36f), "No thanks")) { HandleUploadConsentDecision(approved: false); } } } public Dictionary<string, ScannedPluginResult> ScanAndDisableMods(bool force = false) { try { if (!_initialized) { ((MelonBase)this).LoggerInstance.Error("Cannot scan mods - MLVScan not properly initialized"); return new Dictionary<string, ScannedPluginResult>(); } ((MelonBase)this).LoggerInstance.Msg("Scanning mods for threats..."); Dictionary<string, ScannedPluginResult> source = _pluginScanner.ScanAllPlugins(force); Dictionary<string, ScannedPluginResult> dictionary = source.Where((KeyValuePair<string, ScannedPluginResult> kv) => kv.Value != null && ScanResultFacts.RequiresAttention(kv.Value)).ToDictionary((KeyValuePair<string, ScannedPluginResult> kv) => kv.Key, (KeyValuePair<string, ScannedPluginResult> kv) => kv.Value); if (dictionary.Count > 0) { List<DisabledPluginInfo> list = _pluginDisabler.DisableSuspiciousPlugins(dictionary, force); int count = list.Count; int num = dictionary.Count - count; if (count > 0) { ((MelonBase)this).LoggerInstance.Msg($"Disabled {count} mod(s) that matched the active blocking policy"); } if (num > 0) { ((MelonBase)this).LoggerInstance.Warning($"{num} mod(s) require manual review but were not blocked by the current configuration"); } GenerateDetailedReports(list, dictionary); ((MelonBase)this).LoggerInstance.Msg("To whitelist any false positives, add their SHA256 hash to the MLVScan → WhitelistedHashes setting in MelonPreferences.cfg"); } else { ((MelonBase)this).LoggerInstance.Msg("No mods requiring action were found"); } return dictionary; } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Error("Error scanning mods: " + ex.Message); return new Dictionary<string, ScannedPluginResult>(); } } private void GenerateDetailedReports(List<DisabledPluginInfo> disabledMods, Dictionary<string, ScannedPluginResult> scanResults) { bool valueOrDefault = (_configManager?.Config?.Scan?.DeveloperMode).GetValueOrDefault(); Dictionary<string, DisabledPluginInfo> dictionary = (disabledMods ?? new List<DisabledPluginInfo>()).ToDictionary<DisabledPluginInfo, string>((DisabledPluginInfo info) => info.OriginalPath, StringComparer.OrdinalIgnoreCase); if (valueOrDefault) { ((MelonBase)this).LoggerInstance.Msg("Developer Mode: Enabled"); } ((MelonBase)this).LoggerInstance.Warning("======= DETAILED SCAN REPORT ======="); ((MelonBase)this).LoggerInstance.Msg(PlatformConstants.GetFullVersionInfo()); foreach (KeyValuePair<string, ScannedPluginResult> item in scanResults.OrderBy<KeyValuePair<string, ScannedPluginResult>, string>((KeyValuePair<string, ScannedPluginResult> kv) => Path.GetFileName(kv.Key), StringComparer.OrdinalIgnoreCase)) { item.Deconstruct(out var key, out var value); string text = key; ScannedPluginResult scannedPluginResult = value; dictionary.TryGetValue(text, out var value2); bool flag = value2 != null; string fileName = Path.GetFileName(text); string text2 = value2?.FileHash ?? scannedPluginResult?.FileHash ?? string.Empty; string text3 = value2?.OriginalPath ?? scannedPluginResult?.FilePath ?? string.Empty; string text4 = ((flag && File.Exists(value2.DisabledPath)) ? value2.DisabledPath : (scannedPluginResult?.FilePath ?? text)); List<ScanFinding> list = scannedPluginResult?.Findings ?? new List<ScanFinding>(); ThreatVerdictInfo threatVerdictInfo = value2?.ThreatVerdict ?? scannedPluginResult?.ThreatVerdict ?? new ThreatVerdictInfo(); ScanStatusInfo scanStatusInfo = value2?.ScanStatus ?? scannedPluginResult?.ScanStatus ?? new ScanStatusInfo(); string outcomeLabel = ThreatVerdictTextFormatter.GetOutcomeLabel(scannedPluginResult); string outcomeSummary = ThreatVerdictTextFormatter.GetOutcomeSummary(scannedPluginResult); Dictionary<string, List<ScanFinding>> dictionary2 = (from f in list group f by f.Description).ToDictionary((IGrouping<string, ScanFinding> g) => g.Key, (IGrouping<string, ScanFinding> g) => g.ToList()); ((MelonBase)this).LoggerInstance.Warning((flag ? "BLOCKED MOD" : "REVIEW REQUIRED") + ": " + fileName); ((MelonBase)this).LoggerInstance.Msg("SHA256 Hash: " + text2); ((MelonBase)this).LoggerInstance.Msg("-------------------------------"); if (list.Count == 0 && threatVerdictInfo.Kind == ThreatVerdictKind.None && scanStatusInfo.Kind == ScanStatusKind.Complete) { ((MelonBase)this).LoggerInstance.Msg("No specific findings were retained."); continue; } if (threatVerdictInfo.Kind != 0) { QueueConsentPromptIfNeeded(text4, fileName, list, threatVerdictInfo, flag); } ((MelonBase)this).LoggerInstance.Warning($"Total retained findings: {list.Count}"); if (!string.IsNullOrWhiteSpace(outcomeLabel)) { ((MelonBase)this).LoggerInstance.Warning("Outcome: " + outcomeLabel); } if (!string.IsNullOrWhiteSpace(outcomeSummary)) { ((MelonBase)this).LoggerInstance.Msg(outcomeSummary); } if (scanStatusInfo.Kind != 0) { ((MelonBase)this).LoggerInstance.Msg(flag ? "Action: blocked by current incomplete-scan policy." : "Action: manual review required; not blocked by current config."); } string primaryFamilyLabel = ThreatVerdictTextFormatter.GetPrimaryFamilyLabel(threatVerdictInfo); if (!string.IsNullOrWhiteSpace(primaryFamilyLabel)) { ((MelonBase)this).LoggerInstance.Msg("Family: " + primaryFamilyLabel); } string confidenceLabel = ThreatVerdictTextFormatter.GetConfidenceLabel(threatVerdictInfo); if (!string.IsNullOrWhiteSpace(confidenceLabel)) { ((MelonBase)this).LoggerInstance.Msg("Confidence: " + confidenceLabel); } Dictionary<Severity, int> dictionary3 = (from f in list group f by f.Severity into g orderby (int)g.Key descending select g).ToDictionary((IGrouping<Severity, ScanFinding> g) => g.Key, (IGrouping<Severity, ScanFinding> g) => g.Count()); ((MelonBase)this).LoggerInstance.Warning("Severity breakdown:"); foreach (KeyValuePair<Severity, int> item2 in dictionary3) { string arg = FormatSeverityLabel(item2.Key); ((MelonBase)this).LoggerInstance.Msg($" {arg}: {item2.Value} issue(s)"); } ((MelonBase)this).LoggerInstance.Msg("-------------------------------"); List<string> topFindingSummaries = ThreatVerdictTextFormatter.GetTopFindingSummaries(list); if (topFindingSummaries.Count > 0) { ((MelonBase)this).LoggerInstance.Warning("Top signals:"); foreach (string item3 in topFindingSummaries) { ((MelonBase)this).LoggerInstance.Msg(" - " + item3); } } if (valueOrDefault) { ((MelonBase)this).LoggerInstance.Msg("Developer mode is enabled. Full remediation guidance is included in the report file."); } ((MelonBase)this).LoggerInstance.Msg("Full technical details were written to the saved report file for human review."); ((MelonBase)this).LoggerInstance.Msg("-------------------------------"); DisplaySecurityNotice(fileName, threatVerdictInfo, scanStatusInfo, flag); try { string text5 = _environment?.ReportsDirectory ?? Path.Combine(MelonEnvironment.UserDataDirectory, "MLVScan", "Reports"); if (!Directory.Exists(text5)) { Directory.CreateDirectory(text5); } string text6 = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string text7 = Path.Combine(text5, fileName + "_" + text6 + ".report.txt"); string text8 = Path.Combine(text5, "Prompts"); if (!Directory.Exists(text8)) { Directory.CreateDirectory(text8); } MelonConfigManager configManager = _configManager; if (configManager != null && (configManager.Config?.DumpFullIlReports).GetValueOrDefault() && _ilDumpService != null && scanStatusInfo.Kind == ScanStatusKind.Complete) { string path = Path.Combine(text5, "IL"); string text9 = Path.Combine(path, fileName + "_" + text6 + ".il.txt"); if (_ilDumpService.TryDumpAssembly(text4, text9)) { ((MelonBase)this).LoggerInstance.Msg("Full IL dump saved to: " + text9); } else { ((MelonBase)this).LoggerInstance.Warning("Failed to dump IL for this mod (see logs for details)."); } } else { MelonConfigManager configManager2 = _configManager; if (configManager2 != null && (configManager2.Config?.DumpFullIlReports).GetValueOrDefault() && scanStatusInfo.Kind == ScanStatusKind.RequiresReview) { ((MelonBase)this).LoggerInstance.Warning("Skipped full IL dump because this file was not fully analyzed by the loader."); } } PromptGeneratorService promptGeneratorService = ((scanStatusInfo.Kind == ScanStatusKind.Complete) ? _serviceFactory.CreatePromptGenerator() : null); using (StreamWriter streamWriter = new StreamWriter(text7)) { if (valueOrDefault && _developerReportGenerator != null) { string value3 = _developerReportGenerator.GenerateFileReport(fileName, text2, list, threatVerdictInfo, scanStatusInfo); streamWriter.Write(value3); } else { streamWriter.WriteLine("MLVScan Security Report"); streamWriter.WriteLine(PlatformConstants.GetFullVersionInfo()); streamWriter.WriteLine($"Generated: {DateTime.Now}"); streamWriter.WriteLine("Mod File: " + fileName); streamWriter.WriteLine("Outcome: " + outcomeLabel); if (!string.IsNullOrWhiteSpace(outcomeSummary)) { streamWriter.WriteLine("Outcome Summary: " + outcomeSummary); } streamWriter.WriteLine("Action Taken: " + (flag ? "Blocked" : "Manual review required (not blocked by current config)")); streamWriter.WriteLine("SHA256 Hash: " + text2); streamWriter.WriteLine("Original Path: " + text3); streamWriter.WriteLine((flag ? "Disabled Path" : "Current Path") + ": " + text4); streamWriter.WriteLine("Path Used For Analysis: " + text4); streamWriter.WriteLine($"Total Retained Findings: {list.Count}"); streamWriter.WriteLine(); ThreatVerdictTextFormatter.WriteThreatVerdictSection(streamWriter, threatVerdictInfo); ThreatVerdictTextFormatter.WriteScanStatusSection(streamWriter, scanStatusInfo); streamWriter.WriteLine("\nSeverity Breakdown:"); foreach (KeyValuePair<Severity, int> item4 in dictionary3) { streamWriter.WriteLine($"- {item4.Key}: {item4.Value} issue(s)"); } streamWriter.WriteLine("=============================================="); foreach (KeyValuePair<string, List<ScanFinding>> item5 in dictionary2) { streamWriter.WriteLine("\n== " + item5.Key + " =="); streamWriter.WriteLine($"Severity: {item5.Value[0].Severity}"); streamWriter.WriteLine($"Instances: {item5.Value.Count}"); streamWriter.WriteLine("\nLocations & Analysis:"); foreach (ScanFinding item6 in item5.Value) { streamWriter.WriteLine("- " + item6.Location); if (item6.HasCallChain && item6.CallChain != null) { streamWriter.WriteLine(" Call Chain Analysis:"); streamWriter.WriteLine(" " + item6.CallChain.Summary); streamWriter.WriteLine(" Attack Path:"); foreach (CallChainNode node in item6.CallChain.Nodes) { CallChainNodeType nodeType = node.NodeType; if (1 == 0) { } key = nodeType switch { CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", _ => "[???]", }; if (1 == 0) { } string text10 = key; streamWriter.WriteLine(" " + text10 + " " + node.Location); if (!string.IsNullOrEmpty(node.Description)) { streamWriter.WriteLine(" " + node.Description); } } } if (item6.HasDataFlow && item6.DataFlowChain != null) { streamWriter.WriteLine(" Data Flow Analysis:"); streamWriter.WriteLine($" Pattern: {item6.DataFlowChain.Pattern}"); streamWriter.WriteLine(" " + item6.DataFlowChain.Summary); if (item6.DataFlowChain.IsCrossMethod) { streamWriter.WriteLine($" Cross-method flow through {item6.DataFlowChain.InvolvedMethods.Count} methods"); } streamWriter.WriteLine(" Data Flow Chain:"); foreach (DataFlowNode node2 in item6.DataFlowChain.Nodes) { DataFlowNodeType nodeType2 = node2.NodeType; if (1 == 0) { } key = nodeType2 switch { DataFlowNodeType.Source => "[SOURCE]", DataFlowNodeType.Transform => "[TRANSFORM]", DataFlowNodeType.Sink => "[SINK]", DataFlowNodeType.Intermediate => "[PASS]", _ => "[???]", }; if (1 == 0) { } string text11 = key; streamWriter.WriteLine(" " + text11 + " " + node2.Operation + " (" + node2.DataDescription + ") @ " + node2.Location); } } if (!string.IsNullOrEmpty(item6.CodeSnippet)) { streamWriter.WriteLine(" Code Snippet (IL):"); string[] array = item6.CodeSnippet.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text12 in array) { streamWriter.WriteLine(" " + text12); } streamWriter.WriteLine(); } } } WriteSecurityNoticeToReport(streamWriter, threatVerdictInfo, scanStatusInfo, flag); } } bool flag2 = false; if (promptGeneratorService != null) { flag2 = promptGeneratorService.SavePromptToFile(text4, list, text8); } else if (scanStatusInfo.Kind == ScanStatusKind.RequiresReview) { ((MelonBase)this).LoggerInstance.Warning("Skipped LLM prompt generation because this file was not fully analyzed by the loader."); } if (flag2) { ((MelonBase)this).LoggerInstance.Msg("Detailed report saved to: " + text7); ((MelonBase)this).LoggerInstance.Msg("LLM analysis prompt saved to: " + Path.Combine(text8, fileName + ".prompt.md")); ((MelonBase)this).LoggerInstance.Msg("You can copy the contents of the prompt file into ChatGPT to help determine if this is malware or a false positive, although don't trust ChatGPT to be 100% accurate."); } else { ((MelonBase)this).LoggerInstance.Msg("Detailed report saved to: " + text7); } MelonConfigManager configManager3 = _configManager; if (configManager3 == null || !(configManager3.Config?.EnableReportUpload).GetValueOrDefault() || _reportUploadService == null || threatVerdictInfo.Kind == ThreatVerdictKind.None) { continue; } try { string reportUploadApiBaseUrl = _configManager.GetReportUploadApiBaseUrl(); if (!string.IsNullOrWhiteSpace(reportUploadApiBaseUrl) && File.Exists(text4)) { byte[] assemblyBytes = File.ReadAllBytes(text4); SubmissionMetadata metadata = BuildSubmissionMetadata(fileName, list); _reportUploadService.UploadReportNonBlocking(assemblyBytes, fileName, metadata, reportUploadApiBaseUrl); } } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Warning("Report upload skipped for " + fileName + ": " + ex.Message); } } catch (Exception ex2) { ((MelonBase)this).LoggerInstance.Error("Failed to save detailed report: " + ex2.Message); } } ((MelonBase)this).LoggerInstance.Warning("====== END OF SCAN REPORT ======"); } private void QueueConsentPromptIfNeeded(string accessiblePath, string modName, List<ScanFinding> findings, ThreatVerdictInfo threatVerdict, bool wasBlocked) { if (_configManager != null && !_showUploadConsentPopup) { MLVScanConfig config = _configManager.Config; if (!config.ReportUploadConsentAsked) { _showUploadConsentPopup = true; _pendingUploadPath = accessiblePath; _pendingUploadModName = modName; _pendingUploadVerdictKind = threatVerdict?.Kind.ToString() ?? string.Empty; _pendingUploadWasBlocked = wasBlocked; _pendingUploadFindings = findings; config.ReportUploadConsentPending = true; config.PendingReportUploadPath = accessiblePath ?? string.Empty; config.PendingReportUploadVerdictKind = _pendingUploadVerdictKind; _configManager.SaveConfig(config); ((MelonBase)this).LoggerInstance.Warning("MLVScan is waiting for your upload consent decision in the in-game popup."); } } } private void HandleUploadConsentDecision(bool approved) { _showUploadConsentPopup = false; if (_configManager == null) { return; } MLVScanConfig config = _configManager.Config; config.ReportUploadConsentAsked = true; config.ReportUploadConsentPending = false; config.PendingReportUploadPath = string.Empty; config.PendingReportUploadVerdictKind = string.Empty; config.EnableReportUpload = approved; _configManager.SaveConfig(config); if (!approved) { ((MelonBase)this).LoggerInstance.Msg("MLVScan report upload declined. You will not be prompted again."); _pendingUploadPath = string.Empty; _pendingUploadModName = string.Empty; _pendingUploadVerdictKind = string.Empty; _pendingUploadWasBlocked = true; _pendingUploadFindings = null; return; } ((MelonBase)this).LoggerInstance.Msg("MLVScan report upload enabled. Uploading the flagged mod now."); try { if (_reportUploadService != null && !string.IsNullOrWhiteSpace(_pendingUploadPath) && File.Exists(_pendingUploadPath)) { string reportUploadApiBaseUrl = _configManager.GetReportUploadApiBaseUrl(); if (!string.IsNullOrWhiteSpace(reportUploadApiBaseUrl)) { byte[] assemblyBytes = File.ReadAllBytes(_pendingUploadPath); SubmissionMetadata metadata = BuildSubmissionMetadata(_pendingUploadModName, _pendingUploadFindings ?? new List<ScanFinding>()); _reportUploadService.UploadReportNonBlocking(assemblyBytes, _pendingUploadModName, metadata, reportUploadApiBaseUrl); } } } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Warning("Report upload skipped for " + _pendingUploadModName + ": " + ex.Message); } finally { _pendingUploadPath = string.Empty; _pendingUploadModName = string.Empty; _pendingUploadVerdictKind = string.Empty; _pendingUploadWasBlocked = true; _pendingUploadFindings = null; } } private static SubmissionMetadata BuildSubmissionMetadata(string modName, List<ScanFinding> findings) { List<FindingSummaryItem> findingSummary = (from f in findings.Take(20) select new FindingSummaryItem { RuleId = f.RuleId, Description = f.Description, Severity = f.Severity.ToString(), Location = RedactionHelper.RedactLocation(f.Location) }).ToList(); return new SubmissionMetadata { LoaderType = "MelonLoader", LoaderVersion = null, PluginVersion = "2.0.2", ModName = RedactionHelper.RedactFilename(modName), FindingSummary = findingSummary, ConsentVersion = "1", ConsentTimestamp = DateTime.UtcNow.ToString("o") }; } private static string FormatSeverityLabel(Severity severity) { if (1 == 0) { } string result = severity switch { Severity.Critical => "CRITICAL", Severity.High => "HIGH", Severity.Medium => "MEDIUM", Severity.Low => "LOW", _ => severity.ToString().ToUpper(), }; if (1 == 0) { } return result; } private void DisplaySecurityNotice(string modName, ThreatVerdictInfo threatVerdict, ScanStatusInfo scanStatus, bool wasBlocked) { ((MelonBase)this).LoggerInstance.Warning("IMPORTANT SECURITY NOTICE"); ((MelonBase)this).LoggerInstance.Msg(wasBlocked ? ("MLVScan detected and disabled " + modName + " before it was loaded.") : ("MLVScan flagged " + modName + " for review but did not block it under the current configuration.")); if (IsKnownThreatVerdict(threatVerdict)) { ((MelonBase)this).LoggerInstance.Msg("This mod is likely malware because it matched previously analyzed malware intelligence."); ((MelonBase)this).LoggerInstance.Msg("If this is your first time running the game with this mod, your PC is likely safe."); ((MelonBase)this).LoggerInstance.Msg("However, if you've previously run the game with this mod, your system MAY be infected."); ((MelonBase)this).LoggerInstance.Warning("Recommended security steps:"); ((MelonBase)this).LoggerInstance.Msg("1. Check with the modding community first - no detection is perfect"); ((MelonBase)this).LoggerInstance.Msg(" Join the modding Discord at: https://discord.gg/UD4K4chKak"); ((MelonBase)this).LoggerInstance.Msg(" Ask about this mod in the MLVScan thread of #mod-releases to confirm if it's actually malicious"); ((MelonBase)this).LoggerInstance.Msg("2. Run a full system scan with a trusted antivirus like Malwarebytes"); ((MelonBase)this).LoggerInstance.Msg(" Malwarebytes is recommended as a free and effective antivirus solution"); ((MelonBase)this).LoggerInstance.Msg("3. Use Microsoft Safety Scanner for a secondary scan"); ((MelonBase)this).LoggerInstance.Msg("4. Change important passwords if antivirus shows a threat"); ((MelonBase)this).LoggerInstance.Warning("Resources for malware removal:"); ((MelonBase)this).LoggerInstance.Msg("- Malwarebytes: https://www.malwarebytes.com/cybersecurity/basics/how-to-remove-virus-from-computer"); ((MelonBase)this).LoggerInstance.Msg("- Microsoft Safety Scanner: https://learn.microsoft.com/en-us/defender-endpoint/safety-scanner-download"); } else if (threatVerdict != null && threatVerdict.Kind == ThreatVerdictKind.Suspicious) { ((MelonBase)this).LoggerInstance.Msg("This mod was flagged because it triggered suspicious correlated behavior."); ((MelonBase)this).LoggerInstance.Msg("It may still be a false positive, so review the saved report before assuming infection."); ((MelonBase)this).LoggerInstance.Warning("Recommended review steps:"); ((MelonBase)this).LoggerInstance.Msg("1. Check with the modding community first - no detection is perfect"); ((MelonBase)this).LoggerInstance.Msg(" Join the modding Discord at: https://discord.gg/UD4K4chKak"); ((MelonBase)this).LoggerInstance.Msg(" Ask about this mod in the MLVScan thread of #mod-releases to confirm if it is actually malicious"); ((MelonBase)this).LoggerInstance.Msg("2. Review the saved report for the exact behavior that triggered the block"); ((MelonBase)this).LoggerInstance.Msg("3. Only run a full antivirus scan if you have already executed this mod or still do not trust it"); ((MelonBase)this).LoggerInstance.Msg("4. Whitelist the SHA256 only if you have independently verified the mod is safe"); } else if (scanStatus != null && scanStatus.Kind == ScanStatusKind.RequiresReview) { ((MelonBase)this).LoggerInstance.Msg("This mod could not be fully analyzed by the loader because it exceeded the current in-memory scan limit."); ((MelonBase)this).LoggerInstance.Msg("MLVScan still calculated its SHA-256 hash and checked exact known-malicious sample matches."); ((MelonBase)this).LoggerInstance.Warning("Recommended review steps:"); ((MelonBase)this).LoggerInstance.Msg("1. Review the saved report before assuming the mod is safe"); ((MelonBase)this).LoggerInstance.Msg("2. Validate the mod with trusted community sources or the original author"); ((MelonBase)this).LoggerInstance.Msg("3. Enable BlockIncompleteScans if you want oversized or incomplete scans blocked automatically"); ((MelonBase)this).LoggerInstance.Msg("4. Whitelist the SHA256 only after independent verification"); } else { ((MelonBase)this).LoggerInstance.Msg("Review the saved report before deciding whether to whitelist this mod."); } } private static void WriteSecurityNoticeToReport(StreamWriter writer, ThreatVerdictInfo threatVerdict, ScanStatusInfo scanStatus, bool wasBlocked) { writer.WriteLine("\n\n============== SECURITY NOTICE ==============\n"); writer.WriteLine("IMPORTANT: READ THIS SECURITY INFORMATION\n"); writer.WriteLine(wasBlocked ? "MLVScan detected and disabled this mod before it was loaded." : "MLVScan flagged this mod for review but did not block it under the current configuration."); if (IsKnownThreatVerdict(threatVerdict)) { writer.WriteLine("This mod is likely malware because it matched previously analyzed malware intelligence.\n"); writer.WriteLine("YOUR SYSTEM SECURITY STATUS:"); writer.WriteLine("- If this is your FIRST TIME starting the game with this mod installed:"); writer.WriteLine(" Your PC is likely SAFE as MLVScan prevented the mod from loading."); writer.WriteLine("\n- If you have PREVIOUSLY PLAYED the game with this mod loaded:"); writer.WriteLine(" Your system MAY BE INFECTED with malware. Take action immediately.\n"); writer.WriteLine("RECOMMENDED SECURITY STEPS:"); writer.WriteLine("1. Check with the modding community first - no detection system is perfect"); writer.WriteLine(" Join the modding Discord at: https://discord.gg/UD4K4chKak"); writer.WriteLine(" Ask about this mod in the #MLVScan or #report-mods channels to confirm if it is actually malicious"); writer.WriteLine("\n2. Run a full system scan with a reputable antivirus program"); writer.WriteLine(" Free option: Malwarebytes (https://www.malwarebytes.com/)"); writer.WriteLine(" Malwarebytes is recommended as a free and effective antivirus solution"); writer.WriteLine("\n3. Run Microsoft Safety Scanner as a secondary check"); writer.WriteLine(" Download: https://learn.microsoft.com/en-us/defender-endpoint/safety-scanner-download"); writer.WriteLine("\n4. Update all your software from official sources"); writer.WriteLine("\n5. Change passwords for important accounts (from a clean device if possible)"); writer.WriteLine("\n6. Monitor your accounts for any suspicious activity"); 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"); } else if (threatVerdict != null && threatVerdict.Kind == ThreatVerdictKind.Suspicious) { writer.WriteLine("This mod was flagged because it triggered suspicious correlated behavior.\n"); writer.WriteLine("IMPORTANT:"); writer.WriteLine("- This may still be a false positive."); writer.WriteLine("- Review the report details and verify the mod with trusted community sources before assuming infection.\n"); writer.WriteLine("RECOMMENDED REVIEW STEPS:"); writer.WriteLine("1. Check with the modding community first - no detection system is perfect"); writer.WriteLine(" Join the modding Discord at: https://discord.gg/UD4K4chKak"); writer.WriteLine(" Ask about this mod in the #MLVScan or #report-mods channels to confirm if it is actually malicious"); writer.WriteLine("\n2. Review the detailed findings and call/data-flow evidence in this report"); writer.WriteLine("\n3. Only run a full antivirus scan if you already executed the mod or still do not trust it"); writer.WriteLine("\n4. Whitelist the SHA256 only after independent verification"); } else if (scanStatus != null && scanStatus.Kind == ScanStatusKind.RequiresReview) { writer.WriteLine("This mod could not be fully analyzed by the loader because it exceeded the current in-memory scan limit.\n"); writer.WriteLine("IMPORTANT:"); writer.WriteLine("- MLVScan still calculated the SHA-256 hash and checked exact known-malicious sample matches."); writer.WriteLine("- No retained malicious verdict was produced, but the file was not fully analyzed."); writer.WriteLine("- Review the report details and verify the mod with trusted community sources before assuming it is safe.\n"); writer.WriteLine("RECOMMENDED REVIEW STEPS:"); writer.WriteLine("1. Review the report details and confirm whether this mod is expected to be unusually large"); writer.WriteLine("\n2. Validate the mod with the original author or trusted community sources"); writer.WriteLine("\n3. Enable BlockIncompleteScans if you want oversized or incomplete scans blocked automatically"); writer.WriteLine("\n4. Whitelist the SHA256 only after independent verification"); } writer.WriteLine("\n============================================="); } private static bool IsKnownThreatVerdict(ThreatVerdictInfo threatVerdict) { return (threatVerdict != null && threatVerdict.Kind == ThreatVerdictKind.KnownMaliciousSample) || (threatVerdict != null && threatVerdict.Kind == ThreatVerdictKind.KnownMalwareFamily); } } public class MelonLoaderServiceFactory { private readonly Instance _melonLogger; private readonly IScanLogger _scanLogger; private readonly IAssemblyResolverProvider _resolverProvider; private readonly MelonConfigManager _configManager; private readonly MelonPlatformEnvironment _environment; private readonly MLVScanConfig _fallbackConfig; private readonly LoaderScanTelemetryHub _telemetry; public MelonLoaderServiceFactory(Instance logger) { _melonLogger = logger ?? throw new ArgumentNullException("logger"); _scanLogger = new MelonScanLogger(logger); _telemetry = new LoaderScanTelemetryHub(); _resolverProvider = new GameAssemblyResolverProvider(_telemetry); _environment = new MelonPlatformEnvironment(); _fallbackConfig = new MLVScanConfig(); try { _configManager = new MelonConfigManager(logger); } catch (Exception ex) { _melonLogger.Error("Failed to create ConfigManager: " + ex.Message); _melonLogger.Msg("Using default configuration values"); } } 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() { MLVScanConfig mLVScanConfig = _configManager?.Config ?? _fallbackConfig; IReadOnlyList<IScanRule> rules = RuleFactory.CreateDefaultRules(); return new AssemblyScanner(rules, mLVScanConfig.Scan, _resolverProvider); } public MelonPluginScanner CreatePluginScanner() { MLVScanConfig config = _configManager?.Config ?? _fallbackConfig; return new MelonPluginScanner(_scanLogger, _resolverProvider, config, _configManager, _environment, _telemetry); } public MelonPluginDisabler CreatePluginDisabler() { MLVScanConfig config = _configManager?.Config ?? _fallbackConfig; return new MelonPluginDisabler(_scanLogger, config); } public PromptGeneratorService CreatePromptGenerator() { MLVScanConfig mLVScanConfig = _configManager?.Config ?? _fallbackConfig; return new PromptGeneratorService(mLVScanConfig.Scan, _scanLogger); } public IlDumpService CreateIlDumpService() { return new IlDumpService(_scanLogger, _environment); } public DeveloperReportGenerator CreateDeveloperReportGenerator() { return new DeveloperReportGenerator(_scanLogger); } public ReportUploadService CreateReportUploadService() { return new ReportUploadService(_configManager, delegate(string msg) { _melonLogger.Msg(msg); }, delegate(string msg) { _melonLogger.Warning(msg); }, delegate(string msg) { _melonLogger.Error(msg); }); } } public class MelonConfigManager : IConfigManager { private readonly Instance _logger; private readonly MelonPreferences_Category _category; private readonly MelonPreferences_Entry<bool> _enableAutoScan; private readonly MelonPreferences_Entry<bool> _enableAutoDisable; private readonly MelonPreferences_Entry<bool> _enableScanCache; private readonly MelonPreferences_Entry<bool> _blockKnownThreats; private readonly MelonPreferences_Entry<bool> _blockSuspicious; private readonly MelonPreferences_Entry<bool> _blockIncompleteScans; private readonly MelonPreferences_Entry<string[]> _scanDirectories; private readonly MelonPreferences_Entry<string[]> _whitelistedHashes; private readonly MelonPreferences_Entry<bool> _dumpFullIlReports; private readonly MelonPreferences_Entry<bool> _developerMode; private readonly MelonPreferences_Entry<bool> _enableReportUpload; private readonly MelonPreferences_Entry<bool> _reportUploadConsentAsked; private readonly MelonPreferences_Entry<bool> _reportUploadConsentPending; private readonly MelonPreferences_Entry<string> _pendingReportUploadPath; private readonly MelonPreferences_Entry<string> _pendingReportUploadVerdictKind; private readonly MelonPreferences_Entry<string> _reportUploadApiBaseUrl; private readonly MelonPreferences_Entry<string[]> _uploadedReportHashes; private readonly MelonPreferences_Entry<bool> _includeMods; private readonly MelonPreferences_Entry<bool> _includePlugins; private readonly MelonPreferences_Entry<bool> _includeUserLibs; private readonly MelonPreferences_Entry<bool> _includeThunderstoreProfiles; private readonly MelonPreferences_Entry<string[]> _additionalTargetRoots; private readonly MelonPreferences_Entry<string[]> _excludedTargetRoots; public MLVScanConfig Config { get; private set; } public MelonConfigManager(Instance logger) { _logger = logger ?? throw new ArgumentNullException("logger"); try { _category = MelonPreferences.CreateCategory("MLVScan"); _enableAutoScan = _category.CreateEntry<bool>("EnableAutoScan", true, (string)null, "Whether to scan mods at startup", false, false, (ValueValidator)null, (string)null); _enableAutoDisable = _category.CreateEntry<bool>("EnableAutoDisable", true, (string)null, "Whether to automatically disable mods that meet the active blocking policy", false, false, (ValueValidator)null, (string)null); _enableScanCache = _category.CreateEntry<bool>("EnableScanCache", true, (string)null, "Whether to reuse scan results for unchanged files using a local authenticated cache", false, false, (ValueValidator)null, (string)null); _blockKnownThreats = _category.CreateEntry<bool>("BlockKnownThreats", true, (string)null, "Whether to block mods that match a known threat family or exact malicious sample", false, false, (ValueValidator)null, (string)null); _blockSuspicious = _category.CreateEntry<bool>("BlockSuspicious", true, (string)null, "Whether to block suspicious unknown behavior that may still be a false positive", false, false, (ValueValidator)null, (string)null); _blockIncompleteScans = _category.CreateEntry<bool>("BlockIncompleteScans", false, (string)null, "Whether to block mods that could not be fully analyzed and require manual review", false, false, (ValueValidator)null, (string)null); _scanDirectories = _category.CreateEntry<string[]>("ScanDirectories", new string[2] { "Mods", "Plugins" }, (string)null, "Directories to scan for mods", false, false, (ValueValidator)null, (string)null); _whitelistedHashes = _category.CreateEntry<string[]>("WhitelistedHashes", Array.Empty<string>(), (string)null, "List of mod SHA256 hashes to skip when scanning", false, false, (ValueValidator)null, (string)null); _dumpFullIlReports = _category.CreateEntry<bool>("DumpFullIlReports", false, (string)null, "When enabled, saves full IL dumps for scanned mods next to reports", false, false, (ValueValidator)null, (string)null); _developerMode = _category.CreateEntry<bool>("DeveloperMode", false, (string)null, "Developer mode: Shows remediation guidance to help mod developers fix false positives", false, false, (ValueValidator)null, (string)null); _enableReportUpload = _category.CreateEntry<bool>("EnableReportUpload", false, (string)null, "When enabled (and consent given), send reports to MLVScan API for false positive analysis", false, false, (ValueValidator)null, (string)null); _reportUploadConsentAsked = _category.CreateEntry<bool>("ReportUploadConsentAsked", false, (string)null, "Whether the first-run consent prompt has been shown (internal)", false, false, (ValueValidator)null, (string)null); _reportUploadConsentPending = _category.CreateEntry<bool>("ReportUploadConsentPending", false, (string)null, "Whether an upload consent popup is pending (internal)", false, false, (ValueValidator)null, (string)null); _pendingReportUploadPath = _category.CreateEntry<string>("PendingReportUploadPath", string.Empty, (string)null, "Suspicious mod path waiting for upload consent (internal)", false, false, (ValueValidator)null, (string)null); _pendingReportUploadVerdictKind = _category.CreateEntry<string>("PendingReportUploadVerdictKind", string.Empty, (string)null, "Threat verdict kind for the pending upload consent item (internal)", false, false, (ValueValidator)null, (string)null); _reportUploadApiBaseUrl = _category.CreateEntry<string>("ReportUploadApiBaseUrl", "https://api.mlvscan.com", (string)null, "API base URL for report uploads", false, false, (ValueValidator)null, (string)null); _uploadedReportHashes = _category.CreateEntry<string[]>("UploadedReportHashes", Array.Empty<string>(), (string)null, "List of assembly SHA256 hashes already uploaded to the MLVScan API (internal)", false, false, (ValueValidator)null, (string)null); _includeMods = _category.CreateEntry<bool>("IncludeMods", true, (string)null, "Whether to include Mods folders in the target scan scope", false, false, (ValueValidator)null, (string)null); _includePlugins = _category.CreateEntry<bool>("IncludePlugins", true, (string)null, "Whether to include Plugins folders in the target scan scope", false, false, (ValueValidator)null, (string)null); _includeUserLibs = _category.CreateEntry<bool>("IncludeUserLibs", true, (string)null, "Whether to include UserLibs folders in the target scan scope", false, false, (ValueValidator)null, (string)null); _includeThunderstoreProfiles = _category.CreateEntry<bool>("IncludeThunderstoreProfiles", true, (string)null, "Whether to include Thunderstore profile folders in the target scan scope", false, false, (ValueValidator)null, (string)null); _additionalTargetRoots = _category.CreateEntry<string[]>("AdditionalTargetRoots", Array.Empty<string>(), (string)null, "Additional absolute paths to include in the target scan scope", false, false, (ValueValidator)null, (string)null); _excludedTargetRoots = _category.CreateEntry<string[]>("ExcludedTargetRoots", Array.Empty<string>(), (string)null, "Absolute paths to exclude from the target scan scope", false, false, (ValueValidator)null, (string)null); ((MelonEventBase<LemonAction<bool, bool>>)(object)_enableAutoScan.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_enableAutoDisable.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_enableScanCache.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_blockKnownThreats.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_blockSuspicious.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_blockIncompleteScans.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_scanDirectories.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_whitelistedHashes.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_dumpFullIlReports.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_developerMode.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_enableReportUpload.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_reportUploadConsentAsked.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_reportUploadConsentPending.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)_pendingReportUploadPath.OnEntryValueChanged).Subscribe((LemonAction<string, string>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)_pendingReportUploadVerdictKind.OnEntryValueChanged).Subscribe((LemonAction<string, string>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)_reportUploadApiBaseUrl.OnEntryValueChanged).Subscribe((LemonAction<string, string>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_uploadedReportHashes.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_includeMods.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_includePlugins.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_includeUserLibs.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_includeThunderstoreProfiles.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_additionalTargetRoots.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_excludedTargetRoots.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); UpdateConfigFromPreferences(); CleanupLegacyPreferenceEntries(); _logger.Msg("Configuration loaded successfully"); } catch (Exception ex) { _logger.Error("Failed to initialize config system: " + ex.Message); _logger.Msg("Using fallback in-memory configuration"); Config = new MLVScanConfig(); } } public MLVScanConfig LoadConfig() { UpdateConfigFromPreferences(); return Config; } private void OnConfigChanged<T>(T oldValue, T newValue) { UpdateConfigFromPreferences(); _logger.Msg("Configuration updated"); } private void UpdateConfigFromPreferences() { Config = new MLVScanConfig { EnableAutoScan = _enableAutoScan.Value, EnableAutoDisable = _enableAutoDisable.Value, EnableScanCache = _enableScanCache.Value, BlockKnownThreats = _blockKnownThreats.Value, BlockSuspicious = _blockSuspicious.Value, BlockIncompleteScans = _blockIncompleteScans.Value, ScanDirectories = _scanDirectories.Value, WhitelistedHashes = _whitelistedHashes.Value, DumpFullIlReports = _dumpFullIlReports.Value, Scan = new ScanConfig { DeveloperMode = _developerMode.Value }, EnableReportUpload = _enableReportUpload.Value, ReportUploadConsentAsked = _reportUploadConsentAsked.Value, ReportUploadConsentPending = _reportUploadConsentPending.Value, PendingReportUploadPath = _pendingReportUploadPath.Value, PendingReportUploadVerdictKind = _pendingReportUploadVerdictKind.Value, ReportUploadApiBaseUrl = _reportUploadApiBaseUrl.Value, UploadedReportHashes = NormalizeHashes(_uploadedReportHashes.Value), IncludeMods = _includeMods.Value, IncludePlugins = _includePlugins.Value, IncludeUserLibs = _includeUserLibs.Value, IncludePatchers = false, IncludeThunderstoreProfiles = _includeThunderstoreProfiles.Value, AdditionalTargetRoots = (_additionalTargetRoots.Value ?? Array.Empty<string>()), ExcludedTargetRoots = (_excludedTargetRoots.Value ?? Array.Empty<string>()) }; } public void SaveConfig(MLVScanConfig newConfig) { try { _enableAutoScan.Value = newConfig.EnableAutoScan; _enableAutoDisable.Value = newConfig.EnableAutoDisable; _enableScanCache.Value = newConfig.EnableScanCache; _blockKnownThreats.Value = newConfig.BlockKnownThreats; _blockSuspicious.Value = newConfig.BlockSuspicious; _blockIncompleteScans.Value = newConfig.BlockIncompleteScans; _scanDirectories.Value = newConfig.ScanDirectories; _whitelistedHashes.Value = newConfig.WhitelistedHashes; _dumpFullIlReports.Value = newConfig.DumpFullIlReports; _developerMode.Value = newConfig.Scan?.DeveloperMode ?? false; _enableReportUpload.Value = newConfig.EnableReportUpload; _reportUploadConsentAsked.Value = newConfig.ReportUploadConsentAsked; _reportUploadConsentPending.Value = newConfig.ReportUploadConsentPending; _pendingReportUploadPath.Value = newConfig.PendingReportUploadPath ?? string.Empty; _pendingReportUploadVerdictKind.Value = newConfig.PendingReportUploadVerdictKind ?? string.Empty; _reportUploadApiBaseUrl.Value = newConfig.ReportUploadApiBaseUrl; _uploadedReportHashes.Value = NormalizeHashes(newConfig.UploadedReportHashes); _includeMods.Value = newConfig.IncludeMods; _includePlugins.Value = newConfig.IncludePlugins; _includeUserLibs.Value = newConfig.IncludeUserLibs; _includeThunderstoreProfiles.Value = newConfig.IncludeThunderstoreProfiles; _additionalTargetRoots.Value = newConfig.AdditionalTargetRoots ?? Array.Empty<string>(); _excludedTargetRoots.Value = newConfig.ExcludedTargetRoots ?? Array.Empty<string>(); PersistPreferences(); _logger.Msg("Configuration saved successfully"); } catch (Exception ex) { _logger.Error("Error saving configuration: " + ex.Message); Config = newConfig; } } public string[] GetWhitelistedHashes() { return _whitelistedHashes.Value; } public void SetWhitelistedHashes(string[] hashes) { if (hashes != null) { string[] array = (from h in hashes where !string.IsNullOrWhiteSpace(h) select h.ToLowerInvariant()).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray(); _whitelistedHashes.Value = array; PersistPreferences(); UpdateConfigFromPreferences(); _logger.Msg($"Updated whitelist with {array.Length} hash(es)"); } } public bool IsHashWhitelisted(string hash) { if (string.IsNullOrWhiteSpace(hash)) { return false; } return Config.WhitelistedHashes.Contains<string>(hash.ToLowerInvariant(), StringComparer.OrdinalIgnoreCase); } public string GetReportUploadApiBaseUrl() { return _reportUploadApiBaseUrl.Value; } public bool IsReportHashUploaded(string hash) { if (string.IsNullOrWhiteSpace(hash)) { return false; } return NormalizeHashes(_uploadedReportHashes.Value).Contains<string>(hash.ToLowerInvariant(), StringComparer.OrdinalIgnoreCase); } public void MarkReportHashUploaded(string hash) { if (HashUtility.IsValidHash(hash)) { string[] array = NormalizeHashes((_uploadedReportHashes.Value ?? Array.Empty<string>()).Append(hash)); int num = array.Length; string[] value = _uploadedReportHashes.Value; if (num != ((value != null) ? value.Length : 0) || !IsReportHashUploaded(hash)) { _uploadedReportHashes.Value = array; PersistPreferences(); UpdateConfigFromPreferences(); _logger.Msg("Recorded uploaded report hash: " + hash); } } } private void PersistPreferences() { MelonPreferences.Save(); CleanupLegacyPreferenceEntries(); } private void CleanupLegacyPreferenceEntries() { try { string filePath = Path.Combine(MelonEnvironment.UserDataDirectory, "MelonPreferences.cfg"); if (LegacyConfigCleanup.TryRemoveObsoleteIniEntries(filePath, "MLVScan", out var removedKeys)) { _logger.Msg("Removed legacy config keys from MelonPreferences.cfg: " + string.Join(", ", removedKeys)); } } catch { } } private static string[] NormalizeHashes(IEnumerable<string> hashes) { return (from h in hashes ?? Array.Empty<string>() where !string.IsNullOrWhiteSpace(h) select h.ToLowerInvariant()).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray(); } } public class MelonPlatformEnvironment : IPlatformEnvironment { private readonly string _gameRoot; private readonly string _dataDir; private readonly string _reportsDir; public string GameRootDirectory => _gameRoot; public string[] PluginDirectories => new string[2] { Path.Combine(_gameRoot, "Mods"), Path.Combine(_gameRoot, "Plugins") }; public string DataDirectory { get { EnsureDataDirectory(); return _dataDir; } } public string ReportsDirectory { get { EnsureDataDirectory(); if (!Directory.Exists(_reportsDir)) { Directory.CreateDirectory(_reportsDir); } return _reportsDir; } } public string ManagedDirectory { get { try { string[] directories = Directory.GetDirectories(_gameRoot, "*_Data"); string[] array = directories; foreach (string path in array) { string text = Path.Combine(path, "Managed"); if (Directory.Exists(text)) { return text; } } } catch { } string text2 = Path.Combine(_gameRoot, "MelonLoader", "Managed"); if (Directory.Exists(text2)) { return text2; } return string.Empty; } } public string SelfAssemblyPath { get { try { return typeof(MelonPlatformEnvironment).Assembly.Location; } catch { return string.Empty; } } } public string PlatformName => "MelonLoader"; public MelonPlatformEnvironment() { _gameRoot = MelonEnvironment.GameRootDirectory; _dataDir = Path.Combine(MelonEnvironment.UserDataDirectory, "MLVScan"); _reportsDir = Path.Combine(_dataDir, "Reports"); } private void EnsureDataDirectory() { if (!Directory.Exists(_dataDir)) { Directory.CreateDirectory(_dataDir); } } } public class MelonPluginScanner : PluginScannerBase { [StructLayout(LayoutKind.Auto)] [CompilerGenerated] private struct <>c__DisplayClass2_0 { public HashSet<string> emitted; public MelonPluginScanner <>4__this; public HashSet<string> builtInRoots; } [CompilerGenerated] private sealed class <GetScanDirectories>d__2 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private int <>l__initialThreadId; public MelonPluginScanner <>4__this; private <>c__DisplayClass2_0 <>8__1; private string[] <>s__2; private int <>s__3; private string <scanDir>5__4; private IEnumerator<string> <>s__5; private string <thunderstoreRoot>5__6; private HashSet<string>.Enumerator <>s__7; private string <path>5__8; string IEnumerator<string>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <GetScanDirectories>d__2(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = System.Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <>8__1 = default(<>c__DisplayClass2_0); <>s__2 = null; <scanDir>5__4 = null; <>s__5 = null; <thunderstoreRoot>5__6 = null; <>s__7 = default(HashSet<string>.Enumerator); <path>5__8 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1.<>4__this = <>4__this; <>8__1.emitted = new HashSet<string>(StringComparer.OrdinalIgnoreCase); <>8__1.builtInRoots = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { Path.GetFullPath(Path.Combine(<>4__this._environment.GameRootDirectory, "Mods")), Path.GetFullPath(Path.Combine(<>4__this._environment.GameRootDirectory, "Plugins")), Path.GetFullPath(Path.Combine(<>4__this._environment.GameRootDirectory, "UserLibs")) }; if (<>4__this.Config.IncludeMods) { <>4__this.<GetScanDirectories>g__AddIfPresent|2_0(Path.Combine(<>4__this._environment.GameRootDirectory, "Mods"), ref <>8__1); } if (<>4__this.Config.IncludePlugins) { <>4__this.<GetScanDirectories>g__AddIfPresent|2_0(Path.Combine(<>4__this._environment.GameRootDirectory, "Plugins"), ref <>8__1); } if (<>4__this.Config.IncludeUserLibs) { <>4__this.<GetScanDirectories>g__AddIfPresent|2_0(Path.Combine(<>4__this._environment.GameRootDirectory, "UserLibs"), ref <>8__1); } <>s__2 = <>4__this.Config.ScanDirectories ?? Array.Empty<string>(); for (<>s__3 = 0; <>s__3 < <>s__2.Length; <>s__3++) { <scanDir>5__4 = <>s__2[<>s__3]; <>4__this.<GetScanDirectories>g__AddLegacyIfPresent|2_1(<scanDir>5__4, ref <>8__1); <scanDir>5__4 = null; } <>s__2 = null; if (<>4__this.Config.IncludeThunderstoreProfiles) { <>s__5 = GetThunderstoreDirectories().GetEnumerator(); try { while (<>s__5.MoveNext()) { <thunderstoreRoot>5__6 = <>s__5.Current; <>4__this.<GetScanDirectories>g__AddIfPresent|2_0(<thunderstoreRoot>5__6, ref <>8__1); <thunderstoreRoot>5__6 = null; } } finally { if (<>s__5 != null) { <>s__5.Dispose(); } } <>s__5 = null; } <>s__7 = <>8__1.emitted.GetEnumerator(); <>1__state = -3; break; case 1: <>1__state = -3; <path>5__8 = null; break; } if (<>s__7.MoveNext()) { <path>5__8 = <>s__7.Current; <>2__current = <path>5__8; <>1__state = 1; return true; } <>m__Finally1(); <>s__7 = default(HashSet<string>.Enumerator); return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; ((IDisposable)<>s__7).Dispose(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator<string> IEnumerable<string>.GetEnumerator() { <GetScanDirectories>d__2 result; if (<>1__state == -2 && <>l__initialThreadId == System.Environment.CurrentManagedThreadId) { <>1__state = 0; result = this; } else { result = new <GetScanDirectories>d__2(0) { <>4__this = <>4__this }; } return result; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<string>)this).GetEnumerator(); } } [CompilerGenerated] private sealed class <GetThunderstoreDirectories>d__5 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private int <>l__initialThreadId; private string <appDataPath>5__1; private string <thunderstoreBasePath>5__2; private IEnumerator<string> <>s__3; private string <gameFolder>5__4; private string <profilesPath>5__5; private IEnumerator<string> <>s__6; private string <profileFolder>5__7; private string <modsPath>5__8; private string <pluginsPath>5__9; string IEnumerator<string>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <GetThunderstoreDirectories>d__5(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = System.Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if ((uint)(num - -4) <= 1u || (uint)(num - 1) <= 1u) { try { if (num == -4 || (uint)(num - 1) <= 1u) { try { } finally { <>m__Finally2(); } } } finally { <>m__Finally1(); } } <appDataPath>5__1 = null; <thunderstoreBasePath>5__2 = null; <>s__3 = null; <gameFolder>5__4 = null; <profilesPath>5__5 = null; <>s__6 = null; <profileFolder>5__7 = null; <modsPath>5__8 = null; <pluginsPath>5__9 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (!RuntimeInformationHelper.IsWindows) { return false; } <appDataPath>5__1 = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData); <thunderstoreBasePath>5__2 = Path.Combine(<appDataPath>5__1, "Thunderstore Mod Manager", "DataFolder"); if (!Directory.Exists(<thunderstoreBasePath>5__2)) { return false; } <>s__3 = SafeGetDirectories(<thunderstoreBasePath>5__2).GetEnumerator(); <>1__state = -3; goto IL_01fe; case 1: <>1__state = -4; goto IL_0173; case 2: { <>1__state = -4; goto IL_01bb; } IL_01bb: <modsPath>5__8 = null; <pluginsPath>5__9 = null; <profileFolder>5__7 = null; goto IL_01d1; IL_01d1: if (<>s__6.MoveNext()) { <profileFolder>5__7 = <>s__6.Current; <modsPath>5__8 = Path.Combine(<profileFolder>5__7, "Mods"); if (Directory.Exists(<modsPath>5__8)) { <>2__current = <modsPath>5__8; <>1__state = 1; return true; } goto IL_0173; } <>m__Finally2(); <>s__6 = null; <profilesPath>5__5 = null; <gameFolder>5__4 = null; goto IL_01fe; IL_0173: <pluginsPath>5__9 = Path.Combine(<profileFolder>5__7, "Plugins"); if (Directory.Exists(<pluginsPath>5__9)) { <>2__current = <pluginsPath>5__9; <>1__state = 2; return true; } goto IL_01bb; IL_01fe: do { if (<>s__3.MoveNext()) { <gameFolder>5__4 = <>s__3.Current; <profilesPath>5__5 = Path.Combine(<gameFolder>5__4, "profiles"); continue; } <>m__Finally1(); <>s__3 = null; return false; } while (!Directory.Exists(<profilesPath>5__5)); <>s__6 = SafeGetDirectories(<profilesPath>5__5).GetEnumerator(); <>1__state = -4; goto IL_01d1; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<>s__3 != null) { <>s__3.Dispose(); } } private void <>m__Finally2() { <>1__state = -3; if (<>s__6 != null) { <>s__6.Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator<string> IEnumerable<string>.GetEnumerator() { if (<>1__state == -2 && <>l__initialThreadId == System.Environment.CurrentManagedThreadId) { <>1__state = 0; return this; } return new <GetThunderstoreDirectories>d__5(0); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<string>)this).GetEnumerator(); } } [CompilerGenerated] private sealed class <SafeGetDirectories>d__6 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private int <>l__initialThreadId; private string path; public string <>3__path; private string[] <directories>5__1; private string[] <>s__2; private int <>s__3; private string <directory>5__4; string IEnumerator<string>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SafeGetDirectories>d__6(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = System.Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { <directories>5__1 = null; <>s__2 = null; <directory>5__4 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; try { <directories>5__1 = Directory.GetDirectories(path); } catch (UnauthorizedAccessException) { return false; } catch (DirectoryNotFoundException) { return false; } catch (IOException) { return false; } <>s__2 = <directories>5__1; <>s__3 = 0; break; case 1: <>1__state = -1; <directory>5__4 = null; <>s__3++; break; } if (<>s__3 < <>s__2.Length) { <directory>5__4 = <>s__2[<>s__3]; <>2__current = <directory>5__4; <>1__state = 1; return true; } <>s__2 = null; return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator<string> IEnumerable<string>.GetEnumerator() { <SafeGetDirectories>d__6 <SafeGetDirectories>d__; if (<>1__state == -2 && <>l__initialThreadId == System.Environment.CurrentManagedThreadId) { <>1__state = 0; <SafeGetDirectories>d__ = this; } else { <SafeGetDirectories>d__ = new <SafeGetDirectories>d__6(0); } <SafeGetDirectories>d__.path = <>3__path; return <SafeGetDirectories>d__; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<string>)this).GetEnumerator(); } } private readonly MelonPlatformEnvironment _environment; public MelonPluginScanner(IScanLogger logger, IAssemblyResolverProvider resolverProvider, MLVScanConfig config, IConfigManager configManager, MelonPlatformEnvironment environment, LoaderScanTelemetryHub telemetry) : base(logger, resolverProvider, config, configManager, environment, telemetry) { _environment = environment ?? throw new ArgumentNullException("environment"); } [IteratorStateMachine(typeof(<GetScanDirectories>d__2))] protected override IEnumerable<string> GetScanDirectories() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <GetScanDirectories>d__2(-2) { <>4__this = this }; } protected override bool IsSelfAssembly(string filePath) { try { string selfAssemblyPath = _environment.SelfAssemblyPath; if (string.IsNullOrEmpty(selfAssemblyPath)) { return false; } return Path.GetFullPath(filePath).Equals(Path.GetFullPath(selfAssemblyPath), StringComparison.OrdinalIgnoreCase); } catch { return false; } } protected override IEnumerable<string> GetResolverDirectories() { HashSet<string> emitted = new HashSet<string>(StringComparer.OrdinalIgnoreCase); AddIfPresent(Path.Combine(_environment.GameRootDirectory, "Mods")); AddIfPresent(Path.Combine(_environment.GameRootDirectory, "Plugins")); AddIfPresent(Path.Combine(_environment.GameRootDirectory, "UserLibs")); string[] array = Config.ScanDirectories ?? Array.Empty<string>(); foreach (string text in array) { AddIfPresent(Path.IsPathRooted(text) ? text : Path.Combine(_environment.GameRootDirectory, text)); } if (Config.IncludeThunderstoreProfiles) { foreach (string thunderstoreDirectory in GetThunderstoreDirectories()) { AddIfPresent(thunderstoreDirectory); } } return emitted; void AddIfPresent(string path) { if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path)) { emitted.Add(Path.GetFullPath(path)); } } } [IteratorStateMachine(typeof(<GetThunderstoreDirectories>d__5))] private static IEnumerable<string> GetThunderstoreDirectories() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <GetThunderstoreDirectories>d__5(-2); } [IteratorStateMachine(typeof(<SafeGetDirectories>d__6))] private static IEnumerable<string> SafeGetDirectories(string path) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SafeGetDirectories>d__6(-2) { <>3__path = path }; } [CompilerGenerated] private void <GetScanDirectories>g__AddIfPresent|2_0(string path, ref <>c__DisplayClass2_0 P_1) { if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path)) { P_1.emitted.Add(Path.GetFullPath(path)); } } [CompilerGenerated] private void <GetScanDirectories>g__AddLegacyIfPresent|2_1(string scanDir, ref <>c__DisplayClass2_0 P_1) { if (!string.IsNullOrWhiteSpace(scanDir)) { string fullPath = Path.GetFullPath(Path.IsPathRooted(scanDir) ? scanDir : Path.Combine(_environment.GameRootDirectory, scanDir)); if (!P_1.builtInRoots.Contains(fullPath)) { <GetScanDirectories>g__AddIfPresent|2_0(fullPath, ref P_1); } } } } public class MelonPluginDisabler : PluginDisablerBase { private const string DisabledExtension = ".disabled"; public MelonPluginDisabler(IScanLogger logger, MLVScanConfig config) : base(logger, config) { } protected override string GetDisabledExtension() { return ".disabled"; } protected override string GetDisabledPath(string originalPath) { return Path.ChangeExtension(originalPath, ".disabled"); } } } namespace MLVScan.Services { internal static class ConsentMessageHelper { public static string GetUploadConsentMessage(string modName, string verdictKind, bool wasBlocked = true) { string text = (string.IsNullOrWhiteSpace(modName) ? "this mod" : modName); if (string.Equals(verdictKind, ThreatVerdictKind.KnownMaliciousSample.ToString(), StringComparison.Ordinal) || string.Equals(verdictKind, ThreatVerdictKind.KnownMalwareFamily.ToString(), StringComparison.Ordinal)) { return wasBlocked ? ("MLVScan identified " + text + " as likely malware and disabled it.") : ("MLVScan identified " + text + " as likely malware, but it was not blocked by the current configuration."); } if (string.IsNullOrWhiteSpace(verdictKind) || string.Equals(verdictKind, ThreatVerdictKind.None.ToString(), StringComparison.Ordinal)) { return wasBlocked ? ("MLVScan blocked " + text + " because it could not complete full analysis and manual review is required.") : ("MLVScan could not complete full analysis of " + text + ", so manual review is required before you trust it."); } return wasBlocked ? ("MLVScan blocked " + text + " because it triggered suspicious behavior. It may still be a false positive.") : ("MLVScan flagged " + text + " because it triggered suspicious behavior, but it was not blocked by the current configuration. It may still be a false positive."); } } public abstract class PluginScannerBase { protected readonly IScanLogger Logger; protected readonly IAssemblyResolverProvider ResolverProvider; protected readonly MLVScanConfig Config; protected readonly IConfigManager ConfigManager; protected readonly IPlatformEnvironment Environment; protected readonly AssemblyScanner AssemblyScanner; protected readonly ThreatVerdictBuilder ThreatVerdictBuilder; private const long MaxAssemblyScanBytes = 268435456L; private readonly IFileIdentityProvider _fileIdentityProvider; private readonly IResolverCatalogProvider _resolverCatalogProvider; private readonly LoaderScanTelemetryHub _telemetry; private readonly TargetAssemblyScopeFilter _scopeFilter = new TargetAssemblyScopeFilter(); private readonly string _scannerFingerprint; private readonly string _selfAssemblyHash; private IScanCacheStore _cacheStore; private bool _cacheUnavailable; protected PluginScannerBase(IScanLogger logger, IAssemblyResolverProvider resolverProvider, MLVScanConfig config, IConfigManager configManager, IPlatformEnvironment environment, LoaderScanTelemetryHub telemetry) { Logger = logger ?? throw new ArgumentNullException("logger"); ResolverProvider = resolverProvider ?? throw new ArgumentNullException("resolverProvider"); Config = config ?? throw new ArgumentNullException("config"); ConfigManager = configManager ?? throw new ArgumentNullException("configManager"); Environment = environment ?? throw new ArgumentNullException("environment"); _telemetry = telemetry ?? throw new ArgumentNullException("telemetry"); _fileIdentityProvider = new CrossPlatformFileIdentityProvider(); _resolverCatalogProvider = resolverProvider as IResolverCatalogProvider; IScanRule[] rules = RuleFactory.CreateDefaultRules().ToArray(); AssemblyScanner = new AssemblyScanner(rules, Config.Scan, ResolverProvider); ThreatVerdictBuilder = new ThreatVerdictBuilder(); _scannerFingerprint = ComputeScannerFingerprint(Config.Scan, (IReadOnlyCollection<IScanRule>)(object)rules); _selfAssemblyHash = GetSelfAssemblyHash(environment.SelfAssemblyPath); } protected abstract IEnumerable<string> GetScanDirectories(); protected abstract bool IsSelfAssembly(string filePath); protected virtual IEnumerable<string> GetResolverDirectories() { return GetScanDirectories(); } protected virtual void OnScanComplete(Dictionary<string, ScannedPluginResult> results) { } public Dictionary<string, ScannedPluginResult> ScanAllPlugins(bool forceScanning = false) { Dictionary<string, ScannedPluginResult> dictionary = new Dictionary<string, ScannedPluginResult>(StringComparer.OrdinalIgnoreCase); if (!forceScanning && !Config.EnableAutoScan) { Logger.Info("Automatic scanning is disabled in configuration"); return dictionary; } _telemetry.BeginRun($"{Environment.PlatformName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"); string[] array = GetScanDirectories().ToArray(); foreach (string item in array.Where((string directory) => !Directory.Exists(directory))) { Logger.Warning("Directory not found: " + item); } long startTimestamp = _telemetry.StartTimestamp(); IReadOnlyList<string> readOnlyList = _scopeFilter.BuildEffectiveRoots(array, Config); _telemetry.AddPhaseElapsed("Scope.BuildEffectiveRoots", startTimestamp); StringComparer pathComparer = GetPathComparer(); string[] effectiveRoots = (from root in GetResolverDirectories().Concat(Config.AdditionalTargetRoots) where !string.IsNullOrWhiteSpace(root) select root).Select(Path.GetFullPath).Distinct<string>(pathComparer).ToArray(); string resolverFingerprint = BuildResolverCatalog((IReadOnlyCollection<string>)(object)effectiveRoots); string[] array2 = readOnlyList.SelectMany(EnumerateCandidateFiles).Distinct<string>(pathComparer).ToArray(); HashSet<string> activeCanonicalPaths = new HashSet<string>(pathComparer); HashSet<string> processedCanonicalPaths = new HashSet<string>(pathComparer); string[] array3 = array2; foreach (string text in array3) { try { ScanSingleFile(text, readOnlyList, dictionary, activeCanonicalPaths, processedCanonicalPaths, resolverFingerprint); } catch (Exception ex) { Logger.Error("Error scanning " + Path.GetFileName(text) + ": " + ex.Message); } } GetCacheStore()?.PruneMissingEntries(activeCanonicalPaths); OnScanComplete(dictionary); _telemetry.CompleteRun(readOnlyList.Count, array2.Length, dictionary.Count); string text2 = TryGetDataDirectory(); if (!string.IsNullOrWhiteSpace(text2)) { string text3 = _telemetry.TryWriteArtifact(Path.Combine(text2, "Diagnostics")); if (!string.IsNullOrWhiteSpace(text3)) { Logger.Debug("Wrote loader profile artifact: " + text3); } } return dictionary; } protected virtual IEnumerable<string> EnumerateCandidateFiles(string directoryPath) { Logger.Info("Scanning directory: " + directoryPath); return Directory.EnumerateFiles(directoryPath, "*.dll", SearchOption.AllDirectories); } protected virtual void ScanSingleFile(string filePath, IReadOnlyCollection<string> effectiveRoots, Dictionary<string, ScannedPluginResult> results, ISet<string> activeCanonicalPaths, ISet<string> processedCanonicalPaths, string resolverFingerprint) { long startTimestamp = _telemetry.StartTimestamp(); string fileName = Path.GetFileName(filePath); using FileProbe fileProbe = _fileIdentityProvider.OpenProbe(filePath); if (!_scopeFilter.IsTargetAssembly(fileProbe.CanonicalPath, effectiveRoots, Config)) { _telemetry.IncrementCounter("Files.OutOfScope", 1L); return; } activeCanonicalPaths.Add(fileProbe.CanonicalPath); if (!processedCanonicalPaths.Add(fileProbe.CanonicalPath)) { _telemetry.IncrementCounter("Files.DuplicateCanonicalPath", 1L); return; } if (IsSelfAssembly(fileProbe.CanonicalPath)) { Logger.Debug("Skipping self: " + fileName); _telemetry.IncrementCounter("Files.Self", 1L); return; } if (Config.EnableScanCache && TryReuseByPathCache(fileProbe, filePath, resolverFingerprint, results)) { _telemetry.RecordFileSample(filePath, startTimestamp, "cache-hit:path", 0L, 0); return; } bool flag = fileProbe.Stream.CanSeek && fileProbe.Stream.Length > 268435456; long num = 0L; byte[] array = null; string text; if (flag) { Logger.Warning($"Manual review required for {fileName}: it exceeds the loader scan limit of {256L} MB and cannot be fully analyzed in memory."); _telemetry.IncrementCounter("Files.TooLarge", 1L); num = (fileProbe.Stream.CanSeek ? fileProbe.Stream.Length : 0); if (num > 0) { _telemetry.IncrementCounter("Bytes.Read", num); } long startTimestamp2 = _telemetry.StartTimestamp(); text = CalculateStreamHash(fileProbe.Stream); _telemetry.AddPhaseElapsed("Hash.CalculateSha256", startTimestamp2); } else { long startTimestamp3 = _telemetry.StartTimestamp(); array = ReadFileBytes(fileProbe.Stream); _telemetry.AddPhaseElapsed("File.ReadBytes", startTimestamp3); num = array.Length; _telemetry.IncrementCounter("Bytes.Read", num); long startTimestamp4 = _telemetry.StartTimestamp(); text = HashUtility.CalculateBytesHash(array); _telemetry.AddPhaseElapsed("Hash.CalculateSha256", startTimestamp4); } if (IsExactSelfCopy(text)) { Logger.Debug("Skipping self copy: " + fileName); _telemetry.IncrementCounter("Files.SelfCopy", 1L); } else { if (IsHashWhitelisted(fileName, text)) { return; } if (Config.EnableScanCache && TryReuseByHashCache(text, fileProbe, filePath, resolverFingerprint, results)) { _telemetry.RecordFileSample(filePath, startTimestamp, "cache-hit:hash", num, 0); return; } ScannedPluginResult scannedPluginResult = ThreatVerdictBuilder.Build(filePath, text, new List<ScanFinding>()); if (scannedPluginResult.ThreatVerdict.Kind == ThreatVerdictKind.KnownMaliciousSample) { UpsertCacheEntry(fileProbe, text, resolverFingerprint, scannedPluginResult); RegisterResultIfNeeded(fileName, scannedPluginResult, results); _telemetry.RecordFileSample(filePath, startTimestamp, "hash-only-known-sample", num, 0); return; } if (!flag) { long startTimestamp5 = _telemetry.StartTimestamp(); using MemoryStream assemblyStream = new MemoryStream(array, writable: false); List<ScanFinding> source = AssemblyScanner.Scan(assemblyStream, filePath).ToList(); _telemetry.AddPhaseElapsed("Scan.Assembly", startTimestamp5); List<ScanFinding> list = source.Where((ScanFinding finding) => finding.Location != "Assembly scanning").ToList(); ScannedPluginResult scannedPluginResult2 = ThreatVerdictBuilder.Build(filePath, text, list); UpsertCacheEntry(fileProbe, text, resolverFingerprint, scannedPluginResult2); RegisterResultIfNeeded(fileName, scannedPluginResult2, results); _telemetry.RecordFileSample(filePath, startTimestamp, "scan", num, list.Count); return; } ScannedPluginResult scannedPluginResult3 = CreateOversizedAssemblyResult(filePath, text, num); UpsertCacheEntry(fileProbe, text, resolverFingerprint, scannedPluginResult3); RegisterResultIfNeeded(fileName, scannedPluginResult3, results); _telemetry.RecordFileSample(filePath, startTimestamp, "oversized:review-required", num, scannedPluginResult3.Findings.Count); } } private bool TryReuseByPathCache(FileProbe probe, string filePath, string resolverFingerprint, Dictionary<string, ScannedPluginResult> results) { IScanCacheStore cacheStore = GetCacheStore(); if (cacheStore == null) { return false; } ScanCacheEntry scanCacheEntry = cacheStore.TryGetByPath(probe.CanonicalPath); if (scanCacheEntry == null) { _telemetry.IncrementCounter("Cache.PathMiss", 1L); return false; } if (!scanCacheEntry.CanReuseStrictly(probe, _scannerFingerprint, resolverFingerprint, cacheStore.CanTrustCleanEntries)) { _telemetry.IncrementCounter("Cache.PathRejected", 1L); return false; } _telemetry.IncrementCounter("Cache.PathHit", 1L); RegisterResultIfNeeded(Path.GetFileName(filePath), scanCacheEntry.CloneResultForPath(filePath), results); return true; } private bool TryReuseByHashCache(string hash, FileProbe probe, string filePath, string resolverFingerprint, Dictionary<string, ScannedPluginResult> results) { IScanCacheStore cacheStore = GetCacheStore(); if (cacheStore == null) { return false; } ScanCacheEntry scanCacheEntry = cacheStore.TryGetByHash(hash); if (scanCacheEntry == null) { _telemetry.IncrementCounter("Cache.HashMiss", 1L); return false; } if (!string.Equals(scanCacheEntry.ScannerFingerprint, _scannerFingerprint, StringComparison.Ordinal) || !string.Equals(scanCacheEntry.ResolverFingerprint, resolverFingerprint, StringComparison.Ordinal)) { _telemetry.IncrementCounter("Cache.HashRejected", 1L); return false; } if (!ScanResultFacts.HasThreatVerdict(scanCacheEntry.Result) && !cacheStore.CanTrustCleanEntries) { _telemetry.IncrementCounter("Cache.HashRejected", 1L); return false; } ScannedPluginResult scannedPluginResult = scanCacheEntry.CloneResultForPath(filePath); UpsertCacheEntry(probe, hash, resolverFingerprint, scannedPluginResult); _telemetry.IncrementCounter("Cache.HashHit", 1L); RegisterResultIfNeeded(Path.GetFileName(filePath), scannedPluginResult, results); return true; } private void RegisterResultIfNeeded(string fileName, ScannedPluginResult scannedResult, Dictionary<string, ScannedPluginResult> results) { if (scannedResult == null || IsHashWhitelisted(fileName, scannedResult.FileHash) || !ScanResultFacts.RequiresAttention(scannedResult)) { return; } results[scannedResult.FilePath] = scannedResult; if (scannedResult.ThreatVerdict.Kind == ThreatVerdictKind.KnownMaliciousSample || scannedResult.ThreatVerdict.Kind == ThreatVerdictKind.KnownMalwareFamily) { string text = scannedResult.ThreatVerdict.PrimaryFamily?.DisplayName; if (!string.IsNullOrWhiteSpace(text)) { Logger.Warning("Detected likely malware in " + fileName + " - " + scannedResult.ThreatVerdict.Title + ": " + text); } else { Logger.Warning("Detected likely malware in " + fileName + " - " + scannedResult.ThreatVerdict.Title); } } else if (scannedResult.ThreatVerdict.Kind == ThreatVerdictKind.Suspicious) { Logger.Warning("Detected suspicious behavior in " + fileName + " - " + scannedResult.ThreatVerdict.Title); } else { Logger.Warning("Manual review required for " + fileName + " - " + scannedResult.ScanStatus.Title); } } private bool IsHashWhitelisted(string fileName, string fileHash) { if (!ConfigManager.IsHashWhitelisted(fileHash)) { return false; } Logger.Debug("Skipping whitelisted: " + fileName); _telemetry.IncrementCounter("Files.Whitelisted", 1L); return true; } private void UpsertCacheEntry(FileProbe probe, string hash, string resolverFingerprint, ScannedPluginResult result) { GetCacheStore()?.Upsert(new ScanCacheEntry { CanonicalPath = probe.CanonicalPath, RealPath = probe.OriginalPath, FileIdentity = probe.Identity, Sha256 = hash, ScannerFingerprint = _scannerFingerprint, ResolverFingerprint = resolverFingerprint, Result = result }); } private static ScannedPluginResult CreateOversizedAssemblyResult(string filePath, string fileHash, long fileSizeBytes) { long num = 256L; double num2 = ((fileSizeBytes > 0) ? Math.Ceiling((double)fileSizeBytes / 1048576.0) : 0.0); List<ScanFinding> findings = new List<ScanFinding> { new ScanFinding("Loader scan preflight", $"Assembly exceeds the loader scan limit ({num2:0.#} MB > {num} MB). SHA-256 and exact known-malicious sample checks still ran, but full IL analysis was skipped and the file requires manual review.", Severity.Medium) { RuleId = "OversizedAssembly" } }; return new ScannedPluginResult { FilePath = (filePath ?? string.Empty), FileHash = (fileHash ?? string.Empty), Findings = findings, ThreatVerdict = new ThreatVerdictInfo { Kind = ThreatVerdictKind.None, Title = "No threat verdict", Summary = "No retained malicious verdict was produced before the loader hit a scan-completeness limit.", Confidence = 0.0, ShouldBypassThreshold = false }, ScanStatus = new ScanStatusInfo { Kind = ScanStatusKind.RequiresReview, Title = "Manual review required", Summary = $"This file exceeds the loader scan size limit ({num} MB). MLVScan calculated its SHA-256 hash and checked exact known-malicious sample matches, but full IL analysis was skipped to avoid loading the entire assembly into memory." } }; } private string BuildResolverCatalog(IReadOnlyCollection<string> effectiveRoots) { if (_resolverCatalogProvider == null) { return "resolver-provider-unavailable"; } long startTimestamp = _telemetry.StartTimestamp(); _resolverCatalogProvider.BuildCatalog(effectiveRoots); _telemetry.AddPhaseElapsed("Resolver.BuildCatalog", startTimestamp); return _resolverCatalogProvider.ContextFingerprint; } private IScanCacheStore GetCacheStore() { if (!Config.EnableScanCache || _cacheUnavailable) { return null; } if (_cacheStore != null) { return _cacheStore; } try { _cacheStore = CreateDefaultCacheStore(Environment); } catch (Exception ex) { _cacheUnavailable = true; Logger.Warning("Scan cache unavailable; continuing without cache reuse: " + ex.Message); } return _cacheStore; } private bool IsExactSelfCopy(string hash) { return !string.IsNullOrWhiteSpace(_selfAssemblyHash) && _selfAssemblyHash.Equals(hash, StringComparison.OrdinalIgnoreCase); } private string TryGetDataDirectory() { try { return Environment.DataDirectory; } catch (Exception ex) { Logger.Warning("Diagnostics output unavailable: " + ex.Message); return null; } } private static string GetSelfAssemblyHash(string selfAssemblyPath) { try { if (string.IsNullOrWhiteSpace(selfAssemblyPath) || !File.Exists(selfAssemblyPath)) { return string.Empty; } return HashUtility.CalculateFileHash(selfAssemblyPath); } catch { return string.Empty; } } private static byte[] ReadFileBytes(FileStream stream) { stream.Position = 0L; using MemoryStream memoryStream = new MemoryStream((int)(stream.CanSeek ? stream.Length : 0)); stream.CopyTo(memoryStream); return memoryStream.ToArray(); } private static string CalculateStreamHash(FileStream stream) { stream.Position = 0L; using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(stream); return BitConverter.ToString(array).Replace("-", string.Empty).ToLowerInvariant(); } private static IScanCacheStore CreateDefaultCacheStore(IPlatformEnvironment environment) { string cacheDirectory = Path.Combine(environment.DataDirectory, "Cache"); return new SecureScanCacheStore(cacheDirectory, new ScanCacheSigner(cacheDirectory)); } private static string ComputeScannerFingerprint(ScanConfig config, IReadOnlyCollection<IScanRule> rules) { List<string> list = new List<string> { "MLVScan.MelonLoader", "2.0.2", MLVScanVersions.CoreVersion, "1.2.0".ToString(), $"EnableMultiSignalDetection={config.EnableMultiSignalDetection}", $"AnalyzeExceptionHandlers={config.AnalyzeExceptionHandlers}", $"AnalyzeLocalVariables={config.AnalyzeLocalVariables}", $"AnalyzePropertyAccessors={config.AnalyzePropertyAccessors}", $"DetectAssemblyMetadata={config.DetectAssemblyMetadata}", $"EnableCrossMethodAnalysis={config.EnableCrossMethodAnalysis}", $"MaxCallChainDepth={config.MaxCallChainDepth}", $"EnableReturnValueTracking={config.EnableReturnValueTracking}", $"EnableRecursiveResourceScanning={config.EnableRecursiveResourceScanning}", $"MaxRecursiveResourceSizeMB={config.MaxRecursiveResourceSizeMB}", $"MinimumEncodedStringLength={config.MinimumEncodedStringLength}" }; list.AddRange(from rule in rules.OrderBy<IScanRule, string>((IScanRule rule) => rule.RuleId, StringComparer.Ordinal).ThenBy<IScanRule, string>((IScanRule rule) => rule.GetType().FullName, StringComparer.Ordinal) select $"{rule.RuleId}|{rule.Severity}|{rule.GetType().FullName}"); return HashUtility.CalculateBytesHash(Encoding.UTF8.GetBytes(string.Join("\n", list))); } private static StringComparer GetPathComparer() { return (RuntimeInformationHelper.IsWindows || RuntimeInformationHelper.IsMacOs) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; } } public class DisabledPluginInfo { public string OriginalPath { get; } public string DisabledPath { get; } public string FileHash { get; } public ThreatVerdictInfo ThreatVerdict { get; } public ScanStatusInfo ScanStatus { get; } public DisabledPluginInfo(string originalPath, string disabledPath, string fileHash, ThreatVerdictInfo threatVerdict, ScanStatusInfo scanStatus) { OriginalPath = originalPath; DisabledPath = disabledPath; FileHash = fileHash; ThreatVerdict = threatVerdict ?? new ThreatVerdictInfo(); ScanStatus = scanStatus ?? new ScanStatusInfo(); } } public abstract class PluginDisablerBase { protected readonly IScanLogger Logger; protected readonly MLVScanConfig Config; protected virtual string GetDisabledExtension() { return ".disabled"; } protected PluginDisablerBase(IScanLogger logger, MLVScanConfig config) { Logger = logger ?? throw new ArgumentNullException("logger"); Config = config ?? throw new ArgumentNullException("config"); } protected virtual string GetDisabledPath(string originalPath) { return Path.ChangeExtension(originalPath, GetDisabledExtension()); } protected virtual void OnPluginDisabled(string originalPath, string disabledPath, string hash) { } public List<DisabledPluginInfo> DisableSuspiciousPlugins(Dictionary<string, ScannedPluginResult> scanResults, bool forceDisable = false) { if (!forceDisable && !Config.EnableAutoDisable) { Logger.Info("Automatic disabling is turned off in configuration"); return new List<DisabledPluginInfo>(); } List<DisabledPluginInfo> list = new List<DisabledPluginInfo>(); foreach (var (text2, scannedPluginResult2) in scanResults) { if (!ScanResultFacts.RequiresAttention(scannedPluginResult2)) { continue; } ThreatVerdictInfo threatVerdict = scannedPluginResult2?.ThreatVerdict ?? new ThreatVerdictInfo(); ScanStatusInfo scanStatus = scannedPluginResult2?.ScanStatus ?? new ScanStatusInfo(); if (!ShouldDisable(scannedPluginResult2)) { Logger.Info("Plugin " + Path.GetFileName(text2) + " requires attention (" + GetOutcomeTitle(scannedPluginResult2) + ") but blocking for this outcome is disabled in configuration"); continue; } try { DisabledPluginInfo disabledPluginInfo = DisablePlugin(text2, threatVerdict, scanStatus); if (disabledPluginInfo != null) { list.Add(disabledPluginInfo); OnPluginDisabled(disabledPluginInfo.OriginalPath, disabledPluginInfo.DisabledPath, disabledPluginInfo.FileHash); } } catch (Exception ex) { Logger.Error("Failed to disable " + Path.GetFileName(text2) + ": " + ex.Message); } } return list; } private bool ShouldDisable(ScannedPluginResult scanResult) { ThreatVerdictInfo threatVerdictInfo = scanResult?.ThreatVerdict ?? new ThreatVerdictInfo(); ThreatVerdictKind kind = threatVerdictInfo.Kind; if (1 == 0) { } bool result = kind switch { ThreatVerdictKind.KnownMaliciousSample => Config.BlockKnownThreats, ThreatVerdictKind.KnownMalwareFamily => Config.BlockKnownThreats, ThreatVerdictKind.Suspicious => Config.BlockSuspicious, _ => ShouldDisable(scanResult?.ScanStatus ?? new ScanStatusInfo()), }; if (1 == 0) { } return result; } private bool ShouldDisable(ScanStatusInfo scanStatus) { ScanStatusKind scanStatusKind = scanStatus?.Kind ?? ScanStatusKind.Complete; if (1 == 0) { } bool result = scanStatusKind == ScanStatusKind.RequiresReview && Config.BlockIncompleteScans; if (1 == 0) { } return result; } private static string GetOutcomeTitle(ScannedPluginResult scanResult) { if (ScanResultFacts.HasThreatVerdict(scanResult)) { return scanResult.ThreatVerdict.Title; } if (ScanResultFacts.RequiresManualReview(scanResult)) { return scanResult.ScanStatus.Title; } return "No action required"; } protected virtual DisabledPluginInfo DisablePlugin(string pluginPath, ThreatVerdictInfo threatVerdict, ScanStatusInfo scanStatus) { string fileHash = HashUtility.CalculateFileHash(pluginPath); string disabledPath = GetDisabledPath(pluginPath); if (File.Exists(disabledPath)) { File.Delete(disabledPath); } File.Move(pluginPath, disabledPath); Logger.Warning("BLOCKED: " + Path.GetFileName(pluginPath)); return new DisabledPluginInfo(pluginPath, disabledPath, fileHash, threatVerdict, scanStatus); } } public class DeveloperReportGenerator { private readonly IScanLogger _logger; public DeveloperReportGenerator(IScanLogger logger) { _logger = logger ?? throw new ArgumentNullException("logger"); } public void GenerateConsoleReport(string modName, List<ScanFinding> findings) { if (