using SQLInsight.Core.Data; namespace SQLInsight.Core.Tests; public class PrivilegeAnalysisTests { private static readonly DateTimeOffset Then = new(2026, 9, 29, 10, 0, 0, TimeSpan.Zero); private static readonly DateTimeOffset Now = Then.AddMinutes(15); private static PrivilegeSnapshot Snapshot(params PrivilegedPrincipal[] principals) => new() { CapturedAt = Now, Principals = principals }; private static PrivilegedPrincipal Group(string name, params string[] members) => new(name, "Windows-gruppe", "sysadmin", false, members.Select(m => new GroupMember(m, null, true)).ToList()); [Fact] public void New_and_removed_direct_members_are_found_by_privilege_and_name() { var before = Snapshot(new("sa", "SQL-login", "sysadmin", false), new("KUNDE\\anne", "Windows-login", "dbcreator", false)); var after = Snapshot(new("sa", "SQL-login", "sysadmin", false), new("KUNDE\\anne", "Windows-login", "sysadmin", false)); var changes = PrivilegeAnalysis.Diff(before, after, Now); Assert.Equal(2, changes.Count); Assert.Contains(changes, c => c is { Kind: PrivilegeAnalysis.Added, Privilege: "sysadmin", Principal: "KUNDE\\anne", ViaGroup: null }); Assert.Contains(changes, c => c is { Kind: PrivilegeAnalysis.Removed, Privilege: "dbcreator", Principal: "KUNDE\\anne" }); } [Fact] public void People_added_to_an_unfolded_group_are_new_members_via_the_group() { var before = Snapshot(Group("KUNDE\\SQL-Admins", "KUNDE\\anne")); var after = Snapshot(Group("KUNDE\\SQL-Admins", "KUNDE\\anne", "KUNDE\\bo")); var change = Assert.Single(PrivilegeAnalysis.Diff(before, after, Now)); Assert.Equal((PrivilegeAnalysis.Added, "KUNDE\\bo", "KUNDE\\SQL-Admins", "Windows-bruger"), (change.Kind, change.Principal, change.ViaGroup, change.PrincipalType)); } [Fact] public void A_group_that_could_not_be_unfolded_does_not_look_like_everyone_left() { var before = Snapshot(Group("KUNDE\\SQL-Admins", "KUNDE\\anne", "KUNDE\\bo")); var after = Snapshot(new PrivilegedPrincipal("KUNDE\\SQL-Admins", "Windows-gruppe", "sysadmin", false, ExpansionError: "Active Directory svarede ikke")); Assert.Empty(PrivilegeAnalysis.Diff(before, after, Now)); } [Fact] public void Who_and_when_comes_from_the_matching_trace_event() { var changes = new[] { new PrivilegeChange(Now, PrivilegeAnalysis.Added, "sysadmin", "ny_admin", "SQL-login"), new PrivilegeChange(Now, PrivilegeAnalysis.Removed, "dbcreator", "gammel", "SQL-login"), new PrivilegeChange(Now, PrivilegeAnalysis.Added, "sysadmin", "KUNDE\\bo", "Windows-bruger", "KUNDE\\SQL-Admins"), }; var events = new[] { new TraceSecurityEvent(Then.AddMinutes(3), 108, 1, "KUNDE\\it-chef", "ny_admin", "sysadmin"), new TraceSecurityEvent(Then.AddMinutes(5), 108, 1, "KUNDE\\vikar", "ny_admin", "sysadmin"), // the latest wins new TraceSecurityEvent(Then.AddMinutes(6), 108, 1, "KUNDE\\vikar", "ny_admin", "serveradmin"), // other role new TraceSecurityEvent(Now.AddMinutes(5), 108, 1, "KUNDE\\senere", "ny_admin", "sysadmin"), // after detection new TraceSecurityEvent(Then.AddMinutes(7), 108, 2, "KUNDE\\it-chef", "gammel", "dbcreator"), }; var enriched = PrivilegeAnalysis.Enrich(changes, events); Assert.Equal(("KUNDE\\vikar", Then.AddMinutes(5)), (enriched[0].ChangedBy, enriched[0].ChangedAt)); Assert.Equal("KUNDE\\it-chef", enriched[1].ChangedBy); Assert.Null(enriched[2].ChangedBy); // a change in Active Directory is not in SQL Server's trace } [Fact] public void Accounts_count_people_in_groups_once_and_leave_out_services_and_disabled() { var snapshot = Snapshot( new("sa", "SQL-login", "sysadmin", true), new("KUNDE\\anne", "Windows-login", "sysadmin", false), new("NT SERVICE\\MSSQLSERVER", "Windows-login", "sysadmin", false), new("KUNDE\\SQL-Admins", "Windows-gruppe", "sysadmin", false, [new("KUNDE\\anne", "Anne", true), new("KUNDE\\bo", "Bo", true), new("KUNDE\\fratraadt", null, false), new("KUNDE\\SRV01$", null, true)]), new("KUNDE\\DBA", "Windows-gruppe", "sysadmin", false, ExpansionError: "timeout"), new("KUNDE\\cecilie", "Windows-login", "dbcreator", false)); var accounts = PrivilegeAnalysis.Accounts(snapshot, PrivilegeAnalysis.Sysadmin); Assert.Equal(["KUNDE\\anne", "KUNDE\\bo"], accounts.Select(a => a.Account)); Assert.Equal(PrivilegeAnalysis.Direct, accounts[0].Source); Assert.Equal("KUNDE\\SQL-Admins", accounts[1].Source); Assert.Equal("KUNDE\\DBA", Assert.Single(PrivilegeAnalysis.UnfoldedGroupsMissing(snapshot, PrivilegeAnalysis.Sysadmin)).Name); } [Theory] [InlineData(@"NT SERVICE\SQLSERVERAGENT", true)] [InlineData(@"NT AUTHORITY\SYSTEM", true)] [InlineData("##MS_PolicyTsqlExecutionLogin##", true)] [InlineData(@"KUNDE\gmsa-sql$", true)] [InlineData(@"KUNDE\anne", false)] [InlineData("sa", false)] public void Service_identities_are_not_people(string name, bool expected) => Assert.Equal(expected, PrivilegeAnalysis.IsServiceIdentity(name)); [Theory] [InlineData(@"C:\Program Files\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQL\Log\log_123.trc", @"C:\Program Files\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQL\Log\log.trc")] [InlineData("/var/opt/mssql/log/log_7.trc", "/var/opt/mssql/log/log.trc")] public void Trace_files_are_read_from_the_first_rollover_file(string current, string expected) => Assert.Equal(expected, PrivilegeAnalysis.TraceFileBase(current)); [Fact] public void Fingerprints_are_stable_short_and_free_of_names() { var a = PrivilegeAnalysis.Fingerprint("KUNDE\\anne|"); Assert.Equal(12, a.Length); Assert.Equal(a, PrivilegeAnalysis.Fingerprint("kunde\\ANNE|")); Assert.DoesNotContain("anne", a, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Without_active_directory_groups_fail_with_the_reason() { var expansion = await new NoGroupResolver("Agenten kører ikke på Windows").ExpandAsync("KUNDE\\SQL-Admins", CancellationToken.None); Assert.Empty(expansion.Members); Assert.Equal("Agenten kører ikke på Windows", expansion.Error); } }