Modules/Private/Reporting/Export-AZTIJsonReport.ps1
|
<#
.Synopsis Export inventory data as a structured JSON report .DESCRIPTION Reads all cache files produced by the processing phase and assembles them into a single structured JSON document with a metadata envelope. The JSON file is written alongside (or instead of) the Excel report depending on the -OutputFormat parameter on Invoke-AzureScout. .Link https://github.com/thisismydemo/azure-scout/Modules/Private/Reporting/Export-AZSCJsonReport.ps1 .COMPONENT This PowerShell Module is part of Azure Scout (AZSC) .NOTES Version: 1.0.0 First Release Date: 15th Oct, 2024 Authors: Claudio Merola #> function Export-AZSCJsonReport { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ReportCache, [Parameter(Mandatory)] [string]$File, [Parameter()] [string]$TenantID, [Parameter()] [object]$Subscriptions, [Parameter()] [ValidateSet('All', 'ArmOnly', 'EntraOnly')] [string]$Scope = 'All', [Parameter()] [object]$Quotas, [Parameter()] [switch]$SecurityCenter, [Parameter()] [switch]$SkipAdvisory, [Parameter()] [switch]$SkipPolicy, [Parameter()] [switch]$IncludeCosts ) Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + ' - Starting JSON report export.') # ── Resolve output path ────────────────────────────────────────────── # Derive the .json path from the .xlsx path $JsonFile = [System.IO.Path]::ChangeExtension($File, '.json') Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - JSON output file: $JsonFile") # ── Build metadata envelope ────────────────────────────────────────── $SubscriptionList = @() if ($Subscriptions) { foreach ($sub in $Subscriptions) { $SubscriptionList += [ordered]@{ id = if ($sub.Id) { $sub.Id } elseif ($sub.SubscriptionId) { $sub.SubscriptionId } else { '' } name = if ($sub.Name) { $sub.Name } else { '' } } } } $Metadata = [ordered]@{ tool = 'AzureScout' version = '1.0.0' tenantId = $TenantID subscriptions = $SubscriptionList generatedAt = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ') scope = $Scope } # ── Discover inventory module folders ──────────────────────────────── $ParentPath = (Get-Item $PSScriptRoot).Parent.Parent $InventoryModulesPath = Join-Path $ParentPath 'Public' 'InventoryModules' $ModuleFolders = Get-ChildItem -Path $InventoryModulesPath -Directory # ── Category mapping ───────────────────────────────────────────────── # Map each InventoryModules folder name to a top-level JSON section. # Identity modules go under "entra"; everything else goes under "arm". $EntraFolders = @('Identity') $ArmData = [ordered]@{} $EntraData = [ordered]@{} $CacheFiles = Get-ChildItem -Path $ReportCache -Recurse -Filter '*.json' -ErrorAction SilentlyContinue foreach ($ModuleFolder in $ModuleFolders) { $FolderName = $ModuleFolder.Name $JSONFileName = "$FolderName.json" $CacheFile = $CacheFiles | Where-Object { $_.Name -eq $JSONFileName } if (-not $CacheFile) { Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - No cache file for folder: $FolderName — skipping.") continue } # Read and parse the cache file $Reader = New-Object System.IO.StreamReader($CacheFile.FullName) $RawJson = $Reader.ReadToEnd() $Reader.Dispose() if ([string]::IsNullOrWhiteSpace($RawJson)) { Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Cache file is empty for folder: $FolderName — skipping.") continue } $CacheData = $RawJson | ConvertFrom-Json # Each cache file contains properties keyed by module name (filename without .ps1). # Collect all modules within this folder into a section object. $SectionData = [ordered]@{} $ModulePath = Join-Path $ModuleFolder.FullName '*.ps1' $ModuleFiles = Get-ChildItem -Path $ModulePath -ErrorAction SilentlyContinue foreach ($Module in $ModuleFiles) { $ModName = $Module.BaseName # e.g. "VirtualMachines" $ModResources = $CacheData.$ModName if ($ModResources -and @($ModResources).Count -gt 0) { # Convert the module name to a camelCase JSON key $JsonKey = $ModName.Substring(0,1).ToLower() + $ModName.Substring(1) $SectionData[$JsonKey] = @($ModResources) Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Added $(@($ModResources).Count) resources from $FolderName/$ModName") } } if ($SectionData.Count -eq 0) { continue } # Convert folder name to camelCase JSON key $FolderKey = $FolderName.Substring(0,1).ToLower() + $FolderName.Substring(1) if ($FolderName -in $EntraFolders) { # Entra data — flatten identity modules directly into the "entra" section foreach ($key in $SectionData.Keys) { $EntraData[$key] = $SectionData[$key] } } else { $ArmData[$FolderKey] = $SectionData } } # ── Build extra data sections ──────────────────────────────────────── # Advisory, Policy, Security Center, Quotas — read from extra-report cache files if present $ExtraData = [ordered]@{} # Advisory if (-not $SkipAdvisory.IsPresent) { $AdvisoryCache = $CacheFiles | Where-Object { $_.Name -eq 'Advisory.json' } if ($AdvisoryCache) { try { $AdvReader = New-Object System.IO.StreamReader($AdvisoryCache.FullName) $AdvRaw = $AdvReader.ReadToEnd() $AdvReader.Dispose() if (-not [string]::IsNullOrWhiteSpace($AdvRaw)) { $ExtraData['advisory'] = ($AdvRaw | ConvertFrom-Json) } } catch { Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Failed to read Advisory cache: $_") } } } # Policy if (-not $SkipPolicy.IsPresent) { $PolicyCache = $CacheFiles | Where-Object { $_.Name -eq 'Policy.json' } if ($PolicyCache) { try { $PolReader = New-Object System.IO.StreamReader($PolicyCache.FullName) $PolRaw = $PolReader.ReadToEnd() $PolReader.Dispose() if (-not [string]::IsNullOrWhiteSpace($PolRaw)) { $ExtraData['policy'] = ($PolRaw | ConvertFrom-Json) } } catch { Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Failed to read Policy cache: $_") } } } # Security Center if ($SecurityCenter.IsPresent) { $SecCache = $CacheFiles | Where-Object { $_.Name -eq 'SecurityCenter.json' } if ($SecCache) { try { $SecReader = New-Object System.IO.StreamReader($SecCache.FullName) $SecRaw = $SecReader.ReadToEnd() $SecReader.Dispose() if (-not [string]::IsNullOrWhiteSpace($SecRaw)) { $ExtraData['security'] = ($SecRaw | ConvertFrom-Json) } } catch { Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Failed to read SecurityCenter cache: $_") } } } # Quotas if ($Quotas) { $QuotaCache = $CacheFiles | Where-Object { $_.Name -eq 'Quotas.json' } if ($QuotaCache) { try { $QuotaReader = New-Object System.IO.StreamReader($QuotaCache.FullName) $QuotaRaw = $QuotaReader.ReadToEnd() $QuotaReader.Dispose() if (-not [string]::IsNullOrWhiteSpace($QuotaRaw)) { $ExtraData['quotas'] = ($QuotaRaw | ConvertFrom-Json) } } catch { Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Failed to read Quotas cache: $_") } } } # ── Assemble final report object ───────────────────────────────────── $Report = [ordered]@{ '_metadata' = $Metadata } if ($Scope -in ('All', 'ArmOnly') -and $ArmData.Count -gt 0) { $Report['arm'] = $ArmData } if ($Scope -in ('All', 'EntraOnly') -and $EntraData.Count -gt 0) { $Report['entra'] = $EntraData } # Merge extra data sections at the top level foreach ($key in $ExtraData.Keys) { $Report[$key] = $ExtraData[$key] } # ── Write JSON file ────────────────────────────────────────────────── try { $JsonOutput = $Report | ConvertTo-Json -Depth 40 $JsonOutput | Out-File -FilePath $JsonFile -Encoding utf8 -Force Write-Host "JSON report saved to: " -ForegroundColor Green -NoNewline Write-Host $JsonFile -ForegroundColor Cyan Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - JSON report written successfully.") } catch { Write-Warning "Failed to write JSON report to '$JsonFile': $_" } return $JsonFile } |