Public/Get-ServiceAccountUsage.ps1

function Get-ServiceAccountUsage {
    <#
    .SYNOPSIS
        Scans remote servers to find where service accounts are actually running.
 
    .DESCRIPTION
        Uses WinRM (Invoke-Command) and CIM (Win32_Service) to enumerate services on
        remote servers, identifying those running under domain accounts. Cross-references
        with Active Directory when SearchBase is provided to flag accounts with
        PasswordNeverExpires or other risk conditions.
 
    .PARAMETER ComputerName
        One or more server names to scan for running services. Mandatory.
 
    .PARAMETER SearchBase
        Optional AD search base to cross-reference discovered service accounts with
        their AD properties (password age, expiration policy, etc.).
 
    .EXAMPLE
        Get-ServiceAccountUsage -ComputerName 'SERVER01', 'SERVER02'
 
    .EXAMPLE
        Get-ServiceAccountUsage -ComputerName (Get-Content servers.txt) -SearchBase "DC=contoso,DC=com"
 
    .NOTES
        Requires WinRM enabled on target servers. Run from an account with admin
        rights on the target machines.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter()]
        [string]$SearchBase
    )

    begin {
        Write-Verbose "Starting service account usage scan"

        $AllComputers = [System.Collections.Generic.List[string]]::new()
        $Results = [System.Collections.Generic.List[PSObject]]::new()

        # Well-known local/built-in accounts that are NOT domain service accounts
        $BuiltInAccounts = @(
            'LocalSystem'
            'NT AUTHORITY\LocalService'
            'NT AUTHORITY\NetworkService'
            'NT AUTHORITY\SYSTEM'
            'NT AUTHORITY\LOCAL SERVICE'
            'NT AUTHORITY\NETWORK SERVICE'
            'NT Service\*'
            'Local System'
            ''
        )

        # If SearchBase provided, build a lookup of AD service accounts
        $ADAccountLookup = @{}
        if ($SearchBase) {
            Write-Verbose "Building AD account lookup from SearchBase: $SearchBase"
            try {
                $ADAccounts = Get-ADUser -SearchBase $SearchBase -Filter * -Properties @(
                    'SAMAccountName'
                    'PasswordNeverExpires'
                    'PasswordLastSet'
                    'Enabled'
                ) -ErrorAction SilentlyContinue

                foreach ($ADAccount in $ADAccounts) {
                    $ADAccountLookup[$ADAccount.SAMAccountName] = $ADAccount
                }
                Write-Verbose "Loaded $($ADAccountLookup.Count) AD accounts for cross-reference"
            }
            catch {
                Write-Warning "Could not load AD accounts for cross-reference: $_"
            }
        }

        # Script block to run on remote machines
        $RemoteScriptBlock = {
            Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue |
                Select-Object -Property Name, DisplayName, StartName, StartMode, State
        }
    }

    process {
        foreach ($Computer in $ComputerName) {
            $AllComputers.Add($Computer)
        }
    }

    end {
        Write-Verbose "Scanning $($AllComputers.Count) servers"

        # Execute remote scan
        $RemoteResults = $null
        try {
            $RemoteResults = Invoke-Command -ComputerName $AllComputers -ScriptBlock $RemoteScriptBlock -ErrorAction SilentlyContinue -ErrorVariable RemoteErrors
        }
        catch {
            Write-Warning "Remote scan failed: $_"
            return
        }

        # Log connection failures
        foreach ($Err in $RemoteErrors) {
            Write-Warning "Connection issue: $($Err.Exception.Message)"
        }

        if (-not $RemoteResults) {
            Write-Warning "No results returned from any server."
            return
        }

        foreach ($Service in $RemoteResults) {
            $StartName = $Service.StartName
            if ([string]::IsNullOrWhiteSpace($StartName)) { continue }

            # Skip built-in local accounts
            $IsBuiltIn = $false
            foreach ($BuiltIn in $BuiltInAccounts) {
                if ($BuiltIn.EndsWith('*')) {
                    $Prefix = $BuiltIn.TrimEnd('*')
                    if ($StartName.StartsWith($Prefix, [StringComparison]::OrdinalIgnoreCase)) {
                        $IsBuiltIn = $true
                        break
                    }
                }
                elseif ($StartName -eq $BuiltIn) {
                    $IsBuiltIn = $true
                    break
                }
            }

            # Build findings
            $Findings = [System.Collections.Generic.List[string]]::new()

            # Flag LocalSystem services that look like they could be domain services
            if ($StartName -eq 'LocalSystem' -or $StartName -eq 'NT AUTHORITY\SYSTEM') {
                # Only flag if the service name looks like a custom/third-party service
                $SystemServices = @(
                    'Spooler', 'MSSQLSERVER', 'SQLAgent*', 'W3SVC', 'WAS',
                    'IISADMIN', 'MSSQLServerOLAPService', 'ReportServer'
                )
                $IsSuspect = $false
                foreach ($Pattern in $SystemServices) {
                    if ($Service.Name -like $Pattern) {
                        $IsSuspect = $true
                        break
                    }
                }
                if ($IsSuspect) {
                    $Findings.Add('RUNS AS LOCALSYSTEM (review if domain account needed)')
                }
                else {
                    continue  # Skip standard system services
                }
            }

            if ($IsBuiltIn -and $Findings.Count -eq 0) { continue }

            # Check if this is a domain account (contains backslash or @)
            $IsDomainAccount = ($StartName -match '\\' -and $StartName -notmatch '^NT ') -or ($StartName -match '@')

            if ($IsDomainAccount) {
                # Extract SAMAccountName for AD lookup
                $SAMName = if ($StartName -match '\\(.+)$') {
                    $Matches[1]
                }
                elseif ($StartName -match '^(.+)@') {
                    $Matches[1]
                }
                else {
                    $StartName
                }

                # Cross-reference with AD if available
                if ($ADAccountLookup.ContainsKey($SAMName)) {
                    $ADInfo = $ADAccountLookup[$SAMName]
                    if ($ADInfo.PasswordNeverExpires) {
                        $Findings.Add('DOMAIN ACCOUNT WITH PASSWORD NEVER EXPIRES')
                    }
                    if ($ADInfo.PasswordLastSet) {
                        $Age = [math]::Round(((Get-Date) - $ADInfo.PasswordLastSet).TotalDays)
                        if ($Age -gt 365) {
                            $Findings.Add("PASSWORD $Age DAYS OLD")
                        }
                    }
                    if (-not $ADInfo.Enabled) {
                        $Findings.Add('AD ACCOUNT DISABLED BUT SERVICE RUNNING')
                    }
                }
                else {
                    $Findings.Add('DOMAIN SERVICE ACCOUNT')
                }
            }

            if ($Findings.Count -eq 0 -and -not $IsDomainAccount) { continue }

            $ComputerDisplayName = if ($Service.PSComputerName) {
                $Service.PSComputerName
            }
            else {
                'Unknown'
            }

            $Result = [PSCustomObject]@{
                ComputerName       = $ComputerDisplayName
                ServiceName        = $Service.Name
                ServiceDisplayName = $Service.DisplayName
                StartName          = $StartName
                StartMode          = $Service.StartMode
                State              = $Service.State
                Finding            = ($Findings -join '; ')
            }
            $Result.PSObject.TypeNames.Insert(0, 'ServiceAccountAudit.Usage')
            $Results.Add($Result)
        }

        Write-Verbose "Usage scan complete. Found $($Results.Count) service account entries across $($AllComputers.Count) servers."
        $Results | Sort-Object -Property StartName, ComputerName
    }
}