using System.IO; using System.Reflection; using Microsoft.CodeAnalysis; using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis; /// /// M3.1: the single authoritative source of truth for the ScadaBridge script /// trust model. Previously the forbidden-API deny-list, allowed exceptions, /// reflection-gateway member names, default metadata references, and default /// imports were duplicated (and disagreed) across four call sites — the /// SiteRuntime ScriptCompilationService, the InboundAPI /// ForbiddenApiChecker, and the design-time deploy gate. This class /// fuses them into one collection set that /// and consume; the four consumers delegate /// here in later tasks (M3.2–M3.5). /// /// /// The deny-list is intentionally the UNION of the two existing /// implementations — it forbids System.Diagnostics.Process (not all of /// System.Diagnostics, so Stopwatch stays allowed), all of /// System.Net, all of System.Threading except Tasks / /// CancellationToken(Source), plus System.Reflection, /// System.Runtime.InteropServices, and Microsoft.Win32. /// /// public static class ScriptTrustPolicy { /// /// Forbidden API roots. Each entry is matched as a prefix against the /// resolved symbol's containing namespace and fully-qualified containing /// type — an entry may name a whole namespace ("System.IO") or a single /// type ("System.Diagnostics.Process"). /// public static readonly string[] ForbiddenScopes = [ "System.IO", "System.Diagnostics.Process", "System.Threading", "System.Reflection", "System.Net", "System.Runtime.InteropServices", "Microsoft.Win32", ]; /// /// Specific namespaces/types allowed even though they sit under a forbidden /// root. async/await and cancellation tokens are OK despite /// System.Threading being blocked. /// public static readonly string[] AllowedExceptions = [ "System.Threading.Tasks", "System.Threading.CancellationToken", "System.Threading.CancellationTokenSource", ]; /// /// Member names that are reflection gateways. Reaching any of these — even /// off a permitted type such as typeof(string) — lets a script /// escape the namespace deny-list (obtain an arbitrary Type, load an /// assembly, late-bind a method). They are rejected regardless of the /// receiver expression. /// public static readonly HashSet ReflectionGatewayMembers = new(StringComparer.Ordinal) { "GetType", "GetTypeInfo", "Assembly", "Module", "CreateInstance", "InvokeMember", "GetMethod", "GetMethods", "GetConstructor", "GetConstructors", "GetField", "GetFields", "GetProperty", "GetProperties", "GetMember", "GetMembers", "GetRuntimeMethod", "GetRuntimeMethods", "MethodHandle", "TypeHandle", }; /// /// Bare identifiers that are forbidden outright. dynamic widens /// late-bound member access the static walker cannot see through; /// Activator has no non-reflection use. /// public static readonly HashSet ForbiddenIdentifiers = new(StringComparer.Ordinal) { "dynamic", "Activator", }; /// /// Assemblies referenced by compiled scripts. Shared between the Roslyn /// scripting options and the semantic-analysis compilation built for trust /// validation, so the validator resolves symbols against exactly the same /// metadata the script is compiled against. /// public static readonly IReadOnlyList DefaultAssemblies = [ typeof(object).Assembly, typeof(System.Linq.Enumerable).Assembly, typeof(System.Math).Assembly, typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly, typeof(DynamicJsonElement).Assembly, ]; /// /// Metadata references for the trust-validation semantic compilation and /// the design-time script compilation. /// public static readonly IReadOnlyList DefaultReferences = DefaultAssemblies .Select(a => (MetadataReference)MetadataReference.CreateFromFile(a.Location)) .ToList(); /// /// The full trusted-platform reference set used ONLY by /// 's semantic analysis — NOT by /// . Unlike /// (the minimal, runtime-fidelity set used to decide script validity, /// which must mirror exactly what the site runtime compiles against), the /// trust validator references the entire framework so that EVERY type a /// script names resolves to a real symbol and is judged by its true /// namespace. Without this, a forbidden TYPE that sits inside an ALLOWED /// namespace and is reached as a bare identifier — the only such case in the /// policy being System.Diagnostics.Process via /// using System.Diagnostics; — would not resolve against a minimal /// reference set and would slip past the semantic pass (still blocked /// downstream as an undefined-symbol compile error, but with a misleading /// message). Referencing the full framework lets the validator flag it /// authoritatively as a forbidden API. Enriching the analysis reference set /// can only IMPROVE detection — the verdict is by namespace/type, so more /// resolvable symbols means more correct verdicts, never a false allow. /// public static readonly IReadOnlyList AnalysisReferences = BuildAnalysisReferences(); private static IReadOnlyList BuildAnalysisReferences() { var byPath = new Dictionary(StringComparer.OrdinalIgnoreCase); // Trusted platform assemblies = the full framework reference set the host // started with; lets the semantic pass resolve any BCL type. if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa) { foreach (var path in tpa.Split(Path.PathSeparator)) { if (path.Length == 0 || !path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || byPath.ContainsKey(path) || !File.Exists(path)) { continue; } try { byPath[path] = MetadataReference.CreateFromFile(path); } catch { /* skip an unreadable assembly rather than fail validation */ } } } // Ensure app assemblies the script API surface needs are present even if // not in the TPA list (e.g. Commons / DynamicJsonElement). foreach (var asm in DefaultAssemblies) { var loc = asm.Location; if (loc.Length == 0 || byPath.ContainsKey(loc) || !File.Exists(loc)) continue; try { byPath[loc] = MetadataReference.CreateFromFile(loc); } catch { /* ignore */ } } // Fallback to the minimal set if the TPA list was unavailable (e.g. a // single-file/AOT host) so validation still functions. return byPath.Count > 0 ? byPath.Values.ToList() : DefaultReferences; } /// /// Default namespace imports made available to compiled scripts. /// public static readonly string[] DefaultImports = [ "System", "System.Collections.Generic", "System.Linq", "System.Threading.Tasks", ]; }