using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.ScriptAnalysis; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// WP-19: Script Trust Model — compiles C# scripts using Roslyn with restricted API access. /// The forbidden-API verdict is delegated to the shared authoritative /// (M3.1 consolidation); this service keeps the real /// execution-path compile of the script against / /// . /// public class ScriptCompilationService { private readonly ILogger _logger; /// Initializes a new instance of the ScriptCompilationService class. /// Logger instance. public ScriptCompilationService(ILogger logger) { _logger = logger; } /// /// SiteRuntime-011: validates that the script does not reference forbidden APIs. /// /// As of the M3.1 script-analysis consolidation this delegates to the shared /// authoritative , /// which is the same Roslyn semantic-symbol analysis this service previously hosted /// plus reflection-gateway / dynamic / Activator hardening ported from /// the InboundAPI checker. The shared validator is the single source of truth for the /// forbidden-API deny-list; SiteRuntime retains only the real execution-path compile /// in . /// /// Returns a list of violation messages, empty if clean. /// /// The script code to validate. /// A list of trust-model violation messages; empty if the script is clean. public IReadOnlyList ValidateTrustModel(string code) => ScriptTrustValidator.FindViolations(code); /// /// Assemblies referenced by compiled scripts, used to build the Roslyn scripting /// options for the real execution-path compile. /// private static readonly System.Reflection.Assembly[] ScriptAssemblies = [ typeof(object).Assembly, typeof(Enumerable).Assembly, typeof(Math).Assembly, typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly, typeof(Commons.Types.DynamicJsonElement).Assembly ]; /// /// Shared Roslyn scripting options (references + imports) used by both full /// script compilation and trigger-expression compilation. /// private static ScriptOptions BuildScriptOptions() => ScriptOptions.Default .WithReferences(ScriptAssemblies) .WithImports( "System", "System.Collections.Generic", "System.Linq", "System.Threading.Tasks"); /// /// Compiles a script into a reusable delegate that takes a ScriptRuntimeContext /// and parameters dictionary, and returns an object? result. /// /// The name of the script. /// The script code to compile. /// A containing the compiled script on success, or error messages on failure. public ScriptCompilationResult Compile(string scriptName, string code) => CompileCore(scriptName, code, typeof(ScriptGlobals)); /// /// Compiles a bare C# boolean trigger expression against the restricted /// read-only . The expression is a /// trailing expression (no return); Roslyn scripting yields its /// value, which the caller coerces to bool. Reuses the same script /// options and forbidden-API trust validation as . /// /// The name of the trigger expression. /// The trigger expression to compile. /// A containing the compiled expression on success, or error messages on failure. public ScriptCompilationResult CompileTriggerExpression(string name, string expression) => CompileCore(name, expression, typeof(TriggerExpressionGlobals)); /// /// Shared compilation path: validates the trust model, builds the script /// against the given globals type, and returns the compiled result. /// private ScriptCompilationResult CompileCore(string name, string code, Type globalsType) { // Validate trust model var violations = ValidateTrustModel(code); if (violations.Count > 0) { _logger.LogWarning( "Script {Script} failed trust validation: {Violations}", name, string.Join("; ", violations)); return ScriptCompilationResult.Failed(violations); } try { var script = CSharpScript.Create( code, BuildScriptOptions(), globalsType: globalsType); var diagnostics = script.Compile(); var errors = diagnostics .Where(d => d.Severity == DiagnosticSeverity.Error) .Select(d => d.GetMessage()) .ToList(); if (errors.Count > 0) { _logger.LogWarning( "Script {Script} compilation failed: {Errors}", name, string.Join("; ", errors)); return ScriptCompilationResult.Failed(errors); } _logger.LogDebug("Script {Script} compiled successfully", name); return ScriptCompilationResult.Succeeded(script); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error compiling script {Script}", name); return ScriptCompilationResult.Failed([$"Compilation exception: {ex.Message}"]); } } } /// /// Result of script compilation, containing either the compiled script or error messages. /// public class ScriptCompilationResult { /// Indicates whether compilation succeeded. public bool IsSuccess { get; } /// The compiled script, or null if compilation failed. public Script? CompiledScript { get; } /// List of error messages, empty if compilation succeeded. public IReadOnlyList Errors { get; } private ScriptCompilationResult(bool success, Script? script, IReadOnlyList errors) { IsSuccess = success; CompiledScript = script; Errors = errors; } /// Creates a successful compilation result. /// The compiled script. /// A with set to true and the compiled script attached. public static ScriptCompilationResult Succeeded(Script script) => new(true, script, []); /// Creates a failed compilation result. /// List of error messages. /// A with set to false and the provided error messages. public static ScriptCompilationResult Failed(IReadOnlyList errors) => new(false, null, errors); } /// /// Global variables available to compiled scripts. The ScriptRuntimeContext is injected /// as the "Instance" global, and parameters are available via "Parameters". /// public class ScriptGlobals { /// The script runtime context providing access to instance state. public ScriptRuntimeContext Instance { get; set; } = null!; /// Script parameters passed by the caller. public ScriptParameters Parameters { get; set; } = new ScriptParameters(); /// Cancellation token for script execution. public CancellationToken CancellationToken { get; set; } /// /// Alarm context when this script is invoked as an on-trigger handler. /// Null for instance scripts, shared scripts, and inbound-API-routed /// scripts. Lets on-trigger scripts read the firing alarm's Name, Level /// (HiLo only), Priority, and per-band Message to branch routing logic. /// public Commons.Types.Scripts.AlarmContext? Alarm { get; set; } /// /// Where this script sits in the composition tree. Defaults to root for /// scripts on top-level templates; a flattened composed script gets /// SelfPath = "TempSensor" (etc.) and a ParentPath set to one level up. /// public Commons.Types.Scripts.ScriptScope Scope { get; set; } = Commons.Types.Scripts.ScriptScope.Root; /// /// Top-level ExternalSystem access for scripts (delegates to Instance.ExternalSystem). /// Usage: ExternalSystem.Call("systemName", "methodName", params) /// public ScriptRuntimeContext.ExternalSystemHelper ExternalSystem => Instance.ExternalSystem; /// /// Top-level Database access for scripts (delegates to Instance.Database). /// Usage: Database.Connection("name") or Database.CachedWrite("name", "sql", params) /// public ScriptRuntimeContext.DatabaseHelper Database => Instance.Database; /// /// Top-level Notify access for scripts (delegates to Instance.Notify). /// Usage: Notify.To("listName").Send("subject", "message") /// public ScriptRuntimeContext.NotifyHelper Notify => Instance.Notify; /// /// Top-level Scripts access for shared script calls (delegates to Instance.Scripts). /// Usage: Scripts.CallShared("scriptName", params) /// public ScriptRuntimeContext.ScriptCallHelper Scripts => Instance.Scripts; /// /// Read/write the current template's attributes by name. Resolves to the /// canonical name for the script's scope, so a script on a composed /// TempSensor reads its own Temperature via Attributes["Temperature"]. /// public AttributeAccessor Attributes => new(Instance, Scope.SelfPath); /// /// Indexed access to child compositions. /// Children["TempSensor"].Attributes["Temperature"] reads the /// composed child's attribute. Children["TempSensor"].CallScript("Sample") /// invokes a script on the child. /// public ChildrenAccessor Children => new(Instance, Scope.SelfPath); /// /// Parent composition (null when this script is on a root-level template). /// Parent.Attributes["SpeedRPM"] reaches the parent's attribute; /// Parent.CallScript("Trip") invokes a parent script. /// public CompositionAccessor? Parent => Scope.ParentPath == null ? null : new CompositionAccessor(Instance, Scope.ParentPath); }