fd618cf1dc
Remediation from the full per-module code review at 4307c381 (findings recorded
separately in code-reviews/).
Highs fixed:
- DeploymentManager-025/SiteRuntime-031: stop broadcasting notification lists + SMTP
configs (incl. credentials) to sites; site purges already-persisted rows on apply
(enforces the central-only delivery design; clears plaintext SMTP creds at rest).
- DataConnectionLayer-023: guard the native-alarm subscribe path against the
mid-flight-unsubscribe adapter-feed leak (mirrors the DCL-021 tag-path fix).
- SiteEventLogging-024: normalize From/To query bounds to UTC (the -016 fix the
audit trail claimed but never committed).
- KpiHistory-001: add an in-flight guard to the recorder sample tick.
- ScriptAnalysis-001: harden the trust analyzer's TPA-absent fallback (resolve
forbidden anchors in the minimal reference set; warn on degraded mode) — anchors
added to validation references only, never the compile gate.
(InboundAPI-026 left to the feat/ipsen-movein effort per owner decision.)
Medium/Low: DM-026 deterministic deploy-status tiebreaker; SR-027/028/029/030
native-alarm leak/phantom-active/delete-during-redeploy fixes; AL-013/014/016;
TE-024 (folder-mutation audit rows now persisted)/025; SF-025 gauge-provider
clear-on-stop; ESG-025/026; SEC-023/024/025; SCA-007/008/009; plus doc/test
accuracy COM-023/024, HOST-025/026, HM-024/025, NS-027/028.
Full-solution build 0 warnings; ~3560 tests across 18 touched suites green.
177 lines
14 KiB
Markdown
177 lines
14 KiB
Markdown
# 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 `CSharpScript` compilation (`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 support
|
|
- `System.Threading.CancellationToken` — cooperative cancellation
|
|
- `System.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 `ScriptTrustPolicy.AnalysisReferences` (plus any `extraReferences`).
|
|
- 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`, taking `AllowedExceptions` into account.
|
|
- Correctly handles aliases (`using X = System.IO.File`), `using static`, and `global::` 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 `Process` after `using System.Diagnostics;`).
|
|
|
|
##### `AnalysisReferences` vs `DefaultReferences`
|
|
|
|
The two reference sets are deliberately distinct and must not be conflated:
|
|
|
|
- **`DefaultReferences`** — the **minimal, runtime-fidelity** set (built from `DefaultAssemblies`: CoreLib, LINQ, Math, the C# runtime binder, and the Commons API-surface assembly). It is consumed by `RoslynScriptCompiler` (the compile gate) and must mirror exactly what the site runtime compiles/executes against. It deliberately does **not** reference the forbidden-API anchor assemblies (`System.Diagnostics.Process.dll`, `System.Net.Sockets.dll`, …) so that a forbidden type remains an *undefined symbol* at compile time — the compile gate then independently rejects it, providing a second layer of defence. This set must stay minimal.
|
|
- **`AnalysisReferences`** — the **full-framework** set used *only* by `ScriptTrustValidator`'s Pass 1. It is built from `AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")` (the TPA list of the host) so that *every* type a script names resolves to its true namespace and is judged authoritatively. Enriching the analysis set can only *improve* detection (the verdict is by namespace/type, never a false allow), which is why the Central UI run gate may safely forward its full compilation reference surface as `extraReferences`.
|
|
- **TPA-fallback behaviour** — on a host that does not publish the TPA list (single-file, AOT, or trimmed deployment), `AnalysisReferences` falls back to `DefaultReferences` **enriched with `ForbiddenAnchorAssemblies`** (the assemblies that host the forbidden-API types). This keeps the documented forbidden anchors — notably bare `Process` inside the allowed `System.Diagnostics` namespace — resolvable, so the semantic pass stays authoritative even in the degraded mode. The fallback is **not silent**: `ScriptTrustPolicy.AnalysisReferencesDegraded` is set to `true` and a warning is emitted via `System.Diagnostics.Trace` so operators and tests can detect the weakened mode.
|
|
|
|
**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
|
|
|
|
- `FindViolations` needs 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 `CSharpScript` with the given code, `globalsType`, references (defaults from `ScriptTrustPolicy.DefaultReferences` plus `extraReferences`), and imports (defaults from `ScriptTrustPolicy.DefaultImports` plus `extraImports`).
|
|
- Calls `.Compile()` and returns the resulting `Diagnostic[]` filtered to **error-severity diagnostics only**. This is a compile *gate*: a warning must not block a deploy, so only errors (undefined symbols, type mismatches) are surfaced to callers as gate failures.
|
|
- Each caller passes its own `globalsType` — `ScriptCompileSurface` for the design-time deploy gate, the real `ScriptGlobals` for Site Runtime execution, `null` for pure syntax checks.
|
|
|
|
#### `ParseDiagnostics(string code)`
|
|
|
|
- Parses the script text using Roslyn's `CSharpSyntaxTree.ParseText` and returns **error-severity** syntax-level diagnostics only (consistent with `Compile` — warnings do not fail the gate).
|
|
- 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:
|
|
```csharp
|
|
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 `ScriptCompileSurface` and `TriggerCompileSurface` (e.g., `DataType`, attribute access types).
|
|
- **Microsoft.CodeAnalysis.CSharp.Scripting**: Roslyn scripting APIs used by `RoslynScriptCompiler` and `ScriptTrustValidator`.
|
|
- **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`, and `TriggerCompileSurface` in the design-time deploy gate (`ScriptCompiler.TryCompile`) and expression syntax validator (`ValidationService.CheckExpressionSyntax`).
|
|
- **Site Runtime (#3)**: `ScriptCompilationService.ValidateTrustModel` delegates the trust verdict to `ScriptTrustValidator.FindViolations`; retains its own `CSharpScript.Compile` against the real `ScriptGlobals` for execution-time compilation.
|
|
- **Inbound API (#14)**: `ForbiddenApiChecker.FindViolations` is a thin shim over `ScriptTrustValidator.FindViolations`.
|
|
- **Central UI (#9)**: `ScriptAnalysisService` delegates the run-gate forbidden-API verdict and sources the editor-marker deny-list from `ScriptTrustPolicy`; retains the Test-Run execution host (`SandboxScriptHost`).
|