Dargslan.KerberosAudit.psm1

<#
.SYNOPSIS
    Audit Kerberos posture in Active Directory: unconstrained delegation, duplicate SPNs and AS-REP roastable accounts.

.DESCRIPTION
    Part of the Dargslan Windows Admin Tools collection.
    Free Cheat Sheet: https://dargslan.com/cheat-sheets/kerberos-delegation-audit-2026
    Full Guide: https://dargslan.com/blog/kerberos-delegation-audit-powershell-2026
    More tools: https://dargslan.com

.LINK
    https://dargslan.com

.LINK
    https://github.com/Dargslan/powershell-admin-scripts
#>


$script:Banner = @"
+----------------------------------------------------------+
| Dargslan AD Kerberos Audit
| https://dargslan.com - Free cheat sheets & eBooks |
+----------------------------------------------------------+
"@


function Get-DargslanUnconstrainedDelegation {
    <#
    .SYNOPSIS
        Find user / computer accounts with TRUSTED_FOR_DELEGATION (unconstrained).
    #>

    [CmdletBinding()]
    param()
    Import-Module ActiveDirectory -ErrorAction Stop
    $users = Get-ADUser -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=524288)' -Properties userAccountControl |
        Select SamAccountName, @{N='Type';E={'User'}}
    $comps = Get-ADComputer -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=524288)' -Properties userAccountControl, OperatingSystem |
        Select Name, OperatingSystem, @{N='Type';E={'Computer'}}, @{N='SamAccountName';E={$_.Name + '$'}}
    $users + $comps
}

function Get-DargslanConstrainedDelegation {
    <#
    .SYNOPSIS
        Find accounts with constrained / Resource-Based delegation.
    #>

    [CmdletBinding()]
    param()
    Import-Module ActiveDirectory -ErrorAction Stop
    Get-ADObject -LDAPFilter '(|(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))' -Properties msDS-AllowedToDelegateTo, msDS-AllowedToActOnBehalfOfOtherIdentity, sAMAccountName, objectClass |
        Select sAMAccountName, objectClass,
            @{N='AllowedToDelegateTo';E={($_.'msDS-AllowedToDelegateTo' -join ', ')}},
            @{N='RBCD';E={[bool]$_.'msDS-AllowedToActOnBehalfOfOtherIdentity'}}
}

function Get-DargslanDuplicateSpns {
    <#
    .SYNOPSIS
        Find duplicate Service Principal Names across all accounts.
    #>

    [CmdletBinding()]
    param()
    Import-Module ActiveDirectory -ErrorAction Stop
    $all = Get-ADObject -LDAPFilter '(servicePrincipalName=*)' -Properties servicePrincipalName, sAMAccountName |
        ForEach-Object {
            $name = $_.sAMAccountName
            $_.servicePrincipalName | ForEach-Object {
                [pscustomobject]@{ SPN = $_; Account = $name }
            }
        }
    $all | Group-Object SPN | Where-Object Count -gt 1 |
        Select-Object Name, Count, @{N='Accounts';E={($_.Group.Account -join ', ')}}
}

function Get-DargslanAsRepRoastable {
    <#
    .SYNOPSIS
        Find accounts with DONT_REQ_PREAUTH (AS-REP roastable).
    #>

    [CmdletBinding()]
    param()
    Import-Module ActiveDirectory -ErrorAction Stop
    Get-ADUser -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' -Properties userAccountControl, PasswordLastSet |
        Select SamAccountName, Enabled, PasswordLastSet
}

function Get-DargslanKerberosAuditReport {
    <#
    .SYNOPSIS
        Combined Kerberos audit with PASS / WARN / FAIL verdict.
    #>

    [CmdletBinding()]
    param()
    $un  = @(Get-DargslanUnconstrainedDelegation)
    $cn  = @(Get-DargslanConstrainedDelegation)
    $dup = @(Get-DargslanDuplicateSpns)
    $asr = @(Get-DargslanAsRepRoastable)
    $score = 0
    if ($un.Count -le 1)   { $score++ }
    if ($dup.Count -eq 0)  { $score++ }
    if ($asr.Count -eq 0)  { $score++ }
    $verdict = if ($score -eq 3) { 'PASS' } elseif ($score -ge 1) { 'WARN' } else { 'FAIL' }
    [pscustomobject]@{
        Unconstrained         = $un
        Constrained           = $cn
        DuplicateSpns         = $dup
        AsRepRoastable        = $asr
        UnconstrainedCount    = $un.Count
        DuplicateSpnCount     = $dup.Count
        AsRepRoastableCount   = $asr.Count
        Score                 = $score
        Verdict               = $verdict
        TimeStamp             = (Get-Date).ToString('s')
    }
}

function Export-DargslanKerberosAuditReport {
    <#
    .SYNOPSIS
        Export the Kerberos audit to HTML and JSON.
    #>

    [CmdletBinding()]
    param([string]$OutDir = (Join-Path $env:TEMP 'DargslanKerberosAudit'))
    if (-not (Test-Path $OutDir)) { New-Item -Type Directory -Path $OutDir | Out-Null }
    $r = Get-DargslanKerberosAuditReport
    $json = Join-Path $OutDir 'kerberos-audit.json'
    $html = Join-Path $OutDir 'kerberos-audit.html'
    $r | ConvertTo-Json -Depth 6 | Set-Content $json -Encoding UTF8
    $body  = '<h1>Kerberos Audit</h1>'
    $body += "<p>Verdict: <b>$($r.Verdict)</b> ($($r.Score)/3)</p>"
    $body += '<h2>Unconstrained delegation</h2>' + ($r.Unconstrained | ConvertTo-Html -Fragment)
    $body += '<h2>Constrained / RBCD</h2>' + ($r.Constrained | ConvertTo-Html -Fragment)
    $body += '<h2>Duplicate SPNs</h2>'  + ($r.DuplicateSpns | ConvertTo-Html -Fragment)
    $body += '<h2>AS-REP roastable</h2>' + ($r.AsRepRoastable | ConvertTo-Html -Fragment)
    ConvertTo-Html -Body $body -Title 'Kerberos Audit' | Set-Content $html -Encoding UTF8
    [pscustomobject]@{ Json = $json; Html = $html; Verdict = $r.Verdict }
}