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