Skip to content

Conversation

@q-roland
Copy link
Contributor

@q-roland q-roland commented Nov 6, 2025

  • 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

Description

This pull requests aims to integrate Active Directory Sites into the data collected from the configuration partition.
This pull request goes with the following Bloodhound PR: SpecterOps/BloodHound#2031, as well as the following SharpHound PR: SpecterOps/SharpHound#186

A more complete description on why we thought this could be a good idea is described in details in the BloodHound PR, as well as the following article that we just released: https://www.synacktiv.com/en/publications/site-unseen-enumerating-and-attacking-active-directory-sites

Motivation and Context

Motivation and context are explained in the linked article.

How Has This Been Tested?

This has for the moment been tested in local lab instances. The lab instance was composed of various Active Directory site configurations:

  • Single site
  • 2 sites with associated subnets and 1 server by site
  • 3 sites with several servers by site.

Screenshots (if appropriate):

See the BloodHound PR and linked article for screenshots.

Types of changes

  • Chore (a change that does not modify the application functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • Documentation updates are needed, and have been made accordingly.
  • I have added and/or updated tests to cover my changes.
  • All new and existing tests passed.
  • My changes include a database migration.

As was mentioned in the BloodHound pull request, this is of course only an implementation suggestion, and I remain open to discussion about the choices that were made, requests for implementation changes and so on!

Cheers,

Summary by CodeRabbit

  • New Features
    • Added support for collecting and analyzing Active Directory Sites, Site Servers, and Site Subnets.
    • Extended data collection capabilities to include site-related LDAP properties and group policy links.
    • Enhanced ACL processing to handle permissions on the new site object types.

…ition

* 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
@coderabbitai
Copy link

coderabbitai bot commented Nov 6, 2025

Walkthrough

This PR extends the BloodHound data collection platform with comprehensive support for Active Directory Sites, SiteServers, and SiteSubnets. The changes introduce new enums, LDAP query filters, output types, and processors to collect and handle site-related objects and their relationships, including GPLink associations.

Changes

Cohort / File(s) Change Summary
Enum & Constant Extensions
src/CommonLib/Enums/CollectionMethod.cs, src/CommonLib/Enums/DataType.cs, src/CommonLib/Enums/LDAPProperties.cs, src/CommonLib/Enums/Labels.cs, src/CommonLib/Enums/ObjectClass.cs
Added Site collection flag and corresponding data type, LDAP property, label, and object class constants for Site, SiteServer, and SiteSubnet objects.
LDAP Query Infrastructure
src/CommonLib/LdapProducerQueryGenerator.cs, src/CommonLib/LdapQueries/LdapFilter.cs, src/CommonLib/LdapQueries/CommonProperties.cs
Extended query generation to recognize Site collection flag and invoke site-specific filters. Added AddSites, AddSiteServers, and AddSiteSubnets filter methods. Introduced SiteProps, SiteServerProps, and SiteSubnetProps property arrays.
Output Types
src/CommonLib/OutputTypes/Site.cs, src/CommonLib/OutputTypes/SiteServer.cs, src/CommonLib/OutputTypes/SiteSubnet.cs
Added new output classes inheriting from OutputBase for Site (with GPLink[] Links property), SiteServer, and SiteSubnet.
Utility & Label Resolution
src/CommonLib/LdapUtils.cs
Extended ResolveLabel to classify site-related object classes into their respective labels and updated ComputeDisplayName to format display names for Site, SiteServer, and SiteSubnet objects.
Property & ACL Processing
src/CommonLib/Processors/LdapPropertyProcessor.cs, src/CommonLib/Processors/ACLProcessor.cs
Added ReadSiteProperties, ReadSiteServerProperties, and ReadSiteSubnetProperties methods. Extended ACLProcessor to include Site, SiteServer, and SiteSubnet in GenericWrite and GPLink ACE handling alongside existing object types.
Site Processor
src/CommonLib/Processors/SiteProcessor.cs
New processor class providing site-resolution methods, DN derivation logic, and GPLink extraction/normalization for site-related LDAP data.

Sequence Diagram

sequenceDiagram
    participant Collector
    participant LdapProducerQueryGenerator
    participant LdapFilter
    participant LdapUtils
    participant SiteProcessor
    participant OutputTypes

    Collector->>LdapProducerQueryGenerator: GenerateConfigurationPartitionParameters(with Site flag)
    activate LdapProducerQueryGenerator
    LdapProducerQueryGenerator->>LdapFilter: AddSites()
    LdapProducerQueryGenerator->>LdapFilter: AddSiteServers()
    LdapProducerQueryGenerator->>LdapFilter: AddSiteSubnets()
    LdapProducerQueryGenerator->>Collector: Return queries with site filters
    deactivate LdapProducerQueryGenerator

    Collector->>LdapUtils: Query LDAP for site objects
    activate LdapUtils
    LdapUtils->>LdapUtils: ResolveLabel (classify objectClass as Site/SiteServer/SiteSubnet)
    LdapUtils->>LdapUtils: ComputeDisplayName (format display name per type)
    LdapUtils->>Collector: Return resolved search results
    deactivate LdapUtils

    Collector->>SiteProcessor: Process site results
    activate SiteProcessor
    SiteProcessor->>SiteProcessor: GetContainingSiteForServer/Subnet (resolve DN relationships)
    SiteProcessor->>SiteProcessor: ReadSiteGPLinks (extract and parse GPLink entries)
    SiteProcessor->>Collector: Return site relationships & GPLinks
    deactivate SiteProcessor

    Collector->>OutputTypes: Populate Site/SiteServer/SiteSubnet objects
    activate OutputTypes
    OutputTypes->>Collector: Return typed output
    deactivate OutputTypes
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Duplicate method declarations in LdapPropertyProcessor: The ReadSiteProperties, ReadSiteServerProperties, and ReadSiteSubnetProperties methods appear twice in the diff, which will cause a compilation error. Verify this is unintentional and consolidate.
  • SiteProcessor DN handling logic: Review the DN component stripping logic in GetContainingSiteForServer and GetContainingSiteForSubnet for correctness; ensure assumptions about DN structure are validated.
  • SiteProcessor async enumerable GPLink parsing: Verify the GPLink parsing logic correctly handles the Status == "2" enforcement flag check and error cases.
  • ACLProcessor Site eligibility expansion: Confirm that including Site in GenericWrite and GPLink ACE handling aligns with intended access control semantics.

Possibly related issues

  • Feature: Active Directory Sites data and ACLs BloodHound#2030: This PR directly implements Active Directory Sites data collection and site-related ACL handling through new enum members, LDAP query builders (AddSites/AddSiteServers/AddSiteSubnets), property readers, SiteProcessor logic, and ACLProcessor Site eligibility rules.

Suggested labels

enhancement

Suggested reviewers

  • ktstrader
  • MikeX777

Poem

🐰 A hop through sites, both grand and small,
New types emerge to capture all—
Servers, subnets, links galore,
BloodHound's reach expands once more!
thump thump

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding collection of Active Directory Sites ACLs and data, which is the primary objective across all modified files.
Description check ✅ Passed The description covers key template sections: motivation/context (linked articles), testing environment (lab instances with various configs), and change type marked as 'New feature'. However, the documentation and testing checklist items remain unchecked despite being applicable.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/CommonLib/LdapProducerQueryGenerator.cs (1)

107-110: LGTM!

The Site collection method is properly integrated into the configuration partition query generation. The condition correctly checks for the Site flag, and the filter chain appropriately includes site-related object types.

Optional readability suggestion: Line 110 contains a long method chain (8 methods). Consider breaking it across multiple lines for improved readability:

-            filter = filter.AddContainers().AddConfiguration().AddCertificateTemplates().AddCertificateAuthorities()
-                .AddEnterpriseCertificationAuthorities().AddIssuancePolicies().AddSites().AddSiteServers().AddSiteSubnets();
+            filter = filter.AddContainers()
+                .AddConfiguration()
+                .AddCertificateTemplates()
+                .AddCertificateAuthorities()
+                .AddEnterpriseCertificationAuthorities()
+                .AddIssuancePolicies()
+                .AddSites()
+                .AddSiteServers()
+                .AddSiteSubnets();
src/CommonLib/OutputTypes/Site.cs (1)

7-9: Consider removing or documenting commented code.

The commented-out Subnets and Servers properties could be handled in one of these ways:

  1. If these are planned for near-term implementation: Add a TODO comment instead of leaving commented code:
// TODO: Add Subnets and Servers properties to represent site children
public GPLink[] Links { get; set; } = Array.Empty<GPLink>();
  1. If these are optional future features: Remove the commented code to keep the codebase clean. Document the potential extension points in XML comments or separate documentation if needed.

  2. If implementation is imminent: Uncomment and implement them as part of this PR to align with the containedBy relationships mentioned in the PR description.

Commented code can make maintenance more difficult and may cause confusion about whether it represents unfinished work or optional features.

src/CommonLib/LdapUtils.cs (1)

1423-1434: Prefer DNS host name for SiteServer display

Using the name attribute alone hides the fully qualified host information and can collide in multi-domain forests. server objects expose dNSHostName, so we can surface the FQDN when it’s present.(learn.microsoft.com)

-                        if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name))
-                        {
-                            displayName = $"{name}";
-                        }
-                        else
-                        {
-                            displayName = $"UNKNOWN";
-                        }
+                        if (directoryObject.TryGetProperty(LDAPProperties.DNSHostName, out var dnsHostName) &&
+                            !string.IsNullOrWhiteSpace(dnsHostName))
+                        {
+                            displayName = dnsHostName;
+                        }
+                        else if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name))
+                        {
+                            displayName = name;
+                        }
+                        else
+                        {
+                            displayName = "UNKNOWN";
+                        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2069840 and 621b04e.

📒 Files selected for processing (15)
  • src/CommonLib/Enums/CollectionMethod.cs (1 hunks)
  • src/CommonLib/Enums/DataType.cs (1 hunks)
  • src/CommonLib/Enums/LDAPProperties.cs (1 hunks)
  • src/CommonLib/Enums/Labels.cs (1 hunks)
  • src/CommonLib/Enums/ObjectClass.cs (1 hunks)
  • src/CommonLib/LdapProducerQueryGenerator.cs (2 hunks)
  • src/CommonLib/LdapQueries/CommonProperties.cs (1 hunks)
  • src/CommonLib/LdapQueries/LdapFilter.cs (1 hunks)
  • src/CommonLib/LdapUtils.cs (4 hunks)
  • src/CommonLib/OutputTypes/Site.cs (1 hunks)
  • src/CommonLib/OutputTypes/SiteServer.cs (1 hunks)
  • src/CommonLib/OutputTypes/SiteSubnet.cs (1 hunks)
  • src/CommonLib/Processors/ACLProcessor.cs (3 hunks)
  • src/CommonLib/Processors/LdapPropertyProcessor.cs (1 hunks)
  • src/CommonLib/Processors/SiteProcessor.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-17T13:43:46.833Z
Learnt from: MikeX777
Repo: SpecterOps/SharpHoundCommon PR: 241
File: src/CommonLib/Processors/LdapPropertyProcessor.cs:168-169
Timestamp: 2025-10-17T13:43:46.833Z
Learning: Properties added to dictionaries returned by methods in SharpHoundCommon (such as those in LdapPropertyProcessor) may be consumed by dependent projects like SharpHound (SH) and SharpHoundEnterprise (SHE), even if they are not used within the SharpHoundCommon repository itself.

Applied to files:

  • src/CommonLib/LdapQueries/CommonProperties.cs
  • src/CommonLib/Processors/LdapPropertyProcessor.cs
  • src/CommonLib/Enums/LDAPProperties.cs
🔇 Additional comments (9)
src/CommonLib/Enums/LDAPProperties.cs (1)

99-100: LGTM!

The new LDAP property constants follow the established naming conventions and pattern. They integrate cleanly with the site-related features introduced in this PR.

src/CommonLib/OutputTypes/SiteSubnet.cs (1)

1-7: Verify containedBy relationship implementation for subnets.

The PR description states that subnets should have a "containedBy attribute linking each subnet to its site," but the SiteSubnet class doesn't contain any properties. Please confirm whether:

  • The containedBy relationship is implemented through the base OutputBase class
  • The relationship is stored separately (e.g., in a different data structure)
  • This property is still pending implementation

If the containedBy relationship is not yet implemented, please ensure it's added to properly represent the subnet-to-site linkage as described in the PR objectives.

src/CommonLib/Enums/DataType.cs (1)

18-20: LGTM!

The new data type constants are consistent with existing patterns and properly support the site-related collection features.

src/CommonLib/OutputTypes/SiteServer.cs (1)

1-7: Verify containedBy relationship implementation for servers.

Similar to SiteSubnet, the PR description mentions that servers should have a "containedBy attribute linking each server to its site," but the SiteServer class is empty. Please confirm whether:

  • The containedBy relationship is implemented through the base OutputBase class
  • The relationship is stored separately
  • This property is still pending implementation

If not yet implemented, please add the containedBy property to represent the server-to-site linkage as outlined in the PR objectives.

src/CommonLib/Enums/CollectionMethod.cs (2)

30-30: LGTM!

The Site flag is correctly positioned at bit 24 (next available bit) and follows the established bitwise flag pattern.


37-38: LGTM!

Site is appropriately included in the Default collection method composite, making site collection enabled by default alongside other standard collection methods.

src/CommonLib/LdapProducerQueryGenerator.cs (1)

135-140: LGTM!

Site-related properties are correctly added when the Site collection flag is set, following the same pattern as other collection types in this method.

src/CommonLib/Enums/Labels.cs (1)

22-24: LGTM!

The new site-related label enum members are properly added and follow the existing conventions. They integrate well with the site collection features throughout the PR.

src/CommonLib/OutputTypes/Site.cs (1)

10-10: LGTM!

The Links property is properly implemented with a safe default value using Array.Empty<GPLink>(), which is more efficient than allocating a new empty array.

Comment on lines +44 to +47
if (subnetProperties.TryGetValue("siteObject", out var siteObject))
{
return await GetContainingSiteForSubnet(siteObject.ToString());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against null siteObject before ToString().

If LDAP omitted siteObject, ReadSiteSubnetProperties still adds the key with a null value. TryGetValue will succeed, and siteObject.ToString() will throw, killing the site subnet collection path. Please bail out when the value is missing before converting to string, e.g.:

-        if (subnetProperties.TryGetValue("siteObject", out var siteObject))
-        {
-            return await GetContainingSiteForSubnet(siteObject.ToString());
-        }
+        if (subnetProperties.TryGetValue("siteObject", out var siteObject)
+            && siteObject is string siteDn
+            && !string.IsNullOrWhiteSpace(siteDn))
+        {
+            return await GetContainingSiteForSubnet(siteDn);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (subnetProperties.TryGetValue("siteObject", out var siteObject))
{
return await GetContainingSiteForSubnet(siteObject.ToString());
}
if (subnetProperties.TryGetValue("siteObject", out var siteObject)
&& siteObject is string siteDn
&& !string.IsNullOrWhiteSpace(siteDn))
{
return await GetContainingSiteForSubnet(siteDn);
}
🤖 Prompt for AI Agents
In src/CommonLib/Processors/SiteProcessor.cs around lines 44 to 47, the code
calls siteObject.ToString() without guarding for a null value; if the LDAP entry
added the key with a null value TryGetValue will succeed and ToString() will
throw. Fix by checking the retrieved value for null (and optionally for
empty/whitespace) before converting: if it's null/empty, bail out the method
path (e.g., return null or skip processing) instead of calling ToString(),
otherwise call GetContainingSiteForSubnet with the safe string value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant