Locksmith.psm1

function Convert-IdentityReferenceToSid {
    <#
    .SYNOPSIS
        Converts an identity reference to a security identifier (SID).
 
    .DESCRIPTION
        The ConvertFrom-IdentityReference function takes an identity reference as input and
        converts it to a security identifier (SID). It supports both SID strings and NTAccount objects.
 
    .PARAMETER Object
        Specifies the identity reference to be converted. This parameter is mandatory.
 
    .EXAMPLE
        $object = "S-1-5-21-3623811015-3361044348-30300820-1013"
        ConvertFrom-IdentityReference -Object $object
        # Returns "S-1-5-21-3623811015-3361044348-30300820-1013"
 
    .EXAMPLE
        $object = New-Object System.Security.Principal.NTAccount("DOMAIN\User")
        ConvertFrom-IdentityReference -Object $object
        # Returns "S-1-5-21-3623811015-3361044348-30300820-1013"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [array]$Object
    )

    $Principal = New-Object System.Security.Principal.NTAccount($Object)
    if ($Principal -match '^(S-1|O:)') {
        $SID = $Principal
    }
    else {
        $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
    }
    return $SID
}

function Export-RevertScript {
    <#
    .SYNOPSIS
        Creates a script that reverts the changes performed by Locksmith.
 
    .DESCRIPTION
        This script is used to revert changes performed by Locksmith.
        It takes in various arrays of objects representing auditing issues and ESC misconfigurations.
        It creates a new script called 'Invoke-RevertLocksmith.ps1' and adds the necessary commands
        to revert the changes made by Locksmith.
 
    .PARAMETER AuditingIssues
        An array of auditing issues to be reverted.
 
    .PARAMETER ESC1
        An array of ESC1 changes to be reverted.
 
    .PARAMETER ESC2
        An array of ESC2 changes to be reverted.
 
    .PARAMETER ESC3
        An array of ESC3 changes to be reverted.
 
    .PARAMETER ESC4
        An array of ESC4 changes to be reverted.
 
    .PARAMETER ESC5
        An array of ESC5 changes to be reverted.
 
    .PARAMETER ESC6
        An array of ESC6 changes to be reverted.
 
    .PARAMETER ESC11
        An array of ESC11 changes to be reverted.
 
    .PARAMETER ESC13
        An array of ESC13 changes to be reverted.
 
    .EXAMPLE
        $params = @{
            AuditingIssues = $AuditingIssues
            ESC1 = $ESC1
            ESC2 = $ESC2
            ESC3 = $ESC3
            ESC4 = $ESC4
            ESC5 = $ESC5
            ESC6 = $ESC6
            ESC11 = $ESC11
            ESC13 = $ESC13
        }
        Export-RevertScript @params
        Reverts the changes performed by Locksmith using the specified arrays of objects.
    #>


    [CmdletBinding()]
    param(
        [array]$AuditingIssues,
        [array]$ESC1,
        [array]$ESC2,
        [array]$ESC3,
        [array]$ESC4,
        [array]$ESC5,
        [array]$ESC6,
        [array]$ESC11,
        [array]$ESC13
    )
    begin {
        $Output = 'Invoke-RevertLocksmith.ps1'
        $RevertScript = [System.Text.StringBuilder]::New()
        [void]$RevertScript.Append("<#`nScript to revert changes performed by Locksmith`nCreated $(Get-Date)`n#>`n")
        $Objects = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC11 + $ESC13
    }
    process {
        if ($Objects) {
            $Objects | ForEach-Object {
                [void]$RevertScript.Append("$($_.Revert)`n")
            }
            $RevertScript.ToString() | Out-File -FilePath $Output
        }
    }
}

function Find-AuditingIssue {
    <#
    .SYNOPSIS
        A function to find auditing issues on AD CS CAs.
 
    .DESCRIPTION
        This script takes an array of AD CS objects and filters them based on specific criteria to identify auditing issues.
        It checks if the object's objectClass is 'pKIEnrollmentService' and if the AuditFilter is not equal to '127'.
        For each matching object, it creates a custom object with information about the issue, fix, and revert actions.
 
    .PARAMETER ADCSObjects
        Specifies an array of ADCS objects to be checked for auditing issues.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject
        A custom object is created for each ADCS object that matches the criteria, containing the following properties:
        - Forest: The forest name of the object.
        - Name: The name of the object.
        - DistinguishedName: The distinguished name of the object.
        - Technique: The technique used to detect the issue (always 'DETECT').
        - Issue: The description of the auditing issue.
        - Fix: The command to fix the auditing issue.
        - Revert: The command to revert the auditing issue.
 
    .EXAMPLE
        $ADCSObjects = Get-ADObject -Filter * -SearchBase 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com'
        $AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects
        $AuditingIssues
        This example retrieves ADCS objects from the specified search base and passes them to the Find-AuditingIssue function.
        It then returns the auditing issues for later use.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [switch]$SkipRisk
    )

    $ADCSObjects | Where-Object {
        ($_.objectClass -eq 'pKIEnrollmentService') -and
        ($_.AuditFilter -ne '127')
    } | ForEach-Object {
        $Issue = [pscustomobject]@{
            Forest            = $_.CanonicalName.split('/')[0]
            Name              = $_.Name
            DistinguishedName = $_.DistinguishedName
            Technique         = 'DETECT'
            Issue             = "Auditing is not fully enabled on $($_.CAFullName). Important security events may go unnoticed."
            Fix               = @"
certutil.exe -config `'$($_.CAFullname)`' -setreg `'CA\AuditFilter`' 127
Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock {
    Get-Service -Name `'certsvc`' | Restart-Service -Force
}
"@

            Revert            = @"
certutil.exe -config $($_.CAFullname) -setreg CA\AuditFilter $($_.AuditFilter)
Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock {
    Get-Service -Name `'certsvc`' | Restart-Service -Force
}
"@

        }
        if ($_.AuditFilter -match 'CA Unavailable') {
            $Issue.Issue = $_.AuditFilter
            $Issue.Fix = 'N/A'
            $Issue.Revert = 'N/A'
        }
        if ($SkipRisk -eq $false) {
            Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
        }
        $Issue
    }
}

function Find-ESC1 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC1 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .PARAMETER SafeUsers
        Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory.
 
    .PARAMETER ClientAuthEKUs
        A list of EKUs that can be used for client authentication.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $Targets = Get-Target
        $ADCSObjects = Get-ADCSObject -Targets $Targets
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $ClientAuthEKUs = '1\.3\.6\.1\.5\.5\.7\.3\.2|1\.3\.6\.1\.5\.2\.3\.4|1\.3\.6\.1\.4\.1\.311\.20\.2\.2|2\.5\.29\.37\.0'
        $Results = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEKUs
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        $ClientAuthEKUs,
        [Parameter(Mandatory)]
        [int]$Mode,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    $ADCSObjects | Where-Object {
        ($_.objectClass -eq 'pKICertificateTemplate') -and
        ($_.pkiExtendedKeyUsage -match $ClientAuthEKUs) -and
        ($_.'msPKI-Certificate-Name-Flag' -band 1) -and
        !($_.'msPKI-Enrollment-Flag' -band 2) -and
        ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') )
    } | ForEach-Object {
        foreach ($entry in $_.nTSecurityDescriptor.Access) {
            $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }
            if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) {
                $Issue = [pscustomobject]@{
                    Forest                = $_.CanonicalName.split('/')[0]
                    Name                  = $_.Name
                    DistinguishedName     = $_.DistinguishedName
                    IdentityReference     = $entry.IdentityReference
                    IdentityReferenceSID  = $SID
                    ActiveDirectoryRights = $entry.ActiveDirectoryRights
                    Enabled               = $_.Enabled
                    EnabledOn             = $_.EnabledOn
                    Issue                 = @"
$($entry.IdentityReference) can provide a Subject Alternative Name (SAN) while
enrolling in this Client Authentication template, and enrollment does not require
Manager Approval.
 
The resultant certificate can be used by an attacker to authenticate as any
principal listed in the SAN up to and including Domain Admins, Enterprise Admins,
or Domain Controllers.
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
"@

                    Fix                   = @"
# Enable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}
"@

                    Revert                = @"
# Disable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}
"@

                    Technique             = 'ESC1'
                }

                if ( $Mode -in @(1, 3, 4) ) {
                    Update-ESC1Remediation -Issue $Issue
                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            }
        }
    }
}

function Find-ESC11 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC11 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on objects that have the objectClass
        'pKIEnrollmentService' and the InterfaceFlag set to 'No'. For each matching object, it creates a custom object with
        properties representing various information about the object, such as Forest, Name, DistinguishedName, Technique,
        Issue, Fix, and Revert.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObject -Target (Get-Target)
        Find-ESC11 -ADCSObjects $ADCSObjects
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    process {
        $ADCSObjects | Where-Object {
            ($_.objectClass -eq 'pKIEnrollmentService') -and
            ($_.InterfaceFlag -ne 'Yes')
        } | ForEach-Object {
            [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)"
            $Issue = [pscustomobject]@{
                Forest            = $_.CanonicalName.split('/')[0]
                Name              = $_.Name
                DistinguishedName = $_.DistinguishedName
                Technique         = 'ESC11'
                Issue             = $_.InterfaceFlag
                Fix               = 'N/A'
                Revert            = 'N/A'
            }
            if ($_.InterfaceFlag -eq 'No') {
                $Issue.Issue = @'
The IF_ENFORCEENCRYPTICERTREQUEST flag is disabled on this Certification
Authority (CA). It is possible to relay NTLM authentication to the RPC interface
of this CA.
 
If the LAN Manager authentication level of any domain in this forest is 2 or
less, an attacker can coerce authentication from a Domain Controller (DC) to
receive a certificate which can be used to authenticate as that DC.
 
More info:
  - https://blog.compass-security.com/2022/11/relaying-to-ad-certificate-services-over-rpc/
 
'@

                $Issue.Fix = @"
# Enable the flag
certutil -config $CAFullname -setreg CA\InterfaceFlags +IF_ENFORCEENCRYPTICERTREQUEST
 
# Restart the Certificate Authority service
Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock {
    Get-Service -Name `'certsvc`' | Restart-Service -Force
}
"@

                $Issue.Revert = @"
# Disable the flag
certutil -config $CAFullname -setreg CA\InterfaceFlags -IF_ENFORCEENCRYPTICERTREQUEST
 
# Restart the Certificate Authority service
Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock {
    Get-Service -Name `'certsvc`' | Restart-Service -Force
}
"@

            }
            if ($SkipRisk -eq $false) {
                Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            }
            $Issue
        }
    }
}

function Find-ESC13 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC13 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .PARAMETER SafeUsers
        Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory.
 
    .PARAMETER ClientAuthEKUs
        A list of EKUs that can be used for client authentication.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObjects
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $ClientAuthEKUs = '1\.3\.6\.1\.5\.5\.7\.3\.2|1\.3\.6\.1\.5\.2\.3\.4|1\.3\.6\.1\.4\.1\.311\.20\.2\.2|2\.5\.29\.37\.0'
        $Results = $ADCSObjects | Find-ESC13 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEKUs
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$ClientAuthEKUs,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )

    $ADCSObjects | Where-Object {
        ($_.objectClass -eq 'pKICertificateTemplate') -and
        ($_.pkiExtendedKeyUsage -match $ClientAuthEKUs) -and
        ($_.'msPKI-Certificate-Policy')
    } | ForEach-Object {
        foreach ($policy in $_.'msPKI-Certificate-Policy') {
            if ($ADCSObjects.'msPKI-Cert-Template-OID' -contains $policy) {
                $OidToCheck = $ADCSObjects | Where-Object 'msPKI-Cert-Template-OID' -EQ $policy
                if ($OidToCheck.'msDS-OIDToGroupLink') {
                    foreach ($entry in $_.nTSecurityDescriptor.Access) {
                        $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
                        if ($Principal -match '^(S-1|O:)') {
                            $SID = $Principal
                        }
                        else {
                            $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
                        }
                        if ( ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match 'ExtendedRight') ) {
                            $Issue = [pscustomobject]@{
                                Forest                = $_.CanonicalName.split('/')[0]
                                Name                  = $_.Name
                                DistinguishedName     = $_.DistinguishedName
                                IdentityReference     = $entry.IdentityReference
                                IdentityReferenceSID  = $SID
                                ActiveDirectoryRights = $entry.ActiveDirectoryRights
                                Enabled               = $_.Enabled
                                EnabledOn             = $_.EnabledOn
                                LinkedGroup           = $OidToCheck.'msDS-OIDToGroupLink'
                                Issue                 = @"
$($entry.IdentityReference) can enroll in this Client Authentication template
which is linked to the group $($OidToCheck.'msDS-OIDToGroupLink').
 
If $($entry.IdentityReference) uses this certificate for authentication, they
will gain the rights of the linked group while the group membership appears empty.
 
More info:
  - https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53
 
"@

                                Fix                   = @"
# Enable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}
"@

                                Revert                = @"
# Disable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}
"@

                                Technique             = 'ESC13'
                            }
                            if ($SkipRisk -eq $false) {
                                Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                            }
                            $Issue
                        }
                    }
                }
            }
        }
    }
}

function Find-ESC15 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC15/EUKwu vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $Targets = Get-Target
        $ADCSObjects = Get-ADCSObjects -Targets $Targets
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $Results = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUser $SafeUsers
        $Results
    #>

    [alias('Find-EKUwu')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    $ADCSObjects | Where-Object {
        ($_.objectClass -eq 'pKICertificateTemplate') -and
        ($_.'msPKI-Template-Schema-Version' -eq 1) -and
        ($_.Enabled)
    } | ForEach-Object {
        foreach ($entry in $_.nTSecurityDescriptor.Access) {
            $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }
            if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) {
                $Issue = [pscustomobject]@{
                    Forest                = $_.CanonicalName.split('/')[0]
                    Name                  = $_.Name
                    DistinguishedName     = $_.DistinguishedName
                    IdentityReference     = $entry.IdentityReference
                    IdentityReferenceSID  = $SID
                    ActiveDirectoryRights = $entry.ActiveDirectoryRights
                    Enabled               = $_.Enabled
                    EnabledOn             = $_.EnabledOn
                    Issue                 = @"
$($_.Name) uses AD CS Template Schema Version 1, and $($entry.IdentityReference)
is allowed to enroll in this template.
 
If patches for CVE-2024-49019 have not been applied it may be possible to include
arbitrary Application Policies while enrolling in this template, including
Application Policies that permit Client Authentication or allow the creation
of Subordinate CAs.
 
More info:
  - https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc
  - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49019
 
"@

                    Fix                   = @"
<#
    Option 1: Manual Remediation
    Step 1: Identify if this template is Enabled on any CA.
    Step 2: If Enabled, identify if this template has recently been used to generate a certificate.
    Step 3a: If recently used, either restrict enrollment scope or convert to the template to Schema V2.
    Step 3b: If not recently used, unpublish the template from all CAs.
#>
 
<#
    Option 2: Scripted Remediation
    Step 1: Open an elevated Powershell session as an AD or PKI Admin
    Step 2: Run Unpublish-SchemaV1Templates.ps1
#>
Invoke-WebRequest -Uri https://bit.ly/Fix-ESC15 | Invoke-Expression
 
"@

                    Revert                = '[TODO]'
                    Technique             = 'ESC15/EKUwu'
                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            }
        }
    }
}

function Find-ESC2 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC2 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .PARAMETER SafeUsers
        Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObjects
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $Results = $ADCSObjects | Find-ESC2 -SafeUsers $SafeUsers
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    $ADCSObjects | Where-Object {
        ($_.ObjectClass -eq 'pKICertificateTemplate') -and
        ( (!$_.pkiExtendedKeyUsage) -or ($_.pkiExtendedKeyUsage -match '2.5.29.37.0') ) -and
        !($_.'msPKI-Enrollment-Flag' -band 2) -and
        ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') )
    } | ForEach-Object {
        foreach ($entry in $_.nTSecurityDescriptor.Access) {
            $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }
            if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) {
                $Issue = [pscustomobject]@{
                    Forest                = $_.CanonicalName.split('/')[0]
                    Name                  = $_.Name
                    DistinguishedName     = $_.DistinguishedName
                    IdentityReference     = $entry.IdentityReference
                    IdentityReferenceSID  = $SID
                    ActiveDirectoryRights = $entry.ActiveDirectoryRights
                    Enabled               = $_.Enabled
                    EnabledOn             = $_.EnabledOn
                    Issue                 = @"
$($entry.IdentityReference) can use this template to request any type
of certificate - including Enrollment Agent certificates and Subordinate
Certification Authority (SubCA) certificate - without Manager Approval.
 
If an attacker requests an Enrollment Agent certificate and there exists at least
one enabled ESC3 Condition 2 or ESC15 template available that does not require
Manager Approval, the attacker can request a certificate on behalf of another principal.
The risk presented depends on the privileges granted to the other principal.
 
If an attacker requests a SubCA certificate, the resultant certificate can be used
by an attacker to instantiate their own SubCA which is trusted by AD.
 
By default, certificates created from this attacker-controlled SubCA cannot be
used for authentication, but they can be used for other purposes such as TLS
certs and code signing.
 
However, if an attacker can modify the NtAuthCertificates object (see ESC5),
they can convert their rogue CA into one trusted for authentication.
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
"@

                    Fix                   = @"
# Enable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}
"@

                    Revert                = @"
# Disable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}
"@

                    Technique             = 'ESC2'
                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            }
        }
    }
}

function Find-ESC3C1 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that match the first condition required for ESC3 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .PARAMETER SafeUsers
        Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObjects
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $Results = $ADCSObjects | Find-ESC3C1 -SafeUsers $SafeUsers
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    $ADCSObjects | Where-Object {
        ($_.objectClass -eq 'pKICertificateTemplate') -and
        ($_.pkiExtendedKeyUsage -match $EnrollmentAgentEKU) -and
        !($_.'msPKI-Enrollment-Flag' -band 2) -and
        ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') )
    } | ForEach-Object {
        foreach ($entry in $_.nTSecurityDescriptor.Access) {
            $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }
            if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) {
                $Issue = [pscustomobject]@{
                    Forest                = $_.CanonicalName.split('/')[0]
                    Name                  = $_.Name
                    DistinguishedName     = $_.DistinguishedName
                    IdentityReference     = $entry.IdentityReference
                    IdentityReferenceSID  = $SID
                    ActiveDirectoryRights = $entry.ActiveDirectoryRights
                    Enabled               = $_.Enabled
                    EnabledOn             = $_.EnabledOn
                    Issue                 = @"
$($entry.IdentityReference) can use this template to request an Enrollment Agent
certificate without Manager Approval.
 
The resulting certificate can be used to enroll in any template that requires
an Enrollment Agent to submit the request.
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
"@

                    Fix                   = @"
# Enable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}
"@

                    Revert                = @"
# Disable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}
"@

                    Technique             = 'ESC3'
                    Condition             = 1
                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            }
        }
    }
}

function Find-ESC3C2 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that match the second condition required for ESC3 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .PARAMETER SafeUsers
        Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObject -Targets (Get-Target)
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $Results = $ADCSObjects | Find-ESC3C2 -SafeUsers $SafeUsers
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    $ADCSObjects | Where-Object {
        ($_.objectClass -eq 'pKICertificateTemplate') -and
        ($_.pkiExtendedKeyUsage -match $ClientAuthEKU) -and
        !($_.'msPKI-Enrollment-Flag' -band 2) -and
        ($_.'msPKI-RA-Application-Policies' -match '1.3.6.1.4.1.311.20.2.1') -and
        ($_.'msPKI-RA-Signature' -eq 1)
    } | ForEach-Object {
        foreach ($entry in $_.nTSecurityDescriptor.Access) {
            $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }
            if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) {
                $Issue = [pscustomobject]@{
                    Forest                = $_.CanonicalName.split('/')[0]
                    Name                  = $_.Name
                    DistinguishedName     = $_.DistinguishedName
                    IdentityReference     = $entry.IdentityReference
                    IdentityReferenceSID  = $SID
                    ActiveDirectoryRights = $entry.ActiveDirectoryRights
                    Enabled               = $_.Enabled
                    EnabledOn             = $_.EnabledOn
                    Issue                 = @"
If the holder of a SubCA, Any Purpose, or Enrollment Agent certificate requests
a certificate using this template, they will receive a certificate which allows
them to authenticate as $($entry.IdentityReference).
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
"@

                    Fix                   = @"
First, eliminate unused Enrollment Agent templates.
Then, tightly scope any Enrollment Agent templates that remain and:
# Enable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}
"@

                    Revert                = @"
# Disable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}
"@

                    Technique             = 'ESC3'
                    Condition             = 2
                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            }
        }
    }
}

function Find-ESC4 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC4 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .PARAMETER DangerousRights
        Specifies the list of dangerous rights that should not be assigned to users. This parameter is mandatory.
 
    .PARAMETER SafeUsers
        Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory.
 
    .PARAMETER SafeObjectTypes
        Specifies a list of ObjectTypes which are not a security concern. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObject -Targets (Get-Target)
 
        # GenericAll, WriteDacl, and WriteOwner all permit full control of an AD object.
        # WriteProperty may or may not permit full control depending the specific property and AD object type.
        $DangerousRights = @('GenericAll', 'WriteProperty', 'WriteOwner', 'WriteDacl')
 
        # -512$ = Domain Admins group
        # -519$ = Enterprise Admins group
        # -544$ = Administrators group
        # -18$ = SYSTEM
        # -517$ = Cert Publishers
        # -500$ = Built-in Administrator
        $SafeOwners = '-512$|-519$|-544$|-18$|-517$|-500$'
 
        # -512$ = Domain Admins group
        # -519$ = Enterprise Admins group
        # -544$ = Administrators group
        # -18$ = SYSTEM
        # -517$ = Cert Publishers
        # -500$ = Built-in Administrator
        # -516$ = Domain Controllers
        # -521$ = Read-Only Domain Controllers
        # -9$ = Enterprise Domain Controllers
        # -526$ = Key Admins
        # -527$ = Enterprise Key Admins
        # S-1-5-10 = SELF
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
 
        # The well-known GUIDs for Enroll and AutoEnroll rights on AD CS templates.
        $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2'
 
        # Set output mode
        $Mode = 1
 
        $Results = Find-ESC4 -ADCSObjects $ADCSObjects -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeUsers $SafeUsers -SafeObjectTypes $SafeObjectTypes -Mode $Mode
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$DangerousRights,
        [Parameter(Mandatory)]
        [string]$SafeOwners,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$SafeObjectTypes,
        [Parameter(Mandatory)]
        [int]$Mode,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    $ADCSObjects | Where-Object objectClass -EQ 'pKICertificateTemplate' | ForEach-Object {
        if ($_.Name -ne '' -and $null -ne $_.Name) {
            $Principal = [System.Security.Principal.NTAccount]::New($_.nTSecurityDescriptor.Owner)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }
        }

        if ($SID -notmatch $SafeOwners) {
            $Issue = [pscustomobject]@{
                Forest                = $_.CanonicalName.split('/')[0]
                Name                  = $_.Name
                DistinguishedName     = $_.DistinguishedName
                IdentityReference     = $_.nTSecurityDescriptor.Owner
                IdentityReferenceSID  = $SID
                ActiveDirectoryRights = 'Owner'
                Enabled               = $_.Enabled
                EnabledOn             = $_.EnabledOn
                Issue                 = @"
$($_.nTSecurityDescriptor.Owner) has Owner rights on this template and can
modify it into a template that can create ESC1, ESC2, and ESC3 templates.
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
"@

                Fix                   = @"
`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`')
`$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'
`$ACL.SetOwner(`$Owner)
Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL
"@

                Revert                = @"
`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`')
`$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'
`$ACL.SetOwner(`$Owner)
Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL
"@

                Technique             = 'ESC4'
            }
            if ($SkipRisk -eq $false) {
                Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            }
            $Issue
        }

        foreach ($entry in $_.nTSecurityDescriptor.Access) {
            if ($_.Name -ne '' -and $null -ne $_.Name) {
                $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
                if ($Principal -match '^(S-1|O:)') {
                    $SID = $Principal
                }
                else {
                    $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
                }
            }

            if ( ($SID -notmatch $SafeUsers) -and
                ($entry.AccessControlType -eq 'Allow') -and
                ($entry.ActiveDirectoryRights -match $DangerousRights) -and
                ($entry.ObjectType -notmatch $SafeObjectTypes)
            ) {
                $Issue = [pscustomobject]@{
                    Forest                = $_.CanonicalName.split('/')[0]
                    Name                  = $_.Name
                    DistinguishedName     = $_.DistinguishedName
                    IdentityReference     = $entry.IdentityReference
                    IdentityReferenceSID  = $SID
                    ActiveDirectoryRights = $entry.ActiveDirectoryRights
                    Enabled               = $_.Enabled
                    EnabledOn             = $_.EnabledOn
                    Issue                 = @"
$($entry.IdentityReference) has been granted $($entry.ActiveDirectoryRights) rights on this template.
 
$($entry.IdentityReference) can likely modify this template into an ESC1 template.
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
"@

                    Fix                   = @"
`$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'
foreach ( `$ace in `$ACL.access ) {
    if ( (`$ace.IdentityReference.Value -like '$($Principal.Value)' ) -and ( `$ace.ActiveDirectoryRights -notmatch '^ExtendedRight$') ) {
        `$ACL.RemoveAccessRule(`$ace) | Out-Null
    }
}
Set-Acl -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL
"@

                    Revert                = '[TODO]'
                    Technique             = 'ESC4'
                }

                if ( $Mode -in @(1, 3, 4) ) {
                    Update-ESC4Remediation -Issue $Issue
                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            }
        }
    }
}

function Find-ESC5 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC5 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on the specified conditions.
        For each matching object, it creates a custom object with properties representing various information about
        the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .PARAMETER DangerousRights
        Specifies the list of dangerous rights that should not be assigned to users. This parameter is mandatory.
 
    .PARAMETER SafeOwners
        Specifies the list of SIDs of safe owners who are allowed to have owner rights on the objects. This parameter is mandatory.
 
    .PARAMETER SafeUsers
        Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory.
 
    .PARAMETER SafeObjectTypes
        Specifies a list of ObjectTypes that are not a security concern. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObject
 
        # GenericAll, WriteDacl, and WriteOwner all permit full control of an AD object.
        # WriteProperty may or may not permit full control depending the specific property and AD object type.
        $DangerousRights = @('GenericAll', 'WriteProperty', 'WriteOwner', 'WriteDacl')
 
        # -512$ = Domain Admins group
        # -519$ = Enterprise Admins group
        # -544$ = Administrators group
        # -18$ = SYSTEM
        # -517$ = Cert Publishers
        # -500$ = Built-in Administrator
        $SafeOwners = '-512$|-519$|-544$|-18$|-517$|-500$'
 
        # -512$ = Domain Admins group
        # -519$ = Enterprise Admins group
        # -544$ = Administrators group
        # -18$ = SYSTEM
        # -517$ = Cert Publishers
        # -500$ = Built-in Administrator
        # -516$ = Domain Controllers
        # -521$ = Read-Only Domain Controllers
        # -9$ = Enterprise Domain Controllers
        # -526$ = Key Admins
        # -527$ = Enterprise Key Admins
        # S-1-5-10 = SELF
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
 
        # The well-known GUIDs for Enroll and AutoEnroll rights on AD CS templates.
        $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2'
        $Results = $ADCSObjects | Find-ESC5 -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeUsers $SafeUsers -SafeObjectTypes $SafeObjectTypes
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$DangerousRights,
        [Parameter(Mandatory)]
        [string]$SafeOwners,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$SafeObjectTypes,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    $ADCSObjects | ForEach-Object {
        if ($_.Name -ne '' -and $null -ne $_.Name) {
            $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }
        }

        $IssueDetail = ''
        if ( ($_.objectClass -ne 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) {
            switch ($_.objectClass) {
                container {
                    $IssueDetail = @"
With ownership rights, this principal can modify the container as they wish.
Depending on the exact container, this may result in the rights to create new
CA objects, new templates, new OIDs, etc. to create novel escalation paths.
"@

                }
                computer {
                    $IssueDetail = @"
This computer is hosting a Certification Authority (CA).
 
There is no reason for anyone other than AD Admins to have own CA host objects.
"@

                }
                'msPKI-Cert-Template-OID' {
                    $IssueDetail = @"
This Object Identifier (OID) can be modified into an Application Policy and linked
to an empty Universal Group.
 
If this principal also has ownership or control over a certificate template
(see ESC4), an attacker could link this Application Policy to the template. Once
linked, any certificates issued from that template would allow an attacker to
act as a member of the linked group (see ESC13).
"@

                }
                pKIEnrollmentService {
                    $IssueDetail = @"
Ownership rights can be used to enable currently disabled templates.
 
If this prinicpal also has control over a disabled certificate template (aka ESC4),
they could modify the template into an ESC1 template and enable the certificate.
This ensabled certificate could be use for privilege escalation and persistence.
"@

                }
            }
            if ($_.objectClass -eq 'certificationAuthority' -and $_.Name -eq 'NTAuthCertificates') {
                $IssueDetail = @"
The NTAuthCertificates object determines which Certification Authorities are
trusted by Active Directory (AD) for client authentication of all forms.
 
This principal can use their granted rights on NTAuthCertificates to add their own
rogue CAs. Once the rogue CA is trusted by AD, any client authentication
certificate generated by the CA can be used by the attacker to authenticate.
"@

            }

            $Issue = [pscustomobject]@{
                Forest                = $_.CanonicalName.split('/')[0]
                Name                  = $_.Name
                DistinguishedName     = $_.DistinguishedName
                IdentityReference     = $_.nTSecurityDescriptor.Owner
                IdentityReferenceSID  = $SID
                ActiveDirectoryRights = 'Owner'
                objectClass           = $_.objectClass
                Issue                 = @"
$($_.nTSecurityDescriptor.Owner) has Owner rights on this $($_.objectClass) object. They are able
to modify this object in whatever way they wish.
 
$IssueDetail
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
"@

                Fix                   = @"
`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`')
`$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'
`$ACL.SetOwner(`$Owner)
Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL
"@

                Revert                = "
`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`')
`$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'
`$ACL.SetOwner(`$Owner)
Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL"

                Technique             = 'ESC5'
            } # end switch ($_.objectClass)
            if ($SkipRisk -eq $false) {
                Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            }
            $Issue
        } # end if ( ($_.objectClass -ne 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) )

        $IssueDetail = ''
        foreach ($entry in $_.nTSecurityDescriptor.Access) {
            $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference)
            if ($Principal -match '^(S-1|O:)') {
                $SID = $Principal
            }
            else {
                $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value
            }

            switch ($_.objectClass) {
                container {
                    $IssueDetail = @"
With these rights, this principal may be able to modify the container as they wish.
Depending on the exact container, this may result in the rights to create new
CA objects, new templates, new OIDs, etc. to create novel escalation paths.
"@

                }
                computer {
                    $IssueDetail = @"
This computer is hosting a Certification Authority (CA). It is likely
$($entry.IdentityReference) can take control of this object.
 
There is little reason for anyone other than AD Admins to have elevated rights
to this CA host.
"@

                }
                'msPKI-Cert-Template-OID' {
                    $IssueDetail = @"
This Object Identifier (OID) can be modified into an Application Policy and linked
to an empty Universal Group.
 
If $($entry.IdentityReference) also has control over a certificate template
(see ESC4), an attacker could link this Application Policy to the template. Once
linked, any certificates issued from that template would allow an attacker to
act as a member of the linked group (see ESC13).
"@

                }
                pKIEnrollmentService {
                    $IssueDetail = @"
$($entry.IdentityReference) can use these elevated rights to publish currently
disabled templates.
 
If $($entry.IdentityReference) also has control over a disabled certificate
template (see ESC4), they could modify the template into an ESC1 template then
enable the certificate. This enabled certificate could be use for privilege
escalation and persistence.
"@

                }
            } # end switch ($_.objectClass)
            if ($_.objectClass -eq 'certificationAuthority' -and $_.Name -eq 'NTAuthCertificates') {
                $IssueDetail = @"
The NTAuthCertificates object determines which Certification Authorities are
trusted by Active Directory (AD) for client authentication of all forms.
 
$($entry.IdentityReference) can use their granted rights on NTAuthCertificates
to add their own rogue CAs. Once the rogue CA is trusted, any client authentication
certificates generated by the it can be used by the attacker.
"@

            }

            if ( ($_.objectClass -ne 'pKICertificateTemplate') -and
                ($SID -notmatch $SafeUsers) -and
                ($entry.AccessControlType -eq 'Allow') -and
                ($entry.ActiveDirectoryRights -match $DangerousRights) -and
                ($entry.ObjectType -notmatch $SafeObjectTypes) ) {
                $Issue = [pscustomobject]@{
                    Forest                = $_.CanonicalName.split('/')[0]
                    Name                  = $_.Name
                    DistinguishedName     = $_.DistinguishedName
                    IdentityReference     = $entry.IdentityReference
                    IdentityReferenceSID  = $SID
                    ActiveDirectoryRights = $entry.ActiveDirectoryRights
                    objectClass           = $_.objectClass
                    Issue                 = @"
$($entry.IdentityReference) has $($entry.ActiveDirectoryRights) elevated rights
on this $($_.objectClass) object.
 
$IssueDetail
 
"@

                    Fix                   = @"
`$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'
foreach ( `$ace in `$ACL.access ) {
    if ( (`$ace.IdentityReference.Value -like '$($Principal.Value)' ) -and
        ( `$ace.ActiveDirectoryRights -notmatch '^ExtendedRight$') ) {
        `$ACL.RemoveAccessRule(`$ace) | Out-Null
    }
}
Set-Acl -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL
"@

                    Revert                = '[TODO]'
                    Technique             = 'ESC5'
                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            } # end if ( ($_.objectClass -ne 'pKICertificateTemplate')
        } # end foreach ($entry in $_.nTSecurityDescriptor.Access)
    } # end $ADCSObjects | ForEach-Object
}

function Find-ESC6 {
    <#
    .SYNOPSIS
        This script finds AD CS (Active Directory Certificate Services) objects that have the ESC6 vulnerability.
 
    .DESCRIPTION
        The script takes an array of ADCS objects as input and filters them based on objects that have the objectClass
        'pKIEnrollmentService' and the SANFlag set to 'Yes'. For each matching object, it creates a custom object with
        properties representing various information about the object, such as Forest, Name, DistinguishedName, Technique,
        Issue, Fix, and Revert.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to be processed. This parameter is mandatory.
 
    .OUTPUTS
        The script outputs an array of custom objects representing the matching ADCS objects and their associated information.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObjects
        $Results = $ADCSObjects | Find-ESC6
        $Results
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )
    process {
        $ADCSObjects | Where-Object {
            ($_.objectClass -eq 'pKIEnrollmentService') -and
            ($_.SANFlag -ne 'No')
        } | ForEach-Object {
            [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)"
            $Issue = [pscustomobject]@{
                Forest            = $_.CanonicalName.split('/')[0]
                Name              = $_.Name
                DistinguishedName = $_.DistinguishedName
                Issue             = $_.SANFlag
                Fix               = 'N/A'
                Revert            = 'N/A'
                Technique         = 'ESC6'
            }
            if ($_.SANFlag -eq 'Yes') {
                $Issue.Issue = @"
The dangerous EDITF_ATTRIBUTESUBJECTALTNAME2 flag is enabled on $CAFullname.
All templates enabled on this CA will accept a Subject Alternative Name (SAN)
during enrollment even if the template is not specifically configured to allow a SAN.
 
As of May 2022, Microsoft has neutered this situation by requiring all SANs to
be strongly mapped to certificates.
 
However, if strong mapping has been explicitly disabled on Domain Controllers,
this configuration remains vulnerable to privilege escalation attacks.
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
  - https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16
 
"@

                $Issue.Fix = @"
# Disable the flag
certutil -config $CAFullname -setreg policy\EditFlags -EDITF_ATTRIBUTESUBJECTALTNAME2
 
# Restart the Certificate Authority service
Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock {
    Get-Service -Name `'certsvc`' | Restart-Service -Force
}
"@

                $Issue.Revert = @"
# Enable the flag
certutil -config $CAFullname -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2
 
# Restart the Certificate Authority service
Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock {
    Get-Service -Name `'certsvc`' | Restart-Service -Force
}
"@

            }
            if ($SkipRisk -eq $false) {
                Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            }
            $Issue
        }
    }
}

function Find-ESC8 {
    <#
    .SYNOPSIS
        Finds ADCS objects with enrollment endpoints and identifies the enrollment type.
 
    .DESCRIPTION
        This script takes an array of ADCS objects and filters them based on the presence of a CA enrollment endpoint.
        It then determines the enrollment type (HTTP or HTTPS) for each object and returns the results.
 
    .PARAMETER ADCSObjects
        Specifies the array of ADCS objects to process. This parameter is mandatory.
 
    .OUTPUTS
        An object representing the ADCS object with the following properties:
        - Forest: The forest name of the object.
        - Name: The name of the object.
        - DistinguishedName: The distinguished name of the object.
        - CAEnrollmentEndpoint: The CA enrollment endpoint of the object.
        - Issue: The identified issue with the enrollment type.
        - Fix: The recommended fix for the issue.
        - Revert: The recommended revert action for the issue.
        - Technique: The technique used to identify the issue.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObjects
        $Results = $ADCSObjects | Find-ESC8
        $Results
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )

    process {
        $ADCSObjects | Where-Object {
            $_.CAEnrollmentEndpoint
        } | ForEach-Object {
            foreach ($endpoint in $_.CAEnrollmentEndpoint) {
                $Issue = [pscustomobject]@{
                    Forest               = $_.CanonicalName.split('/')[0]
                    Name                 = $_.Name
                    DistinguishedName    = $_.DistinguishedName
                    CAEnrollmentEndpoint = $endpoint.URL
                    AuthType             = $endpoint.Auth
                    Issue                = @'
An HTTP enrollment endpoint is available. It is possible to relay NTLM
authentication to this HTTP endpoint.
 
If the LAN Manager authentication level of any domain in this forest is 2 or
less, an attacker can coerce authentication from a Domain Controller (DC) and
relay it to this HTTP enrollment endpoint to receive a certificate which can be
used to authenticate as that DC.
 
More info:
  - https://posts.specterops.io/certified-pre-owned-d95910965cd2
 
'@

                    Fix                  = @'
Disable HTTP access and enforce HTTPS.
Enable EPA.
Disable NTLM authentication (if possible.)
'@

                    Revert               = '[TODO]'
                    Technique            = 'ESC8'
                }
                if ($endpoint.URL -match '^https:') {
                    $Issue.Issue = @'
An HTTPS enrollment endpoint is available. It may be possible to relay NTLM
authentication to this HTTPS endpoint. Enabling IIS Extended Protection for
Authentication or disabling NTLM authentication completely, NTLM relay is not
possible.
 
If those protection are not in place, and the LAN Manager authentication level
of any domain in this forest is 2 or less, an attacker can coerce authentication
from a Domain Controller (DC) and relay it to this HTTPS enrollment endpoint to
receive a certificate which can be used to authenticate as that DC.
 
'@

                    $Issue.Fix = @'
Ensure EPA is enabled.
Disable NTLM authentication (if possible.)
'@

                }
                if ($SkipRisk -eq $false) {
                    Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
                }
                $Issue
            }
        }
    }
}

<#
    This is a working POC. I need to test both checks and possibly blend pieces of them.
    Then I need to fold this function into the Locksmith workflow.
#>


function Find-ESC9 {
    <#
    .SYNOPSIS
        Checks for ESC9 (No Security Extension) Vulnerability
 
    .DESCRIPTION
        This function checks for certificate templates that contain the flag CT_FLAG_NO_SECURITY_EXTENSION (0x80000),
        which will likely make them vulnerable to ESC9. Another factor to check for ESC9 is the registry values on AD
        domain controllers that can help harden certificate based authentication for Kerberos and SChannel.
 
    .NOTES
        An ESC9 condition exists when:
 
        - the new msPKI-Enrollment-Flag value on a certificate contains the flag CT_FLAG_NO_SECURITY_EXTENSION (0x80000)
        - AND an insecure registry value is set on domain controllers:
 
          - the StrongCertificateBindingEnforcement registry value for Kerberos is not set to 2 (the default is 1) on domain controllers
            at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc
          - OR the CertificateMappingMethods registry value for SCHANNEL contains the UPN flag on domain controllers at
            HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel
 
        When the CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag is set on a certificate template, the new szOID_NTDS_CA_SECURITY_EXT
        security extension will not be embedded in issued certificates. This security extension was added by Microsoft's
        patch KB5014754 ("Certificate-based authentication changes on Windows domain controllers") on May 10, 2022.
 
        The patch applies to all servers that run Active Directory Certificate Services and Windows domain controllers that
        service certificate-based authentication.
        https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16
 
        Based on research from
        https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7,
        https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16,
        and on a very long conversation with Bing Chat.
 
        Additional notes from Cortana -- Bing when I pressed her to tell me whether both conditions were required for ESC9 or only one of them:
            A certificate template can still be vulnerable to ESC9 even if the msPKI-Enrollment-Flag does not include
            CT_FLAG_NO_SECURITY_EXTENSION. This is because the vulnerability primarily arises from the ability of a
            requester to specify the subjectAltName in a Certificate Signing Request (CSR). If a requester can specify
            the subjectAltName in a CSR, they can request a certificate as anyone, including a domain admin user.
            Therefore, if a certificate template allows requesters to specify a subjectAltName and
            StrongCertificateBindingEnforcement is not set to 2, it could potentially be vulnerable to ESC9. However,
            the presence of CT_FLAG_NO_SECURITY_EXTENSION in msPKI-Enrollment-Flag is a clear indicator of a template
            being vulnerable to ESC9.
#>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [switch]$SkipRisk
    )

    # Import the required module
    Import-Module ActiveDirectory

    # Get the configuration naming context
    $configNC = (Get-ADRootDSE).configurationNamingContext

    # Define the path to the Certificate Templates container
    $path = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC"

    # Get all certificate templates
    $templates = Get-ADObject -Filter * -SearchBase $path -Properties msPKI-Enrollment-Flag, msPKI-Certificate-Name-Flag

    foreach ($template in $templates) {
        # Check if msPKI-Enrollment-Flag contains the CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag
        if ($template.'msPKI-Enrollment-Flag' -band 0x80000) {
            # Check if msPKI-Certificate-Name-Flag contains the CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME (0x2) flag
            if ($template.'msPKI-Certificate-Name-Flag' -band 0x2) {
                # Output the template name
                Write-Output "Template Name: $($template.Name), Vulnerable to ESC9"
            }
        }
    }

    # AND / OR / ALSO

    Import-Module ActiveDirectory

    $templates = Get-ADObject -Filter { ObjectClass -eq 'pKICertificateTemplate' } -Properties *
    foreach ($template in $templates) {
        $name = $template.Name

        $subjectNameFlag = $template.'msPKI-Cert-Template-OID'
        $subjectType = $template.'msPKI-Certificate-Application-Policy'
        $enrollmentFlag = $template.'msPKI-Enrollment-Flag'
        $certificateNameFlag = $template.'msPKI-Certificate-Name-Flag'

        # Check if the template is vulnerable to ESC9
        if ($subjectNameFlag -eq 'Supply in the request' -and
                ($subjectType -eq 'User' -or $subjectType -eq 'Computer') -and
            # 0x200 means a certificate needs to include a template name certificate extension
            # 0x220 instructs the client to perform auto-enrollment for the specified template
                ($enrollmentFlag -eq 0x200 -or $enrollmentFlag -eq 0x220) -and
            # 0x2 instructs the client to supply subject information in the certificate request (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT).
            # This means that any user who is allowed to enroll in a certificate with this setting can request a certificate as any
            # user in the network, including a privileged user.
            # 0x3 instructs the client to supply both the subject and subject alternate name information in the certificate request
                ($certificateNameFlag -eq 0x2 -or $certificateNameFlag -eq 0x3)) {

            # Print the template name and the vulnerability
            Write-Output "$name is vulnerable to ESC9"
        }
        else {
            # Print the template name and the status
            Write-Output "$name is not vulnerable to ESC9"
        }
    }
}

function Format-Result {
    <#
    .SYNOPSIS
        Formats the result of an issue for display.
 
    .DESCRIPTION
        This script formats the result of an issue for display based on the specified mode.
 
    .PARAMETER Issue
        The issue object containing information about the detected issue.
 
    .PARAMETER Mode
        The mode to determine the formatting style. Valid values are 0 and 1.
 
    .EXAMPLE
        Format-Result -Issue $Issue -Mode 0
        Formats the issue result in table format.
 
    .EXAMPLE
        Format-Result -Issue $Issue -Mode 1
        Formats the issue result in list format.
 
    .NOTES
        Author: Spencer Alessi
    #>

    [CmdletBinding()]
    param(
        $Issue,
        [Parameter(Mandatory)]
        [int]$Mode
    )

    $IssueTable = @{
        DETECT        = 'Auditing Not Fully Enabled'
        ESC1          = 'ESC1 - Vulnerable Certificate Template - Authentication'
        ESC2          = 'ESC2 - Vulnerable Certificate Template - Subordinate CA/Any Purpose'
        ESC3          = 'ESC3 - Vulnerable Certificate Template - Enrollment Agent'
        ESC4          = 'ESC4 - Vulnerable Access Control - Certificate Template'
        ESC5          = 'ESC5 - Vulnerable Access Control - PKI Object'
        ESC6          = 'ESC6 - EDITF_ATTRIBUTESUBJECTALTNAME2 Flag Enabled'
        ESC8          = 'ESC8 - HTTP/S Enrollment Enabled'
        ESC11         = 'ESC11 - IF_ENFORCEENCRYPTICERTREQUEST Flag Disabled'
        ESC13         = 'ESC13 - Vulnerable Certificate Template - Group-Linked'
        'ESC15/EKUwu' = 'ESC15 - Vulnerable Certificate Template - Schema V1'
    }

    $RiskTable = @{
        'Informational' = 'Black, White'
        'Low'           = 'Black, Yellow'
        'Medium'        = 'Black, DarkYellow'
        'High'          = 'Black, Red'
        'Critical'      = 'White, DarkRed'
    }

    if ($null -ne $Issue) {
        $UniqueIssue = $Issue.Technique | Sort-Object -Unique
        $Title = $($IssueTable[$UniqueIssue])
        Write-Host "$('-'*($($Title.ToString().Length + 10)))" -ForegroundColor Black -BackgroundColor Magenta -NoNewline; Write-Host
        Write-Host " " -BackgroundColor Magenta -NoNewline
        Write-Host $Title -BackgroundColor Magenta -ForegroundColor Black -NoNewline
        Write-Host " " -BackgroundColor Magenta -NoNewline; Write-Host
        Write-Host "$('-'*($($Title.ToString().Length + 10)))" -ForegroundColor Black -BackgroundColor Magenta -NoNewline; Write-Host


        if ($Mode -eq 0) {
            # TODO Refactor this
            switch ($UniqueIssue) {
                { $_ -in @('DETECT', 'ESC6', 'ESC8', 'ESC11') } {
                    $Issue |
                        Format-Table Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap |
                            Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive
                }
                { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC13', 'ESC15/EKUwu') } {
                    $Issue |
                        Format-Table Technique, @{l = 'Template Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Enabled, Issue -Wrap |
                            Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive
                }
                'ESC5' {
                    $Issue |
                        Format-Table Technique, @{l = 'Object Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap |
                            Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive
                }
            }
        }
        elseif ($Mode -eq 1) {
            switch ($UniqueIssue) {
                { $_ -in @('DETECT', 'ESC6', 'ESC8', 'ESC11') } {
                    $Issue |
                        Format-List Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } |
                            Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive
                }
                { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC13', 'ESC15/EKUwu') } {
                    $Issue |
                        Format-List Technique, @{l = 'Template Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Enabled, EnabledOn, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } |
                            Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive
                }
                'ESC5' {
                    $Issue |
                        Format-List Technique, @{l = 'Object Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, objectClass, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } |
                            Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive
                }
            }
        }
    }
}

function Get-ADCSObject {
    <#
    .SYNOPSIS
        Retrieves Active Directory Certificate Services (AD CS) objects.
 
    .DESCRIPTION
        This script retrieves AD CS objects from the specified forests.
        It can be used to gather information about Public Key Services in Active Directory.
 
    .PARAMETER Targets
        Specifies the forest(s) from which to retrieve AD CS objects.
 
    .PARAMETER Credential
        Specifies the credentials to use for authentication when retrieving ADCS objects.
 
    .EXAMPLE
        Get-ADCSObject -Credential $cred -Targets (Get-Target)
        This example retrieves ADCS objects from the local forest using the specified credentials.
 
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [array]$Targets,
        [System.Management.Automation.PSCredential]$Credential
    )
    foreach ( $forest in $Targets ) {
        if ($Credential) {
            $ADRoot = (Get-ADRootDSE -Credential $Credential -Server $forest).defaultNamingContext
            Get-ADObject -Filter * -SearchBase "CN=Public Key Services,CN=Services,CN=Configuration,$ADRoot" -SearchScope 2 -Properties * -Credential $Credential
        }
        else {
            $ADRoot = (Get-ADRootDSE -Server $forest).defaultNamingContext
            Get-ADObject -Filter * -SearchBase "CN=Public Key Services,CN=Services,CN=Configuration,$ADRoot" -SearchScope 2 -Properties *
        }
    }
}

function Get-CAHostObject {
    <#
    .SYNOPSIS
        Retrieves Certificate Authority (CA) host object(s) from Active Directory.
 
    .DESCRIPTION
        This script retrieves CA host object(s) associated with every CA configured in the target Active Directory forest.
        If a Credential is provided, the script retrieves the CA host object(s) using the specified credentials.
        If no Credential is provided, the script retrieves the CA host object(s) using the current credentials.
 
    .PARAMETER ADCSObjects
        Specifies an array of AD CS objects to retrieve the CA host object for.
 
    .PARAMETER Credential
        Specifies the credentials to use for retrieving the CA host object(s). If not provided, current credentials will be used.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObjects
        $Credential = Get-Credential
        Get-CAHostObject -ADCSObjects $ADCSObjects -Credential $Credential
 
        This example retrieves the CA host object(s) associated with every CA in the target forest using the provided credentials.
 
    .INPUTS
        System.Array
 
    .OUTPUTS
        System.Object
 
    #>

    [CmdletBinding()]
    param (
        [parameter(
            Mandatory = $true,
            ValueFromPipeline = $true)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [System.Management.Automation.PSCredential]$Credential,
        $ForestGC
    )
    process {
        if ($Credential) {
            $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object {
                Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential
            }
        }
        else {
            $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object {
                Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC
            }
        }
    }
}

function Get-RestrictedAdminModeSetting {
    <#
    .SYNOPSIS
        Retrieves the current configuration of the Restricted Admin Mode setting.
 
    .DESCRIPTION
        This script retrieves the current configuration of the Restricted Admin Mode setting from the registry.
        It checks if the DisableRestrictedAdmin value is set to '0' and the DisableRestrictedAdminOutboundCreds value is set to '1'.
        If both conditions are met, it returns $true; otherwise, it returns $false.
 
    .PARAMETER None
 
    .EXAMPLE
        Get-RestrictedAdminModeSetting
        True
    #>


    $Path = 'HKLM:SYSTEM\CurrentControlSet\Control\Lsa'
    try {
        $RAM = (Get-ItemProperty -Path $Path).DisableRestrictedAdmin
        $Creds = (Get-ItemProperty -Path $Path).DisableRestrictedAdminOutboundCreds
        if ($RAM -eq '0' -and $Creds -eq '1') {
            return $true
        }
        else {
            return $false
        }
    }
    catch {
        return $false
    }
}

function Get-Target {
    <#
    .SYNOPSIS
        Retrieves the target forest(s) based on a provided forest name, input file, or current Active Directory forest.
 
    .DESCRIPTION
        This script retrieves the target forest(s) based on the provided forest name, input file, or current Active Directory forest.
        If the $Forest parameter is specified, the script sets the target to the provided forest.
        If the $InputPath parameter is specified, the script reads the target forest(s) from the file specified by the input path.
        If neither $Forest nor $InputPath is specified, the script retrieves objects from the current Active Directory forest.
        If the $Credential parameter is specified, the script retrieves the target(s) using the provided credentials.
 
    .PARAMETER Forest
        Specifies a single forest to retrieve objects from.
 
    .PARAMETER InputPath
        Specifies the path to the file containing the target forest(s).
 
    .PARAMETER Credential
        Specifies the credentials to use for retrieving the target(s) from the Active Directory forest.
 
    .EXAMPLE
        Get-Target -Forest "example.com"
        Sets the target forest to "example.com".
 
    .EXAMPLE
        Get-Target -InputPath "C:\targets.txt"
        Retrieves the target forest(s) from the file located at "C:\targets.txt".
 
    .EXAMPLE
        Get-Target -Credential $cred
        Sets the target forest to the current Active Directory forest using the provided credentials.
 
    .OUTPUTS
        System.String
        The target(s) retrieved based on the specified parameters.
 
    #>


    param (
        [string]$Forest,
        [string]$InputPath,
        [System.Management.Automation.PSCredential]$Credential
    )

    if ($Forest) {
        $Targets = $Forest
    }
    elseif ($InputPath) {
        $Targets = Get-Content $InputPath
    }
    else {
        if ($Credential) {
            $Targets = (Get-ADForest -Credential $Credential).Name
        }
        else {
            $Targets = (Get-ADForest).Name
        }
    }
    return $Targets
}

function Install-RSATADPowerShell {
    <#
    .SYNOPSIS
        Installs the RSAT AD PowerShell module.
    .DESCRIPTION
        This function checks if the current process is elevated and if it is it will prompt to install the RSAT AD PowerShell module.
    .EXAMPLE
        Install-RSATADPowerShell
    #>

    if (Test-IsElevated) {
        $OS = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType
        # 1 - workstation, 2 - domain controller, 3 - non-dc server
        if ($OS -gt 1) {
            Write-Warning "The Active Directory PowerShell module is not installed."
            Write-Host "If you continue, Locksmith will attempt to install the Active Directory PowerShell module for you.`n" -ForegroundColor Yellow
            Write-Host "`nCOMMAND: Install-WindowsFeature -Name RSAT-AD-PowerShell`n" -ForegroundColor Cyan
            Write-Host "Continue with this operation? [Y] Yes " -NoNewline
            Write-Host "[N] " -ForegroundColor Yellow -NoNewline
            Write-Host "No: " -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Write-Host "Beginning the ActiveDirectory PowerShell module installation, please wait.."
                    # Attempt to install ActiveDirectory PowerShell module for Windows Server OSes, works with Windows Server 2012 R2 through Windows Server 2022
                    Install-WindowsFeature -Name RSAT-AD-PowerShell
                }
                catch {
                    Write-Error 'Could not install ActiveDirectory PowerShell module. This module needs to be installed to run Locksmith successfully.'
                }
            }
            else {
                Write-Host "ActiveDirectory PowerShell module NOT installed. Please install to run Locksmith successfully.`n" -ForegroundColor Yellow
                break;
            }
        }
        else {
            Write-Warning "The Active Directory PowerShell module is not installed."
            Write-Host "If you continue, Locksmith will attempt to install the Active Directory PowerShell module for you.`n" -ForegroundColor Yellow
            Write-Host "`nCOMMAND: Add-WindowsCapability -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 -Online`n" -ForegroundColor Cyan
            Write-Host "Continue with this operation? [Y] Yes " -NoNewline
            Write-Host "[N] " -ForegroundColor Yellow -NoNewline
            Write-Host "No: " -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Write-Host "Beginning the ActiveDirectory PowerShell module installation, please wait.."
                    # Attempt to install ActiveDirectory PowerShell module for Windows Desktop OSes
                    Add-WindowsCapability -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 -Online
                }
                catch {
                    Write-Error 'Could not install ActiveDirectory PowerShell module. This module needs to be installed to run Locksmith successfully.'
                }
            }
            else {
                Write-Host "ActiveDirectory PowerShell module NOT installed. Please install to run Locksmith successfully.`n" -ForegroundColor Yellow
                break;
            }
        }
    }
    else {
        Write-Warning -Message "The ActiveDirectory PowerShell module is required for Locksmith, but is not installed. Please launch an elevated PowerShell session to have this module installed for you automatically."
        # The goal here is to exit the script without closing the PowerShell window. Need to test.
        Return
    }
}
function Invoke-Remediation {
    <#
    .SYNOPSIS
    Runs any remediation scripts available.
 
    .DESCRIPTION
    This function offers to run any remediation code associated with identified issues.
 
    .PARAMETER AuditingIssues
    A PS Object containing all necessary information about auditing issues.
 
    .PARAMETER ESC1
    A PS Object containing all necessary information about ESC1 issues.
 
    .PARAMETER ESC2
    A PS Object containing all necessary information about ESC2 issues.
 
    .PARAMETER ESC3
    A PS Object containing all necessary information about ESC3 issues.
 
    .PARAMETER ESC4
    A PS Object containing all necessary information about ESC4 issues.
 
    .PARAMETER ESC5
    A PS Object containing all necessary information about ESC5 issues.
 
    .PARAMETER ESC6
    A PS Object containing all necessary information about ESC6 issues.
 
    .PARAMETER ESC11
    A PS Object containing all necessary information about ESC11 issues.
 
    .PARAMETER ESC13
    A PS Object containing all necessary information about ESC13 issues.
 
    .INPUTS
    PS Objects
 
    .OUTPUTS
    Console output
    #>


    [CmdletBinding()]
    param (
        $AuditingIssues,
        $ESC1,
        $ESC2,
        $ESC3,
        $ESC4,
        $ESC5,
        $ESC6,
        $ESC11,
        $ESC13
    )

    Write-Host "`nExecuting Mode 4 - Attempting to fix identified issues!`n" -ForegroundColor Green
    Write-Host 'Creating a script (' -NoNewline
    Write-Host 'Invoke-RevertLocksmith.ps1' -ForegroundColor White -NoNewline
    Write-Host ") which can be used to revert all changes made by Locksmith...`n"
    try {
        $params = @{
            AuditingIssues = $AuditingIssues
            ESC1           = $ESC1
            ESC2           = $ESC2
            ESC3           = $ESC3
            ESC4           = $ESC4
            ESC5           = $ESC5
            ESC6           = $ESC6
            ESC11          = $ESC11
            ESC13          = $ESC13
        }
        Export-RevertScript @params
    }
    catch {
        Write-Warning 'Creation of Invoke-RevertLocksmith.ps1 failed.'
        Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
        Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
        Write-Host 'No: ' -NoNewline
        $WarningError = ''
        $WarningError = Read-Host
        if ($WarningError -like 'y') {
            # Continue
        }
        else {
            break
        }
    }
    if ($AuditingIssues) {
        $AuditingIssues | ForEach-Object {
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to fully enable auditing on Certification Authority `"$($_.Name)`".`n"
            Write-Host 'COMMAND(S) TO BE RUN:'
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "This change should have little to no impact on the AD CS environment.`n" -ForegroundColor Green
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not modify AD CS auditing. Are you a local admin on the CA host?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }
    if ($ESC1) {
        $ESC1 | ForEach-Object {
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to enable Manager Approval on the `"$($_.Name)`" template.`n"
            Write-Host 'COMMAND(S) TO BE RUN:'
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "WARNING: This change could cause some services to stop working until certificates are approved.`n" -ForegroundColor Yellow
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }
    if ($ESC2) {
        $ESC2 | ForEach-Object {
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to enable Manager Approval on the `"$($_.Name)`" template.`n"
            Write-Host 'COMMAND(S) TO BE RUN:' -ForegroundColor White
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "WARNING: This change could cause some services to stop working until certificates are approved.`n" -ForegroundColor Yellow
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }
    if ($ESC4) {
        $ESC4 | Where-Object Issue -Like '* Owner rights *' | ForEach-Object { # This selector sucks - Jake
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to set the owner of `"$($_.Name)`" template to Enterprise Admins.`n"
            Write-Host 'COMMAND(S) TO BE RUN:' -ForegroundColor White
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "This change should have little to no impact on the AD CS environment.`n" -ForegroundColor Green
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not change Owner. Are you an Active Directory admin?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }
    if ($ESC5) {
        $ESC5 | Where-Object Issue -Like '* Owner rights *' | ForEach-Object { # TODO This selector sucks - Jake
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to set the owner of `"$($_.Name)`" object to Enterprise Admins.`n"
            Write-Host 'COMMAND(S) TO BE RUN:' -ForegroundColor White
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "This change should have little to no impact on the AD CS environment.`n" -ForegroundColor Green
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not change Owner. Are you an Active Directory admin?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }
    if ($ESC6) {
        $ESC6 | ForEach-Object {
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag on the Certificate Authority `"$($_.Name)`".`n"
            Write-Host 'COMMAND(S) TO BE RUN' -ForegroundColor White
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            $WarningError = 'n'
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "WARNING: This change could cause some services to stop working.`n" -ForegroundColor Yellow
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag. Are you an Active Directory or AD CS admin?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }

    if ($ESC11) {
        $ESC11 | ForEach-Object {
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to enable the IF_ENFORCEENCRYPTICERTREQUEST flag on the Certificate Authority `"$($_.Name)`".`n"
            Write-Host 'COMMAND(S) TO BE RUN' -ForegroundColor White
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            $WarningError = 'n'
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "WARNING: This change could cause some services to stop working.`n" -ForegroundColor Yellow
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not enable the IF_ENFORCEENCRYPTICERTREQUEST flag. Are you an Active Directory or AD CS admin?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }

    if ($ESC13) {
        $ESC13 | ForEach-Object {
            $FixBlock = [scriptblock]::Create($_.Fix)
            Write-Host 'ISSUE:' -ForegroundColor White
            Write-Host "$($_.Issue)`n"
            Write-Host 'TECHNIQUE:' -ForegroundColor White
            Write-Host "$($_.Technique)`n"
            Write-Host 'ACTION TO BE PERFORMED:' -ForegroundColor White
            Write-Host "Locksmith will attempt to enable Manager Approval on the `"$($_.Name)`" template.`n"
            Write-Host 'CCOMMAND(S) TO BE RUN:'
            Write-Host 'PS> ' -NoNewline
            Write-Host "$($_.Fix)`n" -ForegroundColor Cyan
            Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White
            Write-Host "WARNING: This change could cause some services to stop working until certificates are approved.`n" -ForegroundColor Yellow
            Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow
            Write-Host 'Continue with this operation? [Y] Yes ' -NoNewline
            Write-Host '[N] ' -ForegroundColor Yellow -NoNewline
            Write-Host 'No: ' -NoNewline
            $WarningError = ''
            $WarningError = Read-Host
            if ($WarningError -like 'y') {
                try {
                    Invoke-Command -ScriptBlock $FixBlock
                }
                catch {
                    Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?'
                }
            }
            else {
                Write-Host "SKIPPED!`n" -ForegroundColor Yellow
            }
        }
    }

    Write-Host "Mode 4 Complete! There are no more issues that Locksmith can automatically resolve.`n" -ForegroundColor Green
    Write-Host 'If you experience any operational impact from using Locksmith Mode 4, use ' -NoNewline
    Write-Host 'Invoke-RevertLocksmith.ps1 ' -ForegroundColor White
    Write-Host "to revert all changes made by Locksmith. It can be found in the current working directory.`n"
    Write-Host @"
[!] Locksmith cannot automatically resolve all AD CS issues at this time.
There may be more AD CS issues remaining in your environment.
Use Locksmith in Modes 0-3 to further investigate your environment
or reach out to the Locksmith team for assistance. We'd love to help!`n
"@
 -ForegroundColor Yellow
}

function Invoke-Scans {
    <#
    .SYNOPSIS
        Invoke-Scans.ps1 is a script that performs various scans on ADCS (Active Directory Certificate Services) objects.
 
    .PARAMETER Scans
        Specifies the type of scans to perform. Multiple scan options can be provided as an array. The default value is 'All'.
        The available scan options are: 'Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11',
            'ESC13', 'ESC15, 'EKUwu', 'All', 'PromptMe'.
 
    .NOTES
        - The script requires the following functions to be defined: Find-AuditingIssue, Find-ESC1, Find-ESC2, Find-ESC3C1,
          Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC11, Find-ESC13, Find-ESC15
        - The script uses Out-GridView or Out-ConsoleGridView for interactive selection when the 'PromptMe' scan option is chosen.
        - The script returns a hash table containing the results of the scans.
 
    .EXAMPLE
    Invoke-Scans
    # Perform all scans
 
    .EXAMPLE
    Invoke-Scans -Scans 'Auditing', 'ESC1'
    # Perform only the 'Auditing' and 'ESC1' scans
 
    .EXAMPLE
    Invoke-Scans -Scans 'PromptMe'
    # Prompt the user to select the scans to perform
    #>


    [CmdletBinding()]
    [OutputType([hashtable])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Performing multiple scans.')]
    param (
        # Could split Scans and PromptMe into separate parameter sets.
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$ClientAuthEkus,
        [Parameter(Mandatory)]
        [string]$DangerousRights,
        [Parameter(Mandatory)]
        [string]$EnrollmentAgentEKU,
        [Parameter(Mandatory)]
        [int]$Mode,
        [Parameter(Mandatory)]
        [string]$SafeObjectTypes,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$SafeOwners,
        [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')]
        [array]$Scans = 'All',
        [Parameter(Mandatory)]
        [string]$UnsafeUsers,
        [Parameter(Mandatory)]
        [System.Security.Principal.SecurityIdentifier]$PreferredOwner
    )

    if ( $Scans -eq 'PromptMe' ) {
        $GridViewTitle = 'Select the tests to run and press Enter or click OK to continue...'

        # Check for Out-GridView or Out-ConsoleGridView
        if ((Get-Command Out-ConsoleGridView -ErrorAction SilentlyContinue) -and ($PSVersionTable.PSVersion.Major -ge 7)) {
            [array]$Scans = ($Dictionary | Select-Object Name, Category, Subcategory | Out-ConsoleGridView -OutputMode Multiple -Title $GridViewTitle).Name | Sort-Object -Property Name
        }
        elseif (Get-Command -Name Out-GridView -ErrorAction SilentlyContinue) {
            [array]$Scans = ($Dictionary | Select-Object Name, Category, Subcategory | Out-GridView -PassThru -Title $GridViewTitle).Name | Sort-Object -Property Name
        }
        else {
            # To Do: Check for admin and prompt to install features/modules or revert to 'All'.
            Write-Information "Out-GridView and Out-ConsoleGridView were not found on your system. Defaulting to `'All`'."
            $Scans = 'All'
        }
    }

    switch ( $Scans ) {
        Auditing {
            Write-Host 'Identifying auditing issues...'
            [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects
        }
        ESC1 {
            Write-Host 'Identifying AD CS templates with dangerous ESC1 configurations...'
            [array]$ESC1 = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -Mode $Mode -UnsafeUsers $UnsafeUsers
        }
        ESC2 {
            Write-Host 'Identifying AD CS templates with dangerous ESC2 configurations...'
            [array]$ESC2 = Find-ESC2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
        }
        ESC3 {
            Write-Host 'Identifying AD CS templates with dangerous ESC3 configurations...'
            [array]$ESC3 = Find-ESC3C1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            [array]$ESC3 += Find-ESC3C2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
        }
        ESC4 {
            Write-Host 'Identifying AD CS templates with poor access control (ESC4)...'
            [array]$ESC4 = Find-ESC4 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -Mode $Mode -UnsafeUsers $UnsafeUsers
        }
        ESC5 {
            Write-Host 'Identifying AD CS objects with poor access control (ESC5)...'
            [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -UnsafeUsers $UnsafeUsers
        }
        ESC6 {
            Write-Host 'Identifying Issuing CAs with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...'
            [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers
        }
        ESC8 {
            Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...'
            [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers
        }
        ESC11 {
            Write-Host 'Identifying Issuing CAs with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...'
            [array]$ESC11 = Find-ESC11 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers
        }
        ESC13 {
            Write-Host 'Identifying AD CS templates with dangerous ESC13 configurations...'
            [array]$ESC13 = Find-ESC13 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEKUs -UnsafeUsers $UnsafeUsers
        }
        ESC15 {
            Write-Host 'Identifying AD CS templates with dangerous ESC15/EKUwu configurations...'
            [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
        }
        EKUwu {
            Write-Host 'Identifying AD CS templates with dangerous ESC15/EKUwu configurations...'
            [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers
        }
        All {
            Write-Host 'Identifying auditing issues...'
            [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects
            Write-Host 'Identifying AD CS templates with dangerous ESC1 configurations...'
            [array]$ESC1 = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -Mode $Mode -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying AD CS templates with dangerous ESC2 configurations...'
            [array]$ESC2 = Find-ESC2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying AD CS templates with dangerous ESC3 configurations...'
            [array]$ESC3 = Find-ESC3C1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            [array]$ESC3 += Find-ESC3C2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying AD CS templates with poor access control (ESC4)...'
            [array]$ESC4 = Find-ESC4 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -Mode $Mode -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying AD CS objects with poor access control (ESC5)...'
            [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying Certificate Authorities with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...'
            [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...'
            [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying Certificate Authorities with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...'
            [array]$ESC11 = Find-ESC11 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying AD CS templates with dangerous ESC13 configurations...'
            [array]$ESC13 = Find-ESC13 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -UnsafeUsers $UnsafeUsers
            Write-Host 'Identifying AD CS templates with dangerous ESC15 configurations...'
            [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            Write-Host
        }
    }

    [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC8 + $ESC11 + $ESC13 + $ESC15

    # If these are all empty = no issues found, exit
    if ($AllIssues.Count -lt 1) {
        Write-Host "`n$(Get-Date) : No ADCS issues were found." -ForegroundColor Green
        break
    }

    # Return a hash table of array names (keys) and arrays (values) so they can be directly referenced with other functions
    return @{
        AllIssues      = $AllIssues
        AuditingIssues = $AuditingIssues
        ESC1           = $ESC1
        ESC2           = $ESC2
        ESC3           = $ESC3
        ESC4           = $ESC4
        ESC5           = $ESC5
        ESC6           = $ESC6
        ESC8           = $ESC8
        ESC11          = $ESC11
        ESC13          = $ESC13
        ESC15          = $ESC15
    }
}

<#
.SYNOPSIS
Create a dictionary of the escalation paths and insecure configurations that Locksmith scans for.
 
.DESCRIPTION
The New-Dictionary function is used to instantiate an array of objects that contain the names, definitions,
descriptions, code used to find, code used to fix, and reference URLs. This is invoked by the module's main function.
 
.NOTES
 
    VulnerableConfigurationItem Class Definition:
        Version Update each time the class definition or the dictionary below is changed.
        Name The short name of the vulnerable configuration item (VCI).
        Category The high level category of VCI types, including escalation path, server configuration, GPO setting, etc.
        Subcategory The subcategory of vulnerable configuration item types.
        Summary A summary of the vulnerability and how it can be abused.
        FindIt The name of the function that is used to look for the VCI, stored as an invocable scriptblock.
        FixIt The name of the function that is used to fix the VCI, stored as an invocable scriptblock.
        ReferenceUrls An array of URLs that are used as references to learn more about the VCI.
#>


function New-Dictionary {
    class VulnerableConfigurationItem {
        static [string] $Version = '2024.11.03.000'
        [string]$Name
        [ValidateSet('Escalation Path', 'Server Configuration', 'GPO Setting')][string]$Category
        [string]$Subcategory
        [string]$Summary
        [scriptblock]$FindIt
        [scriptblock]$FixIt
        [uri[]]$ReferenceUrls
    }

    [VulnerableConfigurationItem[]]$Dictionary = @(
        [VulnerableConfigurationItem]@{
            Name          = 'ESC1'
            Category      = 'Escalation Path'
            Subcategory   = 'Vulnerable Client Authentication Templates'
            Summary       = ''
            FindIt        = { Find-ESC1 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Misconfigured%20Certificate%20Templates%20%E2%80%94%20ESC1'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC2'
            Category      = 'Escalation Path'
            Subcategory   = 'Vulnerable SubCA/Any Purpose Templates'
            Summary       = ''
            FindIt        = { Find-ESC2 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Misconfigured%20Certificate%20Templates%20%E2%80%94%20ESC2'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC3'
            Category      = 'Escalation Path'
            Subcategory   = 'Vulnerable Enrollment Agent Templates'
            Summary       = ''
            FindIt        = {
                Find-ESC3C1
                Find-ESC3C2
            }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Enrollment%20Agent%20Templates%20%E2%80%94%20ESC3'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC4'
            Category      = 'Escalation Path'
            Subcategory   = 'Certificate Templates with Vulnerable Access Controls'
            Summary       = ''
            FindIt        = { Find-ESC4 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20Certificate%20Template%20Access%20Control%20%E2%80%94%20ESC4'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC5'
            Category      = 'Escalation Path'
            Subcategory   = 'PKI Objects with Vulnerable Access Control'
            Summary       = ''
            FindIt        = { Find-ESC5 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20PKI%20Object%20Access%20Control%20%E2%80%94%20ESC5'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC6'
            Category      = 'Escalation Path'
            Subcategory   = 'EDITF_ATTRIBUTESUBJECTALTNAME2'
            Summary       = ''
            FindIt        = { Find-ESC6 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=EDITF_ATTRIBUTESUBJECTALTNAME2%20%E2%80%94%20ESC6'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC7'
            Category      = 'Escalation Path'
            Subcategory   = 'Vulnerable Certificate Authority Access Control'
            Summary       = ''
            FindIt        = { Write-Output 'We have not created Find-ESC7 yet.' }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20Certificate%20Authority%20Access%20Control%20%E2%80%94%20ESC7'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC8'
            Category      = 'Escalation Path'
            Subcategory   = 'AD CS HTTP Endpoints Vulnerable to NTLM Relay'
            Summary       = ''
            FindIt        = { Find-ESC8 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=NTLM%20Relay%20to%20AD%20CS%20HTTP%20Endpoints'
        },
        # [VulnerableConfigurationItem]@{
        # Name = 'ESC9'
        # Category = 'Escalation Path'
        # Subcategory = ''
        # Summary = ''
        # FindIt = {Find-ESC9}
        # FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'}
        # ReferenceUrls = ''
        # },
        # [VulnerableConfigurationItem]@{
        # Name = 'ESC10'
        # Category = 'Escalation Path'
        # Subcategory = ''
        # Summary = ''
        # FindIt = {Find-ESC10}
        # FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'}
        # ReferenceUrls = ''
        # },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC11'
            Category      = 'Escalation Path'
            Subcategory   = 'IF_ENFORCEENCRYPTICERTREQUEST'
            Summary       = ''
            FindIt        = { Find-ESC11 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://blog.compass-security.com/2022/11/relaying-to-ad-certificate-services-over-rpc/'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'ESC13'
            Category      = 'Escalation Path'
            Subcategory   = 'Certificate Template linked to Group'
            Summary       = ''
            FindIt        = { Find-ESC13 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53'
        }, [VulnerableConfigurationItem]@{
            Name          = 'ESC15/EKUwu'
            Category      = 'Escalation Path'
            Subcategory   = 'Certificate Template using Schema V1'
            Summary       = ''
            FindIt        = { Find-ESC15 }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = 'https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc'
        },
        [VulnerableConfigurationItem]@{
            Name          = 'Auditing'
            Category      = 'Server Configuration'
            Subcategory   = 'Gaps in auditing on certificate authorities and AD CS objects.'
            Summary       = ''
            FindIt        = { Find-AuditingIssue }
            FixIt         = { Write-Output 'Add code to fix the vulnerable configuration.' }
            ReferenceUrls = @('https://github.com/TrimarcJake/Locksmith', 'https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/designing-and-implementing-a-pki-part-i-design-and-planning/ba-p/396953')
        }
    )
    Return $Dictionary
}

function New-OutputPath {
    <#
    .SYNOPSIS
        Creates output directories for each forest.
 
    .DESCRIPTION
        This script creates one output directory per forest specified in the $Targets variable.
        The output directories are created under the $OutputPath directory.
 
    .PARAMETER Targets
        Specifies the forests for which output directories need to be created.
 
    .PARAMETER OutputPath
        Specifies the base path where the output directories will be created.
 
    .EXAMPLE
        New-OutputPath -Targets "Forest1", "Forest2" -OutputPath "C:\Output"
        This example creates two output directories named "Forest1" and "Forest2" under the "C:\Output" directory.
 
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param ()
    # Create one output directory per forest
    foreach ( $forest in $Targets ) {
        $ForestPath = $OutputPath + "`\" + $forest
        New-Item -Path $ForestPath -ItemType Directory -Force  | Out-Null
    }
}

function Set-AdditionalCAProperty {
    <#
    .SYNOPSIS
        Sets additional properties for a Certificate Authority (CA) object.
 
    .DESCRIPTION
        This script sets additional properties for a Certificate Authority (CA) object.
        It takes an array of AD CS Objects as input, which represent the CA objects to be processed.
        The script filters the AD CS Objects based on the objectClass property and performs the necessary operations
        to set the additional properties.
 
    .PARAMETER ADCSObjects
        Specifies the array of AD CS Objects to be processed. This parameter is mandatory and supports pipeline input.
 
    .PARAMETER Credential
        Specifies the PSCredential object to be used for authentication when accessing the CA objects.
        If not provided, the script will use the current user's credentials.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObject -Filter
        Set-AdditionalCAProperty -ADCSObjects $ADCSObjects -ForestGC 'dc1.ad.dotdot.horse:3268'
 
    .NOTES
        Author: Jake Hildreth
        Date: July 15, 2022
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
        [parameter(
            Mandatory = $true,
            ValueFromPipeline = $true)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [PSCredential]$Credential,
        $ForestGC
    )

    begin {
        $CAEnrollmentEndpoint = @()
        if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy') ) {
            if ($PSVersionTable.PSEdition -eq 'Desktop') {
                $code = @"
                    using System.Net;
                    using System.Security.Cryptography.X509Certificates;
                    public class TrustAllCertsPolicy : ICertificatePolicy {
                        public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) {
                            return true;
                        }
                    }
"@

                Add-Type -TypeDefinition $code -Language CSharp
                [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
            }
            else {
                Add-Type @"
                    using System.Net;
                    using System.Security.Cryptography.X509Certificates;
                    using System.Net.Security;
                    public class TrustAllCertsPolicy {
                        public static bool TrustAllCerts(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
                            return true;
                        }
                    }
"@

                # Set the ServerCertificateValidationCallback
                [System.Net.ServicePointManager]::ServerCertificateValidationCallback = [TrustAllCertsPolicy]::TrustAllCerts
            }
        }
    }

    process {
        $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object {
            #[array]$CAEnrollmentEndpoint = $_.'msPKI-Enrollment-Servers' | Select-String 'http.*' | ForEach-Object { $_.Matches[0].Value }
            foreach ($directory in @("certsrv/", "$($_.Name)_CES_Kerberos/service.svc", "$($_.Name)_CES_Kerberos/service.svc/CES", "ADPolicyProvider_CEP_Kerberos/service.svc", "certsrv/mscep/")) {
                $URL = "://$($_.dNSHostName)/$directory"
                try {
                    $Auth = 'NTLM'
                    $FullURL = "http$URL"
                    $Request = [System.Net.WebRequest]::Create($FullURL)
                    $Cache = [System.Net.CredentialCache]::New()
                    $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials)
                    $Request.Credentials = $Cache
                    $Request.Timeout = 1000
                    $Request.GetResponse() | Out-Null
                    $CAEnrollmentEndpoint += @{
                        'URL'  = $FullURL
                        'Auth' = $Auth
                    }
                }
                catch {
                    try {
                        $Auth = 'NTLM'
                        $FullURL = "https$URL"
                        $Request = [System.Net.WebRequest]::Create($FullURL)
                        $Cache = [System.Net.CredentialCache]::New()
                        $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials)
                        $Request.Credentials = $Cache
                        $Request.Timeout = 1000
                        $Request.GetResponse() | Out-Null
                        $CAEnrollmentEndpoint += @{
                            'URL'  = $FullURL
                            'Auth' = $Auth
                        }
                    }
                    catch {
                        try {
                            $Auth = 'Negotiate'
                            $FullURL = "https$URL"
                            $Request = [System.Net.WebRequest]::Create($FullURL)
                            $Cache = [System.Net.CredentialCache]::New()
                            $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials)
                            $Request.Credentials = $Cache
                            $Request.Timeout = 1000
                            $Request.GetResponse() | Out-Null
                            $CAEnrollmentEndpoint += @{
                                'URL'  = $FullURL
                                'Auth' = $Auth
                            }
                        }
                        catch {
                        }
                    }
                }
            }
            [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)"
            $CAHostname = $_.dNSHostName.split('.')[0]
            if ($Credential) {
                $CAHostDistinguishedName = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Server $ForestGC -Credential $Credential).DistinguishedName
                $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC -Credential $Credential).DnsHostname
            }
            else {
                $CAHostDistinguishedName = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Server $ForestGC ).DistinguishedName
                $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC).DnsHostname
            }
            $ping = Test-Connection -ComputerName $CAHostFQDN -Quiet -Count 1
            if ($ping) {
                try {
                    if ($Credential) {
                        $CertutilAudit = Invoke-Command -ComputerName $CAHostname -Credential $Credential -ScriptBlock { param($CAFullName); certutil -config $CAFullName -getreg CA\AuditFilter } -ArgumentList $CAFullName
                    }
                    else {
                        $CertutilAudit = certutil -config $CAFullName -getreg CA\AuditFilter
                    }
                }
                catch {
                    $AuditFilter = 'Failure'
                }
                try {
                    if ($Credential) {
                        $CertutilFlag = Invoke-Command -ComputerName $CAHostname -Credential $Credential -ScriptBlock { param($CAFullName); certutil -config $CAFullName -getreg policy\EditFlags } -ArgumentList $CAFullName
                    }
                    else {
                        $CertutilFlag = certutil -config $CAFullName -getreg policy\EditFlags
                    }
                }
                catch {
                    $SANFlag = 'Failure'
                }
                try {
                    if ($Credential) {
                        $CertutilInterfaceFlag = Invoke-Command -ComputerName $CAHostname -Credential $Credential -ScriptBlock { param($CAFullName); certutil -config $CAFullName -getreg CA\InterfaceFlags } -ArgumentList $CAFullName
                    }
                    else {
                        $CertutilInterfaceFlag = certutil -config $CAFullName -getreg CA\InterfaceFlags
                    }
                }
                catch {
                    $InterfaceFlag = 'Failure'
                }
            }
            else {
                $AuditFilter = 'CA Unavailable'
                $SANFlag = 'CA Unavailable'
                $InterfaceFlag = 'CA Unavailable'
            }
            if ($CertutilAudit) {
                try {
                    [string]$AuditFilter = $CertutilAudit | Select-String 'AuditFilter REG_DWORD = ' | Select-String '\('
                    $AuditFilter = $AuditFilter.split('(')[1].split(')')[0]
                }
                catch {
                    try {
                        [string]$AuditFilter = $CertutilAudit | Select-String 'AuditFilter REG_DWORD = '
                        $AuditFilter = $AuditFilter.split('=')[1].trim()
                    }
                    catch {
                        $AuditFilter = 'Never Configured'
                    }
                }
            }
            if ($CertutilFlag) {
                [string]$SANFlag = $CertutilFlag | Select-String ' EDITF_ATTRIBUTESUBJECTALTNAME2 -- 40000 \('
                if ($SANFlag) {
                    $SANFlag = 'Yes'
                }
                else {
                    $SANFlag = 'No'
                }
            }
            if ($CertutilInterfaceFlag) {
                [string]$InterfaceFlag = $CertutilInterfaceFlag | Select-String ' IF_ENFORCEENCRYPTICERTREQUEST -- 200 \('
                if ($InterfaceFlag) {
                    $InterfaceFlag = 'Yes'
                }
                else {
                    $InterfaceFlag = 'No'
                }
            }
            Add-Member -InputObject $_ -MemberType NoteProperty -Name AuditFilter -Value $AuditFilter -Force
            Add-Member -InputObject $_ -MemberType NoteProperty -Name CAEnrollmentEndpoint -Value $CAEnrollmentEndpoint -Force
            Add-Member -InputObject $_ -MemberType NoteProperty -Name CAFullName -Value $CAFullName -Force
            Add-Member -InputObject $_ -MemberType NoteProperty -Name CAHostname -Value $CAHostname -Force
            Add-Member -InputObject $_ -MemberType NoteProperty -Name CAHostDistinguishedName -Value $CAHostDistinguishedName -Force
            Add-Member -InputObject $_ -MemberType NoteProperty -Name SANFlag -Value $SANFlag -Force
            Add-Member -InputObject $_ -MemberType NoteProperty -Name InterfaceFlag -Value $InterfaceFlag -Force
        }
    }
}

function Set-AdditionalTemplateProperty {
    <#
    .SYNOPSIS
        Sets additional properties on a template object.
 
    .DESCRIPTION
        This script sets additional properties on a template object.
        It takes an array of AD CS Objects as input, which includes the templates to be processed and CA objects that
        detail which templates are Enabled.
        The script filters the AD CS Objects based on the objectClass property and performs the necessary operations
        to set the additional properties.
 
    .PARAMETER ADCSObjects
        Specifies the array of AD CS Objects to be processed. This parameter is mandatory and supports pipeline input.
 
    .PARAMETER Credential
        Specifies the PSCredential object to be used for authentication when accessing the CA objects.
        If not provided, the script will use the current user's credentials.
 
    .EXAMPLE
        $ADCSObjects = Get-ADCSObject -Targets (Get-Target)
        Set-AdditionalTemplateProperty -ADCSObjects $ADCSObjects -ForestGC 'dc1.ad.dotdot.horse:3268'
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
        [parameter(Mandatory, ValueFromPipeline)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects
    )

    $ADCSObjects | Where-Object objectClass -Match 'pKICertificateTemplate' -PipelineVariable template | ForEach-Object {
        # Write-Host "[?] Checking if template `"$($template.Name)`" is Enabled on any Certification Authority." -ForegroundColor Blue
        $Enabled = $false
        $EnabledOn = @()
        foreach ($ca in ($ADCSObjects | Where-Object objectClass -EQ 'pKIEnrollmentService')) {
            if ($ca.certificateTemplates -contains $template.Name) {
                $Enabled = $true
                $EnabledOn += $ca.Name
            }

            $template | Add-Member -NotePropertyName Enabled -NotePropertyValue $Enabled -Force
            $template | Add-Member -NotePropertyName EnabledOn -NotePropertyValue $EnabledOn -Force
        }
    }
}

function Set-RiskRating {
    <#
        .SYNOPSIS
        This function takes an Issue object as input and assigns a numerical risk score depending on issue conditions.
 
        .DESCRIPTION
        Risk of Issue is based on:
        - Issue type: Templates issues are more risky than CA/Object issues by default.
        - Template status: Enabled templates are more risky than disabled templates.
        - Principals: Single users are less risky than groups, and custom groups are less risky than default groups.
        - Principal type: AD Admins aren't risky. gMSAs have little risk (assuming proper controls). Non-admins are most risky
        - Modifiers: Some issues are present a higher risk when certain conditions are met.
 
        .PARAMETER Issue
        A PSCustomObject that includes all pertinent information about an AD CS issue.
 
        .INPUTS
        PSCustomObject
 
        .OUTPUTS
        None. This function sets a new attribute on each Issue object and returns nothing to the pipeline.
 
        .EXAMPLE
        $Targets = Get-Target
        $ADCSObjects = Get-ADCSObject -Targets $Targets
        $DangerousRights = @('GenericAll', 'WriteProperty', 'WriteOwner', 'WriteDacl')
        $SafeOwners = '-519$'
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2'
        $ESC4Issues = Find-ESC4 -ADCSObjects $ADCSObjects -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeUsers $SafeUsers -SafeObjectTypes $SafeObjectTypes -Mode 1
        foreach ($issue in $ESC4Issues) {
            if ($SkipRisk -eq $false) {
                Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers
            }
        }
 
        .LINK
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [object]$Issue,
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects,
        [Parameter(Mandatory)]
        [string]$SafeUsers,
        [Parameter(Mandatory)]
        [string]$UnsafeUsers
    )

    #requires -Version 5

    $RiskValue = 0
    $RiskName = ''
    $RiskScoring = @()

    # CA issues don't rely on a principal and have a base risk of Medium.
    if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC8', 'ESC11')) {
        $RiskValue += 3
        $RiskScoring += 'Base Score: 3'

        if ($Issue.CAEnrollmentEndpoint -like 'http:*') {
            $RiskValue += 2
            $RiskScoring += 'HTTP Enrollment: +2'
        }

        # TODO Check NtAuthCertificates for CA thumbnail. If found, +2, else -1
        # TODO Check if NTLMv1 is allowed.
    }

    # Template and object issues rely on a principal and have complex scoring.
    if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC8', 'ESC11')) {
        $RiskScoring += 'Base Score: 0'

        # Templates are more dangerous when enabled, but objects cannot be enabled/disabled.
        if ($Issue.Technique -ne 'ESC5') {
            if ($Issue.Enabled) {
                $RiskValue += 1
                $RiskScoring += 'Enabled: +1'
            }
            else {
                $RiskValue -= 2
                $RiskScoring += 'Disabled: -2'
            }
        }

        # The principal's objectClass impacts the Issue's risk
        $SID = $Issue.IdentityReferenceSID.ToString()
        $IdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $SID } | Select-Object objectClass

        # ESC1 and ESC4 templates are more dangerous than other templates because they can result in immediate compromise.
        if ($Issue.Technique -in @('ESC1', 'ESC4')) {
            $RiskValue += 1
            $RiskScoring += 'ESC1/4: +1'
        }

        if ($Issue.IdentityReferenceSID -match $UnsafeUsers) {
            # Authenticated Users, Domain Users, Domain Computers etc. are very risky
            $RiskValue += 2
            $RiskScoring += 'Very Large Group: +2'
        }
        elseif ($IdentityReferenceObjectClass -eq 'group') {
            # Groups are riskier than individual principals
            $RiskValue += 1
            $RiskScoring += 'Group: +1'
        }

        # Safe users and managed service accounts are inherently safer than other principals - except in ESC3 Condition 2!
        if ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 2) {
            if ($Issue.IdentityReferenceSID -match $SafeUsers) {
                # Safe Users are admins. Authenticating as an admin is bad.
                $RiskValue += 2
                $RiskScoring += 'Privileged Principal: +2'
            }
            elseif ($IdentityReferenceObjectClass -like '*ManagedServiceAccount') {
                # Managed Service Accounts are *probably* privileged in some way.
                $RiskValue += 1
                $RiskScoring += 'Managed Service Account: +1'
            }
        }
        elseif ($Issue.IdentityReferenceSID -notmatch $SafeUsers -and $IdentityReferenceObjectClass -notlike '*ManagedServiceAccount') {
            $RiskValue += 1
            $RiskScoring += 'Unprivileged Principal: +1'
        }

        # Modifiers that rely on the existence of other ESCs
        # ESC2 and ESC3C1 are more dangerous if ES3C2 templates exist or certain ESC15 templates are enabled
        if ($Issue.Technique -eq 'ESC2' -or ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 1)) {
            $ESC3C2 = Find-ESC3C2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers  -SkipRisk |
                Where-Object { $_.Enabled -eq $true }
            $ESC3C2Names = @(($ESC3C2 | Select-Object -Property Name -Unique).Name)
            if ($ESC3C2Names) {
                $CheckedESC3C2Templates = @{}
                foreach ($name in $ESC3C2Names) {
                    $OtherTemplateRisk = 0
                    $Principals = @()
                    foreach ($esc in $($ESC3C2 | Where-Object Name -EQ $name) ) {
                        if ($CheckedESC3C2Templates.GetEnumerator().Name -contains $esc.Name) {
                            $Principals = $CheckedESC3C2Templates.$($esc.Name)
                        }
                        else {
                            $CheckedESC3C2Templates = @{
                                $($esc.Name) = @()
                            }
                        }
                        $escSID = $esc.IdentityReferenceSID.ToString()
                        $escIdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $escSID } | Select-Object objectClass
                        if ($escSID -match $SafeUsers) {
                            # Safe Users are admins. Authenticating as an admin is bad.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 2
                        }
                        elseif ($escSID -match $UnsafeUsers) {
                            # Unsafe Users are large groups that contain practically all principals and likely including admins.
                            # Authenticating as an admin is bad.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 2
                        }
                        elseif ($escIdentityReferenceObjectClass -like '*ManagedServiceAccount') {
                            # Managed Service Accounts are *probably* privileged in some way.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 1
                        }
                        elseif ($escIdentityReferenceObjectClass -eq 'group') {
                            # Groups are more dangerous than individual principals.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 1
                        }
                        $CheckedESC3C2Templates.$($esc.Name) = $Principals
                    }
                    $RiskScoring += "Principals ($($CheckedESC3C2Templates.$($esc.Name) -join ', ')) are able to enroll in an enabled ESC3 Condition 2 template ($name): +$OtherTemplateRisk"
                } # end foreach ($name)
                if ($OtherTemplateRisk -ge 2) {
                    $OtherTemplateRisk = 2
                }
            } # end if ($ESC3C2Names)

            # Default 'User' and 'Machine' templates are more dangerous
            $ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers  -SkipRisk |
                Where-Object { $_.Enabled -eq $true }
            $ESC15Names = @('Machine', 'User')
            if ($ESC15Names) {
                $CheckedESC15Templates = @{}
                foreach ($name in $ESC15Names) {
                    $OtherTemplateRisk = 0
                    $Principals = @()
                    foreach ($esc in $($ESC15 | Where-Object Name -EQ $name) ) {
                        if ($CheckedESC15Templates.GetEnumerator().Name -contains $esc.Name) {
                            $Principals = $CheckedESC15Templates.$($esc.Name)
                        }
                        else {
                            $Principals = @()
                            $CheckedESC15Templates = @{
                                $($esc.Name) = @()
                            }
                        }
                        $escSID = $esc.IdentityReferenceSID.ToString()
                        $escIdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $escSID } | Select-Object objectClass
                        if ($escSID -match $SafeUsers) {
                            # Safe Users are admins. Authenticating as an admin is bad.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 2
                        }
                        elseif ($escSID -match $UnsafeUsers) {
                            # Unsafe Users are large groups that contain practically all principals and likely including admins.
                            # Authenticating as an admin is bad.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 2
                        }
                        elseif ($escIdentityReferenceObjectClass -like '*ManagedServiceAccount') {
                            # Managed Service Accounts are *probably* privileged in some way.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 1
                        }
                        elseif ($escIdentityReferenceObjectClass -eq 'group') {
                            # Groups are more dangerous than individual principals.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 1
                        }
                        $CheckedESC15Templates.$($esc.Name) = $Principals
                    }
                    $RiskScoring += "Principals ($($CheckedESC15Templates.$($esc.Name) -join ', ')) are able to enroll in an enabled ESC15/EKUwu template ($name)): +$OtherTemplateRisk"
                } # end foreach ($name)
                if ($OtherTemplateRisk -ge 2) {
                    $OtherTemplateRisk = 2
                }
            } # end if ($ESC15Names)
            $RiskValue += $OtherTemplateRisk
        }

        # ESC3 Condition 2 and ESC15 User/Machine templates are only dangerous if ESC2 or ESC3 Condition 1 templates exist.
        if ( ($Issue.Technique -match 'ESC15' -and $Issue.Name -match 'User|Machine') -or
            ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 2)
        ) {
            $ESC2 = Find-ESC2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers  -SkipRisk |
                Where-Object { $_.Enabled -eq $true }
            $ESC2Names = @(($ESC2 | Select-Object -Property Name -Unique).Name)
            if ($ESC2Names) {
                $CheckedESC2Templates = @{}
                foreach ($name in $ESC2Names) {
                    $OtherTemplateRisk = 0
                    $Principals = @()
                    foreach ($esc in $($ESC2 | Where-Object Name -EQ $name) ) {
                        if ($CheckedESC2Templates.GetEnumerator().Name -contains $esc.Name) {
                            $Principals = $CheckedESC2Templates.$($esc.Name)
                        }
                        else {
                            $CheckedESC2Templates = @{
                                $($esc.Name) = @()
                            }
                        }
                        $escSID = $esc.IdentityReferenceSID.ToString()
                        $escIdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $escSID } | Select-Object objectClass
                        if ($escSID -match $UnsafeUsers) {
                            # Unsafe Users are large groups.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 2
                        }
                        elseif ($escIdentityReferenceObjectClass -eq 'group') {
                            # Groups are more dangerous than individual principals.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 1
                        }
                        $CheckedESC2Templates.$($esc.Name) = $Principals
                    }
                    $RiskScoring += "Principals ($($CheckedESC2Templates.$($esc.Name) -join ', ')) are able to enroll in an enabled ESC2 template ($name): +$OtherTemplateRisk"
                } # end foreach ($name)
                if ($OtherTemplateRisk -ge 2) {
                    $OtherTemplateRisk = 2
                }
            } # end if ($ESC2Names)

            $ESC3C1 = Find-ESC3C1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers  -SkipRisk |
                Where-Object { $_.Enabled -eq $true }
            $ESC3C1Names = @(($ESC3C1 | Select-Object -Property Name -Unique).Name)
            if ($ESC3C1Names) {
                $CheckedESC3C1Templates = @{}
                foreach ($name in $ESC3C1Names) {
                    $OtherTemplateRisk = 0
                    $Principals = @()
                    foreach ($esc in $($ESC3C1 | Where-Object Name -EQ $name) ) {
                        if ($CheckedESC3C1Templates.GetEnumerator().Name -contains $esc.Name) {
                            $Principals = $CheckedESC3C1Templates.$($esc.Name)
                        }
                        else {
                            $CheckedESC3C1Templates = @{
                                $($esc.Name) = @()
                            }
                        }
                        $escSID = $esc.IdentityReferenceSID.ToString()
                        $escIdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $escSID } | Select-Object objectClass
                        if ($escSID -match $UnsafeUsers) {
                            # Unsafe Users are large groups.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 2
                        }
                        elseif ($escIdentityReferenceObjectClass -eq 'group') {
                            # Groups are more dangerous than individual principals.
                            $Principals += $esc.IdentityReference.Value
                            $OtherTemplateRisk += 1
                        }
                        $CheckedESC3C1Templates.$($esc.Name) = $Principals
                    }
                    $RiskScoring += "Principals ($($CheckedESC3C1Templates.$($esc.Name) -join ', ')) are able to enroll in an enabled ESC3C1 template ($name): +$OtherTemplateRisk"
                } # end foreach ($name...
                if ($OtherTemplateRisk -ge 2) {
                    $OtherTemplateRisk = 2
                }
            } # end if ($ESC3C1Names)
            $RiskValue += $OtherTemplateRisk
        }

        # Disabled ESC1, ESC2, ESC3, ESC4, and ESC15 templates are more dangerous if there's an ESC5 on one or more CA objects
        if ($Issue.Technique -match 'ESC1|ESC2|ESC3|ESC4' -and $Issue.Enabled -eq $false ) {
            $ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers -DangerousRights $DangerousRights -SafeOwners '-519$' -SafeObjectTypes $SafeObjectTypes -SkipRisk |
                Where-Object { $_.objectClass -eq 'pKIEnrollmentService' }
            $ESC5Names = @(($ESC5 | Select-Object -Property Name -Unique).Name)
            if ($ESC5Names) {
                $CheckedESC5Templates = @{}
                foreach ($name in $ESC5Names) {
                    $OtherIssueRisk = 0
                    $Principals = @()
                    foreach ($OtherIssue in $($ESC5 | Where-Object Name -EQ $name) ) {
                        if ($CheckedESC5Templates.GetEnumerator().Name -contains $OtherIssue.Name) {
                            $Principals = $CheckedESC5Templates.$($OtherIssue.Name)
                        }
                        else {
                            $CheckedESC5Templates = @{
                                $($OtherIssue.Name) = @()
                            }
                        }
                        $OtherIssueSID = $OtherIssue.IdentityReferenceSID.ToString()
                        $OtherIssueIdentityReferenceObjectClass = (Get-ADObject -Filter { objectSid -eq $OtherIssueSID } | Select-Object objectClass).objectClass
                        if ($OtherIssueSID -match $UnsafeUsers) {
                            # Unsafe Users are large groups.
                            $Principals += $OtherIssue.IdentityReference.Value
                            $OtherIssueRisk += 2
                        }
                        elseif ($OtherIssueIdentityReferenceObjectClass -eq 'group') {
                            # Groups are more dangerous than individual principals.
                            $Principals += $OtherIssue.IdentityReference.Value
                            $OtherIssueRisk += 1
                        }
                        $CheckedESC5Templates.$($OtherIssue.Name) = $Principals
                    } # forech ($OtherIssue)
                    if ($OtherIssueRisk -ge 2) {
                        $OtherIssueRisk = 2
                    }
                    $RiskScoring += "Principals ($($CheckedESC5Templates.$($OtherIssue.Name) -join ', ')) are able to modify CA Host object ($name): +$OtherIssueRisk"
                } # end foreach ($name...
            } # end if ($ESC5Names)
            $RiskValue += $OtherIssueRisk
        }

        # ESC5 objectClass determines risk
        if ($Issue.Technique -eq 'ESC5') {
            if ($Issue.objectClass -eq 'certificationAuthority' -and $Issue.distinguishedName -like 'CN=NtAuthCertificates*') {
                # Being able to modify NtAuthCertificates is very bad.
                $RiskValue += 2
                $RiskScoring += 'NtAuthCertificates: +2'
            }
            switch ($Issue.objectClass) {
                # Being able to modify Root CA Objects is very bad.
                'certificationAuthority' {
                    $RiskValue += 2; $RiskScoring += 'Root Certification Authority bject: +2' 
                }
                # Being able to modify Issuing CA Objects is also very bad.
                'pKIEnrollmentService' {
                    $RiskValue += 2; $RiskScoring += 'Issuing Certification Authority Object: +2' 
                }
                # Being able to modify CA Hosts? Yeah... very bad.
                'computer' {
                    $RiskValue += 2; $RiskScoring += 'Certification Authority Host Computer: +2' 
                }
                # Being able to modify OIDs could result in ESC13 vulns.
                'msPKI-Enterprise-Oid' {
                    $RiskValue += 1; $RiskScoring += 'OID: +1' 
                }
                # Being able to modify PKS containers is bad.
                'container' {
                    $RiskValue += 1; $RiskScoring += 'Container: +1' 
                }
            }
        }
    }

    # Convert Value to Name
    $RiskName = switch ($RiskValue) {
        { $_ -le 1 } {
            'Informational' 
        }
        2 {
            'Low' 
        }
        3 {
            'Medium' 
        }
        4 {
            'High' 
        }
        { $_ -ge 5 } {
            'Critical' 
        }
    }

    # Write Risk attributes
    $Issue | Add-Member -NotePropertyName RiskValue -NotePropertyValue $RiskValue -Force
    $Issue | Add-Member -NotePropertyName RiskName -NotePropertyValue $RiskName -Force
    $Issue | Add-Member -NotePropertyName RiskScoring -NotePropertyValue $RiskScoring -Force
}

function Show-LocksmithLogo {
    Write-Host '%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%'
    Write-Host '%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%'
    Write-Host '%%%%%%%%%%%%%%%%%#+==============#%%%%%%%%%%%%%%%%%'
    Write-Host '%%%%%%%%%%%%%%#=====================#%%%%%%%%%%%%%%'
    Write-Host '%%%%%%%%%%%%#=========================#%%%%%%%%%%%%'
    Write-Host '%%%%%%%%%%%=============================%%%%%%%%%%%'
    Write-Host '%%%%%%%%%#==============+++==============#%%%%%%%%%'
    Write-Host '%%%%%%%%#===========#%%%%%%%%%#===========#%%%%%%%%'
    Write-Host '%%%%%%%%==========%%%%%%%%%%%%%%%==========%%%%%%%%'
    Write-Host '%%%%%%%*=========%%%%%%%%%%%%%%%%%=========*%%%%%%%'
    Write-Host '%%%%%%%+========*%%%%%%%%%%%%%%%%%#=========%%%%%%%'
    Write-Host '%%%%%%%+========#%%%%%%%%%%%%%%%%%#=========%%%%%%%'
    Write-Host '%%%%%%%+========#%%%%%%%%%%%%%%%%%#=========%%%%%%%'
    Write-Host '%%%%%%%+========#%%%%%%%%%%%%%%%%%#=========%%%%%%%'
    Write-Host '%%%%%%%+========#%%%%%%%%%%%%%%%%%#=========%%%%%%%'
    Write-Host '%%%%%%%+========#%%%%%%%%%%%%%%%%%#=========%%%%%%%'
    Write-Host '%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%'
    Write-Host '#=================================================#'
    Write-Host '#=================================================#'
    Write-Host '#=================+%%%============================#'
    Write-Host '#==================%%%%*==========================#'
    Write-Host '#===================*%%%%+========================#'
    Write-Host '#=====================#%%%%=======================#'
    Write-Host '#======================+%%%%#=====================#'
    Write-Host '#========================*%%%%*===================#'
    Write-Host '#========================+%%%%%===================#'
    Write-Host '#======================#%%%%%+====================#'
    Write-Host '#===================+%%%%%%=======================#'
    Write-Host '#=================#%%%%%+=========================#'
    Write-Host '#==============+%%%%%#============================#'
    Write-Host '#============*%%%%%+====+%%%%%%%%%%===============#'
    Write-Host '#=============%%*========+********+===============#'
    Write-Host '#=================================================#'
    Write-Host '#=================================================#'
    Write-Host '#=================================================#'
}

function Test-IsADAdmin {
    <#
    .SYNOPSIS
        Tests if the current user has administrative rights in Active Directory.
    .DESCRIPTION
        This function returns True if the current user is a Domain Admin (or equivalent) or False if not.
    .EXAMPLE
        Test-IsADAdmin
    .EXAMPLE
        if (!(Test-IsADAdmin)) { Write-Host "You are not running with Domain Admin rights and will not be able to make certain changes." -ForeGroundColor Yellow }
    #>

    if (
        # Need to test to make sure this checks domain groups and not local groups, particularly for 'Administrators' (reference SID instead of name?).
         ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Domain Admins") -or
         ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators") -or
         ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Enterprise Admins")
    ) {
        return $true
    }
    else {
        return $false
    }
}

function Test-IsElevated {
    <#
    .SYNOPSIS
        Tests if PowerShell is running with elevated privileges (run as Administrator).
    .DESCRIPTION
        This function returns True if the script is being run as an administrator or False if not.
    .EXAMPLE
        Test-IsElevated
    .EXAMPLE
        if (!(Test-IsElevated)) { Write-Host "You are not running with elevated privileges and will not be able to make any changes." -ForeGroundColor Yellow }
    .EXAMPLE
        # Prompt to launch elevated if not already running as administrator:
        if (!(Test-IsElevated)) {
            $arguments = "& '" + $MyInvocation.MyCommand.definition + "'"
            Start-Process powershell -Verb runAs -ArgumentList $arguments
            Break
        }
    #>

    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal $identity
    $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}

function Test-IsLocalAccountSession {
    <#
    .SYNOPSIS
        Tests if the current session is running under a local user account or a domain account.
    .DESCRIPTION
        This function returns True if the current session is a local user or False if it is a domain user.
    .EXAMPLE
        Test-IsLocalAccountSession
    .EXAMPLE
        if ( (Test-IsLocalAccountSession) ) { Write-Host "You are running this script under a local account." -ForeGroundColor Yellow }
    #>

    [CmdletBinding()]

    $CurrentSID = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
    $LocalSIDs = (Get-LocalUser).SID.Value
    if ($CurrentSID -in $LocalSIDs) {
        Return $true
    }
}

function Test-IsMemberOfProtectedUsers {
    <#
        .SYNOPSIS
            Check to see if a user is a member of the Protected Users group.
 
        .DESCRIPTION
            This function checks to see if a specified user or the current user is a member of the Protected Users group in AD.
            It also checked the user's primary group ID in case that is set to 525 (Protected Users).
 
        .PARAMETER User
            The user that will be checked for membership in the Protected Users group. This parameter accepts input from the pipeline.
 
        .EXAMPLE
            This example will check if JaneDoe is a member of the Protected Users group.
 
            Test-IsMemberOfProtectedUsers -User JaneDoe
 
        .EXAMPLE
            This example will check if the current user is a member of the Protected Users group.
 
            Test-IsMemberOfProtectedUsers
 
        .INPUTS
            Active Directory user object, user SID, SamAccountName, etc
 
        .OUTPUTS
            Boolean
    #>



    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The name of the group we are checking is plural.')]
    [OutputType([Boolean])]
    [CmdletBinding()]
    param (
        # User parameter accepts any input that is valid for Get-ADUser
        [Parameter(
            ValueFromPipeline = $true
        )]
        $User
    )

    Import-Module ActiveDirectory

    # Use the currently logged in user if none is specified
    # Get the user from Active Directory
    if (-not($User)) {
        # These two are different types. Fixed by referencing $CheckUser.SID later, but should fix here by using one type.
        $CurrentUser = ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name).Split('\')[-1]
        $CheckUser = Get-ADUser $CurrentUser -Properties primaryGroupID
    }
    else {
        $CheckUser = Get-ADUser $User -Properties primaryGroupID
    }

    # Get the Protected Users group by SID instead of by its name to ensure compatibility with any locale or language.
    $DomainSID = (Get-ADDomain).DomainSID.Value
    $ProtectedUsersSID = "$DomainSID-525"

    # Get members of the Protected Users group for the current domain. Recuse in case groups are nested in it.
    $ProtectedUsers = Get-ADGroupMember -Identity $ProtectedUsersSID -Recursive | Select-Object -Unique

    # Check if the current user is in the 'Protected Users' group
    if ($ProtectedUsers.SID.Value -contains $CheckUser.SID) {
        Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is a member of the Protected Users group."
        $true
    }
    else {
        # Check if the user's PGID (primary group ID) is set to the Protected Users group RID (525).
        if ( $CheckUser.primaryGroupID -eq '525' ) {
            $true
        }
        else {
            Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is not a member of the Protected Users group."
            $false
        }
    }
}

function Test-IsRecentVersion {
    <#
    .SYNOPSIS
        Check if the installed version of the Locksmith module is up to date.
 
    .DESCRIPTION
        This script checks the installed version of the Locksmith module against the latest release on GitHub.
        It determines if the installed version is considered "out of date" based on the number of days specified.
        If the installed version is out of date, a warning message is displayed along with information about the latest release.
 
    .PARAMETER Version
        Specifies the version number to check from the script.
 
    .PARAMETER Days
        Specifies the number of days past a module release date at which to consider the release "out of date".
        The default value is 60 days.
 
    .OUTPUTS
        System.Boolean
        Returns $true if the installed version is up to date, and $false if it is out of date.
 
    .EXAMPLE
        Test-IsRecentVersion -Version "2024.1" -Days 30
        True
 
        Test-IsRecentVersion -Version "2023.10" -Days 60
        WARNING: Your currently installed version of Locksmith (2.5) is more than 60 days old. We recommend that you update to ensure the latest findings are included.
        Locksmith Module Details:
        Latest Version: 2024.12.11
        Publishing Date: 01/28/2024 12:47:18
        Install Module: Install-Module -Name Locksmith
        Standalone Script: https://github.com/trimarcjake/locksmith/releases/download/v2.6/Invoke-Locksmith.zip
    #>

    [CmdletBinding()]
    [OutputType([boolean])]
    param (
        # Check a specific version number from the script
        [Parameter(Mandatory)]
        [string]$Version,
        # Define the number of days past a module release date at which to consider the release "out of date."
        [Parameter()]
        [int16]$Days = 60
    )

    # Strip the 'v' if it was used so the script can work with or without it in the input
    $Version = $Version.Replace('v', '')
    try {
        # Checking the most recent release in GitHub, but we could also use PowerShell Gallery.
        $Uri = "https://api.github.com/repos/trimarcjake/locksmith/releases"
        $Releases = Invoke-RestMethod -Uri $uri -Method Get -DisableKeepAlive -ErrorAction Stop
        $LatestRelease = $Releases | Sort-Object -Property Published_At -Descending | Select-Object -First 1
        # Get the release date of the currently running version via the version parameter
        [datetime]$InstalledVersionReleaseDate = ($Releases | Where-Object { $_.tag_name -like "?$Version" }).Published_at
        [datetime]$LatestReleaseDate = $LatestRelease.Published_at
        # $ModuleDownloadLink = ( ($LatestRelease.Assets).Where({$_.Name -like "Locksmith-v*.zip"}) ).browser_download_url
        $ScriptDownloadLink = ( ($LatestRelease.Assets).Where({ $_.Name -eq 'Invoke-Locksmith.zip' }) ).browser_download_url

        $LatestReleaseInfo = @"
Locksmith Module Details:
 
Latest Version:`t`t $($LatestRelease.name)
Publishing Date: `t`t $LatestReleaseDate
Install Module:`t`t Install-Module -Name Locksmith
Standalone Script:`t $ScriptDownloadLink
"@

    }
    catch {
        Write-Warning "Unable to find the latest available version of the Locksmith module on GitHub." -WarningAction Continue
        # Find the approximate release date of the installed version. Handles version with or without 'v' prefix.
        $InstalledVersionMonth = [datetime]::Parse(($Version.Replace('v', '')).Replace('.', '-') + "-01")
        # Release date is typically the first Saturday of the month. Let's guess as close as possible!
        $InstalledVersionReleaseDate = $InstalledVersionMonth.AddDays( 6 - ($InstallVersionMonth.DayOfWeek) )
    }

    # The date at which to consider this module "out of date" is based on the $Days parameter
    $OutOfDateDate = (Get-Date).Date.AddDays(-$Days)
    $OutOfDateMessage = "Your currently installed version of Locksmith ($Version) is more than $Days days old. We recommend that you update to ensure the latest findings are included."

    # Compare the installed version release date to the latest release date
    if ( ($LatestReleaseDate) -and ($InstalledVersionReleaseDate -le ($LatestReleaseDate.AddDays(-$Days))) ) {
        # If we found the latest release date online and the installed version is more than [x] days older than it:
        Write-Warning -Verbose -Message $OutOfDateMessage -WarningAction Continue
        Write-Information -MessageData $LatestReleaseInfo -InformationAction Continue
        $IsRecentVersion = $false
    }
    elseif ( $InstalledVersionReleaseDate -le $OutOfDateDate ) {
        # If we didn't get the latest release date online, use the estimated release date to check age.
        Write-Warning -Verbose -Message $OutOfDateMessage -WarningAction Continue
        $IsRecentVersion = $false
    }
    else {
        # The installed version has not been found to be out of date.
        $IsRecentVersion = $True
    }

    # Return true/false
    $IsRecentVersion
}

function Test-IsRSATInstalled {
    <#
    .SYNOPSIS
        Tests if the RSAT AD PowerShell module is installed.
    .DESCRIPTION
        This function returns True if the RSAT AD PowerShell module is installed or False if not.
    .EXAMPLE
        Test-IsElevated
    #>

    if (Get-Module -Name 'ActiveDirectory' -ListAvailable) {
        $true
    }
    else {
        $false
    }
}
function Update-ESC1Remediation {
    <#
    .SYNOPSIS
        This function asks the user a set of questions to provide the most appropriate remediation for ESC1 issues.
 
    .DESCRIPTION
        This function takes a single ESC1 issue as input then asks a series of questions to determine the correct
        remediation.
 
        Questions:
        1. Does the identified principal need to enroll in this template? [Yes/No/Unsure]
        2. Is this certificate widely used and/or frequently requested? [Yes/No/Unsure]
 
        Depending on answers to these questions, the Issue and Fix attributes on the Issue object are updated.
 
        TODO: More questions:
        Should the identified principal be able to request certs that include a SAN or SANs?
 
    .PARAMETER Issue
        A pscustomobject that includes all pertinent information about the ESC1 issue.
 
    .OUTPUTS
        This function updates ESC1 remediations customized to the user's needs.
 
    .EXAMPLE
        $Targets = Get-Target
        $ADCSObjects = Get-ADCSObject -Targets $Targets
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $ESC1Issues = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers
        foreach ($issue in $ESC1Issues) { Update-ESC1Remediation -Issue $Issue }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Issue
    )

    $Header = "`n[!] ESC1 Issue detected in $($Issue.Name)"
    Write-Host $Header -ForegroundColor Yellow
    Write-Host $('-' * $Header.Length) -ForegroundColor Yellow
    Write-Host "$($Issue.IdentityReference) can provide a Subject Alternative Name (SAN) while enrolling in this"
    Write-Host "template. Manager approval is not required for a certificate to be issued.`n"
    Write-Host 'To provide the most appropriate remediation for this issue, Locksmith will now ask you a few questions.'

    $Enroll = ''
    do {
        $Enroll = Read-Host "`nDoes $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]"
    } while ( ($Enroll -ne 'y') -and ($Enroll -ne 'n') -and ($Enroll -ne 'unsure'))

    if ($Enroll -eq 'y') {
        $Frequent = ''
        do {
            $Frequent = Read-Host "`nIs the $($Issue.Name) certificate frequently requested? [y/n/unsure]"
        } while ( ($Frequent -ne 'y') -and ($Frequent -ne 'n') -and ($Frequent -ne 'unsure'))

        if ($Frequent -ne 'n') {
            $Issue.Fix = @"
# Locksmith cannot currently determine the best remediation course.
# Remediation Options:
# 1. If $($Issue.IdentityReference) is a group, remove its Enroll/AutoEnroll rights and grant those rights
# to a smaller group or a single user/service account.
 
# 2. Remove the ability to submit a SAN (aka disable "Supply in the request").
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Certificate-Name-Flag' = 0}
 
# 3. Enable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}
"@


            $Issue.Revert = @"
# 1. Replace Enroll/AutoEnroll rights from the smaller group/single user/service account and grant those rights
# back to $($Issue.IdentityReference).
 
# 2. Restore the ability to submit a SAN.
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Certificate-Name-Flag' = 1}
 
# 3. Disable Manager Approval
`$Object = `'$($_.DistinguishedName)`'
Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}
"@

        }
    }
    elseif ($Enroll -eq 'n') {
        $Issue.Fix = @"
<#
    1. Open the Certification Templates Console: certtmpl.msc
    2. Double-click the $($Issue.Name) template to open its Properties page.
    3. Select the Security tab.
    4. Select the entry for $($Issue.IdentityReference).
    5. Uncheck the "Enroll" and/or "Autoenroll" boxes.
    6. Click OK.
#>
"@


        $Issue.Revert = @"
<#
    1. Open the Certification Templates Console: certtmpl.msc
    2. Double-click the $($Issue.Name) template to open its Properties page.
    3. Select the Security tab.
    4. Select the entry for $($Issue.IdentityReference).
    5. Check the "Enroll" and/or "Autoenroll" boxes depending on your specific needs.
    6. Click OK.
#>
"@

    } # end if ($Enroll -eq 'y')/elseif ($Enroll -eq 'n')
}

function Update-ESC4Remediation {
    <#
    .SYNOPSIS
        This function asks the user a set of questions to provide the most appropriate remediation for ESC4 issues.
 
    .DESCRIPTION
        This function takes a single ESC4 issue as input. It then prompts the user if the principal with the ESC4 rights
        administers the template in question.
        If the principal is an admin of the template, the Issue attribute is updated to indicate this configuration is
        expected, and the Fix attribute for the issue is updated to indicate no remediation is needed.
        If the the principal is not an admin of the template AND the rights assigned is GenericAll, Locksmith will ask
        if Enroll or AutoEnroll rights are needed.
        Depending on the answers to the listed questions, the Fix attribute is updated accordingly.
 
    .PARAMETER Issue
        A pscustomobject that includes all pertinent information about the ESC4 issue.
 
    .OUTPUTS
        This function updates ESC4 remediations customized to the user's needs.
 
    .EXAMPLE
        $Targets = Get-Target
        $ADCSObjects = Get-ADCSObject -Targets $Targets
        $DangerousRights = @('GenericAll', 'WriteProperty', 'WriteOwner', 'WriteDacl')
        $SafeOwners = '-512$|-519$|-544$|-18$|-517$|-500$'
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'
        $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2'
        $ESC4Issues = Find-ESC4 -ADCSObjects $ADCSObjects -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeUsers $SafeUsers -SafeObjectTypes $SafeObjectTypes -Mode 1
        foreach ($issue in $ESC4Issues) { Update-ESC4Remediation -Issue $Issue }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Issue
    )

    $Header = "`n[!] ESC4 Issue detected in $($Issue.Name)"
    Write-Host $Header -ForegroundColor Yellow
    Write-Host $('-' * $Header.Length) -ForegroundColor Yellow
    Write-Host "$($Issue.IdentityReference) has $($Issue.ActiveDirectoryRights) rights on this template.`n"
    Write-Host 'To provide the most appropriate remediation for this issue, Locksmith will now ask you a few questions.'

    $Admin = ''
    do {
        $Admin = Read-Host "`nDoes $($Issue.IdentityReference) administer and/or maintain the $($Issue.Name) template? [y/n]"
    } while ( ($Admin -ne 'y') -and ($Admin -ne 'n') )

    if ($Admin -eq 'y') {
        $Issue.Issue = "$($Issue.IdentityReference) has $($Issue.ActiveDirectoryRights) rights on this template, but this is expected."
        $Issue.Fix = "No immediate remediation required."
    }
    elseif ($Issue.Issue -match 'GenericAll') {
        $RightsToRestore = 0
        while ($RightsToRestore -notin 1..5) {
            [string]$Question = @"
 
Does $($Issue.IdentityReference) need to Enroll and/or AutoEnroll in the $($Issue.Name) template?
 
  1. Enroll
  2. AutoEnroll
  3. Both
  4. Neither
  5. Unsure
 
Enter your selection [1-5]
"@

            $RightsToRestore = Read-Host $Question
        }

        switch ($RightsToRestore) {
            1 {
                $Issue.Fix = @"
`$Path = 'AD:$($Issue.DistinguishedName)'
`$ACL = Get-Acl -Path `$Path
`$IdentityReference = [System.Security.Principal.NTAccount]::New('$($Issue.IdentityReference)')
`$EnrollGuid = [System.Guid]::New('0e10c968-78fb-11d2-90d4-00c04f79dc55')
`$ExtendedRight = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
`$AccessType = [System.Security.AccessControl.AccessControlType]::Allow
`$InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
`$NewRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule `$IdentityReference, `$ExtendedRight, `$AccessType, `$EnrollGuid, `$InheritanceType
foreach ( `$ace in `$ACL.access ) {
    if ( (`$ace.IdentityReference.Value -like '$($Issue.IdentityReference)' ) -and ( `$ace.ActiveDirectoryRights -notmatch '^ExtendedRight$') ) {
        `$ACL.RemoveAccessRule(`$ace) | Out-Null
    }
}
`$ACL.AddAccessRule(`$NewRule)
Set-Acl -Path `$Path -AclObject `$ACL
"@

            }
            2 {
                $Issue.Fix = @"
`$Path = 'AD:$($Issue.DistinguishedName)'
`$ACL = Get-Acl -Path `$Path
`$IdentityReference = [System.Security.Principal.NTAccount]::New('$($Issue.IdentityReference)')
`$AutoEnrollGuid = [System.Guid]::New('a05b8cc2-17bc-4802-a710-e7c15ab866a2')
`$ExtendedRight = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
`$AccessType = [System.Security.AccessControl.AccessControlType]::Allow
`$InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
`$AutoEnrollRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule `$IdentityReference, `$ExtendedRight, `$AccessType, `$AutoEnrollGuid, `$InheritanceType
foreach ( `$ace in `$ACL.access ) {
    if ( (`$ace.IdentityReference.Value -like '$($Issue.IdentityReference)' ) -and ( `$ace.ActiveDirectoryRights -notmatch '^ExtendedRight$') ) {
        `$ACL.RemoveAccessRule(`$ace) | Out-Null
    }
}
`$ACL.AddAccessRule(`$AutoEnrollRule)
Set-Acl -Path `$Path -AclObject `$ACL
"@

            }
            3 {
                $Issue.Fix = @"
`$Path = 'AD:$($Issue.DistinguishedName)'
`$ACL = Get-Acl -Path `$Path
`$IdentityReference = [System.Security.Principal.NTAccount]::New('$($Issue.IdentityReference)')
`$EnrollGuid = [System.Guid]::New('0e10c968-78fb-11d2-90d4-00c04f79dc55')
`$AutoEnrollGuid = [System.Guid]::New('a05b8cc2-17bc-4802-a710-e7c15ab866a2')
`$ExtendedRight = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
`$AccessType = [System.Security.AccessControl.AccessControlType]::Allow
`$InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
`$EnrollRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule `$IdentityReference, `$ExtendedRight, `$AccessType, `$EnrollGuid, `$InheritanceType
`$AutoEnrollRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule `$IdentityReference, `$ExtendedRight, `$AccessType, `$AutoEnrollGuid, `$InheritanceType
foreach ( `$ace in `$ACL.access ) {
    if ( (`$ace.IdentityReference.Value -like '$($Issue.IdentityReference)' ) -and ( `$ace.ActiveDirectoryRights -notmatch '^ExtendedRight$') ) {
        `$ACL.RemoveAccessRule(`$ace) | Out-Null
    }
}
`$ACL.AddAccessRule(`$EnrollRule)
`$ACL.AddAccessRule(`$AutoEnrollRule)
Set-Acl -Path `$Path -AclObject `$ACL
"@

            }
            4 {
                break 
            }
            5 {
                $Issue.Fix = @"
`$Path = 'AD:$($Issue.DistinguishedName)'
`$ACL = Get-Acl -Path `$Path
`$IdentityReference = [System.Security.Principal.NTAccount]::New('$($Issue.IdentityReference)')
`$EnrollGuid = [System.Guid]::New('0e10c968-78fb-11d2-90d4-00c04f79dc55')
`$AutoEnrollGuid = [System.Guid]::New('a05b8cc2-17bc-4802-a710-e7c15ab866a2')
`$ExtendedRight = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
`$AccessType = [System.Security.AccessControl.AccessControlType]::Allow
`$InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
`$EnrollRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule `$IdentityReference, `$ExtendedRight, `$AccessType, `$EnrollGuid, `$InheritanceType
`$AutoEnrollRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule `$IdentityReference, `$ExtendedRight, `$AccessType, `$AutoEnrollGuid, `$InheritanceType
foreach ( `$ace in `$ACL.access ) {
    if ( (`$ace.IdentityReference.Value -like '$($Issue.IdentityReference)' ) -and ( `$ace.ActiveDirectoryRights -notmatch '^ExtendedRight$') ) {
        `$ACL.RemoveAccessRule(`$ace) | Out-Null
    }
}
`$ACL.AddAccessRule(`$EnrollRule)
`$ACL.AddAccessRule(`$AutoEnrollRule)
Set-Acl -Path `$Path -AclObject `$ACL
"@

            }
        } # end switch ($RightsToRestore)
    } # end elseif ($Issue.Issue -match 'GenericAll')
}

<#
  Prerequisites: PowerShell version 2 or above.
  License: MIT
  Author: Michael Klement <mklement0@gmail.com>
  DOWNLOAD, from PowerShell version 3 or above:
    irm https://gist.github.com/mklement0/243ea8297e7db0e1c03a67ce4b1e765d/raw/Out-HostColored.ps1 | iex
  The above directly defines the function below in your session and offers guidance for making it available in future
  sessions too.
 
  Alternatively, download this file manually and dot-source it (e.g.: . /Out-HostColored.ps1)
  To learn what the function does:
    * see the next comment block
    * or, once downloaded, invoke the function with -? or pass its name to Get-Help.
 
#>


Function Write-HostColorized {
    <#
    .SYNOPSIS
    Colors portions of the default host output that match given patterns.
    .DESCRIPTION
    Colors portions of the default-formatted host output based on either
    regular expressions or literal substrings, assuming the host is a console or
    supports colored output using console colors.
    Matching is restricted to a single line at a time, but coloring multiple
    matches on a given line is supported.
    Two basic syntax forms are supported:
      * Single-color, via -Pattern, -ForegroundColor and -BackgroundColor
      * Multi-color (color per pattern), via a hashtable (dictionary) passed to
        -PatternColorMap.
    Note: Since output is sent to the host rather than the pipeline, you cannot
          chain calls to this function.
    .PARAMETER Pattern
    One or more search patterns specifying what parts of the formatted
    representations of the input objects should be colored.
     * By default, these patterns are interpreted as regular expressions.
     * If -SimpleMatch is also specified, the patterns are interpreted as literal
       substrings.
    .PARAMETER ForegroundColor
    The foreground color to use for the matching portions.
    Defaults to yellow.
    .PARAMETER BackgroundColor
    The optional background color to use for the matching portions.
    .PARAMETER PatternColorMap
    A hashtable (dictionary) with one or more entries in the following format:
      <pattern-or-pattern-array> = <color-spec>
    <pattern-or-pattern-array> is either a single string or an array of strings
    specifying the regex pattern(s) or literal substring(s) (with -SimpleMatch)
    to match.
    NOTE: If you're specifying an array literally, you must enclose it in (...) or
          @(...), and the individual patterns must all be quoted; e.g.:
            @('foo', 'bar')
    <color-spec> is a string that contains either a foreground [ConsoleColor]
    color alone (e.g. 'red'), a combination with a background color separated by ","
    (e.g., 'red,white') or just a background color (e.g, ',white').
    NOTE: If *multiple* patterns stored in a given hashtable may match on a given
          line and you want the *first* matching pattern to "win" predictably, be
          sure to pass an [ordered] hashtable ([ordered] @{ Foo = 'red; ... })
    See the examples for a complete example.
    .PARAMETER CaseSensitive
    Matches the patterns case-sensitively.
    By default, matching is case-insensitive.
    .PARAMETER WholeLine
    Specifies that the entire line containing a match should be colored,
    not just the matching portion.
    .PARAMETER SimpleMatch
    Interprets the -Pattern argument(s) as a literal substrings to match rather
    than as regular expressions.
    .PARAMETER InputObject
    The input object(s) whose formatted representations to color selectively.
    Typically provided via the pipeline.
    .EXAMPLE
    'A fool and his money', 'foo bar' | Out-HostColored foo
    Prints the substring 'foo' in yellow in the two resulting output lines.
    .EXAMPLE
    Get-Date | Out-HostColored '\p{L}+' red white
    Outputs the current date with all tokens composed of letters (p{L}) only in red
    on a white background.
    .EXAMPLE
    Get-Date | Out-HostColored @{ '\p{L}+' = 'red,white' }
    Same as the previous example, only via the dictionary-based -PatternColorMap
    parameter (implied).
    .EXAMPLE
    'It ain''t easy being green.' | Out-HostColored @{ ('easy', 'green') = 'green'; '\bbe.+?\b' = 'black,yellow' }
    Prints the words 'easy' and 'green' in green, and the word 'being' in black on yellow.
    Note the need to enclose pattern array 'easy', 'green' in (...), which also necessitates
    quoting its element.
    .EXAMPLE
    Get-ChildItem | select Name | Out-HostColored -WholeLine -SimpleMatch .txt
    Highlight all text file names in green.
    .EXAMPLE
    'apples', 'kiwi', 'pears' | Out-HostColored '^a', 's$' blue
    Highlight all "A"s at the beginning and "S"s at the end of lines in blue.
    #>


    # === IMPORTANT:
    # * At least for now, we remain PSv2-COMPATIBLE.
    # * Thus:
    # * no `[ordered]`, `::new()`, `[pscustomobject]`, ...
    # * No implicit Boolean properties in [CmdletBinding()] and [Parameter()] attributes (`Mandatory = $true` instead of just `Mandatory`)
    # ===

    [CmdletBinding(DefaultParameterSetName = 'SingleColor')]
    param(
        [Parameter(ParameterSetName = 'SingleColor', Position = 0, Mandatory = $True)] [string[]] $Pattern,
        [Parameter(ParameterSetName = 'SingleColor', Position = 1)] [ConsoleColor] $ForegroundColor = [ConsoleColor]::Yellow,
        [Parameter(ParameterSetName = 'SingleColor', Position = 2)] [ConsoleColor] $BackgroundColor,
        [Parameter(ParameterSetName = 'PerPatternColor', Position = 0, Mandatory = $True)] [System.Collections.IDictionary] $PatternColorMap,
        [Parameter(ValueFromPipeline = $True)] $InputObject,
        [switch] $WholeLine,
        [switch] $SimpleMatch,
        [switch] $CaseSensitive
    )

    begin {

        Set-StrictMode -Version 1

        if ($PSCmdlet.ParameterSetName -eq 'SingleColor') {

            # Translate the indiv. arguments into the dictionary format suppoorted
            # by -PatternColorMap, so we can process $PatternColorMap uniformly below.
            $PatternColorMap = @{
                $Pattern = $ForegroundColor, $BackgroundColor
            }
        }
        # Otherwise: $PSCmdlet.ParameterSetName -eq 'PerPatternColor', i.e. a dictionary
        # mapping patterns to colors was direclty passed in $PatternColorMap

        try {

            # The options for the [regex] instances to create.
            # We precompile them for better performance with many input objects.
            [System.Text.RegularExpressions.RegexOptions] $reOpts =
            if ($CaseSensitive) {
                'Compiled, ExplicitCapture' 
            }
            else {
                'Compiled, ExplicitCapture, IgnoreCase' 
            }

            # Transform the dictionary:
            # * Keys: Consolidate multiple patterns into a single one with alternation and
            # construct a [regex] instance from it.
            # * Values: Transform the "[foregroundColor],[backgroundColor]" strings into an arguments
            # hashtable that can be used for splatting with Write-Host.
            $map = [ordered] @{ } # !! For stable results in repeated enumerations, use [ordered].
            # !! This matters when multiple patterns match on a given line, and also requires the
            # !! *caller* to pass an [ordered] hashtable to -PatternColorMap
            foreach ($entry in $PatternColorMap.GetEnumerator()) {

                # Create a Write-Host color-arguments hashtable for splatting.
                if ($entry.Value -is [array]) {
                    $fg, $bg = $entry.Value # [ConsoleColor[]], from the $PSCmdlet.ParameterSetName -eq 'SingleColor' case.
                }
                else {
                    $fg, $bg = $entry.Value -split ','
                }
                $colorArgs = @{ }
                if ($fg) {
                    $colorArgs['ForegroundColor'] = [ConsoleColor] $fg 
                }
                if ($bg) {
                    $colorArgs['BackgroundColor'] = [ConsoleColor] $bg 
                }

                # Consolidate the patterns into a single pattern with alternation ('|'),
                # escape the patterns if -SimpleMatch was passsed.
                $re = New-Object regex -Args `
                $(if ($SimpleMatch) {
                  ($entry.Key | ForEach-Object { [regex]::Escape($_) }) -join '|'
                    }
                    else {
                  ($entry.Key | ForEach-Object { '({0})' -f $_ }) -join '|'
                    }),
                $reOpts

                # Add the tansformed entry.
                $map[$re] = $colorArgs
            }
        }
        catch {
            throw 
        }

        # Construct the arguments to pass to Out-String.
        $htArgs = @{ Stream = $True }
        if ($PSBoundParameters.ContainsKey('InputObject')) {
            # !! Do not use `$null -eq $InputObject`, because PSv2 doesn't create this variable if the parameter wasn't bound.
            $htArgs.InputObject = $InputObject
        }

        # Construct the script block that is used in the steppable pipeline created
        # further below.
        $scriptCmd = {

            # Format the input objects with Out-String and output the results line
            # by line, then look for matches and color them.
            & $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Out-String', 'Cmdlet') @htArgs | ForEach-Object {

                # Match the input line against all regexes and collect the results.
                $matchInfos = :patternLoop foreach ($entry in $map.GetEnumerator()) {
                    foreach ($m in $entry.Key.Matches($_)) {
                        @{ Index = $m.Index; Text = $m.Value; ColorArgs = $entry.Value }
                        if ($WholeLine) {
                            break patternLoop 
                        }
                    }
                }

                # # Activate this for debugging.
                # $matchInfos | Sort-Object { $_.Index } | Out-String | Write-Verbose -vb

                if (-not $matchInfos) {
                    # No match found - output uncolored.
                    Write-Host -NoNewline $_
                }
                elseif ($WholeLine) {
                    # Whole line should be colored: Use the first match's color
                    $colorArgs = $matchInfos.ColorArgs
                    Write-Host -NoNewline @colorArgs $_
                }
                else {
                    # Parts of the line must be colored:
                    # Process the matches in ascending order of start position.
                    $offset = 0
                    foreach ($mi in $matchInfos | Sort-Object { $_.Index }) {
                        # !! Use of a script-block parameter is REQUIRED in WinPSv5.1-, because hashtable entries cannot be referred to like properties, unlinke in PSv7+
                        if ($mi.Index -lt $offset) {
                            # Ignore subsequent matches that overlap with previous ones whose colored output was already produced.
                            continue
                        }
                        elseif ($offset -lt $mi.Index) {
                            # Output the part *before* the match uncolored.
                            Write-Host -NoNewline $_.Substring($offset, $mi.Index - $offset)
                        }
                        $offset = $mi.Index + $mi.Text.Length
                        # Output the match at hand colored.
                        $colorArgs = $mi.ColorArgs
                        Write-Host -NoNewline @colorArgs $mi.Text
                    }
                    # Print any remaining part of the line uncolored.
                    if ($offset -lt $_.Length) {
                        Write-Host -NoNewline $_.Substring($offset)
                    }
                }
                Write-Host '' # Terminate the current output line with a newline - this also serves to reset the console's colors on Unix.
            }
        }

        # Create the script block as a *steppable pipeline*, which enables
        # to perform regular streaming pipeline processing, without having to collect
        # everything in memory first.
        $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $steppablePipeline.Begin($PSCmdlet)
    } # begin

    process {
        $steppablePipeline.Process($_)
    }

    end {
        $steppablePipeline.End()
    }
}

function Invoke-Locksmith {
    <#
    .SYNOPSIS
    Finds the most common malconfigurations of Active Directory Certificate Services (AD CS).
 
    .DESCRIPTION
    Locksmith uses the Active Directory (AD) Powershell (PS) module to identify 10 misconfigurations
    commonly found in Enterprise mode AD CS installations.
 
    .COMPONENT
    Locksmith requires the AD PS module to be installed in the scope of the Current User.
    If Locksmith does not identify the AD PS module as installed, it will attempt to
    install the module. If module installation does not complete successfully,
    Locksmith will fail.
 
    .PARAMETER Mode
    Specifies sets of common script execution modes.
 
    -Mode 0
    Finds any malconfigurations and displays them in the console.
    No attempt is made to fix identified issues.
 
    -Mode 1
    Finds any malconfigurations and displays them in the console.
    Displays example Powershell snippet that can be used to resolve the issue.
    No attempt is made to fix identified issues.
 
    -Mode 2
    Finds any malconfigurations and writes them to a series of CSV files.
    No attempt is made to fix identified issues.
 
    -Mode 3
    Finds any malconfigurations and writes them to a series of CSV files.
    Creates code snippets to fix each issue and writes them to an environment-specific custom .PS1 file.
    No attempt is made to fix identified issues.
 
    -Mode 4
    Finds any malconfigurations and creates code snippets to fix each issue.
    Attempts to fix all identified issues. This mode may require high-privileged access.
 
    .PARAMETER Scans
    Specify which scans you want to run. Available scans: 'All' or Auditing, ESC1, ESC2, ESC3, ESC4, ESC5, ESC6, ESC8, or 'PromptMe'
 
    -Scans All
    Run all scans (default).
 
    -Scans PromptMe
    Presents a grid view of the available scan types that can be selected and run them after you click OK.
 
    .PARAMETER OutputPath
    Specify the path where you want to save reports and mitigation scripts.
 
    .INPUTS
    None. You cannot pipe objects to Invoke-Locksmith.ps1.
 
    .OUTPUTS
    Output types:
    1. Console display of identified issues.
    2. Console display of identified issues and their fixes.
    3. CSV containing all identified issues.
    4. CSV containing all identified issues and their fixes.
 
    .EXAMPLE
    Invoke-Locksmith -Mode 0 -Scans All -OutputPath 'C:\Temp'
 
    Finds all malconfigurations and displays them in the console.
 
    .EXAMPLE
    Invoke-Locksmith -Mode 2 -Scans All -OutputPath 'C:\Temp'
 
    Finds all malconfigurations and displays them in the console. The findings are saved in a CSV file in C:\Temp.
 
    .NOTES
    The Windows PowerShell cmdlet Restart-Service requires RunAsAdministrator.
    #>


    [CmdletBinding(HelpUri = 'https://trimarcjake.github.io/Locksmith/Invoke-Locksmith')]
    param (
        #[string]$Forest, # Not used yet
        #[string]$InputPath, # Not used yet

        # The mode to run Locksmith in. Defaults to 0.
        [Parameter()]
        [ValidateSet(0, 1, 2, 3, 4)]
        [int]$Mode = 0,

        # The scans to run. Defaults to 'All'.
        [Parameter()]
        [ValidateSet('Auditing',
            'ESC1',
            'ESC2',
            'ESC3',
            'ESC4',
            'ESC5',
            'ESC6',
            'ESC8',
            'ESC11',
            'ESC13',
            'ESC15',
            'EKUwu',
            'All',
            'PromptMe'
        )]
        [array]$Scans = 'All',

        # The directory to save the output in (defaults to the current working directory).
        [Parameter()]
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        [string]$OutputPath = $PWD,

        # The credential to use for working with ADCS.
        [Parameter()]
        [System.Management.Automation.PSCredential]$Credential
    )

    $Version = '2025.1.1'
    $LogoPart1 = @'
    _ _____ _______ _ _ _______ _______ _____ _______ _ _
    | | | | |____/ |______ | | | | | |_____|
    |_____ |_____| |_____ | \_ ______| | | | __|__ | | |
'@

    $LogoPart2 = @'
        .--. .--. .--.
       /.-. '----------. /.-. '----------. /.-. '----------.
       \'-' .---'-''-'-' \'-' .--'--''-'-' \'-' .--'--'-''-'
        '--' '--' '--'
'@

    $VersionBanner = " v$Version"

    Write-Host $LogoPart1 -ForegroundColor Magenta
    Write-Host $LogoPart2 -ForegroundColor White
    Write-Host $VersionBanner -ForegroundColor Red

    # Check if ActiveDirectory PowerShell module is available, and attempt to install if not found
    $RSATInstalled = Test-IsRSATInstalled
    if ($RSATInstalled) {
        # Continue
    }
    else {
        Install-RSATADPowerShell
    }

    # Exit if running in restricted admin mode without explicit credentials
    if (!$Credential -and (Get-RestrictedAdminModeSetting)) {
        Write-Warning "Restricted Admin Mode appears to be in place, re-run with the '-Credential domain\user' option"
        break
    }

    ### Initial variables
    # For output filenames
    [string]$FilePrefix = "Locksmith $(Get-Date -Format 'yyyy-MM-dd hh-mm-ss')"

    # Extended Key Usages for client authentication. A requirement for ESC1, ESC3 Condition 2, and ESC13
    $ClientAuthEKUs = '1\.3\.6\.1\.5\.5\.7\.3\.2|1\.3\.6\.1\.5\.2\.3\.4|1\.3\.6\.1\.4\.1\.311\.20\.2\.2|2\.5\.29\.37\.0'

    # GenericAll, WriteDacl, and WriteOwner all permit full control of an AD object.
    # WriteProperty may or may not permit full control depending the specific property and AD object type.
    $DangerousRights = 'GenericAll|WriteDacl|WriteOwner|WriteProperty'

    # Extended Key Usage for client authentication. A requirement for ESC3.
    $EnrollmentAgentEKU = '1\.3\.6\.1\.4\.1\.311\.20\.2\.1'

    # The well-known GUIDs for Enroll and AutoEnroll rights on AD CS templates.
    $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2'

    <#
        -519$ = Enterprise Admins group
    #>

    $SafeOwners = '-519$'

    <#
        -512$ = Domain Admins group
        -519$ = Enterprise Admins group
        -544$ = Administrators group
        -18$ = SYSTEM
        -517$ = Cert Publishers
        -500$ = Built-in Administrator
        -516$ = Domain Controllers
        -521$ = Read-Only Domain Controllers
        -9$ = Enterprise Domain Controllers
        -498$ = Enterprise Read-Only Domain Controllers
        -526$ = Key Admins
        -527$ = Enterprise Key Admins
        S-1-5-10 = SELF
    #>

    $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'

    <#
        S-1-0-0 = NULL SID
        S-1-1-0 = Everyone
        S-1-5-7 = Anonymous Logon
        S-1-5-32-545 = BUILTIN\Users
        S-1-5-11 = Authenticated Users
        -513$ = Domain Users
        -515$ = Domain Computers
    #>

    $UnsafeUsers = 'S-1-0-0|S-1-1-0|S-1-5-7|S-1-5-32-545|S-1-5-11|-513$|-515$'

    ### Generated variables
    # $Dictionary = New-Dictionary

    $Forest = Get-ADForest
    $ForestGC = $(Get-ADDomainController -Discover -Service GlobalCatalog -ForceDiscover | Select-Object -ExpandProperty Hostname) + ':3268'
    # $DNSRoot = [string]($Forest.RootDomain | Get-ADDomain).DNSRoot
    $EnterpriseAdminsSID = ([string]($Forest.RootDomain | Get-ADDomain).DomainSID) + '-519'
    $PreferredOwner = [System.Security.Principal.SecurityIdentifier]::New($EnterpriseAdminsSID)
    # $DomainSIDs = $Forest.Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value }

    # Add SIDs of (probably) Safe Users to $SafeUsers
    Get-ADGroupMember $EnterpriseAdminsSID | ForEach-Object {
        $SafeUsers += '|' + $_.SID.Value
    }

    $Forest.Domains | ForEach-Object {
        $DomainSID = (Get-ADDomain $_).DomainSID.Value
        <#
            -517 = Cert Publishers
            -512 = Domain Admins group
        #>

        $SafeGroupRIDs = @('-517', '-512')

        # Administrators group
        $SafeGroupSIDs = @('S-1-5-32-544')
        foreach ($rid in $SafeGroupRIDs ) {
            $SafeGroupSIDs += $DomainSID + $rid
        }
        foreach ($sid in $SafeGroupSIDs) {
            $users += (Get-ADGroupMember $sid -Server $_ -Recursive).SID.Value
        }
        foreach ($user in $users) {
            $SafeUsers += '|' + $user
        }
    }
    $SafeUsers = $SafeUsers.Replace('||', '|')

    if ($Credential) {
        $Targets = Get-Target -Credential $Credential
    }
    else {
        $Targets = Get-Target
    }

    Write-Host "Gathering AD CS Objects from $($Targets)..."
    if ($Credential) {
        $ADCSObjects = Get-ADCSObject -Targets $Targets -Credential $Credential
        Set-AdditionalCAProperty -ADCSObjects $ADCSObjects -Credential $Credential -ForestGC $ForestGC
        $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects -Credential $Credential -ForestGC $ForestGC
        $ADCSObjects += $CAHosts
    }
    else {
        $ADCSObjects = Get-ADCSObject -Targets $Targets
        Set-AdditionalCAProperty -ADCSObjects $ADCSObjects -ForestGC $ForestGC
        $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects -ForestGC $ForestGC
        $ADCSObjects += $CAHosts
    }

    Set-AdditionalTemplateProperty -ADCSObjects $ADCSObjects

    # Add SIDs of CA Hosts to $SafeUsers
    $CAHosts | ForEach-Object { $SafeUsers += '|' + $_.objectSid }

    #if ( $Scans ) {
    # If the Scans parameter was used, Invoke-Scans with the specified checks.
    $ScansParameters = @{
        ADCSObjects        = $ADCSObjects
        ClientAuthEkus     = $ClientAuthEKUs
        DangerousRights    = $DangerousRights
        EnrollmentAgentEKU = $EnrollmentAgentEKU
        Mode               = $Mode
        SafeObjectTypes    = $SafeObjectTypes
        SafeOwners         = $SafeOwners
        SafeUsers          = $SafeUsers
        Scans              = $Scans
        UnsafeUsers        = $UnsafeUsers
        PreferredOwner     = $PreferredOwner
    }
    $Results = Invoke-Scans @ScansParameters
    # Re-hydrate the findings arrays from the Results hash table
    $AllIssues = $Results['AllIssues']
    $AuditingIssues = $Results['AuditingIssues']
    $ESC1 = $Results['ESC1']
    $ESC2 = $Results['ESC2']
    $ESC3 = $Results['ESC3']
    $ESC4 = $Results['ESC4']
    $ESC5 = $Results['ESC5']
    $ESC6 = $Results['ESC6']
    $ESC8 = $Results['ESC8']
    $ESC11 = $Results['ESC11']
    $ESC13 = $Results['ESC13']
    $ESC15 = $Results['ESC15']

    # If these are all empty = no issues found, exit
    if ($null -eq $Results) {
        Write-Host "`n$(Get-Date) : No ADCS issues were found.`n" -ForegroundColor Green
        Write-Host 'Thank you for using ' -NoNewline
        Write-Host "❤ Locksmith ❤ `n" -ForegroundColor Magenta
        break
    }

    switch ($Mode) {
        0 {
            Format-Result -Issue $AuditingIssues -Mode 0
            Format-Result -Issue $ESC1 -Mode 0
            Format-Result -Issue $ESC2 -Mode 0
            Format-Result -Issue $ESC3 -Mode 0
            Format-Result -Issue $ESC4 -Mode 0
            Format-Result -Issue $ESC5 -Mode 0
            Format-Result -Issue $ESC6 -Mode 0
            Format-Result -Issue $ESC8 -Mode 0
            Format-Result -Issue $ESC11 -Mode 0
            Format-Result -Issue $ESC13 -Mode 0
            Format-Result -Issue $ESC15 -Mode 0
            Write-Host @"
[!] You ran Locksmith in Mode 0 which only provides an high-level overview of issues
identified in the environment. For more details including:
 
  - DistinguishedName of impacted object(s)
  - Remediation guidance and/or code
  - Revert guidance and/or code (in case remediation breaks something!)
 
Run Locksmith in Mode 1!
 
# Module version
Invoke-Locksmith -Mode 1
 
# Script version
.\Invoke-Locksmith.ps1 -Mode 1`n
"@
 -ForegroundColor Yellow
        }
        1 {
            Format-Result -Issue $AuditingIssues -Mode 1
            Format-Result -Issue $ESC1 -Mode 1
            Format-Result -Issue $ESC2 -Mode 1
            Format-Result -Issue $ESC3 -Mode 1
            Format-Result -Issue $ESC4 -Mode 1
            Format-Result -Issue $ESC5 -Mode 1
            Format-Result -Issue $ESC6 -Mode 1
            Format-Result -Issue $ESC8 -Mode 1
            Format-Result -Issue $ESC11 -Mode 1
            Format-Result -Issue $ESC13 -Mode 1
            Format-Result -Issue $ESC15 -Mode 1
        }
        2 {
            $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSIssues.CSV"
            Write-Host "Writing AD CS issues to $Output..."
            try {
                $AllIssues | Select-Object Forest, Technique, Name, Issue | Export-Csv -NoTypeInformation $Output
                Write-Host "$Output created successfully!`n"
            }
            catch {
                Write-Host 'Ope! Something broke.'
            }
        }
        3 {
            $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSRemediation.CSV"
            Write-Host "Writing AD CS issues to $Output..."
            try {
                $AllIssues | Select-Object Forest, Technique, Name, DistinguishedName, Issue, Fix | Export-Csv -NoTypeInformation $Output
                Write-Host "$Output created successfully!`n"
            }
            catch {
                Write-Host 'Ope! Something broke.'
            }
        }
        4 {
            $params = @{
                AuditingIssues = $AuditingIssues
                ESC1           = $ESC1
                ESC2           = $ESC2
                ESC3           = $ESC3
                ESC4           = $ESC4
                ESC5           = $ESC5
                ESC6           = $ESC6
                ESC11          = $ESC11
                ESC13          = $ESC13
            }
            Invoke-Remediation @params
        }
    }
    Write-Host 'Thank you for using ' -NoNewline
    Write-Host "Locksmith <3`n" -ForegroundColor Magenta
}


# Export functions and aliases as required
Export-ModuleMember -Function @('Invoke-Locksmith') -Alias @()