Private/Set/Set-LowPrivilegeEnrollee.ps1

function Set-LowPrivilegeEnrollee {
    <#
        .SYNOPSIS
        Adds a LowPrivilegeEnrollee property to AD CS certificate template objects.
 
        .DESCRIPTION
        Examines the access control lists (ACLs) of Active Directory Certificate Services
        certificate template objects to identify enrollment permissions granted to principals
        that are neither high-privilege administrators nor overly broad dangerous groups.
         
        This function identifies "middle ground" enrollees - specific users or groups that have
        enrollment permissions but aren't part of the standard administrative hierarchy or the
        dangerous principals that represent broad attack surfaces.
         
        The function excludes two categories of principals:
        1. Safe/Administrative principals: Domain Admins, Enterprise Admins, SYSTEM, etc.
        2. Dangerous principals: Everyone, Authenticated Users, Domain Users, etc.
         
        What remains are custom enrollees that may represent specific service accounts, security
        groups, or users that have been granted enrollment permissions outside the standard model.
        These should be reviewed to ensure they align with security policies and least privilege
        principles.
         
        The function adds two properties to each template object:
        1. LowPrivilegeEnrollee: Array of SIDs for custom enrollees
        2. LowPrivilegeEnrolleeNames: Array of human-readable names formatted as "DOMAIN\User (SID)"
           or "SID (could not resolve)" if the principal cannot be resolved.
 
        .PARAMETER AdcsObject
        One or more DirectoryEntry objects representing AD CS certificate templates.
        These objects must contain ObjectSecurity.Access information.
 
        .PARAMETER Credential
        PSCredential for authenticating to Active Directory. Required for LDAP queries
        when converting identity references to directory entries and SIDs.
 
        .PARAMETER RootDSE
        A DirectoryEntry object for the RootDSE. Used to determine the domain context
        for LDAP queries when resolving principals.
 
        .INPUTS
        System.DirectoryServices.DirectoryEntry[]
        You can pipe certificate template DirectoryEntry objects to this function.
 
        .OUTPUTS
        System.DirectoryServices.DirectoryEntry[]
        Returns the input objects with added properties:
        - LowPrivilegeEnrollee: Array of SIDs
        - LowPrivilegeEnrolleeNames: Array of human-readable names
 
        .EXAMPLE
        $templates = Get-AdcsObject |
            Where-Object { $_.objectClass -contains 'pKICertificateTemplate' }
        $templates | Set-LowPrivilegeEnrollee
        Processes all certificate templates and adds the LowPrivilegeEnrollee property to each.
 
        .EXAMPLE
        Get-AdcsObject |
            Set-LowPrivilegeEnrollee |
            Where-Object { $_.LowPrivilegeEnrollee.Count -gt 0 }
        Retrieves all AD CS objects, adds LowPrivilegeEnrollee property, and filters to
        only those with custom enrollees.
 
        .EXAMPLE
        $template = Get-AdcsObject | Where-Object Name -eq 'WebServer'
        $template | Set-LowPrivilegeEnrollee
        if ($template.LowPrivilegeEnrollee) {
            Write-Host "Template has custom enrollees:"
            $template.LowPrivilegeEnrolleeNames | ForEach-Object { Write-Host " $_" }
        }
        Checks a specific template for custom/low-privilege enrollees and displays human-readable names.
 
        .NOTES
        Safe/Administrative principals excluded by default:
        - Domain Admins (-512), Enterprise Admins (-519), Builtin Administrators (-544)
        - SYSTEM (-18), Builtin Administrator (-500)
        - Cert Publishers (-517)
        - Domain Controllers (-516), Read-Only Domain Controllers (-521)
        - Enterprise Domain Controllers (-498), Enterprise Read-Only Domain Controllers (-9)
        - Key Admins (-526), Enterprise Key Admins (-527)
        - SELF (S-1-5-10)
         
        Dangerous principals excluded by default:
        - NULL SID, Everyone, Anonymous Logon, BUILTIN\Users
        - Authenticated Users, Domain Users, Domain Computers
 
        .LINK
        https://posts.specterops.io/certified-pre-owned-d95910965cd2
    #>

    [CmdletBinding()]
    [OutputType([System.DirectoryServices.DirectoryEntry[]])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.DirectoryServices.DirectoryEntry[]]$AdcsObject
    )

    #requires -Version 5.1

    begin {
        Write-Verbose "Identifying templates that allow low-privilege principals to enroll..."
    }

    process {
        $AdcsObject | Where-Object SchemaClassName -eq pKICertificateTemplate | ForEach-Object {
            try {
                $objectName = if ($_.Properties.displayName.Count -gt 0) {
                    $_.Properties.displayName[0] 
                } elseif ($_.Properties.name.Count -gt 0) {
                    $_.Properties.name[0]
                } else {
                    $_.Properties.distinguishedName[0]
                }
                Write-Verbose "Processing template: $objectName"
                
                [array]$lowPrivilegeIdentityReference = foreach ($ace in $_.ObjectSecurity.Access) {
                    $aceSid = $ace.IdentityReference | Convert-IdentityReferenceToSid
                    $isLowPrivilegeEnrollee = $aceSid | Test-IsLowPrivilegePrincipal
                    if ($isLowPrivilegeEnrollee) {
                        $isEnrollmentAce = Test-IsEnrollmentAce -Ace $ace
                        if ($isEnrollmentAce) {
                            Write-Verbose "Low-privilege enrollee found: $($ace.IdentityReference)"
                            # Ensure the principal is in the store (triggers cache population)
                            $null = $ace.IdentityReference | Resolve-Principal
                            # Convert to SID and return as the key to PrincipalStore
                            $aceSid.Value
                        }
                    }
                }

                $lowPrivilegeIdentityReference = $lowPrivilegeIdentityReference | Sort-Object -Unique
                
                if ($lowPrivilegeIdentityReference) {
                    Write-Verbose "Template has $($lowPrivilegeIdentityReference.Count) low privilege enrollee(s): $($lowPrivilegeIdentityReference -join ', ')"
                    
                    # Expand any groups to include their direct members
                    Write-Verbose "Expanding group memberships for low privilege enrollees..."
                    $lowPrivilegeIdentityReference = Expand-GroupMembership -SidList $lowPrivilegeIdentityReference
                    Write-Verbose "After expansion: $($lowPrivilegeIdentityReference.Count) unique principal(s)"
                } else {
                    Write-Verbose "No low privilege enrollees found in template"
                }

                # Build human-readable names array from PrincipalStore
                [array]$lowPrivilegeEnrolleeNames = $lowPrivilegeIdentityReference | ForEach-Object {
                    if ($script:PrincipalStore -and $script:PrincipalStore.ContainsKey($_)) {
                        $name = $script:PrincipalStore[$_].ntAccountName
                        if ($name) {
                            "$name ($_)"
                        } else {
                            "$_ (could not resolve)"
                        }
                    } else {
                        "$_ (could not resolve)"
                    }
                } | Sort-Object -Unique

                # Update the AD CS Object Store with the LowPrivilegeEnrollee property
                $dn = $_.Properties.distinguishedName[0]
                if ($script:AdcsObjectStore.ContainsKey($dn)) {
                    $script:AdcsObjectStore[$dn] | Add-Member -NotePropertyName LowPrivilegeEnrollee -NotePropertyValue $lowPrivilegeIdentityReference -Force
                    $script:AdcsObjectStore[$dn] | Add-Member -NotePropertyName LowPrivilegeEnrolleeNames -NotePropertyValue $lowPrivilegeEnrolleeNames -Force
                    Write-Verbose "Updated AD CS Object Store for $dn with LowPrivilegeEnrollee"
                }

                # Also add to the pipeline object for backward compatibility
                $_ | Add-Member -NotePropertyName LowPrivilegeEnrollee -NotePropertyValue $lowPrivilegeIdentityReference -Force
                $_ | Add-Member -NotePropertyName LowPrivilegeEnrolleeNames -NotePropertyValue $lowPrivilegeEnrolleeNames -Force
                
                # Return the modified object
                $_
                
            } catch {
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $_.Exception,
                    'LowPrivilegeEnrolleeProcessingFailed',
                    [System.Management.Automation.ErrorCategory]::InvalidOperation,
                    $_
                )
                $PSCmdlet.WriteError($errorRecord)
                
                # Still return the object even if processing failed
                $_
            }
        }
    }

    end {
        Write-Verbose "Done identifying templates that allow low-privilege principals to enroll."
    }
}