Skip to main content

File Inventory via Hardware Inventory

If you are less than pleased with how file inventory functions (badly named 'Software Inventory' in CM), like me, this is possibly something you'd want to test in your lab (you have a lab, right) and see if for those occasional requests we all seem to get for "can't you just inventory a file...", that this script + mof edit would work for you.

If you have file inventory on at all (software inventory), for things like *.exe in ProgramFiles, leave it on for that (don't change everything).

But let's say you got a request to "find all .pst files stored anywhere on local drives", because your company is still battling cleaning up email archives people saved as local .pst files.  Sure, you can add that to file inventory, but as you know file inventory takes hours and HOURS to run on computers.

This script, in testing, took between 16 and 90 seconds when I was testing.

After you have customized the script for YOUR weird one-off rule(s), and tested it interactively completely outside of CM, and are happy it works as you expect it to work to populate the WMI class with values, then..

Add the customized-by-you script as a powershell script inside a Configuration Item; where "what means compliant" is existential, any value returned.
Add the CI to a Baseline, deploy the baseline to your test collection.

After a target has run the baseline, you would go into your CM Console, Administration, Client Settings, Custom Client settings, hardware inventory, Add... and connect to that target, and in root\cimv2, add the 'cm_CustomFileInventory' class.  Monitor your CM server dataldr.log to confirm it made the views, and assuming you left the class enabled, after the target gets the new policy with the new instruction for inventory, trigger a hardware inventory.  Once inventory arrives at your CM database, look at the newly available view (probably called something like v_gs_cm_customfileinventory0), and there you go.

Only devices which have run the baseline will have something to say, so you can limit the targets reporting back with this custom file inventory by only deploying the Baseline to a specific collection.

 

 

<#
.SYNOPSIS
For specific files or types of files, Populate a Custom WMI with that information, for later inventory retrieval

 

.DESCRIPTION
Query for files, and populate WMI

 

.NOTES
 2023-03-17 Sherry Kissinger

 

CAUTION! CAUTION!  this is NOT meant to be a replacement for File Inventory completely. This routine will populate WMI, and depending upon
the rules YOU might make, you could inadvertently cause WMI Bloat (which can cause problems, and those problems might be difficult to
identify), and then hardware inventory mif size might be too big, resulting in Hardware Inventory failing to report at all. 
For example, do NOT query for c:\ , *.*... you are just asking for everything to blow up, and do so badly.

 

This routine was originally created as a response to "we need to know about any/every pst file on the C: drive".  As you know, file inventory,
searching for *.zzz files on all of the C: drive can take 30+ minutes, even if you have an SSD, and relatively few files. 
This script, when tested interactively on test devices (ok, it was 2 whole machines in the lab...)
was taking anywhere from 16 to 90 seconds, depending upon the # of files on the drive, size of the drive, etc.

 

$VerbosePreference options are
'Continue' (she the messages)
'SilentlyContinue' (Do not show the messages, this should be the default)
'Stop' Show the message and halt (tuse for Debugging)
'Inquire' Prompt the user if ok to continue
 

 

Example lines for gathering files.  These lines, would, for example...
1) Find all *.pst files anywhere on the First known drive (this does not include things like OneDrive or redirected Documents folders however)
2) Find any *.exe which happen to be in first known drive (which is usually c:) \WierdAppInTheRoot and subfolders under WierdAppInTheRoot
3) Find fubar.xml, but only if it is specifically in c:\program files\Widgets (or program files x86\widgets), do not even look in subdirectories under that. (-Recurse has been removed from those lines)
 

 

$LocalFileSystemDrives = (Get-psdrive -PSProvider FileSystem)
[System.IO.FileSystemInfo[]]$files =  Get-ChildItem -Path ($LocalFileSystemDrives.Root)[0] -include ('*.pst') -Recurse -OutBuffer 1000 -ErrorAction SilentlyContinue
[System.IO.FileSystemInfo[]]$files += Get-ChildItem -Path ($LocalFileSystemDrives.Root)[0]'\WierdAppInTheRoot' -include ('*.exe') -Recurse -OutBuffer 1000 -ErrorAction SilentlyContinue
[System.IO.FileSystemInfo[]]$files += Get-ChildItem -Path $env:programfiles'\Widgets' -include ('fubar.xml') -OutBuffer 1000 -ErrorAction SilentlyContinue  | Where-Object {$_.DirectoryName -in ($env:programfiles'\Widgets')}
[System.IO.FileSystemInfo[]]$files += Get-ChildItem -Path ${env:ProgramFiles(x86)}'\Widgets' -include ('fubar.xml') -OutBuffer 1000 -ErrorAction SilentlyContinue  | Where-Object {$_.DirectoryName -in (${env:ProgramFiles(x86)}'\Widgets')}

 

Once you have all of the objects you want, then the next section in the script will populate the class with the values, by reading relevant
information from the $files object.

 

Once you have tested this interactively (NOT as a CI yet), and you are happy with the results you see interactively in wmiexplorer, then
create a CI with this script (modified for your purposes), and deploy to a test number of devices.  Add the custom WMI class
to your inventory, and monitor the results.

 

*if* your environment has more than just '1 drive', and you were tasked with "find pst files on ANY/all local drives... you can use
this sql query to see 'how many' of the if statements you might need to cover the max # of Drives your clients have:

 

;with cte as (select ld.resourceid, count(*) as 'count' from v_gs_logical_disk ld where ld.DriveType0=3 group by ResourceID)
Select max(cte.count) from cte

 

for example, in my environment the max # was 5.  True, there was literally only ONE box with that many logical disks... and 99.5% of the
environment 'only' had 1 local disk; but about 0.5% had 2 disks... so it "doesn't hurt" to account for your max# of logical disks, they will
only be queried if they actually exist.
#>

 

 

 

Param (
    $Namespace             = 'root\cimv2',
    $Class                 = 'cm_CustomFileInventory',
    $VerbosePreference     = 'SilentlyContinue',
    $ErrorActionPreference = 'SilentlyContinue',
    $ScriptRanDate         = [System.DateTime]::UtcNow
    )

 

Function New-WMIClassHC {
if (Get-CimClass -Namespace "$NameSpace" | Where-Object {$_.CimClassName -eq $Class} ) {
   Write-Verbose "WMI Class $Class exists"
   }

 

else {
   Write-Verbose "Create WMI Class '$Class'"
   $NewClass = New-Object System.Management.ManagementClass ("$Namespace", [String]::Empty,$Null);
   $NewClass['__CLASS']=$Class
   $NewClass.Qualifiers.Add('Static',$true)
   $NewClass.Properties.Add('FileName', [System.Management.CimType]::String,$False)
   $NewClass.Properties['FileName'].Qualifiers.Add('Key', $true)
   $NewClass.Properties.Add('FilePath', [System.Management.CimType]::String,$False)
   $NewClass.Properties['FilePath'].Qualifiers.Add('Key', $true)
   $NewClass.Properties.Add('FileVersion', [System.Management.CimType]::String,$False)
   $NewClass.Properties.Add('FileSizeKB', [System.Management.CimType]::Uint32,$False)
   $NewClass.Properties.Add('FilePath', [System.Management.CimType]::String,$False)
   $NewClass.Properties.Add('LastWriteTimeUTC', [System.Management.CimType]::DateTime,$false)
   $NewClass.Properties.Add('ScriptLastRan', [System.Management.CimType]::DateTime, $false)
   $NewClass.Put() | Out-Null
   }
   Write-Verbose "End of Trying to Create an empty $Class in $Namespace to populate later"
}

 

Write-Verbose "Delete the values in $Class in $Namespace so we can populate it cleanly. If $Class exist, you must have rights to it for this to work."
Remove-CimInstance -Namespace $Namespace -Query "Select * from $Class" -ErrorAction SilentlyContinue

 

Write-Verbose "Create $Class if it does not exist at all yet (this will only occur once per device)"
New-WMIClassHC

 

Write-Verbose "Add to the object any additional rules you may want."
Write-Verbose "localFilesystemDrives is used in case you need to 'find a file on any/all local drives'"
Write-Verbose "you may have to check how many local drives your environment has, and have enough lines to address possibilities"

 

$LocalFileSystemDrives = (Get-psdrive -PSProvider FileSystem | Where-Object {$_.DisplayRoot -notlike "\\*"})

 

$DriveLetter = ($LocalFileSystemDrives).Root[0]

 

if  ( $DriveLetter.length -eq 1) {
  $DriveLetter = $DriveLetter+':\'
}
[System.IO.FileSystemInfo[]]$files =  Get-ChildItem -Path $DriveLetter -include ('*.pst','*.foo') -Recurse -OutBuffer 1000 -ErrorAction SilentlyContinue
#------------------------
$DriveLetter1 = ($LocalFileSystemDrives).Root[1]
if  ( $DriveLetter1.length -eq 1) {
  $DriveLetter1 = $DriveLetter1+':\'
}

 

If ($LocalFileSystemDrives.count -eq 2) {
[System.IO.FileSystemInfo[]]$files +=  Get-ChildItem -Path $DriveLetter1 -include ('*.pst','*.foo') -Recurse -OutBuffer 1000 -ErrorAction SilentlyContinue
  }
#------------------------
$DriveLetter2 = ($LocalFileSystemDrives).Root[2]
if  ( $DriveLetter2.length -eq 1) {
  $DriveLetter2 = $DriveLetter2+':\'
}

 

If ($LocalFileSystemDrives.count -eq 3) {
[System.IO.FileSystemInfo[]]$files +=  Get-ChildItem -Path $DriveLetter2 -include ('*.pst','*.foo') -Recurse -OutBuffer 1000 -ErrorAction SilentlyContinue
  }
#-------------------------
$DriveLetter3 = ($LocalFileSystemDrives).Root[3]
if  ( $DriveLetter3.length -eq 1) {
  $DriveLetter3 = $DriveLetter3+':\'
}

 

If ($LocalFileSystemDrives.count -eq 4) {
[System.IO.FileSystemInfo[]]$files +=  Get-ChildItem -Path $DriveLetter3 -include ('*.pst','*.foo') -Recurse -OutBuffer 1000 -ErrorAction SilentlyContinue
  }
#-------------------------
$DriveLetter4 = ($LocalFileSystemDrives).Root[4]
if  ( $DriveLetter4.length -eq 1) {
  $DriveLetter4 = $DriveLetter4+':\'
}

 

If ($LocalFileSystemDrives.count -eq 5) {
[System.IO.FileSystemInfo[]]$files +=  Get-ChildItem -Path $DriveLetter4 -include ('*.pst','*.foo') -Recurse -OutBuffer 1000 -ErrorAction SilentlyContinue
  }
#-------------------------
 

 

#### example where you only want to look in the specific root folder, and not recursively in all subfolders.
[System.IO.FileSystemInfo[]]$files += Get-ChildItem -Path $env:windir -include ('SomeFileOnlyInWindowsFolder.txt') -OutBuffer 1000 -ErrorAction SilentlyContinue | Where-Object {$_.DirectoryName -in ($Env:windir)}

 


Write-Verbose "Populate $Class in $Namespace with the file object information as queried"
Foreach ($File in $Files) {
Write-Verbose "This section is to try to get the productversion or fileversion, if the file has that metadata"
  if (![string]::IsNullOrEmpty($File.VersionInfo.ProductVersion)) {
      $FileVersion = $File.VersionInfo.ProductVersion
    } Else
    {
      if (![string]::IsNullOrEmpty($File.VersionInfo.FileVersion)) {
         $FileVersion = $file.VersionInfo.FileVersion
         }
      else {$FileVersion = ''}
    }

 

  $Size = [uint32][math]::Round(((Get-Item $File.FullName).length / 1kb),0)

 

New-CimInstance -Namespace "$Namespace" -class $Class -argument @{
        FileName=$File.Name;
        FilePath=$File.DirectoryName;
        FileVersion=$FileVersion;
        LastWriteTimeUTC=$file.LastWriteTimeUtc;
        FileSizeKB=$Size; 
        ScriptLastRan=$ScriptRanDate
        } | Out-Null
    }

 

Write-Host "Compliant"

 

SCCM, ConfigMgr

  • Created on .