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);