output/20260723_081532/govviz/ALZ_20260723_081601/Enterprise-Scale/src/Alz.Tools/Alz.Classes/Alz.Classes.psm1

#!/usr/bin/pwsh

using module "../Alz.Enums/"

#############################
# ProviderApiVersions Class #
#############################

# [ProviderApiVersions] class is used to create cache of latest API versions for all Azure Providers.
# This can be used to retrieve the latest or stable API version in string format.
# Can also output the API version as a param string for use within a Rest API request.
# To minimise the number of Rest API requests needed, this class creates a cache and populates.
# it with all results from the request. The cache is then used to return the requested result.
# Need to store and lookup the key in lowercase to avoid case sensitivity issues while providing
# better performance as allows using ContainsKey method to search for key in cache.
# Should be safe to ignore case as Providers are not case sensitive.
class ProviderApiVersions {

    # Public class properties
    [String]$Provider
    [String]$ResourceType
    [String]$Type
    [Array]$ApiVersions

    # Static properties
    hidden static [String]$ProvidersApiVersion = "2020-06-01"

    # Default empty constructor
    ProviderApiVersions() {
    }

    # Default constructor using PSCustomObject to populate object
    ProviderApiVersions([PSCustomObject]$PSCustomObject) {
        $this.Provider = $PSCustomObject.Provider
        $this.ResourceType = $PSCustomObject.ResourceType
        $this.Type = $PSCustomObject.Type
        $this.ApiVersions = $PSCustomObject.ApiVersions
    }

    # Static method to get Api Version using Type
    static [Array] GetByType([String]$Type) {
        if ([ProviderApiVersions]::Cache.Count -lt 1) {
            [ProviderApiVersions]::UpdateCache()
        }
        $private:ProviderApiVersionsFromCache = [ProviderApiVersions]::SearchCache($Type)
        return $private:ProviderApiVersionsFromCache.ApiVersions
    }

    # Static method to get latest Api Version using Type
    static [String] GetLatestByType([String]$Type) {
        $private:GetLatestByType = [ProviderApiVersions]::GetByType($Type) |
        Sort-Object -Descending |
        Select-Object -First 1
        return $private:GetLatestByType
    }

    # Static method to get latest stable Api Version using Type
    # If no stable release, will return latest
    static [String] GetLatestStableByType([String]$Type) {
        $private:GetByType = [ProviderApiVersions]::GetByType($Type)
        $private:GetLatestStableByType = $private:GetByType |
        Where-Object { $_ -Match "^[0-9-]{10}$" } |
        Sort-Object -Descending |
        Select-Object -First 1
        if ($private:GetLatestStableByType) {
            return $private:GetLatestStableByType.ToString()
        }
        else {
            return [ProviderApiVersions]::GetLatestByType($Type).ToString()
        }
    }

    static [String[]] ListTypes() {
        if ([ProviderApiVersions]::Cache.Count -lt 1) {
            [ProviderApiVersions]::UpdateCache()
        }
        $private:ShowCacheTypes = [ProviderApiVersions]::ShowCache().Type | Sort-Object
        return $private:ShowCacheTypes
    }

    # Static property to store cache of ProviderApiVersions using a threadsafe
    # dictionary variable to allow caching across parallel jobs
    # https://docs.microsoft.com/powershell/module/microsoft.powershell.core/foreach-object#example-14--using-thread-safe-variable-references
    static [System.Collections.Concurrent.ConcurrentDictionary[String, ProviderApiVersions]]$Cache

    # Static method to show all entries in Cache
    static [ProviderApiVersions[]] ShowCache() {
        return ([ProviderApiVersions]::Cache).Values
    }

    # Static method to show all entries in Cache matching the specified type using the specified release type
    static [ProviderApiVersions[]] SearchCache([String]$Type) {
        return [ProviderApiVersions]::Cache[$Type.ToString().ToLower()]
    }

    # Static method to return [Boolean] for Resource Type in Cache query using the specified release type
    static [Boolean] InCache([String]$Type) {
        if ([ProviderApiVersions]::Cache) {
            $private:CacheKeyLowercase = $Type.ToString().ToLower()
            $private:InCache = ([ProviderApiVersions]::Cache).ContainsKey($private:CacheKeyLowercase)
            if ($private:InCache) {
                Write-Verbose "[ProviderApiVersions] Resource Type found in Cache [$Type]"
            }
            else {
                Write-Verbose "[ProviderApiVersions] Resource Type not found in Cache [$Type]"
            }
            return $private:InCache
        }
        else {
            # The following prevents needing to initialize the cache
            # manually if not exist on first attempt to use
            [ProviderApiVersions]::InitializeCache()
            return $false
        }
    }

    # Static method to update Cache using current Subscription from context
    static [Void] UpdateCache() {
        $private:SubscriptionId = (Get-AzContext).Subscription.Id
        [ProviderApiVersions]::UpdateCache($private:SubscriptionId)
    }

    # Static method to update Cache using specified SubscriptionId
    static [Void] UpdateCache([String]$SubscriptionId) {
        $private:Method = "GET"
        $private:Path = "/subscriptions/$subscriptionId/providers?api-version=$([ProviderApiVersions]::ProvidersApiVersion)"
        $private:PSHttpResponse = Invoke-AzRestMethod -Method $private:Method -Path $private:Path
        $private:PSHttpResponseContent = $private:PSHttpResponse.Content
        $private:Providers = ($private:PSHttpResponseContent | ConvertFrom-Json).value
        if ($private:Providers) {
            [ProviderApiVersions]::InitializeCache()
        }
        foreach ($private:Provider in $private:Providers) {
            Write-Verbose "[ProviderApiVersions] Processing Provider Namespace [$($private:Provider.namespace)]"
            foreach ($private:Type in $private:Provider.resourceTypes) {
                # Check for latest ApiVersions and add to cache
                [ProviderApiVersions]::AddToCache(
                    $private:Provider.namespace.ToString(),
                    $private:Type.resourceType.ToString(),
                    $private:Type.ApiVersions
                )
            }
        }
    }

    # Static method to add provider instance to Cache
    hidden static [Void] AddToCache([String]$Provider, [String]$ResourceType, [Array]$ApiVersions) {
        Write-Debug "[ProviderApiVersions] Adding [$($Provider)/$($ResourceType)] to Cache"
        $private:AzStateProviderObject = [PsCustomObject]@{
            Provider     = "$Provider"
            ResourceType = "$ResourceType"
            Type         = "$Provider/$ResourceType"
            ApiVersions  = $ApiVersions
        }
        $private:CacheKey = "$Provider/$ResourceType"
        $private:CacheKeyLowercase = $private:CacheKey.ToString().ToLower()
        $private:CacheValue = [ProviderApiVersions]::new($private:AzStateProviderObject)
        $private:TryAdd = ([ProviderApiVersions]::Cache).TryAdd($private:CacheKeyLowercase, $private:CacheValue)
        if ($private:TryAdd) {
            Write-Verbose "[ProviderApiVersions] Added Resource Type to Cache [$private:CacheKey]"
        }
    }

    # Static method to initialize Cache
    # Will also reset cache if exists
    static [Void] InitializeCache() {
        Write-Verbose "[ProviderApiVersions] Initializing Cache (Empty)"
        [ProviderApiVersions]::Cache = [System.Collections.Concurrent.ConcurrentDictionary[String, ProviderApiVersions]]::new()
    }

    # Static method to clear all entries from Cache
    static [Void] ClearCache() {
        [ProviderApiVersions]::InitializeCache()
    }

    # Static method to save all entries from Cache to filesystem
    static [Void] SaveCacheToDirectory() {
        [ProviderApiVersions]::SaveCacheToDirectory("./")
    }

    # Static method to save all entries from Cache to filesystem
    static [Void] SaveCacheToDirectory([String]$Directory) {
        if ([ProviderApiVersions]::Cache.Count -lt 1) {
            [ProviderApiVersions]::UpdateCache()
        }
        $private:saveCachePath = "$Directory/ProviderApiVersions"
        [ProviderApiVersions]::Cache |
        ConvertTo-Json -Depth 10 -Compress |
        Out-File -FilePath "$($private:saveCachePath).json" `
            -Force
        try {
            Compress-Archive -Path "$($private:saveCachePath).json" `
                -DestinationPath "$($private:saveCachePath).zip" `
                -Force
        }
        finally {
            Remove-Item -Path "$($private:saveCachePath).json" `
                -Force
        }
    }

    # Static method to load all entries from filesystem to Cache
    static [Void] LoadCacheFromDirectory() {
        [ProviderApiVersions]::LoadCacheFromDirectory("./")
    }

    # Static method to load all entries from filesystem to Cache
    static [Void] LoadCacheFromDirectory([String]$Directory) {
        [ProviderApiVersions]::ClearCache()
        $private:loadCachePath = "$Directory/ProviderApiVersions"
        Expand-Archive -Path "$($private:loadCachePath).zip" `
            -DestinationPath "$Directory" `
            -Force
        try {
            $private:loadCacheObject = Get-Content `
                -Path "$($private:loadCachePath).json" `
                -Force |
            ConvertFrom-Json
            foreach ($key in $private:loadCacheObject.psobject.Properties.Name) {
                $private:value = $private:loadCacheObject."$key"
                ([ProviderApiVersions]::Cache).TryAdd($key, $private:value)
            }
        }
        catch {
            Write-Error $_.Exception.Message
        }
        finally {
            Remove-Item -Path "$($private:loadCachePath).json" `
                -Force
        }
    }

}

###############
# ALZ Classes #
###############

# The ALZ classes are used to create resource objects with consistent
# formatting for all Azure resources handled by the ALZ Tools module.

class ALZBase : System.Collections.Specialized.OrderedDictionary {

    ALZBase(): base() {}

    [String] ToString() {
        if ($this.GetType() -notin "String", "Boolean", "Int") {
            return $this | ConvertTo-Json -Depth 1 -WarningAction SilentlyContinue | ConvertFrom-Json
        }
        else {
            return $this
        }
    }

}

class PolicyAssignmentProperties : ALZBase {
    [String]$displayName = ""
    [Object]$policyDefinitionId = ""
    [String]$scope = ""
    [String[]]$notScopes = @()
    [Object]$parameters = @{}
    [String]$description = ""
    [Object]$metadata = @{}
    [String]$enforcementMode = "Default"

    PolicyAssignmentProperties(): base() {}

    PolicyAssignmentProperties([Object]$that): base() {
        $this.displayName = $that.displayName
        $this.policyDefinitionId = $that.policyDefinitionId
        $this.scope = $that.scope
        $this.notScopes = $that.notScopes ?? $this.notScopes
        $this.parameters = $that.parameters ?? $this.parameters
        $this.description = $that.description ?? $that.displayName
        $this.metadata = $that.metadata ?? $this.metadata
        $this.enforcementMode = ([PolicyAssignmentPropertiesEnforcementMode]($that.enforcementMode ?? $this.enforcementMode)).ToString()
    }

}

class PolicyAssignmentIdentity : ALZBase {
    [String]$type = "None"

    PolicyAssignmentIdentity(): base() {}

    PolicyAssignmentIdentity([Object]$that): base() {
        $this.type = ([PolicyAssignmentIdentityType]($that.type ?? $this.type)).ToString()
    }

}

class PolicyDefinitionProperties : ALZBase {
    [String]$policyType = "NotSpecified"
    [String]$mode = ""
    [String]$displayName = ""
    [String]$description = ""
    [Object]$metadata = @{}
    [Object]$parameters = @{}
    [Object]$policyRule = @{}

    PolicyDefinitionProperties(): base() {}

    PolicyDefinitionProperties([Object]$that): base() {
        $this.policyType = ([PolicySetDefinitionPropertiesPolicyType]($that.policyType ?? $this.policyType)).ToString()
        $this.mode = ([PolicyDefinitionPropertiesMode]($that.mode)).ToString()
        $this.displayName = $that.displayName
        $this.description = $that.description ?? $that.displayName
        $this.metadata = $that.metadata ?? $this.metadata
        $this.parameters = $that.parameters ?? $this.parameters
        $this.policyRule = $that.policyRule
    }

}

class PolicySetDefinitionPropertiesPolicyDefinitions : ALZBase {
    [String]$policyDefinitionReferenceId = ""
    [String]$policyDefinitionId = ""
    [Object]$parameters = @{}
    [Array]$groupNames = @()
    [String]$definitionVersion = ""

    PolicySetDefinitionPropertiesPolicyDefinitions(): base() {}

    PolicySetDefinitionPropertiesPolicyDefinitions([Object]$that): base() {
        $this.policyDefinitionReferenceId = $that.policyDefinitionReferenceId
        $this.policyDefinitionId = $that.policyDefinitionId
        $this.parameters = $that.parameters ?? $this.parameters
        $this.groupNames = $that.groupNames ?? $this.groupNames
        $this.definitionVersion = $that.definitionVersion ?? $this.definitionVersion
    }

}

class PolicySetDefinitionPropertiesPolicyDefinitionGroup : ALZBase {
    [String]$name = ""
    [String]$displayName = ""
    [String]$category = ""
    [String]$description = ""
    [String]$additionalMetadataId = ""

    PolicySetDefinitionPropertiesPolicyDefinitionGroup(): base() {}

    PolicySetDefinitionPropertiesPolicyDefinitionGroup([Object]$that): base() {
        $this.name = $that.name
        $this.displayName = $that.displayName
        $this.category = $that.category
        $this.description = $that.description
        $this.additionalMetadataId = $that.additionalMetadataId
    }

}

class PolicySetDefinitionProperties : ALZBase {
    [String]$policyType = "NotSpecified"
    [String]$displayName = ""
    [String]$description = ""
    [Object]$metadata = @{}
    [Object]$parameters = @{}
    [Array]$policyDefinitions = @()
    [Array]$policyDefinitionGroups = $null

    PolicySetDefinitionProperties(): base() {}

    PolicySetDefinitionProperties([Object]$that): base() {
        $this.policyType = ([PolicySetDefinitionPropertiesPolicyType]($that.policyType ?? $this.policyType)).ToString()
        $this.displayName = $that.displayName ?? ""
        $this.description = $that.description ?? $that.displayName
        $this.metadata = $that.metadata ?? $this.metadata
        $this.parameters = $that.parameters ?? $this.parameters
        $this.policyDefinitions = foreach ($policyDefinition in $that.policyDefinitions) {
            [PolicySetDefinitionPropertiesPolicyDefinitions]::new($policyDefinition)
        }
        $this.policyDefinitionGroups = foreach ($policyDefinitionGroup in $that.policyDefinitionGroups) {
            [PolicySetDefinitionPropertiesPolicyDefinitionGroup]::new($that.policyDefinitionGroups)
        }
    }

}

class RoleAssignmentProperties : ALZBase {
    RoleAssignmentProperties(): base() {}
}

class RoleDefinitionPropertiesPermissions {
    [String[]]$actions = @()
    [String[]]$notActions = @()
    [String[]]$dataActions = @()
    [String[]]$notDataActions = @()

    RoleDefinitionPropertiesPermissions(): base() {}

    RoleDefinitionPropertiesPermissions([Object]$that): base() {
        $this.actions = $that.actions ?? $this.actions
        $this.notActions = $that.notActions ?? $that.notActions
        $this.dataActions = $that.dataActions ?? $this.dataActions
        $this.notDataActions = $that.notDataActions ?? $this.notDataActions
    }

}

class RoleDefinitionProperties : ALZBase {
    [String]$roleName = ""
    [String]$description = ""
    [String]$type = "customRole"
    [Array]$permissions = @()
    [Array]$assignableScopes = @()

    RoleDefinitionProperties(): base() {}

    RoleDefinitionProperties([Object]$that): base() {
        $this.roleName = $that.roleName
        $this.description = $that.description ?? $that.roleName
        $this.type = $that.type ?? $this.type
        $this.permissions = @(
            [PolicyAssignmentIdentity]::new($that.permissions[0])
        )
        $this.assignableScopes = $that.assignableScopes ?? $this.assignableScopes
    }

}

class ArmTemplateResource : ALZBase {

    # Public class properties
    # Need to declare base object properties with default values to set order
    [String]$name = ""
    [String]$type = ""
    [String]$apiVersion = ""
    [Object]$scope = $null # Needs to be declared as object to avoid null returning empty string in JSON output
    [Object]$properties = @{}

    # Hidden static class properties
    hidden static [GetFileNameCaseModifier]$GetFileNameCaseModifier = "ToLower" # Default to make lowercase
    hidden static [Regex]$regexReplaceFileNameCharacters = "\W" # Default to replace all non word characters
    hidden static [String]$GetFileNameSubstituteCharacter = "_"
    hidden static [Regex]$regexExtractProviderId = "\/providers\/(?!.*\/providers\/)[\/\w-.]+"

    ArmTemplateResource(): base() {}

    ArmTemplateResource([PSCustomObject]$that): base() {
        $this.name = $that.name
        $this.type = $that.ResourceType ?? $that.type
        $this.apiVersion = $that.apiVersion
        $this.scope = if ($that.scope.Length -gt 0) { $that.scope } else { $null }
        $this.properties = $that.properties
    }

    # Initialize [ArmTemplateResource] object
    [Void] SetApiVersion([String]$ResourceType) {
        $this.apiVersion = [ProviderApiVersions]::GetLatestStableByType($ResourceType)
    }

    # String modifier for template languages
    static [String] ConvertToTemplateVariable([String]$Variable, [ExportFormat]$ExportFormat) {
        $TemplateVariable = "$Variable"
        Switch ($ExportFormat) {
            "Jinja2" { $TemplateVariable = "{{ $Variable }}" }
            "Terraform" { $TemplateVariable = "`${$Variable}" }
            Default { $TemplateVariable = "$Variable" }
        }
        return $TemplateVariable
    }

    # Update resource values as per requirements for export format
    [Object] Format([ExportFormat]$ExportFormat) {
        if ($this.type -eq "Microsoft.Authorization/policyAssignments") {
            $this.properties.scope = [ArmTemplateResource]::ConvertToTemplateVariable("current_scope_resource_id", $ExportFormat)
            $this.properties.policyDefinitionId = [ArmTemplateResource]::ConvertToTemplateVariable("root_scope_resource_id", $ExportFormat)
            $this.location = [ArmTemplateResource]::ConvertToTemplateVariable("default_location", $ExportFormat)
        }
        if ($this.type -eq "Microsoft.Authorization/policyDefinitions") {
            $this.properties.policyType = "Custom"
        }
        if ($this.type -eq "Microsoft.Authorization/policySetDefinitions") {
            $this.properties.policyType = "Custom"
            foreach ($policyDefinition in $this.properties.policyDefinitions) {
                $regexMatches = [ArmTemplateResource]::regexExtractProviderId.Matches($policyDefinition.policyDefinitionId)
                $policyDefinitionId = switch ($ExportFormat) {
                    "ArmResource" { "/providers/Microsoft.Management/managementGroups/contoso$($regexMatches.Value)" }
                    "ArmVariable" { "[concat(variables('scope'), '$($regexMatches.Value)')]" }
                    "Bicep" { "`${varTargetManagementGroupResourceId}$($regexMatches.Value)" }
                    "Raw" { "$($policyDefinition.policyDefinitionId)" }
                    "Jinja2" { "$([ArmTemplateResource]::ConvertToTemplateVariable("root_scope_resource_id", $ExportFormat))$($regexMatches.Value)" }
                    "Terraform" { "$([ArmTemplateResource]::ConvertToTemplateVariable("root_scope_resource_id", $ExportFormat))$($regexMatches.Value)" }
                    Default { "$($policyDefinition.policyDefinitionId)" }
                }
                if ($regexMatches.Index -gt 0) {
                    $policyDefinition.policyDefinitionId = "$policyDefinitionId"
                }
                else {
                    $policyDefinition.policyDefinitionId = $regexMatches.Value
                }
            }
        }
        return $this
    }

    [String] GetFileName() {
        $fileName = $this.GetFileName("", ".json", "Raw")
        return $fileName
    }

    [String] GetFileName([String]$Prefix, [String]$Suffix, [ExportFormat]$ExportFormat) {
        $fileName = "$($this.name)"
        if ($ExportFormat -eq "Terraform") {
            # Perform character substitution
            $fileName = [ArmTemplateResource]::regexReplaceFileNameCharacters.Replace($fileName, [ArmTemplateResource]::GetFileNameSubstituteCharacter)
            # Modify case
            $fileName = $fileName.$([ArmTemplateResource]::GetFileNameCaseModifier)()
        }
        $fileName = $Prefix + $fileName + $Suffix
        return $fileName
    }

}

class PolicyAssignment : ArmTemplateResource {

    # Need to re-declare base object properties with default values to maintain order
    [String]$name = ""
    [String]$type = ""
    [String]$apiVersion = ""
    [String]$scope = ""
    [Object]$properties = @{}
    [String]$location = ""
    [Object]$identity = @{}

    PolicyAssignment(): base() {}

    PolicyAssignment([PSCustomObject]$that): base($that) {
        $this.type = "Microsoft.Authorization/policyAssignments"
        $this.SetApiVersion($this.type)
        $this.location = $that.location
        $this.identity = [PolicyAssignmentIdentity]::new($that.identity)
        $this.properties = [PolicyAssignmentProperties]::new($this.properties)
    }

}

class PolicyDefinition : ArmTemplateResource {

    PolicyDefinition(): base() {}

    PolicyDefinition([PSCustomObject]$that): base($that) {
        $this.type = "Microsoft.Authorization/policyDefinitions"
        $this.SetApiVersion($this.type)
        $this.properties = [PolicyDefinitionProperties]::new($this.properties)
    }

}

class PolicySetDefinition : ArmTemplateResource {

    PolicySetDefinition(): base() {}

    PolicySetDefinition([PSCustomObject]$that): base($that) {
        $this.type = "Microsoft.Authorization/policySetDefinitions"
        $this.SetApiVersion($this.type)
        $this.properties = [PolicySetDefinitionProperties]::new($this.properties)
    }

}

class RoleAssignment : ArmTemplateResource {

    RoleAssignment(): base() {}

    RoleAssignment([PSCustomObject]$that): base($that) {
        $this.type = "Microsoft.Authorization/roleAssignments"
        $this.SetApiVersion($this.type)
        $this.properties = [RoleAssignmentProperties]::new($this.properties)
    }
}

class RoleDefinition : ArmTemplateResource {

    RoleDefinition(): base() {}

    RoleDefinition([PSCustomObject]$that): base($that) {
        $this.type = "Microsoft.Authorization/roleDefinitions"
        $this.SetApiVersion($this.type)
        $this.properties = [RoleDefinitionProperties]::new($this.properties)
    }

}