Public/Find-UnquotedServicePath.ps1

#requires -Version 2
function Find-UnquotedServicePath
{
    <#
            .Synopsis
            Finds services that use unquoted paths.
 
            .DESCRIPTION
            The Find-UnquotedServicePath function uses Powershell remoting to query the executable paths of those services which are set to automatically start on remote computers. If an unquoted service path on a computer is detected, the name and path of the service along with name of the remote computer will be returned. By default, the Find-UnquotedServicePath function is set to query all domain joined computers.
 
            .EXAMPLE
            PS C:\> Find-UnquotedServicePath
         
            This command queries all domain joined computers for unquoted service paths using the current users credentials.
 
            .EXAMPLE
            PS C:\> Find-UnquotedServicePath | Export-csv -Path c:\UnquotedServicePaths.csv
         
            This command queries all domain joined computers for unquoted service paths using the current users credentials. The results are then exported to a CSV file named UnquotedServicePaths.csv at the root of the C drive.
 
            .EXAMPLE
            PS C:\>Find-UnquotedServicePath -Fix | Export-csv -Path c:\UnquotedServicePaths.csv
 
            This command queries all domain joined computers for unquoted service paths using the current users credentials. Once all unquoted service paths are found, the user will be prompted to provide quoted service paths for the relevant services. After providing a quoted path, the affected service path will be updated with the path provided by the user on all computers where the unquoted path was found. The results are then exported to a CSV file named UnquotedServicePaths.csv at the root of the C drive.
 
            .EXAMPLE
            PS C:\> Find-UnquotedServicePath -Credential (Get-Credential)
         
            This command displays a credential prompt and then queries all domain joined computers for unquoted service paths using the specified credentials.
 
            .EXAMPLE
            PS C:\>Set-Item -Path wsman:\localhost\Client\TrustedHosts -Value 172.16.0.0
     
            In this example a non-domain-joined computer is queried for unquoted service paths. First, in order to use an IP adress in the value of the ComputerName parameter, the IP address of the remote computer must be included in the WinRM TrustedHosts list on the local computer. To do so, the first command is run. Please note, this command assumes the WinRM TrustedHosts list on the local computer has not been previously set or is empty.
 
            Next, the non-domain-joined computer can now be queried. The credentials of an administrator account on the remote computer must be provided as shown in the command below.
     
 
            PS C:\>Find-UnquotedServicePath -ComputerName 172.16.0.0 -Credential Administrator
 
 
            Finally, unless otherwise needed, the IP address of the remote computer can now be removed from the WinRM TrustedHosts list on the local computer. To do so, the below command is run. Please note, the above command assumes the WinRM TrustedHosts list on the local computer has not been previously set or is empty.
 
 
            PS C:\>Clear-Item -Path wsman:\localhost\Client\TrustedHosts
 
            .EXAMPLE
            PS C:\> Find-UnquotedServicePath -ComputerName 'TESTSVR1'
         
            This command queries a remote computer named TESTSVR1 for unquoted service paths using the current users credentials.
 
            .EXAMPLE
            PS C:\> Find-UnquotedServicePath -ComputerName 'CLIENT1','CLIENT2','CLIENT4'
         
            This command queries remote computers CLIENT1, CLIENT2, & CLIENT4 for unquoted service paths using the current users credentials.
 
            .EXAMPLE
            PS C:\> Find-UnquotedServicePath -ComputerName 'TESTSVR1' -Credential (Get-Credential)
         
            This command queries a remote computer named TESTSVR1 for unquoted service paths using alternate credentials.
 
            .NOTES
            The Find-UnquotedServicePath function requires administrator rights on the remote computer(s) to be queried.
         
            Powershell remoting must be enabled on the remote computer to properly query service paths.
         
            If Powershell remoting is not enabled on a remote computer it can be enabled by either
            - Running Enable-PSRemoting locally or
            - By running Enable-RemotePSRemoting and specifying the name of the remote computer.
 
            .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
     
            Type a user name, such as "User01" or "Domain01\User01", or enter a variable that contains a PSCredential object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password.
 
            .PARAMETER ComputerName
            Specifies the computers on which the command runs. The default is all domain-joined computers.
     
            Type the NETBIOS name, IP address, or fully-qualified domain name of one or more computers in a comma-separated list. To specify the local computer, type the computer name, "localhost", or a dot (.).
     
            To use an IP address in the value of the ComputerName parameter, the command must include the Credential parameter. Also, the computer must be configured for HTTPS transport or the IP address of the remote computer must be included in the WinRM TrustedHosts list on the local computer. For instructions for adding a computer name to the TrustedHosts list, see "How to Add a Computer to the Trusted Host List" in about_Remote_Troubleshooting.
 
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('PSComputerName')]
        [string[]]$ComputerName = (Get-ADComputer -Filter *).Name,
        
        [Parameter(Mandatory = $false)]
        [pscredential]$Credential = $null,
        
        [Parameter(Mandatory = $false)]
        [switch]$Fix
    )
    
    
    Begin{
        If($PSBoundParameters.ContainsKey('Credential'))
        {
            $User = $Credential.UserName
            $Password = $Credential.GetNetworkCredential().Password

            If($User -like "$env:USERDOMAIN*")
            {
                Add-Type -AssemblyName System.DirectoryServices.AccountManagement
                $Domain = $env:USERDOMAIN
                $ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Domain
                $PrincipalContext = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ContextType, $Domain
                $CredentialValidity = $PrincipalContext.ValidateCredentials($User,$Password)
                If(-not $CredentialValidity)
                {
                    Write-Warning -Message 'Logon failure: Unknown username or bad password.' 
                    Break
                }
            }
        }
    }
    
    Process{
        $ScriptBlock = {
            $VerboseSwitch = $Using:PSBoundParameters.Verbose
            $WarningPreference = $Using:WarningPreference

            $Services = Get-WmiObject -Class win32_service -Filter "StartMode='Auto' AND NOT PathName LIKE 'c:\\Windows\\%' AND PathName LIKE '% %' AND NOT PathName LIKE '%`"%'" -Verbose:$false
            
            If($Services)
            {
                Foreach($Service in $Services)
                {
                    $Service | Select-Object -Property Name, PathName
                }
            }
        }
        
        $InvokeArgs = @{
            ComputerName = $ComputerName
        }
    
        If($null -ne $Credential)
        {
            $InvokeArgs.Credential = $Credential
        }
        
        $InvokeArgs.ComputerName = Test-PSRemoting @InvokeArgs -WarningAction $WarningPreference
        
        If($null -eq $InvokeArgs.ComputerName)
        {
            Break
        }
        
        $InvokeArgs.ScriptBlock = $ScriptBlock
        
        If($Fix)
        {
            $Results = Invoke-Command @InvokeArgs | Select-Object -ExcludeProperty RunspaceID, PSShowComputerName -Property *
            
            $UnquotedServicePaths = $Results |
            Group-Object -Property PathName |
            Sort-Object Count -Descending
            
            Add-Type -AssemblyName 'Microsoft.VisualBasic'
            
            $FixParams = @{}
            
            If($null -ne $Credential)
            {
                $FixParams.Credential = $Credential
            }

            Foreach($Grouping in $UnquotedServicePaths)
            {
                $ExistingPath = $Grouping.Group.PathName | Select-Object -Unique
                $ServiceName = $Grouping.Group.Name  | Select-Object -Unique
    
                $NewPath = [Microsoft.VisualBasic.Interaction]::InputBox('Please provide quoted path:', 'Update Service Path', $ExistingPath)

                If($NewPath -match '"')
                {
                    $FixParams.ComputerName = $Grouping.Group.PSComputerName
                        
                    Invoke-Command @FixParams -ScriptBlock{
                        $VerboseSwitch = $Using:PSBoundParameters.Verbose
                        $WarningPreference = $Using:WarningPreference
                        
                        $ServiceKey = Get-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$Using:ServiceName"
                        
                        $OldPath = $ServiceKey.GetValue('ImagePath')
            
                        If($ServiceKey)
                        {
                            $ServiceKey |
                            Set-ItemProperty -Name ImagePath -Value $Using:NewPath -PassThru |
                            Select-Object @{
                                n = 'Name'
                                e = {
                                    $_.PSChildName
                                }
                            }, @{
                                n = 'OldPath'
                                e = {
                                    $OldPath
                                }
                            }, @{
                                n = 'NewPath'
                                e = {
                                    $_.ImagePath
                                }
                            }
                        }
                    } | Select-Object -ExcludeProperty RunspaceID, PSShowComputerName -Property *
                }
                Else
                {
                    Write-Warning -Message 'No quotes provided.'
                }
            }
        }
        Else
        {
            Invoke-Command @InvokeArgs | Select-Object -ExcludeProperty RunspaceID, PSShowComputerName -Property *
        }
    }
    
    End{}
}