Private/Set-TemplateProperty.ps1

function Set-TemplateProperty {
    <#
        .SYNOPSIS
        Sets properties on a certificate template object in Active Directory.
 
        .DESCRIPTION
        This function modifies properties of an existing certificate template by connecting to the
        Certificate Templates container in the Active Directory Configuration partition and applying
        the specified property values. It supports both simple values (strings, integers, byte arrays)
        and complex collection values (arrays, lists).
 
        The function automatically sets the displayName and description properties for tracking
        purposes and handles different property value types appropriately. If no properties are
        specified, it defaults to ESC1 vulnerability configuration that allows enrollee-supplied
        subject names. It uses System.DirectoryServices to interact with Active Directory without
        requiring additional PowerShell modules.
 
        .PARAMETER TemplateName
        The name of the certificate template to modify. This must be an existing template in the
        Certificate Templates container.
 
        .PARAMETER Properties
        Optional. A hashtable containing the properties to set on the template. Keys should be valid LDAP
        attribute names and values can be simple types or collections. If not provided, defaults to ESC1
        vulnerability configuration properties.
 
        .PARAMETER Server
        Optional. The domain controller to use for the operation. If not specified, the function
        will use the default domain controller for the current domain.
 
        .PARAMETER Description
        Optional. Custom description to set on the template. If not provided, defaults to
        'Generated by ESCalator'.
 
        .INPUTS
        System.String
        Template name can be provided via pipeline input.
 
        .OUTPUTS
        PSCustomObject
        Returns a result object indicating success/failure and details of the operation.
 
        .EXAMPLE
        Set-TemplateProperty -TemplateName "VulnerableTemplate"
        Applies default ESC1 vulnerability properties to create a template with enrollee-supplied subject names.
 
        .EXAMPLE
        $props = @{
            'msPKI-Certificate-Name-Flag' = 1
            'msPKI-Enrollment-Flag' = 32
        }
        Set-TemplateProperty -TemplateName "CustomTemplate" -Properties $props
 
        .EXAMPLE
        $props = @{
            'pKIExtendedKeyUsage' = @('1.3.6.1.5.5.7.3.2', '1.3.6.1.5.5.7.3.4')
            'msPKI-Template-Schema-Version' = 4
        }
        "TestTemplate" | Set-TemplateProperty -Properties $props -Description "Modified by ESCalator"
 
        .EXAMPLE
        Set-TemplateProperty -TemplateName "UserTemplate" -Properties @{'displayName' = 'Custom User Template'} -Server "dc01.contoso.com"
 
        .LINK
        https://docs.microsoft.com/en-us/windows/win32/adschema/c-pkicertificatetemplate
 
        .NOTES
        Requires permissions to modify certificate template objects in Active Directory.
        The function will automatically set displayName and description properties for tracking.
         
        WARNING: The default properties create ESC1 vulnerabilities with enrollee-supplied subject names.
        Modifying template properties can affect certificate enrollment behavior. Ensure you understand
        the implications of the changes before proceeding. Only use in test environments.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$TemplateName,
        
        [Parameter()]
        [ValidateNotNull()]
        [hashtable]$Properties = @{
            flags                                  = 131642
            'msPKI-Certificate-Application-Policy' = @( '1.3.6.1.4.1.311.10.3.4', '1.3.6.1.5.5.7.3.4', '1.3.6.1.5.5.7.3.2' )
            'msPKI-Certificate-Name-Flag'          = 1
            'msPKI-Cert-Template-OID'              = '1.3.6.1.4.1.311.21.8.4872519.969661.15616387.15184524.2635045.52.984423.9484079'
            'msPKI-Enrollment-Flag'                = 9
            'msPKI-Minimal-Key-Size'               = 2048
            'msPKI-Private-Key-Flag'               = 16842768
            'msPKI-RA-Signature'                   = 0
            'msPKI-Template-Minor-Revision'        = 2
            'msPKI-Template-Schema-Version'        = 2
            pKICriticalExtensions                  = @( '2.5.29.15', '2.5.29.7' )
            pKIDefaultCSPs                         = @( '1,Microsoft Enhanced Cryptographic Provider v1.0', '2,Microsoft Base Cryptographic Provider v1.0' )
            pKIDefaultKeySpec                      = 1
            pKIExpirationPeriod                    = [byte[]](0x00, 0x40, 0x39, 0x87, 0x2E, 0xE1, 0xFE, 0xFF)
            pKIExtendedKeyUsage                    = @( '1.3.6.1.4.1.311.10.3.4', '1.3.6.1.5.5.7.3.4', '1.3.6.1.5.5.7.3.2' )
            pKIKeyUsage                            = [byte[]](160, 0)
            pKIMaxIssuingDepth                     = 0
            pKIOverlapPeriod                       = [byte[]](0x00, 0x80, 0xA6, 0x0A, 0xFF, 0xDE, 0xFF, 0xFF)
            revision                               = 100
        },
        
        [Parameter()]
        [string]$Server,
        
        [Parameter()]
        [string]$Description = 'Generated by ESCalator'
    )


    begin {
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Starting $($MyInvocation.MyCommand) on $env:COMPUTERNAME..."
        
        # Load System.DirectoryServices assembly
        try {
            Add-Type -AssemblyName System.DirectoryServices
            Write-Verbose "System.DirectoryServices assembly loaded successfully"
        } catch {
            Write-Error "Failed to load System.DirectoryServices assembly: $($_.Exception.Message)"
            return
        }

        # Get the Configuration partition automatically via RootDSE
        try {
            if ($Server) {
                $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/RootDSE")
                Write-Verbose "Connected to domain controller: $Server"
            } else {
                $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
                Write-Verbose "Connected to default domain controller"
            }
            
            $configurationPartition = $rootDSE.configurationNamingContext
            Write-Verbose "Configuration Naming Context: $configurationPartition"
        } catch {
            Write-Error "Failed to connect to Active Directory or retrieve Configuration partition: $($_.Exception.Message)"
            return
        }
    }

    process {
        Write-Verbose "Modifying certificate template: $TemplateName"
        
        # Build the template Distinguished Name
        $templateDN = "CN=$TemplateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$configurationPartition"
        Write-Verbose "Template DN: $templateDN"
        
        # Initialize variables
        $template = $null
        $success = $false
        $appliedProperties = @()
        $errors = @()
        
        try {
            # Connect to the specific template
            if ($Server) {
                $template = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/$templateDN")
            } else {
                $template = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$templateDN")
            }
            
            # Verify the template exists by accessing a property
            try {
                $null = $template.Properties['objectClass'].Value
                Write-Verbose "Successfully connected to template '$TemplateName'"
            } catch [System.DirectoryServices.DirectoryServicesCOMException] {
                throw "Certificate template '$TemplateName' was not found or is not accessible"
            }
            
            if ($PSCmdlet.ShouldProcess($TemplateName, "Set template properties")) {
                # Set displayName and description for tracking purposes
                try {
                    $template.Properties['displayName'].Value = $TemplateName
                    $appliedProperties += "displayName = '$TemplateName'"
                    Write-Verbose "Set displayName to: $TemplateName"
                } catch {
                    $errors += "Failed to set displayName: $($_.Exception.Message)"
                    Write-Warning "Failed to set displayName on template '$TemplateName': $($_.Exception.Message)"
                }
                
                try {
                    $template.Properties['description'].Value = $Description
                    $appliedProperties += "description = '$Description'"
                    Write-Verbose "Set description to: $Description"
                } catch {
                    $errors += "Failed to set description: $($_.Exception.Message)"
                    Write-Warning "Failed to set description on template '$TemplateName': $($_.Exception.Message)"
                }
                
                # Apply the specified properties
                foreach ($property in $Properties.GetEnumerator()) {
                    Write-Verbose "Attempting to set property '$($property.Key)' on template '$TemplateName' to: $($property.Value)"
                    
                    try {
                        # Handle different property value types
                        if ($property.Value -is [System.Collections.ICollection] -and $property.Value -isnot [byte[]]) {
                            # Handle collection types (ArrayList, Array, etc.) but not byte arrays
                            Write-Verbose "Setting complex collection value for property '$($property.Key)'"
                            
                            # Clear existing values
                            $template.Properties[$property.Key].Clear()
                            
                            # Add each value in the collection
                            foreach ($value in $property.Value) {
                                $template.Properties[$property.Key].Add($value) | Out-Null
                                Write-Verbose "Added value '$value' to property '$($property.Key)'"
                            }
                            
                            $appliedProperties += "$($property.Key) = @($($property.Value -join ', '))"
                            
                        } else {
                            # Handle simple values (strings, integers, byte arrays, etc.)
                            Write-Verbose "Setting simple value for property '$($property.Key)'"
                            $template.Properties[$property.Key].Value = $property.Value
                            
                            if ($property.Value -is [byte[]]) {
                                $appliedProperties += "$($property.Key) = [byte array of length $($property.Value.Length)]"
                            } else {
                                $appliedProperties += "$($property.Key) = '$($property.Value)'"
                            }
                        }
                        
                        Write-Verbose "Successfully set property '$($property.Key)'"
                        
                    } catch {
                        $errorMsg = "Failed to set property '$($property.Key)': $($_.Exception.Message)"
                        $errors += $errorMsg
                        Write-Warning $errorMsg
                    }
                }
                
                # Commit all changes to Active Directory
                Write-Verbose "Committing changes to Active Directory for template '$TemplateName'"
                $template.CommitChanges()
                Write-Verbose "Successfully committed changes to template '$TemplateName'"
                $success = $true
            }
            
        } catch [System.DirectoryServices.DirectoryServicesCOMException] {
            $comError = $_.Exception
            $errorMsg = "Failed to modify template '$TemplateName': $($comError.Message) (HRESULT: 0x$($comError.ErrorCode.ToString('X8')))"
            $errors += $errorMsg
            Write-Error $errorMsg
            
        } catch {
            $errorMsg = "Unexpected error modifying template '$TemplateName': $($_.Exception.Message)"
            $errors += $errorMsg
            Write-Error $errorMsg
            
        } finally {
            # Clean up DirectoryEntry object
            if ($template) {
                $template.Dispose()
                Write-Verbose "Disposed of template DirectoryEntry object"
            }
        }
        
        # Return result object
        return [PSCustomObject]@{
            Success = $success
            TemplateName = $TemplateName
            AppliedProperties = $appliedProperties
            PropertyCount = $appliedProperties.Count
            Errors = $errors
            HasErrors = $errors.Count -gt 0
        }
    }

    end {
        # Clean up RootDSE object
        if ($rootDSE) {
            $rootDSE.Dispose()
            Write-Verbose "Disposed of rootDSE DirectoryEntry object"
        }
        
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Finishing $($MyInvocation.MyCommand) on $env:COMPUTERNAME..."
    }
}