From 621b04e12090c95dbde5184643384f56ea04f96e Mon Sep 17 00:00:00 2001 From: quentinr Date: Wed, 5 Nov 2025 21:47:57 -0800 Subject: [PATCH 01/11] feat: Collect data for Active Directory sites from configuration partition * Collect data related to AD sites * Collect data related to AD site subnets - add containedBy attribute for related site * Collect data related to AD site servers - add containedBy attribute for related site --- src/CommonLib/Enums/CollectionMethod.cs | 3 +- src/CommonLib/Enums/DataType.cs | 3 + src/CommonLib/Enums/LDAPProperties.cs | 2 + src/CommonLib/Enums/Labels.cs | 5 +- src/CommonLib/Enums/ObjectClass.cs | 3 + src/CommonLib/LdapProducerQueryGenerator.cs | 12 +- src/CommonLib/LdapQueries/CommonProperties.cs | 18 +++ src/CommonLib/LdapQueries/LdapFilter.cs | 39 ++++++ src/CommonLib/LdapUtils.cs | 54 ++++++++- src/CommonLib/OutputTypes/Site.cs | 12 ++ src/CommonLib/OutputTypes/SiteServer.cs | 7 ++ src/CommonLib/OutputTypes/SiteSubnet.cs | 7 ++ src/CommonLib/Processors/ACLProcessor.cs | 12 +- .../Processors/LdapPropertyProcessor.cs | 23 ++++ src/CommonLib/Processors/SiteProcessor.cs | 111 ++++++++++++++++++ 15 files changed, 301 insertions(+), 10 deletions(-) create mode 100644 src/CommonLib/OutputTypes/Site.cs create mode 100644 src/CommonLib/OutputTypes/SiteServer.cs create mode 100644 src/CommonLib/OutputTypes/SiteSubnet.cs create mode 100644 src/CommonLib/Processors/SiteProcessor.cs diff --git a/src/CommonLib/Enums/CollectionMethod.cs b/src/CommonLib/Enums/CollectionMethod.cs index 19191d126..458a3a05b 100644 --- a/src/CommonLib/Enums/CollectionMethod.cs +++ b/src/CommonLib/Enums/CollectionMethod.cs @@ -27,6 +27,7 @@ public enum CollectionMethod { WebClientService = 1 << 21, SmbInfo = 1 << 22, NTLMRegistry = 1 << 23, + Site = 1 << 24, //TODO: Re-introduce this when we're ready for Event Log collection //EventLogs = 1 << 23, LocalGroups = DCOM | RDP | LocalAdmin | PSRemote, @@ -34,7 +35,7 @@ public enum CollectionMethod { DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices, Default = Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container | CertServices | - LdapServices | SmbInfo | WebClientService, + LdapServices | SmbInfo | WebClientService | Site, All = Default | LoggedOn | GPOLocalGroup | UserRights | CARegistry | DCRegistry | WebClientService | LdapServices | NTLMRegistry diff --git a/src/CommonLib/Enums/DataType.cs b/src/CommonLib/Enums/DataType.cs index c96be5be9..2553fd529 100644 --- a/src/CommonLib/Enums/DataType.cs +++ b/src/CommonLib/Enums/DataType.cs @@ -15,5 +15,8 @@ public static class DataType public const string EnterpriseCAs = "enterprisecas"; public const string CertTemplates = "certtemplates"; public const string IssuancePolicies = "issuancepolicies"; + public const string Sites = "sites"; + public const string SiteServers = "siteservers"; + public const string SiteSubnets = "sitesubnets"; } } diff --git a/src/CommonLib/Enums/LDAPProperties.cs b/src/CommonLib/Enums/LDAPProperties.cs index 0bf6b726e..da1298d05 100644 --- a/src/CommonLib/Enums/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -96,5 +96,7 @@ public static class LDAPProperties public const string LockOutObservationWindow = "lockoutobservationwindow"; public const string PrincipalName = "msds-principalname"; public const string GroupType = "grouptype"; + public const string ServerReference = "serverreference"; + public const string SiteObject = "siteobject"; } } diff --git a/src/CommonLib/Enums/Labels.cs b/src/CommonLib/Enums/Labels.cs index b0bacb68e..76e05812e 100644 --- a/src/CommonLib/Enums/Labels.cs +++ b/src/CommonLib/Enums/Labels.cs @@ -18,6 +18,9 @@ public enum Label AIACA, EnterpriseCA, NTAuthStore, - IssuancePolicy + IssuancePolicy, + Site, + SiteServer, + SiteSubnet } } diff --git a/src/CommonLib/Enums/ObjectClass.cs b/src/CommonLib/Enums/ObjectClass.cs index f8bab0fc9..db98dd5e4 100644 --- a/src/CommonLib/Enums/ObjectClass.cs +++ b/src/CommonLib/Enums/ObjectClass.cs @@ -12,4 +12,7 @@ public static class ObjectClass { public const string OIDContainerClass = "msPKI-Enterprise-Oid"; public const string GMSAClass = "msds-groupmanagedserviceaccount"; public const string MSAClass = "msds-managedserviceaccount"; + public const string SiteClass = "site"; + public const string SiteServerClass = "server"; + public const string SiteSubnetClass = "subnet"; } \ No newline at end of file diff --git a/src/CommonLib/LdapProducerQueryGenerator.cs b/src/CommonLib/LdapProducerQueryGenerator.cs index 25c6cb9bf..c3ce2e25f 100644 --- a/src/CommonLib/LdapProducerQueryGenerator.cs +++ b/src/CommonLib/LdapProducerQueryGenerator.cs @@ -104,9 +104,10 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C properties.AddRange(CommonProperties.TypeResolutionProps); if (methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.ObjectProps) || - methods.HasFlag(CollectionMethod.Container) || methods.HasFlag(CollectionMethod.CertServices)) { + methods.HasFlag(CollectionMethod.Container) || methods.HasFlag(CollectionMethod.CertServices) || + methods.HasFlag(CollectionMethod.Site)) { filter = filter.AddContainers().AddConfiguration().AddCertificateTemplates().AddCertificateAuthorities() - .AddEnterpriseCertificationAuthorities().AddIssuancePolicies(); + .AddEnterpriseCertificationAuthorities().AddIssuancePolicies().AddSites().AddSiteServers().AddSiteSubnets(); if (methods.HasFlag(CollectionMethod.ObjectProps)) { properties.AddRange(CommonProperties.ObjectPropsProps); @@ -131,6 +132,13 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C properties.AddRange(CommonProperties.CertAbuseProps); } + if (methods.HasFlag(CollectionMethod.Site)) + { + properties.AddRange(CommonProperties.SiteProps); + properties.AddRange(CommonProperties.SiteServerProps); + properties.AddRange(CommonProperties.SiteSubnetProps); + } + return new GeneratedLdapParameters { Filter = filter, Attributes = properties.Distinct().ToArray() diff --git a/src/CommonLib/LdapQueries/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs index 508b5490c..88cef7462 100644 --- a/src/CommonLib/LdapQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -98,5 +98,23 @@ public static class CommonProperties public static readonly string[] StealthProperties = { LDAPProperties.HomeDirectory, LDAPProperties.ScriptPath, LDAPProperties.ProfilePath }; + + public static readonly string[] SiteProps = + { + LDAPProperties.DisplayName, LDAPProperties.Name, LDAPProperties.ObjectGUID, LDAPProperties.GPLink, + LDAPProperties.GroupPolicyOptions, LDAPProperties.ObjectClass + }; + + public static readonly string[] SiteServerProps = + { + LDAPProperties.DisplayName, LDAPProperties.Name, LDAPProperties.ObjectGUID, LDAPProperties.ObjectClass, LDAPProperties.DNSHostName, + LDAPProperties.ServerReference + }; + + public static readonly string[] SiteSubnetProps = + { + LDAPProperties.DisplayName, LDAPProperties.Name, LDAPProperties.CanonicalName, LDAPProperties.ObjectGUID, LDAPProperties.ObjectClass, + LDAPProperties.SiteObject + }; } } \ No newline at end of file diff --git a/src/CommonLib/LdapQueries/LdapFilter.cs b/src/CommonLib/LdapQueries/LdapFilter.cs index e98660ce1..a16584dce 100644 --- a/src/CommonLib/LdapQueries/LdapFilter.cs +++ b/src/CommonLib/LdapQueries/LdapFilter.cs @@ -215,6 +215,45 @@ public LdapFilter AddComputersNoMSAs(params string[] conditions) { return this; } + /// + /// Add a filter that will match Active Directory sites + /// + /// + /// + public LdapFilter AddSites(params string[] conditions) + { + _filterParts.Add(BuildString( + "(objectClass=site)", + conditions)); + return this; + } + + /// + /// Add a filter that will match Active Directory site servers + /// + /// + /// + public LdapFilter AddSiteServers(params string[] conditions) + { + _filterParts.Add(BuildString( + "(objectClass=server)", + conditions)); + return this; + } + + /// + /// Add a filter that will match Active Directory site subnets + /// + /// + /// + public LdapFilter AddSiteSubnets(params string[] conditions) + { + _filterParts.Add(BuildString( + "(objectClass=subnet)", + conditions)); + return this; + } + /// /// Adds a generic user specified filter /// diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index 14612da12..8c33a614c 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -1205,6 +1205,18 @@ internal static bool ResolveLabel(string objectIdentifier, string distinguishedN type = Label.IssuancePolicy; } } + else if (objectClasses.Contains(ObjectClass.SiteClass, StringComparer.OrdinalIgnoreCase)) + { + type = Label.Site; + } + else if (objectClasses.Contains(ObjectClass.SiteServerClass, StringComparer.OrdinalIgnoreCase)) + { + type = Label.SiteServer; + } + else if (objectClasses.Contains(ObjectClass.SiteSubnetClass, StringComparer.OrdinalIgnoreCase)) + { + type = Label.SiteSubnet; + } return type != Label.Base; } @@ -1214,7 +1226,7 @@ internal static bool ResolveLabel(string objectIdentifier, string distinguishedN if (!directoryObject.GetObjectIdentifier(out var objectIdentifier)) { return (false, default); } - + var res = new ResolvedSearchResult { ObjectId = objectIdentifier }; @@ -1270,11 +1282,10 @@ await utils.GetDomainNameFromSid(objectIdentifier) is (true, var domainName)) { if (await utils.GetWellKnownPrincipal(objectIdentifier, domain) is (true, var convertedPrincipal)) { res.ObjectId = convertedPrincipal.ObjectIdentifier; } - return (true, res); } - res.ObjectType = await ComputeLabel(directoryObject, objectIdentifier, domain, utils); + res.ObjectType = await ComputeLabel(directoryObject, objectIdentifier, domain, utils); directoryObject.TryGetProperty(LDAPProperties.SAMAccountName, out var samAccountName); res.DisplayName = ComputeDisplayName(directoryObject, domain, res.ObjectType, samAccountName); @@ -1395,6 +1406,43 @@ private static string ComputeDisplayName(IDirectoryObject directoryObject, strin displayName = $"UNKNOWN@{domain}"; } + break; + } + case Label.Site: { + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}@{domain}"; + } + else + { + displayName = $"UNKNOWN@{domain}"; + } + break; + } + case Label.SiteServer: + { + // Not specifying @{domain} here since Site servers may belong to other domains, so this might confuse the user + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}"; + } + else + { + displayName = $"UNKNOWN"; + } + break; + } + case Label.SiteSubnet: + { + // Not specifying @{domain} here since subnets are not domain-specific + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}"; + } + else + { + displayName = $"UNKNOWN"; + } break; } default: diff --git a/src/CommonLib/OutputTypes/Site.cs b/src/CommonLib/OutputTypes/Site.cs new file mode 100644 index 000000000..b2ce5c241 --- /dev/null +++ b/src/CommonLib/OutputTypes/Site.cs @@ -0,0 +1,12 @@ +using System; + +namespace SharpHoundCommonLib.OutputTypes +{ + public class Site : OutputBase + { + // Subnets and Servers are common site children; keep them optional and empty by default. + //public string[] Subnets { get; set; } = Array.Empty(); + //public TypedPrincipal[] Servers { get; set; } = Array.Empty(); + public GPLink[] Links { get; set; } = Array.Empty(); + } +} \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/SiteServer.cs b/src/CommonLib/OutputTypes/SiteServer.cs new file mode 100644 index 000000000..07c1542fc --- /dev/null +++ b/src/CommonLib/OutputTypes/SiteServer.cs @@ -0,0 +1,7 @@ +namespace SharpHoundCommonLib.OutputTypes +{ + public class SiteServer : OutputBase + { + + } +} \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/SiteSubnet.cs b/src/CommonLib/OutputTypes/SiteSubnet.cs new file mode 100644 index 000000000..568277f0b --- /dev/null +++ b/src/CommonLib/OutputTypes/SiteSubnet.cs @@ -0,0 +1,7 @@ +namespace SharpHoundCommonLib.OutputTypes +{ + public class SiteSubnet : OutputBase + { + + } +} \ No newline at end of file diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 383b69aff..a79c0a16b 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -38,7 +38,10 @@ static ACLProcessor() { { Label.EnterpriseCA, "ee4aa692-3bba-11d2-90cc-00c04fd91ab1" }, { Label.NTAuthStore, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, { Label.CertTemplate, "e5209ca2-3bba-11d2-90cc-00c04fd91ab1" }, - { Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563" } + { Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563" }, + { Label.Site, "bf967ab3-0de6-11d0-a285-00aa003049e2" }, + { Label.SiteServer, "bf967a92-0de6-11d0-a285-00aa003049e2" }, + { Label.SiteSubnet, "b7b13124-b82e-11d0-afee-0000f80367c1" } }; } @@ -734,7 +737,10 @@ or Label.RootCA or Label.EnterpriseCA or Label.AIACA or Label.NTAuthStore - or Label.IssuancePolicy) + or Label.IssuancePolicy + or Label.Site + or Label.SiteServer + or Label.SiteSubnet) if (aceType is ACEGuids.AllGuid or "") yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, @@ -776,7 +782,7 @@ or Label.NTAuthStore IsPermissionForOwnerRightsSid = isPermissionForOwnerRightsSid, IsInheritedPermissionForOwnerRightsSid = isInheritedPermissionForOwnerRightsSid, }; - else if (objectType is Label.OU or Label.Domain && aceType == ACEGuids.WriteGPLink) + else if (objectType is Label.OU or Label.Domain or Label.Site && aceType == ACEGuids.WriteGPLink) yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 4899b390f..3d38fb461 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -643,6 +643,29 @@ public async Task ReadIssuancePolicyProperties(IDirect return ret; } + public static Dictionary ReadSiteProperties(IDirectoryObject entry) + { + var props = GetCommonProps(entry); + return props; + } + + + public static Dictionary ReadSiteServerProperties(IDirectoryObject entry) + { + var props = GetCommonProps(entry); + props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName)); + props.Add("serverreference", entry.GetProperty(LDAPProperties.ServerReference)); + return props; + } + + public static Dictionary ReadSiteSubnetProperties(IDirectoryObject entry) + { + var props = GetCommonProps(entry); + props.Add("cn", entry.GetProperty(LDAPProperties.CanonicalName)); + props.Add("siteObject", entry.GetProperty(LDAPProperties.SiteObject)); + return props; + } + /// /// Attempts to parse all LDAP attributes outside of the ones already collected and converts them to a human readable /// format using a best guess diff --git a/src/CommonLib/Processors/SiteProcessor.cs b/src/CommonLib/Processors/SiteProcessor.cs new file mode 100644 index 000000000..a38bcdbbb --- /dev/null +++ b/src/CommonLib/Processors/SiteProcessor.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.OutputTypes; + +namespace SharpHoundCommonLib.Processors +{ + public class SiteProcessor + { + private readonly ILogger _log; + private readonly ILdapUtils _utils; + + public SiteProcessor(ILdapUtils utils, ILogger log = null) + { + _utils = utils; + _log = log ?? Logging.LogProvider.CreateLogger("SiteProc"); + } + + + /// + /// Helper function to pass commonlib types to GetContainingSiteForServer + /// + /// + /// + public async Task<(bool Success, TypedPrincipal principal)> GetContainingSiteForServer(IDirectoryObject entry) + { + if (entry.TryGetDistinguishedName(out var dn)) + { + _log.LogTrace("Reading containing site for server {DN}", dn); + return await GetContainingSiteForServer(dn); + } + + return (false, default); + } + + /// + /// Helper function to pass commonlib types to GetContainingSiteForSubnet + /// + /// + /// + public async Task<(bool Success, TypedPrincipal principal)> GetContainingSiteForSubnet(Dictionary subnetProperties) + { + if (subnetProperties.TryGetValue("siteObject", out var siteObject)) + { + return await GetContainingSiteForSubnet(siteObject.ToString()); + } + return (false, default); + } + + /// + /// Uses the distinguishedname of a site server object to get its containing site by stripping the two first parts and using the remainder to find the container object + /// Saves lots of LDAP calls compared to enumerating container info directly + /// + /// + /// + public async Task<(bool Success, TypedPrincipal Principal)> GetContainingSiteForServer(string distinguishedName) + { + var servercontainerdn = Helpers.RemoveDistinguishedNamePrefix(distinguishedName); + var sitedn = Helpers.RemoveDistinguishedNamePrefix(servercontainerdn); + return await _utils.ResolveDistinguishedName(sitedn); + } + + /// + /// Uses the siteObject of a subnet to get its containing site + /// + /// + /// + public async Task<(bool Success, TypedPrincipal Principal)> GetContainingSiteForSubnet(string siteObject) + { + return await _utils.ResolveDistinguishedName(siteObject); + } + + public IAsyncEnumerable ReadSiteGPLinks(ResolvedSearchResult result, IDirectoryObject entry) + { + if (entry.TryGetProperty(LDAPProperties.GPLink, out var links)) + { + return ReadSiteGPLinks(links); + } + + return AsyncEnumerable.Empty(); + } + + /// + /// Reads the "gplink" property from a SearchResult and converts the links into the acceptable SharpHound format + /// + /// + /// + public async IAsyncEnumerable ReadSiteGPLinks(string gpLink) + { + if (gpLink == null) + yield break; + + foreach (var link in Helpers.SplitGPLinkProperty(gpLink)) + { + var enforced = link.Status.Equals("2"); + + var res = await _utils.ResolveDistinguishedName(link.DistinguishedName); + + if (res.Success) + { + yield return new GPLink + { + GUID = res.Principal.ObjectIdentifier, + IsEnforced = enforced + }; + } + } + } + } +} \ No newline at end of file From 9214843557ff7a3fae778268468b4a1a4ec1d2ed Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 10:26:03 +0200 Subject: [PATCH 02/11] Guard against null siteObject --- src/CommonLib/Processors/SiteProcessor.cs | 11 ++++- test/unit/SiteProcessorTest.cs | 52 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 test/unit/SiteProcessorTest.cs diff --git a/src/CommonLib/Processors/SiteProcessor.cs b/src/CommonLib/Processors/SiteProcessor.cs index a38bcdbbb..1bb1e888b 100644 --- a/src/CommonLib/Processors/SiteProcessor.cs +++ b/src/CommonLib/Processors/SiteProcessor.cs @@ -43,7 +43,14 @@ public SiteProcessor(ILdapUtils utils, ILogger log = null) { if (subnetProperties.TryGetValue("siteObject", out var siteObject)) { - return await GetContainingSiteForSubnet(siteObject.ToString()); + if (siteObject == null) + return (false, default); + + var siteObjectDn = siteObject.ToString(); + if (string.IsNullOrWhiteSpace(siteObjectDn)) + return (false, default); + + return await GetContainingSiteForSubnet(siteObjectDn); } return (false, default); } @@ -108,4 +115,4 @@ public async IAsyncEnumerable ReadSiteGPLinks(string gpLink) } } } -} \ No newline at end of file +} diff --git a/test/unit/SiteProcessorTest.cs b/test/unit/SiteProcessorTest.cs new file mode 100644 index 000000000..a9a24bb93 --- /dev/null +++ b/test/unit/SiteProcessorTest.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; +using SharpHoundCommonLib.Processors; +using Xunit; + +namespace CommonLibTest +{ + public class SiteProcessorTest + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task SiteProcessor_GetContainingSiteForSubnet_InvalidSiteObject_ReturnsFalse(string siteObject) + { + var utils = new Mock(MockBehavior.Strict); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetContainingSiteForSubnet(new Dictionary + { + ["siteObject"] = siteObject + }); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SiteProcessor_GetContainingSiteForSubnet_ValidSiteObject_ResolvesDistinguishedName() + { + const string siteObject = "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=local"; + var expected = new TypedPrincipal("TESTLAB.LOCAL-SITE", Label.Site); + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(siteObject)).ReturnsAsync((true, expected)); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetContainingSiteForSubnet(new Dictionary + { + ["siteObject"] = siteObject + }); + + Assert.True(success); + Assert.Equal(expected, principal); + utils.Verify(x => x.ResolveDistinguishedName(siteObject), Times.Once); + } + } +} From a615dced2083e8978918ec0874d73de2970bb7b5 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 10:45:16 +0200 Subject: [PATCH 03/11] avoid ACL edges to SiteServer and SiteSubnet --- src/CommonLib/Processors/ACLProcessor.cs | 5 +++++ test/unit/ACLProcessorTest.cs | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 00ed764b5..d9427b23c 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -452,6 +452,11 @@ public IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, string obje public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDomain, Label objectType, bool hasLaps, bool checkForOwnerRights, string objectName) { + if (objectType is Label.SiteServer or Label.SiteSubnet) { + _log.LogDebug("Skipping ACL processing for {ObjectType} object {ObjectName}", objectType, objectName); + yield break; + } + await BuildGuidCache(objectDomain); if (ntSecurityDescriptor == null) { diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs index a8e4d3b33..ecf33564b 100644 --- a/test/unit/ACLProcessorTest.cs +++ b/test/unit/ACLProcessorTest.cs @@ -230,6 +230,24 @@ public async Task ACLProcessor_ProcessACL_Null_NTSecurityDescriptor() Assert.Empty(result); } + [Theory] + [InlineData(Label.SiteServer)] + [InlineData(Label.SiteSubnet)] + public async Task ACLProcessor_ProcessACL_SiteServerAndSiteSubnet_ReturnsNothing(Label objectType) + { + var mockLDAPUtils = new Mock(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + + var result = await processor.ProcessACL(bytes, _testDomainName, objectType, false).ToArrayAsync(); + + Assert.Empty(result); + mockLDAPUtils.Verify(x => x.PagedQuery(It.IsAny(), It.IsAny()), + Times.Never); + mockLDAPUtils.Verify(x => x.MakeSecurityDescriptor(), Times.Never); + mockLDAPUtils.Verify(x => x.ResolveIDAndType(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public async Task ACLProcessor_ProcessACL_Yields_Owns_ACE() { var expectedSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; @@ -2289,4 +2307,4 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_Computer_WritePublicInfor Assert.Equal(actual.RightName, expectedRightName); } } -} \ No newline at end of file +} From 6fd6029b8f912a2ce31170493987fc63aa46fe71 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 10:53:47 +0200 Subject: [PATCH 04/11] add codex to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6e28db82c..45103240f 100644 --- a/.gitignore +++ b/.gitignore @@ -603,3 +603,6 @@ FodyWeavers.xsd # JetBrains Rider .idea/ *.sln.iml + +# Codex local workspace state +.codex From 20d164a7fddda20f08381bcb8794d4181331d119 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 11:54:53 +0200 Subject: [PATCH 05/11] tests for site objects --- test/unit/ACLProcessorTest.cs | 39 ++++++++++ test/unit/DirectoryObjectTests.cs | 20 ++++- test/unit/LDAPFilterTest.cs | 29 +++++++- test/unit/LDAPUtilsTest.cs | 34 ++++++++- test/unit/LdapProducerQueryGeneratorTest.cs | 32 ++++++++ test/unit/LdapPropertyTests.cs | 76 +++++++++++++++++++ test/unit/SiteProcessorTest.cs | 82 +++++++++++++++++++++ 7 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 test/unit/LdapProducerQueryGeneratorTest.cs diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs index ecf33564b..bb5a3a371 100644 --- a/test/unit/ACLProcessorTest.cs +++ b/test/unit/ACLProcessorTest.cs @@ -1782,6 +1782,45 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_OU_WriteGPLink() Assert.Equal(expectedRightName, actual.RightName); } + [Fact] + public async Task ACLProcessor_ProcessACL_GenericWrite_Site_WriteGPLink() + { + var expectedPrincipalType = Label.Group; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var expectedRightName = EdgeNames.WriteGPLink; + + var mockLDAPUtils = new Mock(); + var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); + var mockRule = new Mock(MockBehavior.Loose, null); + var collection = new List(); + mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow); + mockRule.Setup(x => x.IsAceInheritedFrom("bf967ab3-0de6-11d0-a285-00aa003049e2")).Returns(true); + mockRule.Setup(x => x.IdentityReference()).Returns(expectedPrincipalSID); + mockRule.Setup(x => x.ActiveDirectoryRights()).Returns(ActiveDirectoryRights.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WriteGPLink)); + collection.Add(mockRule.Object); + + mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(collection); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); + + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Site, true).ToArrayAsync(); + + Assert.Single(result); + var actual = result.First(); + Assert.Equal(expectedPrincipalType, actual.PrincipalType); + Assert.Equal(expectedPrincipalSID, actual.PrincipalSID); + Assert.False(actual.IsInherited); + Assert.Equal(expectedRightName, actual.RightName); + } + [Fact] public async Task ACLProcessor_ProcessACL_GenericWrite_User_AddKeyPrincipal() { diff --git a/test/unit/DirectoryObjectTests.cs b/test/unit/DirectoryObjectTests.cs index 44966a57d..1b6409a97 100644 --- a/test/unit/DirectoryObjectTests.cs +++ b/test/unit/DirectoryObjectTests.cs @@ -299,6 +299,24 @@ public void Test_GetLabel_CertificationAuthorityObjects() { Assert.True(mock.GetLabel(out label)); Assert.Equal(Label.NTAuthStore, label); } + + [Theory] + [InlineData(ObjectClass.SiteClass, Label.Site)] + [InlineData(ObjectClass.SiteServerClass, Label.SiteServer)] + [InlineData(ObjectClass.SiteSubnetClass, Label.SiteSubnet)] + public void Test_GetLabel_SiteObjects(string objectClass, Label expectedLabel) { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", objectClass } }, + }; + + var mock = new MockDirectoryObject("CN=Test,CN=Sites,CN=Configuration,DC=Testlab,DC=local", + attribs, + "", + new Guid().ToString()); + + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(expectedLabel, label); + } [Fact] public void Test_GetLabel_NTAuthCertificateObject() { @@ -330,4 +348,4 @@ public void Test_GetLabel_NoLabel() { Assert.Equal(Label.Base, label); } } -} \ No newline at end of file +} diff --git a/test/unit/LDAPFilterTest.cs b/test/unit/LDAPFilterTest.cs index 3c5f86d07..5eacefc2e 100644 --- a/test/unit/LDAPFilterTest.cs +++ b/test/unit/LDAPFilterTest.cs @@ -106,6 +106,33 @@ public void LDAPFilter_GetFilterList_MergeFilter() Assert.Equal(2, filters.Count); } + [Theory] + [InlineData("site", "(objectClass=site)", "(&(objectClass=site)(name=Test))")] + [InlineData("server", "(objectClass=server)", "(&(objectClass=server)(name=Test))")] + [InlineData("subnet", "(objectClass=subnet)", "(&(objectClass=subnet)(name=Test))")] + public void LDAPFilter_SiteFilters_FilterCorrect(string objectClass, string expectedFilter, + string expectedFilterWithCondition) + { + var test = objectClass switch + { + "site" => new LdapFilter().AddSites(), + "server" => new LdapFilter().AddSiteServers(), + "subnet" => new LdapFilter().AddSiteSubnets(), + _ => throw new ArgumentOutOfRangeException(nameof(objectClass)) + }; + + var testWithCondition = objectClass switch + { + "site" => new LdapFilter().AddSites("name=Test"), + "server" => new LdapFilter().AddSiteServers("name=Test"), + "subnet" => new LdapFilter().AddSiteSubnets("name=Test"), + _ => throw new ArgumentOutOfRangeException(nameof(objectClass)) + }; + + Assert.Equal(expectedFilter, test.GetFilter()); + Assert.Equal(expectedFilterWithCondition, testWithCondition.GetFilter()); + } + #endregion } -} \ No newline at end of file +} diff --git a/test/unit/LDAPUtilsTest.cs b/test/unit/LDAPUtilsTest.cs index 33ea5d067..acf9f2ca5 100644 --- a/test/unit/LDAPUtilsTest.cs +++ b/test/unit/LDAPUtilsTest.cs @@ -249,6 +249,38 @@ public async Task Test_ResolveSearchResult_TrustAccount() { Assert.False(result.Deleted); } + [Theory] + [InlineData(ObjectClass.SiteClass, Label.Site, + "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", + "Default-First-Site-Name", "DEFAULT-FIRST-SITE-NAME@TESTLAB.LOCAL")] + [InlineData(ObjectClass.SiteServerClass, Label.SiteServer, + "CN=PRIMARY,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", + "primary.testlab.local", "PRIMARY.TESTLAB.LOCAL")] + [InlineData(ObjectClass.SiteSubnetClass, Label.SiteSubnet, + "CN=10.0.0.0/24,CN=Subnets,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", + "10.0.0.0/24", "10.0.0.0/24")] + public async Task Test_ResolveSearchResult_SiteObjects(string objectClass, Label expectedLabel, + string distinguishedName, string name, string expectedDisplayName) { + var utils = new MockLdapUtils(); + var guid = new Guid().ToString(); + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", objectClass } }, + { LDAPProperties.Name, name } + }; + + var mock = new MockDirectoryObject(distinguishedName, attribs, "", guid); + + var (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + + Assert.True(success); + Assert.Equal(guid, result.ObjectId); + Assert.Equal(expectedLabel, result.ObjectType); + Assert.Equal(expectedDisplayName, result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); + } + [Fact] public async Task Test_ResolveHostToSid_BlankHost() { var spn = "MSSQLSvc/:1433"; @@ -287,4 +319,4 @@ public async Task EnterpriseDomainControllersGroup_CorrectValues() { Assert.Equal(3, entDCGroup.Members.Length); } } -} \ No newline at end of file +} diff --git a/test/unit/LdapProducerQueryGeneratorTest.cs b/test/unit/LdapProducerQueryGeneratorTest.cs new file mode 100644 index 000000000..82548fc35 --- /dev/null +++ b/test/unit/LdapProducerQueryGeneratorTest.cs @@ -0,0 +1,32 @@ +using System.Linq; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.LDAPQueries; +using Xunit; + +namespace CommonLibTest; + +public class LdapProducerQueryGeneratorTest +{ + [Fact] + public void GenerateConfigurationPartitionParameters_Site_IncludesSiteFiltersAndProperties() + { + var expectedFilter = new LdapFilter() + .AddContainers() + .AddConfiguration() + .AddCertificateTemplates() + .AddCertificateAuthorities() + .AddEnterpriseCertificationAuthorities() + .AddIssuancePolicies() + .AddSites() + .AddSiteServers() + .AddSiteSubnets() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters(CollectionMethod.Site); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + Assert.All(CommonProperties.SiteProps.Concat(CommonProperties.SiteServerProps).Concat(CommonProperties.SiteSubnetProps), + attribute => Assert.Contains(attribute, result.Attributes)); + } +} diff --git a/test/unit/LdapPropertyTests.cs b/test/unit/LdapPropertyTests.cs index 9d3053d13..e72b15d1a 100644 --- a/test/unit/LdapPropertyTests.cs +++ b/test/unit/LdapPropertyTests.cs @@ -1063,6 +1063,82 @@ public async Task LDAPPropertyProcessor_ReadIssuancePolicyProperties_NoOIDGroupL Assert.Contains("certtemplateoid", keys); } + [Fact] + public void LDAPPropertyProcessor_ReadSiteProperties() + { + var mock = new MockDirectoryObject("CN=DEFAULT-FIRST-SITE-NAME,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL", + new Dictionary + { + {LDAPProperties.Description, "Default site"}, + {LDAPProperties.WhenCreated, 1712567279}, + {"domain", "TESTLAB.LOCAL"}, + {"name", "DEFAULT-FIRST-SITE-NAME@TESTLAB.LOCAL"}, + {"domainsid", "S-1-5-21-3130019616-2776909439-2417379446"} + }, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var test = LdapPropertyProcessor.ReadSiteProperties(mock); + var keys = test.Keys; + + Assert.DoesNotContain("domain", keys); + Assert.DoesNotContain("name", keys); + Assert.DoesNotContain("domainsid", keys); + Assert.Contains("description", keys); + Assert.Contains("whencreated", keys); + } + + [Fact] + public void LDAPPropertyProcessor_ReadSiteServerProperties() + { + var serverReference = "CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"; + var mock = new MockDirectoryObject("CN=PRIMARY,CN=SERVERS,CN=DEFAULT-FIRST-SITE-NAME,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL", + new Dictionary + { + {LDAPProperties.Description, "Site server"}, + {LDAPProperties.WhenCreated, 1712567279}, + {LDAPProperties.DNSHostName, "primary.testlab.local"}, + {LDAPProperties.ServerReference, serverReference}, + {"domain", "TESTLAB.LOCAL"}, + {"name", "PRIMARY"} + }, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var test = LdapPropertyProcessor.ReadSiteServerProperties(mock); + var keys = test.Keys; + + Assert.DoesNotContain("domain", keys); + Assert.DoesNotContain("name", keys); + Assert.Contains("description", keys); + Assert.Contains("whencreated", keys); + Assert.Equal("primary.testlab.local", test["dnshostname"]); + Assert.Equal(serverReference, test["serverreference"]); + } + + [Fact] + public void LDAPPropertyProcessor_ReadSiteSubnetProperties() + { + var siteObject = "CN=DEFAULT-FIRST-SITE-NAME,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL"; + var canonicalName = "TESTLAB.LOCAL/Configuration/Sites/Subnets/10.0.0.0/24"; + var mock = new MockDirectoryObject("CN=10.0.0.0/24,CN=SUBNETS,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL", + new Dictionary + { + {LDAPProperties.Description, "Site subnet"}, + {LDAPProperties.WhenCreated, 1712567279}, + {LDAPProperties.CanonicalName, canonicalName}, + {LDAPProperties.SiteObject, siteObject}, + {"domain", "TESTLAB.LOCAL"}, + {"name", "10.0.0.0/24"} + }, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var test = LdapPropertyProcessor.ReadSiteSubnetProperties(mock); + var keys = test.Keys; + + Assert.DoesNotContain("domain", keys); + Assert.DoesNotContain("name", keys); + Assert.Contains("description", keys); + Assert.Contains("whencreated", keys); + Assert.Equal(canonicalName, test["cn"]); + Assert.Equal(siteObject, test["siteObject"]); + } + [Fact] public void LDAPPropertyProcessor_ParseAllProperties() { diff --git a/test/unit/SiteProcessorTest.cs b/test/unit/SiteProcessorTest.cs index a9a24bb93..d487b7e4d 100644 --- a/test/unit/SiteProcessorTest.cs +++ b/test/unit/SiteProcessorTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using CommonLibTest.Facades; using Moq; using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; @@ -11,6 +12,9 @@ namespace CommonLibTest { public class SiteProcessorTest { + private const string TestGPLinkString = + "[LDAP://cn={94DD0260-38B5-497E-8876-10E7A96E80D0},cn=policies,cn=system,DC=testlab,DC=local;0][LDAP://cn={C52F168C-CD05-4487-B405-564934DA8EFF},cn=policies,cn=system,DC=testlab,DC=local;2]"; + [Theory] [InlineData(null)] [InlineData("")] @@ -48,5 +52,83 @@ public async Task SiteProcessor_GetContainingSiteForSubnet_ValidSiteObject_Resol Assert.Equal(expected, principal); utils.Verify(x => x.ResolveDistinguishedName(siteObject), Times.Once); } + + [Fact] + public async Task SiteProcessor_GetContainingSiteForServer_NoDistinguishedName_ReturnsFalse() + { + var utils = new Mock(MockBehavior.Strict); + var processor = new SiteProcessor(utils.Object); + var entry = new MockDirectoryObject("", new Dictionary(), "", ""); + + var (success, principal) = await processor.GetContainingSiteForServer(entry); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SiteProcessor_GetContainingSiteForServer_ValidDistinguishedName_ResolvesParentSite() + { + const string serverDn = "CN=PRIMARY,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=local"; + const string siteDn = "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=local"; + var expected = new TypedPrincipal("TESTLAB.LOCAL-SITE", Label.Site); + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(siteDn)).ReturnsAsync((true, expected)); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetContainingSiteForServer(serverDn); + + Assert.True(success); + Assert.Equal(expected, principal); + utils.Verify(x => x.ResolveDistinguishedName(siteDn), Times.Once); + } + + [Fact] + public async Task SiteProcessor_ReadSiteGPLinks_IgnoresNull() + { + var processor = new SiteProcessor(new MockLdapUtils()); + + var test = await processor.ReadSiteGPLinks(null).ToArrayAsync(); + + Assert.Empty(test); + } + + [Fact] + public async Task SiteProcessor_ReadSiteGPLinks_UnresolvedGPLink_IsIgnored() + { + var processor = new SiteProcessor(new MockLdapUtils()); + const string gpLink = + "[LDAP://cn={94DD0260-38B5-497E-8876-ABCDEFG},cn=policies,cn=system,DC=testlab,DC=local;0]"; + + var test = await processor.ReadSiteGPLinks(gpLink).ToArrayAsync(); + + Assert.Empty(test); + } + + [Fact] + public async Task SiteProcessor_ReadSiteGPLinks_ReturnsCorrectValues() + { + var processor = new SiteProcessor(new MockLdapUtils()); + + var test = await processor.ReadSiteGPLinks(TestGPLinkString).ToArrayAsync(); + + var expected = new GPLink[] + { + new() + { + GUID = "B39818AF-6349-401A-AE0A-E4972F5BF6D9", + IsEnforced = false + }, + new() + { + GUID = "ACDD64D3-67B3-401F-A6CC-804B3F7B1533", + IsEnforced = true + } + }; + + Assert.Equal(2, test.Length); + Assert.Equal(expected, test); + } } } From 7946b5607cc8cfe6f567358f46064dcfd65667a9 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 11:56:29 +0200 Subject: [PATCH 06/11] Add IsServer --- src/CommonLib/Enums/EdgeNames.cs | 3 +- src/CommonLib/OutputTypes/SiteServer.cs | 4 +- src/CommonLib/Processors/SiteProcessor.cs | 38 ++++++++++++ test/unit/SiteProcessorTest.cs | 76 +++++++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/CommonLib/Enums/EdgeNames.cs b/src/CommonLib/Enums/EdgeNames.cs index ed0bd4af9..84749b9e4 100644 --- a/src/CommonLib/Enums/EdgeNames.cs +++ b/src/CommonLib/Enums/EdgeNames.cs @@ -24,6 +24,7 @@ public static class EdgeNames public const string WriteGPLink = "WriteGPLink"; public const string WriteAltSecurityIdentities = "WriteAltSecurityIdentities"; public const string WritePublicInformation = "WritePublicInformation"; + public const string SeverIs = "SeverIs"; //CertAbuse edges public const string WritePKIEnrollmentFlag = "WritePKIEnrollmentFlag"; @@ -32,4 +33,4 @@ public static class EdgeNames public const string ManageCertificates = "ManageCertificates"; public const string Enroll = "Enroll"; } -} \ No newline at end of file +} diff --git a/src/CommonLib/OutputTypes/SiteServer.cs b/src/CommonLib/OutputTypes/SiteServer.cs index 07c1542fc..a2112a773 100644 --- a/src/CommonLib/OutputTypes/SiteServer.cs +++ b/src/CommonLib/OutputTypes/SiteServer.cs @@ -2,6 +2,6 @@ { public class SiteServer : OutputBase { - + public TypedPrincipal SeverIs { get; set; } } -} \ No newline at end of file +} diff --git a/src/CommonLib/Processors/SiteProcessor.cs b/src/CommonLib/Processors/SiteProcessor.cs index 1bb1e888b..f485764d0 100644 --- a/src/CommonLib/Processors/SiteProcessor.cs +++ b/src/CommonLib/Processors/SiteProcessor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; namespace SharpHoundCommonLib.Processors @@ -55,6 +56,27 @@ public SiteProcessor(ILdapUtils utils, ILogger log = null) return (false, default); } + public async Task<(bool Success, TypedPrincipal principal)> GetReferencedComputerForServer(IDirectoryObject entry) + { + if (entry.TryGetProperty(LDAPProperties.ServerReference, out var serverReference)) + { + return await GetReferencedComputerForServer(serverReference); + } + + return (false, default); + } + + public async Task<(bool Success, TypedPrincipal principal)> GetReferencedComputerForServer(Dictionary serverProperties) + { + if (!serverProperties.TryGetValue(LDAPProperties.ServerReference, out var serverReference) || + serverReference == null) + { + return (false, default); + } + + return await GetReferencedComputerForServer(serverReference.ToString()); + } + /// /// Uses the distinguishedname of a site server object to get its containing site by stripping the two first parts and using the remainder to find the container object /// Saves lots of LDAP calls compared to enumerating container info directly @@ -78,6 +100,22 @@ public SiteProcessor(ILdapUtils utils, ILogger log = null) return await _utils.ResolveDistinguishedName(siteObject); } + public async Task<(bool Success, TypedPrincipal Principal)> GetReferencedComputerForServer(string serverReference) + { + if (string.IsNullOrWhiteSpace(serverReference)) + { + return (false, default); + } + + var resolved = await _utils.ResolveDistinguishedName(serverReference); + if (!resolved.Success || resolved.Principal == null || resolved.Principal.ObjectType != Label.Computer) + { + return (false, default); + } + + return resolved; + } + public IAsyncEnumerable ReadSiteGPLinks(ResolvedSearchResult result, IDirectoryObject entry) { if (entry.TryGetProperty(LDAPProperties.GPLink, out var links)) diff --git a/test/unit/SiteProcessorTest.cs b/test/unit/SiteProcessorTest.cs index d487b7e4d..11c7a8b9b 100644 --- a/test/unit/SiteProcessorTest.cs +++ b/test/unit/SiteProcessorTest.cs @@ -84,6 +84,82 @@ public async Task SiteProcessor_GetContainingSiteForServer_ValidDistinguishedNam utils.Verify(x => x.ResolveDistinguishedName(siteDn), Times.Once); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task SiteProcessor_GetReferencedComputerForServer_InvalidServerReference_ReturnsFalse(string serverReference) + { + var utils = new Mock(MockBehavior.Strict); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetReferencedComputerForServer(new Dictionary + { + [LDAPProperties.ServerReference] = serverReference + }); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SiteProcessor_GetReferencedComputerForServer_ValidServerReference_ResolvesComputer() + { + const string serverReference = "CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"; + var expected = new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-1001", Label.Computer); + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(serverReference)).ReturnsAsync((true, expected)); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetReferencedComputerForServer(new Dictionary + { + [LDAPProperties.ServerReference] = serverReference + }); + + Assert.True(success); + Assert.Equal(expected, principal); + utils.Verify(x => x.ResolveDistinguishedName(serverReference), Times.Once); + } + + [Fact] + public async Task SiteProcessor_GetReferencedComputerForServer_NonComputerReference_ReturnsFalse() + { + const string serverReference = "CN=ADMINISTRATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL"; + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(serverReference)) + .ReturnsAsync((true, new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-544", Label.Group))); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetReferencedComputerForServer(serverReference); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(serverReference), Times.Once); + } + + [Fact] + public async Task SiteProcessor_GetReferencedComputerForServer_DirectoryObject_UsesServerReference() + { + const string serverReference = "CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"; + var processor = new SiteProcessor(new MockLdapUtils()); + var entry = new MockDirectoryObject("", new Dictionary + { + [LDAPProperties.ServerReference] = serverReference + }, "", ""); + + var (success, principal) = await processor.GetReferencedComputerForServer(entry); + + Assert.True(success); + Assert.Equal(new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-1001", Label.Computer), principal); + } + + [Fact] + public void SiteServer_SeverIs_EdgeNameMatchesOutputProperty() + { + Assert.Equal(nameof(SiteServer.SeverIs), EdgeNames.SeverIs); + } + [Fact] public async Task SiteProcessor_ReadSiteGPLinks_IgnoresNull() { From 4e96f6a002037e948c1dea7a466b2a88f3bd5ab3 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 12:39:05 +0200 Subject: [PATCH 07/11] fix many things - Fix typo in ServerIs edge name - Fix collection scope - Fix node names --- src/CommonLib/Enums/EdgeNames.cs | 2 +- src/CommonLib/LdapProducerQueryGenerator.cs | 29 +++++++++--- src/CommonLib/LdapUtils.cs | 17 ++++--- src/CommonLib/OutputTypes/Site.cs | 7 ++- src/CommonLib/OutputTypes/SiteServer.cs | 2 +- src/CommonLib/Processors/ACLProcessor.cs | 4 +- test/unit/LdapProducerQueryGeneratorTest.cs | 49 +++++++++++++++++++-- test/unit/SiteProcessorTest.cs | 14 +++++- 8 files changed, 98 insertions(+), 26 deletions(-) diff --git a/src/CommonLib/Enums/EdgeNames.cs b/src/CommonLib/Enums/EdgeNames.cs index 84749b9e4..833621bc1 100644 --- a/src/CommonLib/Enums/EdgeNames.cs +++ b/src/CommonLib/Enums/EdgeNames.cs @@ -24,7 +24,7 @@ public static class EdgeNames public const string WriteGPLink = "WriteGPLink"; public const string WriteAltSecurityIdentities = "WriteAltSecurityIdentities"; public const string WritePublicInformation = "WritePublicInformation"; - public const string SeverIs = "SeverIs"; + public const string ServerIs = "ServerIs"; //CertAbuse edges public const string WritePKIEnrollmentFlag = "WritePKIEnrollmentFlag"; diff --git a/src/CommonLib/LdapProducerQueryGenerator.cs b/src/CommonLib/LdapProducerQueryGenerator.cs index c3ce2e25f..501f1eb56 100644 --- a/src/CommonLib/LdapProducerQueryGenerator.cs +++ b/src/CommonLib/LdapProducerQueryGenerator.cs @@ -103,11 +103,30 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C properties.AddRange(CommonProperties.BaseQueryProps); properties.AddRange(CommonProperties.TypeResolutionProps); - if (methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.ObjectProps) || - methods.HasFlag(CollectionMethod.Container) || methods.HasFlag(CollectionMethod.CertServices) || + var collectBroadConfigObjects = methods.HasFlag(CollectionMethod.ACL) || + methods.HasFlag(CollectionMethod.ObjectProps) || + methods.HasFlag(CollectionMethod.Container); + + if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.CertServices) || methods.HasFlag(CollectionMethod.Site)) { - filter = filter.AddContainers().AddConfiguration().AddCertificateTemplates().AddCertificateAuthorities() - .AddEnterpriseCertificationAuthorities().AddIssuancePolicies().AddSites().AddSiteServers().AddSiteSubnets(); + filter = filter.AddContainers() + .AddConfiguration(); + + if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.CertServices)) { + filter.AddCertificateTemplates() + .AddCertificateAuthorities() + .AddEnterpriseCertificationAuthorities() + .AddIssuancePolicies(); + } + else if (methods.HasFlag(CollectionMethod.CARegistry)) { + filter.AddEnterpriseCertificationAuthorities(); + } + + if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.Site)) { + filter.AddSites() + .AddSiteServers() + .AddSiteSubnets(); + } if (methods.HasFlag(CollectionMethod.ObjectProps)) { properties.AddRange(CommonProperties.ObjectPropsProps); @@ -160,4 +179,4 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C public class GeneratedLdapParameters { public string[] Attributes { get; set; } public LdapFilter Filter { get; set; } -} \ No newline at end of file +} diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index a86c49c6a..04b8c0301 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -1435,27 +1435,30 @@ private static string ComputeDisplayName(IDirectoryObject directoryObject, strin } case Label.SiteServer: { - // Not specifying @{domain} here since Site servers may belong to other domains, so this might confuse the user - if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + if (directoryObject.TryGetProperty(LDAPProperties.DNSHostName, out var dnsHostName) && + !string.IsNullOrWhiteSpace(dnsHostName)) + { + displayName = dnsHostName; + } + else if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) { - displayName = $"{name}"; + displayName = $"{name}@{domain}"; } else { - displayName = $"UNKNOWN"; + displayName = $"UNKNOWN@{domain}"; } break; } case Label.SiteSubnet: { - // Not specifying @{domain} here since subnets are not domain-specific if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) { - displayName = $"{name}"; + displayName = $"{name}@{domain}"; } else { - displayName = $"UNKNOWN"; + displayName = $"UNKNOWN@{domain}"; } break; } diff --git a/src/CommonLib/OutputTypes/Site.cs b/src/CommonLib/OutputTypes/Site.cs index b2ce5c241..b18976063 100644 --- a/src/CommonLib/OutputTypes/Site.cs +++ b/src/CommonLib/OutputTypes/Site.cs @@ -4,9 +4,8 @@ namespace SharpHoundCommonLib.OutputTypes { public class Site : OutputBase { - // Subnets and Servers are common site children; keep them optional and empty by default. - //public string[] Subnets { get; set; } = Array.Empty(); - //public TypedPrincipal[] Servers { get; set; } = Array.Empty(); + public TypedPrincipal[] ChildObjects { get; set; } = Array.Empty(); public GPLink[] Links { get; set; } = Array.Empty(); + public string[] InheritanceHashes { get; set; } = Array.Empty(); } -} \ No newline at end of file +} diff --git a/src/CommonLib/OutputTypes/SiteServer.cs b/src/CommonLib/OutputTypes/SiteServer.cs index a2112a773..5a82cfc50 100644 --- a/src/CommonLib/OutputTypes/SiteServer.cs +++ b/src/CommonLib/OutputTypes/SiteServer.cs @@ -2,6 +2,6 @@ { public class SiteServer : OutputBase { - public TypedPrincipal SeverIs { get; set; } + public TypedPrincipal ServerIs { get; set; } } } diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index d9427b23c..1488a4938 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -743,9 +743,7 @@ or Label.EnterpriseCA or Label.AIACA or Label.NTAuthStore or Label.IssuancePolicy - or Label.Site - or Label.SiteServer - or Label.SiteSubnet) + or Label.Site) if (aceType is ACEGuids.AllGuid or "") yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, diff --git a/test/unit/LdapProducerQueryGeneratorTest.cs b/test/unit/LdapProducerQueryGeneratorTest.cs index 82548fc35..7fcd9b24f 100644 --- a/test/unit/LdapProducerQueryGeneratorTest.cs +++ b/test/unit/LdapProducerQueryGeneratorTest.cs @@ -10,6 +10,50 @@ public class LdapProducerQueryGeneratorTest { [Fact] public void GenerateConfigurationPartitionParameters_Site_IncludesSiteFiltersAndProperties() + { + var expectedFilter = new LdapFilter() + .AddContainers() + .AddConfiguration() + .AddSites() + .AddSiteServers() + .AddSiteSubnets() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters(CollectionMethod.Site); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + Assert.All(CommonProperties.SiteProps.Concat(CommonProperties.SiteServerProps).Concat(CommonProperties.SiteSubnetProps), + attribute => Assert.Contains(attribute, result.Attributes)); + Assert.DoesNotContain("(objectclass=pKICertificateTemplate)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=certificationAuthority)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectCategory=pKIEnrollmentService)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=msPKI-Enterprise-Oid)", result.Filter.GetFilter()); + } + + [Fact] + public void GenerateConfigurationPartitionParameters_CertServices_IncludesCertFiltersAndProperties() + { + var expectedFilter = new LdapFilter() + .AddContainers() + .AddConfiguration() + .AddCertificateTemplates() + .AddCertificateAuthorities() + .AddEnterpriseCertificationAuthorities() + .AddIssuancePolicies() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters(CollectionMethod.CertServices); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + Assert.All(CommonProperties.CertAbuseProps, attribute => Assert.Contains(attribute, result.Attributes)); + Assert.DoesNotContain(LDAPProperties.ServerReference, result.Attributes); + Assert.DoesNotContain("(objectClass=site)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=server)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=subnet)", result.Filter.GetFilter()); + } + + [Fact] + public void GenerateConfigurationPartitionParameters_SiteAndCertServices_IncludesBothFilterSets() { var expectedFilter = new LdapFilter() .AddContainers() @@ -23,10 +67,9 @@ public void GenerateConfigurationPartitionParameters_Site_IncludesSiteFiltersAnd .AddSiteSubnets() .GetFilter(); - var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters(CollectionMethod.Site); + var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters( + CollectionMethod.Site | CollectionMethod.CertServices); Assert.Equal(expectedFilter, result.Filter.GetFilter()); - Assert.All(CommonProperties.SiteProps.Concat(CommonProperties.SiteServerProps).Concat(CommonProperties.SiteSubnetProps), - attribute => Assert.Contains(attribute, result.Attributes)); } } diff --git a/test/unit/SiteProcessorTest.cs b/test/unit/SiteProcessorTest.cs index 11c7a8b9b..6b45195e3 100644 --- a/test/unit/SiteProcessorTest.cs +++ b/test/unit/SiteProcessorTest.cs @@ -155,9 +155,19 @@ public async Task SiteProcessor_GetReferencedComputerForServer_DirectoryObject_U } [Fact] - public void SiteServer_SeverIs_EdgeNameMatchesOutputProperty() + public void Site_DefaultRelationships_AreEmpty() { - Assert.Equal(nameof(SiteServer.SeverIs), EdgeNames.SeverIs); + var site = new Site(); + + Assert.Empty(site.ChildObjects); + Assert.Empty(site.Links); + Assert.Empty(site.InheritanceHashes); + } + + [Fact] + public void SiteServer_ServerIs_EdgeNameMatchesOutputProperty() + { + Assert.Equal(nameof(SiteServer.ServerIs), EdgeNames.ServerIs); } [Fact] From b81e13a9cd08aa034aec38eca23d7704da210808 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 12:54:26 +0200 Subject: [PATCH 08/11] add more containers and objectClass --- src/CommonLib/Enums/ObjectClass.cs | 4 ++- src/CommonLib/LdapProducerQueryGenerator.cs | 6 ++-- src/CommonLib/LdapQueries/LdapFilter.cs | 24 +++++++++++++++- src/CommonLib/LdapUtils.cs | 6 +++- .../Processors/ContainerProcessor.cs | 5 ++-- .../Processors/LdapPropertyProcessor.cs | 3 ++ test/unit/ContainerProcessorTest.cs | 20 ++++++++++++- test/unit/DirectoryObjectTests.cs | 15 ++++++++++ test/unit/LDAPFilterTest.cs | 13 +++++++++ test/unit/LdapProducerQueryGeneratorTest.cs | 23 +++++++++++++++ test/unit/LdapPropertyTests.cs | 28 +++++++++++++++++++ 11 files changed, 139 insertions(+), 8 deletions(-) diff --git a/src/CommonLib/Enums/ObjectClass.cs b/src/CommonLib/Enums/ObjectClass.cs index db98dd5e4..1113999df 100644 --- a/src/CommonLib/Enums/ObjectClass.cs +++ b/src/CommonLib/Enums/ObjectClass.cs @@ -6,6 +6,8 @@ public static class ObjectClass { public const string DomainClass = "domain"; public const string ContainerClass = "container"; public const string ConfigurationClass = "configuration"; + public const string BuiltinDomainClass = "builtinDomain"; + public const string SitesContainerClass = "sitesContainer"; public const string PKICertificateTemplateClass = "pKICertificateTemplate"; public const string PKIEnrollmentServiceClass = "pKIEnrollmentService"; public const string CertificationAuthorityClass = "certificationAuthority"; @@ -15,4 +17,4 @@ public static class ObjectClass { public const string SiteClass = "site"; public const string SiteServerClass = "server"; public const string SiteSubnetClass = "subnet"; -} \ No newline at end of file +} diff --git a/src/CommonLib/LdapProducerQueryGenerator.cs b/src/CommonLib/LdapProducerQueryGenerator.cs index 501f1eb56..a328ea0ea 100644 --- a/src/CommonLib/LdapProducerQueryGenerator.cs +++ b/src/CommonLib/LdapProducerQueryGenerator.cs @@ -15,7 +15,8 @@ public static GeneratedLdapParameters GenerateDefaultPartitionParameters(Collect if (methods.HasFlag(CollectionMethod.ObjectProps) || methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.Container)) { - filter = filter.AddComputers().AddDomains().AddUsers().AddContainers().AddGPOs().AddOUs().AddGroups(); + filter = filter.AddComputers().AddDomains().AddUsers().AddContainers().AddBuiltinDomains().AddGPOs() + .AddOUs().AddGroups(); if (methods.HasFlag(CollectionMethod.Container)) { properties.AddRange(CommonProperties.ContainerProps); @@ -110,7 +111,8 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.CertServices) || methods.HasFlag(CollectionMethod.Site)) { filter = filter.AddContainers() - .AddConfiguration(); + .AddConfiguration() + .AddSitesContainer(); if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.CertServices)) { filter.AddCertificateTemplates() diff --git a/src/CommonLib/LdapQueries/LdapFilter.cs b/src/CommonLib/LdapQueries/LdapFilter.cs index a16584dce..b700b2c0f 100644 --- a/src/CommonLib/LdapQueries/LdapFilter.cs +++ b/src/CommonLib/LdapQueries/LdapFilter.cs @@ -129,6 +129,17 @@ public LdapFilter AddContainers(params string[] conditions) { return this; } + /// + /// Add a filter that will include Builtin domain container objects. + /// + /// + /// + public LdapFilter AddBuiltinDomains(params string[] conditions) { + _filterParts.Add(BuildString("(objectClass=builtinDomain)", conditions)); + + return this; + } + /// /// Add a filter that will include Configuration objects /// @@ -140,6 +151,17 @@ public LdapFilter AddConfiguration(params string[] conditions) { return this; } + /// + /// Add a filter that will include Sites container objects. + /// + /// + /// + public LdapFilter AddSitesContainer(params string[] conditions) { + _filterParts.Add(BuildString("(objectClass=sitesContainer)", conditions)); + + return this; + } + /// /// Add a filter that will include Computer objects /// @@ -313,4 +335,4 @@ public IEnumerable GetFilterList() { } } } -} \ No newline at end of file +} diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index 04b8c0301..037e1ac5c 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -1197,6 +1197,10 @@ internal static bool ResolveLabel(string objectIdentifier, string distinguishedN type = Label.Container; else if (objectClasses.Contains(ObjectClass.ConfigurationClass, StringComparer.OrdinalIgnoreCase)) type = Label.Configuration; + else if (objectClasses.Contains(ObjectClass.BuiltinDomainClass, StringComparer.OrdinalIgnoreCase)) + type = Label.Container; + else if (objectClasses.Contains(ObjectClass.SitesContainerClass, StringComparer.OrdinalIgnoreCase)) + type = Label.Container; else if (objectClasses.Contains(ObjectClass.PKICertificateTemplateClass, StringComparer.OrdinalIgnoreCase)) type = Label.CertTemplate; else if (objectClasses.Contains(ObjectClass.PKIEnrollmentServiceClass, StringComparer.OrdinalIgnoreCase)) @@ -1469,4 +1473,4 @@ private static string ComputeDisplayName(IDirectoryObject directoryObject, strin return displayName.ToUpper(); } } -} \ No newline at end of file +} diff --git a/src/CommonLib/Processors/ContainerProcessor.cs b/src/CommonLib/Processors/ContainerProcessor.cs index 4539d7217..861c588ec 100644 --- a/src/CommonLib/Processors/ContainerProcessor.cs +++ b/src/CommonLib/Processors/ContainerProcessor.cs @@ -97,7 +97,8 @@ public IAsyncEnumerable GetContainerChildObjects(ResolvedSearchR /// public async IAsyncEnumerable GetContainerChildObjects(string distinguishedName, string containerName = "") { - var filter = new LdapFilter().AddComputers().AddUsers().AddGroups().AddOUs().AddContainers(); + var filter = new LdapFilter().AddComputers().AddUsers().AddGroups().AddOUs().AddContainers() + .AddBuiltinDomains().AddSitesContainer(); filter.AddCertificateAuthorities().AddCertificateTemplates().AddEnterpriseCertificationAuthorities(); await foreach (var childEntryResult in _utils.Query(new LdapQueryParameters { DomainName = Helpers.DistinguishedNameToDomain(distinguishedName), @@ -175,4 +176,4 @@ public static bool ReadBlocksInheritance(string gpOptions) return gpOptions is "1"; } } -} \ No newline at end of file +} diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index e77484522..380bb0935 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -45,6 +45,9 @@ public LdapPropertyProcessor(ILdapUtils utils, ILogger log = null) { private static Dictionary GetCommonProps(IDirectoryObject entry) { var ret = new Dictionary(); + entry.TryGetArrayProperty(LDAPProperties.ObjectClass, out var objectClasses); + ret["objectClass"] = objectClasses; + if (entry.TryGetProperty(LDAPProperties.Description, out var description)) { ret["description"] = description; } diff --git a/test/unit/ContainerProcessorTest.cs b/test/unit/ContainerProcessorTest.cs index a4111bca1..be1ee781e 100644 --- a/test/unit/ContainerProcessorTest.cs +++ b/test/unit/ContainerProcessorTest.cs @@ -120,6 +120,24 @@ public async Task ContainerProcessor_GetContainerChildObjects_ReturnsCorrectData } + [Fact] + public async Task ContainerProcessor_GetContainerChildObjects_QueryIncludesAdditionalContainerClasses() + { + var mock = new Mock(); + LdapQueryParameters queryParameters = null; + mock.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Callback((parameters, _) => queryParameters = parameters) + .Returns(Array.Empty>().ToAsyncEnumerable); + + var processor = new ContainerProcessor(mock.Object); + + await processor.GetContainerChildObjects("DC=testlab,DC=local").ToArrayAsync(); + + Assert.NotNull(queryParameters); + Assert.Contains("(objectClass=builtinDomain)", queryParameters.LDAPFilter); + Assert.Contains("(objectClass=sitesContainer)", queryParameters.LDAPFilter); + } + [Fact] public void ContainerProcessor_ReadBlocksInheritance_ReturnsCorrectValues() { @@ -164,4 +182,4 @@ public async Task ContainerProcessor_GetContainingObject_BadDN_ReturnsNull() Assert.False(success); } } -} \ No newline at end of file +} diff --git a/test/unit/DirectoryObjectTests.cs b/test/unit/DirectoryObjectTests.cs index 1b6409a97..2975420ed 100644 --- a/test/unit/DirectoryObjectTests.cs +++ b/test/unit/DirectoryObjectTests.cs @@ -255,6 +255,21 @@ public void Test_GetLabel_ConfigurationObjects() { Assert.Equal(Label.Configuration, label); } + [Theory] + [InlineData(ObjectClass.BuiltinDomainClass)] + [InlineData(ObjectClass.SitesContainerClass)] + public void Test_GetLabel_AdditionalContainerClasses(string objectClass) { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", objectClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.Container, label); + } + [Fact] public void Test_GetLabel_CertTemplateObjects() { var attribs = new Dictionary { diff --git a/test/unit/LDAPFilterTest.cs b/test/unit/LDAPFilterTest.cs index 5eacefc2e..e02b5f829 100644 --- a/test/unit/LDAPFilterTest.cs +++ b/test/unit/LDAPFilterTest.cs @@ -110,6 +110,7 @@ public void LDAPFilter_GetFilterList_MergeFilter() [InlineData("site", "(objectClass=site)", "(&(objectClass=site)(name=Test))")] [InlineData("server", "(objectClass=server)", "(&(objectClass=server)(name=Test))")] [InlineData("subnet", "(objectClass=subnet)", "(&(objectClass=subnet)(name=Test))")] + [InlineData("sitesContainer", "(objectClass=sitesContainer)", "(&(objectClass=sitesContainer)(name=Test))")] public void LDAPFilter_SiteFilters_FilterCorrect(string objectClass, string expectedFilter, string expectedFilterWithCondition) { @@ -118,6 +119,7 @@ public void LDAPFilter_SiteFilters_FilterCorrect(string objectClass, string expe "site" => new LdapFilter().AddSites(), "server" => new LdapFilter().AddSiteServers(), "subnet" => new LdapFilter().AddSiteSubnets(), + "sitesContainer" => new LdapFilter().AddSitesContainer(), _ => throw new ArgumentOutOfRangeException(nameof(objectClass)) }; @@ -126,6 +128,7 @@ public void LDAPFilter_SiteFilters_FilterCorrect(string objectClass, string expe "site" => new LdapFilter().AddSites("name=Test"), "server" => new LdapFilter().AddSiteServers("name=Test"), "subnet" => new LdapFilter().AddSiteSubnets("name=Test"), + "sitesContainer" => new LdapFilter().AddSitesContainer("name=Test"), _ => throw new ArgumentOutOfRangeException(nameof(objectClass)) }; @@ -133,6 +136,16 @@ public void LDAPFilter_SiteFilters_FilterCorrect(string objectClass, string expe Assert.Equal(expectedFilterWithCondition, testWithCondition.GetFilter()); } + [Fact] + public void LDAPFilter_BuiltinDomain_FilterCorrect() + { + var test = new LdapFilter().AddBuiltinDomains(); + var testWithCondition = new LdapFilter().AddBuiltinDomains("name=Builtin"); + + Assert.Equal("(objectClass=builtinDomain)", test.GetFilter()); + Assert.Equal("(&(objectClass=builtinDomain)(name=Builtin))", testWithCondition.GetFilter()); + } + #endregion } } diff --git a/test/unit/LdapProducerQueryGeneratorTest.cs b/test/unit/LdapProducerQueryGeneratorTest.cs index 7fcd9b24f..455f13385 100644 --- a/test/unit/LdapProducerQueryGeneratorTest.cs +++ b/test/unit/LdapProducerQueryGeneratorTest.cs @@ -8,12 +8,33 @@ namespace CommonLibTest; public class LdapProducerQueryGeneratorTest { + [Fact] + public void GenerateDefaultPartitionParameters_Container_IncludesBuiltinDomainFilter() + { + var expectedFilter = new LdapFilter() + .AddComputers() + .AddDomains() + .AddUsers() + .AddContainers() + .AddBuiltinDomains() + .AddGPOs() + .AddOUs() + .AddGroups() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateDefaultPartitionParameters(CollectionMethod.Container); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + Assert.Contains("(objectClass=builtinDomain)", result.Filter.GetFilter()); + } + [Fact] public void GenerateConfigurationPartitionParameters_Site_IncludesSiteFiltersAndProperties() { var expectedFilter = new LdapFilter() .AddContainers() .AddConfiguration() + .AddSitesContainer() .AddSites() .AddSiteServers() .AddSiteSubnets() @@ -36,6 +57,7 @@ public void GenerateConfigurationPartitionParameters_CertServices_IncludesCertFi var expectedFilter = new LdapFilter() .AddContainers() .AddConfiguration() + .AddSitesContainer() .AddCertificateTemplates() .AddCertificateAuthorities() .AddEnterpriseCertificationAuthorities() @@ -58,6 +80,7 @@ public void GenerateConfigurationPartitionParameters_SiteAndCertServices_Include var expectedFilter = new LdapFilter() .AddContainers() .AddConfiguration() + .AddSitesContainer() .AddCertificateTemplates() .AddCertificateAuthorities() .AddEnterpriseCertificationAuthorities() diff --git a/test/unit/LdapPropertyTests.cs b/test/unit/LdapPropertyTests.cs index e72b15d1a..274794f72 100644 --- a/test/unit/LdapPropertyTests.cs +++ b/test/unit/LdapPropertyTests.cs @@ -119,6 +119,34 @@ public void LDAPPropertyProcessor_ReadOUProperties_TestGoodData() Assert.Equal("Test", test["description"] as string); } + [Fact] + public void LDAPPropertyProcessor_ReadContainerProperties_IncludesObjectClass() + { + var objectClasses = new[] { "top", ObjectClass.ContainerClass }; + var mock = new MockDirectoryObject("CN=Users,DC=testlab,DC=local", + new Dictionary + { + {LDAPProperties.ObjectClass, objectClasses} + }, "", "ECAD920E-8EB1-4E31-A80E-DD36367F81F4"); + + var test = LdapPropertyProcessor.ReadContainerProperties(mock); + + Assert.True(test.TryGetValue("objectClass", out var actual)); + Assert.Equal(objectClasses, Assert.IsType(actual)); + } + + [Fact] + public void LDAPPropertyProcessor_ReadOUProperties_ObjectClassDefaultsToEmptyArray() + { + var mock = new MockDirectoryObject("OU=TestOU,DC=testlab,DC=local", + new Dictionary(), "", "2A374493-816A-4193-BEFD-D2F4132C6DCA"); + + var test = LdapPropertyProcessor.ReadOUProperties(mock); + + Assert.True(test.TryGetValue("objectClass", out var actual)); + Assert.Empty(Assert.IsType(actual)); + } + [Fact] public async Task LDAPPropertyProcessor_ReadGroupProperties_TestGoodData() { From 8634f87ee8d02cb203c427c1d1675c2562144452 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Fri, 12 Jun 2026 13:25:15 +0200 Subject: [PATCH 09/11] fix test and add Site to DCOnly --- src/CommonLib/Enums/CollectionMethod.cs | 2 +- test/unit/LDAPUtilsTest.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CommonLib/Enums/CollectionMethod.cs b/src/CommonLib/Enums/CollectionMethod.cs index 458a3a05b..68ebcdf89 100644 --- a/src/CommonLib/Enums/CollectionMethod.cs +++ b/src/CommonLib/Enums/CollectionMethod.cs @@ -32,7 +32,7 @@ public enum CollectionMethod { //EventLogs = 1 << 23, LocalGroups = DCOM | RDP | LocalAdmin | PSRemote, ComputerOnly = LocalGroups | Session | UserRights | CARegistry | DCRegistry | WebClientService | SmbInfo | NTLMRegistry, - DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices, + DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices | Site, Default = Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container | CertServices | LdapServices | SmbInfo | WebClientService | Site, diff --git a/test/unit/LDAPUtilsTest.cs b/test/unit/LDAPUtilsTest.cs index acf9f2ca5..906318a1a 100644 --- a/test/unit/LDAPUtilsTest.cs +++ b/test/unit/LDAPUtilsTest.cs @@ -255,10 +255,10 @@ public async Task Test_ResolveSearchResult_TrustAccount() { "Default-First-Site-Name", "DEFAULT-FIRST-SITE-NAME@TESTLAB.LOCAL")] [InlineData(ObjectClass.SiteServerClass, Label.SiteServer, "CN=PRIMARY,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", - "primary.testlab.local", "PRIMARY.TESTLAB.LOCAL")] + "primary.testlab.local", "PRIMARY.TESTLAB.LOCAL@TESTLAB.LOCAL")] [InlineData(ObjectClass.SiteSubnetClass, Label.SiteSubnet, "CN=10.0.0.0/24,CN=Subnets,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", - "10.0.0.0/24", "10.0.0.0/24")] + "10.0.0.0/24", "10.0.0.0/24@TESTLAB.LOCAL")] public async Task Test_ResolveSearchResult_SiteObjects(string objectClass, Label expectedLabel, string distinguishedName, string name, string expectedDisplayName) { var utils = new MockLdapUtils(); From c29e4dc3fda74ddbe7d6995b418342acf5bbfe3c Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 11:51:16 +0200 Subject: [PATCH 10/11] fix builtin contains --- src/CommonLib/Processors/ContainerProcessor.cs | 12 ------------ test/unit/ContainerProcessorTest.cs | 4 ++-- test/unit/Facades/MockLdapUtils.cs | 4 +++- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/CommonLib/Processors/ContainerProcessor.cs b/src/CommonLib/Processors/ContainerProcessor.cs index 861c588ec..298e0fd29 100644 --- a/src/CommonLib/Processors/ContainerProcessor.cs +++ b/src/CommonLib/Processors/ContainerProcessor.cs @@ -57,18 +57,6 @@ private static bool IsDistinguishedNameFiltered(string distinguishedName) { var containerDn = Helpers.RemoveDistinguishedNamePrefix(distinguishedName); - //If the container is the builtin container, we want to redirect the containing object to the domain of the object - if (containerDn.StartsWith("CN=BUILTIN", StringComparison.OrdinalIgnoreCase)) - { - //This is always safe - var domain = Helpers.DistinguishedNameToDomain(distinguishedName); - if (await _utils.GetDomainSidFromDomainName(domain) is (true, var domainSid)) { - return (true, new TypedPrincipal(domainSid, Label.Domain)); - } - - return (false, default); - } - return await _utils.ResolveDistinguishedName(containerDn); } diff --git a/test/unit/ContainerProcessorTest.cs b/test/unit/ContainerProcessorTest.cs index be1ee781e..3014d55f5 100644 --- a/test/unit/ContainerProcessorTest.cs +++ b/test/unit/ContainerProcessorTest.cs @@ -167,8 +167,8 @@ public async Task ContainerProcessor_GetContainingObject_ExpectedResult() Assert.True(success); (success, result) = await proc.GetContainingObject("CN=ADMINISTRATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL"); - Assert.Equal(Label.Domain, result.ObjectType); - Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.ObjectIdentifier); + Assert.Equal(Label.Container, result.ObjectType); + Assert.Equal("S-1-5-32", result.ObjectIdentifier); Assert.True(success); } diff --git a/test/unit/Facades/MockLdapUtils.cs b/test/unit/Facades/MockLdapUtils.cs index f60608d16..b2c7d190f 100644 --- a/test/unit/Facades/MockLdapUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -431,6 +431,7 @@ public virtual IAsyncEnumerable> RangedRetrieval(string distingui "S-1-5-21-3130019616-2776909439-2417379446-2105", Label.Computer), "S-1-5-21-3130019616-2776909439-2417379446-2120" => new TypedPrincipal( "S-1-5-21-3130019616-2776909439-2417379446-2120", Label.Computer), + "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("S-1-5-32", Label.Container), "CN=REPLICATOR,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-552", Label.Group), "CN=PRINT OPERATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-550", @@ -762,6 +763,7 @@ public async Task ConvertWellKnownPrincipal(string sid, string domain) public async Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName) { var result = distinguishedName.ToUpper() switch { + "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("S-1-5-32", Label.Container), "CN=REPLICATOR,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-552", Label.Group), "CN=PRINT OPERATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-550", @@ -1127,4 +1129,4 @@ public void Dispose() { return (true, "0"); } } -} \ No newline at end of file +} From 5deab297a63a0da6161d4e9ed8781023ce7edf40 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 14:42:57 +0200 Subject: [PATCH 11/11] fix builtin container id --- .../DirectoryObjects/DirectoryObjectExtensions.cs | 9 ++++++++- src/CommonLib/LdapQueries/CommonProperties.cs | 5 +++-- test/unit/ContainerProcessorTest.cs | 2 +- test/unit/DirectoryObjectTests.cs | 14 ++++++++++++++ test/unit/Facades/MockDirectoryObject.cs | 4 ++-- test/unit/Facades/MockLdapUtils.cs | 6 ++++-- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs b/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs index 57399d66a..a5af2c693 100644 --- a/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs +++ b/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs @@ -22,6 +22,13 @@ public static bool IsGMSA(this IDirectoryObject directoryObject) { } public static bool GetObjectIdentifier(this IDirectoryObject directoryObject, out string objectIdentifier) { + // Builtin container has a SID (always "S-1-5-32"). We use the ObjectGUID as ID like for other containers + if (directoryObject.TryGetArrayProperty(LDAPProperties.ObjectClass, out var objectClasses) && + objectClasses.Contains(ObjectClass.BuiltinDomainClass, StringComparer.OrdinalIgnoreCase) && + directoryObject.TryGetGuid(out objectIdentifier) && !string.IsNullOrWhiteSpace(objectIdentifier)) { + return true; + } + if (directoryObject.TryGetSecurityIdentifier(out objectIdentifier) && !string.IsNullOrWhiteSpace(objectIdentifier)) { return true; } @@ -68,4 +75,4 @@ public static bool HasLAPS(this IDirectoryObject directoryObject) { return false; } -} \ No newline at end of file +} diff --git a/src/CommonLib/LdapQueries/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs index 88cef7462..f9a8e05c7 100644 --- a/src/CommonLib/LdapQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -9,7 +9,8 @@ public static class CommonProperties LDAPProperties.Flags }; - public static readonly string[] ObjectID = { LDAPProperties.ObjectSID, LDAPProperties.ObjectGUID }; + public static readonly string[] ObjectID = + { LDAPProperties.ObjectSID, LDAPProperties.ObjectGUID, LDAPProperties.ObjectClass }; public static readonly string[] ObjectSID = { LDAPProperties.ObjectSID }; public static readonly string[] GPCFileSysPath = { LDAPProperties.GPCFileSYSPath }; @@ -117,4 +118,4 @@ public static class CommonProperties LDAPProperties.SiteObject }; } -} \ No newline at end of file +} diff --git a/test/unit/ContainerProcessorTest.cs b/test/unit/ContainerProcessorTest.cs index 3014d55f5..f500b4acf 100644 --- a/test/unit/ContainerProcessorTest.cs +++ b/test/unit/ContainerProcessorTest.cs @@ -168,7 +168,7 @@ public async Task ContainerProcessor_GetContainingObject_ExpectedResult() (success, result) = await proc.GetContainingObject("CN=ADMINISTRATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL"); Assert.Equal(Label.Container, result.ObjectType); - Assert.Equal("S-1-5-32", result.ObjectIdentifier); + Assert.Equal(MockLdapUtils.BuiltinContainerGuid, result.ObjectIdentifier); Assert.True(success); } diff --git a/test/unit/DirectoryObjectTests.cs b/test/unit/DirectoryObjectTests.cs index 2975420ed..0940361a3 100644 --- a/test/unit/DirectoryObjectTests.cs +++ b/test/unit/DirectoryObjectTests.cs @@ -140,6 +140,20 @@ public void Test_GetLabel_WellKnownAdministratorsObject() { Assert.Equal(Label.Group, label); } + [Fact] + public void Test_GetObjectIdentifier_BuiltinContainer_UsesGuid() { + var expectedGuid = Guid.NewGuid().ToString().ToUpper(); + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.BuiltinDomainClass } }, + }; + + var mock = new MockDirectoryObject("CN=BuiltIn,DC=Testlab,DC=Local", attribs, + "S-1-5-32", expectedGuid); + + Assert.True(mock.GetObjectIdentifier(out var objectIdentifier)); + Assert.Equal(expectedGuid, objectIdentifier); + } + [Fact] public void Test_GetLabel_Computer_Objects() { var attribs = new Dictionary { diff --git a/test/unit/Facades/MockDirectoryObject.cs b/test/unit/Facades/MockDirectoryObject.cs index 131b890af..8e78b6e0b 100644 --- a/test/unit/Facades/MockDirectoryObject.cs +++ b/test/unit/Facades/MockDirectoryObject.cs @@ -16,7 +16,7 @@ public class MockDirectoryObject : IDirectoryObject { public MockDirectoryObject(string distinguishedName, IDictionary properties, string sid = "", string guid = "") { DistinguishedName = distinguishedName; - Properties = properties; + Properties = properties ?? new Dictionary(); _objectSID = sid; _objectGuid = guid; } @@ -176,4 +176,4 @@ public int PropertyCount(string propertyName) { public IEnumerable PropertyNames() { foreach (var property in Properties.Keys) yield return property.ToString().ToLower(); } -} \ No newline at end of file +} diff --git a/test/unit/Facades/MockLdapUtils.cs b/test/unit/Facades/MockLdapUtils.cs index b2c7d190f..ca74bb598 100644 --- a/test/unit/Facades/MockLdapUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -20,6 +20,7 @@ namespace CommonLibTest.Facades [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockLdapUtils : ILdapUtils { + public const string BuiltinContainerGuid = "C8D73525-F9B2-4F68-B1D3-9457896B3C80"; private readonly ConcurrentDictionary _domainControllers = new(); private readonly Forest _forest; private readonly ConcurrentDictionary _seenWellKnownPrincipals = new(); @@ -58,6 +59,7 @@ public virtual IAsyncEnumerable> RangedRetrieval(string distingui "S-1-5-21-3130019616-2776909439-2417379446-512", Label.Group), "S-1-5-21-3130019616-2776909439-2417379446-2606" => new TypedPrincipal( "S-1-5-21-3130019616-2776909439-2417379446-2606", Label.User), + BuiltinContainerGuid => new TypedPrincipal(BuiltinContainerGuid, Label.Container), "E32A6AC7-083B-4DD7-ACFF-6D9C2B1AFAF5" => new TypedPrincipal("E32A6AC7-083B-4DD7-ACFF-6D9C2B1AFAF5", Label.Container), "S-1-5-21-3130019616-2776909439-2417379446-519" => new TypedPrincipal( @@ -431,7 +433,7 @@ public virtual IAsyncEnumerable> RangedRetrieval(string distingui "S-1-5-21-3130019616-2776909439-2417379446-2105", Label.Computer), "S-1-5-21-3130019616-2776909439-2417379446-2120" => new TypedPrincipal( "S-1-5-21-3130019616-2776909439-2417379446-2120", Label.Computer), - "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("S-1-5-32", Label.Container), + "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal(BuiltinContainerGuid, Label.Container), "CN=REPLICATOR,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-552", Label.Group), "CN=PRINT OPERATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-550", @@ -763,7 +765,7 @@ public async Task ConvertWellKnownPrincipal(string sid, string domain) public async Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName) { var result = distinguishedName.ToUpper() switch { - "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("S-1-5-32", Label.Container), + "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal(BuiltinContainerGuid, Label.Container), "CN=REPLICATOR,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-552", Label.Group), "CN=PRINT OPERATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-550",