Public/Export-CloudInventory.ps1

function Export-CloudInventory {
    <#
        .SYNOPSIS
            Exports all connected cloud inventory to a file.

        .DESCRIPTION
            Exports a point-in-time snapshot of every resource across every connected cloud
            to a file in JSON or CSV format. Useful for compliance audits, before/after snapshots,
            and inventory diffing.

        .EXAMPLE
            Export-CloudInventory -Path 'inventory.json'

            Exports all resources from all connected providers to inventory.json (JSON format).

        .EXAMPLE
            Export-CloudInventory -Path 'inventory.csv' -Format Csv

            Exports all resources to inventory.csv in CSV format.

        .EXAMPLE
            Export-CloudInventory -Path 'azure-inventory.json' -Provider Azure -Kind Instance, Disk

            Exports only Azure instances and disks to azure-inventory.json.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.IO.FileInfo])]
    param(
        # The output file path.
        [Parameter(Mandatory, Position = 0)]
        [string]$Path,

        # The output format.
        [ValidateSet('Json', 'Csv')]
        [string]$Format = 'Json',

        # The resource kinds to include.
        [ValidateSet('Instance', 'Disk', 'Storage', 'Network', 'Function')]
        [string[]]$Kind = @('Instance', 'Disk', 'Storage', 'Network', 'Function'),

        # Limit to specific providers.
        [ValidateSet('Azure', 'AWS', 'GCP')]
        [string[]]$Provider
    )

    process {
        # Resolve provider list
        $providersToExport = if ($Provider) {
            $Provider | Where-Object { $script:PSCumulusContext.Providers[$_] }
        } else {
            @('Azure', 'AWS', 'GCP') | Where-Object { $script:PSCumulusContext.Providers[$_] }
        }

        # Build (provider, kind) matrix and collect records
        $inventory = [ordered]@{}

        $azureRgCacheLoaded = $false

        try {
            foreach ($providerName in $providersToExport) {
                foreach ($kindName in $Kind) {
                    $key = "$providerName/$kindName"
                    $commandName = "Get-Cloud$kindName"
                    $scopeParameterSets = [System.Collections.Generic.List[hashtable]]::new()

                    # Add provider-specific scope from context
                    $ctx = $script:PSCumulusContext.Providers[$providerName]
                    $skipProvider = $false

                    switch ($providerName) {
                        'Azure' {
                            if (-not $azureRgCacheLoaded) {
                                $azureRgCacheLoaded = $true
                                if (Get-Command Get-AzResourceGroup -ErrorAction SilentlyContinue) {
                                    $script:__PSCumulusInventoryAzureRgCache = @(Get-AzResourceGroup -ErrorAction SilentlyContinue)
                                } else {
                                    $script:__PSCumulusInventoryAzureRgCache = @()
                                }
                            }

                            if ($script:__PSCumulusInventoryAzureRgCache.Count -gt 0) {
                                foreach ($rg in $script:__PSCumulusInventoryAzureRgCache) {
                                    if (-not [string]::IsNullOrWhiteSpace($rg.ResourceGroupName)) {
                                        $scopeParameterSets.Add(@{ ResourceGroup = $rg.ResourceGroupName })
                                    }
                                }
                            } else {
                                Write-Verbose "Export-CloudInventory: no resource groups returned for Azure subscription $($ctx.SubscriptionId); skipping."
                                $skipProvider = $true
                            }
                            break
                        }
                        'AWS' {
                            if ($ctx.Region) {
                                $scopeParameterSets.Add(@{ Region = $ctx.Region })
                            } else {
                                Write-Verbose "Export-CloudInventory: no region found for AWS context; skipping."
                                $skipProvider = $true
                            }
                            break
                        }
                        'GCP' {
                            if ($ctx.Project) {
                                $scopeParameterSets.Add(@{ Project = $ctx.Project })
                            } else {
                                Write-Verbose "Export-CloudInventory: no project found for GCP context; skipping."
                                $skipProvider = $true
                            }
                            break
                        }
                    }

                    if ($skipProvider) {
                        continue
                    }

                    $mergedRecords = [System.Collections.Generic.List[psobject]]::new()
                    foreach ($scopeParams in $scopeParameterSets) {
                        try {
                            $commandParams = @{ Provider = $providerName }
                            foreach ($paramName in $scopeParams.Keys) {
                                $commandParams[$paramName] = $scopeParams[$paramName]
                            }

                            $records = & $commandName @commandParams -ErrorAction SilentlyContinue
                            foreach ($record in @($records)) {
                                if ($null -ne $record) {
                                    $mergedRecords.Add($record)
                                }
                            }
                        } catch {
                            Write-Verbose "Export-CloudInventory: Failed to query $key`: $_"
                        }
                    }

                    $inventory[$key] = @($mergedRecords)
                }
            }
        } finally {
            Remove-Variable -Scope Script -Name __PSCumulusInventoryAzureRgCache -ErrorAction SilentlyContinue -WhatIf:$false
        }

        # Export based on format
        if ($PSCmdlet.ShouldProcess($Path, 'Export cloud inventory')) {
            if ($Format -eq 'Json') {
                $json = $inventory | ConvertTo-Json -Depth 8
                $json | Out-File -FilePath $Path -Encoding utf8 -Force
            } else {
                # CSV format: flatten each record
                $flatRecords = [System.Collections.Generic.List[pscustomobject]]::new()

                foreach ($key in $inventory.Keys) {
                    $parts = $key -split '/'
                    $providerName = $parts[0]
                    $kindName = $parts[1]

                    foreach ($record in $inventory[$key]) {
                        $flat = [PSCustomObject]@{
                            Provider    = $providerName
                            Kind        = $kindName
                            Name        = $record.Name
                            Id          = $record.Id
                            Status      = $record.Status
                            Tags        = if ($record.Tags) { ($record.Tags.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ';' } else { $null }
                            Metadata    = if ($record.Metadata) { ($record.Metadata.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ';' } else { $null }
                        }

                        # Add provider-specific properties
                        switch ($providerName) {
                            'Azure' {
                                if ($record.PSObject.Properties.Match('ResourceGroup').Count) {
                                    $flat | Add-Member -MemberType NoteProperty -Name 'ResourceGroup' -Value $record.ResourceGroup -Force
                                }
                            }
                            'AWS' {
                                if ($record.PSObject.Properties.Match('Region').Count) {
                                    $flat | Add-Member -MemberType NoteProperty -Name 'Region' -Value $record.Region -Force
                                }
                                if ($record.PSObject.Properties.Match('InstanceId').Count) {
                                    $flat | Add-Member -MemberType NoteProperty -Name 'InstanceId' -Value $record.InstanceId -Force
                                }
                            }
                            'GCP' {
                                if ($record.PSObject.Properties.Match('Project').Count) {
                                    $flat | Add-Member -MemberType NoteProperty -Name 'Project' -Value $record.Project -Force
                                }
                                if ($record.PSObject.Properties.Match('Zone').Count) {
                                    $flat | Add-Member -MemberType NoteProperty -Name 'Zone' -Value $record.Zone -Force
                                }
                            }
                        }

                        $flatRecords.Add($flat)
                    }
                }

                $flatRecords | Export-Csv -Path $Path -NoTypeInformation -Encoding utf8 -Force
            }

            Get-Item -Path $Path
        }
    }
}