sqlinsight-pro/tests/SQLInsight.IntegrationTests/LeastPrivilegeTests.cs
Claude 63f23c4bbd
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
Fase 1: collectors and rule pack (41 rules)
Collectors (SQL Server 2000+, version-specific SQL, least privilege):
server, configuration, trace flags, databases (settings, backups in UTC,
CHECKDB, VLFs, Query Store, files, tempdb), suspect pages, plan cache,
wait stats and file I/O as deltas with restart/clear detection,
performance counters, CPU from the scheduler monitor ring buffer,
blocking (sleeping head blockers with open transactions), and deadlocks
from system_health continuing from the last file offset.

Rules: configuration (memory, MAXDOP per Microsoft NUMA guidance, cost
threshold, ad hoc bloat, risky options, pending changes), databases
(state, settings, backups, log backups, CHECKDB, suspect pages, VLFs,
autogrowth, max size, Query Store, tempdb), waits (THREADPOOL, signal
waits, locks, I/O, WRITELOG, parallelism, PAGELATCH), I/O latency, CPU,
memory grants, blocking and deadlocks. Missing data makes a rule not
measurable with the collector's reason.

Integration tests run every collector as the least-privilege login and
reproduce real blocking and a real deadlock; green on 2017 and 2022.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
2026-09-29 06:17:08 +00:00

73 lines
3.2 KiB
C#

using Microsoft.Data.SqlClient;
namespace SQLInsight.IntegrationTests;
/// <summary>
/// Runs db/permissions/grant-least-privilege.sql and proves that the agent login can read everything it needs,
/// and nothing else.
/// </summary>
[Collection(SqlServerTestGroup.Name)]
[Trait("Category", "Integration")]
public class LeastPrivilegeTests(SqlServerFixture sql)
{
private const string Database = SqlServerFixture.TestDatabase;
[Fact]
public async Task Agent_login_is_not_sysadmin()
{
Assert.Equal(0, await ScalarAsync<int>("SELECT IS_SRVROLEMEMBER('sysadmin')"));
}
[Theory]
[InlineData("SELECT COUNT(*) FROM sys.dm_os_wait_stats")]
[InlineData("SELECT COUNT(*) FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t")]
[InlineData("SELECT COUNT(*) FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) p")]
[InlineData("SELECT COUNT(*) FROM sys.dm_io_virtual_file_stats(NULL, NULL)")]
[InlineData("SELECT COUNT(*) FROM sys.dm_os_ring_buffers")]
[InlineData("SELECT COUNT(*) FROM sys.dm_xe_sessions WHERE name = 'system_health'")]
[InlineData("SELECT COUNT(*) FROM sys.configurations")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.backupset")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.sysjobs")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.sysjobhistory")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.suspect_pages")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.database_query_store_options")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.query_store_runtime_stats")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.indexes WHERE object_id = OBJECT_ID(N'[{Database}].dbo.CustomerData')")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.dm_db_index_usage_stats")]
[InlineData($"SELECT COUNT(*) WHERE DATABASEPROPERTYEX(N'{Database}', 'LastGoodCheckDbTime') IS NOT NULL")]
public async Task Agent_can_read_monitoring_sources(string query)
{
Assert.True(await ScalarAsync<int>(query) >= 0);
}
[Fact]
public async Task Agent_cannot_read_customer_data()
{
var ex = await Assert.ThrowsAsync<SqlException>(
() => ScalarAsync<int>($"SELECT COUNT(*) FROM [{Database}].dbo.CustomerData"));
Assert.Equal(229, ex.Number); // The SELECT permission was denied.
}
[Fact]
public async Task Agent_cannot_change_anything()
{
var ex = await Assert.ThrowsAsync<SqlException>(
() => ScalarAsync<int>($"USE [{Database}]; CREATE TABLE dbo.ShouldNotExist (Id int); SELECT 1"));
Assert.Equal(262, ex.Number); // CREATE TABLE permission denied.
}
[Fact]
public async Task Script_is_idempotent()
{
await sql.ExecuteAsSaAsync(await SqlServerFixture.GrantScriptAsync());
}
private async Task<T> ScalarAsync<T>(string query)
{
await using var connection = new SqlConnection(sql.AgentConnectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = query;
return (T)Convert.ChangeType((await command.ExecuteScalarAsync())!, typeof(T), System.Globalization.CultureInfo.InvariantCulture);
}
}