# 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.
