Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
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.
- 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 catchesglobal::prefixes, aliases andusing static. - Syntactic hardening pass (a
CSharpSyntaxWalker) rejects reflection-gateway member accesses, forbidden identifiers, and forbiddenusingdirectives and qualified names, which catches reflection reached through permitted types such astypeof(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:
DefaultReferencesis built fromDefaultAssemblies(the assemblies ofobject,Enumerable,Math, the C# runtime binder andDynamicJsonElement). 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.AnalysisReferencesis the full trusted-platform-assemblies set (TRUSTED_PLATFORM_ASSEMBLIES) plusDefaultAssemblies, 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 plusForbiddenAnchorAssemblies, setsAnalysisReferencesDegradedtotrueand emits aTracewarning.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
ScriptCompilationServiceand Inbound APIForbiddenApiCheckercallScriptTrustValidator.FindViolations. - Template Engine
ScriptCompilerrunsRoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)), the trust validator andBlockingWaitAnalyzer;ValidationServiceuses the validator andRoslynScriptCompiler.Compile(expression, typeof(TriggerCompileSurface))for trigger expressions. - Transport
BundleImporterruns the validator on script bodies at preview and apply. - Central UI
FlowDebugServicecalls 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
- Commons (#16) — script-facing types (
ScriptAlarm,TrackedOperationId,DynamicJsonElement, flow enums) referenced by the compile surface and predictor. - Template Engine (#1) — design-time script and trigger validation.
- Site Runtime (#3) — execution-time compile and trust gate.
- Inbound API (#14) — method script gate.
- Transport (#24) — import-time gate.
- Central UI (#9) — Flow Debugger prediction.
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.