using Microsoft.Data.SqlClient; namespace SQLInsight.IntegrationTests; /// /// Runs db/permissions/grant-least-privilege.sql and proves that the agent login can read everything it needs, /// and nothing else. /// [Collection(SqlServerTestGroup.Name)] [Trait("Category", "Integration")] public class LeastPrivilegeTests(SqlServerFixture sql) : IAsyncLifetime { private const string Login = "sqlinsight_agent"; private const string Password = "Agent-Pa55word!"; private const string Database = "SQLInsightQsTest"; public async Task InitializeAsync() { await sql.ExecuteAsSaAsync($""" IF SUSER_ID(N'{Login}') IS NULL CREATE LOGIN [{Login}] WITH PASSWORD = N'{Password}', CHECK_POLICY = OFF; IF DB_ID(N'{Database}') IS NULL CREATE DATABASE [{Database}]; """); await sql.ExecuteAsSaAsync($""" USE [{Database}]; ALTER DATABASE [{Database}] SET QUERY_STORE = ON; IF OBJECT_ID(N'dbo.CustomerData') IS NULL BEGIN CREATE TABLE dbo.CustomerData (Id int NOT NULL PRIMARY KEY, Secret nvarchar(50) NOT NULL); INSERT dbo.CustomerData VALUES (1, N'fortroligt'); END; """); var script = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "Scripts", "grant-least-privilege.sql")); script = script.Replace(@"N'DOMÆNE\svc-sqlinsight$'", $"N'{Login}'", StringComparison.Ordinal); await sql.ExecuteAsSaAsync(script); } public Task DisposeAsync() => Task.CompletedTask; [Fact] public async Task Agent_login_is_not_sysadmin() { Assert.Equal(0, await ScalarAsync("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(query) >= 0); } [Fact] public async Task Agent_cannot_read_customer_data() { var ex = await Assert.ThrowsAsync( () => ScalarAsync($"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( () => ScalarAsync($"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() { var script = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "Scripts", "grant-least-privilege.sql")); script = script.Replace(@"N'DOMÆNE\svc-sqlinsight$'", $"N'{Login}'", StringComparison.Ordinal); await sql.ExecuteAsSaAsync(script); } private async Task ScalarAsync(string query) { var builder = new SqlConnectionStringBuilder(sql.SaConnectionString) { UserID = Login, Password = Password, InitialCatalog = "master", }; await using var connection = new SqlConnection(builder.ConnectionString); 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); } }