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 seen1eb6e97/ 2026-05-28) plus first-ever reviews of KpiHistory (#26) and ScriptAnalysis (#25), at HEAD4307c381. 67 new findings (0 Critical, 6 High, 27 Medium, 34 Low). Remediation in commitfd618cf1closed 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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user