using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// Factory for the every user script is compiled 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's default references mscorlib / /// System.Runtime transitively which pulls in every type in the BCL — this /// class overrides that with an explicit minimal allow-list. /// /// /// 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 used for every virtual-tag / alarm /// script. is the concrete /// subclass the globals will be of — the compiler /// uses its type to resolve ctx.GetTag(...) calls. /// public static ScriptOptions 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)); // Allow-listed assemblies — each explicitly chosen. Adding here is a // plan-level decision; do not expand casually. HashSet so adding the // contextType's assembly is idempotent when it happens to be Core.Scripting // already. var allowedAssemblies = new HashSet { // System.Private.CoreLib — primitives (int, double, bool, string, DateTime, // TimeSpan, Math, Convert, nullable). Can't practically script without it. typeof(object).Assembly, // System.Linq — IEnumerable extensions (Where / Select / Sum / Average / etc.). typeof(System.Linq.Enumerable).Assembly, // Core.Abstractions — DataValueSnapshot + DriverDataType so scripts can name // the types they receive from ctx.GetTag. typeof(DataValueSnapshot).Assembly, // Core.Scripting itself — ScriptContext base class + Deadband static. 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, }; var allowedImports = new[] { "System", "System.Linq", "ZB.MOM.WW.OtOpcUa.Core.Abstractions", "ZB.MOM.WW.OtOpcUa.Core.Scripting", }; return ScriptOptions.Default .WithReferences(allowedAssemblies) .WithImports(allowedImports); } }