PSRule/Rules/AAC.Tags.Rule.ps1

# Azure.Admin.Console's own PSRule rules, run by Invoke-AACPSRule with PSRule
# for Azure's. They check what PSRule for Azure leaves to each organisation:
# which tags resources must carry, and the values they may have. Each rule
# does nothing until its setting is given, e.g.
#
# Invoke-AACPSRule -Configuration @{
# AAC_REQUIRED_TAGS = @('Owner', 'CostCenter', 'Environment')
# AAC_ALLOWED_TAG_VALUES = @{ Environment = @('prod', 'test', 'dev') }
# }
#
# Their help (synopsis, recommendation, links) is in en\<rule name>.md next to
# this file. Leave them out with -ExcludeRule 'AAC.*'.
#
# To add rules of your own, write them the same way - PowerShell
# (*.Rule.ps1), YAML (*.Rule.yaml) or JSON (*.Rule.jsonc); see
# https://microsoft.github.io/PSRule/v2/authoring/writing-rules/ - and pass
# their files or folder to Invoke-AACPSRule -RulePath.

# A resource that can carry tags: anything under a resource provider - not a
# resource group or subscription, which have rules of their own.
function global:Test-AACTaggableResource {
    [string]$TargetObject.id -match '/providers/' -and [string]$TargetObject.type -notmatch '^microsoft\.(resources|subscription)'
}

# Synopsis: Resources carry every tag your organisation requires.
Rule 'AAC.Resource.RequiredTags' -Ref 'AAC-001' -Level Error -Tag @{ release = 'GA'; 'Azure.WAF/pillar' = 'Operational Excellence' } -If {
    (Test-AACTaggableResource) -and @($Configuration.GetStringValues('AAC_REQUIRED_TAGS')).Count -gt 0
} {
    foreach ($name in $Configuration.GetStringValues('AAC_REQUIRED_TAGS')) {
        $Assert.HasFieldValue($TargetObject, "tags.$name").Reason('The required tag ''{0}'' is missing or empty.', $name)
    }
}

# Synopsis: Resource groups carry every tag your organisation requires.
Rule 'AAC.ResourceGroup.RequiredTags' -Ref 'AAC-002' -Level Error -Type 'Microsoft.Resources/resourceGroups' -Tag @{ release = 'GA'; 'Azure.WAF/pillar' = 'Operational Excellence' } -If {
    @($Configuration.GetStringValues('AAC_REQUIRED_TAGS')).Count -gt 0
} {
    foreach ($name in $Configuration.GetStringValues('AAC_REQUIRED_TAGS')) {
        $Assert.HasFieldValue($TargetObject, "tags.$name").Reason('The required tag ''{0}'' is missing or empty.', $name)
    }
}

# Synopsis: Tags with a fixed set of values use one of them.
Rule 'AAC.Resource.AllowedTagValues' -Ref 'AAC-003' -Level Warning -Tag @{ release = 'GA'; 'Azure.WAF/pillar' = 'Operational Excellence' } -If {
    ((Test-AACTaggableResource) -or [string]$TargetObject.type -eq 'Microsoft.Resources/resourceGroups') -and
    $null -ne $Configuration.GetValueOrDefault('AAC_ALLOWED_TAG_VALUES', $null)
} {
    $allowed = $Configuration.GetValueOrDefault('AAC_ALLOWED_TAG_VALUES', $null)
    $names = if ($allowed -is [System.Collections.IDictionary]) { @($allowed.Keys) } else { @($allowed.PSObject.Properties.Name) }
    $checked = 0
    foreach ($name in $names) {
        $values = @($(if ($allowed -is [System.Collections.IDictionary]) { $allowed[$name] } else { $allowed.$name }) | ForEach-Object { [string]$_ })
        $tag = $TargetObject.tags.PSObject.Properties | Where-Object { $_.Name -eq $name } | Select-Object -First 1
        if ($tag) {
            $checked++
            $Assert.In($TargetObject, "tags.$($tag.Name)", $values).Reason('The tag ''{0}'' is ''{1}''; it should be one of: {2}.', $name, $tag.Value, ($values -join ', '))
        }
    }
    if ($checked -eq 0) {
        $Assert.Pass()
    }
}