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,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.
|
||||
|
||||
Reference in New Issue
Block a user