Dargslan.WinPersistenceAudit.psm1

<#
.SYNOPSIS
    Audit Windows persistence locations: registry Run keys, scheduled tasks and unsigned autorun binaries. JSON / HTML report.

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

.LINK
    https://dargslan.com

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


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


function Get-DargslanRunKeys {
    <#
    .SYNOPSIS
        Read every Run / RunOnce registry key for HKLM and HKCU.
    #>

    [CmdletBinding()]
    param()
    $keys = @(
        'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run',
        'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce',
        'HKLM:\\Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Run',
        'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run',
        'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'
    )
    foreach ($k in $keys) {
        if (-not (Test-Path $k)) { continue }
        $item = Get-ItemProperty -Path $k
        $item.PSObject.Properties |
            Where-Object { $_.Name -notmatch '^PS' -and $_.Name -notin 'PSPath','PSParentPath','PSChildName','PSDrive','PSProvider' } |
            ForEach-Object {
                [pscustomobject]@{
                    Hive    = $k
                    Name    = $_.Name
                    Command = $_.Value
                }
            }
    }
}

function Get-DargslanScheduledTasks {
    <#
    .SYNOPSIS
        Return every enabled scheduled task with author, run-as account and action.
    #>

    [CmdletBinding()]
    param()
    Get-ScheduledTask | Where-Object State -ne 'Disabled' | ForEach-Object {
        $info = Get-ScheduledTaskInfo -TaskName $_.TaskName -TaskPath $_.TaskPath
        [pscustomobject]@{
            TaskName  = $_.TaskName
            Path      = $_.TaskPath
            State     = $_.State
            Author    = $_.Author
            RunAs     = $_.Principal.UserId
            RunLevel  = $_.Principal.RunLevel
            Action    = ($_.Actions | ForEach-Object { $_.Execute + ' ' + $_.Arguments }) -join ';'
            LastRun   = $info.LastRunTime
            NextRun   = $info.NextRunTime
        }
    }
}

function Get-DargslanUnsignedAutoruns {
    <#
    .SYNOPSIS
        Classify Run-key + scheduled-task binaries by Authenticode signature.
    #>

    [CmdletBinding()]
    param()
    $paths = @()
    Get-DargslanRunKeys | ForEach-Object {
        $cmd = $_.Command -replace '"',''
        $bin = ($cmd -split ' ')[0]
        if (Test-Path $bin) { $paths += [pscustomobject]@{ Source = 'RunKey'; Binary = $bin } }
    }
    Get-DargslanScheduledTasks | ForEach-Object {
        $bin = ($_.Action -split ' ')[0] -replace '"',''
        if (Test-Path $bin) { $paths += [pscustomobject]@{ Source = 'Task'; Binary = $bin } }
    }
    $paths | Sort Binary -Unique | ForEach-Object {
        $sig = Get-AuthenticodeSignature $_.Binary -ErrorAction SilentlyContinue
        [pscustomobject]@{
            Source   = $_.Source
            Binary   = $_.Binary
            Status   = $sig.Status
            Signer   = $sig.SignerCertificate.Subject
        }
    }
}

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

    [CmdletBinding()]
    param()
    $run    = @(Get-DargslanRunKeys)
    $tasks  = @(Get-DargslanScheduledTasks)
    $auto   = @(Get-DargslanUnsignedAutoruns)
    $unsig  = @($auto | Where-Object Status -ne 'Valid')
    $nonMs  = @($auto | Where-Object { $_.Signer -and $_.Signer -notmatch 'Microsoft' })
    $score = 0
    if ($unsig.Count -le 2) { $score++ }
    if ($nonMs.Count -le 10) { $score++ }
    if ($run.Count -gt 0)    { $score++ }
    $verdict = if ($score -eq 3) { 'PASS' } elseif ($score -ge 1) { 'WARN' } else { 'FAIL' }
    [pscustomobject]@{
        ComputerName = $env:COMPUTERNAME
        RunKeyCount  = $run.Count
        TaskCount    = $tasks.Count
        UnsignedCount= $unsig.Count
        NonMsCount   = $nonMs.Count
        RunKeys      = $run
        Tasks        = $tasks
        Autoruns     = $auto
        Score        = $score
        Verdict      = $verdict
        TimeStamp    = (Get-Date).ToString('s')
    }
}

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

    [CmdletBinding()]
    param([string]$OutDir = (Join-Path $env:TEMP 'DargslanPersistenceAudit'))
    if (-not (Test-Path $OutDir)) { New-Item -Type Directory -Path $OutDir | Out-Null }
    $r = Get-DargslanPersistenceAuditReport
    $json = Join-Path $OutDir ('persist-' + $env:COMPUTERNAME + '.json')
    $html = Join-Path $OutDir ('persist-' + $env:COMPUTERNAME + '.html')
    $r | ConvertTo-Json -Depth 6 | Set-Content $json -Encoding UTF8
    $body  = "<h1>Persistence Audit - $($r.ComputerName)</h1>"
    $body += "<p>Verdict: <b>$($r.Verdict)</b> ($($r.Score)/3)</p>"
    $body += '<h2>Run Keys</h2>' + ($r.RunKeys  | ConvertTo-Html -Fragment)
    $body += '<h2>Tasks</h2>'    + ($r.Tasks    | ConvertTo-Html -Fragment)
    $body += '<h2>Autoruns + signature</h2>' + ($r.Autoruns | ConvertTo-Html -Fragment)
    ConvertTo-Html -Body $body -Title 'Persistence Audit' | Set-Content $html -Encoding UTF8
    [pscustomobject]@{ Json = $json; Html = $html; Verdict = $r.Verdict }
}