diff --git a/test/unit/CollectionDefinitions/CacheTestsCollectionDefinition.cs b/test/unit/CollectionDefinitions/CacheTestsCollectionDefinition.cs new file mode 100644 index 00000000..c03cbc59 --- /dev/null +++ b/test/unit/CollectionDefinitions/CacheTestsCollectionDefinition.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace CommonLibTest.CollectionDefinitions; + +/// +/// Test that use cache cannot run in parallel, they can have flaky behavior +/// +[CollectionDefinition(nameof(CacheTestCollectionDefinition), DisableParallelization = true)] +public class CacheTestCollectionDefinition{} diff --git a/test/unit/CommonLibHelperTests.cs b/test/unit/CommonLibHelperTests.cs index e2cfe24f..0cad80e1 100644 --- a/test/unit/CommonLibHelperTests.cs +++ b/test/unit/CommonLibHelperTests.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Principal; using System.Text; using System.Threading.Tasks; using SharpHoundCommonLib; @@ -293,5 +294,82 @@ public async Task RetryOnException_SucceedsOnLastAttempt() { Assert.True(success); } + + [Fact] + public void DomainNameToDistinguishedName_DotsBecomeDcComponents() + { + var result = Helpers.DomainNameToDistinguishedName("test.local"); + Assert.Equal("DC=test,DC=local", result); + } + + [Theory] + [InlineData("S-1-5-32-544", "\\01\\02\\00\\00\\00\\00\\00\\05\\20\\00\\00\\00\\20\\02\\00\\00")] + public void ConvertSidToHexSid_ValidSid_MatchesSecurityIdentifierBinaryForm(string sid, string expectedHexSid) + { + // Arrange & Act + var actual = Helpers.ConvertSidToHexSid(sid); + + // Assert + Assert.Equal(expectedHexSid, actual); + return; + + static string BuildExpectedHexSid(string sid) + { + var securityIdentifier = new SecurityIdentifier(sid); + var sidBytes = new byte[securityIdentifier.BinaryLength]; + securityIdentifier.GetBinaryForm(sidBytes, 0); + return $"\\{BitConverter.ToString(sidBytes).Replace('-', '\\')}"; + } + } + + [Fact] + public void ConvertSidToHexSid_InvalidSid_Throws() + { + Assert.ThrowsAny(() => Helpers.ConvertSidToHexSid("NOT-A-SID")); + } + + [Theory] + [InlineData("s-1-5-18")] + [InlineData("S-1-5-18")] + public void IsSidFiltered_FilteredWellKnownSidsCaseInsensitive_ReturnsTrue(string sid) + { + Assert.True(Helpers.IsSidFiltered(sid)); + } + + [Theory] + [InlineData("S-1-5-80-1234567890")] + [InlineData("S-1-5-82-1234567890")] + [InlineData("S-1-5-90-0")] + [InlineData("S-1-5-96-0")] + public void IsSidFiltered_FilteredPrefixes_ReturnsTrue(string sid) + { + Assert.True(Helpers.IsSidFiltered(sid)); + } + + [Theory] + [InlineData("S-1-5-21-1234567890")] + [InlineData("S-1-5-21")] + public void IsSidFiltered_ReturnsFalse(string sid) + { + Assert.False(Helpers.IsSidFiltered(sid)); + } + + [Fact] + public void ConvertLdapTimeToLong_Null_ReturnsMinusOne() + { + Assert.Equal(-1, Helpers.ConvertLdapTimeToLong(null)); + } + + [Fact] + public void ConvertLdapTimeToLong_InvalidNumber_ThrowsFormatException() + { + Assert.Throws(() => Helpers.ConvertLdapTimeToLong("not-a-number")); + } + + [Fact] + public void ConvertLdapTimeToLong_ValidNumber_Parses() + { + Assert.Equal(123456789L, Helpers.ConvertLdapTimeToLong("123456789")); + } } } \ No newline at end of file diff --git a/test/unit/CommonLibTests.cs b/test/unit/CommonLibTests.cs new file mode 100644 index 00000000..8f1f595e --- /dev/null +++ b/test/unit/CommonLibTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Reflection; +using CommonLibTest.CollectionDefinitions; +using Microsoft.Extensions.Logging; +using Moq; +using SharpHoundCommonLib; +using Xunit; + +namespace CommonLibTest; + +[Collection(nameof(CacheTestCollectionDefinition))] +public class CommonLibTests +{ + + public CommonLibTests() + { + ResetCommonLibState(); + } + + [Fact] + public void InitializeCommonLib_FirstCallWithoutCache_CreatesAndSetsCacheInstance() + { + // Arrange & Act + CommonLib.InitializeCommonLib(); + + // Assert + var cache = Cache.GetCacheInstance(); + Assert.NotNull(cache); + Assert.NotNull(cache.IdToTypeCache); + Assert.NotNull(cache.ValueToIdCache); + Assert.NotNull(cache.GlobalCatalogCache); + Assert.NotNull(cache.MachineSidCache); + Assert.NotNull(cache.SIDToDomainCache); + } + + [Fact] + public void InitializeCommonLib_UsesProvidedInstance() + { + // Arrange + var provided = Cache.CreateNewCache(); + + // Act + CommonLib.InitializeCommonLib(cache: provided); + + // Assert + Assert.Same(provided, Cache.GetCacheInstance()); + } + + [Fact] + public void InitializeCommonLib_2Calls_LogsWarningAndDoesNotReplaceCache() + { + // Arrange + var cache1 = Cache.CreateNewCache(); + CommonLib.InitializeCommonLib(cache: cache1); + + var cache2 = Cache.CreateNewCache(); + var logger = new Mock(); + + // Act + CommonLib.InitializeCommonLib(logger.Object, cache2); + + // Assert + Assert.Same(cache1, Cache.GetCacheInstance()); // cache1 should be then one used since lib was already initialized + + logger.Verify(x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, _) => + v.ToString() != null && + v.ToString().Contains("already initialized", StringComparison.InvariantCultureIgnoreCase)), + It.IsAny(), + It.IsAny>()), + Times.Once()); + } + + private static void ResetCommonLibState() + { + // Reset CommonLib._initialized (private static) + var commonLibType = typeof(CommonLib); + var initializedField = commonLibType.GetField("_initialized", + BindingFlags.Static | BindingFlags.NonPublic); + + if (initializedField == null) + throw new InvalidOperationException("CommonLib _initialized field not found"); + + initializedField.SetValue(null, false); + + // Reset cache singleton so tests don't leak state into each other + Cache.SetCacheInstance(null); + } +} \ No newline at end of file diff --git a/test/unit/UserRightsAssignmentProcessorTest.cs b/test/unit/UserRightsAssignmentProcessorTest.cs index 459c06bf..14fbc9f8 100644 --- a/test/unit/UserRightsAssignmentProcessorTest.cs +++ b/test/unit/UserRightsAssignmentProcessorTest.cs @@ -1,146 +1,151 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommonLibTest.Facades; -using CommonLibTest.Facades.LSAMocks.DCMocks; -using CommonLibTest.Facades.LSAMocks.WorkstationMocks; -using Moq; -using Newtonsoft.Json; -using SharpHoundCommonLib; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.Processors; -using SharpHoundRPC; -using Xunit; -using Xunit.Abstractions; - -namespace CommonLibTest -{ - public class UserRightsAssignmentProcessorTest - { - private readonly ITestOutputHelper _testOutputHelper; - - public UserRightsAssignmentProcessorTest(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestWorkstation() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockWorkstationLSAPolicy(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1001"; - var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) - .ToArrayAsync(); - - var privilege = results[0]; - Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); - Assert.Equal(3, results[0].Results.Length); - var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); - Assert.Equal($"{machineDomainSid}-544", adminResult.ObjectIdentifier); - Assert.Equal(Label.LocalGroup, adminResult.ObjectType); - var rdpResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-555")); - Assert.Equal($"{machineDomainSid}-555", rdpResult.ObjectIdentifier); - Assert.Equal(Label.LocalGroup, rdpResult.ObjectType); - } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestDC() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockDCLSAPolicy(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1000"; - var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true) - .ToArrayAsync(); - - var privilege = results[0]; - _testOutputHelper.WriteLine(JsonConvert.SerializeObject(privilege)); - Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); - Assert.Single(results[0].Results); - var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); - Assert.Equal("TESTLAB.LOCAL-S-1-5-32-544", adminResult.ObjectIdentifier); - Assert.Equal(Label.Group, adminResult.ObjectType); - } - - // Obsolete by AdaptiveTimeout - // [Fact] - // public async Task UserRightsAssignmentProcessor_TestTimeout() { - // var mockProcessor = new Mock(new MockLdapUtils(), null); - // mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { - // Task.Delay(100).Wait(); - // return NtStatus.StatusAccessDenied; - // }); - // var processor = mockProcessor.Object; - // var machineDomainSid = $"{Consts.MockDomainSid}-1000"; - // var receivedStatus = new List(); - // processor.ComputerStatusEvent += status => { - // receivedStatus.Add(status); - // return Task.CompletedTask; - // }; - // var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true, null) - // .ToArrayAsync(); - // Assert.Empty(results); - // Assert.Single(receivedStatus); - // var status = receivedStatus[0]; - // Assert.Equal("Timeout", status.Status); - // } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestGetLocalDomainInformationFail() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockFailLSAPolicy_GetLocalDomainInformation(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { - Task.Delay(100).Wait(); - return NtStatus.StatusAccessDenied; - }); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1001"; - var receivedStatus = new List(); - processor.ComputerStatusEvent += async status => { - receivedStatus.Add(status); - }; - var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) - .ToArrayAsync(); - - Assert.Empty(results); - Assert.Single(receivedStatus); - var status = receivedStatus[0]; - Assert.Equal("StatusAccessDenied", status.Status); - Assert.Equal("LSAGetMachineSID", status.Task); - } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestGetResolvedPrincipalsWithPrivilegeFail() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockFailLSAPolicy_GetResolvedPrincipalsWithPrivilege(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1001"; - var receivedStatus = new List(); - processor.ComputerStatusEvent += async status => { - receivedStatus.Add(status); - }; - var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) - .ToArrayAsync(); - - Assert.Single(results); - - var result = results[0]; - Assert.False(result.Collected); - Assert.Equal("LSAEnumerateAccountsWithUserRights returned StatusAccessDenied", result.FailureReason); - Assert.Single(receivedStatus); - var status = receivedStatus[0]; - Assert.Equal("StatusAccessDenied", status.Status); - Assert.Equal("LSAEnumerateAccountsWithUserRight", status.Task); - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommonLibTest.CollectionDefinitions; +using CommonLibTest.Facades; +using CommonLibTest.Facades.LSAMocks.DCMocks; +using CommonLibTest.Facades.LSAMocks.WorkstationMocks; +using Moq; +using Newtonsoft.Json; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.Processors; +using SharpHoundRPC; +using Xunit; +using Xunit.Abstractions; + +namespace CommonLibTest +{ + [Collection(nameof(CacheTestCollectionDefinition))] + public class UserRightsAssignmentProcessorTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public UserRightsAssignmentProcessorTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + + //reseting cache + Cache.SetCacheInstance(null); + } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestWorkstation() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockWorkstationLSAPolicy(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1001"; + var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) + .ToArrayAsync(); + + var privilege = results[0]; + Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); + Assert.Equal(3, results[0].Results.Length); + var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); + Assert.Equal($"{machineDomainSid}-544", adminResult.ObjectIdentifier); + Assert.Equal(Label.LocalGroup, adminResult.ObjectType); + var rdpResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-555")); + Assert.Equal($"{machineDomainSid}-555", rdpResult.ObjectIdentifier); + Assert.Equal(Label.LocalGroup, rdpResult.ObjectType); + } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestDC() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockDCLSAPolicy(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1000"; + var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true) + .ToArrayAsync(); + + var privilege = results[0]; + _testOutputHelper.WriteLine(JsonConvert.SerializeObject(privilege)); + Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); + Assert.Single(results[0].Results); + var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); + Assert.Equal("TESTLAB.LOCAL-S-1-5-32-544", adminResult.ObjectIdentifier); + Assert.Equal(Label.Group, adminResult.ObjectType); + } + + // Obsolete by AdaptiveTimeout + // [Fact] + // public async Task UserRightsAssignmentProcessor_TestTimeout() { + // var mockProcessor = new Mock(new MockLdapUtils(), null); + // mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { + // Task.Delay(100).Wait(); + // return NtStatus.StatusAccessDenied; + // }); + // var processor = mockProcessor.Object; + // var machineDomainSid = $"{Consts.MockDomainSid}-1000"; + // var receivedStatus = new List(); + // processor.ComputerStatusEvent += status => { + // receivedStatus.Add(status); + // return Task.CompletedTask; + // }; + // var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true, null) + // .ToArrayAsync(); + // Assert.Empty(results); + // Assert.Single(receivedStatus); + // var status = receivedStatus[0]; + // Assert.Equal("Timeout", status.Status); + // } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestGetLocalDomainInformationFail() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockFailLSAPolicy_GetLocalDomainInformation(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { + Task.Delay(100).Wait(); + return NtStatus.StatusAccessDenied; + }); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1001"; + var receivedStatus = new List(); + processor.ComputerStatusEvent += async status => { + receivedStatus.Add(status); + }; + var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) + .ToArrayAsync(); + + Assert.Empty(results); + Assert.Single(receivedStatus); + var status = receivedStatus[0]; + Assert.Equal("StatusAccessDenied", status.Status); + Assert.Equal("LSAGetMachineSID", status.Task); + } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestGetResolvedPrincipalsWithPrivilegeFail() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockFailLSAPolicy_GetResolvedPrincipalsWithPrivilege(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1001"; + var receivedStatus = new List(); + processor.ComputerStatusEvent += async status => { + receivedStatus.Add(status); + }; + var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) + .ToArrayAsync(); + + Assert.Single(results); + + var result = results[0]; + Assert.False(result.Collected); + Assert.Equal("LSAEnumerateAccountsWithUserRights returned StatusAccessDenied", result.FailureReason); + Assert.Single(receivedStatus); + var status = receivedStatus[0]; + Assert.Equal("StatusAccessDenied", status.Status); + Assert.Equal("LSAEnumerateAccountsWithUserRight", status.Task); + } + } } \ No newline at end of file