Public/Get-M365UsageReport.ps1
|
function Get-M365UsageReport { <# .SYNOPSIS Retrieves a usage report from Microsoft Graph. .DESCRIPTION Generic cmdlet driven by -ReportName. Report definitions live in Data/ReportDefinitions.psd1. Handles CSV download, BOM stripping, header deduplication, column sanitization. .OUTPUTS [PSCustomObject[]] One object per row in the report. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$AccessToken, [Parameter(Mandatory)] [string]$ReportName, [ValidateSet('D7', 'D30', 'D90', 'D180')] [string]$Period = 'D30' ) # Load report definitions $defsPath = Join-Path $PSScriptRoot '..\Data\ReportDefinitions.psd1' $reportDefs = Import-PowerShellDataFile -Path $defsPath if (-not $reportDefs.ContainsKey($ReportName)) { $validNames = ($reportDefs.Keys | Sort-Object) -join ', ' throw "Unknown ReportName '$ReportName'. Valid names: $validNames" } $def = $reportDefs[$ReportName] $uri = "https://graph.microsoft.com/$($def.ApiVersion)/$($def.Endpoint -f $Period)" Write-M365Log "Fetching $($def.DisplayName)..." $headers = @{ 'Authorization' = "Bearer $AccessToken" } $response = Invoke-WebRequest -Uri $uri -Headers $headers -Method Get # Decode response if ($response.Content -is [byte[]]) { $csvContent = [System.Text.Encoding]::UTF8.GetString($response.Content) } else { $csvContent = $response.Content } # Strip BOM and normalize line endings if ($csvContent.Length -gt 0 -and $csvContent[0] -eq [char]0xFEFF) { $csvContent = $csvContent.Substring(1) } $csvContent = $csvContent -replace "`r`n", "`n" -replace "`r", "`n" # Deduplicate headers (beta APIs may return duplicate column names) $csvLines = $csvContent -split "`n", 2 if ($csvLines.Count -lt 2) { Write-M365Log " No data returned for $($def.DisplayName)" -Level Warning return } $rawHeaders = $csvLines[0] -split ',' $seen = [System.Collections.Generic.Dictionary[string,int]]::new([System.StringComparer]::OrdinalIgnoreCase) $dedupedHeaders = @() foreach ($h in $rawHeaders) { $cleanHeader = ($h -replace '"', '').Trim().TrimEnd(':').Trim() if ($seen.ContainsKey($cleanHeader)) { $seen[$cleanHeader]++ $dedupedHeaders += "${cleanHeader}_$($seen[$cleanHeader])" } else { $seen[$cleanHeader] = 1 $dedupedHeaders += $cleanHeader } } $csvContent = ($dedupedHeaders -join ',') + "`n" + $csvLines[1] $records = $csvContent | ConvertFrom-Csv if (-not $records -or $records.Count -eq 0) { Write-M365Log " No data returned for $($def.DisplayName)" -Level Warning return } Write-M365Log " Retrieved $($records.Count) records" # Sanitize column names to UpperCamelCase and add metadata $ingestionTime = (Get-Date).ToUniversalTime().ToString('o') $periodValue = [int]($Period -replace '\D', '') foreach ($record in $records) { $sanitizedRecord = [PSCustomObject]@{} foreach ($prop in $record.PSObject.Properties) { $sanitizedName = $prop.Name -replace '\s*\(', '' -replace '\)', '' -replace '/', '' -replace '-', '' -replace ':', '' -replace '"', '' $words = $sanitizedName.Trim() -split '\s+' $sanitizedName = ($words | ForEach-Object { if ($_.Length -gt 0) { $_.Substring(0, 1).ToUpper() + $_.Substring(1) } }) -join '' $sanitizedRecord | Add-Member -NotePropertyName $sanitizedName -NotePropertyValue $prop.Value -Force } $sanitizedRecord | Add-Member -NotePropertyName 'IngestionTime' -NotePropertyValue $ingestionTime -Force $sanitizedRecord | Add-Member -NotePropertyName 'IngestionReportPeriod' -NotePropertyValue $periodValue -Force $sanitizedRecord } Write-M365Log "Emitted $($def.DisplayName) records to pipeline" } |