fix(security): close Theme 7 — 8 secrets / redaction / append-only findings
Security-sensitive batch, handled main-thread for careful judgment on secret-leak and pepper-bypass paths. Secret leak / pepper bypass: - CD-016 (pepper bypass): InboundApiRepository's GetApiKeyByValueAsync no longer hashes the candidate with the unpeppered ApiKeyHasher.Default — ctor takes a lazy Func<IApiKeyHasher> accessor (lazy so test composition roots without a pepper still bring up the repository), and the DI registration wires sp.GetService<IApiKeyHasher>() so the production peppered hasher matches the stored KeyHash. Regression test asserts positive (peppered roundtrip) AND negative (Default hasher misses the same key — proving the lookup uses the injected hasher). - MgmtSvc-020 (SMTP credential leak): UpdateSmtpConfig/ListSmtpConfigs now project through SmtpConfigPublicShape so the response payload and audit-row afterState never carry the Credentials field — only a HasCredentials bool. The SMTP password / OAuth2 client secret no longer leaves the Admin-only UpdateSmtpConfig boundary the caller already supplied it to. Redaction: - AuditLog-008 (test-fixture under-redact): new SafeDefaultAuditPayloadFilter (stateless singleton) does HTTP header redaction for the always-sensitive defaults (Authorization, X-Api-Key, Cookie, Set-Cookie). FallbackAuditWriter, CentralAuditWriter, and AuditLogIngestActor (both ingest paths) default to it instead of null — composition roots that bypass AddAuditLog can no longer write unredacted auth headers to the audit store. - NotifService-025 (over-mask): CredentialRedactor.Scrub now only masks the last colon-separated component (password / clientSecret) AND only if it's >= 12 chars (typical password heuristic). Short user names like "root" no longer become global redaction tokens that eat unrelated diagnostic text. The full packed string is always masked regardless of length. 3 new negative tests pin the no-over-mask contract. Audit-row correctness / fail-loud: - InboundAPI-025: Program.cs UseWhen predicate now excludes /api/audit, /api/management, /api/centralui, /api/script-analysis AND requires POST — the AuditWriteMiddleware no longer emits spurious ApiInbound rows for audit-log query/export endpoints (write-on-read recursion broken). - ESG-021: ApplyAuth now logs Warning (not silent) on empty AuthConfiguration for apikey/basic, unknown AuthType, and malformed Basic config. AuthConfiguration value NEVER logged. AuthType=none remains silent (documented unauthenticated sentinel). - Security-021: AddSecurity now logs a startup Warning when RequireHttpsCookie=false — an HTTP-only deployment that previously transmitted the cookie-embedded JWT silently in cleartext is now audible in the log. Defensive: - CD-021: SwitchOutPartitionAsync's monthBoundary format string now yyyy-MM-dd HH:mm:ss.fffffff (datetime2(7) precision) so a future sub-second / non-midnight boundary doesn't silently round to the wrong partition. Plus reconciled stale per-module Open-findings counters that had drifted from earlier sessions (AuditLog, CD, ESG, IAPI, MgmtSvc, NotifService, Security). Build clean; all affected test projects green (Host 208, ConfigDB 242, ESG 69, IAPI 151, MgmtSvc 100, NotifService 55, Security 85, AuditLog 247/248 — 1 pre-existing date-sensitive integration test flake on PartitionPurgeTests, unrelated). README regenerated: 46 open (was 54).
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 2 |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -411,9 +411,22 @@ resolution instead.
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Site/FallbackAuditWriter.cs:51-77`, `src/ScadaLink.AuditLog/Central/CentralAuditWriter.cs:77-104`, `src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs:125,155` |
|
||||
|
||||
**Resolution (2026-05-28):** New `SafeDefaultAuditPayloadFilter` in
|
||||
`src/ScadaLink.AuditLog/Payload/` — a stateless singleton that performs HTTP
|
||||
header redaction for the hard-coded sensitive defaults (Authorization,
|
||||
X-Api-Key, Cookie, Set-Cookie). The three writer-chain sites
|
||||
(`FallbackAuditWriter`, `CentralAuditWriter`, `AuditLogIngestActor` —
|
||||
both the audit-only and cached-telemetry paths) now default to
|
||||
`SafeDefaultAuditPayloadFilter.Instance` instead of null when no filter is
|
||||
injected, so a test fixture (or any composition root that bypasses
|
||||
`AddAuditLog`) never persists those headers verbatim. The real
|
||||
`DefaultAuditPayloadFilter` (truncation + body / SQL-param redaction +
|
||||
per-target overrides) is wired by `AddAuditLog` and takes precedence in
|
||||
production.
|
||||
|
||||
**Description**
|
||||
|
||||
`FallbackAuditWriter`, `CentralAuditWriter`, and `AuditLogIngestActor` all accept an
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 3 |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -946,9 +946,22 @@ throws and exactly one row lands.
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/InboundApiRepository.cs:35-39` |
|
||||
|
||||
**Resolution (2026-05-28):** Took option (a) — `InboundApiRepository` ctor now
|
||||
accepts `Func<IApiKeyHasher>? hasherAccessor = null` (deferred resolution to
|
||||
sidestep startup-time pepper-validation in test composition roots that don't
|
||||
configure one). `GetApiKeyByValueAsync` calls `_hasherAccessor()` so the
|
||||
hash matches the production peppered digest. `AddConfigurationDatabase`
|
||||
registers the repository with a factory wiring
|
||||
`() => sp.GetService<IApiKeyHasher>() ?? ApiKeyHasher.Default` — the
|
||||
peppered hasher is used when available, falling back to Default only when
|
||||
none is registered (legacy / test composition). Regression test
|
||||
`CD016_GetApiKeyByValue_UsesInjectedPepperedHasher_NotDefault` asserts
|
||||
positively (peppered roundtrip) and negatively (Default hasher misses the
|
||||
key entirely, proving the lookup uses the injected hasher).
|
||||
|
||||
**Description**
|
||||
|
||||
`GetApiKeyByValueAsync` resolves an API key by its presented plaintext value by hashing
|
||||
@@ -1191,9 +1204,20 @@ converter on the column) so the defence at the read site is no longer required.
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs:192-338` |
|
||||
|
||||
**Resolution (2026-05-28):** Took the targeted (1) part of the recommendation —
|
||||
the `monthBoundary` format string is now `"yyyy-MM-dd HH:mm:ss.fffffff"`
|
||||
matching `datetime2(7)` precision, so a future sub-second or non-midnight
|
||||
boundary won't silently round to the wrong partition. The staging table name
|
||||
is still interpolated (T-SQL DDL identifiers can't be parameterised) but
|
||||
remains internally constructed as `$"AuditLog_Staging_{Guid.NewGuid():N}"`
|
||||
so SQL injection is structurally impossible. The larger
|
||||
`sp_executesql`-with-typed-parameter migration was scoped to a future
|
||||
follow-up since the current shape is fully controlled and the precision-
|
||||
mismatch hazard was the only practical concern.
|
||||
|
||||
**Description**
|
||||
|
||||
`SwitchOutPartitionAsync` builds two large SQL batches via interpolated strings
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 4 |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1180,9 +1180,20 @@ that happens to encode a number (already correctly returns `string`).
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:385-415` |
|
||||
|
||||
**Resolution (2026-05-28):** `ApplyAuth` is now an instance method that uses
|
||||
the existing `_logger`. Three previously-silent fail-open paths now emit a
|
||||
`LogWarning` so an operator debugging a recurring 401 sees the cause inside
|
||||
ScadaLink: (1) empty `AuthConfiguration` for `AuthType=apikey`/`basic`,
|
||||
(2) unknown `AuthType` (anything except `apikey`/`basic`/`none`),
|
||||
(3) malformed Basic config (no `:` separator). The `AuthConfiguration`
|
||||
value is NEVER included in the log message. `AuthType="none"` remains
|
||||
silent — it's the documented sentinel for intentionally-unauthenticated
|
||||
systems. Behaviour is otherwise unchanged: the request still goes out
|
||||
(never block on auth-config issues), the failure mode is just now visible.
|
||||
|
||||
**Description**
|
||||
|
||||
`ApplyAuth` has three fail-open paths that all result in an HTTP request being sent
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 2 |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1233,9 +1233,21 @@ immediate change required; this is a watch-list item.
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:183-185`; consumers: `src/ScadaLink.ManagementService/AuditEndpoints.cs:93-94`; emitter: `src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs:175-252` |
|
||||
|
||||
**Resolution (2026-05-28):** Took the defensive path-exclusion in
|
||||
`Program.cs` (Option 1 from the recommendation). The `UseWhen` predicate
|
||||
now excludes the four known `/api/*` sub-trees that belong to other modules
|
||||
(`/api/audit`, `/api/management`, `/api/centralui`, `/api/script-analysis`)
|
||||
AND additionally requires `POST` — the inbound API method route is the
|
||||
only POST under `/api/`, so future GET-y additions under any of those
|
||||
sub-trees still survive the gate. The endpoint-filter refactor option
|
||||
(move the audit emission into an `IEndpointFilter` co-located with
|
||||
`MapInboundAPI`) was rejected for batch scope — touches more test fixtures
|
||||
and the path-predicate is fragile only against future POST additions on
|
||||
the excluded prefixes, which would be noisy in code review.
|
||||
|
||||
**Description**
|
||||
|
||||
`Program.cs` wires the audit middleware as
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 3 (1 Deferred — see ManagementService-012) |
|
||||
| Open findings | 1 (1 Deferred — see ManagementService-012) |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -927,9 +927,20 @@ mixed-set intersected.
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:1136`–`:1153` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `SmtpConfigPublicShape` projection that
|
||||
returns every non-secret field plus a `HasCredentials` bool — never the
|
||||
`Credentials` field itself. Both `HandleListSmtpConfigs` (broader read
|
||||
access via OperationalAuditRoles) and `HandleUpdateSmtpConfig` (Admin-only
|
||||
write) now project through it. The audit-row `afterState` and the response
|
||||
payload both carry the credential-free shape, so the SMTP password / OAuth2
|
||||
client secret never leaves the `UpdateSmtpConfig` boundary that the caller
|
||||
already supplied them to. ManagementService 100/100 tests still pass.
|
||||
Follow-up to tag `SmtpConfiguration.Credentials` with `[JsonIgnore]` in
|
||||
Commons remains useful belt-and-braces but is out of scope here.
|
||||
|
||||
**Description**
|
||||
|
||||
`HandleUpdateSmtpConfig` reads the existing `SmtpConfiguration` entity, applies the
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 5 |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -813,9 +813,23 @@ After NS-019 is decided:
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/CredentialRedactor.cs:34-48` |
|
||||
|
||||
**Resolution (2026-05-28):** Tightened the policy per the recommendation —
|
||||
only the LAST colon-separated component (password in Basic / clientSecret
|
||||
in OAuth2) is considered a secret, AND only when it's plausibly secret-shaped
|
||||
(>= `MinSecretLength` = 12 chars). The full packed string is always masked
|
||||
regardless of length (its exact appearance can only come from the
|
||||
credential itself). A short user name like `root` (4 chars) or a sender
|
||||
alias like `smtp` (4 chars) no longer becomes a global redaction token
|
||||
that eats unrelated diagnostic text. 3 new negative tests added
|
||||
(`Scrub_ShortUserName_IsNotMaskedOutsidePackedString`,
|
||||
`Scrub_TenantId_IsNotMaskedOutsidePackedString`,
|
||||
`Scrub_FullPackedCredential_IsAlwaysMaskedRegardlessOfLength`); 1
|
||||
existing test bumped its inline password length from 10→16 chars to stay
|
||||
above the new threshold.
|
||||
|
||||
**Description**
|
||||
|
||||
```csharp
|
||||
|
||||
+12
-20
@@ -41,31 +41,31 @@ module file and counted in **Total**.
|
||||
|----------|---------------|
|
||||
| Critical | 0 |
|
||||
| High | 0 |
|
||||
| Medium | 19 |
|
||||
| Low | 35 |
|
||||
| **Total** | **54** |
|
||||
| Medium | 16 |
|
||||
| Low | 30 |
|
||||
| **Total** | **46** |
|
||||
|
||||
## Module Status
|
||||
|
||||
| Module | Last reviewed | Commit | Open (C/H/M/L) | Open | Total |
|
||||
|--------|---------------|--------|----------------|------|-------|
|
||||
| [AuditLog](AuditLog/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/1 | 2 | 11 |
|
||||
| [AuditLog](AuditLog/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/0 | 1 | 11 |
|
||||
| [CLI](CLI/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/2 | 2 | 23 |
|
||||
| [CentralUI](CentralUI/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/3 | 3 | 33 |
|
||||
| [ClusterInfrastructure](ClusterInfrastructure/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/3 | 3 | 14 |
|
||||
| [Commons](Commons/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/4 | 4 | 23 |
|
||||
| [Communication](Communication/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/1 | 1 | 22 |
|
||||
| [ConfigurationDatabase](ConfigurationDatabase/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/2 | 3 | 24 |
|
||||
| [ConfigurationDatabase](ConfigurationDatabase/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/1 | 1 | 24 |
|
||||
| [DataConnectionLayer](DataConnectionLayer/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 22 |
|
||||
| [DeploymentManager](DeploymentManager/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/3 | 3 | 24 |
|
||||
| [ExternalSystemGateway](ExternalSystemGateway/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/1 | 2 | 23 |
|
||||
| [ExternalSystemGateway](ExternalSystemGateway/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/0 | 1 | 23 |
|
||||
| [HealthMonitoring](HealthMonitoring/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/2 | 2 | 23 |
|
||||
| [Host](Host/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/3 | 4 | 22 |
|
||||
| [InboundAPI](InboundAPI/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/1 | 2 | 25 |
|
||||
| [ManagementService](ManagementService/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/2/0 | 2 | 23 |
|
||||
| [InboundAPI](InboundAPI/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/1 | 1 | 25 |
|
||||
| [ManagementService](ManagementService/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/0 | 1 | 23 |
|
||||
| [NotificationOutbox](NotificationOutbox/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/1 | 1 | 10 |
|
||||
| [NotificationService](NotificationService/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/1 | 2 | 25 |
|
||||
| [Security](Security/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/1 | 1 | 21 |
|
||||
| [NotificationService](NotificationService/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/1/0 | 1 | 25 |
|
||||
| [Security](Security/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 21 |
|
||||
| [SiteCallAudit](SiteCallAudit/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/2/1 | 3 | 6 |
|
||||
| [SiteEventLogging](SiteEventLogging/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/2 | 2 | 23 |
|
||||
| [SiteRuntime](SiteRuntime/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/2/0 | 2 | 26 |
|
||||
@@ -88,16 +88,13 @@ _None open._
|
||||
|
||||
_None open._
|
||||
|
||||
### Medium (19)
|
||||
### Medium (16)
|
||||
|
||||
| ID | Module | Title |
|
||||
|----|--------|-------|
|
||||
| AuditLog-001 | [AuditLog](AuditLog/findings.md) | Combined-telemetry transport is plumbed end-to-end but never invoked in production |
|
||||
| ConfigurationDatabase-016 | [ConfigurationDatabase](ConfigurationDatabase/findings.md) | `InboundApiRepository.GetApiKeyByValueAsync` hashes the candidate with the unpeppered `ApiKeyHasher.Default` |
|
||||
| ExternalSystemGateway-020 | [ExternalSystemGateway](ExternalSystemGateway/findings.md) | `JsonElementToParameterValue` silently downcasts non-Int64 JSON numbers to `double`, losing precision for `decimal` SQL parameters on retry |
|
||||
| Host-016 | [Host](Host/findings.md) | Site `CentralContactPoints` second entry targets the site's own remoting port |
|
||||
| InboundAPI-025 | [InboundAPI](InboundAPI/findings.md) | `AuditWriteMiddleware` runs against the entire `/api/*` branch — emits spurious `ApiInbound` audit rows for `/api/audit/query` and `/api/audit/export` |
|
||||
| ManagementService-020 | [ManagementService](ManagementService/findings.md) | UpdateSmtpConfig returns and audits the SMTP Credentials field verbatim |
|
||||
| ManagementService-021 | [ManagementService](ManagementService/findings.md) | Transport bundle handlers have zero test coverage |
|
||||
| NotificationService-024 | [NotificationService](NotificationService/findings.md) | No test affirms the central-only invariant; the orphaned-path tests give a false coverage signal |
|
||||
| SiteCallAudit-001 | [SiteCallAudit](SiteCallAudit/findings.md) | SupervisorStrategy override is dead code; XML claims Resume that is not enforced |
|
||||
@@ -112,11 +109,10 @@ _None open._
|
||||
| TemplateEngine-020 | [TemplateEngine](TemplateEngine/findings.md) | `Create*` audit entries are written with `EntityId = "0"` before `SaveChangesAsync` populates the real key |
|
||||
| Transport-010 | [Transport](Transport/findings.md) | Critical Overwrite + cross-cutting paths uncovered by tests |
|
||||
|
||||
### Low (35)
|
||||
### Low (30)
|
||||
|
||||
| ID | Module | Title |
|
||||
|----|--------|-------|
|
||||
| AuditLog-008 | [AuditLog](AuditLog/findings.md) | Test composition roots that omit `IAuditPayloadFilter` silently pass UNREDACTED payloads through the writer chain |
|
||||
| CLI-020 | [CLI](CLI/findings.md) | `bundle export` success-envelope parse is unguarded |
|
||||
| CLI-022 | [CLI](CLI/findings.md) | `CommandTreeTests` excludes the two new command groups |
|
||||
| CentralUI-029 | [CentralUI](CentralUI/findings.md) | `ConfigurationAuditLog` uses `JS.InvokeAsync<int>("eval", ...)` instead of a dedicated JS module |
|
||||
@@ -130,12 +126,10 @@ _None open._
|
||||
| Commons-020 | [Commons](Commons/findings.md) | Transport types and new Audit-message types have no unit tests in `ScadaLink.Commons.Tests` |
|
||||
| Commons-023 | [Commons](Commons/findings.md) | Trailing-optional `SourceNode` on positional records mixes additive evolution patterns |
|
||||
| Communication-020 | [Communication](Communication/findings.md) | `SiteAddressCacheLoaded` carries mutable `Dictionary`/`List` types |
|
||||
| ConfigurationDatabase-021 | [ConfigurationDatabase](ConfigurationDatabase/findings.md) | `SwitchOutPartitionAsync` interpolates `monthBoundary` / staging table name into raw SQL |
|
||||
| ConfigurationDatabase-024 | [ConfigurationDatabase](ConfigurationDatabase/findings.md) | Missing test coverage for SPLIT-RANGE failure-continuation and production-shape rowversion delete |
|
||||
| DeploymentManager-021 | [DeploymentManager](DeploymentManager/findings.md) | `ResolveSiteIdentifierAsync` silently substitutes the DB id when the site row is missing |
|
||||
| DeploymentManager-022 | [DeploymentManager](DeploymentManager/findings.md) | `Pending` and `InProgress` are written back-to-back with no intervening work |
|
||||
| DeploymentManager-024 | [DeploymentManager](DeploymentManager/findings.md) | Test probe actors hold mutable static state across tests |
|
||||
| ExternalSystemGateway-021 | [ExternalSystemGateway](ExternalSystemGateway/findings.md) | `ApplyAuth` silently sends an unauthenticated request on unknown `AuthType`, empty `AuthConfiguration`, or malformed Basic config |
|
||||
| HealthMonitoring-021 | [HealthMonitoring](HealthMonitoring/findings.md) | `CentralSiteId = "central"` reserved constant silently collides with a real site named "central" |
|
||||
| HealthMonitoring-022 | [HealthMonitoring](HealthMonitoring/findings.md) | `CentralHealthReportLoopTests` uses real-time `PeriodicTimer` + `Task.Delay`; flake-prone on slow CI |
|
||||
| Host-018 | [Host](Host/findings.md) | Shipped per-role configs omit `NodeOptions.NodeName`, leaving `SourceNode` null |
|
||||
@@ -143,8 +137,6 @@ _None open._
|
||||
| Host-021 | [Host](Host/findings.md) | Microsoft `Logging:LogLevel` section in `appsettings.json` is dead config under Serilog |
|
||||
| InboundAPI-023 | [InboundAPI](InboundAPI/findings.md) | `EndpointExtensions.HandleInboundApiRequest` composition wiring has no test coverage |
|
||||
| NotificationOutbox-008 | [NotificationOutbox](NotificationOutbox/findings.md) | `FallbackMaxRetries` / `FallbackRetryDelay` path is unreachable in production AND untested |
|
||||
| NotificationService-025 | [NotificationService](NotificationService/findings.md) | `CredentialRedactor` over-masks: any 4-character credential component is masked anywhere it appears, including unrelated log text |
|
||||
| Security-021 | [Security](Security/findings.md) | `RequireHttpsCookie=false` dev opt-out has no warning path — an HTTP production deployment silently transmits the JWT bearer credential in cleartext |
|
||||
| SiteCallAudit-006 | [SiteCallAudit](SiteCallAudit/findings.md) | Stuck-only paging test does not exercise the multi-page boundary with an interleaved non-stuck row at the cursor |
|
||||
| SiteEventLogging-018 | [SiteEventLogging](SiteEventLogging/findings.md) | `FailedWriteCount` is exposed but never consumed by Health Monitoring |
|
||||
| SiteEventLogging-023 | [SiteEventLogging](SiteEventLogging/findings.md) | Concurrent-stress test uses a non-volatile `stop` flag |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 1 (Security-021); 1 deferred (Security-008) |
|
||||
| Open findings | 0 (1 deferred — Security-008) |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -971,9 +971,21 @@ every required `SecurityOptions` field is enforced at startup, not at first use.
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/SecurityOptions.cs:100-108`; `src/ScadaLink.Security/ServiceCollectionExtensions.cs:54-59` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `ILoggerFactory` to the cookie-options
|
||||
`Configure` callback in `AddSecurity` so an explicit `RequireHttpsCookie=false`
|
||||
opt-out now emits a startup `LogWarning` ("auth cookie SecurePolicy is
|
||||
SameAsRequest. The cookie-embedded JWT will be transmitted in cleartext
|
||||
over plain HTTP. This setting is intended for local dev only — set
|
||||
SecurityOptions:RequireHttpsCookie=true in production."). Default is still
|
||||
`true`, so production deployments unchanged. The "fail startup when
|
||||
RequireHttpsCookie=false AND ASPNETCORE_ENVIRONMENT=Production" hard-stop
|
||||
option was not implemented (the dev Docker cluster intentionally runs with
|
||||
the flag false, and the env-var name varies across deploy mechanisms);
|
||||
the warning is the right ergonomic floor.
|
||||
|
||||
**Description**
|
||||
|
||||
The Security-002 fix added `RequireHttpsCookie` (default `true`) so the auth cookie's
|
||||
|
||||
Reference in New Issue
Block a user