using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Mono.Cecil; using Sandbox; namespace Sandbox.WhitelistGen; /// /// Expands an access-control whitelist wildcard pattern into the full, explicit list of every type /// and method it would match - in the exact "touch name" format the scanner compares against (via /// ). Paste the output into a Rules file and comment out the entries you /// don't want to allow, so the whitelist stays explicit (no wildcards) and new members can't sneak in. /// internal static class Program { static int Main( string[] args ) { if ( args.Length < 1 ) { Console.Error.WriteLine( "Usage: WhitelistGen \"/\" [--dll ] [--all]" ); Console.Error.WriteLine( " e.g. WhitelistGen \"System.Private.CoreLib/System.Memory*\"" ); Console.Error.WriteLine( " --dll load a specific assembly instead of resolving from the runtime dir" ); Console.Error.WriteLine( " --all include non-public types/methods (default: public + protected only)" ); return 1; } var pattern = args[0]; string dllOverride = null; var includeNonPublic = false; for ( var i = 1; i < args.Length; i++ ) { if ( args[i] == "--dll" && i + 1 < args.Length ) dllOverride = args[++i]; else if ( args[i] == "--all" ) includeNonPublic = true; } var slash = pattern.IndexOf( '/' ); if ( slash <= 0 ) { Console.Error.WriteLine( "Pattern must be '/', e.g. System.Private.CoreLib/System.Memory*" ); return 1; } var asmName = pattern.Substring( 0, slash ); var runtimeDir = RuntimeEnvironment.GetRuntimeDirectory(); var dllPath = dllOverride ?? Path.Combine( runtimeDir, asmName + ".dll" ); if ( !File.Exists( dllPath ) ) { Console.Error.WriteLine( $"Couldn't find '{dllPath}'. Pass --dll to point at the assembly." ); return 1; } // Same wildcard -> regex translation as AccessRules.AddRule. var regex = new Regex( "^" + Regex.Escape( pattern ).Replace( "\\*", ".*" ) + "$", RegexOptions.Compiled ); var resolver = new DefaultAssemblyResolver(); resolver.AddSearchDirectory( runtimeDir ); if ( dllOverride != null ) resolver.AddSearchDirectory( Path.GetDirectoryName( Path.GetFullPath( dllPath ) ) ); using var assembly = AssemblyDefinition.ReadAssembly( dllPath, new ReaderParameters { AssemblyResolver = resolver } ); var lines = new SortedSet( StringComparer.Ordinal ); foreach ( var type in AllTypes( assembly.MainModule.Types ) ) { if ( !includeNonPublic && !IsTypeVisible( type ) ) continue; var typeLine = AccessSignature.Type( type ); if ( regex.IsMatch( typeLine ) ) lines.Add( typeLine ); foreach ( var method in type.Methods ) { if ( !includeNonPublic && !IsMethodVisible( method ) ) continue; var methodLine = AccessSignature.Method( method ); if ( regex.IsMatch( methodLine ) ) lines.Add( methodLine ); } } foreach ( var line in lines ) Console.WriteLine( $"\t\t\t\"{line}\"," ); Console.Error.WriteLine( $"// {lines.Count} entries matched \"{pattern}\"" ); return 0; } static IEnumerable AllTypes( IEnumerable types ) { foreach ( var type in types ) { yield return type; if ( type.HasNestedTypes ) { foreach ( var nested in AllTypes( type.NestedTypes ) ) yield return nested; } } } // Sandboxed code can only reference public (and, for types it derives from, protected) members, // so those are the only ones the whitelist ever needs to authorize. static bool IsTypeVisible( TypeDefinition t ) => t.IsPublic || t.IsNestedPublic || t.IsNestedFamily || t.IsNestedFamilyOrAssembly; static bool IsMethodVisible( MethodDefinition m ) => m.IsPublic || m.IsFamily || m.IsFamilyOrAssembly; }