Processors/UrlProcessor.ps1
|
# Module-level cache for control mappings $script:ControlMappingsCache = $null $script:ControlMappingsFlat = $null $script:ConfigPath = $null function Initialize-UrlProcessor { <# .SYNOPSIS Initializes the URL processor with configuration. .DESCRIPTION Loads control URL mappings from JSON configuration file. .PARAMETER ConfigPath Path to the control-mappings.json configuration file. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ConfigPath ) if (-not (Test-Path $ConfigPath)) { throw "Control mappings configuration file not found: $ConfigPath" } try { $script:ConfigPath = $ConfigPath $configContent = Get-Content -Path $ConfigPath -Raw -Encoding UTF8 $script:ControlMappingsCache = $configContent | ConvertFrom-Json # Build a flat hashtable for O(1) lookup by control name $script:ControlMappingsFlat = @{} foreach ($category in $script:ControlMappingsCache.controlMappings.PSObject.Properties) { foreach ($mapping in $category.Value.PSObject.Properties) { $script:ControlMappingsFlat[$mapping.Name] = $mapping.Value } } Write-Verbose "Loaded $($script:ControlMappingsFlat.Count) control mappings from $ConfigPath" } catch { throw "Failed to load control mappings configuration: $_" } } function Get-ControlMapping { <# .SYNOPSIS Gets the URL mapping for a specific control name. .DESCRIPTION Searches through all category mappings to find a matching control. .PARAMETER ControlName Name of the control to find mapping for. .OUTPUTS String URL if mapping found, otherwise $null. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ControlName ) if (-not $script:ControlMappingsFlat) { Write-Warning "URL processor not initialized. Call Initialize-UrlProcessor first." return $null } # O(1) hashtable lookup instead of nested loop if ($script:ControlMappingsFlat.ContainsKey($ControlName)) { $url = $script:ControlMappingsFlat[$ControlName] Write-Verbose "Found exact mapping for '$ControlName': $url" return $url } return $null } function Get-FallbackUrl { <# .SYNOPSIS Gets a fallback URL based on control name keywords. .DESCRIPTION Analyzes control name for keywords and returns appropriate portal URL. .PARAMETER ControlName Name of the control. .OUTPUTS String URL for the appropriate portal. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ControlName ) if (-not $script:ControlMappingsCache) { return $null } # Check each fallback rule foreach ($rule in $script:ControlMappingsCache.fallbackRules.PSObject.Properties) { $keywords = $rule.Value.keywords $url = $rule.Value.url foreach ($keyword in $keywords) { if ($ControlName -match $keyword) { Write-Verbose "Using fallback URL for '$ControlName' based on keyword '$keyword': $url" return $url } } } return $null } function Update-LegacyPortalUrl { <# .SYNOPSIS Updates legacy portal URLs to current equivalents. .DESCRIPTION Replaces old portal URLs (portal.office.com, aad.portal.azure.com) with new ones. .PARAMETER Url URL to update. .OUTPUTS Updated URL string. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Url ) if (-not $script:ControlMappingsCache) { return $Url } $updatedUrl = $Url # Apply URL replacements from config foreach ($replacement in $script:ControlMappingsCache.urlReplacements.PSObject.Properties) { $oldUrl = $replacement.Name $newUrl = $replacement.Value $updatedUrl = $updatedUrl -replace [regex]::Escape($oldUrl), $newUrl } # Additional Entra ID specific updates (commercial cloud only — do not rewrite sovereign cloud URLs) if ($updatedUrl -match 'Microsoft_AAD' -and $updatedUrl -notmatch 'entra\.microsoft\.com') { # Only rewrite portal.azure.com (commercial), not sovereign cloud variants if ($updatedUrl -match '^https://portal\.azure\.com/' -and $updatedUrl -notmatch '\.azure\.(us|cn|de)/') { $updatedUrl = $updatedUrl -replace 'https://portal\.azure\.com', 'https://entra.microsoft.com' } } return $updatedUrl } function Add-TenantContext { <# .SYNOPSIS Adds tenant ID context to a URL. .DESCRIPTION Injects tenant ID parameter into portal URLs for proper tenant scoping. .PARAMETER Url URL to add tenant context to. .PARAMETER TenantId Tenant identifier to inject. .OUTPUTS URL with tenant context added. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Url, [Parameter(Mandatory = $false)] [string]$TenantId ) if ([string]::IsNullOrEmpty($TenantId)) { return $Url } $updatedUrl = $Url # Replace existing tenant ID if present if ($updatedUrl -match 'tid=') { $updatedUrl = $updatedUrl -replace 'tid=[a-f0-9-]+', "tid=$TenantId" } # Add tenant ID to Entra and Azure portal URLs that don't have it (including sovereign clouds) elseif ($updatedUrl -match '^https://(portal\.azure\.com|portal\.azure\.us|portal\.azure\.cn|portal\.microsoftazure\.de|aad\.portal\.azure\.com|entra\.microsoft\.com)') { if ($updatedUrl -match '\?') { $updatedUrl = $updatedUrl -replace '\?', "?tid=$TenantId&" } elseif ($updatedUrl -match '#') { $updatedUrl = $updatedUrl -replace '#', "?tid=$TenantId#" } else { $updatedUrl += "?tid=$TenantId" } } return $updatedUrl } function Optimize-ControlUrl { <# .SYNOPSIS Optimizes and enhances a control's action URL. .DESCRIPTION Performs full URL optimization including: - Exact control mapping lookup - Documentation URL fallback routing - Legacy URL updates - Tenant context injection .PARAMETER Url Original URL from API. .PARAMETER ControlName Name of the control. .PARAMETER TenantId Tenant identifier for context injection. .OUTPUTS Optimized URL string. #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$Url, [Parameter(Mandatory = $true)] [string]$ControlName, [Parameter(Mandatory = $false)] [string]$TenantId ) if ([string]::IsNullOrEmpty($Url)) { return $Url } $optimizedUrl = $Url # Step 1: Check for exact control mapping $exactMapping = Get-ControlMapping -ControlName $ControlName if ($exactMapping) { $optimizedUrl = $exactMapping Write-Verbose "Using exact mapping for '$ControlName'" } # Step 2: If URL is not a valid HTTP(S) URL, try fallback or clear it elseif ($optimizedUrl -notmatch '^https?://') { $fallbackUrl = Get-FallbackUrl -ControlName $ControlName if ($fallbackUrl) { $optimizedUrl = $fallbackUrl Write-Verbose "Replaced non-HTTP URL with portal fallback for '$ControlName'" } else { Write-Verbose "Cleared non-HTTP URL for '$ControlName': $optimizedUrl" return "" } } # Step 3: If URL points to documentation, find a better config URL elseif ($optimizedUrl -match 'learn\.microsoft\.com') { $fallbackUrl = Get-FallbackUrl -ControlName $ControlName if ($fallbackUrl) { $optimizedUrl = $fallbackUrl Write-Verbose "Replaced documentation URL with portal fallback for '$ControlName'" } } # Step 3: Update legacy portal URLs $optimizedUrl = Update-LegacyPortalUrl -Url $optimizedUrl # Step 4: Add tenant context $optimizedUrl = Add-TenantContext -Url $optimizedUrl -TenantId $TenantId return $optimizedUrl } function Test-UrlProcessorInitialized { <# .SYNOPSIS Checks if URL processor is initialized. .OUTPUTS Boolean indicating initialization status. #> [CmdletBinding()] param() return ($null -ne $script:ControlMappingsCache -and $null -ne $script:ControlMappingsFlat) } # Functions are exported via the main module manifest (.psd1) |