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
+150 -2
View File
@@ -5,9 +5,9 @@
| Module | `src/ZB.MOM.WW.ScadaBridge.NotificationService` |
| Design doc | `docs/requirements/Component-NotificationService.md` |
| Status | Reviewed |
| Last reviewed | 2026-05-28 |
| Last reviewed | 2026-06-20 |
| Reviewer | claude-agent |
| Commit reviewed | `1eb6e97` |
| Commit reviewed | `4307c381` |
| Open findings | 0 |
## Summary
@@ -100,6 +100,39 @@ and `CredentialRedactor` masks any component of the credential string that is
≥ 4 characters long — a 4-character user name like `root` or a 4-char tenant
prefix could be aggressively scrubbed out of unrelated log text (NS-025).
#### Re-review 2026-06-20 (commit `4307c381`) — full review
Re-reviewed the whole current state. Since `1eb6e97` the module was renamed
(`ScadaLink → ZB.MOM.WW.ScadaBridge`, so the diff shows every file as "added" at
the new path), gained complete XML-doc coverage, and — most importantly — **NS-021
is now RESOLVED**: `ISmtpClientWrapper.AuthenticateAsync` threads an `oauth2UserName`
and the OAuth2 branch builds `new SaslMechanismOAuth2(oauth2UserName, token)`,
throwing `SmtpPermanentException` if the identity is empty; the real production
caller `EmailNotificationDeliveryAdapter.SendAsync` passes `config.FromAddress`
(`src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs:196`),
and `MailKitSmtpClientWrapperTests` pins both the throw and the XOAUTH2 framing.
All ten prior NS-019..NS-025 findings hold; the module is now cleanly scoped to the
shared central SMTP primitives (no orphaned site-delivery code). The re-review
surfaced **three new findings**, none above Medium: the OAuth2 token endpoint and
scope are hardcoded to Microsoft 365 so no other OAuth2 provider can authenticate
despite the design doc promising "other modern SMTP providers" (NS-026), one test
file is misplaced in this test project (its system-under-test lives in
NotificationOutbox — NS-027), and one test's XML doc still names the
NS-019-deleted `NotificationDeliveryService` (NS-028).
| # | Category | Examined | Notes |
|---|----------|----------|-------|
| 1 | Correctness & logic bugs | ☑ | NS-021 now resolved (oauth2 user identity threaded + tested). Classification/redaction correct. No new correctness defect. |
| 2 | Akka.NET conventions | ☑ | No actors. `AddNotificationServiceActors` is a documented no-op. |
| 3 | Concurrency & thread safety | ☑ | `OAuth2TokenService` per-credential `ConcurrentDictionary` + per-entry `SemaphoreSlim` double-check is correct; concurrent-fetch test passes. No issues. |
| 4 | Error handling & resilience | ☑ | Delivery error path now lives entirely in NotificationOutbox; the shared `SmtpErrorClassifier` is sound (typed exceptions + numeric SMTP code, cancellation excluded). No issues here. |
| 5 | Security | ☑ | OAuth2 secret/token never logged (tenant only); `CredentialRedactor` tightened (NS-025). At-rest encryption of `SmtpConfiguration.Credentials` still NS-013 Deferred (Commons-scoped). OAuth2 endpoint hardcoded to M365 (NS-026). |
| 6 | Performance & resource management | ☑ | Single `SmtpClient` per wrapper, disposed by caller's `using`; per-delivery factory documented (NS-022). No leak. |
| 7 | Design-document adherence | ☑ | Central-only redesign clean. New gap: OAuth2 hardcoded to M365 token URL + outlook.office365.com scope contradicts "other modern SMTP providers" in the doc (NS-026). |
| 8 | Code organization & conventions | ☑ | One-type-per-file holds. `NotificationOutboxKpiSampleSourceTests.cs` is in this test project but its SUT is in NotificationOutbox (NS-027). |
| 9 | Testing coverage | ☑ | Primitives well covered (classifier, TLS parser, redactor, OAuth2 token cache/refresh/concurrency, auth-wrapper negative paths incl. NS-021). No false-coverage; orphaned tests gone (NS-024). |
| 10 | Documentation & comments | ☑ | XML docs accurate after NS-019/023, EXCEPT `SmtpErrorClassifierTests.cs:11` still references the deleted `NotificationDeliveryService` (NS-028). |
## Checklist coverage
| # | Category | Examined | Notes |
@@ -860,3 +893,118 @@ The threshold is a defence-in-depth choice; the existing tests assert that `Hunt
**Recommendation**
Tighten the redaction policy: mask only the obviously-secret components — the password (Basic), the client secret (OAuth2), and the whole packed string — not the user name / tenant / client id. The simplest implementation is to redact only the **last** colon-separated component (the secret) plus the full packed string. Bump the per-component minimum length to something high enough that a typical short user name does not match (≥ 12 chars is the usual heuristic for a password). Add a test asserting `Scrub("/root/.config", "root:hunter2")` does not mask `/root/.config`'s `root`.
### NotificationService-026 — OAuth2 token endpoint and scope are hardcoded to Microsoft 365; no other OAuth2 SMTP provider can authenticate
| | |
|--|--|
| Severity | Medium |
| Category | Design-document adherence |
| Status | Deferred |
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/OAuth2TokenService.cs:73`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/OAuth2TokenService.cs:80` |
**Description**
`OAuth2TokenService.GetTokenAsync` constructs the token URL as
`$"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"` (`:73`) and
requests the fixed scope `"https://outlook.office365.com/.default"` (`:80`). Both
are Microsoft-365-specific: the authority host is Azure AD's, and the scope is the
Outlook/Office-365 SMTP-send resource. The design doc (`Component-NotificationService.md`,
"Email Server Configuration") states the OAuth2 Client Credentials mode is
"For Microsoft 365 **and other modern SMTP providers** that require OAuth2", and
CLAUDE.md's External Integrations decision likewise says "OAuth2 Client Credentials
(Microsoft 365) **or** Basic Auth". As implemented, only Microsoft 365 can ever
work: a Google Workspace, on-prem Keycloak/ADFS, or any non-Azure OAuth2 SMTP relay
would need a different authority host and a different scope, neither of which is
configurable. The credential triple carries only `tenantId:clientId:clientSecret`
— there is no authority-host or scope field to override either constant. An operator
who configures an `oauth2` SMTP config pointing at a non-M365 provider gets a token
request sent to `login.microsoftonline.com` with `{tenantId}` interpolated as a path
segment, which fails (404/400) and surfaces as a permanent delivery failure with no
hint that the provider is simply unsupported.
This is latent rather than actively broken — the only documented/tested target is
M365 — but it contradicts the design doc's stated breadth and is the kind of gap that
silently blocks a future deployment against a non-Microsoft tenant.
**Recommendation**
Either (a) make the authority host and scope configurable — add optional
`OAuth2Authority`/`OAuth2Scope` fields to `SmtpConfiguration` (defaulting to the M365
values) and thread them into `GetTokenAsync` — and update the cache key to include
them; or (b) narrow the design doc and CLAUDE.md to say OAuth2 Client Credentials is
**Microsoft 365 only** and document the M365-only constraint on `OAuth2TokenService`,
so an operator is not misled into configuring an unsupported provider. Option (b) is
the smaller change if M365-only is acceptable for v1.
**Resolution**
Deferred 2026-06-20: the OAuth2 token endpoint + scope are hardcoded to Microsoft 365 while the doc implies multi-provider support. Whether to make authority/scope configurable or narrow the doc to 'Microsoft 365 only' for v1 is a product/scope decision. Latent today (only M365 is deployed/tested). Recorded for that decision.
### NotificationService-027 — `NotificationOutboxKpiSampleSourceTests` is in the wrong test project (its system-under-test lives in NotificationOutbox)
| | |
|--|--|
| Severity | Low |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/Kpi/NotificationOutboxKpiSampleSourceTests.cs:1`, `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Kpi/NotificationOutboxKpiSampleSource.cs` |
**Description**
`tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/Kpi/NotificationOutboxKpiSampleSourceTests.cs`
was added (commit `0d6c026d`, "K6 — NotificationOutbox sample source") inside the
**NotificationService** test project, but the class it exercises —
`NotificationOutboxKpiSampleSource` — lives in the **NotificationOutbox** project
(`src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Kpi/NotificationOutboxKpiSampleSource.cs`),
confirmed by a repo-wide grep showing the type defined only there. A module's test
project should test that module: this test belongs in
`tests/ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests`. As placed, it forces the
NotificationService test project to reference the NotificationOutbox project, blurs
the module boundary the review process treats as the unit of review, and means a
reviewer scoping "the NotificationService tests" picks up coverage for a different
component. It also inflates this module's apparent test count with tests that assert
nothing about NotificationService.
**Recommendation**
Move `Kpi/NotificationOutboxKpiSampleSourceTests.cs` to
`tests/ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests/Kpi/`, and drop the
NotificationOutbox project reference from `ZB.MOM.WW.ScadaBridge.NotificationService.Tests.csproj`
if nothing else in that test project needs it.
**Resolution**
Resolved 2026-06-20 (commit `fd618cf1`): moved `NotificationOutboxKpiSampleSourceTests` from NotificationService.Tests into NotificationOutbox.Tests (where its SUT lives), with the namespace adjusted to that project's convention. Passes in its new home.
### NotificationService-028 — Stale XML cross-reference: `SmtpErrorClassifierTests` still names the NS-019-deleted `NotificationDeliveryService`
| | |
|--|--|
| Severity | Low |
| Category | Documentation & comments |
| Status | Resolved |
| Location | `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/SmtpErrorClassifierTests.cs:11` |
**Description**
The class-summary XML doc on `SmtpErrorClassifierTests` says the classification
policy "is shared between `<see cref="NotificationDeliveryService"/>` and the central
outbox's `EmailNotificationDeliveryAdapter`". `NotificationDeliveryService` was
deleted as part of the NS-019 resolution (the orphaned site-side sender), so this
cross-reference points at a type that no longer exists. The `<see cref=...>` will not
resolve and the sentence describes a sharing relationship that ended with the
central-only redesign — the classifier is now consumed only by
`EmailNotificationDeliveryAdapter`. This is the same stale-doc class NS-023 cleaned up
in the production source, missed in this one test file. Minor, but misleading to a
maintainer and a dangling `cref`.
**Recommendation**
Drop the `NotificationDeliveryService` reference from the summary; state that the
classifier is shared by the central `EmailNotificationDeliveryAdapter` (and is a
reusable primitive of this module), consistent with the NS-019/NS-023 cleanup.
**Resolution**
Resolved 2026-06-20 (commit `fd618cf1`): removed the dangling `<see cref="NotificationDeliveryService"/>` (type deleted by NS-019) from the `SmtpErrorClassifierTests` XML doc, repointing the reference to the actual production caller. Doc-only.