Reviewed all 31 src/ production projects against the 10-category checklist in REVIEW-PROCESS.md. Each module gets its own findings.md; code-reviews/README.md is regenerated from them. 334 findings: 6 Critical, 46 High, 126 Medium, 156 Low. Critical findings: - Server-001: WriteNodeIdUnknown recurses unconditionally — a HistoryRead on an unresolvable node crashes the process (remote DoS). - Admin-001/002: app-wide auth bypass (RouteView not AuthorizeRouteView) plus unauthenticated mutating routes. - Core.Scripting-001: System.Environment reachable from operator scripts; Environment.Exit() terminates the server. - Core.AlarmHistorian-001: rowIds/events parallel-list desync on a corrupt payload misapplies outcomes — silent alarm-event data loss. - Driver.Galaxy-001: ReconnectSupervisor is built but never triggered, so a transient gateway drop permanently kills the event stream. All findings are Status=Open; resolution is tracked per REVIEW-PROCESS.md section 4. Review only — no source code changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.8 KiB
Code Review — Driver.S7.Cli
| Field | Value |
|---|---|
| Module | src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli |
| Reviewer | Claude Code |
| Review date | 2026-05-22 |
| Commit reviewed | 76d35d1 |
| Status | Reviewed |
| Open findings | 7 |
Checklist coverage
A comprehensive review completes every category, recording "No issues found" where a category produced nothing rather than leaving it blank.
| # | Category | Result |
|---|---|---|
| 1 | Correctness & logic bugs | Driver.S7.Cli-001, Driver.S7.Cli-002 |
| 2 | OtOpcUa conventions | No issues found |
| 3 | Concurrency & thread safety | No issues found |
| 4 | Error handling & resilience | Driver.S7.Cli-001, Driver.S7.Cli-003 |
| 5 | Security | No issues found |
| 6 | Performance & resource management | Driver.S7.Cli-004 |
| 7 | Design-document adherence | Driver.S7.Cli-002 |
| 8 | Code organization & conventions | Driver.S7.Cli-005 |
| 9 | Testing coverage | Driver.S7.Cli-006 |
| 10 | Documentation & comments | Driver.S7.Cli-002, Driver.S7.Cli-007 |
Findings
Driver.S7.Cli-001
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Error handling & resilience |
| Location | src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs:65-80 |
| Status | Open |
Description: WriteCommand.ParseValue parses numeric and DateTime values with the
raw BCL parsers (short.Parse, float.Parse, DateTime.Parse, etc.). On malformed
input these throw FormatException / OverflowException, which are not
CliFx.Exceptions.CommandException. CliFx renders a CommandException as a clean
one-line error with a non-zero exit code, but renders any other exception as a full
.NET stack trace. The ParseValue bool path is handled correctly (it throws
CommandException for unrecognised input), so the command is internally inconsistent:
write -t Bool -v maybe gives a friendly message while write -t Int16 -v xyz dumps a
stack trace. The module own test ParseValue_non_numeric_for_numeric_types_throws
asserts the raw FormatException leaks, confirming the behaviour is unintended-but-shipped.
Recommendation: Wrap the numeric / DateTime parses in a try/catch that
re-throws FormatException and OverflowException as
CliFx.Exceptions.CommandException with a message that names the --type and the
offending value — matching the bool path. Update the test to expect CommandException.
Resolution: (open)
Driver.S7.Cli-002
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Design-document adherence |
| Location | src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs:22-29, Commands/WriteCommand.cs:21-33, Commands/SubscribeCommand.cs:18-21; docs/Driver.S7.Cli.md:70-73,80-81 |
| Status | Open |
Description: The --type option help text on read, write, and subscribe
advertises the full S7DataType set (Int64 / UInt64 / Float64 / String / DateTime),
and docs/Driver.S7.Cli.md shows a worked read ... -t String --string-length 80
example plus a --string-length flag on read/write. The underlying S7Driver
(S7Driver.cs:241-245 for reads, :316-320 for writes) throws NotSupportedException
for Int64, UInt64, Float64, String, and DateTime — the driver maps that to
BadNotSupported. Consequently every CLI invocation using one of those types — and the
documented --string-length string-read example — fails at runtime with
0x803D0000 (Bad). The CLI surface and docs promise capability the driver does not yet
implement.
Recommendation: Either (a) trim the --type help text and the --string-length
flag/examples to the implemented set (Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32) until the follow-up driver PR lands, or (b) keep the surface but add a one-line
"types beyond Float32 are not yet implemented and surface BadNotSupported" caveat to the
help text and docs/Driver.S7.Cli.md. Option (a) is preferred so the CLI does not offer
options that cannot succeed.
Resolution: (open)
Driver.S7.Cli-003
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Error handling & resilience |
| Location | src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs:38-50 |
| Status | Open |
Description: ProbeCommand XML doc and the Driver.S7.Cli.md "fastest is the
device talking" framing say the probe "connects ... prints health" and "surfaces
BadNotSupported" when PUT/GET is disabled. But when the PLC is unreachable (connection
refused, host down, wrong slot), driver.InitializeAsync throws and the exception
propagates straight out of ExecuteAsync — the code that prints Host:, Health:,
Last error:, and the snapshot is never reached. The most common probe failure (device
not reachable at all) therefore produces a CliFx stack trace rather than the structured
health report the command exists to give. Note PUT/GET-disabled only surfaces during
ReadAsync (after a successful connect), so that one path does reach the health print —
but a refused TCP connect does not.
Recommendation: Wrap the InitializeAsync + ReadAsync body in a try/catch that,
on failure, still prints the Host: / CPU: lines and a Health: / Last error:
report derived from driver.GetHealth() (which InitializeAsync sets to
Faulted with the exception message before re-throwing). The probe should report an
unreachable device, not crash on it.
Resolution: (open)
Driver.S7.Cli-004
| Field | Value |
|---|---|
| Severity | Low |
| Category | Performance & resource management |
| Location | src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs:36,53, Commands/ReadCommand.cs:45,54, Commands/WriteCommand.cs:51,60, Commands/SubscribeCommand.cs:39,73 |
| Status | Open |
Description: Every command declares the driver with await using var driver = new S7Driver(...) and also calls await driver.ShutdownAsync(...) in a finally block.
S7Driver.DisposeAsync itself calls ShutdownAsync, so shutdown runs twice per command
(three times for subscribe, which also unsubscribes). ShutdownAsync is idempotent
(Plc?.Close() is best-effort, _subscriptions is cleared) so there is no functional
bug, but the explicit finally-block ShutdownAsync call is redundant given the
await using. It is also slightly misleading — a reader may assume the await using is
not actually disposing.
Recommendation: Drop the explicit await driver.ShutdownAsync(...) from the
finally blocks and rely on await using for teardown; keep only the
subscribe command UnsubscribeAsync. Alternatively drop await using
and keep the explicit finally. Pick one disposal mechanism per command.
Resolution: (open)
Driver.S7.Cli-005
| Field | Value |
|---|---|
| Severity | Low |
| Category | Code organization & conventions |
| Location | tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ |
| Status | Open |
Description: A stale directory tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/
exists containing only an obj/ folder — no .csproj, no source. The real test
project lives at tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/. The empty
directory is a leftover from the project move into tests/Drivers/Cli/ and is not
referenced by ZB.MOM.WW.OtOpcUa.slnx. It is dead clutter that can mislead anyone
grepping the tree for the S7 CLI test project.
Recommendation: Delete the stale tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/
directory (including its obj/). This is outside the module src/ tree but is the
S7 CLI own orphaned test folder, so it belongs to this module cleanup.
Resolution: (open)
Driver.S7.Cli-006
| Field | Value |
|---|---|
| Severity | Low |
| Category | Testing coverage |
| Location | tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/WriteCommandParseValueTests.cs |
| Status | Open |
Description: The only test file covers WriteCommand.ParseValue and
ReadCommand.SynthesiseTagName. S7CommandBase.BuildOptions — which maps the
host / port / CPU / rack / slot / timeout flags onto an S7DriverOptions and forces
Probe.Enabled = false — has no test, despite being pure, deterministic, and
internal-visible to the test assembly via InternalsVisibleTo. A regression that
dropped Probe = new S7ProbeOptions { Enabled = false } (which would start an
unwanted background probe loop in a one-shot CLI run) or mis-mapped TimeoutMs would
not be caught. ParseValue is also missing an explicit overflow-edge test (e.g.
Byte value 256) — the current ParseValue_Byte_ranges test stops at 255.
Recommendation: Add a BuildOptions test (assert Probe.Enabled == false,
Timeout matches TimeoutMs, and host/port/CPU/rack/slot flow through). Add an
overflow case to the ParseValue numeric tests once Driver.S7.Cli-001 is resolved so
the test asserts the wrapped CommandException.
Resolution: (open)
Driver.S7.Cli-007
| Field | Value |
|---|---|
| Severity | Low |
| Category | Documentation & comments |
| Location | src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs:45-51 |
| Status | Open |
Description: The Modbus CLI SubscribeCommand carries an explanatory comment on
the OnDataChange handler ("Route every data-change event to the CliFx console (not
System.Console — the analyzer flags it + IConsole is the testable abstraction)"). The S7
SubscribeCommand is a near-verbatim copy but dropped that comment, so the non-obvious
reason the handler uses console.Output.WriteLine (synchronous, on a driver background
thread) instead of System.Console or the async WriteLineAsync is undocumented here.
Minor, but the rationale is worth keeping consistent across the CLI family.
Recommendation: Re-add the one-line comment from the Modbus SubscribeCommand so
the S7 copy explains why the event handler writes via console.Output synchronously.
Resolution: (open)