sqlinsight-pro/tests/SQLInsight.Rules.Tests/ErrorLogRuleTests.cs
Søren Eggert Lundsteen Olsen e6840e40d7 SQL Server error log without elevated rights (8a)
A five-minute collector reads important error log entries from the best
source the agent's rights allow: the error log itself (xp_readerrorlog,
only with securityadmin), the default trace's ErrorLog and Audit Login
Failed events (optional ALTER TRACE), or the exception and connectivity
ring buffers (VIEW SERVER STATE: numbers and severity, failed logins).
Repetitions are summed per error; the watermark is the newest entry
read, because the default trace is buffered. Stored in
insight.ErrorLogEvents (migration 0013, EventDays), resumed after restart.

Rules SQLI-LOG-001..006: corruption 823/824/825, I/O over 15 s (833),
severe errors (17+), login failure bursts (configurable), stack dumps and
AG errors; rules needing the log text are "not measurable" with the
right to grant. Page /instanser/{navn}/fejllog (menu Overvågning) and
GET /api/v1/instances/{name}/error-log with messages redacted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 10:30:41 +02:00

56 lines
2.8 KiB
C#

using SQLInsight.Core.Data;
using SQLInsight.Core.Findings;
using SQLInsight.Rules.Engine;
using SQLInsight.Rules.Instance;
using static SQLInsight.Rules.Tests.TestInstances;
namespace SQLInsight.Rules.Tests;
public class ErrorLogRuleTests
{
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
private static ErrorLogSnapshot Log(ErrorLogSource source, params ErrorLogGroup[] groups) => new()
{
CapturedAt = Now, Source = source, ReadUntil = Now, Groups = groups, Recent = groups, LoginFailureBurst = 20,
Coverage = source == ErrorLogSource.ErrorLog ? null : "Giv agenten ALTER TRACE eller securityadmin",
};
private static ErrorLogGroup G(ErrorLogCategory category, int error, int severity, int count = 1) =>
new(category, error, severity, count, Now.AddMinutes(-3), Now.AddMinutes(-1), "Error text with login 'anne'");
[Theory]
[InlineData(824, Severity.Critical)]
[InlineData(825, Severity.High)]
public void Corruption_is_critical_and_read_retries_high(int error, Severity expected)
{
var finding = Assert.Single(Run(new CorruptionErrorRule(), With(Log(ErrorLogSource.DefaultTrace, G(ErrorLogCategory.Corruption, error, 24)))).Findings);
Assert.Equal(expected, finding.Severity);
Assert.True(finding.Evidence.Single(e => e.Name == "Eksempel").Sensitive);
}
[Fact]
public void Slow_io_and_stack_dumps_need_the_text_of_the_log()
{
var result = Run(new SlowIoErrorRule(), With(Log(ErrorLogSource.RingBuffer)));
Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
Assert.Contains("ALTER TRACE", result.Reason, StringComparison.Ordinal);
Assert.Equal(RuleOutcome.NotMeasurable, Run(new SlowIoErrorRule(), With(Log(ErrorLogSource.DefaultTrace))).Outcome);
Assert.Equal(RuleOutcome.Findings, Run(new SlowIoErrorRule(), With(Log(ErrorLogSource.ErrorLog, G(ErrorLogCategory.SlowIo, 833, 10)))).Outcome);
Assert.Equal(RuleOutcome.NotMeasurable, Run(new StackDumpRule(), With(Log(ErrorLogSource.RingBuffer))).Outcome);
}
[Theory]
[InlineData(19, RuleOutcome.Passed)]
[InlineData(20, RuleOutcome.Findings)]
public void Login_failures_are_a_finding_from_the_burst_size(int count, RuleOutcome expected) =>
Assert.Equal(expected, Run(new LoginFailureBurstRule(), With(Log(ErrorLogSource.RingBuffer, G(ErrorLogCategory.LoginFailed, 18456, 14, count)))).Outcome);
[Theory]
[InlineData(17, Severity.Medium)]
[InlineData(21, Severity.High)]
public void Severe_errors_by_severity(int severity, Severity expected) =>
Assert.Equal(expected, Assert.Single(Run(new SevereErrorRule(), With(Log(ErrorLogSource.RingBuffer, G(ErrorLogCategory.Severe, 701, severity)))).Findings).Severity);
}