Private/New-AGTemplateOid.ps1

function New-AGTemplateOid {
    <#
    .SYNOPSIS
        Mints a fresh certificate-template OID under the forest's base arc.

    .DESCRIPTION
        Implements the Microsoft-authentic OID pattern settled in ADCSGoat
        issue #22: read the forest's base OID from the msPKI-Cert-Template-OID
        attribute on CN=OID,CN=Public Key Services,CN=Services,<ConfigNC>,
        append two random numeric segments, and create a companion
        ms-PKI-Enterprise-Oid object under the OID container recording the
        OID-to-display-name mapping that the Certificate Templates MMC snap-in
        and certutil resolve.

        The companion class has no description attribute, so companion
        ownership for teardown/replacement is established by OID equality
        against the owned template object (see Remove-AGTemplate), never by
        inspecting the companion's cn.

    .PARAMETER TemplateName
        The display name the new OID resolves to (the destination template's
        identity).

    .PARAMETER Server
        The domain controller to write to. Defaults to the logon server.

    .OUTPUTS
        System.String. The freshly minted template OID.

    .EXAMPLE
        $oid = New-AGTemplateOid -TemplateName 'Copy of Web Server'

    .NOTES
        The OID container's schema-mandated cn format is "<numeric>.<32 hex
        chars>" (a sequence number plus a GUID-like hex suffix). The sequence
        number is derived from existing children to keep the format authentic.
    #>

    # ShouldProcess is deliberately not implemented: internal building block
    # whose AD write is gated by the caller's (Copy-AGTemplate) collision
    # prompt / -Force contract, matching the module's existing precedent
    # (New-AGBlankTemplateObject performs direct AD writes without
    # ShouldProcess). The collision prompt, not WhatIf, is the safety gate.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Internal building block; AD writes gated by caller collision contract per module precedent.')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$TemplateName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Server
    )

    begin {
        Add-Type -AssemblyName System.DirectoryServices

        if ([string]::IsNullOrEmpty($Server)) {
            $Server = [System.Net.Dns]::GetHostEntry($env:LOGONSERVER.TrimStart('\')).HostName
        }

        $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/RootDSE")
        $configurationPartition = $rootDSE.configurationNamingContext
        $oidContainerDN = "CN=OID,CN=Public Key Services,CN=Services,$configurationPartition"
        $oidContainer = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/$oidContainerDN")
    }

    process {
        $forestBase = "$($oidContainer.Properties['msPKI-Cert-Template-OID'].Value)"
        if ([string]::IsNullOrEmpty($forestBase)) {
            $exception = New-Object System.InvalidOperationException("Forest base OID not found at '$oidContainerDN' (msPKI-Cert-Template-OID is empty). Is AD CS installed in this forest?")
            $errorRecord = New-Object System.Management.Automation.ErrorRecord($exception, 'ForestBaseOidNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $oidContainerDN)
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        # Two random numeric segments per the observed Microsoft pattern.
        $segment1 = Get-Random -Minimum 10000000 -Maximum 99999999
        $segment2 = Get-Random -Minimum 10000000 -Maximum 99999999
        $templateOid = "$forestBase.$segment1.$segment2"

        # Companion object cn: <seq>.<32 hex chars>, matching existing children.
        $existingSeq = @(
            foreach ($child in $oidContainer.Children) {
                $childName = "$($child.Name)"
                if ($childName -match '^CN=(\d+)\.') { [int]$Matches[1] }
                $child.Dispose()
            }
        )
        $nextSeq = if ($existingSeq.Count -gt 0) { ($existingSeq | Measure-Object -Maximum).Maximum + 1 } else { 1 }
        $hexSuffix = [guid]::NewGuid().ToString('N').ToUpperInvariant()
        $companionCn = "$nextSeq.$hexSuffix"

        $companion = $oidContainer.Children.Add("CN=$companionCn", 'msPKI-Enterprise-Oid')
        $companion.CommitChanges()
        $companion.Properties['displayName'].Value = $TemplateName
        $companion.Properties['msPKI-Cert-Template-OID'].Value = $templateOid
        $companion.CommitChanges()
        $companion.Dispose()
        Write-Verbose "Minted template OID '$templateOid' with companion object 'CN=$companionCn,$oidContainerDN'."

        $oidContainer.Dispose()

        Write-Output ([pscustomobject]@{
            Oid               = $templateOid
            CompanionObjectDN = "CN=$companionCn,$oidContainerDN"
        })
    }
}