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:

  1. Scans your entire device for files matching a name or pattern and generates a detailed inventory report (CSV and JSON).

  2. 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

  1. Register an app in Azure AD with permissions ( Sites.FullControl.All, Sites.ReadWrite.All,Sites.Selected )

  2. 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

  3. 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.