Dargslan.PrintNightmareAudit.psm1

<#
.SYNOPSIS
    Audit Windows Print Spooler exposure (PrintNightmare): spooler state, Point-and-Print restrictions, unsigned drivers, package install policy.

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

.LINK
    https://dargslan.com

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


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


function Get-DargslanPrintSpoolerStatus {
    <#
    .SYNOPSIS
        Return spooler service state and shared printer count.
    #>

    [CmdletBinding()]
    param()
    $svc = Get-Service -Name Spooler
    $shared = @(Get-Printer -ErrorAction SilentlyContinue | Where-Object Shared)
    $isDc = (Get-WmiObject Win32_ComputerSystem).DomainRole -ge 4
    [pscustomobject]@{
        ComputerName    = $env:COMPUTERNAME
        SpoolerStatus   = $svc.Status
        SpoolerStartType= $svc.StartType
        IsDomainController = $isDc
        SharedPrinters  = $shared.Count
    }
}

function Get-DargslanPrintNightmareConfig {
    <#
    .SYNOPSIS
        Read PrintNightmare-relevant policy keys.
    #>

    [CmdletBinding()]
    param()
    $pp = 'HKLM:\\Software\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint'
    $nh = 'HKLM:\\Software\\Policies\\Microsoft\\Windows NT\\Printers'
    $ppk = if (Test-Path $pp) { Get-ItemProperty $pp } else { $null }
    $nhk = if (Test-Path $nh) { Get-ItemProperty $nh } else { $null }
    [pscustomobject]@{
        NoWarningNoElevationOnInstall   = $ppk.NoWarningNoElevationOnInstall
        UpdatePromptSettings            = $ppk.UpdatePromptSettings
        InForest                         = $ppk.InForest
        TrustedServers                   = $ppk.TrustedServers
        ServerList                       = $ppk.ServerList
        RestrictDriverInstallationToAdministrators = $nhk.RestrictDriverInstallationToAdministrators
    }
}

function Get-DargslanPrinterDrivers {
    <#
    .SYNOPSIS
        List installed printer drivers and check vendor + signature.
    #>

    [CmdletBinding()]
    param()
    Get-PrinterDriver -ErrorAction SilentlyContinue | ForEach-Object {
        $inf = $_.InfPath
        $signed = if ($inf -and (Test-Path $inf)) {
            (Get-AuthenticodeSignature $inf -ErrorAction SilentlyContinue).Status
        } else { 'Unknown' }
        [pscustomobject]@{
            Name    = $_.Name
            Manufacturer = $_.Manufacturer
            DriverVersion = $_.DriverVersion
            InfPath = $inf
            SignatureStatus = $signed
        }
    }
}

function Get-DargslanPrintNightmareAuditReport {
    <#
    .SYNOPSIS
        Combined report with PASS / WARN / FAIL verdict.
    #>

    [CmdletBinding()]
    param()
    $st  = Get-DargslanPrintSpoolerStatus
    $cfg = Get-DargslanPrintNightmareConfig
    $drv = @(Get-DargslanPrinterDrivers)
    $unsig = @($drv | Where-Object { $_.SignatureStatus -ne 'Valid' -and $_.SignatureStatus -ne 'Unknown' })
    $score = 0
    if ($cfg.RestrictDriverInstallationToAdministrators -eq 1) { $score++ }
    if ($cfg.NoWarningNoElevationOnInstall -ne 1) { $score++ }
    if ($cfg.UpdatePromptSettings -ne 1) { $score++ }
    if ($st.IsDomainController -and $st.SpoolerStatus -ne 'Running') { $score++ } elseif (-not $st.IsDomainController) { $score++ }
    if ($unsig.Count -eq 0) { $score++ }
    $verdict = if ($score -ge 4) { 'PASS' } elseif ($score -ge 2) { 'WARN' } else { 'FAIL' }
    [pscustomobject]@{
        ComputerName    = $env:COMPUTERNAME
        Spooler         = $st
        Config          = $cfg
        Drivers         = $drv
        UnsignedDrivers = $unsig
        Score           = $score
        Verdict         = $verdict
        TimeStamp       = (Get-Date).ToString('s')
    }
}

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

    [CmdletBinding()]
    param([string]$OutDir = (Join-Path $env:TEMP 'DargslanPrintAudit'))
    if (-not (Test-Path $OutDir)) { New-Item -Type Directory -Path $OutDir | Out-Null }
    $r = Get-DargslanPrintNightmareAuditReport
    $json = Join-Path $OutDir ('print-' + $env:COMPUTERNAME + '.json')
    $html = Join-Path $OutDir ('print-' + $env:COMPUTERNAME + '.html')
    $r | ConvertTo-Json -Depth 6 | Set-Content $json -Encoding UTF8
    $body  = "<h1>PrintNightmare Audit - $($r.ComputerName)</h1>"
    $body += "<p>Verdict: <b>$($r.Verdict)</b> ($($r.Score)/5)</p>"
    $body += '<h2>Spooler</h2>' + ($r.Spooler | ConvertTo-Html -Fragment)
    $body += '<h2>Policy</h2>'  + ($r.Config  | ConvertTo-Html -Fragment)
    $body += '<h2>Drivers</h2>' + ($r.Drivers | ConvertTo-Html -Fragment)
    ConvertTo-Html -Body $body -Title 'PrintNightmare Audit' | Set-Content $html -Encoding UTF8
    [pscustomobject]@{ Json = $json; Html = $html; Verdict = $r.Verdict }
}