.local/share/powershell/Modules/Locksmith2/Locksmith2.Classes.ps1

class LS2AdcsObject {
    # Common properties for all AD CS objects
    [string]$distinguishedName
    [string[]]$objectClass
    [string]$name
    [string]$displayName
    [string]$cn
    [System.DirectoryServices.ActiveDirectorySecurity]$ObjectSecurity
    [string]$Path
    [string]$Owner
    [Nullable[bool]]$HasNonStandardOwner
    
    # Certificate Template properties (pKICertificateTemplate)
    [Nullable[int]]$flags
    [Nullable[int]]$pKIDefaultKeySpec
    [Nullable[int]]$pKIMaxIssuingDepth
    [string[]]$pKICriticalExtensions
    [string[]]$pKIExtendedKeyUsage
    [Nullable[int]]$CertificateNameFlag      # msPKI-Certificate-Name-Flag
    [Nullable[int]]$EnrollmentFlag            # msPKI-Enrollment-Flag
    [Nullable[int]]$PrivateKeyFlag            # msPKI-Private-Key-Flag
    [Nullable[int]]$RASignature               # msPKI-RA-Signature
    [Nullable[int]]$TemplateSchemaVersion     # msPKI-Template-Schema-Version
    [Nullable[int]]$TemplateMinorRevision     # msPKI-Template-Minor-Revision
    
    # CA properties (pKIEnrollmentService)
    [string[]]$certificateTemplates
    [string]$dNSHostName
    [object[]]$CAAdministrators
    [object[]]$CertificateManagers
    [string[]]$DangerousCAAdministrator
    [string[]]$DangerousCAAdministratorNames
    [string[]]$LowPrivilegeCAAdministrator
    [string[]]$LowPrivilegeCAAdministratorNames
    [string[]]$DangerousCACertificateManager
    [string[]]$DangerousCACertificateManagerNames
    [string[]]$LowPrivilegeCACertificateManager
    [string[]]$LowPrivilegeCACertificateManagerNames
    
    # Computed properties (added by Set-* functions)
    [Nullable[bool]]$SANAllowed
    [Nullable[bool]]$AuthenticationEKUExist
    [Nullable[bool]]$AnyPurposeEKUExist
    [Nullable[bool]]$DangerousEnrollee
    [Nullable[bool]]$LowPrivilegeEnrollee
    [string[]]$DangerousEditor
    [string[]]$DangerousEditorNames
    [string[]]$LowPrivilegeEditor
    [string[]]$LowPrivilegeEditorNames
    [Nullable[bool]]$ManagerApprovalNotRequired
    [Nullable[bool]]$AuthorizedSignatureNotRequired
    [Nullable[bool]]$Enabled
    [string[]]$EnabledOn
    [string]$ComputerPrincipal
    [Nullable[bool]]$RPCEncryptionNotRequired
    [object[]]$EditFlags
    [Nullable[bool]]$SANFlagEnabled
    [object[]]$InterfaceFlags
    [Nullable[int]]$AuditFilter
    [object[]]$DisableExtensionList
    [Nullable[bool]]$SecurityExtensionDisabled
    
    # Schema class name for easy type checking
    [string]$SchemaClassName
    
    # Constructor from DirectoryEntry
    LS2AdcsObject([System.DirectoryServices.DirectoryEntry]$DirectoryEntry) {
        # Common properties - set these FIRST
        $this.distinguishedName = if ($DirectoryEntry.distinguishedName) { $DirectoryEntry.distinguishedName.Value } else { $null }
        $this.objectClass = if ($DirectoryEntry.objectClass) { @($DirectoryEntry.objectClass) } else { @() }
        $this.name = if ($DirectoryEntry.name) { $DirectoryEntry.name.Value } else { $null }
        $this.displayName = if ($DirectoryEntry.displayName) { $DirectoryEntry.displayName.Value } else { $null }
        $this.cn = if ($DirectoryEntry.cn) { $DirectoryEntry.cn.Value } else { $null }
        $this.Path = $DirectoryEntry.Path
        
        # CA properties - set dNSHostName early for CAFullName ScriptProperty
        $this.certificateTemplates = if ($DirectoryEntry.Properties.Contains('certificateTemplates')) { @($DirectoryEntry.certificateTemplates) } else { @() }
        $this.dNSHostName = if ($DirectoryEntry.Properties.Contains('dNSHostName')) { $DirectoryEntry.Properties['dNSHostName'][0] } else { $null }
        
        # Determine schema class name (most specific objectClass)
        if ($this.objectClass.Count -gt 0) {
            $this.SchemaClassName = $this.objectClass[$this.objectClass.Count - 1]
        }
        
        # Certificate Template properties
        $this.flags = if ($DirectoryEntry.Properties.Contains('flags')) { $DirectoryEntry.flags.Value } else { $null }
        $this.pKIDefaultKeySpec = if ($DirectoryEntry.Properties.Contains('pKIDefaultKeySpec')) { $DirectoryEntry.pKIDefaultKeySpec.Value } else { $null }
        $this.pKIMaxIssuingDepth = if ($DirectoryEntry.Properties.Contains('pKIMaxIssuingDepth')) { $DirectoryEntry.pKIMaxIssuingDepth.Value } else { $null }
        $this.pKICriticalExtensions = if ($DirectoryEntry.Properties.Contains('pKICriticalExtensions')) { @($DirectoryEntry.pKICriticalExtensions) } else { @() }
        $this.pKIExtendedKeyUsage = if ($DirectoryEntry.Properties.Contains('pKIExtendedKeyUsage')) { @($DirectoryEntry.pKIExtendedKeyUsage) } else { @() }
        
        # msPKI-* properties with proper type conversion
        $this.CertificateNameFlag = if ($DirectoryEntry.Properties.Contains('msPKI-Certificate-Name-Flag')) { [int]$DirectoryEntry.Properties['msPKI-Certificate-Name-Flag'][0] } else { $null }
        $this.EnrollmentFlag = if ($DirectoryEntry.Properties.Contains('msPKI-Enrollment-Flag')) { [int]$DirectoryEntry.Properties['msPKI-Enrollment-Flag'][0] } else { $null }
        $this.PrivateKeyFlag = if ($DirectoryEntry.Properties.Contains('msPKI-Private-Key-Flag')) { [int]$DirectoryEntry.Properties['msPKI-Private-Key-Flag'][0] } else { $null }
        $this.RASignature = if ($DirectoryEntry.Properties.Contains('msPKI-RA-Signature')) { [int]$DirectoryEntry.Properties['msPKI-RA-Signature'][0] } else { $null }
        $this.TemplateSchemaVersion = if ($DirectoryEntry.Properties.Contains('msPKI-Template-Schema-Version')) { [int]$DirectoryEntry.Properties['msPKI-Template-Schema-Version'][0] } else { $null }
        $this.TemplateMinorRevision = if ($DirectoryEntry.Properties.Contains('msPKI-Template-Minor-Revision')) { [int]$DirectoryEntry.Properties['msPKI-Template-Minor-Revision'][0] } else { $null }
        
        # Security descriptor and ownership
        try {
            $this.ObjectSecurity = $DirectoryEntry.ObjectSecurity
            $this.Owner = $this.ObjectSecurity.Owner
        } catch {
            Write-Verbose "Could not retrieve ObjectSecurity for '$($this.distinguishedName)': $_"
            $this.ObjectSecurity = $null
            $this.Owner = $null
        }
        
        # Initialize HasNonStandardOwner (preserve value if already set by Set-HasNonStandardOwner)
        if ($DirectoryEntry.PSObject.Properties['HasNonStandardOwner'] -and $null -ne $DirectoryEntry.HasNonStandardOwner) {
            $this.HasNonStandardOwner = $DirectoryEntry.HasNonStandardOwner
        } else {
            $this.HasNonStandardOwner = $null
        }
        
        # Initialize computed properties to defaults
        $this.SANAllowed = $null
        $this.AuthenticationEKUExist = $null
        $this.AnyPurposeEKUExist = $null
        $this.DangerousEnrollee = $null
        $this.LowPrivilegeEnrollee = $null
        $this.DangerousEditor = @()
        $this.DangerousEditorNames = @()
        $this.LowPrivilegeEditor = @()
        $this.LowPrivilegeEditorNames = @()
        $this.ManagerApprovalNotRequired = $null
        $this.AuthorizedSignatureNotRequired = $null
        $this.Enabled = $null
        $this.EnabledOn = @()
        $this.RPCEncryptionNotRequired = $null
        $this.SANFlagEnabled = $null
        $this.AuditFilter = $null
        $this.DisableExtensionList = @()
        
        # Initialize CA-specific properties
        $this.CAAdministrators = @()
        $this.CertificateManagers = @()
        $this.DangerousCAAdministrator = @()
        $this.DangerousCAAdministratorNames = @()
        $this.LowPrivilegeCAAdministrator = @()
        $this.LowPrivilegeCAAdministratorNames = @()
        $this.DangerousCACertificateManager = @()
        $this.DangerousCACertificateManagerNames = @()
        $this.LowPrivilegeCACertificateManager = @()
        $this.LowPrivilegeCACertificateManagerNames = @()
        
        # Add CAFullName as a ScriptProperty for CA objects
        if ($this.IsCertificationAuthority()) {
            $this | Add-Member -MemberType ScriptProperty -Name CAFullName -Value {
                if ($this.dNSHostName -and $this.cn) {
                    return "$($this.dNSHostName)\$($this.cn)"
                } elseif ($this.cn) {
                    return $this.cn
                } else {
                    return $null
                }
            }
        }
        
        # Add nTSecurityDescriptor as an alias for ObjectSecurity
        $this | Add-Member -MemberType ScriptProperty -Name nTSecurityDescriptor -Value {
            return $this.ObjectSecurity
        }
    }
    
    # Method to check if this is a Certificate Template
    [bool] IsCertificateTemplate() {
        return $this.SchemaClassName -eq 'pKICertificateTemplate'
    }
    
    # Method to check if this is a CA
    [bool] IsCertificationAuthority() {
        return $this.objectClass -contains 'pKIEnrollmentService'
    }
    
    # Method to get a friendly name for logging
    [string] GetFriendlyName() {
        if ($this.displayName) {
            return $this.displayName
        } elseif ($this.name) {
            return $this.name
        } elseif ($this.cn) {
            return $this.cn
        } else {
            return $this.distinguishedName
        }
    }
}

class LS2Issue {
    # Core issue identification
    [string]$Technique              # ESC1, ESC2, ESC6, etc.
    [string]$Forest                 # Forest where issue was found
    [string]$Name                   # Friendly name of vulnerable object
    [string]$DistinguishedName      # DN of vulnerable object
    
    # Principal information (for permission-based issues)
    [string]$IdentityReference      # DOMAIN\User or group name
    [string]$IdentityReferenceSID   # SID of the principal
    [string]$ActiveDirectoryRights  # GenericAll, ExtendedRight, etc.
    
    # Template-specific properties
    [Nullable[bool]]$Enabled        # Whether template is enabled on any CA
    [string[]]$EnabledOn            # List of CAs where template is enabled
    
    # CA-specific properties
    [string]$CAFullName             # For CA issues: SERVER\CA
    
    # Ownership properties
    [string]$Owner                  # Owner of the vulnerable object
    [Nullable[bool]]$HasNonStandardOwner  # Whether object has non-standard owner
    
    # Group expansion properties
    [Nullable[int]]$MemberCount     # For group issues: number of members expanded
    
    # Issue details
    [string]$Issue                  # Description of the vulnerability
    [string]$Fix                    # PowerShell script to remediate
    [string]$Revert                 # PowerShell script to undo remediation
    
    # Constructor for creating issues from hashtable
    LS2Issue([hashtable]$Properties) {
        # Core properties
        $this.Technique = $Properties.Technique
        $this.Forest = $Properties.Forest
        $this.Name = $Properties.Name
        $this.DistinguishedName = $Properties.DistinguishedName
        
        # Principal properties (may be null for non-permission issues)
        $this.IdentityReference = $Properties.IdentityReference
        $this.IdentityReferenceSID = $Properties.IdentityReferenceSID
        $this.ActiveDirectoryRights = $Properties.ActiveDirectoryRights
        
        # Template properties (may be null for CA issues)
        $this.Enabled = $Properties.Enabled
        $this.EnabledOn = $Properties.EnabledOn
        
        # CA properties (may be null for template issues)
        $this.CAFullName = $Properties.CAFullName
        
        # Ownership properties
        $this.Owner = $Properties.Owner
        $this.HasNonStandardOwner = $Properties.HasNonStandardOwner
        
        # Issue details
        $this.Issue = $Properties.Issue
        $this.Fix = $Properties.Fix
        $this.Revert = $Properties.Revert
    }
    
    # Method to get a friendly identifier for the issue
    [string] GetIdentifier() {
        if ($this.IdentityReference) {
            return "$($this.Technique): $($this.Name) - $($this.IdentityReference)"
        } elseif ($this.Owner) {
            return "$($this.Technique): $($this.Name) - Owner: $($this.Owner)"
        } else {
            return "$($this.Technique): $($this.Name)"
        }
    }
    
    # Method to check if this is a permission-based issue
    [bool] HasPrincipal() {
        return -not [string]::IsNullOrEmpty($this.IdentityReference)
    }
    
    # Method to check if this is a template issue
    [bool] IsTemplateIssue() {
        return $null -ne $this.Enabled
    }
    
    # Method to check if this is a CA issue
    [bool] IsCAIssue() {
        return -not [string]::IsNullOrEmpty($this.CAFullName)
    }
    
    # Method to check if this issue matches another issue (for deduplication)
    [bool] Matches([LS2Issue]$Other) {
        if ($null -eq $Other) {
            return $false
        }
        
        # Core properties must match
        if ($this.Technique -ne $Other.Technique) { return $false }
        if ($this.DistinguishedName -ne $Other.DistinguishedName) { return $false }
        
        # Principal properties must match (null-safe comparison)
        if ($this.IdentityReferenceSID -ne $Other.IdentityReferenceSID) { return $false }
        if ($this.ActiveDirectoryRights -ne $Other.ActiveDirectoryRights) { return $false }
        
        # CA property must match
        if ($this.CAFullName -ne $Other.CAFullName) { return $false }
        
        # Owner must match
        if ($this.Owner -ne $Other.Owner) { return $false }
        
        return $true
    }
}

class LS2Principal {
    [string]$distinguishedName
    [string]$objectSid
    [string]$sAMAccountName
    [string]$objectClass
    [string]$displayName
    [string]$NTAccountName
    [string]$userPrincipalName
    [string[]]$memberOf
    [int]$MemberCount
    [System.DirectoryServices.ActiveDirectorySecurity]$ObjectSecurity

    # Constructor from SearchResult
    LS2Principal(
        [System.DirectoryServices.SearchResult]$SearchResult,
        [string]$Server,
        [System.Security.Principal.SecurityIdentifier]$SidKey,
        [string]$NTAccountName
    ) {
        $this.distinguishedName = $SearchResult.Properties['distinguishedName'][0]
        
        # Create DirectoryEntry to get ObjectSecurity
        if (-not $Server) {
            throw "Server parameter is null or empty"
        }
        
        $objectPath = "LDAP://$Server/$($this.distinguishedName)"
        Write-Verbose "LS2Principal: Creating DirectoryEntry for $objectPath"
        $tempEntry = New-AuthenticatedDirectoryEntry -Path $objectPath
        
        # Handle case where DirectoryEntry creation fails
        if (-not $tempEntry) {
            throw "Failed to create DirectoryEntry for path: $objectPath"
        }
        
        # Set objectSid
        if ($SearchResult.Properties['objectSid'].Count -gt 0) {
            $this.objectSid = (New-Object System.Security.Principal.SecurityIdentifier($SearchResult.Properties['objectSid'][0], 0)).Value
        }
        
        # Set sAMAccountName
        if ($SearchResult.Properties['sAMAccountName'].Count -gt 0) {
            $this.sAMAccountName = $SearchResult.Properties['sAMAccountName'][0]
        }
        
        # Set objectClass (get the most specific class)
        if ($SearchResult.Properties['objectClass'].Count -gt 0) {
            $classes = @($SearchResult.Properties['objectClass'])
            $this.objectClass = $classes[$classes.Count - 1]
        }
        
        # Set displayName
        if ($SearchResult.Properties['displayName'].Count -gt 0) {
            $this.displayName = $SearchResult.Properties['displayName'][0]
        }
        
        # Set NTAccountName - use provided or build from sAMAccountName + domain
        if ($NTAccountName) {
            $this.NTAccountName = $NTAccountName
        } elseif ($this.sAMAccountName) {
            # Build NTAccount name from sAMAccountName and domain NetBIOS name
            $domainDN = $this.distinguishedName -replace '^.*?,(?=DC=)', ''
            
            if ($script:DomainStore -and $script:DomainStore.ContainsKey($domainDN)) {
                $domainNetBiosName = $script:DomainStore[$domainDN].nETBIOSName.ToUpper()
                $this.NTAccountName = "$domainNetBiosName\$($this.sAMAccountName)"
            } else {
                # Fallback: extract first DC component from DN
                if ($domainDN -match 'DC=([^,]+)') {
                    $domainNetBiosName = $Matches[1].ToUpper()
                    $this.NTAccountName = "$domainNetBiosName\$($this.sAMAccountName)"
                }
            }
        }
        
        # Set userPrincipalName
        if ($SearchResult.Properties['userPrincipalName'].Count -gt 0) {
            $this.userPrincipalName = $SearchResult.Properties['userPrincipalName'][0]
        }
        
        # Set memberOf
        if ($SearchResult.Properties['memberOf'].Count -gt 0) {
            $this.memberOf = @($SearchResult.Properties['memberOf'])
        } else {
            $this.memberOf = @()
        }
        
        # Set ObjectSecurity (may fail for some objects in PS5.1)
        try {
            $this.ObjectSecurity = $tempEntry.ObjectSecurity
        } catch {
            Write-Verbose "Could not retrieve ObjectSecurity for '$($this.distinguishedName)': $_"
            $this.ObjectSecurity = $null
        }
        
        # Dispose only if not null
        if ($tempEntry) {
            $tempEntry.Dispose()
        }
        
        # Add nTSecurityDescriptor as an alias for ObjectSecurity
        $this | Add-Member -MemberType ScriptProperty -Name nTSecurityDescriptor -Value {
            return $this.ObjectSecurity
        }
    }
    
    # Constructor for well-known principals that don't exist in AD
    # Used for BUILTIN groups (S-1-5-32-*) and other machine-local SIDs
    # Note: Some well-known SIDs like S-1-5-11 exist as foreignSecurityPrincipal objects
    # and will use the main constructor instead
    LS2Principal(
        [string]$ObjectSid,
        [string]$NTAccountName
    ) {
        $this.distinguishedName = $null
        $this.objectSid = $ObjectSid
        $this.sAMAccountName = $null
        $this.objectClass = 'wellKnownPrincipal'
        $this.displayName = $null
        $this.NTAccountName = $NTAccountName
        $this.userPrincipalName = $null
        $this.memberOf = @()
        $this.ObjectSecurity = $null
    }
}