PowerShell Scripting with CIM_DataFile
I've been exploring the world of CIM scripting a lot these days. I thought we'd continue that journey today. I'm sure most of you are familiar with the Win32 classes in the Root\CimV2
namespace. These are common classes we've used since the days of VBScript to get system management information.
But there's another class you may not be familiar with that offers a few tantalizing opportunities. Instead of querying the file system to retrieve file information, you can use the CIM_DataFile
class to get file information. This class is part of the Root\CimV2
namespace.
This is a rich object. In the last update to the PSScriptTools module, I added several CIM-related functions. These functions are designed to make it easier to discover information about CIM classes.
PS C:\> Get-CimMember Cim_DataFile
Class: Root/Cimv2:CIM_DataFile
Property ValueType Flags
-------- --------- -----
AccessMask UInt32 ReadOnly, NullValue
Archive Boolean ReadOnly, NullValue
Caption String ReadOnly, NullValue
Compressed Boolean ReadOnly, NullValue
CompressionMethod String ReadOnly, NullValue
CreationClassName String ReadOnly, NullValue
CreationDate DateTime ReadOnly, NullValue
CSCreationClassName String ReadOnly, NullValue
CSName String ReadOnly, NullValue
Description String ReadOnly, NullValue
Drive String ReadOnly, NullValue
EightDotThreeFileName String ReadOnly, NullValue
Encrypted Boolean ReadOnly, NullValue
EncryptionMethod String ReadOnly, NullValue
Extension String ReadOnly, NullValue
FileName String ReadOnly, NullValue
FileSize UInt64 ReadOnly, NullValue
FileType String ReadOnly, NullValue
FSCreationClassName String ReadOnly, NullValue
FSName String ReadOnly, NullValue
Hidden Boolean ReadOnly, NullValue
InstallDate DateTime ReadOnly, NullValue
InUseCount UInt64 ReadOnly, NullValue
LastAccessed DateTime ReadOnly, NullValue
LastModified DateTime ReadOnly, NullValue
Manufacturer String ReadOnly, NullValue
Name String Key, ReadOnly, NullValue
Path String ReadOnly, NullValue
Readable Boolean ReadOnly, NullValue
Status String ReadOnly, NullValue
System Boolean ReadOnly, NullValue
Version String ReadOnly, NullValue
Writeable Boolean ReadOnly, NullValue
Key properties will be highlighted in green. This class, by the way, is derived from the CIM_LogicalFile
class. If you query for this class, you still end up with the same information, but it will be a CIM_DataFile
instance. Remember that you need to escape backslashes in a WMI/CIM query.
PS C:\> Get-CimInstance -ClassName CIM_DataFile -Filter "Name='C:\\Windows\\System32\\notepad.exe'" | Tee -Variable f
Compressed : False
Encrypted : False
Size :
Hidden : False
Name : C:\Windows\System32\notepad.exe
Readable : True
System : False
Version : 10.0.22621.3646
Writeable : True
PS C:\> $f.PSObject.TypeNames
Microsoft.Management.Infrastructure.CimInstance#root/cimv2/CIM_DataFile
Microsoft.Management.Infrastructure.CimInstance#ROOT/cimv2/CIM_LogicalFile
Microsoft.Management.Infrastructure.CimInstance#ROOT/cimv2/CIM_LogicalElement
Microsoft.Management.Infrastructure.CimInstance#ROOT/cimv2/CIM_ManagedSystemElement
Microsoft.Management.Infrastructure.CimInstance#CIM_DataFile
Microsoft.Management.Infrastructure.CimInstance#CIM_LogicalFile
Microsoft.Management.Infrastructure.CimInstance#CIM_LogicalElement
Microsoft.Management.Infrastructure.CimInstance#CIM_ManagedSystemElement
Microsoft.Management.Infrastructure.CimInstance
System.Object
Let's see what kind of fun we can have with this class.
Searching for Files
The more specific you can make your query the better.
Get-CimInstance -ClassName CIM_DataFile -Filter "Drive='C:' AND Filename ='firefox' AND extension='exe'" | Tee -variable f
On my laptop this took about three seconds to run.
PS C:\> $f
Compressed : False
Encrypted : False
Size :
Hidden : False
Name : C:\Program Files\Mozilla Firefox\firefox.exe
Readable : True
System : False
Version : 126.0.1.8912
Writeable : True
Caution
This is where you need to be careful. Because you likely have thousands of files, querying for all files on a drive could take a long time, if it completes at all or you may get incomplete results.
This works for me.
PS C:\> Get-CimInstance -ClassName CIM_DataFile -Filter "Drive='C:' AND Filename ='pwsh' AND extension='exe'" | Tee -variable p
Compressed : False
Encrypted : False
Size :
Hidden : False
Name : C:\Program Files\PowerShell\7-preview\pwsh.exe
Readable : True
System : False
Version : 7.5.0.3
Writeable : True
Compressed : False
Encrypted : False
Size :
Hidden : False
Name : C:\Program Files\PowerShell\7\pwsh.exe
Readable : True
System : False
Version : 7.4.2.500
Writeable : True
But this fails.
Get-CimInstance -ClassName CIM_DataFile -Filter "Drive='C:' AND Filename ='powershell' AND extension='exe'"
I'll get no result and no error. However, I can get the file with a more specific query that includes the path.
PS C:\> Get-CimInstance -ClassName CIM_DataFile -Filter "Drive='C:' AND Filename ='powershell' AND extension='exe' AND Path = '\\windows\\system32\\windowspowershell\\v1.0\\'"
Compressed : False
Encrypted : False
Size :
Hidden : False
Name : C:\windows\system32\windowspowershell\v1.0\powershell.exe
Readable : True
System : False
Version : 10.0.22621.3130
Writeable : True
I have found that a broad query won't find items under C:\Windows or the user profile.
PS C:\> Get-CimInstance -ClassName CIM_DataFile -Filter "Drive='C:' AND Filename ='discord' AND extension='exe'"
PS C:\> $p = (Get-process Discord)[0].path.Replace("\","\\")
PS C:\> Get-CimInstance CIM_DataFile -filter "Name ='$p'"
Compressed : False
Encrypted : False
Size :
Hidden : False
Name : C:\Users\Jeff\AppData\Local\Discord\app-1.0.9148\Discord.exe
Readable : True
System : False
Version : 1.0.9148.0
Writeable : True
What About LIKE
You technically can use the LIKE
operator in a CIM query.
PS C:\> Get-CimInstance CIM_DataFile -filter "FileName LIKE 'cim%' AND extension = 'ps1' AND drive='c:' AND path = '\\scripts\\'" | Select Name,FileSize,LastModified
Name FileSize LastModified
---- -------- ------------
c:\scripts\CimData.ps1 528 7/23/2016 9:48:12 AM
c:\scripts\cimhash.ps1 502 7/29/2013 9:20:27 PM
c:\scripts\CIMScriptMaker.ps1 6995 9/14/2017 4:57:53 PM
What you don't want to do is try to use LIKE
on the Name
or Path
property. More than likely it will simply fail with no error.
Get-CimInstance CIM_DataFile -filter "Extension = 'psd1' and drive='c:' and Path LIKE '\\scripts\\%\\'"
So while I an easily list all files in a specific path:
PS C:\> Get-CimInstance CIM_DataFile -filter "Extension = 'psd1' and drive='c:' and Path = '\\scripts\\'" | measure | select count
Count
-----
18
I can't use the LIKE
operator to include sub-folders. I'll show you alternatives in a future article.
Scripting with CIM_DataFile
Before we wrap up, let's build a tool to get a CIM_DataFile
. I want this to be able to work remotely so I'll leverage the CIMSession techniques I demonstrated recently.
#requires -version 5.1
Function Get-CIMDataFile {
[cmdletbinding(DefaultParameterSetName = 'filename')]
[OutputType('CIM_DataFile')]
Param(
[Parameter(
Position = 0,
Mandatory,
HelpMessage = "Enter a filename like 'notepad.exe' or 'notepad.*' to search for.",
ParameterSetName = 'filename'
)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter(
Mandatory,
HelpMessage = "Enter a Folder name like C:\Scripts or C:\Users\john\Documents. A drive root like C:\ will not work. Do not include a trailing slash.",
ParameterSetName = 'filename'
)]
[ValidatePattern("^([a-z]|[A-Z]):(\\\w+)+")]
[ValidateNotNullOrEmpty()]
[string]$FolderPath,
[Parameter(
Mandatory,
ValueFromPipelineByPropertyName,
HelpMessage = "Enter the full filename like c:\windows\notepad.exe. Wildcards are not allowed.",
ParameterSetName = 'path'
)]
[string]$FullName,
[Parameter(HelpMessage = "Enter the computer name or an existing CIMSession object.")]
[ValidateNotNullOrEmpty()]
[Alias('CN', 'Server')]
[CIMSession]$CimSession = $env:COMPUTERNAME
)
Begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
#initialize reference variables
New-Variable -Name ci
New-Variable -Name ce
}
Process {
Switch ($PSCmdlet.ParameterSetName) {
"filename" {
$FileName = $Name.Split(".")[0]
$Extension = $Name.Split(".")[1]
$DeviceID = $FolderPath.Substring(0,2)
$Path =$FolderPath.Substring(2).Replace("\","\\")
if ($FileName -match "\*") {
$fileOp = "LIKE"
$FileName = $FileName.Replace("*","%")
}
else {
$fileOp = "="
}
if ($Extension -match "\*") {
$extOp = "LIKE"
$Extension = $Extension.Replace("*","%")
}
else {
$extOp = "="
}
$Query = "SELECT * FROM CIM_DataFile WHERE Drive='$DeviceID' AND FileName $FileOp '$FileName' AND Extension $extOp '$Extension' AND Path = '$Path\\'"
}
"path" {
$Name = $FullName.Replace("\","\\")
$Query = "SELECT * FROM CIM_DataFile WHERE Name = '$Name'"
}
}
if ($CimSession.TestConnection([ref]$ci, [ref]$ce)) {
Write-Verbose "Connected to $($CimSession.ComputerName.ToUpper())"
Write-Verbose $Query
$CimSession.QueryInstances('Root/CimV2', 'WQL', $Query)
}
else {
Write-Error "Could not connect to $($CimSession.ComputerName.ToUpper()). $($ce.Message)"
Break
}
} #Process
End {
Write-Verbose "Ending $($MyInvocation.MyCommand)"
}
} #end function
You can use the function to get a file by its name components, and wildcards are allowed. You should specify a filename with an extension.
PS C:\> $r = Get-CIMDataFile cim*.ps* -FolderPath c:\scripts -Verbose
VERBOSE: Starting Get-CIMDataFile
VERBOSE: Connected to THINKX1-JH
VERBOSE: SELECT * FROM CIM_DataFile WHERE Drive='c:' AND FileName LIKE 'cim%' AND Extension LIKE 'ps%' AND Path = '\\scripts\\'
VERBOSE: Ending Get-CIMDataFile
PS C:\> $r.count
4
PS C:\> $r | Select Name,FileSize,LastModified
Name FileSize LastModified
---- -------- ------------
c:\scripts\CimData.ps1 528 7/23/2016 9:48:12 AM
c:\scripts\cimhash.ps1 502 7/29/2013 9:20:27 PM
c:\scripts\CIMScriptMaker.ps1 6995 9/14/2017 4:57:53 PM
c:\scripts\cimvolume.format.ps1xml 2483 2/1/2020 11:36:34 AM
Or you can specify a single file.
PS C:\> Get-Item $profile | Get-CIMDataFile | Format-List Name,FileSize,LastModified
Name : C:\Users\Jeff\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
FileSize : 0
LastModified : 12/22/2021 11:08:55 AM
The size of my profile is 0 because it is a symbolic link. The LastModified
time is for the symbolic link, not the actual profile script.
Here's one more example.
PS C:\> Get-CimDataFile -FullName (Get-Command pwsh.exe).source | Select *
Status : OK
Name : C:\Program Files\PowerShell\7\pwsh.exe
Caption : C:\Program Files\PowerShell\7\pwsh.exe
Description : C:\Program Files\PowerShell\7\pwsh.exe
InstallDate : 4/9/2024 11:36:36 PM
AccessMask :
Archive : True
Compressed : False
CompressionMethod :
CreationClassName : CIM_LogicalFile
CreationDate : 4/9/2024 11:36:36 PM
CSCreationClassName : Win32_ComputerSystem
CSName : THINKX1-JH
Drive : c:
EightDotThreeFileName : c:\program files\powershell\7\pwsh.exe
Encrypted : False
EncryptionMethod :
Extension : exe
FileName : pwsh
FileSize : 282160
FileType : Application
FSCreationClassName : Win32_FileSystem
FSName : NTFS
Hidden : False
InUseCount :
LastAccessed : 6/8/2024 6:34:22 PM
LastModified : 4/9/2024 11:36:36 PM
Path : \program files\powershell\7\
Readable : True
System : False
Writeable : True
Manufacturer :
Version : 7.4.2.500
PSComputerName : THINKX1-JH
CimClass : root/cimv2:CIM_DataFile
CimInstanceProperties : {Caption, Description, InstallDate, Name…}
CimSystemProperties : Microsoft.Management.Infrastructure.CimSystemProperties
Summary
The function is a simple wrapper that abstracts some of the CIM query process. However, I hope you'll give it a try. There's much more to show you but I'll save it until next time.