Automate File Inventory Scanning and Uploading to SharePoint using PowerShell and Intune
Table of content
As an IT admin or EUC engineer, you’ve probably been asked to identify specific files across hundreds of endpoints — whether it’s a license file, configuration file, or a particular executable. Doing that manually isn’t just inefficient it’s nearly impossible at scale.
That’s where automation comes in.
In this blog, we’ll build a PowerShell automation that does two things:
-
Scans your entire device for files matching a name or pattern and generates a detailed inventory report (CSV and JSON).
-
Automatically uploads the results to SharePoint using the Microsoft Graph API, making the reports accessible and centralized.
You can deploy this script using Microsoft Intune, allowing every managed device to report its findings back to your SharePoint site automatically.
Section 1: File Inventory Scanning with PowerShell
This section focuses on automating the discovery process — finding specific files (e.g., setup.exe, license.txt, config.json) across all drives and optionally in OneDrive folders.
Here’s what the script does:
- Scans all local drives (C:\, D:\, etc.)
- Optionally includes OneDrive folders
- Accepts search patterns or a configuration JSON file
- Exports results to CSV and JSON under C:\ProgramData\FileInventory
- Captures metadata such as file name, path, version, owner, and timestamps
PowerShell file inventory scanner for Windows devices
This Script scans all local filesystem drives for files matching a configurable search string or list of patterns. The result is stored in CSV and JSON at C:\ProgramData\FileInventory location.
<#
.SYNOPSIS
Client-side PowerShell file-inventory scanner for Windows devices.
.DESCRIPTION
- Scans all local filesystem drives for files matching a configurable search string or list of patterns.
- Optionally includes OneDrive for Business (per-user) locations. Can be disabled.
- Outputs results to CSV and JSON, and writes a short log to the console.
- Supports configuration via a JSON file (C:\ProgramData\FileScanConfig.json) so you can update search criteria without changing the script binary deployed to clients.
.NOTES
Author: Skatufa
Usage: Deploy via Intune (PowerShell script). When running in System context.
Run using : powershell -ep bypass -file "C:\Users\Atoofa\Downloads\testnew.ps1" "*google*"
#>
param(
[string]$SearchString = '',
[string[]]$SearchPatterns = @(),
[switch]$DisableOneDrive = $false,
[string]$OutputFolder = "$env:ProgramData\FileInventory",
[switch]$IncludeFileVersionInfo = $true,
[switch]$VerboseLogging = $false
)
# Helper: Load config from C:\ProgramData\FileScanConfig.json (if exists)
$ConfigPath = 'C:\ProgramData\FileScanConfig.json'
if (Test-Path $ConfigPath) {
try {
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json -ErrorAction Stop
if ($null -ne $cfg.SearchString -and -not [string]::IsNullOrWhiteSpace($cfg.SearchString)) { $SearchString = $cfg.SearchString }
if ($null -ne $cfg.SearchPatterns -and $cfg.SearchPatterns.Count -gt 0) { $SearchPatterns = $cfg.SearchPatterns }
if ($cfg.DisableOneDrive -eq $true) { $DisableOneDrive = $true }
if ($null -ne $cfg.OutputFolder -and -not [string]::IsNullOrWhiteSpace($cfg.OutputFolder)) { $OutputFolder = $cfg.OutputFolder }
} catch {
Write-Verbose "Failed to load config $ConfigPath : $_"
}
}
# If user provided only a SearchString, build wildcard pattern
if (($SearchPatterns -eq $null) -or ($SearchPatterns.Count -eq 0)) {
if (-not [string]::IsNullOrWhiteSpace($SearchString)) {
$SearchPatterns = @("*$SearchString*")
} else {
Write-Host "No search string/pattern provided. Provide -SearchString or deploy a config file to $ConfigPath." -ForegroundColor Yellow
exit 2
}
}
# Ensure output folder exists
try { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } catch { }
$Hostname = $env:COMPUTERNAME
$Now = Get-Date -Format 'yyyyMMdd_HHmmss'
$CsvPath = Join-Path $OutputFolder "FileInventory_${Hostname}_$Now.csv"
$JsonPath = Join-Path $OutputFolder "FileInventory_${Hostname}_$Now.json"
# Build list of root paths to search (all file-system PSDrives)
$fsDrives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -and (Test-Path $_.Root) }
$rootPaths = @()
foreach ($d in $fsDrives) {
$rootPaths += $d.Root
}
# Add OneDrive folders if not disabled and running in user context
$includeOneDrive = -not $DisableOneDrive
if ($includeOneDrive) {
if ($env:USERNAME -and $env:USERNAME -ne 'SYSTEM') {
$oneDriveCandidates = @(
"$env:USERPROFILE\OneDrive",
"$env:USERPROFILE\OneDrive - *",
"$env:USERPROFILE\OneDrive - * - Personal"
)
foreach ($p in $oneDriveCandidates) {
try {
$resolved = Get-ChildItem -Path $p -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
if ($resolved) { $rootPaths += $resolved }
} catch { }
}
} else {
Write-Verbose "OneDrive scan requested but running as SYSTEM or cannot access profile. Skipping OneDrive paths." -Verbose:$VerboseLogging
}
}
# Deduplicate
$rootPaths = $rootPaths | Sort-Object -Unique
Write-Host "Scanning $($rootPaths.Count) root path(s) for patterns: $($SearchPatterns -join ', ')" -ForegroundColor Cyan
$results = [System.Collections.Generic.List[PSObject]]::new()
# Function to build result object (compatible with PS 5.1)
function New-ResultObject {
param($fileInfo, $fileVersion)
$owner = $null
if ($null -ne $fileInfo.FullName) {
try {
$owner = (Get-Acl -LiteralPath $fileInfo.FullName -ErrorAction Stop).Owner
}
catch {
$owner = $null
}
}
[PSCustomObject]@{
Hostname = $Hostname
FileName = $fileInfo.Name
FullName = $fileInfo.FullName
FilePath = $fileInfo.DirectoryName
SizeBytes = $fileInfo.Length
DateCreated = $fileInfo.CreationTimeUtc.ToString('o')
DateAccessed = $fileInfo.LastAccessTimeUtc.ToString('o')
DateModified = $fileInfo.LastWriteTimeUtc.ToString('o')
Owner = $owner
ProductName = $fileVersion.ProductName
ProductVersion = $fileVersion.ProductVersion
FileDescription = $fileVersion.FileDescription
ScanTimestamp = (Get-Date).ToString('o')
}
}
# Start scanning
foreach ($root in $rootPaths) {
foreach ($pattern in $SearchPatterns) {
Write-Verbose "Searching root: $root pattern: $pattern" -Verbose:$VerboseLogging
try {
$files = Get-ChildItem -LiteralPath $root -Filter $pattern -File -Recurse -ErrorAction SilentlyContinue
} catch {
try {
$files = Get-ChildItem -LiteralPath $root -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -like $pattern }
} catch {
Write-Verbose "Failed to enumerate $root : $_" -Verbose:$VerboseLogging
continue
}
}
foreach ($f in $files) {
$fv = [PSCustomObject]@{ ProductName = $null; ProductVersion = $null; FileDescription = $null }
if ($IncludeFileVersionInfo) {
try {
$vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($f.FullName)
$fv.ProductName = $vi.ProductName
$fv.ProductVersion = $vi.ProductVersion
$fv.FileDescription = $vi.FileDescription
} catch { }
}
$results.Add((New-ResultObject -fileInfo $f -fileVersion $fv))
}
}
}
# Write outputs
$resultsArray = $results.ToArray()
try {
if ($resultsArray.Count -gt 0) {
$resultsArray | Export-Csv -Path $CsvPath -NoTypeInformation -Force
$resultsArray | ConvertTo-Json -Depth 5 | Out-File -FilePath $JsonPath -Encoding UTF8 -Force
Write-Host "Found $($resultsArray.Count) matching file(s). CSV -> $CsvPath JSON -> $JsonPath" -ForegroundColor Green
} else {
Write-Host "No matching files found." -ForegroundColor Yellow
}
} catch {
Write-Host "Failed to write output: $_" -ForegroundColor Red
}
# Audit log entry
$AuditFile = Join-Path $OutputFolder "FileInventory_Audit.log"
$summary = "$(Get-Date -Format o) - Host:$Hostname - Patterns:$($SearchPatterns -join ',') - Results:$($resultsArray.Count) - OutputCSV:$CsvPath"
Add-Content -Path $AuditFile -Value $summary -ErrorAction SilentlyContinue
# Show sample in console
if ($resultsArray.Count -gt 0) {
$resultsArray | Select-Object Hostname, FileName, FullName, ProductName, ProductVersion, FileDescription, FilePath, DateCreated, DateAccessed | Select-Object -First 20 | Format-Table -AutoSize
}
exit 0
Once the script is ran, it will create below files.
Section 2: Uploading the Report to SharePoint Automatically
Now that your scan results are generated, let’s automate their upload to SharePoint.
We’ll use Microsoft Graph API to securely authenticate (via App Registration) and upload the generated files to a SharePoint Document Library. This ensures all your file inventory reports are centralized and accessible for auditing or analysis.
Pre-requisites
-
Register an app in Azure AD with permissions ( Sites.FullControl.All, Sites.ReadWrite.All,Sites.Selected )
-
Generate a Client Secret and note your: Tenant ID, Client ID and Client Secret. You can refer to blog for setting up API permission and App from blog How to Upload Files to SharePoint Online Using Microsoft Graph API and PowerShell – Tech EUC
-
Identify your SharePoint Site ID and Document Library (Drive) ID
Here is your full script which will scan the windows files based on names and then automatically upload the results to SharePoint.
<#
.SYNOPSIS
File inventory scanner and uploader to SharePoint.
.DESCRIPTION
- Scans local file system (and optionally OneDrive folders) for matching files.
- Saves results to CSV/JSON.
- Uploads CSV result to SharePoint Online via Microsoft Graph API.
.NOTES
Author: Skatufa
Last Updated: 2025-10-23
#>
param(
[string]$SearchString = '',
[string[]]$SearchPatterns = @(),
[switch]$DisableOneDrive = $false,
[string]$OutputFolder = "$env:ProgramData\FileInventory",
[switch]$IncludeFileVersionInfo = $true,
[switch]$VerboseLogging = $false
)
# ─── 1️ Load Config ──────────────────────────────────────────────────────────────
$ConfigPath = 'C:\ProgramData\FileScanConfig.json'
if (Test-Path $ConfigPath) {
try {
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json -ErrorAction Stop
if ($null -ne $cfg.SearchString -and -not [string]::IsNullOrWhiteSpace($cfg.SearchString)) { $SearchString = $cfg.SearchString }
if ($null -ne $cfg.SearchPatterns -and $cfg.SearchPatterns.Count -gt 0) { $SearchPatterns = $cfg.SearchPatterns }
if ($cfg.DisableOneDrive -eq $true) { $DisableOneDrive = $true }
if ($null -ne $cfg.OutputFolder -and -not [string]::IsNullOrWhiteSpace($cfg.OutputFolder)) { $OutputFolder = $cfg.OutputFolder }
} catch {
Write-Verbose "Failed to load config $ConfigPath : $_"
}
}
if (($SearchPatterns -eq $null) -or ($SearchPatterns.Count -eq 0)) {
if (-not [string]::IsNullOrWhiteSpace($SearchString)) {
$SearchPatterns = @("*$SearchString*")
} else {
Write-Host " No search string or pattern provided." -ForegroundColor Yellow
exit 2
}
}
# ─── 2️ Prepare Output Paths ────────────────────────────────────────────────────
try { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } catch { }
$Hostname = $env:COMPUTERNAME
$Now = Get-Date -Format 'yyyyMMdd_HHmmss'
$CsvPath = Join-Path $OutputFolder "FileInventory_${Hostname}_$Now.csv"
$JsonPath = Join-Path $OutputFolder "FileInventory_${Hostname}_$Now.json"
# ─── 3️ Build Search Paths ───────────────────────────────────────────────────────
$fsDrives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -and (Test-Path $_.Root) }
$rootPaths = $fsDrives.Root
if (-not $DisableOneDrive -and $env:USERNAME -and $env:USERNAME -ne 'SYSTEM') {
$oneDriveCandidates = @("$env:USERPROFILE\OneDrive", "$env:USERPROFILE\OneDrive - *", "$env:USERPROFILE\OneDrive - * - Personal")
foreach ($p in $oneDriveCandidates) {
$resolved = Get-ChildItem -Path $p -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
if ($resolved) { $rootPaths += $resolved }
}
}
$rootPaths = $rootPaths | Sort-Object -Unique
Write-Host " Scanning $($rootPaths.Count) root path(s) for patterns: $($SearchPatterns -join ', ')" -ForegroundColor Cyan
# ─── 4️ Perform File Scan ────────────────────────────────────────────────────────
$results = [System.Collections.Generic.List[PSObject]]::new()
function New-ResultObject {
param($fileInfo, $fileVersion)
$owner = $null
try { $owner = (Get-Acl -LiteralPath $fileInfo.FullName -ErrorAction Stop).Owner } catch {}
[PSCustomObject]@{
Hostname = $env:COMPUTERNAME
FileName = $fileInfo.Name
FullName = $fileInfo.FullName
FilePath = $fileInfo.DirectoryName
SizeBytes = $fileInfo.Length
DateCreated = $fileInfo.CreationTimeUtc.ToString('o')
DateAccessed = $fileInfo.LastAccessTimeUtc.ToString('o')
DateModified = $fileInfo.LastWriteTimeUtc.ToString('o')
Owner = $owner
ProductName = $fileVersion.ProductName
ProductVersion = $fileVersion.ProductVersion
FileDescription = $fileVersion.FileDescription
ScanTimestamp = (Get-Date).ToString('o')
}
}
foreach ($root in $rootPaths) {
foreach ($pattern in $SearchPatterns) {
try {
$files = Get-ChildItem -LiteralPath $root -Filter $pattern -File -Recurse -ErrorAction SilentlyContinue
} catch {
$files = Get-ChildItem -LiteralPath $root -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -like $pattern }
}
foreach ($f in $files) {
$fv = [PSCustomObject]@{ ProductName = $null; ProductVersion = $null; FileDescription = $null }
if ($IncludeFileVersionInfo) {
try {
$vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($f.FullName)
$fv.ProductName = $vi.ProductName
$fv.ProductVersion = $vi.ProductVersion
$fv.FileDescription = $vi.FileDescription
} catch {}
}
$results.Add((New-ResultObject -fileInfo $f -fileVersion $fv))
}
}
}
# ─── 5️ Write Outputs ───────────────────────────────────────────────────────────
if ($results.Count -gt 0) {
$results | Export-Csv -Path $CsvPath -NoTypeInformation -Force
$results | ConvertTo-Json -Depth 5 | Out-File -FilePath $JsonPath -Encoding UTF8 -Force
Write-Host " Found $($results.Count) file(s). CSV saved to $CsvPath" -ForegroundColor Green
} else {
Write-Host " No matching files found." -ForegroundColor Yellow
}
# ─── 6️ Upload to SharePoint ────────────────────────────────────────────────────
Write-Host " Uploading results to SharePoint..." -ForegroundColor Cyan
$Tenant = "WARYAATECH"
$ClientID = "b363318e-20e3-4340-88f6-a764aac4e0c5"
$Secret = "yEo8Q~edXKnNvCDNd~xx"
$SharePoint_SiteID = "2662f6e0-7e88-415d-a825-a86a6837a73b"
$SharePoint_Path = "https://waryaatech.sharepoint.com/sites/Waryaastorage/Shared%20DocumentS"
$SharePoint_ExportFolder = "FileScanResults"
$Body = @{
client_id = $ClientID
client_secret = $Secret
scope = "https://graph.microsoft.com/.default"
grant_type = 'client_credentials'
}
$Graph_Url = "https://login.microsoftonline.com/$Tenant.onmicrosoft.com/oauth2/v2.0/token"
$Token = Invoke-RestMethod -Uri $Graph_Url -Method Post -Body $Body
$Access_token = $Token.access_token
$Header = @{ Authorization = $Access_token; "Content-Type" = "application/json" }
$SharePoint_Graph_URL = "https://graph.microsoft.com/v1.0/sites/$SharePoint_SiteID/drives"
$Result = Invoke-RestMethod -Uri $SharePoint_Graph_URL -Method GET -Headers $Header
$DriveID = $Result.value | Where-Object { $_.webURL -eq $SharePoint_Path } | Select-Object -ExpandProperty id
$FileName = Split-Path $CsvPath -Leaf
$UploadSessionUri = "https://graph.microsoft.com/v1.0/sites/${SharePoint_SiteID}/drives/${DriveID}/root:/${SharePoint_ExportFolder}/${FileName}:/createUploadSession"
$UploadSession = Invoke-RestMethod -Uri $UploadSessionUri -Method POST -Headers $Header -ContentType "application/json"
$fileBytes = [System.IO.File]::ReadAllBytes($CsvPath)
$fileLength = $fileBytes.Length
$headers = @{ 'Content-Range' = "bytes 0-$($fileLength-1)/$fileLength" }
Invoke-RestMethod -Method Put -Uri $UploadSession.uploadUrl -Body $fileBytes -Headers $headers
Write-Host " Successfully uploaded $FileName to SharePoint folder '$SharePoint_ExportFolder'" -ForegroundColor Green
# ─── 7️⃣ Logging ────────────────────────────────────────────────────────────────
$AuditFile = Join-Path $OutputFolder "FileInventory_Audit.log"
$summary = "$(Get-Date -Format o) - Host:$Hostname - Results:$($results.Count) - OutputCSV:$CsvPath - UploadedTo:$SharePoint_ExportFolder"
Add-Content -Path $AuditFile -Value $summary -ErrorAction SilentlyContinue
exit 0
How to Deploy via Intune (Win32 App Method) or SCCM Package/Application
You can deploy this script from Intune as Win32 Application, Or if you need to deploy this as script, then you can specify the search string inside the script directly.
If you are using SCCM, then you can create this as Package model and the install command you can set as
powershell -ep bypass -file “ScriptName.ps1” “*google*”
This script also search multiple patterns where you can pass the arguments like below
powershell -ep bypass -file “ScriptName.ps1” “*google*”, “*zoom*”
Result
Once deployed, each endpoint will:
-
Scan its local storage for your defined file patterns
-
Generate CSV and JSON reports
-
Upload them to SharePoint using Graph API
You’ll now have a centralized dashboard of file inventories, directly accessible through your SharePoint library.
Conclusion
Automating file discovery and report uploads saves hours of manual effort and eliminates human error. By combining PowerShell, Intune, and Microsoft Graph, we’ve turned what used to be a tedious IT task into a fully autonomous workflow.
This approach scales across your entire organization and provides real-time visibility into your environment. Whether it’s for compliance, audits, or vulnerability checks. you now have a single automated solution doing the heavy lifting.
