12 KiB
Component: Script Analysis
Purpose
The Script Analysis component is the single authoritative source of truth for the ScadaBridge script trust model. It provides a unified forbidden-API deny-list, a fused semantic and syntactic trust validator, a Roslyn compile wrapper, and compile-only globals stubs used by the design-time deploy gate. All four call sites that enforce the script trust boundary — Template Engine, Site Runtime, Inbound API, and Central UI — delegate to this component rather than maintaining their own divergent implementations.
Location
src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/
Referenced by: Template Engine, Site Runtime, Inbound API, Central UI.
Responsibilities
- Define the canonical forbidden-API deny-list (
ScriptTrustPolicy) as the single source of truth for all trust enforcement decisions across the system. - Provide an authoritative forbidden-API verdict (
ScriptTrustValidator.FindViolations) that fuses semantic symbol resolution with syntactic reflection-gateway hardening. - Wrap Roslyn
CSharpScriptcompilation (RoslynScriptCompiler) so callers share one implementation of compile + diagnostics extraction. - Provide compile-only globals stubs (
ScriptCompileSurface,TriggerCompileSurface) that mirror the real execution-time globals member-for-member, allowing the design-time deploy gate to do a real type-checking compile without depending on the execution-time projects.
Requirements
REQ-SA-1: Trust Policy (ScriptTrustPolicy)
ScriptTrustPolicy is a static class (or record) that publishes the complete, authoritative forbidden-API policy used at every call site.
Forbidden scopes
The following namespace/type prefixes are forbidden in all scripts:
| Scope | Rationale |
|---|---|
System.IO |
File system access — forbidden entirely |
System.Diagnostics.Process |
Process spawning — forbidden; Stopwatch, Debug, Activity, and other System.Diagnostics types are allowed |
System.Threading |
Raw thread manipulation — forbidden, with the exceptions below |
System.Reflection |
Reflection — forbidden entirely |
System.Net |
Raw network access — forbidden entirely (scripts must use ExternalSystem.Call) |
System.Runtime.InteropServices |
Native interop — forbidden entirely |
Microsoft.Win32 |
Win32 API access — forbidden entirely |
Allowed exceptions within forbidden scopes
The following types are explicitly allowed despite falling within a forbidden namespace:
System.Threading.Tasks(and all subtypes) — async/await supportSystem.Threading.CancellationToken— cooperative cancellationSystem.Threading.CancellationTokenSource— cooperative cancellation
The scoping rationale: System.Diagnostics.Process is the dangerous type (spawns processes); Stopwatch, Debug, and Activity are harmless diagnostic utilities. Forbidding the whole System.Diagnostics namespace, as some earlier call sites did, was overly broad.
Reflection-gateway members
The following member names are blocked regardless of the receiver type, to prevent reflection-based bypasses such as typeof(x).Assembly.GetType("System.IO.File"):
GetType, GetTypeInfo, Assembly, Module, CreateInstance, InvokeMember, GetMethod, GetMethods, GetConstructor, GetConstructors, GetField, GetFields, GetProperty, GetProperties, GetMember, GetMembers, GetRuntimeMethod, GetRuntimeMethods, MethodHandle, TypeHandle.
Forbidden identifiers
The identifiers dynamic and Activator are forbidden at any scope, as they provide type-system escape hatches equivalent to reflection.
Default references and imports
ScriptTrustPolicy also publishes DefaultReferences (the canonical set of trusted-platform MetadataReference entries used when constructing the Roslyn script compilation context) and DefaultImports (the default using directives injected into every script). These are consumed by RoslynScriptCompiler and by the compile-only surfaces below.
REQ-SA-2: Trust Validator (ScriptTrustValidator)
ScriptTrustValidator.FindViolations(string code, IEnumerable<MetadataReference>? extraReferences = null) is the authoritative forbidden-API gate. It returns a list of violation messages; an empty list means the script is clean.
Two-pass design
Pass 1 — semantic symbol resolution (adapted from Site Runtime)
- Builds a Roslyn compilation using the full trusted-platform reference set from
ScriptTrustPolicy.DefaultReferences(plus anyextraReferences). - For each identifier in the syntax tree, resolves the underlying symbol to its fully qualified containing namespace and type name.
- Flags any symbol whose containing namespace or type matches a forbidden scope in
ScriptTrustPolicy.ForbiddenScopes, takingAllowedExceptionsinto account. - Correctly handles aliases (
using X = System.IO.File),using static, andglobal::prefixes — the resolved symbol is checked, not the spelling. - Because the full reference set is loaded, this pass also catches a forbidden type accessed inside an otherwise-allowed namespace (e.g., bare
Processafterusing System.Diagnostics;).
Pass 2 — syntactic reflection-gateway and identifier hardening (adapted from Inbound API)
- Walks the syntax tree for member-access expressions and simple name references.
- Flags any member name found in
ScriptTrustPolicy.ReflectionGatewayMembers, regardless of receiver type. - Flags any identifier token found in
ScriptTrustPolicy.ForbiddenIdentifiers(dynamic,Activator).
Violations from both passes are merged and deduplicated before being returned.
Design notes
FindViolationsneeds no globals type; it operates solely on the script text and the trusted-platform reference set.- The function is stateless and thread-safe — callers share a single instance or call it as a static method.
- A violation does not abort compilation; callers may choose to report violations and continue, or treat any violation as a hard reject.
REQ-SA-3: Roslyn Compile Wrapper (RoslynScriptCompiler)
RoslynScriptCompiler wraps CSharpScript to give callers a single implementation of compile + diagnostics extraction.
Compile(string code, Type? globalsType = null, IEnumerable<MetadataReference>? extraReferences = null, IEnumerable<string>? extraImports = null)
- Creates a
CSharpScriptwith the given code,globalsType, references (defaults fromScriptTrustPolicy.DefaultReferencesplusextraReferences), and imports (defaults fromScriptTrustPolicy.DefaultImportsplusextraImports). - Calls
.Compile()and returns the resultingDiagnostic[]filtered to errors and warnings. - Each caller passes its own
globalsType—ScriptCompileSurfacefor the design-time deploy gate, the realScriptGlobalsfor Site Runtime execution,nullfor pure syntax checks.
ParseDiagnostics(string code)
- Parses the script text using Roslyn's
CSharpSyntaxTree.ParseTextand returns syntax-level diagnostics (errors and warnings). - No compilation is performed — useful for fast syntax checks where no globals type is available.
REQ-SA-4: Compile-Only Globals Stubs
The deploy gate in Template Engine must do a real type-checking compile (to catch undefined-symbol and type errors) but cannot depend on the execution-time projects (Site Runtime, Inbound API) that own the real globals. Two compile-only stubs solve this:
ScriptCompileSurface
Mirrors ScriptGlobals member-for-member (same public property names, same return types, same method signatures) but with no implementation bodies. All properties return default and all methods return default or Task.CompletedTask. Depends only on Commons.Types — no Akka.NET, no external system clients.
Used by the Template Engine deploy gate:
var errors = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
This allows the compile to bind Attributes["name"], Notify.To("x").Send(...), ExternalSystem.Call(...), and similar API calls against real types, catching undefined-symbol and type-mismatch errors before deployment.
TriggerCompileSurface
Mirrors TriggerExpressionGlobals in the same way. Used by ValidationService.CheckExpressionSyntax in the Template Engine for conditional and expression trigger validation.
Parity guard
A reflection-based parity test in SiteRuntime.Tests compares the public member names on ScriptCompileSurface against ScriptGlobals (and TriggerCompileSurface against TriggerExpressionGlobals). Any drift between the stub and the real globals causes this test to fail, ensuring the stubs cannot silently fall out of sync.
REQ-SA-5: Consumer Delegation
All four call sites that previously maintained their own script trust enforcement now delegate to this component. The key behavioral changes per consumer:
| Consumer | Before | After |
|---|---|---|
Template Engine ScriptCompiler.TryCompile |
Substring scan + brace-balance (advisory, bypassable) | FindViolations + real Compile against ScriptCompileSurface — authoritative gate |
Template Engine ValidationService.CheckExpressionSyntax |
Regex / brace scan | FindViolations + Compile against TriggerCompileSurface |
Site Runtime ScriptCompilationService.ValidateTrustModel |
Semantic resolver, no reflection-gateway hardening | Delegates to FindViolations; retains CSharpScript.Compile against real ScriptGlobals for execution |
Inbound API ForbiddenApiChecker.FindViolations |
Syntactic walker, forbade all System.Diagnostics |
Thin shim delegating to ScriptTrustValidator.FindViolations; System.Diagnostics loosened to .Process-only |
Central UI ScriptAnalysisService |
Semantic + full compile, lenient threading | Delegates forbidden-API verdict and sources editor-marker deny-list from ScriptTrustPolicy; retains Test-Run execution host |
The static enforcement is defence-in-depth, not a true runtime sandbox. Scripts execute in-process; the denied API list prevents obvious escapes at compile time but does not provide the isolation guarantees of an out-of-process sandbox or a restricted AssemblyLoadContext. This caveat applies to all consumers.
Dependencies
- Commons: Shared types referenced by
ScriptCompileSurfaceandTriggerCompileSurface(e.g.,DataType, attribute access types). - Microsoft.CodeAnalysis.CSharp.Scripting: Roslyn scripting APIs used by
RoslynScriptCompilerandScriptTrustValidator. - Microsoft.CodeAnalysis.CSharp.Workspaces: Roslyn workspace/syntax APIs used by
ScriptTrustValidator.
No dependency on Akka.NET, ASP.NET Core, Entity Framework, or any other ScadaBridge component above Commons.
Interactions
- Template Engine (#1): Consumes
ScriptTrustValidator.FindViolations,RoslynScriptCompiler.Compile,ScriptCompileSurface, andTriggerCompileSurfacein the design-time deploy gate (ScriptCompiler.TryCompile) and expression syntax validator (ValidationService.CheckExpressionSyntax). - Site Runtime (#3):
ScriptCompilationService.ValidateTrustModeldelegates the trust verdict toScriptTrustValidator.FindViolations; retains its ownCSharpScript.Compileagainst the realScriptGlobalsfor execution-time compilation. - Inbound API (#14):
ForbiddenApiChecker.FindViolationsis a thin shim overScriptTrustValidator.FindViolations. - Central UI (#9):
ScriptAnalysisServicedelegates the run-gate forbidden-API verdict and sources the editor-marker deny-list fromScriptTrustPolicy; retains the Test-Run execution host (SandboxScriptHost).