using SQLInsight.Core.Data; using SQLInsight.Core.Findings; using SQLInsight.Rules.Engine; using SQLInsight.Rules.Security; using static SQLInsight.Rules.Tests.TestInstances; namespace SQLInsight.Rules.Tests; public class PrivilegeRuleTests { private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context)); private static PrivilegeSnapshot Snapshot(IReadOnlyList? recent = null, int max = 3, params PrivilegedPrincipal[] principals) => new() { CapturedAt = Now, Principals = principals, RecentChanges = recent ?? [], MaxSysadminAccounts = max, }; [Fact] public void New_sysadmin_is_high_with_who_and_when_as_sensitive_evidence() { var change = new PrivilegeChange(Now, PrivilegeAnalysis.Added, "sysadmin", "ny_admin", "SQL-login", ChangedBy: @"KUNDE\it-chef", ChangedAt: Now.AddMinutes(-4)); var finding = Assert.Single(Run(new NewPrivilegeRule(), With(Snapshot([change]))).Findings); Assert.Equal(Severity.High, finding.Severity); Assert.DoesNotContain("ny_admin", finding.Title, StringComparison.Ordinal); Assert.DoesNotContain("ny_admin", finding.Subject, StringComparison.Ordinal); Assert.True(Assert.Single(finding.Evidence, e => e.Name == "Hvem fik den").Sensitive); var by = Assert.Single(finding.Evidence, e => e.Name == "Ændret af"); Assert.Equal((@"KUNDE\it-chef", true), (by.Value, by.Sensitive)); Assert.Contains(finding.Evidence, e => e.Name == "Ændret" && e.Value.StartsWith("2026-09-29 11:56", StringComparison.Ordinal)); } [Theory] [InlineData("securityadmin", Severity.High)] [InlineData("CONTROL SERVER", Severity.High)] [InlineData("dbcreator", Severity.Medium)] [InlineData("bulkadmin", Severity.Medium)] public void Severity_follows_the_privilege(string privilege, Severity expected) { var change = new PrivilegeChange(Now, PrivilegeAnalysis.Added, privilege, @"KUNDE\bo", "Windows-bruger", @"KUNDE\SQL-Admins"); var finding = Assert.Single(Run(new NewPrivilegeRule(), With(Snapshot([change]))).Findings); Assert.Equal(expected, finding.Severity); Assert.Contains(finding.Evidence, e => e.Name == "Via Windows-gruppe" && e.Sensitive); } [Fact] public void Removed_rights_and_quiet_weeks_give_no_finding() { var removed = new PrivilegeChange(Now, PrivilegeAnalysis.Removed, "sysadmin", "gammel", "SQL-login"); Assert.Equal(RuleOutcome.Passed, Run(new NewPrivilegeRule(), With(Snapshot([removed]))).Outcome); Assert.Equal(RuleOutcome.Passed, Run(new NewPrivilegeRule(), With(Snapshot())).Outcome); } [Fact] public void Unknown_who_explains_why() { var change = new PrivilegeChange(Now, PrivilegeAnalysis.Added, "sysadmin", "ny_admin", "SQL-login"); var snapshot = Snapshot([change]) with { TraceStatus = "Giv agenten ALTER TRACE" }; var finding = Assert.Single(Run(new NewPrivilegeRule(), With(snapshot)).Findings); Assert.Equal("ukendt", finding.Evidence.Single(e => e.Name == "Ændret af").Value); Assert.Contains(finding.Evidence, e => e.Name == "Hvorfor ukendt" && e.Value.Contains("ALTER TRACE", StringComparison.Ordinal)); } [Theory] [InlineData(3, RuleOutcome.Passed)] [InlineData(2, RuleOutcome.Findings)] public void Too_many_sysadmins_counts_people_in_groups(int max, RuleOutcome expected) { var snapshot = Snapshot(null, max, new("sa", "SQL-login", "sysadmin", false), new(@"KUNDE\SQL-Admins", "Windows-gruppe", "sysadmin", false, [new(@"KUNDE\anne", "Anne", true), new(@"KUNDE\bo", "Bo", true)]), new(@"NT SERVICE\MSSQLSERVER", "Windows-login", "sysadmin", false)); var result = Run(new TooManySysadminsRule(), With(snapshot)); Assert.Equal(expected, result.Outcome); if (expected == RuleOutcome.Findings) { var finding = Assert.Single(result.Findings); Assert.Equal("3", finding.Evidence.Single(e => e.Name == "Antal").Value); Assert.True(finding.Evidence.Single(e => e.Name == "Personer og konti").Sensitive); } } }