using Microsoft.CodeAnalysis; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// Factory for the compile-time sandbox every user script is built against. /// Implements Phase 7 plan decision #6 (read-only sandbox) by whitelisting only the /// assemblies + namespaces the script API needs; no System.IO, no /// System.Net, no System.Diagnostics.Process, no /// System.Reflection. Attempts to reference those types in a script fail at /// compile with a compiler error that points at the exact span — the operator sees /// the rejection before publish, not at evaluation. /// /// /// /// Roslyn would otherwise pull in every type in the BCL transitively via /// mscorlib / System.Runtime — this class overrides that with an /// explicit minimal allow-list. The list is the same regardless of whether /// uses the legacy /// CSharpScript path or the collectible-AssemblyLoadContext path /// (Core.Scripting-008): both go through . /// /// /// Namespaces pre-imported so scripts don't have to write using clauses: /// System, System.Math-style statics are reachable via /// , and ZB.MOM.WW.OtOpcUa.Core.Abstractions so scripts /// can name directly. /// /// /// The sandbox cannot prevent a script from allocating unbounded memory or /// spinning in a tight loop — those are budget concerns, handled by the /// per-evaluation timeout (Stream A.4) + the test-harness (Stream F.4) that lets /// operators preview output before publishing. /// /// public static class ScriptSandbox { /// /// Build the sandbox configuration used for every virtual-tag / alarm script. /// is the concrete /// subclass the script's ctx will be of — the compiler uses its assembly /// to resolve ctx.GetTag(...) calls. /// /// The concrete script context type to use for compilation. public static SandboxConfig Build(Type contextType) { if (contextType is null) throw new ArgumentNullException(nameof(contextType)); if (!typeof(ScriptContext).IsAssignableFrom(contextType)) throw new ArgumentException( $"Script context type must derive from {nameof(ScriptContext)}", nameof(contextType)); // OtOpcUa-owned assemblies — pinned by typeof(...) so they survive a rename. var pinnedAssemblies = new HashSet { // Core.Abstractions — DataValueSnapshot + DriverDataType so scripts can name // the types they receive from ctx.GetTag. typeof(DataValueSnapshot).Assembly, // Core.Scripting.Abstractions — ScriptContext base class + Deadband static. // Intentionally NOT Core.Scripting (which holds ScriptEvaluator/ScriptSandbox + Roslyn): // keeping it out of the sandbox pin keeps Roslyn out of the globalsType assembly closure. typeof(ScriptContext).Assembly, // Serilog.ILogger — script-side logger type. typeof(Serilog.ILogger).Assembly, // Concrete context type's assembly — production contexts subclass // ScriptContext in Core.VirtualTags / Core.ScriptedAlarms; tests use their // own subclass. The globals wrapper is generic on this type so Roslyn must // be able to resolve it during compilation. contextType.Assembly, }; // BCL references. We list the trusted-platform-assemblies set restricted to // System.* and netstandard so the synthesized wrapper can reference every BCL // type by FQN — including the ones we forbid (HttpClient, File, Process, // Registry, etc.). Letting those types resolve at compile is intentional: the // hard security gate is ForbiddenTypeAnalyzer in step 3 of the compile pipeline // (Core.Scripting-001 / -002 established the analyzer must be the sole gate // because type forwarding makes any references-list-only restriction porous). // The references list now serves only as scoping hygiene — out-of-band BCL // surface (operator-authored hosting helpers, third-party packages, app code) // is not on the list and stays unreachable. var references = new List(); foreach (var asm in pinnedAssemblies) references.Add(MetadataReference.CreateFromFile(asm.Location)); foreach (var path in EnumerateBclAssemblyPaths()) references.Add(MetadataReference.CreateFromFile(path)); var imports = new[] { "System", "System.Linq", "ZB.MOM.WW.OtOpcUa.Core.Abstractions", "ZB.MOM.WW.OtOpcUa.Core.Scripting", }; return new SandboxConfig(references, imports); } private static IEnumerable EnumerateBclAssemblyPaths() { // The .NET host advertises the resolved runtime-shared-framework + BCL DLL set // via the TRUSTED_PLATFORM_ASSEMBLIES AppContext data slot. This is what the // ALC fallback uses when resolving assemblies, so anything in here is already // loadable by the host process. We restrict to System.* and netstandard to keep // the script's reachable surface to the BCL — anything else (Microsoft.*, // application code, third-party packages happening to be in the runtime store) // would expand the analyzer's deny-list job unnecessarily. var raw = (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"); if (string.IsNullOrEmpty(raw)) yield break; var separator = OperatingSystem.IsWindows() ? ';' : ':'; foreach (var path in raw.Split(separator, StringSplitOptions.RemoveEmptyEntries)) { var name = System.IO.Path.GetFileName(path); if (name.StartsWith("System.", StringComparison.Ordinal) || string.Equals(name, "netstandard.dll", StringComparison.Ordinal) || string.Equals(name, "mscorlib.dll", StringComparison.Ordinal) || // Microsoft.Win32.Registry isn't a System.* DLL but the analyzer's // Microsoft.Win32 deny-list relies on the type being resolvable so it // can identify + reject it (Core.Scripting-001 / -002). Add the one // DLL we need rather than broadening to Microsoft.* (which would also // pull in compilers, build tooling, etc.). string.Equals(name, "Microsoft.Win32.Registry.dll", StringComparison.Ordinal)) { yield return path; } } } } /// /// Compile-time sandbox configuration. Returned by ; /// consumed by 's manual /// CSharpCompilation path. /// /// /// Metadata references (allow-listed assemblies) the script compilation is built /// against. Anything not in this set is unresolved at compile, which is the sandbox's /// first line of defense — is the second. /// /// /// Namespaces pre-imported into the wrapper compilation as using directives /// so scripts can write Math.Abs rather than System.Math.Abs. /// public sealed record SandboxConfig( IReadOnlyList References, IReadOnlyList Imports);