Public/system/Get-StartupProgram.ps1

#Requires -Version 5.1
function Get-StartupProgram {
    <#
        .SYNOPSIS
            Retrieves programs configured to run at system startup or user logon
 
        .DESCRIPTION
            Enumerates startup entries from multiple sources: registry Run and RunOnce
            keys (both Machine and User scope, including WOW6432Node for 32-bit entries
            on 64-bit systems) and the common Startup folder. Provides a consolidated
            view of all auto-start programs across all launch points.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local computer.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional PSCredential for authenticating to remote computers.
            Not used for local queries.
 
        .EXAMPLE
            Get-StartupProgram
 
            Lists all startup programs on the local computer.
 
        .EXAMPLE
            Get-StartupProgram -ComputerName 'SRV01'
 
            Lists startup programs from a remote server.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Get-StartupProgram
 
            Lists startup programs from multiple servers via pipeline.
 
        .OUTPUTS
            PSWinOps.StartupProgram
            Returns objects with program name, command line, location source,
            scope (Machine or User), and timestamp.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-03-25
            Requires: PowerShell 5.1+ / Windows only
            Requires: WinRM for remote computers
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.StartupProgram')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"
        $localNames = @($env:COMPUTERNAME, 'localhost', '.')

        $registrySources = @(
            @{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run';                          Scope = 'Machine'; Label = 'HKLM\...\Run' }
            @{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce';                      Scope = 'Machine'; Label = 'HKLM\...\RunOnce' }
            @{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run';              Scope = 'Machine'; Label = 'HKLM\...\WOW6432Node\Run' }
            @{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce';          Scope = 'Machine'; Label = 'HKLM\...\WOW6432Node\RunOnce' }
            @{ Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run';                          Scope = 'User';    Label = 'HKCU\...\Run' }
            @{ Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce';                      Scope = 'User';    Label = 'HKCU\...\RunOnce' }
        )

        $scriptBlock = {
            param(
                [array]$Sources
            )

            $results = [System.Collections.Generic.List[object]]::new()

            foreach ($source in $Sources) {
                $regPath = $source.Path
                if (-not (Test-Path -Path $regPath)) { continue }

                try {
                    $regItem = Get-Item -Path $regPath -ErrorAction Stop
                    foreach ($valueName in $regItem.GetValueNames()) {
                        if ([string]::IsNullOrEmpty($valueName)) { continue }
                        $results.Add([PSCustomObject]@{
                            ProgramName = $valueName
                            Command     = $regItem.GetValue($valueName)
                            Location    = $source.Label
                            Scope       = $source.Scope
                            Source      = 'Registry'
                        })
                    }
                }
                catch {
                    Write-Warning "Failed to read '$regPath': $_"
                }
            }

            # Common Startup folder (All Users)
            $startupPath = [Environment]::GetFolderPath('CommonStartup')
            if ($startupPath -and (Test-Path -Path $startupPath)) {
                $shortcuts = Get-ChildItem -Path $startupPath -Filter '*.lnk' -ErrorAction SilentlyContinue
                foreach ($shortcut in $shortcuts) {
                    try {
                        $shell = New-Object -ComObject WScript.Shell
                        $lnk = $shell.CreateShortcut($shortcut.FullName)
                        $results.Add([PSCustomObject]@{
                            ProgramName = [System.IO.Path]::GetFileNameWithoutExtension($shortcut.Name)
                            Command     = $lnk.TargetPath
                            Location    = 'Common Startup Folder'
                            Scope       = 'Machine'
                            Source      = 'StartupFolder'
                        })
                    }
                    catch {
                        $results.Add([PSCustomObject]@{
                            ProgramName = [System.IO.Path]::GetFileNameWithoutExtension($shortcut.Name)
                            Command     = $shortcut.FullName
                            Location    = 'Common Startup Folder'
                            Scope       = 'Machine'
                            Source      = 'StartupFolder'
                        })
                    }
                }
            }

            # User Startup folder
            $userStartupPath = [Environment]::GetFolderPath('Startup')
            if ($userStartupPath -and (Test-Path -Path $userStartupPath)) {
                $shortcuts = Get-ChildItem -Path $userStartupPath -Filter '*.lnk' -ErrorAction SilentlyContinue
                foreach ($shortcut in $shortcuts) {
                    try {
                        $shell = New-Object -ComObject WScript.Shell
                        $lnk = $shell.CreateShortcut($shortcut.FullName)
                        $results.Add([PSCustomObject]@{
                            ProgramName = [System.IO.Path]::GetFileNameWithoutExtension($shortcut.Name)
                            Command     = $lnk.TargetPath
                            Location    = 'User Startup Folder'
                            Scope       = 'User'
                            Source      = 'StartupFolder'
                        })
                    }
                    catch {
                        $results.Add([PSCustomObject]@{
                            ProgramName = [System.IO.Path]::GetFileNameWithoutExtension($shortcut.Name)
                            Command     = $shortcut.FullName
                            Location    = 'User Startup Folder'
                            Scope       = 'User'
                            Source      = 'StartupFolder'
                        })
                    }
                }
            }

            $results
        }
    }

    process {
        foreach ($machine in $ComputerName) {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Processing '$machine'"

            try {
                $isLocal = $localNames -contains $machine
                $displayName = if ($isLocal) { $env:COMPUTERNAME } else { $machine }

                if ($isLocal) {
                    $rawEntries = & $scriptBlock -Sources $registrySources
                }
                else {
                    $invokeParams = @{
                        ComputerName = $machine
                        ScriptBlock  = $scriptBlock
                        ArgumentList = @(, $registrySources)
                        ErrorAction  = 'Stop'
                    }
                    if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) {
                        $invokeParams['Credential'] = $Credential
                    }
                    $rawEntries = Invoke-Command @invokeParams
                }

                foreach ($entry in $rawEntries) {
                    [PSCustomObject]@{
                        PSTypeName   = 'PSWinOps.StartupProgram'
                        ComputerName = $displayName
                        ProgramName  = $entry.ProgramName
                        Command      = $entry.Command
                        Location     = $entry.Location
                        Scope        = $entry.Scope
                        Source       = $entry.Source
                        Timestamp    = Get-Date -Format 'o'
                    }
                }
            }
            catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_"
                continue
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}