Use PowerShell and WMI to Locate Multiple Files on Any Drive in your Domain

A couple of days ago I was browsing my RSS feeds and came across an article that PowerShell MVP Jeffery Hicks wrote regarding the use of finding files using WMI with a function called Get-CimFile. That article is listed below:

http://jdhitsolutions.com/blog/2013/01/find-files-with-wmi-and-powershell/

In this article, Jeff shows how you can specify a filename and you can search through WMI to find a file or files quicker than what you could using Get-ChildItem. Of course, this is under the pretense that you have no idea where this file could be at (otherwise you would just use Get-ChildItem). In the article it was mentioned that one could add for optional credentials since Get-WMIObject already has a Credential parameter. In the comments it was asked about allowing wildcard support to the search as well.

Well, I had some free time and decided to take this on and add those features as well as making my own personal changes to include allowing for multiple files and allowing the use of just an extension or extensions. Another thing I did was remove the PSJobs infrastructure that Jeff used with the –AsJob parameter. If you’ve read my other blog posts, you probably already know why I did this, if not you can find the article here. Nothing terribly against using PSJobs as it definitely has its place in the PowerShell ecosystem, but I felt like making the change to runspaces instead.

Alternate Credentials

The first item I added was allowing the use of alternate credentials so you can access remote systems if you are using a different account. For that, I just added the following parameter for $Credential:

[System.Management.Automation.Credential()]$Credential = 
[System.Management.Automation.PSCredential]::Empty

This method works great because it will allow you to use the –Credential one of two ways by either calling without any arguments: –Credential or with an argument –Credential domain\username. The important part of this is the [System.Management.Automation.PSCredential]::Empty. Without this, you would get a prompt for a credential every time you ran the function.

WildCard Support

This one was a little more complex because I had to check to see if a wildcard character was being used in the –Name parameter. To first determine if a wildcard was being used, I setup a regular expression to perform the search:

[regex]$regex_wildcards = "\*|%|_|\?"

You may be asking, “Why are you allowing a * and ? when they are clearly not legal wildcards for Windows Query Language (WQL)?”. The answer to that will be revealed shortly. Once I know that I am dealing with a wildcard, I then take the filter fragment that I built and replace the “=” with “LIKE” for the filter fragment before it is added to the main filter.

If ($filenameFilter -match $regex_wildcards) {
    $filenameFilter = $filenameFilter -replace "="," LIKE "
}
If ($_extensionFilter -match $regex_wildcards) {
    $_extensionFilter = $_extensionFilter -replace "="," LIKE "
}

Ok, so lets assume we have the main filter completed and are ready to add it into the WMI splat table that will be used for the Get-WMIObject cmldet. If you remember, there is a possibility that there are illegal wildcard characters that will cause this query to fail instantly. So how do we get around this issue? Regex? I suppose, but that seems like overkill. Instead, lets use something that already exists and does everything for you with no work at all! The class being mentioned is Management.Automation.WildcardPattern which has a method called ToWQL() which takes a string containing the filter for its constructor and translates everything, including those illegal wildcards into a WQL friendly string. Watch this example to see it in action:

([Management.Automation.WildcardPattern]"Filename LIKE pow?rsh*.e?e").ToWql()

image

As you can see, everything was translated perfectly into a WQL friendly string. It is important to note that using wildcards will degrade the performance on the query, so use at your own discretion.

The Rest of the Additions

As mentioned before, I swapped out PSJobs for runspaces based on my own personal preferences. I also added the ability to supply multiple files and extensions in case you need to find multiple files/extensions. By doing this, I dynamically build out the filter based on if it is a filename, extension or both.

#region Reformat for WMI    
$firstRun = $True
ForEach ($item in $name) {
    If ($item -match "\.") {
        $index = $item.LastIndexOf(".")
        $filename = $item.Substring(0,$index)
        $_extension = $item.Substring($index+1)
        $filenameFilter= ("Filename='{0}'" -f $filename)
        $_extensionFilter= ("Extension='{0}'" -f $_extension)
        If ($filenameFilter -match $regex_wildcards) {
            $filenameFilter = $filenameFilter -replace "="," LIKE "
        }
        If ($_extensionFilter -match $regex_wildcards) {
            $_extensionFilter = $_extensionFilter -replace "="," LIKE "
        }
        If ($firstRun) {
            If ($filename.length -gt 0 -AND $_extension.length -gt 0) {
                $filter = "{0}({1} AND {2})" -f $filter,$filenameFilter,$_extensionFilter
                $firstRun = $False                
            } ElseIf ($filename.length -gt 0) {
                $filter = "{0}{1}" -f $filter,$filenameFilter
                $firstRun = $False 
            } ElseIf ($_extension.length -gt 0) {
                $filter = "{0}{1}" -f $filter,$_extensionFilter
                $firstRun = $False                 
            }
        } Else {
            If ($filename.length -gt 0 -AND $_extension.length -gt 0) {
                $filter = "{0} OR ({1} AND {2})" -f $filter,$filenameFilter,$_extensionFilter
                $firstRun = $False                
            } ElseIf ($filename.length -gt 0) {
                $filter = "{0} OR {1}" -f $filter,$filenameFilter
                $firstRun = $False 
            } ElseIf ($_extension.length -gt 0) {
                $filter = "{0} OR {1}" -f $filter,$_extensionFilter
                $firstRun = $False                 
            }
        }
    } Else {
        $filenameFilter= ("Filename='{0}'" -f $item)
        If ($filenameFilter -match $regex_wildcards) {
            $filenameFilter = $filenameFilter -replace "="," LIKE "
        }
        If ($firstRun) {
            $filter = "{0}{1}" -f $filter,$filenameFilter
            $firstRun = $False 
        } Else {
            $filter = "{0} OR {1}" -f $filter,$filenameFilter
        }
    }
}  
#Add a closing ) at the end of the filter
If ($Drive.Length -gt 0) {
    $Filter = "{0})" -f $filter
}  

With the use of runspaces, I added the ability to display the number of runspace jobs remaining in the title bar of the console that then reverts back to the original title once the jobs have completed. Just a small tweak that helps to better track the progress of the jobs.

image

 

Function in Action

Now that I have covered my additions, it is time to see this in action. In the following examples I will use a wildcards and multiple files for the queries. I’ll also show the verbose output so you can see the filter being dynamically built based on the parameters supplied.

The example below shows a simple query and specifies the local drive.

Get-CIMFile -Name powershell.log -Verbose -Drive C:

image

 

The next example uses wildcards, which you can see were converted for the filter in the verbose stream. I will note that this took at looooong time to complete because of the use of wildcards.

Get-CIMFile -Name power?hell.*g -Verbose -Drive C: `
-Computername DC1.rivendell.com,Boe-PC

image

The final example shows how you can specify more than one item, be it a filename or just an extension.

Get-CIMFile -Name .ps1,powershell.log -Drive C: -Verbose

image

image

Those were just a few of quick examples of using the function to search for files and extensions. A more real world approach was that I used this to quickly search all of the servers in my network for .p12 and .pfx files so we can audit and remove these files from the network.

Download the Function

Script Repository

This entry was posted in powershell, scripts and tagged , , , . Bookmark the permalink.

9 Responses to Use PowerShell and WMI to Locate Multiple Files on Any Drive in your Domain

  1. Pingback: Use PowerShell and WMI to Locate Multiple Files on Any Drive in your Domain | Learn Powershell | Achieve More | Soyka's Blog

  2. Alex says:

    Ok…after MONTHS without a response…here is my modifications. Including a pretty export to .csv. Sadly, there is no “Remove” (force?) method.

    $computers=Get-Content “C:\computers.txt”
    $i=1
    do{

    Function Get-CIMFile {

    [cmdletbinding()]
    #region Parameters
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [Alias("Computer","__Server","IPAddress","CN","dnshostname")]
        [string[]]$Computername = $env:COMPUTERNAME,
        [parameter(Mandatory=$True,ParameterSetName='File')]
        [ValidateNotNullOrEmpty()]
        [Alias('File','Path','Fullname')]
        [string[]]$Name,
        [parameter()]
        [string]$Drive,
        [parameter()]
        [Alias('RunAs')]
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,    
        [parameter()]
        [Alias("MaxJobs")]
        [int]$Throttle = 10
    )
    #endregion Parameters
    Begin {
        [regex]$regex_wildcards = "\*|%|_|\?"
        $oldTitle = [console]::Title
        #region Functions
        #Function to perform runspace job cleanup
        Function Get-RunspaceData {
            [cmdletbinding()]
            param(
                [switch]$Wait
            )
            Do {
                $more = $false         
                Foreach($runspace in $runspaces) {
                    If ($runspace.Runspace.isCompleted) {
                        $runspace.powershell.EndInvoke($runspace.Runspace)
                        $runspace.powershell.dispose()
                        $runspace.Runspace = $null
                        $runspace.powershell = $null                 
                    } ElseIf ($runspace.Runspace -ne $null) {
                        $more = $true
                    }
                }
                If ($more -AND $PSBoundParameters['Wait']) {
                    Start-Sleep -Milliseconds 100
                }   
                #Clean out unused runspace jobs
                $temphash = $runspaces.clone()
                $temphash | Where {
                    $_.runspace -eq $Null
                } | ForEach {
                    Write-Verbose ("Removing {0}" -f $_.computer)
                    $Runspaces.remove($_)
                }  
                [String]$Title = ("Remaining Runspace Jobs: {0}" -f ((@($runspaces | Where {$_.Runspace -ne $Null}).Count)))           
            } while ($more -AND $PSBoundParameters['Wait'])
        }
        #endregion Functions
        #region Begin Filter Build
        If ($Drive.Length -gt 0) {
            $Filter = "Drive='{0}' AND (" -f $Drive
        }
        #endregion Begin Filter Build
        #region Reformat for WMI    
        $firstRun = $True
        ForEach ($item in $name) {
            If ($item -match "\.") {
                $index = $item.LastIndexOf(".")
                $filename = $item.Substring(0,$index)
                $_extension = $item.Substring($index+1)
                $filenameFilter= ("Filename='{0}'" -f $filename)
                $_extensionFilter= ("Extension='{0}'" -f $_extension)
                If ($filenameFilter -match $regex_wildcards) {
                    $filenameFilter = $filenameFilter -replace "="," LIKE "
                }
                If ($_extensionFilter -match $regex_wildcards) {
                    $_extensionFilter = $_extensionFilter -replace "="," LIKE "
                }
                If ($firstRun) {
                    If ($filename.length -gt 0 -AND $_extension.length -gt 0) {
                        $filter = "{0}({1} AND {2})" -f $filter,$filenameFilter,$_extensionFilter
                        $firstRun = $False                
                    } ElseIf ($filename.length -gt 0) {
                        $filter = "{0}{1}" -f $filter,$filenameFilter
                        $firstRun = $False 
                    } ElseIf ($_extension.length -gt 0) {
                        $filter = "{0}{1}" -f $filter,$_extensionFilter
                        $firstRun = $False                 
                    }
                } Else {
                    If ($filename.length -gt 0 -AND $_extension.length -gt 0) {
                        $filter = "{0} OR ({1} AND {2})" -f $filter,$filenameFilter,$_extensionFilter
                        $firstRun = $False                
                    } ElseIf ($filename.length -gt 0) {
                        $filter = "{0} OR {1}" -f $filter,$filenameFilter
                        $firstRun = $False 
                    } ElseIf ($_extension.length -gt 0) {
                        $filter = "{0} OR {1}" -f $filter,$_extensionFilter
                        $firstRun = $False                 
                    }
                }
            } Else {
                $filenameFilter= ("Filename='{0}'" -f $item)
                If ($filenameFilter -match $regex_wildcards) {
                    $filenameFilter = $filenameFilter -replace "="," LIKE "
                }
                If ($firstRun) {
                    $filter = "{0}{1}" -f $filter,$filenameFilter
                    $firstRun = $False 
                } Else {
                    $filter = "{0} OR {1}" -f $filter,$filenameFilter
                }
            }
        }  
        #Add a closing ) at the end of the filter
        If ($Drive.Length -gt 0) {
            $Filter = "{0})" -f $filter
        }        
        #Use the appropriate method to handle illegal wildcards
        If ($host.version.Major -eq 3) {
            #Format the filter so it has the correct WQL wildcards, if applicable - V3 only
            $filter = (([Management.Automation.WildcardPattern]$filter).ToWql()).Trim()            
        } Else {
            #Using V2 so we have to use a little different approach
            $Filter = ($Filter -replace "\*","%") -replace "\?","_"
        }
        Write-Verbose ("Filter: {0}" -f $Filter)
        #endregion Reformat for WMI
    
        #region Splat Tables
        #Define hash table for Get-RunspaceData function
        $runspacehash = @{}
    
        #Define hash table for WMI
        $wmi = @{
            Class = 'CIM_DataFile'
            Filter = $filter
            ErrorAction = 'Stop'
        }
        If ($Credential) {
            $wmi.Credential = $Credential
        }
        #Define Test-Connection hash table
        $testConn = @{
            Count = 1
            Quiet = $True
        }
        #endregion Splat Tables
    
        #region ScriptBlock
        $scriptBlock = {
            Param ($Computer,$wmi,$testConn)
            $testConn.Computername = $Computer
            If (Test-Connection @testConn){
                $wmi.Computername = $Computer
                If ($Computer -eq $env:COMPUTERNAME) {
                    #Alternate credentials are not valid for a local query
                    $wmi.Remove('Credential')
                }
                Get-WmiObject @wmi
                Try {
                } Catch {
                    Write-Warning ("{0}: {1}" -f $Computer,$_.Exception.Message)
                }
            } Else {
                Write-Warning ("{0}: Unavailable" -f $Computer)
            }               
        }
        #endregion ScriptBlock
    
        #region Runspace Creation
        Write-Verbose ("Creating runspace pool and session states")
        $sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)
        $runspacepool.Open()  
    
        Write-Verbose ("Creating empty collection to hold runspace jobs")
        $Script:runspaces = New-Object System.Collections.ArrayList        
        #endregion Runspace Creation
    }
    Process {
        ForEach ($Computer in $Computername) {
            #Create the powershell instance and supply the scriptblock with the other parameters 
            $powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($wmi).AddArgument($testConn)
    
            #Add the runspace into the powershell instance
            $powershell.RunspacePool = $runspacepool
    
            #Create a temporary collection for each runspace
            $temp = "" | Select-Object PowerShell,Runspace,Computer
            $Temp.Computer = $Computer
            $temp.PowerShell = $powershell
    
            #Save the handle output when calling BeginInvoke() that will be used later to end the runspace
            $temp.Runspace = $powershell.BeginInvoke()
            Write-Verbose ("Adding {0} collection" -f $temp.Computer)
            $runspaces.Add($temp) | Out-Null
    
            Write-Verbose ("Checking status of runspace jobs")
            Get-RunspaceData @runspacehash        
        }
    }
    End {
        Write-Verbose ("Finish processing the remaining runspace jobs: {0}" -f ((@($runspaces | Where {$_.Runspace -ne $Null}).Count)))
        $runspacehash.Wait = $true
        Get-RunspaceData @runspacehash
        [console]::Title = $oldTitle
        #region Cleanup Runspace
        Write-Verbose ("Closing the runspace pool")
        $runspacepool.close()  
        $runspacepool.Dispose() 
        #endregion Cleanup Runspace
    }
    

    }

    Get-CIMFile -name .p12,.pfx -comp $computers | Select-Object -Property PSComputerName,Drive,Description,FileName,FileSize,Hidden,LastAccessed | export-csv -path “\SERVERNAME\PKCS12\pkcs12_$((Get-Date).ToString(“yyyyMMdd”)).csv” -NoTypeInformation

    write-host $i; $i++}
    While ($i -lt 9999)

    • Boe Prox says:

      Sorry about that. I try to get to the comments that are left here but sometimes I miss some for one reason or another. I am glad to see that you were able to put this together!

  3. Alex says:

    Ok…so now that I have the files…and exported them…how do I run a recursive remove?

  4. Alex says:

    How can I use this script and include “export-csv”? I used
    Get-CIMFile -name .p12,.pfx -comp $computers | export-csv -path “\\SERVERNAME\C$\PKCS12\YEAR\MONTH\pkcs12_[DDMMMYYYY].csv”

    Using Windows Powershell ISE (x86) this gives me a run time error reading:

    Exception setting “Title”: “The handle is invalid.

    At line:125 char:75
    + [console]::Title = (“Remaining Runspace Jobs: {0}” -f ((@($runsp …
    + ~~~~~~
    + CategoryInfo : NotSpecified: (:) [], SetValueInvocationException
    + FullyQualifiedErrorId : ExceptionWhenSetting

  5. Pingback: Find Files with WMI and PowerShell Revisited | The Lonely Administrator

  6. Alex McFarland says:

    Funny, I was reading through this thinking “wow, this would be really great for auditing pfx files” , then I got to the end of the article and started laughing. I had been using Get-ChildItem and remoting to do the same thing, this looks much more elegant. Nice Job!

  7. That is a handy tip about credentials. I am so stealing (I mean borrowing) that.

Leave a comment