Public/Invoke-ServiceAccountAudit.ps1

function Invoke-ServiceAccountAudit {
    <#
    .SYNOPSIS
        Orchestrates a full service account security audit and generates an HTML dashboard.
 
    .DESCRIPTION
        Runs all ServiceAccount-Audit functions in sequence:
          1. Get-ServiceAccountInventory - Discovers service accounts in AD
          2. Get-ServiceAccountAge - Analyzes password age risk
          3. Get-ServiceAccountUsage - Scans servers for running services (if ComputerName specified)
          4. Get-SPNReport - Audits Kerberoasting risk
 
        Collects all results and generates a dark-themed HTML dashboard report.
 
    .PARAMETER SearchBase
        The AD distinguished name to scope the search. Defaults to the current domain root.
 
    .PARAMETER ComputerName
        One or more server names to scan for running services. If omitted, the usage
        scan is skipped.
 
    .PARAMETER OutputPath
        Path where the HTML report will be saved. Defaults to
        "ServiceAccountAudit_<timestamp>.html" in the current directory.
 
    .PARAMETER DaysOldThreshold
        Minimum password age in days for the password age report. Default: 365.
 
    .PARAMETER IncludeMSA
        Include Managed Service Accounts in the inventory scan.
 
    .EXAMPLE
        Invoke-ServiceAccountAudit -OutputPath "C:\Reports\svc-audit.html"
 
    .EXAMPLE
        Invoke-ServiceAccountAudit -ComputerName (Get-Content servers.txt) -DaysOldThreshold 180 -IncludeMSA
 
    .EXAMPLE
        $servers = Get-ADComputer -Filter "OperatingSystem -like '*Server*'" | Select-Object -ExpandProperty Name
        Invoke-ServiceAccountAudit -ComputerName $servers -SearchBase "DC=contoso,DC=com"
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$SearchBase,

        [Parameter()]
        [string[]]$ComputerName,

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$DaysOldThreshold = 365,

        [Parameter()]
        [switch]$IncludeMSA
    )

    begin {
        $StartTime = Get-Date
        Write-Verbose "Starting full service account audit at $StartTime"

        if (-not $OutputPath) {
            $Timestamp = $StartTime.ToString('yyyyMMdd_HHmmss')
            $OutputPath = Join-Path -Path (Get-Location) -ChildPath "ServiceAccountAudit_$Timestamp.html"
        }

        # Ensure output directory exists
        $OutputDir = Split-Path -Path $OutputPath -Parent
        if ($OutputDir -and -not (Test-Path -Path $OutputDir)) {
            New-Item -Path $OutputDir -ItemType Directory -Force | Out-Null
        }
    }

    process {
        # --- Phase 1: Service Account Inventory ---
        Write-Verbose "Phase 1: Service Account Inventory"
        Write-Progress -Activity 'Service Account Audit' -Status 'Discovering service accounts...' -PercentComplete 10

        $InventoryParams = @{}
        if ($SearchBase)  { $InventoryParams['SearchBase']  = $SearchBase }
        if ($IncludeMSA)  { $InventoryParams['IncludeMSA']  = $true }

        try {
            $Inventory = @(Get-ServiceAccountInventory @InventoryParams)
            Write-Verbose "Inventory: $($Inventory.Count) accounts discovered"
        }
        catch {
            Write-Warning "Inventory scan failed: $_"
            $Inventory = @()
        }

        # --- Phase 2: Password Age Analysis ---
        Write-Verbose "Phase 2: Password Age Analysis"
        Write-Progress -Activity 'Service Account Audit' -Status 'Analyzing password age...' -PercentComplete 30

        $AgeParams = @{
            DaysOldThreshold = $DaysOldThreshold
        }
        if ($SearchBase) { $AgeParams['SearchBase'] = $SearchBase }

        try {
            $PasswordAge = @(Get-ServiceAccountAge @AgeParams)
            Write-Verbose "Password Age: $($PasswordAge.Count) accounts above threshold"
        }
        catch {
            Write-Warning "Password age analysis failed: $_"
            $PasswordAge = @()
        }

        # --- Phase 3: Service Usage Scan ---
        $UsageResults = @()
        if ($ComputerName -and $ComputerName.Count -gt 0) {
            Write-Verbose "Phase 3: Service Usage Scan ($($ComputerName.Count) servers)"
            Write-Progress -Activity 'Service Account Audit' -Status "Scanning $($ComputerName.Count) servers..." -PercentComplete 50

            $UsageParams = @{
                ComputerName = $ComputerName
            }
            if ($SearchBase) { $UsageParams['SearchBase'] = $SearchBase }

            try {
                $UsageResults = @(Get-ServiceAccountUsage @UsageParams)
                Write-Verbose "Usage: $($UsageResults.Count) service entries found"
            }
            catch {
                Write-Warning "Usage scan failed: $_"
                $UsageResults = @()
            }
        }
        else {
            Write-Verbose "Phase 3: Skipped (no ComputerName specified)"
        }

        # --- Phase 4: SPN / Kerberoasting Audit ---
        Write-Verbose "Phase 4: SPN / Kerberoasting Audit"
        Write-Progress -Activity 'Service Account Audit' -Status 'Auditing SPNs...' -PercentComplete 70

        $SPNParams = @{}
        if ($SearchBase) { $SPNParams['SearchBase'] = $SearchBase }

        try {
            $SPNResults = @(Get-SPNReport @SPNParams)
            Write-Verbose "SPN: $($SPNResults.Count) SPN entries analyzed"
        }
        catch {
            Write-Warning "SPN audit failed: $_"
            $SPNResults = @()
        }

        # --- Phase 5: Generate HTML Report ---
        Write-Verbose "Phase 5: Generating HTML report"
        Write-Progress -Activity 'Service Account Audit' -Status 'Generating report...' -PercentComplete 90

        $EndTime = Get-Date
        $Duration = $EndTime - $StartTime

        # Build summary statistics
        $TotalAccounts     = $Inventory.Count
        $CriticalFindings  = ($PasswordAge | Where-Object { $_.RiskRating -eq 'CRITICAL' }).Count
        $HighFindings      = ($PasswordAge | Where-Object { $_.RiskRating -eq 'HIGH' }).Count
        $Kerberoastable    = ($SPNResults | Where-Object { $_.Finding -match 'KERBEROASTABLE' }).Count
        $NoOwner           = ($Inventory | Where-Object { $_.Finding -match 'NO OWNER' }).Count
        $PrivilegedAccounts = ($Inventory | Where-Object { $_.Finding -match 'IN PRIVILEGED GROUP' }).Count
        $ServersScanned    = if ($ComputerName) { $ComputerName.Count } else { 0 }
        $ServiceEntries    = $UsageResults.Count

        $DashboardData = @{
            Title              = 'Service Account Security Audit'
            GeneratedAt        = $EndTime.ToString('yyyy-MM-dd HH:mm:ss')
            Duration           = '{0:mm}m {0:ss}s' -f $Duration
            SearchBase         = if ($SearchBase) { $SearchBase } else { '(Domain Root)' }
            DaysOldThreshold   = $DaysOldThreshold
            TotalAccounts      = $TotalAccounts
            CriticalFindings   = $CriticalFindings
            HighFindings       = $HighFindings
            Kerberoastable     = $Kerberoastable
            NoOwner            = $NoOwner
            PrivilegedAccounts = $PrivilegedAccounts
            ServersScanned     = $ServersScanned
            ServiceEntries     = $ServiceEntries
            Inventory          = $Inventory
            PasswordAge        = $PasswordAge
            UsageResults       = $UsageResults
            SPNResults         = $SPNResults
        }

        try {
            New-HtmlDashboard -Data $DashboardData -OutputPath $OutputPath
            Write-Verbose "Report saved to: $OutputPath"
        }
        catch {
            Write-Warning "HTML report generation failed: $_"
        }

        Write-Progress -Activity 'Service Account Audit' -Completed
    }

    end {
        # Return a summary object
        $Summary = [PSCustomObject]@{
            AuditDate          = $EndTime
            Duration           = $Duration
            SearchBase         = if ($SearchBase) { $SearchBase } else { '(Domain Root)' }
            TotalAccounts      = $TotalAccounts
            CriticalPasswords  = $CriticalFindings
            HighPasswords      = $HighFindings
            KerberoastableAccounts = $Kerberoastable
            NoOwnerAccounts    = $NoOwner
            PrivilegedAccounts = $PrivilegedAccounts
            ServersScanned     = $ServersScanned
            ServiceEntries     = $ServiceEntries
            ReportPath         = $OutputPath
        }
        $Summary.PSObject.TypeNames.Insert(0, 'ServiceAccountAudit.Summary')

        Write-Verbose "Audit complete. $TotalAccounts accounts, $CriticalFindings critical, $Kerberoastable kerberoastable."
        $Summary
    }
}