Get-AzureLandingZoneInventory.ps1

#Requires -Version 7.0
<#
.SYNOPSIS
    Azure Landing Zone Inventory Collection Module
.DESCRIPTION
    Collects comprehensive inventory data from Azure Landing Zone environments including
    management groups, policies, subscriptions, networking, and governance settings.
    
    Network Requirements:
    - Internet connectivity to management.azure.com (port 443)
    - DNS resolution for Azure endpoints
    - If behind a proxy, ensure Azure endpoints are accessible
    
    The script will gracefully skip subscriptions with connectivity issues and continue
    collecting data from accessible resources.
.NOTES
    Requires PowerShell 7.0 or higher
    Required Modules: Az.Accounts, Az.Resources, Az.Network, Az.PolicyInsights
#>


# Script version
$script:Version = "1.0.0"

# Progress file path for real-time progress reporting (per-user app dir, not the shared temp dir)
$script:AppDataDir = Join-Path $HOME '.documenter-azure-landingzone'
if (-not (Test-Path $script:AppDataDir)) {
    New-Item -ItemType Directory -Path $script:AppDataDir -Force | Out-Null
}
$script:ProgressFilePath = Join-Path $script:AppDataDir "inventory-progress.json"

function Update-CollectionProgress {
    param(
        [int]$Step,
        [int]$TotalSteps,
        [string]$Status
    )
    $percentage = [math]::Round(($Step / $TotalSteps) * 95)
    $progressData = @{
        step = $Step
        totalSteps = $TotalSteps
        percentage = $percentage
        status = $Status
        timestamp = (Get-Date).ToString('o')
    } | ConvertTo-Json -Compress
    try {
        [System.IO.File]::WriteAllText($script:ProgressFilePath, $progressData)
    } catch {
        # Non-critical - continue even if progress file write fails
    }
    Write-Host " ○ [$Step/$TotalSteps] $Status" -ForegroundColor Gray
}

# --- Safe scoring-condition evaluation (no Invoke-Expression) ---------------
# Grammar: <token> <op> <token> joined by AND / OR (AND binds tighter than OR).
# Tokens are metric names, numbers, or true/false. Operators: >= <= == != > <

function Resolve-ConditionToken {
    param(
        [string]$Token,
        [hashtable]$Metrics
    )
    $Token = $Token.Trim()
    if ($Metrics.ContainsKey($Token)) { return $Metrics[$Token] }
    if ($Token -eq 'true')  { return $true }
    if ($Token -eq 'false') { return $false }
    $num = 0.0
    if ([double]::TryParse($Token, [ref]$num)) { return $num }
    throw "Unknown token '$Token' in scoring condition"
}

function Test-ConditionComparison {
    param(
        [string]$Expression,
        [hashtable]$Metrics
    )
    if ($Expression -match '^\s*([\w.]+)\s*(>=|<=|==|!=|>|<)\s*([\w.]+)\s*$') {
        $left  = Resolve-ConditionToken -Token $Matches[1] -Metrics $Metrics
        $op    = $Matches[2]
        $right = Resolve-ConditionToken -Token $Matches[3] -Metrics $Metrics
        
        if ($left -is [bool] -or $right -is [bool]) {
            $l = [bool]$left; $r = [bool]$right
            switch ($op) {
                '==' { return $l -eq $r }
                '!=' { return $l -ne $r }
                default { throw "Operator '$op' is not valid for boolean values" }
            }
        }
        
        $l = [double]$left; $r = [double]$right
        switch ($op) {
            '>=' { return $l -ge $r }
            '<=' { return $l -le $r }
            '==' { return $l -eq $r }
            '!=' { return $l -ne $r }
            '>'  { return $l -gt $r }
            '<'  { return $l -lt $r }
        }
    }
    # Bare token — treat as a boolean metric
    return [bool](Resolve-ConditionToken -Token $Expression -Metrics $Metrics)
}

function Test-ScoringCondition {
    param(
        [string]$Condition,
        [hashtable]$Metrics
    )
    foreach ($orPart in ($Condition -split '\bOR\b')) {
        $andResult = $true
        foreach ($andPart in ($orPart -split '\bAND\b')) {
            if (-not (Test-ConditionComparison -Expression $andPart -Metrics $Metrics)) {
                $andResult = $false
                break
            }
        }
        if ($andResult) { return $true }
    }
    return $false
}

function Get-AzureLandingZoneInventory {
    [CmdletBinding()]
    param()
    
    # Verify required modules are loaded
    $requiredModules = @('Az.Accounts', 'Az.Resources', 'Az.Network', 'Az.PolicyInsights')
    $missingModules = @()
    
    foreach ($module in $requiredModules) {
        if (-not (Get-Module -Name $module)) {
            $missingModules += $module
        }
    }
    
    if ($missingModules.Count -gt 0) {
        Write-Host "❌ ERROR: Required modules not loaded: $($missingModules -join ', ')" -ForegroundColor Red
        Write-Host "Please import the required modules before running this function." -ForegroundColor Yellow
        throw "Required modules not loaded: $($missingModules -join ', ')"
    }
    
    $totalSteps = 11
    Update-CollectionProgress -Step 0 -TotalSteps $totalSteps -Status 'Initializing inventory collection...'
    Write-Host " Note: Requires connectivity to management.azure.com and Azure endpoints" -ForegroundColor Gray
    
    $inventory = @{
        version = $script:Version
        collectionTime = (Get-Date).ToString('o')
        tenantId = (Get-AzContext).Tenant.Id
        managementGroups = @()
        subscriptions = @()
        policies = @{
            definitions = @()
            initiatives = @()
            assignments = @()
        }
        roleAssignments = @()
        networking = @{
            vnets = @()
            peerings = @()
            vpnGateways = @()
            expressRoutes = @()
            virtualWans = @()
            firewalls = @()
            firewallPolicies = @()
            networkSecurityGroups = @()
            privateDnsZones = @()
            privateEndpoints = @()
        }
        compute = @{
            virtualMachines = @()
        }
        governance = @{
            budgets = @()
            tags = @{}
            locks = @()
            diagnosticSettings = @()
        }
        summary = @{
            totalManagementGroups = 0
            totalSubscriptions = 0
            totalPolicyDefinitions = 0
            totalPolicyInitiatives = 0
            totalPolicyAssignments = 0
            totalRoleAssignments = 0
            totalVNets = 0
            totalPeerings = 0
            totalBudgets = 0
            totalLocks = 0
            totalVMs = 0
            totalPrivateDnsZones = 0
            totalPrivateEndpoints = 0
            totalVirtualWans = 0
            totalFirewalls = 0
            totalFirewallPolicies = 0
        }
        explanations = @{
            overview = @"
Azure Landing Zone is an enterprise-scale architecture pattern that provides a standardized foundation for cloud adoption.
It implements governance, security, networking, and identity best practices aligned with the Cloud Adoption Framework (CAF).

Key Components:
• Management Groups: Hierarchical containers for organizing subscriptions
• Policies: Governance rules enforced across workloads
• Role Assignments: Identity and access management (IAM) controls
• Networking: Hub-spoke topology for secure, scalable connectivity
• Governance: Budgets, locks, tags, and diagnostic settings
"@

            managementGroups = @"
Management Groups provide a hierarchical structure for organizing subscriptions and applying governance controls at scale.

Structure:
• Root Management Group: Top-level container for the tenant
• Platform: Infrastructure services (identity, management, connectivity)
• Landing Zones: Application workloads (corp-connected, online)
• Sandboxes: Development and testing environments
• Decommissioned: Archived or sunset resources

Each management group can have policies, role assignments, and budgets applied that cascade to all child subscriptions.
"@

            policies = @"
Azure Policy helps enforce organizational standards and assess compliance at scale.

Types:
• Policy Definitions: Individual rules (e.g., "Require tag on resources")
• Policy Initiatives (Sets): Groups of related policies (e.g., "CIS Benchmark")
• Policy Assignments: Application of policies to specific scopes

Effects:
• Deny: Block non-compliant resource creation
• Audit: Log non-compliance without blocking
• DeployIfNotExists: Automatically remediate resources
• Modify: Change resource properties to comply
• Disabled: Policy exists but is not enforced

Policies enable automated compliance monitoring and enforcement across all workloads.
"@

            roleAssignments = @"
Role-Based Access Control (RBAC) manages who can access Azure resources and what they can do.

Key Concepts:
• Security Principal: User, group, service principal, or managed identity
• Role Definition: Collection of permissions (e.g., Owner, Contributor, Reader)
• Scope: Where the assignment applies (management group, subscription, resource group, resource)

Built-in Roles:
• Owner: Full access including the ability to assign roles
• Contributor: Full access except role assignment
• Reader: View-only access
• Custom Roles: Tailored permissions for specific scenarios

Best Practices:
• Use least-privilege principle
• Assign roles to groups, not individual users
• Regular access reviews and audits
• Use managed identities for service-to-service authentication
"@

            networking = @"
Azure Landing Zone networking follows a hub-spoke topology for secure, scalable connectivity.

Hub-Spoke Architecture:
• Hub VNet: Central connectivity point with shared services
  - VPN Gateway or ExpressRoute for on-premises connectivity
  - Azure Firewall for traffic inspection and filtering
  - DNS and other shared services
• Spoke VNets: Isolated workload networks
  - Application resources and services
  - Peered to hub for centralized connectivity
  - Network security groups for micro-segmentation

Connectivity Options:
• VNet Peering: High-speed Azure network connections
• VPN Gateway: Encrypted tunnels over internet
• ExpressRoute: Private dedicated connection to on-premises
• Azure Firewall: Network and application-level filtering
• Virtual WAN: Microsoft-managed hub for global connectivity

Security:
• Network Security Groups (NSGs): Subnet/NIC-level firewall rules
• Application Security Groups: Group VMs by application role
• Service Endpoints: Private connectivity to Azure services
• Private Link: Private IP access to PaaS services

Private Connectivity:
• Private DNS Zones: DNS resolution for private endpoints and custom domains
  - Integrated with VNets for automatic registration
  - Support for Azure service-specific zones (privatelink.*)
  - Centralized DNS management across landing zones
• Private Endpoints: Private IP addresses for Azure PaaS services
  - Eliminates exposure to public internet
  - Traffic stays on Microsoft backbone network
  - Integrates with Private DNS Zones for name resolution
  - Supports blob storage, SQL databases, Key Vault, and more
"@

            governance = @"
Governance ensures consistent management, security, and compliance across all Azure resources.

Budgets:
• Spending thresholds with alerting
• Forecast-based monitoring
• Action groups for automated responses

Resource Locks:
• ReadOnly: Prevents modifications
• CanNotDelete: Prevents deletion
• Applied at subscription, resource group, or resource scope
• Inherited by child resources

Tags:
• Key-value pairs for resource organization
• Enable cost tracking and allocation
• Support automation and lifecycle management
• Common tags: Environment, Owner, CostCenter, Application

Diagnostic Settings:
• Log and metric collection configuration
• Send to Log Analytics, Storage, or Event Hub
• Activity logs, resource logs, and metrics
• Enable monitoring, auditing, and troubleshooting

Blueprints (deprecated, use Template Specs):
• Declarative definition of environment
• Includes ARM templates, policies, role assignments
• Versioning and tracking of deployments
"@

            subscriptions = @"
Azure subscriptions are billing and management boundaries within the landing zone.

Placement Strategy:
• Platform Subscriptions:
  - Identity: AD DS, Azure AD Connect
  - Management: Monitoring, backup, governance tooling
  - Connectivity: Hub networking, VPN/ExpressRoute
• Landing Zone Subscriptions:
  - Corp-Connected: On-premises connectivity required
  - Online: Internet-facing workloads
  - Sandbox: Development and testing
• Decommissioned: Sunset applications

Subscription Limits:
• Resource quotas (VMs, storage, networking)
• API throttling limits
• Service-specific constraints

Move Strategy:
• Move between management groups for different governance
• Subscription transfer for billing ownership change
• Resource move for reorganization within/across subscriptions
"@

        }
    }
    
    try {
        # Get Management Groups
        Update-CollectionProgress -Step 1 -TotalSteps $totalSteps -Status 'Collecting Management Groups...'
        
        # Store current context
        $currentContext = Get-AzContext
        Write-Host " ○ Current Context: Tenant=$($currentContext.Tenant.Id), Sub=$($currentContext.Subscription.Name)" -ForegroundColor Gray
        Write-Host " ○ Authenticated as: $($currentContext.Account.Id)" -ForegroundColor Gray
        
        try {
            # Management groups are tenant-level resources and don't require subscription context
            # Some errors about resource provider registration can be safely ignored
            $mgList = @()
            
            try {
                $mgList = @(Get-AzManagementGroup -ErrorAction Stop)
            } catch {
                # If we get a resource provider registration error, try again with SilentlyContinue
                # This error is misleading - MGs don't need subscription-level resource provider registration
                if ($_.Exception.Message -match 'Microsoft\.Management/register/action|resource provider|does not have authorization') {
                    Write-Host " ○ Note: Ignoring subscription-level permission check (MGs are tenant-level)..." -ForegroundColor Gray
                    $mgList = @(Get-AzManagementGroup -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)
                } else {
                    # Real error, report it
                    throw
                }
            }
            
            $inventory.summary.totalManagementGroups = $mgList.Count
            Write-Host " ○ Found $($mgList.Count) management group(s) at tenant level" -ForegroundColor Gray
            
            foreach ($mg in $mgList) {
                try {
                    $mgDetails = Get-AzManagementGroup -GroupId $mg.Name -Expand -Recurse -ErrorAction SilentlyContinue
                    $inventory.managementGroups += @{
                        id = $mg.Id
                        name = $mg.Name
                        displayName = $mg.DisplayName
                        tenantId = $mg.TenantId
                        type = $mg.Type
                        children = @($mgDetails.Children | ForEach-Object {
                            @{
                                id = $_.Id
                                name = $_.Name
                                displayName = $_.DisplayName
                                type = $_.Type
                            }
                        })
                        parentName = if ($mgDetails.ParentName) { $mgDetails.ParentName } else { "Root" }
                    }
                } catch {
                    Write-Host " ⚠️ Could not get details for MG: $($mg.Name) - $($_.Exception.Message)" -ForegroundColor Yellow
                }
            }
            Write-Host " ✓ Collected $($inventory.summary.totalManagementGroups) management groups" -ForegroundColor Green
        } catch {
            Write-Host " ⚠️ Unexpected error accessing Management Groups: $($_.Exception.Message)" -ForegroundColor Yellow
            Write-Host " ○ Error details: $($_.Exception.GetType().FullName)" -ForegroundColor Gray
            Write-Host " ○ This is not a common subscription-level permission issue" -ForegroundColor Gray
            # Check if it's a tenant-level permissions issue
            if ($_.Exception.Message -match 'AuthorizationFailed|Forbidden') {
                Write-Host " ℹ️ Tip: Ensure you have 'Management Group Reader' role at tenant root level" -ForegroundColor Cyan
            }
        }
        
        # Get Subscriptions
        Update-CollectionProgress -Step 2 -TotalSteps $totalSteps -Status 'Collecting Subscriptions...'
        $subs = Get-AzSubscription
        $inventory.summary.totalSubscriptions = $subs.Count
        
        foreach ($sub in $subs) {
            try {
                # Try to set context with better error handling
                try {
                    Set-AzContext -SubscriptionId $sub.Id -ErrorAction Stop | Out-Null
                } catch {
                    if ($_.Exception.Message -match 'No such host is known|Unable to acquire token') {
                        Write-Host " ⚠️ Network connectivity issue for subscription: $($sub.Name) - Skipping" -ForegroundColor Yellow
                        continue
                    } else {
                        throw
                    }
                }
                
                $subDetails = @{
                    id = $sub.Id
                    name = $sub.Name
                    state = $sub.State
                    subscriptionId = $sub.SubscriptionId
                    tenantId = $sub.TenantId
                    tags = @{}
                }
                
                # Get subscription tags
                try {
                    $subResource = Get-AzSubscription -SubscriptionId $sub.Id
                    if ($subResource.Tags) {
                        $subDetails.tags = $subResource.Tags
                    }
                } catch {}
                
                $inventory.subscriptions += $subDetails
            } catch {
                Write-Host " ⚠️ Could not access subscription: $($sub.Name)" -ForegroundColor Yellow
            }
        }
        Write-Host " ✓ Collected $($inventory.summary.totalSubscriptions) subscriptions" -ForegroundColor Green
        
        # Get Policy Definitions
        Update-CollectionProgress -Step 3 -TotalSteps $totalSteps -Status 'Collecting Policy Definitions...'
        try {
            # Collect both custom and built-in policies
            $customPolicyDefs = Get-AzPolicyDefinition -Custom -ErrorAction SilentlyContinue
            $builtInPolicyDefs = Get-AzPolicyDefinition -Builtin -ErrorAction SilentlyContinue
            $allPolicyDefs = @($customPolicyDefs) + @($builtInPolicyDefs)
            
            $inventory.summary.totalPolicyDefinitions = $customPolicyDefs.Count
            
            foreach ($policy in $allPolicyDefs) {
                # Try to get display name, falling back to name or a friendly message
                $displayName = if ($policy.Properties.DisplayName) { 
                    $policy.Properties.DisplayName 
                } elseif ($policy.DisplayName) { 
                    $policy.DisplayName 
                } elseif ($policy.Name) { 
                    $policy.Name 
                } else { 
                    'Unnamed Policy' 
                }
                
                # Try to get policy type from multiple locations
                $policyType = if ($policy.Properties.PolicyType) { 
                    $policy.Properties.PolicyType 
                } elseif ($policy.PolicyType) { 
                    $policy.PolicyType 
                } else { 
                    'Custom' 
                }
                
                $inventory.policies.definitions += @{
                    name = if ($policy.Name) { $policy.Name } else { 'Unknown' }
                    displayName = $displayName
                    description = if ($policy.Properties.Description) { $policy.Properties.Description } else { 'No description available' }
                    policyType = $policyType
                    mode = if ($policy.Properties.Mode) { $policy.Properties.Mode } else { 'All' }
                    metadata = $policy.Properties.Metadata
                }
            }
        } catch {
            Write-Host " ⚠️ Error: $($_.Exception.Message)" -ForegroundColor Yellow
        }
        
        # Get Policy Initiatives (SetDefinitions)
        Update-CollectionProgress -Step 4 -TotalSteps $totalSteps -Status 'Collecting Policy Initiatives...'
        try {
            # Collect both custom and built-in initiatives
            $customInitiatives = Get-AzPolicySetDefinition -Custom -ErrorAction SilentlyContinue
            $builtInInitiatives = Get-AzPolicySetDefinition -Builtin -ErrorAction SilentlyContinue
            $allInitiatives = @($customInitiatives) + @($builtInInitiatives)
            
            $inventory.summary.totalPolicyInitiatives = $customInitiatives.Count
            
            foreach ($initiative in $allInitiatives) {
                # Try to get display name from multiple locations
                $displayName = if ($initiative.Properties.DisplayName) { 
                    $initiative.Properties.DisplayName 
                } elseif ($initiative.DisplayName) { 
                    $initiative.DisplayName
                } elseif ($initiative.Name) { 
                    $initiative.Name 
                } else { 
                    'Unnamed Initiative' 
                }
                
                # Try to get policy type from multiple possible locations
                $policyType = if ($initiative.Properties.PolicyType) { 
                    $initiative.Properties.PolicyType 
                } elseif ($initiative.PolicyType) { 
                    $initiative.PolicyType 
                } else { 
                    'BuiltIn' 
                }
                
                $inventory.policies.initiatives += @{
                    name = if ($initiative.Name) { $initiative.Name } else { 'Unknown' }
                    displayName = $displayName
                    description = if ($initiative.Properties.Description) { $initiative.Properties.Description } else { 'No description available' }
                    policyType = $policyType
                    metadata = $initiative.Properties.Metadata
                    policyDefinitions = @($initiative.Properties.PolicyDefinitions | ForEach-Object {
                        @{
                            policyDefinitionId = $_.policyDefinitionId
                            parameters = $_.parameters
                        }
                    })
                }
            }
        } catch {
            Write-Host " ⚠️ Error: $($_.Exception.Message)" -ForegroundColor Yellow
        }
        
        # Get Policy Assignments
        Update-CollectionProgress -Step 5 -TotalSteps $totalSteps -Status 'Collecting Policy Assignments...'
        try {
            $assignments = Get-AzPolicyAssignment -ErrorAction SilentlyContinue
            $inventory.summary.totalPolicyAssignments = $assignments.Count
            
            foreach ($assignment in $assignments) {
                # Extract policy name from definition ID
                $policyName = 'Unknown'
                if ($assignment.Properties.PolicyDefinitionId) {
                    $policyName = Split-Path $assignment.Properties.PolicyDefinitionId -Leaf
                }
                
                # Try to get display name from multiple locations
                $displayName = if ($assignment.Properties.DisplayName) { 
                    $assignment.Properties.DisplayName 
                } elseif ($assignment.DisplayName) { 
                    $assignment.DisplayName 
                } elseif ($assignment.Name) { 
                    $assignment.Name 
                } else { 
                    "Assignment of $policyName" 
                }
                
                # Try to get scope from multiple possible locations
                $assignmentScope = if ($assignment.Properties.Scope) { 
                    $assignment.Properties.Scope 
                } elseif ($assignment.Scope) { 
                    $assignment.Scope 
                } else { 
                    'Not specified' 
                }
                
                $inventory.policies.assignments += @{
                    name = if ($assignment.Name) { $assignment.Name } else { 'Unknown' }
                    displayName = $displayName
                    description = if ($assignment.Properties.Description) { $assignment.Properties.Description } else { "Assignment of $policyName" }
                    enforcementMode = if ($assignment.Properties.EnforcementMode) { $assignment.Properties.EnforcementMode } else { 'Default' }
                    scope = $assignmentScope
                    policyDefinitionId = $assignment.Properties.PolicyDefinitionId
                    policyName = $policyName
                    parameters = $assignment.Properties.Parameters
                    notScopes = $assignment.Properties.NotScopes
                }
            }
        } catch {
            Write-Host " ⚠️ Error: $($_.Exception.Message)" -ForegroundColor Yellow
        }
        
        # Get Role Assignments
        Update-CollectionProgress -Step 6 -TotalSteps $totalSteps -Status 'Collecting Role Assignments...'
        try {
            $roles = Get-AzRoleAssignment -ErrorAction SilentlyContinue
            $inventory.summary.totalRoleAssignments = $roles.Count
            
            foreach ($role in $roles) {
                $inventory.roleAssignments += @{
                    displayName = $role.DisplayName
                    signInName = $role.SignInName
                    roleDefinitionName = $role.RoleDefinitionName
                    roleDefinitionId = $role.RoleDefinitionId
                    scope = $role.Scope
                    objectType = $role.ObjectType
                    objectId = $role.ObjectId
                }
            }
        } catch {
            Write-Host " ⚠️ Could not retrieve role assignments" -ForegroundColor Yellow
        }
        
        # Get Networking Resources (loop through subscriptions)
        Update-CollectionProgress -Step 7 -TotalSteps $totalSteps -Status 'Collecting Networking Resources...'
        foreach ($sub in $subs) {
            try {
                # Try to set context with better error handling
                try {
                    Set-AzContext -SubscriptionId $sub.Id -ErrorAction Stop | Out-Null
                } catch {
                    if ($_.Exception.Message -match 'No such host is known|Unable to acquire token') {
                        Write-Host " ⚠️ Network connectivity issue for subscription: $($sub.Name) - Skipping" -ForegroundColor Yellow
                        continue
                    } else {
                        throw
                    }
                }
                
                # Virtual Networks
                $vnets = Get-AzVirtualNetwork -ErrorAction SilentlyContinue
                foreach ($vnet in $vnets) {
                    $inventory.networking.vnets += @{
                        name = $vnet.Name
                        resourceGroup = $vnet.ResourceGroupName
                        location = $vnet.Location
                        addressSpace = $vnet.AddressSpace.AddressPrefixes
                        subnets = @($vnet.Subnets | ForEach-Object {
                            @{
                                name = $_.Name
                                addressPrefix = $_.AddressPrefix
                                serviceEndpoints = @($_.ServiceEndpoints | ForEach-Object { $_.Service })
                            }
                        })
                        dnsServers = $vnet.DhcpOptions.DnsServers
                        tags = $vnet.Tag
                        subscription = $sub.Name
                    }
                }
                $inventory.summary.totalVNets += $vnets.Count
                
                # VNet Peerings
                foreach ($vnet in $vnets) {
                    $peerings = $vnet.VirtualNetworkPeerings
                    foreach ($peer in $peerings) {
                        $inventory.networking.peerings += @{
                            name = $peer.Name
                            id = $peer.Id
                            sourceVNet = $vnet.Name
                            sourceVNetId = $vnet.Id
                            sourceResourceGroup = $vnet.ResourceGroupName
                            remoteVNet = Split-Path $peer.RemoteVirtualNetwork.Id -Leaf
                            remoteVNetId = $peer.RemoteVirtualNetwork.Id
                            peeringState = $peer.PeeringState
                            provisioningState = $peer.ProvisioningState
                            allowForwardedTraffic = $peer.AllowForwardedTraffic
                            allowGatewayTransit = $peer.AllowGatewayTransit
                            allowVirtualNetworkAccess = $peer.AllowVirtualNetworkAccess
                            doNotVerifyRemoteGateways = $peer.DoNotVerifyRemoteGateways
                            useRemoteGateways = $peer.UseRemoteGateways
                            remoteAddressSpace = $peer.RemoteVirtualNetworkAddressSpace.AddressPrefixes
                            subscription = $sub.Name
                        }
                    }
                }
                $inventory.summary.totalPeerings = $inventory.networking.peerings.Count
                
                # VPN Gateways - use Get-AzResource to find them
                try {
                    $vpnGatewayResources = Get-AzResource -ResourceType 'Microsoft.Network/virtualNetworkGateways' -ErrorAction SilentlyContinue
                    foreach ($vpnResource in $vpnGatewayResources) {
                        try {
                            $vpn = Get-AzVirtualNetworkGateway -ResourceGroupName $vpnResource.ResourceGroupName -Name $vpnResource.Name -ErrorAction SilentlyContinue
                            if ($vpn) {
                                $inventory.networking.vpnGateways += @{
                                    name = $vpn.Name
                                    resourceGroup = $vpn.ResourceGroupName
                                    location = $vpn.Location
                                    gatewayType = $vpn.GatewayType
                                    vpnType = $vpn.VpnType
                                    sku = $vpn.Sku.Name
                                    activeActive = $vpn.ActiveActive
                                    enableBgp = $vpn.EnableBgp
                                    subscription = $sub.Name
                                }
                            }
                        } catch {}
                    }
                } catch {}
                
                # Azure Firewalls - use Get-AzResource to find them
                try {
                    $firewallResources = Get-AzResource -ResourceType 'Microsoft.Network/azureFirewalls' -ErrorAction SilentlyContinue
                    foreach ($fwResource in $firewallResources) {
                        try {
                            $fw = Get-AzFirewall -ResourceGroupName $fwResource.ResourceGroupName -Name $fwResource.Name -ErrorAction SilentlyContinue
                            if ($fw) {
                                # Check if using Firewall Policy
                                $firewallPolicyId = $null
                                $firewallPolicyName = $null
                                $totalRules = 0
                                
                                if ($fw.FirewallPolicy -and $fw.FirewallPolicy.Id) {
                                    $firewallPolicyId = $fw.FirewallPolicy.Id
                                    $firewallPolicyName = Split-Path $firewallPolicyId -Leaf
                                } else {
                                    # Classic rules (not using policy)
                                    $totalRules = $fw.ApplicationRuleCollections.Count + $fw.NetworkRuleCollections.Count + $fw.NatRuleCollections.Count
                                }
                                
                                $inventory.networking.firewalls += @{
                                    name = $fw.Name
                                    resourceGroup = $fw.ResourceGroupName
                                    location = $fw.Location
                                    tier = $fw.Sku.Tier
                                    threatIntelMode = $fw.ThreatIntelMode
                                    applicationRuleCollections = $fw.ApplicationRuleCollections.Count
                                    networkRuleCollections = $fw.NetworkRuleCollections.Count
                                    natRuleCollections = $fw.NatRuleCollections.Count
                                    totalClassicRules = $totalRules
                                    firewallPolicyId = $firewallPolicyId
                                    firewallPolicyName = $firewallPolicyName
                                    usingPolicy = ($null -ne $firewallPolicyId)
                                    subscription = $sub.Name
                                }
                            }
                        } catch {}
                    }
                    $inventory.summary.totalFirewalls += $firewallResources.Count
                } catch {}
                
                # Azure Firewall Policies
                try {
                    $firewallPolicyResources = Get-AzResource -ResourceType 'Microsoft.Network/firewallPolicies' -ErrorAction SilentlyContinue
                    foreach ($policyResource in $firewallPolicyResources) {
                        try {
                            $policy = Get-AzFirewallPolicy -ResourceGroupName $policyResource.ResourceGroupName -Name $policyResource.Name -ErrorAction SilentlyContinue
                            if ($policy) {
                                # Get rule collection groups via REST API (more reliable than cmdlets for nested data)
                                $rcgApiPath = "$($policy.Id)/ruleCollectionGroups?api-version=2023-11-01"
                                $rcgListResponse = Invoke-AzRestMethod -Path $rcgApiPath -Method GET -ErrorAction SilentlyContinue
                                $rcgList = @()
                                if ($rcgListResponse -and $rcgListResponse.StatusCode -eq 200) {
                                    $rcgListBody = $rcgListResponse.Content | ConvertFrom-Json -Depth 20 -ErrorAction SilentlyContinue
                                    if ($rcgListBody.value) {
                                        $rcgList = @($rcgListBody.value)
                                    }
                                }
                                
                                $totalRuleCollections = 0
                                $totalRules = 0
                                $applicationRuleCollections = 0
                                $networkRuleCollections = 0
                                $natRuleCollections = 0
                                $ruleCollectionGroupCount = 0
                                $ruleCollectionGroupDetails = @()
                                
                                # Parse rule details directly from the REST API response
                                foreach ($rcgItem in $rcgList) {
                                    try {
                                        $rcgProps = $rcgItem.properties
                                        if (-not $rcgProps) { continue }
                                        
                                        $ruleCollectionGroupCount++
                                        $rcgDetail = @{
                                            name            = (Split-Path $rcgItem.id -Leaf)
                                            priority        = $rcgProps.priority
                                            ruleCollections = @()
                                        }
                                        
                                        $ruleCollections = @($rcgProps.ruleCollections)
                                        if ($ruleCollections.Count -gt 0) {
                                            $totalRuleCollections += $ruleCollections.Count
                                            
                                            foreach ($rc in $ruleCollections) {
                                                $rcDetail = @{
                                                    name               = $rc.name
                                                    priority           = $rc.priority
                                                    action             = $rc.action.type
                                                    ruleCollectionType = $rc.ruleCollectionType
                                                    rules              = @()
                                                }
                                                
                                                $rules = @($rc.rules)
                                                if ($rules.Count -gt 0) {
                                                    $totalRules += $rules.Count
                                                    
                                                    foreach ($rule in $rules) {
                                                        $ruleDetail = @{
                                                            name        = $rule.name
                                                            ruleType    = $rule.ruleType
                                                            description = $rule.description
                                                        }
                                                        
                                                        if ($rule.sourceAddresses)  { $ruleDetail.sourceAddresses  = @($rule.sourceAddresses) }
                                                        if ($rule.sourceIpGroups)   { $ruleDetail.sourceIpGroups   = @($rule.sourceIpGroups) }
                                                        
                                                        if ($rule.ruleType -eq 'ApplicationRule') {
                                                            if ($rule.targetFqdns)  { $ruleDetail.targetFqdns  = @($rule.targetFqdns) }
                                                            if ($rule.fqdnTags)     { $ruleDetail.fqdnTags     = @($rule.fqdnTags) }
                                                            if ($rule.webCategories) { $ruleDetail.webCategories = @($rule.webCategories) }
                                                            if ($rule.targetUrls)   { $ruleDetail.targetUrls   = @($rule.targetUrls) }
                                                            if ($rule.protocols)    { $ruleDetail.protocols    = @($rule.protocols | ForEach-Object { "$($_.protocolType):$($_.port)" }) }
                                                        } elseif ($rule.ruleType -eq 'NetworkRule') {
                                                            if ($rule.destinationAddresses) { $ruleDetail.destinationAddresses = @($rule.destinationAddresses) }
                                                            if ($rule.destinationIpGroups)  { $ruleDetail.destinationIpGroups  = @($rule.destinationIpGroups) }
                                                            if ($rule.destinationFqdns)     { $ruleDetail.destinationFqdns     = @($rule.destinationFqdns) }
                                                            if ($rule.destinationPorts)     { $ruleDetail.destinationPorts     = @($rule.destinationPorts) }
                                                            if ($rule.ipProtocols)          { $ruleDetail.ipProtocols          = @($rule.ipProtocols) }
                                                        } elseif ($rule.ruleType -eq 'NatRule') {
                                                            if ($rule.sourceAddresses)      { $ruleDetail.sourceAddresses      = @($rule.sourceAddresses) }
                                                            if ($rule.destinationAddresses) { $ruleDetail.destinationAddresses = @($rule.destinationAddresses) }
                                                            if ($rule.destinationPorts)     { $ruleDetail.destinationPorts     = @($rule.destinationPorts) }
                                                            if ($rule.ipProtocols)          { $ruleDetail.ipProtocols          = @($rule.ipProtocols) }
                                                            if ($rule.translatedAddress)    { $ruleDetail.translatedAddress    = $rule.translatedAddress }
                                                            if ($rule.translatedPort)       { $ruleDetail.translatedPort       = $rule.translatedPort }
                                                            if ($rule.translatedFqdn)       { $ruleDetail.translatedFqdn       = $rule.translatedFqdn }
                                                        }
                                                        
                                                        $rcDetail.rules += $ruleDetail
                                                    }
                                                    
                                                    # Categorize rule collections by type
                                                    if ($rc.ruleCollectionType -eq 'FirewallPolicyFilterRuleCollection') {
                                                        if ($rules[0].ruleType -eq 'ApplicationRule') {
                                                            $applicationRuleCollections++
                                                        } elseif ($rules[0].ruleType -eq 'NetworkRule') {
                                                            $networkRuleCollections++
                                                        }
                                                    } elseif ($rc.ruleCollectionType -eq 'FirewallPolicyNatRuleCollection') {
                                                        $natRuleCollections++
                                                    }
                                                }
                                                
                                                $rcgDetail.ruleCollections += $rcDetail
                                            }
                                        }
                                        
                                        $ruleCollectionGroupDetails += $rcgDetail
                                    } catch {
                                        Write-Host " ⚠️ Error parsing rule collection group: $($_.Exception.Message)" -ForegroundColor Yellow
                                    }
                                }
                                
                                $inventory.networking.firewallPolicies += @{
                                    name = $policy.Name
                                    id = $policy.Id
                                    resourceGroup = $policy.ResourceGroupName
                                    location = $policy.Location
                                    tier = $policy.Sku.Tier
                                    threatIntelMode = $policy.ThreatIntelMode
                                    threatIntelWhitelist = if ($policy.ThreatIntelWhitelist) { $true } else { $false }
                                    dnsSettings = if ($policy.DnsSettings) { $true } else { $false }
                                    intrusionDetection = if ($policy.IntrusionDetection) { $policy.IntrusionDetection.Mode } else { 'Off' }
                                    ruleCollectionGroups = $ruleCollectionGroupCount
                                    totalRuleCollections = $totalRuleCollections
                                    totalRules = $totalRules
                                    applicationRuleCollections = $applicationRuleCollections
                                    networkRuleCollections = $networkRuleCollections
                                    natRuleCollections = $natRuleCollections
                                    ruleCollectionGroupDetails = $ruleCollectionGroupDetails
                                    basePolicy = if ($policy.BasePolicy) { Split-Path $policy.BasePolicy.Id -Leaf } else { $null }
                                    subscription = $sub.Name
                                }
                            }
                        } catch {
                            Write-Host " ⚠️ Error processing Firewall Policy: $($policyResource.Name)" -ForegroundColor Yellow
                        }
                    }
                    $inventory.summary.totalFirewallPolicies += $firewallPolicyResources.Count
                } catch {
                    Write-Host " ⚠️ Error collecting Firewall Policies" -ForegroundColor Yellow
                }
                
                # Virtual WANs
                try {
                    $vwanResources = Get-AzResource -ResourceType 'Microsoft.Network/virtualWans' -ErrorAction SilentlyContinue
                    foreach ($vwanResource in $vwanResources) {
                        try {
                            $vwan = Get-AzVirtualWan -ResourceGroupName $vwanResource.ResourceGroupName -Name $vwanResource.Name -ErrorAction SilentlyContinue
                            if ($vwan) {
                                # Get Virtual Hubs associated with this VWAN
                                $vHubs = Get-AzVirtualHub -ErrorAction SilentlyContinue | Where-Object { 
                                    $_.VirtualWan.Id -eq $vwan.Id 
                                }
                                
                                $hubDetails = @()
                                foreach ($hub in $vHubs) {
                                    $hubDetails += @{
                                        name = $hub.Name
                                        location = $hub.Location
                                        addressPrefix = $hub.AddressPrefix
                                        routingState = $hub.RoutingState
                                    }
                                }
                                
                                $inventory.networking.virtualWans += @{
                                    name = $vwan.Name
                                    id = $vwan.Id
                                    resourceGroup = $vwan.ResourceGroupName
                                    location = $vwan.Location
                                    type = $vwan.Type
                                    allowBranchToBranchTraffic = $vwan.AllowBranchToBranchTraffic
                                    allowVnetToVnetTraffic = $vwan.AllowVnetToVnetTraffic
                                    disableVpnEncryption = $vwan.DisableVpnEncryption
                                    virtualHubCount = $vHubs.Count
                                    virtualHubs = $hubDetails
                                    tags = $vwan.Tag
                                    subscription = $sub.Name
                                }
                            }
                        } catch {
                            Write-Host " ⚠️ Error processing Virtual WAN: $($vwanResource.Name)" -ForegroundColor Yellow
                        }
                    }
                    $inventory.summary.totalVirtualWans += $vwanResources.Count
                } catch {
                    Write-Host " ⚠️ Error collecting Virtual WANs" -ForegroundColor Yellow
                }
                
                # Network Security Groups - use Get-AzResource to find them
                try {
                    $nsgResources = Get-AzResource -ResourceType 'Microsoft.Network/networkSecurityGroups' -ErrorAction SilentlyContinue
                    foreach ($nsgResource in $nsgResources) {
                        try {
                            $nsg = Get-AzNetworkSecurityGroup -ResourceGroupName $nsgResource.ResourceGroupName -Name $nsgResource.Name -ErrorAction SilentlyContinue
                            if ($nsg) {
                                # Get associated subnets
                                $associatedSubnets = @()
                                foreach ($subnet in $nsg.Subnets) {
                                    if ($subnet.Id) {
                                        $subnetName = Split-Path $subnet.Id -Leaf
                                        $vnetName = ($subnet.Id -split '/subnets/')[0] | Split-Path -Leaf
                                        $associatedSubnets += "$vnetName/$subnetName"
                                    }
                                }
                                
                                # Get associated network interfaces
                                $associatedNICs = @()
                                foreach ($nic in $nsg.NetworkInterfaces) {
                                    if ($nic.Id) {
                                        $nicName = Split-Path $nic.Id -Leaf
                                        $associatedNICs += $nicName
                                    }
                                }
                                
                                $inventory.networking.networkSecurityGroups += @{
                                    name = $nsg.Name
                                    resourceGroup = $nsg.ResourceGroupName
                                    location = $nsg.Location
                                    securityRulesCount = $nsg.SecurityRules.Count
                                    defaultSecurityRulesCount = $nsg.DefaultSecurityRules.Count
                                    associatedSubnets = $associatedSubnets
                                    associatedNICs = $associatedNICs
                                    subscription = $sub.Name
                                }
                            }
                        } catch {}
                    }
                } catch {}
                
                # Private DNS Zones
                try {
                    $privateDnsZones = Get-AzPrivateDnsZone -ErrorAction SilentlyContinue
                    foreach ($zone in $privateDnsZones) {
                        try {
                            # Get virtual network links for this zone
                            $vnetLinks = Get-AzPrivateDnsVirtualNetworkLink -ResourceGroupName $zone.ResourceGroupName -ZoneName $zone.Name -ErrorAction SilentlyContinue
                            $linkedVNets = @()
                            foreach ($link in $vnetLinks) {
                                $vnetName = if ($link.VirtualNetwork.Id) { Split-Path $link.VirtualNetwork.Id -Leaf } else { 'N/A' }
                                $linkedVNets += @{
                                    linkName = $link.Name
                                    vnetName = $vnetName
                                    vnetId = $link.VirtualNetwork.Id
                                    registrationEnabled = $link.RegistrationEnabled
                                }
                            }
                            
                            # Get record sets count
                            $recordSets = Get-AzPrivateDnsRecordSet -ResourceGroupName $zone.ResourceGroupName -ZoneName $zone.Name -ErrorAction SilentlyContinue
                            
                            $inventory.networking.privateDnsZones += @{
                                name = $zone.Name
                                id = $zone.Id
                                resourceGroup = $zone.ResourceGroupName
                                location = $zone.Location
                                numberOfRecordSets = $recordSets.Count
                                numberOfVirtualNetworkLinks = $vnetLinks.Count
                                virtualNetworkLinks = $linkedVNets
                                tags = $zone.Tags
                                subscription = $sub.Name
                            }
                        } catch {
                            Write-Host " ⚠️ Error processing Private DNS Zone: $($zone.Name)" -ForegroundColor Yellow
                        }
                    }
                    $inventory.summary.totalPrivateDnsZones += $privateDnsZones.Count
                } catch {
                    Write-Host " ⚠️ Error collecting Private DNS Zones" -ForegroundColor Yellow
                }
                
                # Private Endpoints
                try {
                    $privateEndpoints = Get-AzPrivateEndpoint -ErrorAction SilentlyContinue
                    foreach ($pe in $privateEndpoints) {
                        try {
                            # Get private link service connection details
                            $connections = @()
                            foreach ($conn in $pe.PrivateLinkServiceConnections) {
                                $connections += @{
                                    name = $conn.Name
                                    privateLinkServiceId = $conn.PrivateLinkServiceId
                                    groupIds = $conn.GroupIds
                                    requestMessage = $conn.RequestMessage
                                    status = $conn.PrivateLinkServiceConnectionState.Status
                                }
                            }
                            
                            # Get subnet and VNet info
                            $subnetId = if ($pe.Subnet.Id) { $pe.Subnet.Id } else { $null }
                            $vnetName = if ($subnetId) { ($subnetId -split '/subnets/')[0] | Split-Path -Leaf } else { 'N/A' }
                            $subnetName = if ($subnetId) { Split-Path $subnetId -Leaf } else { 'N/A' }
                            
                            # Get private IP addresses
                            $privateIPs = @()
                            foreach ($ipConfig in $pe.NetworkInterfaces) {
                                if ($ipConfig.Id) {
                                    try {
                                        $nicRG = ($ipConfig.Id -split '/')[4]
                                        $nicName = Split-Path $ipConfig.Id -Leaf
                                        $nic = Get-AzNetworkInterface -ResourceGroupName $nicRG -Name $nicName -ErrorAction SilentlyContinue
                                        if ($nic) {
                                            foreach ($ip in $nic.IpConfigurations) {
                                                if ($ip.PrivateIpAddress) {
                                                    $privateIPs += $ip.PrivateIpAddress
                                                }
                                            }
                                        }
                                    } catch {}
                                }
                            }
                            
                            # Get connected resource name from the first connection
                            $connectedResource = 'N/A'
                            if ($connections.Count -gt 0 -and $connections[0].privateLinkServiceId) {
                                $connectedResource = Split-Path $connections[0].privateLinkServiceId -Leaf
                            }
                            
                            $inventory.networking.privateEndpoints += @{
                                name = $pe.Name
                                id = $pe.Id
                                resourceGroup = $pe.ResourceGroupName
                                location = $pe.Location
                                vnet = $vnetName
                                subnet = $subnetName
                                privateIPs = $privateIPs
                                connections = $connections
                                connectedResource = $connectedResource
                                provisioningState = $pe.ProvisioningState
                                tags = $pe.Tag
                                subscription = $sub.Name
                            }
                        } catch {
                            Write-Host " ⚠️ Error processing Private Endpoint: $($pe.Name)" -ForegroundColor Yellow
                        }
                    }
                    $inventory.summary.totalPrivateEndpoints += $privateEndpoints.Count
                } catch {
                    Write-Host " ⚠️ Error collecting Private Endpoints" -ForegroundColor Yellow
                }
                
            } catch {
                Write-Host " ⚠️ Error collecting network resources in sub: $($sub.Name)" -ForegroundColor Yellow
            }
        }
        
        # Get Virtual Machines
        Update-CollectionProgress -Step 8 -TotalSteps $totalSteps -Status 'Collecting Virtual Machines...'
        foreach ($sub in $subs) {
            try {
                # Try to set context with better error handling
                try {
                    Set-AzContext -SubscriptionId $sub.Id -ErrorAction Stop | Out-Null
                } catch {
                    if ($_.Exception.Message -match 'No such host is known|Unable to acquire token') {
                        Write-Host " ⚠️ Network connectivity issue for subscription: $($sub.Name) - Skipping" -ForegroundColor Yellow
                        continue
                    } else {
                        throw
                    }
                }
                
                $vms = Get-AzVM -Status -ErrorAction SilentlyContinue
                foreach ($vm in $vms) {
                    # Get network interfaces
                    $nics = @()
                    $vmVNet = $null
                    $vmSubnet = $null
                    $privateIPs = @()
                    $publicIPs = @()
                    
                    foreach ($nicRef in $vm.NetworkProfile.NetworkInterfaces) {
                        try {
                            $nicId = $nicRef.Id
                            $nicRG = ($nicId -split '/')[4]
                            $nicName = Split-Path $nicId -Leaf
                            $nic = Get-AzNetworkInterface -ResourceGroupName $nicRG -Name $nicName -ErrorAction SilentlyContinue
                            
                            if ($nic) {
                                $nics += $nic.Name
                                
                                foreach ($ipConfig in $nic.IpConfigurations) {
                                    if ($ipConfig.PrivateIpAddress) {
                                        $privateIPs += $ipConfig.PrivateIpAddress
                                    }
                                    
                                    if ($ipConfig.Subnet) {
                                        $subnetId = $ipConfig.Subnet.Id
                                        $vmVNet = ($subnetId -split '/subnets/')[0] | Split-Path -Leaf
                                        $vmSubnet = Split-Path $subnetId -Leaf
                                    }
                                    
                                    if ($ipConfig.PublicIpAddress) {
                                        $pipId = $ipConfig.PublicIpAddress.Id
                                        $pipRG = ($pipId -split '/')[4]
                                        $pipName = Split-Path $pipId -Leaf
                                        $pip = Get-AzPublicIpAddress -ResourceGroupName $pipRG -Name $pipName -ErrorAction SilentlyContinue
                                        if ($pip -and $pip.IpAddress) {
                                            $publicIPs += $pip.IpAddress
                                        }
                                    }
                                }
                            }
                        } catch {}
                    }
                    
                    # Get OS disk info
                    $osDiskSize = 0
                    if ($vm.StorageProfile.OsDisk.DiskSizeGB) {
                        $osDiskSize = $vm.StorageProfile.OsDisk.DiskSizeGB
                    }
                    
                    $powerState = ($vm.PowerState -replace 'PowerState/', '')
                    
                    $inventory.compute.virtualMachines += @{
                        name = $vm.Name
                        id = $vm.Id
                        resourceGroup = $vm.ResourceGroupName
                        location = $vm.Location
                        vmSize = $vm.HardwareProfile.VmSize
                        osType = $vm.StorageProfile.OsDisk.OsType
                        osDiskSizeGB = $osDiskSize
                        dataDisksCount = $vm.StorageProfile.DataDisks.Count
                        powerState = $powerState
                        provisioningState = $vm.ProvisioningState
                        networkInterfaces = $nics
                        vnet = $vmVNet
                        subnet = $vmSubnet
                        privateIPs = $privateIPs
                        publicIPs = $publicIPs
                        availabilitySet = if ($vm.AvailabilitySetReference) { Split-Path $vm.AvailabilitySetReference.Id -Leaf } else { $null }
                        tags = $vm.Tags
                        subscription = $sub.Name
                    }
                }
                $inventory.summary.totalVMs += $vms.Count
                
            } catch {
                Write-Host " ⚠️ Error collecting VMs in sub: $($sub.Name)" -ForegroundColor Yellow
            }
        }
        
        # Get Governance Resources
        Update-CollectionProgress -Step 9 -TotalSteps $totalSteps -Status 'Collecting Governance Resources...'
        foreach ($sub in $subs) {
            try {
                # Try to set context with better error handling
                try {
                    Set-AzContext -SubscriptionId $sub.Id -ErrorAction Stop | Out-Null
                } catch {
                    if ($_.Exception.Message -match 'No such host is known|Unable to acquire token') {
                        Write-Host " ⚠️ Network connectivity issue for subscription: $($sub.Name) - Skipping" -ForegroundColor Yellow
                        continue
                    } else {
                        throw
                    }
                }
                
                # Budgets (requires Az.CostManagement module - skip if not available)
                try {
                    if (Get-Command Get-AzConsumptionBudget -ErrorAction SilentlyContinue) {
                        $budgets = Get-AzConsumptionBudget -ErrorAction SilentlyContinue
                        foreach ($budget in $budgets) {
                            $inventory.governance.budgets += @{
                                name = $budget.Name
                                amount = $budget.Amount
                                timeGrain = $budget.TimeGrain
                                timePeriod = @{
                                    startDate = $budget.TimePeriod.StartDate
                                    endDate = $budget.TimePeriod.EndDate
                                }
                                currentSpend = $budget.CurrentSpend.Amount
                                subscription = $sub.Name
                            }
                        }
                        $inventory.summary.totalBudgets += $budgets.Count
                    }
                } catch {}
                
                # Resource Locks (subscription, resource groups, and resources)
                Write-Host " • Collecting resource locks..." -ForegroundColor Gray
                try {
                    # Get locks at subscription level
                    $subLocks = Get-AzResourceLock -Scope "/subscriptions/$($sub.Id)" -ErrorAction SilentlyContinue
                    foreach ($lock in $subLocks) {
                        $inventory.governance.locks += @{
                            name = $lock.Name
                            resourceName = if ($lock.ResourceName) { $lock.ResourceName } else { "Subscription: $($sub.Name)" }
                            resourceType = if ($lock.ResourceType) { $lock.ResourceType } else { "Subscription" }
                            resourceGroup = $lock.ResourceGroupName
                            level = $lock.Properties.Level
                            notes = $lock.Properties.Notes
                            subscription = $sub.Name
                            scope = "Subscription"
                        }
                    }
                    
                    # Get locks at resource group level
                    $resourceGroups = Get-AzResourceGroup -ErrorAction SilentlyContinue
                    foreach ($rg in $resourceGroups) {
                        $rgLocks = Get-AzResourceLock -ResourceGroupName $rg.ResourceGroupName -ErrorAction SilentlyContinue
                        foreach ($lock in $rgLocks) {
                            # Avoid duplicates if already collected at subscription level
                            $lockExists = $inventory.governance.locks | Where-Object { 
                                $_.name -eq $lock.Name -and $_.resourceGroup -eq $lock.ResourceGroupName 
                            }
                            if (-not $lockExists) {
                                $inventory.governance.locks += @{
                                    name = $lock.Name
                                    resourceName = if ($lock.ResourceName) { $lock.ResourceName } else { "Resource Group: $($rg.ResourceGroupName)" }
                                    resourceType = if ($lock.ResourceType) { $lock.ResourceType } else { "ResourceGroup" }
                                    resourceGroup = $rg.ResourceGroupName
                                    level = $lock.Properties.Level
                                    notes = $lock.Properties.Notes
                                    subscription = $sub.Name
                                    scope = "ResourceGroup"
                                }
                            }
                        }
                    }
                    
                    $lockCount = ($inventory.governance.locks | Where-Object { $_.subscription -eq $sub.Name }).Count
                    $inventory.summary.totalLocks += $lockCount
                    Write-Host " ✓ Collected $lockCount locks" -ForegroundColor Green
                } catch {
                    Write-Host " ⚠️ Error collecting locks" -ForegroundColor Yellow
                }
                
                # Collect tags from subscriptions, resource groups, and selected resources
                Write-Host " • Collecting tags..." -ForegroundColor Gray
                try {
                    # Subscription tags
                    $subResource = Get-AzSubscription -SubscriptionId $sub.Id -ErrorAction SilentlyContinue
                    if ($subResource.Tags) {
                        foreach ($tagKey in $subResource.Tags.Keys) {
                            if (-not $inventory.governance.tags.ContainsKey($tagKey)) {
                                $inventory.governance.tags[$tagKey] = @()
                            }
                            $tagValue = $subResource.Tags[$tagKey]
                            if ($tagValue -notin $inventory.governance.tags[$tagKey]) {
                                $inventory.governance.tags[$tagKey] += $tagValue
                            }
                        }
                    }
                    
                    # Resource group tags
                    foreach ($rg in $resourceGroups) {
                        if ($rg.Tags) {
                            foreach ($tagKey in $rg.Tags.Keys) {
                                if (-not $inventory.governance.tags.ContainsKey($tagKey)) {
                                    $inventory.governance.tags[$tagKey] = @()
                                }
                                $tagValue = $rg.Tags[$tagKey]
                                if ($tagValue -notin $inventory.governance.tags[$tagKey]) {
                                    $inventory.governance.tags[$tagKey] += $tagValue
                                }
                            }
                        }
                    }
                    
                    # Sample resource tags (limit to avoid performance issues)
                    $sampleResources = Get-AzResource -ErrorAction SilentlyContinue | Select-Object -First 100
                    foreach ($resource in $sampleResources) {
                        if ($resource.Tags) {
                            foreach ($tagKey in $resource.Tags.Keys) {
                                if (-not $inventory.governance.tags.ContainsKey($tagKey)) {
                                    $inventory.governance.tags[$tagKey] = @()
                                }
                                $tagValue = $resource.Tags[$tagKey]
                                if ($tagValue -notin $inventory.governance.tags[$tagKey]) {
                                    $inventory.governance.tags[$tagKey] += $tagValue
                                }
                            }
                        }
                    }
                    
                    Write-Host " ✓ Collected $($inventory.governance.tags.Keys.Count) unique tag keys" -ForegroundColor Green
                } catch {
                    Write-Host " ⚠️ Error collecting tags" -ForegroundColor Yellow
                }
                
            } catch {
                Write-Host " ⚠️ Error collecting governance resources in sub: $($sub.Name)" -ForegroundColor Yellow
            }
        }
        
        Write-Host " ✓ Inventory collection complete" -ForegroundColor Green
        
        # Evaluate Best Practices Compliance
        Update-CollectionProgress -Step 10 -TotalSteps $totalSteps -Status 'Evaluating Cloud Adoption Framework compliance...'
        $inventory.bestPractices = Get-LandingZoneBestPracticesAssessment -Inventory $inventory
        Update-CollectionProgress -Step 11 -TotalSteps $totalSteps -Status 'Complete!'
        Write-Host " ✓ Best practices assessment complete" -ForegroundColor Green
        
    } catch {
        Write-Host " ✗ Error during inventory collection: $($_.Exception.Message)" -ForegroundColor Red
        throw
    }
    
    return $inventory
}

function Get-LandingZoneBestPracticesAssessment {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [hashtable]$Inventory,
        [Parameter(Mandatory=$false)]
        [string]$ConfigPath = "$PSScriptRoot/scoring-config.json"
    )
    
    # Load scoring configuration
    if (-not (Test-Path $ConfigPath)) {
        Write-Warning "Scoring configuration file not found at: $ConfigPath. Using default scoring."
        $ConfigPath = "$PSScriptRoot/scoring-config.json"
    }
    
    $scoringConfig = Get-Content $ConfigPath -Raw | ConvertFrom-Json
    Write-Host " 📋 Loaded scoring configuration v$($scoringConfig.version)" -ForegroundColor Cyan
    
    # Initialize assessment structure from config
    $assessment = @{
        overallScore = 0
        maxScore = 0
        recommendations = @()
        configVersion = $scoringConfig.version
        categories = @{}
    }
    
    # Build categories from config
    foreach ($catKey in $scoringConfig.categories.PSObject.Properties.Name) {
        $catConfig = $scoringConfig.categories.$catKey
        $assessment.categories[$catKey] = @{
            score = 0
            maxScore = $catConfig.maxScore
            status = 'fail'
            findings = @()
            name = $catConfig.name
            description = $catConfig.description
        }
    }
    
    # Gather metrics from inventory (with defaults to handle missing data)
    $metrics = @{
        mgCount = $Inventory.summary.totalManagementGroups ?? 0
        subCount = $Inventory.summary.totalSubscriptions ?? 0
        policyDefs = $Inventory.summary.totalPolicyDefinitions ?? 0
        policyInits = $Inventory.summary.totalPolicyInitiatives ?? 0
        policyAssigns = $Inventory.summary.totalPolicyAssignments ?? 0
        roleAssigns = $Inventory.summary.totalRoleAssignments ?? 0
        vnetCount = $Inventory.summary.totalVNets ?? 0
        peeringCount = $Inventory.summary.totalPeerings ?? 0
        vpnCount = if ($Inventory.networking.vpnGateways) { $Inventory.networking.vpnGateways.Count } else { 0 }
        fwCount = if ($Inventory.networking.firewalls) { $Inventory.networking.firewalls.Count } else { 0 }
        locks = $Inventory.summary.totalLocks ?? 0
        nsgCount = if ($Inventory.networking.networkSecurityGroups) { $Inventory.networking.networkSecurityGroups.Count } else { 0 }
        budgets = $Inventory.summary.totalBudgets ?? 0
        tagCount = if ($Inventory.governance.tags.Keys) { $Inventory.governance.tags.Keys.Count } else { 0 }
        hasPrivilegedRoles = [bool]($Inventory.roleAssignments | Where-Object { $_.roleDefinitionName -match 'Owner|Contributor' })
    }
    
    # Process each category and its rules from config
    foreach ($catKey in $scoringConfig.categories.PSObject.Properties.Name) {
        $catConfig = $scoringConfig.categories.$catKey
        $category = $assessment.categories[$catKey]
        
        Write-Host " Evaluating: $($catConfig.name)" -ForegroundColor Gray
        
        foreach ($rule in $catConfig.rules) {
            $conditionMet = $false
            $partialConditionMet = $false
            
            # Evaluate main condition
            try {
                $conditionMet = Test-ScoringCondition -Condition $rule.condition -Metrics $metrics
            } catch {
                Write-Warning "Failed to evaluate condition for rule $($rule.id): $($rule.condition) | Error: $($_.Exception.Message)"
                $conditionMet = $false
            }
            
            # Evaluate partial points condition if exists
            if ($rule.partialPoints -and -not $conditionMet) {
                try {
                    $partialConditionMet = Test-ScoringCondition -Condition $rule.partialPoints.condition -Metrics $metrics
                } catch {
                    $partialConditionMet = $false
                }
            }
            
            # Award points and add findings
            if ($conditionMet) {
                $category.score += $rule.points
                $category.findings += "✓ $($rule.description)"
            } elseif ($partialConditionMet) {
                $category.score += $rule.partialPoints.points
                $category.findings += "⚠ $($rule.description) (Partial)"
                $assessment.recommendations += $rule.recommendation
            } else {
                $category.findings += "✗ $($rule.description)"
                $assessment.recommendations += $rule.recommendation
            }
        }
    }
    
    # Legacy scoring logic kept for backward compatibility (can be removed if config covers all cases)
    # The sections below are now handled by the config file
    
    # 1. Management Group Hierarchy Assessment - NOW IN CONFIG
    <#
    $mgCount = $Inventory.summary.totalManagementGroups
    $subCount = $Inventory.summary.totalSubscriptions
    
    if ($mgCount -ge 2) {
        $assessment.categories.managementGroupHierarchy.score += 5
        $assessment.categories.managementGroupHierarchy.findings += "✓ Management group hierarchy is implemented ($mgCount groups)"
    } else {
        $assessment.categories.managementGroupHierarchy.findings += "✗ Limited management group structure. Recommended: Use hierarchical management groups for organizational alignment"
        $assessment.recommendations += "Implement a management group hierarchy (e.g., Root > Platform/Landing Zones > Corp/Online)"
    }
    
    if ($mgCount -ge 3 -and $mgCount -le 6) {
        $assessment.categories.managementGroupHierarchy.score += 5
        $assessment.categories.managementGroupHierarchy.findings += "✓ Management group hierarchy follows CAF recommendations (3-6 levels)"
    } elseif ($mgCount -gt 6) {
        $assessment.categories.managementGroupHierarchy.findings += "⚠ Management group hierarchy may be too deep. Recommended: 3-6 levels maximum"
        $assessment.recommendations += "Simplify management group structure to 3-6 levels for better manageability"
    }
    
    if ($subCount -ge 3) {
        $assessment.categories.managementGroupHierarchy.score += 5
        $assessment.categories.managementGroupHierarchy.findings += "✓ Multiple subscriptions for workload isolation ($subCount subscriptions)"
    } elseif ($subCount -gt 0) {
        $assessment.categories.managementGroupHierarchy.findings += "⚠ Consider using multiple subscriptions for better workload isolation and scale"
        $assessment.recommendations += "Adopt subscription democratization: Use separate subscriptions for platform, landing zones, and workloads"
    }
    
    # 2. Policy-Driven Governance Assessment
    $policyDefs = $Inventory.summary.totalPolicyDefinitions
    $policyInits = $Inventory.summary.totalPolicyInitiatives
    $policyAssigns = $Inventory.summary.totalPolicyAssignments
    
    if ($policyDefs -gt 0 -or $policyInits -gt 0) {
        $assessment.categories.policyDrivenGovernance.score += 5
        $assessment.categories.policyDrivenGovernance.findings += "✓ Custom policies/initiatives defined ($policyDefs definitions, $policyInits initiatives)"
    } else {
        $assessment.categories.policyDrivenGovernance.findings += "⚠ No custom policies found. Consider implementing custom policies for organizational requirements"
        $assessment.recommendations += "Define custom Azure Policies for security, compliance, and governance requirements"
    }
    
    if ($policyAssigns -ge 5) {
        $assessment.categories.policyDrivenGovernance.score += 10
        $assessment.categories.policyDrivenGovernance.findings += "✓ Active policy assignments at scale ($policyAssigns assignments)"
    } elseif ($policyAssigns -gt 0) {
        $assessment.categories.policyDrivenGovernance.score += 5
        $assessment.categories.policyDrivenGovernance.findings += "⚠ Limited policy assignments. Recommended: Apply policies at management group level"
        $assessment.recommendations += "Increase policy coverage by assigning policies at management group scope"
    } else {
        $assessment.categories.policyDrivenGovernance.findings += "✗ No policy assignments found. Critical: Implement Azure Policy for governance"
        $assessment.recommendations += "Assign Azure Policy initiatives (e.g., Azure Security Benchmark, regulatory compliance)"
    }
    
    if ($policyInits -gt 0) {
        $assessment.categories.policyDrivenGovernance.score += 5
        $assessment.categories.policyDrivenGovernance.findings += "✓ Policy initiatives (grouped policies) are being used"
    }
    
    # 3. Identity and Access Management Assessment
    $roleAssigns = $Inventory.summary.totalRoleAssignments
    
    if ($roleAssigns -ge 10) {
        $assessment.categories.identityAndAccess.score += 10
        $assessment.categories.identityAndAccess.findings += "✓ RBAC actively implemented ($roleAssigns role assignments)"
    } elseif ($roleAssigns -gt 0) {
        $assessment.categories.identityAndAccess.score += 5
        $assessment.categories.identityAndAccess.findings += "⚠ Limited RBAC implementation. Recommended: Implement least-privilege access"
        $assessment.recommendations += "Expand RBAC implementation with custom roles and group-based assignments"
    } else {
        $assessment.categories.identityAndAccess.findings += "✗ No custom role assignments. Implement RBAC for identity-based access control"
        $assessment.recommendations += "Define and assign Azure RBAC roles at management group and subscription scopes"
    }
    
    # Check for privileged roles (Owner/Contributor)
    $hasPrivilegedRoles = $Inventory.roleAssignments | Where-Object { $_.roleDefinitionName -match 'Owner|Contributor' }
    if ($hasPrivilegedRoles) {
        $assessment.categories.identityAndAccess.score += 5
        $assessment.categories.identityAndAccess.findings += "✓ Privileged roles detected. Ensure these follow least-privilege principle"
    }
    
    # 4. Network Topology and Connectivity Assessment
    $vnetCount = $Inventory.summary.totalVNets
    $peeringCount = $Inventory.summary.totalPeerings
    $vpnCount = $Inventory.networking.vpnGateways.Count
    $fwCount = $Inventory.networking.firewalls.Count
    
    if ($vnetCount -ge 2) {
        $assessment.categories.networkTopology.score += 5
        $assessment.categories.networkTopology.findings += "✓ Multiple VNets for network segmentation ($vnetCount VNets)"
    } elseif ($vnetCount -eq 1) {
        $assessment.categories.networkTopology.findings += "⚠ Single VNet detected. Consider hub-spoke topology for scalable architecture"
        $assessment.recommendations += "Implement hub-spoke network topology or Azure Virtual WAN for enterprise-scale connectivity"
    } else {
        $assessment.categories.networkTopology.findings += "✗ No VNets found. Deploy networking infrastructure"
    }
    
    if ($peeringCount -gt 0 -and $vnetCount -ge 2) {
        $assessment.categories.networkTopology.score += 10
        $assessment.categories.networkTopology.findings += "✓ VNet peering implemented for connectivity ($peeringCount peerings)"
    } elseif ($vnetCount -ge 2) {
        $assessment.categories.networkTopology.findings += "⚠ Multiple VNets without peering. Recommended: Implement hub-spoke with VNet peering"
        $assessment.recommendations += "Connect VNets using peering or Virtual WAN for centralized connectivity"
    }
    
    if ($vpnCount -gt 0 -or $fwCount -gt 0) {
        $assessment.categories.networkTopology.score += 5
        $assessment.categories.networkTopology.findings += "✓ Network security appliances deployed (VPN: $vpnCount, Firewalls: $fwCount)"
    } else {
        $assessment.categories.networkTopology.findings += "⚠ No VPN gateways or firewalls detected. Consider Azure Firewall or NVA for hub connectivity"
        $assessment.recommendations += "Deploy Azure Firewall in hub VNet for centralized security and egress control"
    }
    
    # 5. Security and Governance Assessment
    $locks = $Inventory.summary.totalLocks
    $nsgCount = $Inventory.networking.networkSecurityGroups.Count
    
    if ($locks -gt 0) {
        $assessment.categories.securityGovernance.score += 5
        $assessment.categories.securityGovernance.findings += "✓ Resource locks implemented for protection ($locks locks)"
    } else {
        $assessment.categories.securityGovernance.findings += "⚠ No resource locks found. Recommended: Lock critical resources"
        $assessment.recommendations += "Apply CanNotDelete locks on critical resources (networking, shared services)"
    }
    
    if ($nsgCount -gt 0) {
        $assessment.categories.securityGovernance.score += 5
        $assessment.categories.securityGovernance.findings += "✓ Network Security Groups deployed ($nsgCount NSGs)"
    } else {
        $assessment.categories.securityGovernance.findings += "⚠ No NSGs detected. Implement network segmentation"
        $assessment.recommendations += "Deploy Network Security Groups for subnet-level security and micro-segmentation"
    }
    
    # 6. Cost Management Assessment
    $budgets = $Inventory.summary.totalBudgets
    
    if ($budgets -ge 3) {
        $assessment.categories.costManagement.score += 10
        $assessment.categories.costManagement.findings += "✓ Cost management with budgets ($budgets budgets)"
    } elseif ($budgets -gt 0) {
        $assessment.categories.costManagement.score += 5
        $assessment.categories.costManagement.findings += "⚠ Limited budget coverage. Recommended: Budget per subscription"
        $assessment.recommendations += "Create budgets for all subscriptions with alerting thresholds"
    } else {
        $assessment.categories.costManagement.findings += "✗ No budgets configured. Implement cost management and monitoring"
        $assessment.recommendations += "Configure Azure Budgets and Cost Management alerts for financial governance"
    }
    
    # 7. Resource Organization Assessment
    $tagCount = $Inventory.governance.tags.Keys.Count
    
    if ($tagCount -ge 5) {
        $assessment.categories.resourceOrganization.score += 10
        $assessment.categories.resourceOrganization.findings += "✓ Comprehensive tagging strategy ($tagCount tag keys)"
    } elseif ($tagCount -gt 0) {
        $assessment.categories.resourceOrganization.score += 5
        $assessment.categories.resourceOrganization.findings += "⚠ Basic tagging implemented. Recommended: Standardize tags (Environment, CostCenter, Owner, etc.)"
        $assessment.recommendations += "Define and enforce tagging policy with required tags: Environment, CostCenter, Owner, Application"
    } else {
        $assessment.categories.resourceOrganization.findings += "✗ No tags detected. Implement tagging strategy for resource organization"
        $assessment.recommendations += "Implement mandatory tagging policy using Azure Policy Modify effect"
    }
    #>


    
    # Calculate overall scores using config thresholds
    foreach ($category in $assessment.categories.Keys) {
        $cat = $assessment.categories[$category]
        $assessment.maxScore += $cat.maxScore
        $assessment.overallScore += $cat.score
        
        # Determine status from config thresholds
        $percentage = if ($cat.maxScore -gt 0) { ($cat.score / $cat.maxScore) * 100 } else { 0 }
        
        if ($percentage -ge $scoringConfig.scoringThresholds.excellent.min) {
            $cat.status = 'excellent'
        } elseif ($percentage -ge $scoringConfig.scoringThresholds.good.min) {
            $cat.status = 'good'
        } elseif ($percentage -ge $scoringConfig.scoringThresholds.fair.min) {
            $cat.status = 'fair'
        } else {
            $cat.status = 'needs-improvement'
        }
    }
    
    # Overall assessment using config thresholds
    $overallPercentage = if ($assessment.maxScore -gt 0) { 
        [math]::Round(($assessment.overallScore / $assessment.maxScore) * 100, 0) 
    } else { 
        0 
    }
    $assessment.overallPercentage = $overallPercentage
    
    # Set overall status and message from config
    if ($overallPercentage -ge $scoringConfig.scoringThresholds.excellent.min) {
        $assessment.overallStatus = 'excellent'
        $assessment.overallMessage = $scoringConfig.scoringThresholds.excellent.message
    } elseif ($overallPercentage -ge $scoringConfig.scoringThresholds.good.min) {
        $assessment.overallStatus = 'good'
        $assessment.overallMessage = $scoringConfig.scoringThresholds.good.message
    } elseif ($overallPercentage -ge $scoringConfig.scoringThresholds.fair.min) {
        $assessment.overallStatus = 'fair'
        $assessment.overallMessage = $scoringConfig.scoringThresholds.fair.message
    } else {
        $assessment.overallStatus = 'needs-improvement'
        $assessment.overallMessage = $scoringConfig.scoringThresholds.'needs-improvement'.message
    }
    
    Write-Host " ✅ Assessment complete: $overallPercentage% ($($assessment.overallStatus))" -ForegroundColor Green
    
    return $assessment
}