Files
ScadaBridge/docs/components/ScriptAnalysis.md
T

9.9 KiB

Script Analysis

Script Analysis (#25) is the shared, stateless library that decides whether a C# script is acceptable before it is deployed or run: it owns the script trust model (a deny-list of forbidden APIs), the design-time compile gate, an advisory blocking-wait check and the static flow-shape predictor used by the Flow Debugger. It has no actors, configuration section or persistence.

Overview

The library lives in src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ (net10.0, Microsoft.CodeAnalysis.CSharp.Scripting and .Workspaces, project reference to Commons only). Before it existed the deny-list, reference set and imports were duplicated across four call sites; ScriptTrustPolicy is now the single source of truth and the others delegate to it.

Type Role
ScriptTrustPolicy The deny-list, reflection-gateway names, forbidden identifiers, reference sets and default imports.
ScriptTrustValidator Two-pass trust analysis: FindViolations(code, extraReferences?).
TrustVerdictCache Internal process-wide memo of default-reference verdicts.
RoslynScriptCompiler Diagnostics-only compile gate: ParseDiagnostics, Compile.
CachingScriptMetadataResolver Cross-compilation metadata resolver cache.
ScriptCompileSurface, TriggerCompileSurface Compile-only globals types.
BlockingWaitAnalyzer Advisory .Result / .Wait() detection: Find(code, globalsType).
FlowShapePredictor Syntax-only flow prediction: PredictInboundMethod, PredictSiteScript.

Key Concepts

The trust policy

ScriptTrustPolicy.ForbiddenScopes lists prefixes matched against a resolved symbol's containing namespace and type: System.IO, System.Diagnostics.Process (a single type, so Stopwatch stays allowed), System.Threading, System.Reflection, System.Net, System.Runtime.InteropServices, System.Runtime.CompilerServices.Unsafe, System.Runtime.CompilerServices.RuntimeHelpers, System.Runtime.Loader, Microsoft.Win32, System.Environment, System.GC, and the concrete ADO.NET providers Microsoft.Data, System.Data.SqlClient, System.Data.Odbc, System.Data.OleDb. System.Data and System.Data.Common are deliberately not forbidden, so scripts reach connections only through the Database helper.

AllowedExceptions re-allows System.Threading.Tasks, System.Threading.CancellationToken and System.Threading.CancellationTokenSource. ReflectionGatewayMembers (for example GetType, Assembly, GetMethod, InvokeMember, DynamicInvoke) are rejected regardless of receiver; Invoke is deliberately excluded because delegate.Invoke() is common. ForbiddenIdentifiers is dynamic and Activator.

Two validation passes

ScriptTrustValidator.FindViolations parses the script as SourceCodeKind.Script and returns a sorted, de-duplicated list of messages; empty means clean, and blank code returns empty.

  1. Semantic pass resolves each identifier, type reference, member access and object creation to its symbol and checks the containing namespace and type against ForbiddenScopes. This catches global:: prefixes, aliases and using static.
  2. Syntactic hardening pass (a CSharpSyntaxWalker) rejects reflection-gateway member accesses, forbidden identifiers, and forbidden using directives and qualified names, which catches reflection reached through permitted types such as typeof(string).Assembly.GetType(...).

The class documentation states neither pass is a true sandbox; it is defence in depth. A forbidden type inside nameof(...) is flagged on purpose.

Reference sets

Two reference sets are kept apart on purpose:

  • DefaultReferences is built from DefaultAssemblies (the assemblies of object, Enumerable, Math, the C# runtime binder and DynamicJsonElement). It is minimal and mirrors what the site runtime compiles against. It must never include the assemblies hosting forbidden APIs, so that a forbidden type is an undefined symbol and the compile gate independently rejects it.
  • AnalysisReferences is the full trusted-platform-assemblies set (TRUSTED_PLATFORM_ASSEMBLIES) plus DefaultAssemblies, used only by the trust validator so every named type resolves to its true namespace. When the TPA list is unavailable (single-file, AOT or trimmed host) it falls back to the minimal set plus ForbiddenAnchorAssemblies, sets AnalysisReferencesDegraded to true and emits a Trace warning. BuildMinimalFallbackReferences() exposes that fallback for tests.

Extra references passed to FindViolations can only improve resolution; the verdict is by resolved namespace, so they cannot whitelist anything.

Verdict cache

When no extra references are supplied, verdicts are memoised by SHA-256 of the code in TrustVerdictCache: two generations of up to 1024 entries each (hot and cold); a cold hit is promoted and overflow discards the cold generation. An analysis that throws is not cached, and stored lists are read-only.

Compile gate

RoslynScriptCompiler.Compile(code, globalsType, extraReferences, extraImports) creates a script over DefaultReferences and DefaultImports (System, System.Collections.Generic, System.Linq, System.Threading.Tasks, ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts) and returns the error-severity messages from GetCompilation().GetDiagnostics(). It does not emit or load, so no assembly is created per script; it also has no catch-all, so an unexpected exception propagates instead of being cached as a "failed to compile" verdict. ParseDiagnostics returns syntax errors only. All scripts share one InteractiveAssemblyLoader and the CachingScriptMetadataResolver.Instance, which memoises ResolveReference and ResolveMissingAssembly results (including "not found") so repeated compiles do not re-resolve the transitive reference closure.

Compile-only surfaces

ScriptCompileSurface is the globals type for compiling scripts: it exposes Instance, Parameters, CancellationToken, Alarm, Scope, and the members ExternalSystem, Database, Notify, Scripts, Attributes, Children, Parent, Alarms, Log, Files with nested stand-in types whose bodies throw NotSupportedException("compile-only surface"). It must be kept in step with the real runtime script API by hand. TriggerCompileSurface is the narrower surface for trigger expressions (Attributes, Children, Parent), used by the Template Engine validation.

Advisory blocking-wait check

BlockingWaitAnalyzer.Find binds the script against the given globals type and reports .Result, .Wait(), Task.WaitAll/WaitAny and .GetAwaiter().GetResult() on Task/ValueTask types. It is advisory, not a trust violation, because making it blocking would break already-deployed scripts on redeploy. Unresolved code is skipped.

Flow shape prediction

FlowShapePredictor walks script syntax (text-matching member-access chains with whitespace stripped) and returns a PredictedFlow of PredictedNode and PredictedEdge records in source order. PredictInboundMethod(methodName, script, resolveRouteScript, maxDepth) starts with an inbound-request node, expands routed site scripts through the caller's resolver, and ends with a response node. PredictSiteScript(scriptName, body, maxDepth) starts with a script-run node. DefaultMaxDepth is 4. Non-literal targets are flagged InputDependent; branching is not modelled; an early if (...) return guard becomes a note on the next node. A parse failure degrades to the request/response pair with the note parse error.

Architecture

The library is called synchronously from several components:

  • Site Runtime ScriptCompilationService and Inbound API ForbiddenApiChecker call ScriptTrustValidator.FindViolations.
  • Template Engine ScriptCompiler runs RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)), the trust validator and BlockingWaitAnalyzer; ValidationService uses the validator and RoslynScriptCompiler.Compile(expression, typeof(TriggerCompileSurface)) for trigger expressions.
  • Transport BundleImporter runs the validator on script bodies at preview and apply.
  • Central UI FlowDebugService calls the predictor.

Usage

var violations = ScriptTrustValidator.FindViolations(code);       // trust model
var errors = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)); // compile gate
var advisories = BlockingWaitAnalyzer.Find(code, typeof(ScriptCompileSurface));
var flow = FlowShapePredictor.PredictSiteScript("Start", code);

Configuration

None. All policy is compiled in ScriptTrustPolicy.

Dependencies & Interactions

Troubleshooting

A script passes the trust check but fails to compile

Expected for forbidden types: DefaultReferences omits their assemblies, so the compile gate rejects them as undefined symbols.

Compile gate and runtime disagree

ScriptCompileSurface is a hand-maintained mirror of the runtime API; a member present at runtime but absent here compiles at runtime only and fails the design-time gate.

AnalysisReferencesDegraded is true

The host has no TRUSTED_PLATFORM_ASSEMBLIES; only the documented anchor types are guaranteed to resolve in the semantic pass.