Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 260 additions & 0 deletions Darling/Darling.Tests/PlanForceActionAuditRedactionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using PerformanceMonitor.Analysis;
using PerformanceMonitor.Darling.Service;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// #4346: <c>PgPlanForceActionStore.SanitizeDetailForAudit</c> is the read-side gate that keeps
/// pre-#4326 rows' raw exception text out of <c>get_plan_force_actions</c> (and any other reader of
/// <c>GetRecentActionsAsync</c>). There is no build marker on the row, so the gate is a positive match
/// against the one line #4326 made the only safe shape for a <c>state_unavailable</c> evidence line;
/// anything else on that line is legacy and gets replaced.
/// </summary>
public sealed class PlanForceActionAuditRedactionTests
{
[Fact]
public void PostFixLine_PassesThrough_TypeOnly()
{
var detail = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
Assert.Equal(detail, PgPlanForceActionStore.SanitizeDetailForAudit(detail));
}

[Fact]
public void PostFixLine_PassesThrough_WithSqlState()
{
var detail = "state_unavailable: the forcing and automatic-plan-correction state read failed (PostgresException, SQLSTATE 57014; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
Assert.Equal(detail, PgPlanForceActionStore.SanitizeDetailForAudit(detail));
}

[Fact]
public void PostFixLine_PassesThrough_MissingSchemaNote()
{
var detail = "state_unavailable: the forcing and automatic-plan-correction state read failed (PostgresException, SQLSTATE 42P01; a table or column the read needs is missing, logged only at Debug level) \u2014 an unattended force cannot proceed on an unknown engine state";
Assert.Equal(detail, PgPlanForceActionStore.SanitizeDetailForAudit(detail));
}

/// <summary>The pin: a pre-#4326 row's raw exception text (host, credential, relation names — anything
/// an exception message can carry) never reaches the caller. This is the exact shape
/// <c>PgPlanForceActionStore.TryGetTargetStatesAsync</c>'s old catch wrote before #4326.</summary>
[Fact]
public void LegacyLine_WithRawExceptionText_IsRedacted()
{
var legacy = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException: 28P01: password authentication failed for user \"darling_ro\" at host db-primary-02.internal)";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(legacy);

Assert.DoesNotContain("darling_ro", sanitized);
Assert.DoesNotContain("db-primary-02", sanitized);
Assert.DoesNotContain("password authentication failed", sanitized);
Assert.Equal(
"state_unavailable: the forcing and automatic-plan-correction state read failed \u2014 an unattended force cannot proceed on an unknown engine state",
sanitized);
}

[Fact]
public void OtherBlockerLines_AreNeverTouched()
{
var detail = "apc_owns_it: query_store_stats: plan 7 is_forced_plan = true\nstate_unavailable: garbage (Exception: whatever leaked)";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(detail)!;
var lines = sanitized.Split('\n');

Assert.Equal("apc_owns_it: query_store_stats: plan 7 is_forced_plan = true", lines[0]);
Assert.DoesNotContain("whatever leaked", lines[1]);
}

[Fact]
public void NonStateUnavailableDetail_IsUnchanged()
{
var detail = "no write path in this build (#2138 phase 1 is detection, evidence and dry-run only)";
Assert.Equal(detail, PgPlanForceActionStore.SanitizeDetailForAudit(detail));
}

[Fact]
public void NullDetail_StaysNull()
{
Assert.Null(PgPlanForceActionStore.SanitizeDetailForAudit(null));
}

/// <summary>pin (a) from the #4363 anchoring round: a legacy line that STARTS with the safe prefix but
/// carries exception text after the fixed sentence — the shape a naive prefix check (rather than a full
/// anchor) would have let through.</summary>
[Fact]
public void LegacyLine_SafePrefixWithTrailingExceptionText_IsRedacted()
{
var legacy = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) extra: password authentication failed for user \"darling_ro\" at host db-primary-02.internal \u2014 an unattended force cannot proceed on an unknown engine state";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(legacy)!;

Assert.DoesNotContain("darling_ro", sanitized);
Assert.DoesNotContain("db-primary-02", sanitized);
Assert.DoesNotContain("password authentication failed", sanitized);
Assert.Equal(
"state_unavailable: the forcing and automatic-plan-correction state read failed \u2014 an unattended force cannot proceed on an unknown engine state",
sanitized);
}

/// <summary>pin (b): the safe shape followed by a newline and exception text — an embedded newline in
/// a legacy exception message must not let the second line escape as if it were a sibling blocker's
/// evidence line.</summary>
[Fact]
public void LegacyLine_SafeShapeFollowedByNewlineAndExceptionText_IsRedacted()
{
var safeLine = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
var smuggled = safeLine + "\nleaked: password=hunter2 host=db-primary-02.internal";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(smuggled)!;

Assert.DoesNotContain("hunter2", sanitized);
Assert.DoesNotContain("db-primary-02", sanitized);
Assert.Equal(
"state_unavailable: the forcing and automatic-plan-correction state read failed \u2014 an unattended force cannot proceed on an unknown engine state",
sanitized);
}

/// <summary>pin (c), safe half: a CRLF-terminated sibling blocker line ahead of the safe shape must not
/// stop the safe shape from passing — the anchor treats the position right after '\n' as the line start
/// regardless of a preceding '\r'.</summary>
[Fact]
public void CrlfPrecededSafeLine_StillPassesThrough()
{
var safeLine = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
var detail = "apc_owns_it: query_store_stats: plan 7 is_forced_plan = true\r\n" + safeLine;
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(detail)!;

Assert.Equal(detail, sanitized);
}

/// <summary>pin (c), unsafe half: a CRLF-joined legacy line still gets redacted.</summary>
[Fact]
public void CrlfSmuggledExceptionText_IsRedacted()
{
var safeLine = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
var smuggled = safeLine + "\r\nleaked: password=hunter2 host=db-primary-02.internal";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(smuggled)!;

Assert.DoesNotContain("hunter2", sanitized);
Assert.DoesNotContain("db-primary-02", sanitized);
}

/// <summary>A lone '\r' between the safe shape and smuggled text is not a line boundary to .NET's
/// '^'/'\z' under <c>Multiline</c>, so the smuggled text stays inside the redacted block rather than
/// surviving as if it were a new line.</summary>
[Fact]
public void LoneCrSmuggledExceptionText_IsRedacted()
{
var safeLine = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
var smuggled = safeLine + "\rleaked: password=hunter2";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(smuggled)!;

Assert.DoesNotContain("hunter2", sanitized);
}

/// <summary>The Unicode LINE SEPARATOR (U+2028) is not a line boundary to .NET's '^'/'\z' either, so
/// smuggled text after one stays inside the redacted block.</summary>
[Fact]
public void UnicodeLineSeparatorSmuggledExceptionText_IsRedacted()
{
var safeLine = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
var smuggled = safeLine + "\u2028leaked: password=hunter2";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(smuggled)!;

Assert.DoesNotContain("hunter2", sanitized);
}

/// <summary>Same as the LINE SEPARATOR pin, for the Unicode PARAGRAPH SEPARATOR (U+2029).</summary>
[Fact]
public void UnicodeParagraphSeparatorSmuggledExceptionText_IsRedacted()
{
var safeLine = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException; the log has the full error) \u2014 an unattended force cannot proceed on an unknown engine state";
var smuggled = safeLine + "\u2029leaked: password=hunter2";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(smuggled)!;

Assert.DoesNotContain("hunter2", sanitized);
}

/// <summary>F1 (#4363 security round): <c>ForcePlanBotPolicy.Blockers</c> always adds
/// <c>state_unavailable</c> LAST — <c>apc_owns_it</c>/<c>apc_enabled_for_database</c> both require a
/// non-null, non-empty <c>state</c> and cannot co-occur with the null/empty-state condition that adds
/// <c>state_unavailable</c> — so a real detail never has a sibling blocker AFTER it. A pre-#4326 row's
/// raw exception message CAN embed <c>"\n" + "apc_owns_it:"</c> (or any other sibling's exact prefix)
/// as free text; a sibling-prefix lookahead would have ended the block there and let everything after
/// it — unredacted — through. The fix anchors the block to <c>\z</c> instead: everything from the
/// legacy row's <c>state_unavailable:</c> onward is redacted, including the impersonating text.</summary>
[Fact]
public void LegacyLine_EmbeddingASiblingPrefix_IsRedactedToTheEnd()
{
var legacy = "apc_owns_it: query_store_stats: plan 7 is_forced_plan = true\n" +
"state_unavailable: the forcing and automatic-plan-correction state read failed (PostgresException: 28P01: password authentication failed for user \"fake_user\"\n" +
"apc_owns_it: host=db-fake-01.example password=hunter2) \u2014 an unattended force cannot proceed on an unknown engine state";
var sanitized = PgPlanForceActionStore.SanitizeDetailForAudit(legacy)!;

Assert.DoesNotContain("hunter2", sanitized);
Assert.DoesNotContain("db-fake-01", sanitized);
Assert.DoesNotContain("fake_user", sanitized);
Assert.StartsWith("apc_owns_it: query_store_stats: plan 7 is_forced_plan = true\n", sanitized);
Assert.Equal(
"apc_owns_it: query_store_stats: plan 7 is_forced_plan = true\n" +
"state_unavailable: the forcing and automatic-plan-correction state read failed \u2014 an unattended force cannot proceed on an unknown engine state",
sanitized);
}

/// <summary>F2 (#4363 security round): the null-state "returned no row" shape
/// (<c>ForcePlanBotPolicy.Blockers(target, null, null)</c>) never carried exception text, and must
/// pass through unchanged — built from the writer itself so wording drift there turns this test red
/// instead of silently over-redacting.</summary>
[Fact]
public void NoRowShape_FromTheWriter_PassesThroughUnchanged()
{
var target = new ForcePlanTarget(Database: "db1", QueryId: 42, PlanId: 7);
var blockers = ForcePlanBotPolicy.Blockers(target, state: null, stateUnavailableReason: null);
var detail = ForcePlanBotPolicy.Evidence(blockers)!;

Assert.Equal(detail, PgPlanForceActionStore.SanitizeDetailForAudit(detail));
Assert.Contains("returned no row for plan 7 of query 42 in db1", detail);
}

/// <summary>F2: the empty-state "ran and observed nothing" shape
/// (<c>ForcePlanBotPolicy.Blockers(target, emptyState, null)</c>) — the common production case —
/// never carried exception text either, and must pass through unchanged.</summary>
[Fact]
public void EmptyStateShape_FromTheWriter_PassesThroughUnchanged()
{
var target = new ForcePlanTarget(Database: "db1", QueryId: 42, PlanId: 7);
var emptyState = new ForcePlanTargetState(
PlanIsForced: null, PlanForcingType: null, ForceFailureCount: null, LastForceFailureReason: null,
PlanObservedAtUtc: null, OtherForcedPlanId: null, OtherForcedPlanForcingType: null,
OtherForcedPlanObservedAtUtc: null, ApcState: null, ApcStateReason: null, ApcRegressedPlanId: null,
ApcLastGoodPlanId: null, ApcLastGoodPlanForcingType: null, ApcLastGoodPlanIsForced: null,
ApcLastGoodPlanForceFailureReason: null, ApcExecuteActionInitiatedBy: null, ApcObservedAtUtc: null,
ForceLastGoodPlanActualState: null, EnablementObservedAtUtc: null);
Assert.True(emptyState.IsEmpty);

var blockers = ForcePlanBotPolicy.Blockers(target, emptyState, stateUnavailableReason: null);
var detail = ForcePlanBotPolicy.Evidence(blockers)!;

Assert.Equal(detail, PgPlanForceActionStore.SanitizeDetailForAudit(detail));
Assert.Contains("ran and observed nothing for this target inside the last", detail);
}

/// <summary>Both journal reads apply the sanitizer — the review's missing pin. Exercises
/// <see cref="PgPlanForceActionStore.SanitizeDetailForAudit"/> the same way both
/// <c>GetRecentActionsAsync</c> and <c>GetPendingReviewsAsync</c> do at their respective call sites
/// (<c>record with { Detail = SanitizeDetailForAudit(record.Detail) }</c>), pinning that a legacy
/// exception-bearing line never survives either read's output shape.</summary>
[Fact]
public void BothReads_ApplyTheSameSanitizer()
{
var legacy = "state_unavailable: the forcing and automatic-plan-correction state read failed (NpgsqlException: 28P01: password authentication failed for user \"darling_ro\" at host db-primary-02.internal)";
var recentActionsShape = legacy;
var pendingReviewShape = legacy;

Assert.DoesNotContain("db-primary-02", PgPlanForceActionStore.SanitizeDetailForAudit(recentActionsShape));
Assert.DoesNotContain("db-primary-02", PgPlanForceActionStore.SanitizeDetailForAudit(pendingReviewShape));
}
}
Loading
Loading