tests/New-SyntheticSampleReport.ps1
|
<#
.SYNOPSIS Generates a synthetic sample-report.json for the Excel test harness. .DESCRIPTION Reads every InventoryModule .ps1 file, extracts the column names from the Reporting section, and generates 2-3 fake rows per module with Contoso-themed data. Outputs a JSON file matching the structure that Export-AZSCJsonReport produces. .NOTES Run once to regenerate tests/datadump/sample-report.json. This script does NOT require an Azure connection. #> [CmdletBinding()] param( [string]$OutputFile ) $RepoRoot = Split-Path $PSScriptRoot -Parent if (-not $OutputFile) { $OutputFile = Join-Path $RepoRoot 'tests' 'datadump' 'sample-report.json' } $InventoryModulesPath = Join-Path $RepoRoot 'Modules' 'Public' 'InventoryModules' $EntraFolders = @('Identity') # ── Fake data pools ────────────────────────────────────────────────────── $Subscriptions = @( @{ id = '00000000-1111-2222-3333-444444444444'; name = 'scout-prod-001' }, @{ id = '11111111-2222-3333-4444-555555555555'; name = 'scout-dev-001' }, @{ id = '22222222-3333-4444-5555-666666666666'; name = 'scout-staging-001' } ) $Locations = @('eastus', 'westus2', 'westeurope', 'northeurope', 'centralus') $ResourceGroups = @('rg-scout-prod-eus', 'rg-scout-dev-wus2', 'rg-scout-shared-weu', 'rg-scout-data-eus', 'rg-scout-network-eus') $TagNames = @('Environment', 'CostCenter', 'Owner') $TagValues = @('Production', 'CC-42', 'platform-team@intergalactic.fish') $SKUs = @('Standard', 'Premium', 'Basic', 'Free', 'Standard_D2s_v3', 'Standard_B2ms') $States = @('Succeeded', 'Running', 'Ready', 'Active', 'Enabled') $VNets = @('vnet-scout-hub-eus', 'vnet-scout-spoke-wus2', 'vnet-scout-prod-weu') $Subnets = @('snet-default', 'snet-workload', 'snet-data', 'snet-web') $Zones = @('1', '2', '3', 'Not Configured') function Get-FakeValue { param([string]$FieldName, [int]$RowIndex) # Common field patterns switch -Wildcard ($FieldName) { 'Subscription' { return $Subscriptions[$RowIndex % 3].name } 'SubscriptionId' { return $Subscriptions[$RowIndex % 3].id } 'Resource Group' { return $ResourceGroups[$RowIndex % $ResourceGroups.Count] } 'Location' { return $Locations[$RowIndex % $Locations.Count] } 'Zone' { return $Zones[$RowIndex % $Zones.Count] } 'Tag Name' { return $TagNames[$RowIndex % $TagNames.Count] } 'Tag Value' { return $TagValues[$RowIndex % $TagValues.Count] } 'Resource U' { return 1 } 'SKU' { return $SKUs[$RowIndex % $SKUs.Count] } 'SKU *' { return $SKUs[$RowIndex % $SKUs.Count] } 'Retiring Feature' { return $null } 'Retiring Date' { return $null } '*State' { return $States[$RowIndex % $States.Count] } '*Status' { return $States[$RowIndex % $States.Count] } 'Provisioning State' { return 'Succeeded' } 'Public Network Access' { return 'Enabled' } '*FQDN' { return "resource-$RowIndex.intergalactic.fish" } '*Endpoint*' { return "https://resource-$RowIndex.intergalactic.fish" } '*URI*' { return "https://resource-$RowIndex.intergalactic.fish" } '*Url*' { return "https://resource-$RowIndex.intergalactic.fish" } 'Virtual Network' { return $VNets[$RowIndex % $VNets.Count] } 'Subnet' { return $Subnets[$RowIndex % $Subnets.Count] } '*IP Address*' { return "10.$($RowIndex + 1).0.$($RowIndex + 10)" } '*IP*' { return "10.$($RowIndex + 1).0.$($RowIndex + 10)" } 'Admin Login' { return 'scoutadmin' } 'Admin Username' { return 'scoutadmin' } '*Time*' { return '2026-01-15T10:30:00Z' } '*Date*' { return '2026-01-15' } '*DateTime*' { return '2026-01-15T10:30:00Z' } '*Version*' { return '1.0.0' } '*Enabled' { return $true } '*Count*' { return ($RowIndex + 1) * 2 } '*Size*' { return ($RowIndex + 1) * 50 } '*Score*' { return [math]::Round(70 + ($RowIndex * 10), 1) } '*Percentage*' { return [math]::Round(0.7 + ($RowIndex * 0.1), 2) } '*Cost*' { return [math]::Round(125.50 + ($RowIndex * 50), 2) } '*Days*' { return 30 } '*Retention*' { return 90 } '*Capacity*' { return ($RowIndex + 1) * 4 } '*Tier' { return $SKUs[$RowIndex % 3] } '*Description' { return "Synthetic test resource $RowIndex" } 'Severity' { return @('High', 'Medium', 'Low')[$RowIndex % 3] } 'Direction' { return @('Inbound', 'Outbound')[$RowIndex % 2] } 'Action' { return @('Allow', 'Deny')[$RowIndex % 2] } 'Protocol' { return @('TCP', 'UDP', 'Any')[$RowIndex % 3] } 'Priority' { return 100 + ($RowIndex * 100) } '*Type' { return "SampleType-$RowIndex" } default { return "sample-$FieldName-$RowIndex" } } } function Get-FieldsFromModule { param([string]$FilePath) $Content = Get-Content $FilePath -Raw $Fields = @() # ── Primary: $Exc.Add('FieldName') — the definitive column list ───── $excMatches = [regex]::Matches($Content, '\$Exc\.Add\(''([^'']+)''\)') foreach ($m in $excMatches) { $field = $m.Groups[1].Value if ($field.Length -gt 1 -and $Fields -notcontains $field) { $Fields += $field } } # ── Fallback: single-quoted keys in @{ 'FieldName' = ... } ────────── if ($Fields.Count -eq 0) { $kvMatches = [regex]::Matches($Content, "'([^']+)'\s*=\s*\$") foreach ($m in $kvMatches) { $field = $m.Groups[1].Value # Skip known non-field patterns (case-sensitive check) if ($field.Length -gt 1 -and $Fields -notcontains $field -and $field -cnotmatch '^\$|^-' -and $field -cnotmatch '^(Processing|Reporting|Import|Export|Write|Get|Set|New|Remove|Start|Stop|Invoke|Test|Clear|Select|Where|Format|Out|Sort|Group|Measure|Compare|ConvertTo|ConvertFrom|Add|Join|Split|Read|ForEach)') { $Fields += $field } } } # ── Fallback 2: unquoted keys in [ordered]@{ Key = $... } ─────────── if ($Fields.Count -eq 0) { $bareMatches = [regex]::Matches($Content, '^\s+([A-Z]\w+)\s*=\s*\$', [System.Text.RegularExpressions.RegexOptions]::Multiline) foreach ($m in $bareMatches) { $field = $m.Groups[1].Value if ($field.Length -gt 1 -and $Fields -notcontains $field -and $field -cnotmatch '^(If|Else|ForEach|While|Switch|Try|Catch|Finally|Param|Begin|Process|End|Return|Break|Continue|Throw|Exit|Function|Filter|Workflow|Configuration|Class|Enum|Using|Trap|Data|DynamicParam|Parallel|Sequence|InlineScript)$') { $Fields += $field } } } return $Fields } # ── Name mapping helper ────────────────────────────────────────────────── # The Reporting modules use different naming patterns for the worksheet tab # vs what goes into the cache. But for the JSON, we just need PascalCase module names. Write-Host "`nGenerating synthetic sample report..." -ForegroundColor Cyan Write-Host "Reading module schemas from: $InventoryModulesPath`n" -ForegroundColor Gray $ArmData = [ordered]@{} $EntraData = [ordered]@{} $ModuleCount = 0 $RowsPerModule = 3 $ModuleFolders = Get-ChildItem -Path $InventoryModulesPath -Directory | Sort-Object Name foreach ($folder in $ModuleFolders) { $folderKey = $folder.Name.Substring(0, 1).ToLower() + $folder.Name.Substring(1) $sectionData = [ordered]@{} $modules = Get-ChildItem $folder.FullName -Filter '*.ps1' | Sort-Object Name foreach ($mod in $modules) { $modName = $mod.BaseName $modKey = $modName.Substring(0, 1).ToLower() + $modName.Substring(1) # Extract fields $fields = Get-FieldsFromModule -FilePath $mod.FullName if ($fields.Count -eq 0) { Write-Host " [!] $($folder.Name)/$modName — no fields found, using defaults" -ForegroundColor Yellow $fields = @('Subscription', 'Resource Group', 'Name', 'Location', 'Resource U') } # Generate fake rows $rows = @() for ($i = 0; $i -lt $RowsPerModule; $i++) { $row = [ordered]@{} foreach ($field in $fields) { $row[$field] = Get-FakeValue -FieldName $field -RowIndex $i } # Override Name field with something descriptive if ($row.Contains('Name')) { $row['Name'] = "scout-$(@('prod','dev','staging')[$i % 3])-$($modName.ToLower())-$($i + 1)" } $rows += $row } $sectionData[$modKey] = $rows $ModuleCount++ } if ($sectionData.Count -gt 0) { if ($folder.Name -in $EntraFolders) { foreach ($key in $sectionData.Keys) { $EntraData[$key] = $sectionData[$key] } Write-Host " [+] Identity ($($sectionData.Count) modules → entra)" -ForegroundColor Green } else { $ArmData[$folderKey] = $sectionData Write-Host " [+] $($folder.Name) ($($sectionData.Count) modules)" -ForegroundColor Green } } } # ── Build the full report structure ────────────────────────────────────── $Report = [ordered]@{ _metadata = [ordered]@{ tool = 'AzureScout' version = '1.0.0' tenantId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' subscriptions = @( [ordered]@{ id = '00000000-1111-2222-3333-444444444444'; name = 'scout-prod-001' }, [ordered]@{ id = '11111111-2222-3333-4444-555555555555'; name = 'scout-dev-001' }, [ordered]@{ id = '22222222-3333-4444-5555-666666666666'; name = 'scout-staging-001' } ) generatedAt = '2026-02-25T12:00:00Z' scope = 'All' } arm = $ArmData entra = $EntraData } # ── Write the JSON ─────────────────────────────────────────────────────── $JsonOutput = $Report | ConvertTo-Json -Depth 20 # Ensure directory exists $OutputDir = Split-Path $OutputFile -Parent if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } $JsonOutput | Out-File -FilePath $OutputFile -Encoding utf8 $FileSize = [math]::Round((Get-Item $OutputFile).Length / 1KB, 1) Write-Host "`n═════════════════════════════════════════════════════" -ForegroundColor DarkCyan Write-Host " Generated: $OutputFile" -ForegroundColor White Write-Host " Modules: $ModuleCount across $($ModuleFolders.Count) categories" -ForegroundColor Gray Write-Host " Rows: $($ModuleCount * $RowsPerModule) total ($RowsPerModule per module)" -ForegroundColor Gray Write-Host " Size: $FileSize KB" -ForegroundColor Gray Write-Host "═════════════════════════════════════════════════════`n" -ForegroundColor DarkCyan |