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..." } } |