# 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 ```csharp 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)](./Commons.md) — script-facing types (`ScriptAlarm`, `TrackedOperationId`, `DynamicJsonElement`, flow enums) referenced by the compile surface and predictor. - [Template Engine (#1)](./TemplateEngine.md) — design-time script and trigger validation. - [Site Runtime (#3)](./SiteRuntime.md) — execution-time compile and trust gate. - [Inbound API (#14)](./InboundAPI.md) — method script gate. - [Transport (#24)](./Transport.md) — import-time gate. - [Central UI (#9)](./CentralUI.md) — 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. ## Related Documentation - [Script Analysis design specification](../requirements/Component-ScriptAnalysis.md) - [Template Engine](./TemplateEngine.md) - [Site Runtime](./SiteRuntime.md) - [Inbound API](./InboundAPI.md) - [Transport](./Transport.md)