docs(code-review): full review at 4307c381 — 18 modules, 67 findings recorded + remediation tracked

Full per-module re-review of the 16 stale modules (last seen 1eb6e97 / 2026-05-28)
plus first-ever reviews of KpiHistory (#26) and ScriptAnalysis (#25), at HEAD 4307c381.

67 new findings (0 Critical, 6 High, 27 Medium, 34 Low). Remediation in commit
fd618cf1 closed 5 of the 6 Highs and ~33 Medium/Low; the rest are Deferred/Won't Fix
with rationale. Remaining pending (4) are all InboundAPI's Database-helper findings
(IA-026 High .. IA-029), left to the active feat/ipsen-movein effort per owner decision.

Highlights: caught a central-only-delivery security drift (SMTP creds broadcast to
sites — DM-025/SR-031), a never-committed 'Resolved' fix (SiteEventLogging-016 → -024),
an unguarded KPI recorder tick (KH-001), a trust-analyzer fallback weakening (SA-001),
and a native-alarm subscribe-path leak (DCL-023). ScriptAnalysis verdict: trust boundary
is semantically sound (symbol-based) in the production cluster config.

README regenerated; regen-readme.py --check passes (4 pending / 567 total).
This commit is contained in:
Joseph Doherty
2026-06-20 18:02:32 -04:00
parent fd618cf1dc
commit d39089f4ed
19 changed files with 4031 additions and 69 deletions
+200 -3
View File
@@ -5,10 +5,10 @@
| Module | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine` |
| Design doc | `docs/requirements/Component-TemplateEngine.md` |
| Status | Reviewed |
| Last reviewed | 2026-05-28 |
| Last reviewed | 2026-06-20 |
| Reviewer | claude-agent |
| Commit reviewed | `1eb6e97` |
| Open findings | 1 |
| Commit reviewed | `4307c381` |
| Open findings | 0 |
## Summary
@@ -87,6 +87,45 @@ the duplicate-Id / null-sentinel fix from the last batch, and audit-write
ordering inconsistency between `TemplateService` (logs then saves) and
`InstanceService` (saves then logs).
#### Re-review 2026-06-20 (commit `4307c381`) — full review
Re-reviewed the whole module against all ten checklist categories at commit
`4307c381`. The large delta since `1eb6e97` (722 commits, including the
`ScadaLink → ZB.MOM.WW.ScadaBridge` rename, the Script Analysis #25 extraction,
native-alarm-source authoring/flatten, the read-only inheritance-resolve service,
List-attribute semantics, and Transport line-diff) holds up well. All 22 prior
findings remain closed and verified: the deploy gate now delegates to the
authoritative `ScriptTrustValidator` + `RoslynScriptCompiler` (real semantic
compile, no residual substring scan or hand-rolled brace counter in the security
path — `ScriptCompiler` and `ValidationService.CheckExpressionSyntax` both call
the shared analyzer); the recursive composition walk, per-slot alarm override,
duplicate-Id-tolerant `BuildLookup`, `int?`-parent walk, and the revision-hash /
diff `Description`+`Connections`+`ElementDataType` coverage are all in place and
test-guarded. The new `TemplateInheritanceResolver` (M9-T26a) mirrors the
flattener's inheritance precedence exactly (including HiLo per-setpoint merge),
verified by `Resolve_AgreesWithFlatteningService_*` tests. Three new findings
surfaced — all lock/audit asymmetries where a newer feature did not pick up an
invariant an older sibling already enforces: native-alarm-source instance
overrides bypass the lock at flatten time (the unfixed-for-native sibling of the
closed TemplateEngine-008; `ResolvedNativeAlarmSource` carries no `IsLocked`
flag), the four `TemplateFolderService` mutators stage their audit row *after*
the final `SaveChangesAsync` so the row is never persisted, and a dead/misnamed
`SemanticValidator.ParseParameterDefinitions` legacy branch returns types not
names.
| # | Category | Examined | Notes |
|---|----------|----------|-------|
| 1 | Correctness & logic bugs | ✓ | New: native-source instance override applies with no lock check (TemplateEngine-023). Flattening inherit/compose/override precedence + recursion verified correct for attrs/alarms/scripts/native-sources; HiLo per-setpoint merge consistent with resolver. |
| 2 | Akka.NET conventions | ✓ | No actors. `AddTemplateEngineActors` is still an empty placeholder. Nothing to assess. |
| 3 | Concurrency & thread safety | ✓ | Services stateless/scoped; static helpers hold no mutable state. No new findings. |
| 4 | Error handling & resilience | ✓ | `Result<T>` used consistently; `Flatten` wraps in try/catch; JSON parse failures fall back safely. No store-and-forward/failover surface. |
| 5 | Security | ✓ | Deploy gate is now authoritative via Script Analysis #25 (`ScriptTrustValidator.FindViolations` + `RoslynScriptCompiler.Compile`) — alias/`using static`/`global::` bypasses caught by symbol resolution; no residual substring scan in the security path. No new findings. |
| 6 | Performance & resource management | ✓ | TemplateEngine-009 N+1 fix holds (`Compositions` navigation reused). `JsonDocument`/streams disposed. No new leaks. |
| 7 | Design-document adherence | ✓ | Native-alarm-source lock rule ("Lock applies to the entire source") not enforced in flattening (TemplateEngine-023). Otherwise the doc matches: revision-hash/diff payload coverage, semantic validation set, acyclicity, derived-naming all align. |
| 8 | Code organization & conventions | ✓ | New: `TemplateFolderService` Create/Rename/Move/Reorder log after the final save → audit row never persisted (TemplateEngine-024), asymmetric with `TemplateService` (which always saves after `LogAsync`). |
| 9 | Testing coverage | ✓ | Strong overall (inheritance-resolver agrees-with-flattener tests, hash determinism test). Gaps: no test for a locked native source rejecting an instance override; folder tests mock `IAuditService` so the never-saved audit row is invisible; `ParseParameterDefinitions` legacy branch untested. |
| 10 | Documentation & comments | ✓ | New: `ParseParameterDefinitions` is documented as returning parameter NAMES but its legacy-array branch returns the type field (TemplateEngine-025). XML docs otherwise accurate and thorough. |
## Checklist coverage
_Re-review (2026-05-17, `39d737e`):_
@@ -1191,3 +1230,161 @@ false rejected, false→true / true→true / false→false accepted) and
`TemplateServiceTests.UpdateAttribute_LockedInDerivedDowngrade_OnBase_Rejected`
(end-to-end on the attribute update path).
### TemplateEngine-023 — Instance native-alarm-source override bypasses the source lock during flattening
| | |
|--|--|
| Severity | Medium |
| Category | Correctness & logic bugs |
| Status | Deferred |
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:775`, `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs:137` |
**Description**
`ApplyInstanceNativeAlarmSourceOverrides` applies a per-instance native-alarm-source
override unconditionally — it looks the binding up by canonical name and, if found,
overlays the override's non-null fields with **no lock check**:
```csharp
foreach (var ovr in overrides)
{
if (!sources.TryGetValue(ovr.SourceCanonicalName, out var existing))
continue; // Cannot add new bindings via overrides.
sources[ovr.SourceCanonicalName] = existing with { ConnectionName = ..., ... };
}
```
Contrast `ApplyInstanceOverrides` (attributes, `:309``if (existing.IsLocked) continue;`)
and `ApplyInstanceAlarmOverrides` (alarms, `:337``if (existing.IsLocked) continue;`).
The design (`Component-TemplateEngine.md` Override Granularity, Native alarm sources:
*"Lock applies to the entire source"*) requires the same treatment. The root cause is
in Commons: `ResolvedNativeAlarmSource` (`FlattenedConfiguration.cs:137`) carries **no
`IsLocked` field**, so the flattener has nothing to gate on even if it wanted to —
unlike `ResolvedAttribute`/`ResolvedAlarm`, which both expose `IsLocked`. The
inheritance-side `ResolveInheritedNativeAlarmSources` does track a base lock (via the
`lockedNames` set) so a *derived template* cannot override a base-locked source, but
that lock state is dropped on the way out and the *instance* layer is unguarded.
This is the same defect class as the closed **TemplateEngine-008** (which fixed the
identical instance-override lock-bypass for alarms), never applied to native alarm
sources because they were introduced after that finding. The companion gap lives in
`ManagementService.HandleSetInstanceNativeAlarmSourceOverride`
(`ManagementActor.cs:868`), which — unlike `InstanceService.SetAlarmOverrideAsync` /
`SetAttributeOverrideAsync` — performs neither an existence check (an override for an
unknown source name is silently persisted as a dead record) nor a lock check before
writing the override row straight through the repository, bypassing `InstanceService`
entirely. Blast radius is limited (native alarms are a read-only mirror — no write-back
to the source), but the documented lock invariant is not enforced anywhere on this path.
**Recommendation**
Add an `IsLocked` field to `ResolvedNativeAlarmSource` (Commons) and carry it through
`ResolveInheritedNativeAlarmSources` / `ResolveComposedNativeAlarmSources`; then gate
`ApplyInstanceNativeAlarmSourceOverrides` on `if (existing.IsLocked) continue;`,
mirroring the attribute/alarm paths. Add the matching existence + lock guard to
`HandleSetInstanceNativeAlarmSourceOverride` (resolve the effective native-source set
via `TemplateResolver`/flattening as `SetAlarmOverrideAsync` does). Add a flattening
regression test (`Flatten_LockedNativeSource_InstanceOverrideIgnored`) and a management
test rejecting an override of a locked / unknown source.
**Resolution**
Deferred 2026-06-20: the native-alarm-source override lock-bypass fix spans Commons (`ResolvedNativeAlarmSource` has no `IsLocked` field to gate on) and ManagementService (the override handler skips the existence/lock checks), and the blast radius is limited (native alarms are read-only mirrors, no write-back). Recorded as a cross-module follow-up.
### TemplateEngine-024 — `TemplateFolderService` mutators stage their audit row after the final save, so it is never persisted
| | |
|--|--|
| Severity | Medium |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs:58`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs:91`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs:154`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs:204` |
**Description**
`IAuditService.LogAsync` only *stages* the audit row in the EF change tracker — the
implementation (`ConfigurationDatabase/Services/AuditService.cs:61`) ends with
`AddAsync(entry)` and explicitly documents *"caller is responsible for calling
SaveChangesAsync to ensure atomicity."* Four of the five `TemplateFolderService`
mutators call `LogAsync` **after** their final `SaveChangesAsync` and never save again:
- `CreateFolderAsync` (`:56-58`): `AddFolderAsync``SaveChangesAsync``LogAsync` (no save).
- `RenameFolderAsync` (`:89-91`): `UpdateFolderAsync``SaveChangesAsync``LogAsync` (no save).
- `MoveFolderAsync` (`:152-154`): `UpdateFolderAsync``SaveChangesAsync``LogAsync` (no save).
- `ReorderFolderAsync` (`:201-204`): `Update`×2 → `SaveChangesAsync``LogAsync` (no save).
`ManagementActor.ProcessCommand` (`ManagementActor.cs:130`) wraps each command in a DI
scope, dispatches, and disposes the scope with no outer `SaveChangesAsync`; EF Core does
**not** flush on dispose. The folder operation itself persists (it was saved *before* the
`LogAsync`), but the staged `TemplateFolder` audit row is silently discarded when the
scoped `DbContext` is disposed. Every folder Create / Rename / Move / Reorder therefore
leaves **no audit trail**, even though `_auditService.LogAsync(...)` is called and reads
as if it does. (`DeleteFolderAsync` has the same shape but the lost row is the *Delete*
record specifically.) This is asymmetric with `TemplateService`, where every mutator
ends with `LogAsync``SaveChangesAsync` (e.g. `:225-226`). The folder tests mock
`IAuditService` and the repository, so they never exercise the real change-tracker
lifecycle and the gap is invisible to them.
**Recommendation**
Add a final `await _repository.SaveChangesAsync(cancellationToken);` after the `LogAsync`
in all five folder mutators (matching `TemplateService`), or — cleaner — reorder each to
mutate → log → single `SaveChangesAsync` so the entity change and its audit row commit
atomically. Add a test that uses a real (in-memory) `AuditService` + `DbContext` and
asserts an `AuditLogEntry` row exists after a folder create/rename/move/reorder.
**Resolution**
Resolved 2026-06-20 (commit `fd618cf1`): added `await _repository.SaveChangesAsync(...)` after `LogAsync` in all five `TemplateFolderService` mutators (Create/Rename/Move/Reorder/Delete), so folder-mutation audit rows are now persisted (previously staged then discarded with the scope). Matches the `TemplateService` two-save pattern. Tests added.
### TemplateEngine-025 — `SemanticValidator.ParseParameterDefinitions` legacy-array branch returns types, not names; method is dead production code
| | |
|--|--|
| Severity | Low |
| Category | Documentation & comments |
| Status | Resolved |
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs:573` |
**Description**
`ParseParameterDefinitions` is documented and named as returning the declared parameter
**names**:
```csharp
/// ... returns the declared parameter names.
internal static List<string> ParseParameterDefinitions(string? parameterDefinitionsJson)
```
Its JSON-Schema branch correctly returns `props.EnumerateObject().Select(p => p.Name)`,
but its legacy flat-array branch returns the `type` field:
```csharp
else if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return doc.RootElement.EnumerateArray()
.Select(e => e.TryGetProperty("type", out var t) ? t.GetString() ?? "unknown" : "unknown")
.ToList();
}
```
This is a copy-paste from the sibling `ParseParameterTypes` (`:491`, which legitimately
returns types) — so for legacy `[{name,type}]` definitions this method returns the wrong
field (types instead of names). The method has **no non-test caller** in `src/` (only its
own `SemanticValidatorTests`), and the test exercises only the JSON-Schema branch, so the
buggy legacy branch is both dead and untested. The live param-count/type validation path
uses `ParseParameterTypes`, not this method, so there is no behavioural impact today — but
the method is `internal` (callable from the rest of the module) and silently returns
mislabelled data for one input shape.
**Recommendation**
Delete `ParseParameterDefinitions` (and its tests) as dead code, or — if it is meant to
back a future feature — fix the legacy branch to return `e.TryGetProperty("name", ...)`
and add a legacy-array test asserting names are returned.
**Resolution**
Resolved 2026-06-20 (commit `fd618cf1`): deleted the dead, misnamed `SemanticValidator.ParseParameterDefinitions` (no production caller per grep; its only references were two tests, which asserted the buggy type-not-name output). Tests removed.