Skip to content

B3 Phase 2: always-safe DB-config Apply Fix (ALTER DATABASE SET)#1039

Merged
erikdarlingdata merged 1 commit into
devfrom
feature/b3-phase2-dbconfig
Jun 1, 2026
Merged

B3 Phase 2: always-safe DB-config Apply Fix (ALTER DATABASE SET)#1039
erikdarlingdata merged 1 commit into
devfrom
feature/b3-phase2-dbconfig

Conversation

@erikdarlingdata
Copy link
Copy Markdown
Owner

What shipped

The second privileged Apply Fix type — the three always-safe database settings, applied via gated ALTER DATABASE <db> SET …:

  • AUTO_SHRINK OFF
  • AUTO_CLOSE OFF
  • PAGE_VERIFY CHECKSUM

RCSI is excluded (destructive — never emitted by the extractor); recovery_model/query_store are out of scope. Apply-only (no un-apply for DB-config — these have no sensible reverse; the prior value is recorded for manual reversal). Reuses the Phase-1 remediation framework verbatim and adds one handler + the model/extractor/executor/schema/UI deltas. Single PR per plan §10.

Implements the approved plan b3-phase2-dbconfig.md (cleared two adversarial security review rounds).

Security invariants enforced (file:line)

  1. Identifier safety (§1): the executed ALTER DATABASE statement's ONLY non-constant token is the DB identifier — (a) validated by a parameterized sys.databases existence check (@db NVARCHAR(128), no concat) at Dashboard/Services/DatabaseService.Remediation.cs:441-452; (b) bracketed by an inline ]]] routine QuoteIdentifier at :325 (its OWN routine — FactRemediation.QuoteName is private in PerformanceMonitor.Analysis, m-B); (c) the SET clause is a hardcoded literal chosen by switch over DbConfigSetting in SetClauseFor at :333. Same-string invariant (M-1): ValidatedName is set to the exact @db value only on existence (:465) and is the exact string passed to BuildAlterStatement (:357, executed at :344).
  2. Single-connection self-gating (§2 / R2-MOD-1): existence + permission (HAS_PERMS_BY_NAME(@db,'DATABASE','ALTER') wrapped ISNULL(...,0), :448) + live freshness read + the ALTER all on ONE monitoring connection — no InitialCatalog retarget, no re-open, no elevation (SetDatabaseOptionAsync, :300-388). GateSpid/ExecSpid emitted (:385-386).
  3. Audit integrity (§7): 02_ create script amended in place (2.12.0 unreleased — no tag, csprojs 2.11.0): query_id/plan_id → NULL, action varchar(16)→(32), prior_value nvarchar(128) added (upgrades/2.11.0-to-2.12.0/02_create_remediation_action_log.sql:56-59). Writer passes DBNull for null IDs (RemediationAuditWriter.cs:151-152) and widens @action to VarChar,32 + adds @prior_value (:155-157). RemediationAuditRecord.QueryId/PlanId → long? + PriorValue (RemediationResults.cs:154-157).
  4. Reachability guard (M-2): "DbConfigHandler", "SetDatabaseOptionAsync", "PreflightDbConfigAsync" added to CoreMachineryMarkers (Dashboard.Tests/RemediationTests.cs:380-383); CoreMachinery_OnlyReferencedInRemediationCore now covers the new surface.
  5. Deserialize threading (m-A): FromDto deserializes DbConfigTargets and passes them to the 4-arg ctor (PerformanceMonitor.Notifications/AlertContext.cs:189-203).
  6. Un-apply fail-safe (m-C): RunAsync short-circuits to a clean UnapplyNotSupported report when isUnapply && !handler.SupportsUnapply (RemediationApplyService.cs:171-178) — never bubbles NotSupportedException.

Both drill-down collectors enriched with structured auto_shrink/auto_close/page_verify fields (identical JSON names+types) so the shared extractor reads typed fields, never the human issues strings (SqlServerDrillDownCollector.cs, Lite/Analysis/DrillDownCollector.cs).

Headless test results (real)

  • dotnet build Dashboard/Dashboard.csproj -c Debugclean (0 warnings, 0 errors)
  • Dashboard.Tests124 passed, 0 failed, 0 skipped
  • Lite.Tests310 passed, 0 failed, 0 skipped
  • Analysis + Notifications libs build clean.

New tests cover: no-injection/QUOTENAME against the executor's own builder, same-string invariant (trailing-whitespace), extractor (never RCSI / multi-setting / empty-db / RCSI-only→null), render-stability golden, audit-absent hard-block, fail-closed-on-no-ALTER, freshness-skip, DB-not-found, success (null IDs + prior_value + precise taxonomy), applied-but-unlogged, per-target independence, SupportsUnapply, apply-only enforcement, confirm-path threading, AnyActionable, serialization round-trip (DbConfigTargets survive — m-A), CoreMachineryMarkers coverage.

Note: the single-connection SPID-equality and the writer/column no-truncation are executor/real-server concerns (SPID equality cannot be proven against a faked executor) — see the maintainer checklist below.

Maintainer's real-server pre-merge gate (plan §11, steps 1-6)

I did NOT run sql2022 — these fold into your end-to-end pass (same as Phase 1):

  1. Scratch DB AUTO_SHRINK ON → apply → is_auto_shrink_on=0, audit row action='set_auto_shrink_off', result='success', prior_value='ON', generated_sql matches executed statement. Re-apply → freshness-skip (skipped, no second ALTER).
  2. Multi-setting (AUTO_SHRINK ON + AUTO_CLOSE ON) → two ALTERs, two success rows, both flags 0.
  3. PAGE_VERIFY NONECHECKSUM, prior_value='NONE'; separate run from TORN_PAGE_DETECTION confirms distinct prior.
  4. Fail-closed on a db_owner-on-PerformanceMonitor-only login → PermissionDenied, no ALTER, grant guidance.
  5. Drop scratch DB between finding and apply → Blocked, no ALTER.
  6. Audit-absent server → hard block, no mutation.
    • Verify the amended 02_ via an Installer built at 2.12.0: table created with query_id/plan_id NULLABLE, action varchar(32), prior_value nvarchar(128); a set_page_verify_checksum row stores untruncated; re-run is a no-op. O1 boundary: if v2.12.0 tags before this lands, switch to a 03_ ALTER in 2.12.0-to-2.13.0/.

🤖 Generated with Claude Code

Adds the second privileged "Apply Fix" type — the three always-safe
database settings AUTO_SHRINK OFF, AUTO_CLOSE OFF, PAGE_VERIFY CHECKSUM —
reusing the Phase-1 remediation framework verbatim. RCSI is excluded
(destructive); recovery_model/query_store are out of scope. Apply-only
(no un-apply for DB-config).

Security invariants enforced:
- Identifier safety (§1): the executed ALTER DATABASE statement's ONLY
  non-constant token is the database identifier, which is (a) validated
  by a PARAMETERIZED sys.databases existence check (@db NVARCHAR(128)),
  (b) bracketed by an inline ]-doubling routine in the Dashboard executor
  (FactRemediation.QuoteName is private in another assembly — m-B), and
  (c) the SET clause is a hardcoded compile-time literal chosen by a
  switch over DbConfigSetting. Same-string invariant (M-1): the validated
  @db string is byte-identical to the bracketed token.
- Single-connection self-gating (§2/R2-MOD-1): existence + permission
  (HAS_PERMS_BY_NAME(@db,'DATABASE','ALTER') wrapped ISNULL(...,0), fail
  closed) + live freshness read + the ALTER all run on ONE monitoring
  connection (no InitialCatalog retarget, no elevation). GateSpid/ExecSpid
  emitted for the SPID-equality proof.
- Audit integrity (§7): 02_ create script amended in place (2.12.0
  unreleased) — query_id/plan_id NULLABLE, action varchar(16->32),
  prior_value nvarchar(128) added; writer passes DBNull for null IDs and
  widens the @action SqlParameter to VarChar(32) (the prior 16 truncated
  the new taxonomy before the column).
- Reachability guard (M-2): DbConfigHandler, SetDatabaseOptionAsync,
  PreflightDbConfigAsync added to CoreMachineryMarkers.
- Deserialize threading (m-A): FromDto passes the deserialized
  DbConfigTargets to the ctor (round-trip test asserts they survive).
- Un-apply fail-safe (m-C): RunAsync short-circuits to a clean report when
  isUnapply && !SupportsUnapply (never bubbles NotSupportedException).

Both drill-down collectors enriched with structured auto_shrink/auto_close
/page_verify fields (identical JSON names+types) so the shared extractor
reads typed fields, never the human issues strings. Confirm/preview path
threaded for DB_CONFIG (BuildConfirmRequest, RenderPreview, confirm-window
rows + dispositions, banner). 124 Dashboard tests + 310 Lite tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit 20636dd into dev Jun 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant