PSRule/Rules/AAC.Naming.Rule.ps1

# Azure.Admin.Console's naming rule, run by Invoke-AACPSRule with PSRule for
# Azure's. Every resource type in Get-AACNamingDefault below must have names
# matching its pattern - Microsoft's Cloud Adoption Framework abbreviations
# (https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations).
# It runs without any setting:
#
# Invoke-AACPSRule -Rule 'AAC.Resource.Naming'
#
# To change the convention:
# - add or edit lines in Get-AACNamingDefault below (one per type), or
# - pass AAC_NAMING_PATTERNS in -Configuration: its types replace or add to
# the defaults, and '' turns a type off
# - AAC_NAMING_IGNORE: name patterns never checked
#
# Invoke-AACPSRule -Rule 'AAC.Resource.Naming' -Configuration @{
# AAC_NAMING_PATTERNS = @{ 'Microsoft.Compute/virtualMachines' = '^vm-(prod|dev)-'; 'Microsoft.Web/sites' = '' }
# AAC_NAMING_IGNORE = @('^legacy-')
# }
#
# Types and names are compared case-insensitively. Names Azure creates itself,
# which can't be renamed (AKS node resource groups, NetworkWatcherRG, ...),
# are skipped. Its help is in en\AAC.Resource.Naming.md. Leave it out with
# -ExcludeRule 'AAC.Resource.Naming'.
#
# Invoke-AACPSRule reads this table too (without running this file), so a run
# of this rule alone reads only these types: keep it a plain table of
# 'type' = 'pattern' strings.

# Resource type -> the pattern its names must match. One line per type: add
# your own here. Disks, network interfaces and VM extensions are left out:
# Azure and the portal name them (vm-web-01_OsDisk_1_...).
function global:Get-AACNamingDefault {
    @{
        'Microsoft.Resources/resourceGroups'               = '^rgp-'
        'Microsoft.Compute/virtualMachines'                = '^vm'
        'Microsoft.Compute/virtualMachineScaleSets'        = '^vmss'
        'Microsoft.Network/virtualNetworks'                = '^vnet-'
        'Microsoft.Network/networkSecurityGroups'          = '^nsg-'
        'Microsoft.Network/publicIPAddresses'              = '^pip-'
        'Microsoft.Network/loadBalancers'                  = '^lb[ie]?-'
        'Microsoft.Network/applicationGateways'            = '^agw-'
        'Microsoft.Network/azureFirewalls'                 = '^afw-'
        'Microsoft.Network/routeTables'                    = '^rt-'
        'Microsoft.Network/bastionHosts'                   = '^bas-'
        'Microsoft.Network/privateEndpoints'               = '^pep-'
        'Microsoft.KeyVault/vaults'                        = '^kv-'
        'Microsoft.Storage/storageAccounts'                = '^st[a-z0-9]{3,22}$'
        'Microsoft.Web/sites'                              = '^(app|func)-'
        'Microsoft.Web/serverFarms'                        = '^asp-'
        'Microsoft.Sql/servers'                            = '^sql-'
        'Microsoft.ContainerService/managedClusters'       = '^aks-'
        'Microsoft.ContainerRegistry/registries'           = '^cr[a-z0-9]+$'
        'Microsoft.OperationalInsights/workspaces'         = '^log-'
        'Microsoft.Insights/components'                    = '^appi-'
        'Microsoft.ApiManagement/service'                  = '^apim-'
        'Microsoft.DocumentDB/databaseAccounts'            = '^cosmos-'
        'Microsoft.ServiceBus/namespaces'                  = '^sbns-'
        'Microsoft.EventHub/namespaces'                    = '^evhns-'
        'Microsoft.ManagedIdentity/userAssignedIdentities' = '^id-'
        'Microsoft.Automation/automationAccounts'          = '^aa-'
        'Microsoft.RecoveryServices/vaults'                = '^rsv-'
    }
}

# The pattern for the target's type, and the type as the table spells it:
# -Configuration's AAC_NAMING_PATTERNS first ('' turns the type off), then the
# defaults. The setting arrives as a hashtable, or as an object when read
# from JSON. Returns @{ Type; Pattern }, or nothing.
function global:Get-AACNamingPattern {
    $type = [string]$TargetObject.type
    $patterns = $Configuration.GetValueOrDefault('AAC_NAMING_PATTERNS', $null)
    if ($null -ne $patterns) {
        $pairs = if ($patterns -is [System.Collections.IDictionary]) {
            @($patterns.Keys | ForEach-Object { @{ Type = [string]$_; Pattern = $patterns[$_] } })
        }
        else {
            @($patterns.PSObject.Properties | ForEach-Object { @{ Type = [string]$_.Name; Pattern = $_.Value } })
        }
        foreach ($pair in $pairs) {
            if ($pair.Type -eq $type) { return @{ Type = $pair.Type; Pattern = [string]$pair.Pattern } }
        }
    }
    $defaults = Get-AACNamingDefault
    foreach ($key in $defaults.Keys) {
        if ($key -eq $type) { return @{ Type = [string]$key; Pattern = [string]$defaults[$key] } }
    }
}

# A name Azure (or a service) creates and manages, which can't be renamed -
# or one the AAC_NAMING_IGNORE patterns leave out.
function global:Test-AACNamingIgnored {
    $name = [string]$TargetObject.name
    $builtIn = @(
        '^MC_'                        # AKS node resource group
        '^NetworkWatcherRG$'          # Network Watcher
        '^DefaultResourceGroup-'      # Log Analytics / Defender defaults
        '^AzureBackupRG_'             # Azure Backup restore points
        '^databricks-rg-'             # Azure Databricks managed group
        '^cloud-shell-storage-'       # Cloud Shell
        '^LogAnalyticsDefaultResources$'
    )
    foreach ($pattern in @($builtIn) + @($Configuration.GetStringValues('AAC_NAMING_IGNORE'))) {
        if ($pattern -and $name -match $pattern) { return $true }
    }
    # A resource another service manages (managedBy set) keeps the name it was given.
    [bool]$TargetObject.managedBy
}

# Synopsis: Resources and resource groups follow the naming convention.
Rule 'AAC.Resource.Naming' -Ref 'AAC-004' -Level Warning -Tag @{ release = 'GA'; 'Azure.WAF/pillar' = 'Operational Excellence' } -If {
    $convention = Get-AACNamingPattern
    $convention -and $convention.Pattern -and -not (Test-AACNamingIgnored)
} {
    $convention = Get-AACNamingPattern
    $Assert.Match($TargetObject, 'name', $convention.Pattern).Reason('The name doesn''t match ''{0}'', the naming convention for {1}.', $convention.Pattern, $convention.Type)
}