ad4744a295
Flip DataConnectionLayer-029, InboundAPI-031, SiteRuntime-032/033, StoreAndForward-028, AuditLog-017, CentralUI-037, ScriptAnalysis-009 from Open to Resolved with the fixing commit + a one-line resolution each; regen README (0 pending / 576 total across 25 modules).
452 lines
30 KiB
Markdown
452 lines
30 KiB
Markdown
# Code Review — ScriptAnalysis
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Module | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis` |
|
|
| Design doc | `docs/requirements/Component-ScriptAnalysis.md` |
|
|
| Status | Reviewed |
|
|
| Last reviewed | 2026-06-24 |
|
|
| Reviewer | claude-agent |
|
|
| Commit reviewed | `1f9de8a2` |
|
|
| Open findings | 0 |
|
|
|
|
## Summary
|
|
|
|
This is the first formal review of the ScriptAnalysis component (#25), the single
|
|
authoritative source of truth for the ScadaBridge script trust boundary. The module
|
|
is small (5 source files, ~700 lines) and well-documented, and its core design is
|
|
**sound**: the trust verdict is genuinely **semantic** (Roslyn symbol resolution in
|
|
Pass 1), not a string/regex deny-list, so the classic spelling-based evasions —
|
|
aliases, `using static`, `global::`, partial-namespace qualification — are correctly
|
|
defeated by resolving the symbol rather than the text. The reflection-gateway
|
|
hardening (Pass 2) closes the `typeof(x).Assembly.GetType("System.IO.File")` class of
|
|
bypass that a namespace-only deny-list cannot see. All four documented consumers
|
|
(Template Engine, Site Runtime, Inbound API, Central UI) genuinely delegate to
|
|
`ScriptTrustValidator.FindViolations` and **hard-reject** on a non-empty result; none
|
|
re-implements or relaxes the policy. A reflection-based parity guard for the
|
|
compile-only surfaces exists in `SiteRuntime.Tests`. **No working trust-boundary
|
|
bypass was found at HEAD against the production (cluster-hosted) configuration.**
|
|
|
|
That said, the review surfaced several real issues. The highest-impact one is a
|
|
**silent degradation of the semantic pass when `TRUSTED_PLATFORM_ASSEMBLIES` is
|
|
unavailable** (single-file/AOT/trimmed host): `AnalysisReferences` falls back to the
|
|
5-assembly minimal set, after which a *bare* forbidden type inside an *allowed*
|
|
namespace (the documented `Process` via `using System.Diagnostics;` case) no longer
|
|
resolves and slips Pass 1 entirely — and Pass 2 never flags bare identifiers, only
|
|
dotted spellings (ScriptAnalysis-001). This is the only finding that can weaken the
|
|
verdict, and it is narrow (the production hosts carry a TPA list, and the downstream
|
|
real `Compile` gate catches it for two of the four consumers), so it is rated High
|
|
rather than Critical. The remaining findings are robustness/correctness/doc issues:
|
|
the semantic pass swallows nothing but the unresolved-symbol fallback is namespace-
|
|
spelling-dependent (Medium); `nameof(...)` over-flags legitimate non-forbidden uses
|
|
inconsistently (Low, documented-intentional but worth pinning); `ParseDiagnostics`/
|
|
`Compile` drop warnings despite the doc promising "errors and warnings"; there is no
|
|
caching/throttling of the Roslyn compilations (performance); the `extraReferences`
|
|
XML-doc on `FindViolations` is contradicted by the Central UI consumer; and the
|
|
adversarial test corpus, though good, has gaps (no extension-method, no
|
|
TPA-fallback, no `file`-scoped/`unsafe`, no verbatim/Unicode-escape identifier
|
|
cases). The trust boundary is **semantically sound in production**; the open items
|
|
harden the edges and align the spec.
|
|
|
|
## Checklist coverage
|
|
|
|
| # | Category | Examined | Notes |
|
|
|---|----------|----------|-------|
|
|
| 1 | Correctness & logic bugs | ☑ | `nameof` over-flags inconsistently (ScriptAnalysis-005); `GetSyntacticScopes` only returns text for dotted names so a bare unresolved forbidden identifier yields no scope (root of ScriptAnalysis-001/002). |
|
|
| 2 | Akka.NET conventions | ☑ | No issues found — module has no actors, no Akka dependency (by design; csproj references only Roslyn + Commons). Stateless static APIs, thread-safe. |
|
|
| 3 | Concurrency & thread safety | ☑ | No issues found — all public surfaces are static/stateless; the shared `static readonly` policy collections are read-only after init; `HardeningWalker` is per-call. `AnalysisReferences` eager-built once at type init. |
|
|
| 4 | Error handling & resilience | ☑ | `RoslynScriptCompiler.Compile` converts exceptions into a fail-the-gate error entry (fail-safe). `BuildAnalysisReferences` swallows per-assembly read errors and silently falls back to the minimal set — the silent fallback is the resilience gap (ScriptAnalysis-001). |
|
|
| 5 | Security | ☑ | Semantic (symbol-based) verdict — no spelling bypass found in production. One conditional degradation (TPA-absent host) weakens Pass 1 to spelling-dependent (ScriptAnalysis-001); `extraReferences` can never cause a false allow (verified). Defence-in-depth only, as documented — not a runtime sandbox (ScriptAnalysis-008). |
|
|
| 6 | Performance & resource management | ☑ | No compilation caching/throttling; every gate call re-parses, rebuilds a compilation, and re-runs Roslyn — `FindViolations` builds a fresh `CSharpCompilation` against the *full framework* reference set on every call (ScriptAnalysis-006). |
|
|
| 7 | Design-document adherence | ☑ | `ParseDiagnostics`/`Compile` drop warnings though doc says "errors and warnings" (ScriptAnalysis-004); `AnalysisReferences` (full-framework set) is not described in REQ-SA-2, which says DefaultReferences (ScriptAnalysis-007). |
|
|
| 8 | Code organization & conventions | ☑ | No issues found — POCO/static layout is clean, namespace correct, depends only on Commons + Roslyn. `ScriptCompileSurface` lives in ScriptAnalysis (not Commons) — acceptable as it mirrors a runtime surface and is consumed here. |
|
|
| 9 | Testing coverage | ☑ | Good adversarial reject/allow corpus, but missing: TPA-fallback degradation, extension methods, verbatim/Unicode-escape identifiers, `unsafe`/pointer, comment/trivia, and `AnalysisReferences`/exception paths (ScriptAnalysis-003). |
|
|
| 10 | Documentation & comments | ☑ | `FindViolations` XML-doc claim "Forbidden references are NOT added here" is contradicted by the Central UI consumer forwarding `compilation.References` (ScriptAnalysis-007, doc half). Otherwise thorough and accurate. |
|
|
|
|
## Findings
|
|
|
|
### ScriptAnalysis-001 — Semantic trust pass silently degrades to spelling-dependent when `TRUSTED_PLATFORM_ASSEMBLIES` is unavailable
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | High |
|
|
| Category | Security |
|
|
| Status | Resolved |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustPolicy.cs:146-184`; interacts with `ScriptTrustValidator.cs:111-136`, `:195-213` |
|
|
|
|
**Description**
|
|
|
|
`AnalysisReferences` is the reference set that lets Pass 1 (the semantic symbol pass —
|
|
the *authoritative* guard) resolve every type a script names to its true namespace.
|
|
It is built from `AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")`. When that value
|
|
is absent — a single-file deployment, an AOT/trimmed host, or any runtime that does
|
|
not publish the TPA list — `BuildAnalysisReferences` falls back to the 5-assembly
|
|
`DefaultReferences` set (`ScriptTrustPolicy.cs:183`) **silently** (the per-assembly
|
|
`catch { }` at `:165`/`:178` and the `Count > 0` fallback at `:183` emit nothing).
|
|
|
|
After that fallback, a *bare* forbidden type that lives inside an *allowed* namespace
|
|
no longer resolves. The policy's own documented example is exactly this case:
|
|
`using System.Diagnostics; var p = Process.Start("x");`. `System.Diagnostics` is an
|
|
allowed namespace (so `VisitUsingDirective` does not flag the import), and bare
|
|
`Process` is an `IdentifierName`. With `Process.dll` absent from the minimal
|
|
reference set, `model.GetSymbolInfo` returns no symbol, so Pass 1 falls to
|
|
`GetSyntacticScopes`, which returns an empty scope list for a bare (dotless)
|
|
identifier (`ScriptTrustValidator.cs:210-212`). Pass 2's `HardeningWalker` only flags
|
|
forbidden *dotted* names and forbidden `using` imports — never a bare identifier
|
|
token (`VisitIdentifierName`, `:373-395`, only checks `dynamic`/`Activator`). The
|
|
forbidden `Process.Start` therefore slips the trust validator entirely. The same
|
|
degradation reopens the broader "forbidden type in an allowed namespace reached as a
|
|
bare identifier" class that the full-framework reference set was specifically added
|
|
(commit `06975720`, M3.6) to close.
|
|
|
|
This is rated High not Critical because: (a) the production hosts (Akka.NET cluster,
|
|
docker topology) run on a normal framework-dependent runtime that publishes the TPA
|
|
list, so the full set is loaded; and (b) for two of the four consumers (Template
|
|
Engine, Site Runtime) the downstream real `Compile` gate against a curated reference
|
|
set independently rejects an undefined `Process` symbol. But Inbound API and the
|
|
Central UI run gate rely on `FindViolations` for the reflection/namespace verdict,
|
|
and a deployment-mode change that drops the TPA list would silently weaken the
|
|
boundary with no diagnostic.
|
|
|
|
**Recommendation**
|
|
|
|
Make the degradation loud and fail-safe: when the TPA list is unavailable, log a
|
|
warning (or expose a diagnostic flag the consumers can surface), and — more
|
|
importantly — add the BCL assemblies that host the forbidden-type-in-allowed-namespace
|
|
cases (at minimum `System.Diagnostics.Process`) to `DefaultAssemblies` so they resolve
|
|
even in the minimal fallback. Alternatively, extend Pass 2 to flag bare identifiers
|
|
whose name matches the simple name of a known forbidden type (e.g. `Process`,
|
|
`WebClient`), independent of symbol resolution, so the verdict never depends solely on
|
|
the reference set being complete.
|
|
|
|
**Resolution**
|
|
|
|
Resolved 2026-06-20 (commit `fd618cf1`): the TPA-absent fallback now folds the
|
|
assemblies hosting the forbidden anchor types (Process, Socket, File, Thread, Assembly,
|
|
Marshal) into the minimal validation reference set so a bare forbidden type still
|
|
resolves and is flagged; an `AnalysisReferencesDegraded` flag + a `Trace.TraceWarning`
|
|
make the degraded mode observable. CRITICAL: anchors are added ONLY to the validation
|
|
`AnalysisReferences`, never to `DefaultReferences` — keeping the compile gate from being
|
|
able to bind `Process`. Verified by a TPA-fallback test that a bare `Process.Start` is
|
|
still rejected.
|
|
|
|
### ScriptAnalysis-002 — Pass 2 (syntactic hardening) is spelling-dependent and cannot catch a bare forbidden identifier on its own
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Medium |
|
|
| Category | Security |
|
|
| Status | Deferred |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustValidator.cs:289-395`; `:195-213` |
|
|
|
|
**Description**
|
|
|
|
Pass 2 (`HardeningWalker`) is purely textual: `VisitUsingDirective` flags forbidden
|
|
`using` imports (`:289-295`), `VisitQualifiedName`/`VisitMemberAccessExpression` flag
|
|
forbidden *dotted* names (`:298-369`), and `VisitIdentifierName` flags only the bare
|
|
identifiers `dynamic`/`Activator` (`:373-395`). It cannot independently flag any other
|
|
bare forbidden identifier — it relies entirely on Pass 1's semantic resolution for
|
|
that. The two passes are therefore not the "two independent guards" the design implies
|
|
for the bare-identifier case: for an unresolved bare identifier (see
|
|
ScriptAnalysis-001) **neither** pass fires. `GetSyntacticScopes` deliberately returns
|
|
nothing for dotless names (`:210-212`, "bare local identifiers cannot reach a forbidden
|
|
namespace") — which is true only while Pass 1 resolves them; once resolution fails the
|
|
assumption breaks.
|
|
|
|
This is the structural root of ScriptAnalysis-001 and is recorded separately because
|
|
it is a design property (the syntactic pass is not self-sufficient against bare names)
|
|
that should be acknowledged even after the TPA-fallback hole is closed: the defence is
|
|
single-layered, not double-layered, for the bare-identifier vector.
|
|
|
|
**Recommendation**
|
|
|
|
Document explicitly that Pass 1 is the sole guard for bare (single-identifier)
|
|
forbidden references and that Pass 2 only backstops *spelled* forbidden namespaces. If
|
|
genuine defence-in-depth is wanted for bare names, add a simple-name deny-set to Pass 2
|
|
(the simple names of the forbidden root types — `Process`, `WebClient`, `Marshal`,
|
|
`Registry`, `Thread`, `File`, `Directory`, etc.), accepting the small false-positive
|
|
risk on a script's own identically-named local, which a security gate should tolerate.
|
|
|
|
**Resolution**
|
|
|
|
Deferred 2026-06-20: strengthening Pass-2 (syntactic) so a bare forbidden identifier is
|
|
caught without Pass-1 symbol resolution is a defence-in-depth enhancement; the
|
|
production path (TPA present, two-layer compile gate on most call sites) is sound.
|
|
Recorded for a follow-up.
|
|
|
|
### ScriptAnalysis-003 — Adversarial test corpus has bypass-relevant gaps
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Medium |
|
|
| Category | Testing coverage |
|
|
| Status | Resolved |
|
|
| Location | `tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/ScriptTrustValidatorTests.cs`; `RoslynScriptCompilerTests.cs` |
|
|
|
|
**Description**
|
|
|
|
The reject/allow corpus is solid for the spelled-namespace, alias, `using static`,
|
|
`global::`, reflection-gateway, generic-argument, and nested-lambda vectors. For a
|
|
security-critical trust boundary, however, several bypass-relevant cases are untested:
|
|
|
|
- **TPA-fallback degradation** (ScriptAnalysis-001) — no test forces
|
|
`AnalysisReferences` onto the minimal set and asserts that bare `Process`/`WebClient`
|
|
is still rejected. This is the highest-value missing test.
|
|
- **Extension methods** — no test that a forbidden-namespace extension method invoked
|
|
in receiver-position (`x.SomeForbiddenExt()`) is resolved and flagged.
|
|
- **Verbatim / Unicode-escaped identifiers** — `@dynamic`, `dynamic`,
|
|
`Activator`, or `GetType` are not exercised; `VisitIdentifierName` compares
|
|
`Identifier.ValueText`, which decodes Unicode escapes (so it likely holds), but the
|
|
case is not pinned, and `dynamic` is matched via `text == "dynamic"` (`:380`) which is
|
|
a separate path from the `ForbiddenIdentifiers` set.
|
|
- **`unsafe`/pointer/`stackalloc`** — relies implicitly on `ScriptOptions` defaulting
|
|
`AllowUnsafe=false`; not asserted.
|
|
- **Comment/trivia and string-literal payloads** — no test that a forbidden namespace
|
|
inside a comment or string is *not* flagged (false-positive guard) and that a
|
|
forbidden token reconstructed only at runtime from strings is correctly *not* the
|
|
validator's concern (documents the sandbox caveat).
|
|
|
|
**Recommendation**
|
|
|
|
Add the cases above, prioritising the TPA-fallback test (inject a minimal-reference
|
|
build of `AnalysisReferences` or factor the reference set so a test can force the
|
|
fallback) and the extension-method test. Add a verbatim/Unicode-escape identifier
|
|
reject test for `dynamic`/`Activator`/`GetType`.
|
|
|
|
**Resolution**
|
|
|
|
Resolved 2026-06-20 (commit `fd618cf1`): added 18 adversarial tests — TPA-fallback
|
|
bare-Process/Socket, extension-method invocation, verbatim/Unicode-escaped identifiers,
|
|
`unsafe` blocks (benign vs forbidden-API-inside), and comment/string-literal
|
|
false-positive guards.
|
|
|
|
### ScriptAnalysis-004 — `ParseDiagnostics`/`Compile` drop warnings though the design doc promises "errors and warnings"
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Low |
|
|
| Category | Design-document adherence |
|
|
| Status | Resolved |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/RoslynScriptCompiler.cs:30-33`, `:73-76` |
|
|
|
|
**Description**
|
|
|
|
REQ-SA-3 states `Compile` returns the diagnostics "filtered to errors and warnings"
|
|
and `ParseDiagnostics` returns "syntax-level diagnostics (errors and warnings)". The
|
|
code filters `d.Severity == DiagnosticSeverity.Error` only in both methods
|
|
(`RoslynScriptCompiler.cs:31` and `:74`), so warnings are silently dropped. Filtering
|
|
to errors-only is arguably the safer choice for a compile *gate* (a warning should not
|
|
block a deploy), but the code and the spec disagree, and a future maintainer reading
|
|
the doc will expect warnings in the returned list.
|
|
|
|
**Recommendation**
|
|
|
|
Reconcile: either change the doc (REQ-SA-3) to say "errors only", or include warnings
|
|
and let callers decide. Given the gate semantics, updating the doc to "error-severity
|
|
diagnostics" is the cleaner fix.
|
|
|
|
**Resolution**
|
|
|
|
Resolved 2026-06-20 (commit `fd618cf1`): corrected the design doc (REQ-SA-3) to
|
|
'error-severity only', matching the compile wrapper's existing behaviour (a warning must
|
|
not block a deploy gate). Code unchanged.
|
|
|
|
### ScriptAnalysis-005 — `nameof(forbiddenType)` flagging is inconsistent between passes and across resolution states
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Low |
|
|
| Category | Correctness & logic bugs |
|
|
| Status | Deferred |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustValidator.cs:42-48`, `:99-136`, `:298-325` |
|
|
|
|
**Description**
|
|
|
|
The class comment (`:42-48`) states that a forbidden type inside `nameof(...)` is
|
|
"deliberately flagged" as a conservative fail-safe, and `Rejects_NameOf_ForbiddenType`
|
|
pins `nameof(System.IO.File)`. However the flagging is incidental, not deliberate, and
|
|
inconsistent: `nameof(System.IO.File)` is caught because the inner `System.IO.File` is
|
|
a `QualifiedNameSyntax`/member access that both passes inspect directly — the
|
|
`nameof` wrapper is irrelevant. A bare `nameof(Process)` (after `using
|
|
System.Diagnostics;`, with full TPA loaded) resolves `Process` to a real symbol and
|
|
*is* flagged by Pass 1; but the same `nameof(Process)` under the minimal-reference
|
|
fallback resolves to nothing and is *not* flagged. So the "deliberate fail-safe"
|
|
behaviour actually varies with the reference set, and `nameof` — which produces only a
|
|
compile-time string and reaches no API — is a genuine false positive that will reject a
|
|
legitimate script doing `nameof(System.Net.IPAddress)` for a log message.
|
|
|
|
**Recommendation**
|
|
|
|
Decide the intended semantics explicitly. If `nameof` should be allowed (it reaches no
|
|
runtime API), special-case `NameOfExpression`/`InvocationExpression` named `nameof` to
|
|
suppress the inner-reference report. If it should be rejected, make that uniform
|
|
regardless of resolution state (tie to a simple-name check), and keep the test. Either
|
|
way, align the comment with the actual, deterministic behaviour.
|
|
|
|
**Resolution**
|
|
|
|
Deferred 2026-06-20: the `nameof(forbiddenType)` over-flagging is a documented
|
|
deliberate fail-safe; whether `nameof` (which reaches no API) should be allowed vs
|
|
uniformly rejected is a design-owner decision. Recorded; no behaviour change this pass.
|
|
|
|
### ScriptAnalysis-006 — No caching or throttling of Roslyn compilations; full-framework compilation rebuilt per call
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Low |
|
|
| Category | Performance & resource management |
|
|
| Status | Deferred |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustValidator.cs:73-97`; `RoslynScriptCompiler.cs:50-71` |
|
|
|
|
**Description**
|
|
|
|
`FindViolations` parses the script and builds a fresh `CSharpCompilation` against the
|
|
**entire framework reference set** (`AnalysisReferences`, potentially hundreds of
|
|
`MetadataReference`s) on every call (`:87-95`). `RoslynScriptCompiler.Compile`
|
|
similarly creates a new `CSharpScript` and calls `.Compile()` per call. Roslyn
|
|
compilation is the expensive part of these calls. The metadata references are cached
|
|
(built once in `AnalysisReferences`), which is the main cost, so this is Low rather
|
|
than Medium — but for the Central UI editor path (which can call the validator on every
|
|
keystroke-debounced analysis) and for batch template validation (many scripts per
|
|
deploy), the absence of any per-source memoisation or a bounded compilation cache means
|
|
repeated work. There is no back-pressure or concurrency cap either; the Inbound API
|
|
review (InboundAPI-009) already noted uncapped recompilation downstream.
|
|
|
|
**Recommendation**
|
|
|
|
Consider a small bounded cache keyed on a hash of `(code, globalsType)` for `Compile`
|
|
results and on `code` for `FindViolations` verdicts (the verdict is a pure function of
|
|
the source and the static policy). At minimum, document that callers on hot paths
|
|
(editor live-analysis) should debounce and cache, and confirm the Central UI debounces
|
|
before calling.
|
|
|
|
**Resolution**
|
|
|
|
Deferred 2026-06-20: compilation caching/throttling (the full-framework
|
|
`CSharpCompilation` is rebuilt per `FindViolations` call) is a performance enhancement,
|
|
not a correctness defect. Recorded for a follow-up.
|
|
|
|
### ScriptAnalysis-007 — `FindViolations` `extraReferences` XML-doc contradicts the Central UI consumer; `AnalysisReferences` undocumented in REQ-SA-2
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Low |
|
|
| Category | Documentation & comments |
|
|
| Status | Resolved |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustValidator.cs:57-64`; `ScriptTrustPolicy.cs:125-144`; `docs/requirements/Component-ScriptAnalysis.md:74-80` |
|
|
|
|
**Description**
|
|
|
|
Two doc/code mismatches, neither a vulnerability:
|
|
|
|
1. The `extraReferences` XML-doc on `FindViolations` (`:57-64`) says "Forbidden
|
|
references are NOT added here". But the Central UI run gate calls
|
|
`FindViolations(tree.ToString(), compilation.References)`
|
|
(`CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:1319-1320`), forwarding the full
|
|
BCL surface as `extraReferences`. This is in fact *safe* (the deny-list is by
|
|
namespace/type, so adding references can only improve resolution, never produce a
|
|
false allow — and the policy comment at `ScriptTrustPolicy.cs:138-142` says exactly
|
|
that), but the `FindViolations` doc as written implies the caller must not pass
|
|
forbidden assemblies, which is both unnecessary and contradicted in practice.
|
|
|
|
2. REQ-SA-2 in the design doc describes Pass 1 as using "the full trusted-platform
|
|
reference set from `ScriptTrustPolicy.DefaultReferences`". The code uses a separate
|
|
`ScriptTrustPolicy.AnalysisReferences` (the full framework), and `DefaultReferences`
|
|
is explicitly the *minimal* set used by `RoslynScriptCompiler`. The doc conflates the
|
|
two and never mentions `AnalysisReferences` or the TPA mechanism — material because
|
|
that mechanism is the heart of the security verdict (and of ScriptAnalysis-001).
|
|
|
|
**Recommendation**
|
|
|
|
Rewrite the `extraReferences` doc to state that extra references only widen symbol
|
|
resolution and never whitelist a forbidden API. Update REQ-SA-2 to describe
|
|
`AnalysisReferences` (full-framework, TPA-sourced) versus `DefaultReferences` (minimal,
|
|
runtime-fidelity), and note the TPA-fallback behaviour.
|
|
|
|
**Resolution**
|
|
|
|
Resolved 2026-06-20 (commit `fd618cf1`): corrected the `FindViolations`
|
|
`extraReferences` XML doc (extra references only widen resolution, never whitelist) and
|
|
documented `AnalysisReferences` vs `DefaultReferences` in REQ-SA-2.
|
|
|
|
### ScriptAnalysis-008 — Static trust gate is defence-in-depth, not a sandbox; correctly documented but worth recording as a standing risk
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Low |
|
|
| Category | Security |
|
|
| Status | Won't Fix |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustValidator.cs:34-40`; `docs/requirements/Component-ScriptAnalysis.md:151` |
|
|
|
|
**Description**
|
|
|
|
The component is explicit (both in code comments and REQ-SA-5) that it is a
|
|
compile-time deny-list, not a runtime sandbox: scripts execute in-process, and a
|
|
sufficiently determined script could reconstruct a forbidden API at runtime via paths
|
|
the static walker cannot see (e.g. building type names from runtime strings, or any
|
|
gap in the deny-list). The reflection-gateway hardening narrows this, and the
|
|
`dynamic`/`Activator` bans close the obvious late-binding routes, but the residual risk
|
|
is real and inherent to in-process execution of partially-trusted C#. This is recorded
|
|
(not as a defect of this module, which behaves as designed) so the standing risk is
|
|
visible in the audit trail and is revisited if/when the deferred runtime boundary
|
|
(restricted `AssemblyLoadContext` / curated reference set / out-of-process sandbox) is
|
|
scoped.
|
|
|
|
**Recommendation**
|
|
|
|
Keep the caveat prominent. Track the runtime-boundary work as a roadmap item; in the
|
|
meantime ensure the curated *execution* reference sets (SiteRuntime `ScriptAssemblies`,
|
|
`RoslynScriptCompiler` `DefaultReferences`) stay minimal so that even a deny-list miss
|
|
cannot bind a forbidden type at execution time (this is the real second layer of
|
|
defence for the compile-running consumers).
|
|
|
|
**Resolution**
|
|
|
|
Won't Fix 2026-06-20: the static gate is intentionally defence-in-depth, not a runtime
|
|
sandbox — this is the explicitly-documented design posture, not a defect. Recorded as a
|
|
standing risk for visibility.
|
|
|
|
## Re-review — 2026-06-24 (commit `1f9de8a2`)
|
|
|
|
Focused re-review of the changes since the prior review — verifying the code-review remediation + feature fixes are sound and regression-free. Reviewed by a per-module workflow agent; findings code-verified by the orchestrator.
|
|
|
|
**Changes reviewed:** The diff remediates prior finding ScriptAnalysis-001: when TRUSTED_PLATFORM_ASSEMBLIES is unavailable (single-file/AOT/trimmed host), BuildAnalysisReferences now folds new ForbiddenAnchorAssemblies (Process, Socket, File, Thread, Assembly, Marshal) into the minimal fallback so a bare forbidden type in an allowed namespace still resolves and is flagged, sets an observable AnalysisReferencesDegraded flag plus a Trace.TraceWarning, and refactors per-assembly add into TryAddAssembly. A new BuildMinimalFallbackReferences() helper and a FindViolations(code, baseReferences, extraReferences) overload let tests pin the degraded path; the extraReferences XML-doc was corrected to state extra references only widen resolution and can never whitelist a forbidden API. Test files added 11 adversarial cases (SA-003) covering TPA-fallback, extension methods, verbatim/Unicode-escape identifiers, unsafe blocks, and comment/string-literal false-positive guards.
|
|
|
|
**Verdict:** The changed code is sound and regression-free. The SA-001 fix is correctly scoped: forbidden-API anchor assemblies are folded in only on the degraded (no-TPA) path and only into the validator's AnalysisReferences, never into DefaultReferences, so the RoslynScriptCompiler compile gate keeps rejecting forbidden types as undefined symbols (its independent second layer of defence is preserved). Static field-initialization order is correct (anchor lists declared before AnalysisReferences), the degradation is now loud, and the new FindViolations overload is fully backward-compatible with all four existing single-arg consumers. The 34 ScriptTrustValidator tests pass, including the 11 new adversarial cases, and the design doc (REQ-SA-2, TPA-fallback section) is fully in sync with the code. The only observations are minor: no test asserts the production non-degraded flag value, and one inline comment is slightly imprecise inside the parameterized overload — neither affects correctness or security.
|
|
|
|
| # | Category | Examined | Notes |
|
|
|---|----------|----------|-------|
|
|
| 1 | Correctness & logic bugs | ☑ | Fallback enrichment, tpaAvailable detection (byPath.Count>0), and TryAddAssembly dedup logic are correct. Static init order (DefaultAssemblies/ForbiddenAnchorAssemblies before AnalysisReferences) is safe. No issues found. |
|
|
| 2 | Akka.NET conventions | ☑ | Module has no actors/Akka dependency by design; the change adds only static policy/validator members. Not applicable; no issues found. |
|
|
| 3 | Concurrency & thread safety | ☑ | AnalysisReferencesDegraded is set once during static initialization of AnalysisReferences (single-threaded type-init) and read-only thereafter; the new overload is stateless and per-call. No shared mutable state introduced. |
|
|
| 4 | Error handling & resilience | ☑ | TryAddAssembly swallows per-assembly read errors (fail-open on a single bad assembly, as before) and the degraded fallback is now LOUD via flag + Trace.TraceWarning rather than silent. Fail-safe posture preserved. |
|
|
| 5 | Security | ☑ | Core fix verified: anchors added only to AnalysisReferences on the degraded path, never to DefaultReferences, so the compile-gate's undefined-symbol defence is intact. Bare Process/Socket now caught on the minimal set (test-proven). extraReferences cannot produce a false allow. No new bypass introduced. |
|
|
| 6 | Performance & resource management | ☑ | Anchor enrichment adds at most 6 MetadataReference.CreateFromFile calls, and only on the degraded path, all at one-time static init. No per-call cost added (the pre-existing no-cache concern, SA-006, is unchanged and out of delta scope). |
|
|
| 7 | Design-document adherence | ☑ | Component-ScriptAnalysis.md (lines 86-88) documents DefaultReferences vs AnalysisReferences, ForbiddenAnchorAssemblies, the not-silent fallback, and AnalysisReferencesDegraded exactly as implemented. No drift in either direction. |
|
|
| 8 | Code organization & conventions | ☑ | Extraction of TryAddAssembly removes duplication; ForbiddenAnchorAssemblies / BuildMinimalFallbackReferences are well-placed and thoroughly XML-documented. Overload delegation pattern is clean. No issues found. |
|
|
| 9 | Testing coverage | ☑ | 11 new adversarial tests close SA-003 gaps and pass (34/34). Minor: no test asserts AnalysisReferencesDegraded==false on a normal (TPA-present) host, so the production non-degraded path is exercised only implicitly. |
|
|
| 10 | Documentation & comments | ☑ | New XML-docs are accurate and the corrected extraReferences doc matches verified behaviour. Minor: the inline comment at ScriptTrustValidator.cs:106-111 still says 'Use the full trusted-platform reference set' but the method now resolves against the baseReferences parameter (minimal-enriched on the fallback path). |
|
|
|
|
**New findings from this re-review (1):**
|
|
|
|
### ScriptAnalysis-009 — Inline Pass 1 comment no longer reflects parameterized base references
|
|
|
|
| | |
|
|
|--|--|
|
|
| Severity | Low |
|
|
| Category | Documentation & comments |
|
|
| Status | Resolved |
|
|
| Location | `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustValidator.cs:106` |
|
|
|
|
**Description**
|
|
|
|
The inline comment at lines 106-111 still asserts 'Use the full trusted-platform reference set (not the minimal runtime-fidelity DefaultReferences)'. After the refactor, Pass 1 now resolves against the baseReferences parameter (line 112), which on the test/degraded path is the minimal anchor-enriched fallback set, not the full TPA set. The comment describes only the default public-entry-point case and is misleading for a maintainer reading the parameterized overload. The overload's own XML-doc (lines 77-89) is correct, so impact is limited to an internal comment.
|
|
|
|
**Recommendation**
|
|
|
|
Reword the comment to note that Pass 1 resolves against the supplied baseReferences — full TPA set on the public entry point, minimal anchor-enriched fallback when a caller (e.g. tests) passes BuildMinimalFallbackReferences().
|
|
|
|
**Resolution**
|
|
|
|
Resolved in commit `9ab1c002` (2026-06-24): reworded the Pass 1 comment to state it resolves against the supplied `baseReferences` — the full trusted-platform reference set on the public entry point, or the minimal anchor-enriched fallback (`BuildMinimalFallbackReferences()`) on the degraded/test path.
|