Private/Get-AACWafCatalog.ps1
|
function Get-AACWafCatalog { <# .SYNOPSIS The Well-Architected checks Invoke-AACWafAssessment runs: each pillar's design principles, and the checks that test them against the shared Resource Graph read. .DESCRIPTION Returns @{ Principles; Checks; Overlaps }: Principles ordered: pillar -> @(@{ Name; Link; Question }), in Microsoft Learn's order - a principle with no check is shown as not assessed (it needs a person to attest it) Checks @{ Id; Pillar; Principle; Title; Severity (High, Medium, Low); Type (the resource type it tests, lower case; 'microsoft.resources/subscriptions' for a subscription); Applies (optional: { param($Row, $Context) } - $false leaves the resource out); Test ({ param($Row, $Context) } - $true passes, $false fails); Recommendation; Link } Overlaps @{ Pattern; Checks } - an Advisor recommendation or Defender assessment whose text matches Pattern (a regular expression, any case) is the same finding as those catalogue checks: on a resource they tested, it is counted once, by the catalogue check $Context.At($Row, 'properties', 'x', ...) reads a value safely (exact key first, then any case); $Context.Backup is the set of protected resource IDs; $Context.Pricing[sub][plan] the Defender plan tier; $Context.SecureScore[sub] the secure score as a percentage; $C.Extensions[vm id] the VM's extension types; $C.Autoscale the resource IDs an enabled autoscale setting targets; $C.Slots[site id] how many deployment slots an app has; $C.TypeCount[sub][type] how many resources of a type a subscription has; $C.CostTag and $C.OwnerTag the tag names that count; $C.Now the time of the run. Advisor and Defender fold into checks of their own in ConvertTo-AACWafAssessment. #> [CmdletBinding()] [OutputType([hashtable])] param() $learn = 'https://learn.microsoft.com/azure/well-architected' $principles = [ordered]@{ Reliability = @( @{ Name = 'Design for business requirements'; Link = "$learn/reliability/principles#design-for-business-requirements" } @{ Name = 'Design for resilience'; Link = "$learn/reliability/principles#design-for-resilience" } @{ Name = 'Design for recovery'; Link = "$learn/reliability/principles#design-for-recovery" } @{ Name = 'Design for operations'; Link = "$learn/reliability/principles#design-for-operations" } @{ Name = 'Keep it simple'; Link = "$learn/reliability/principles#keep-it-simple" } ) Security = @( @{ Name = 'Plan your security readiness'; Link = "$learn/security/principles#plan-your-security-readiness" } @{ Name = 'Design to protect confidentiality'; Link = "$learn/security/principles#design-to-protect-confidentiality" } @{ Name = 'Design to protect integrity'; Link = "$learn/security/principles#design-to-protect-integrity" } @{ Name = 'Design to protect availability'; Link = "$learn/security/principles#design-to-protect-availability" } @{ Name = 'Sustain and evolve your security posture'; Link = "$learn/security/principles#sustain-and-evolve-your-security-posture" } ) 'Cost Optimization' = @( @{ Name = 'Develop cost-management discipline'; Link = "$learn/cost-optimization/principles#develop-cost-management-discipline" } @{ Name = 'Design with a cost-efficiency mindset'; Link = "$learn/cost-optimization/principles#design-with-a-cost-efficiency-mindset" } @{ Name = 'Design for usage optimization'; Link = "$learn/cost-optimization/principles#design-for-usage-optimization" } @{ Name = 'Design for rate optimization'; Link = "$learn/cost-optimization/principles#design-for-rate-optimization" } @{ Name = 'Monitor and optimize over time'; Link = "$learn/cost-optimization/principles#monitor-and-optimize-over-time" } ) 'Operational Excellence' = @( @{ Name = 'Embrace DevOps culture'; Link = "$learn/operational-excellence/principles#embrace-devops-culture" } @{ Name = 'Establish development standards'; Link = "$learn/operational-excellence/principles#establish-development-standards" } @{ Name = 'Evolve operations with observability'; Link = "$learn/operational-excellence/principles#evolve-operations-with-observability" } @{ Name = 'Deploy with confidence'; Link = "$learn/operational-excellence/principles#deploy-with-confidence" } @{ Name = 'Automate for efficiency'; Link = "$learn/operational-excellence/principles#automate-for-efficiency" } ) 'Performance Efficiency' = @( @{ Name = 'Negotiate realistic performance targets'; Link = "$learn/performance-efficiency/principles#negotiate-realistic-performance-targets" } @{ Name = 'Design to meet capacity requirements'; Link = "$learn/performance-efficiency/principles#design-to-meet-capacity-requirements" } @{ Name = 'Achieve and sustain performance'; Link = "$learn/performance-efficiency/principles#achieve-and-sustain-performance" } @{ Name = 'Improve efficiency through optimization'; Link = "$learn/performance-efficiency/principles#improve-efficiency-through-optimization" } ) } # The questionnaire: one question per principle, for what no API can test # (Export-AACWafQuestionnaire writes them; -AttestationPath reads the answers). $questions = @{ 'Design for business requirements' = 'Have the availability, RTO and RPO targets been agreed with the business, and does the design meet them?' 'Design for resilience' = 'Does the workload keep running through the loss of an instance, a zone - and a region, if required?' 'Design for recovery' = 'Are backups and failover tested regularly against the recovery targets, with a documented recovery plan?' 'Design for operations' = 'Do health probes, monitoring and alerts detect failures early, with runbooks for the response?' 'Keep it simple' = 'Is the design as simple as the requirements allow, without components or dependencies it doesn''t need?' 'Plan your security readiness' = 'Is there a security baseline, a threat model and an owner, with security in the definition of done?' 'Design to protect confidentiality' = 'Is data classified, encrypted in transit and at rest, and reachable only over private or restricted networks?' 'Design to protect integrity' = 'Are identities least-privileged (managed identities, no shared secrets), and are code and configuration changes controlled?' 'Design to protect availability' = 'Are the public entry points protected from DDoS and abuse (WAF, rate limiting)?' 'Sustain and evolve your security posture' = 'Are security findings reviewed and remediated on a schedule, and is incident response practised?' 'Develop cost-management discipline' = 'Does the workload have a budget, cost alerts and an accountable owner, with costs allocated by tag?' 'Design with a cost-efficiency mindset' = 'Were cost trade-offs weighed when choosing services, tiers and regions?' 'Design for usage optimization' = 'Are unused and idle resources removed, and non-production scaled down or shut off outside working hours?' 'Design for rate optimization' = 'Are reservations, savings plans and Azure Hybrid Benefit used for steady workloads?' 'Monitor and optimize over time' = 'Are costs and Advisor''s cost recommendations reviewed regularly against the forecast?' 'Embrace DevOps culture' = 'Does the team own the workload end to end, with shared on-call, blameless reviews and time for operational work?' 'Establish development standards' = 'Are infrastructure and configuration defined as code, reviewed, and named and tagged to a standard?' 'Evolve operations with observability' = 'Are logs, metrics and traces collected centrally, with dashboards and actionable alerts?' 'Deploy with confidence' = 'Are deployments automated through a pipeline, with tests, progressive exposure and a fast rollback?' 'Automate for efficiency' = 'Are repetitive operations - patching, scaling, certificate renewal, remediation - automated?' 'Negotiate realistic performance targets' = 'Are performance targets (latency, throughput, concurrency) defined and agreed for the key user flows?' 'Design to meet capacity requirements' = 'Is capacity planned for expected and peak load, with autoscaling and load tests to prove it?' 'Achieve and sustain performance' = 'Is performance monitored against the targets in production, with regressions caught before release?' 'Improve efficiency through optimization' = 'Are hot paths, data access and caching reviewed and tuned regularly?' } foreach ($pillar in @($principles.Keys)) { foreach ($p in $principles[$pillar]) { $p.Question = $questions[$p.Name] } } # Small helpers the checks share, all through $C (the context). $true1 = { param($Value) [string]$Value -eq 'True' } $count = { param($Value) @($Value | Where-Object { $null -ne $_ -and [string]$_ -ne '' }).Count } $restricted = { param($Row, $C) [string](& $C.At $Row 'properties', 'publicNetworkAccess') -eq 'Disabled' -or [string](& $C.At $Row 'properties', 'networkAcls', 'defaultAction') -eq 'Deny' } $paidPlan = { param($Row, $C) [string](& $C.At $Row 'sku', 'tier') -notin 'Free', 'Shared', 'Basic', 'Dynamic', 'FlexConsumption', '' } $planOn = { param($Row, $C, [string] $Plan) $tiers = $C.Pricing[[string](& $C.At $Row 'subscriptionId')]; $tiers -and $tiers.Contains($Plan) -and $tiers[$Plan] -eq 'Standard' } $managementOpen = { # An inbound allow from anywhere to SSH (22) or RDP (3389). param($Row, $C) foreach ($rule in @(& $C.At $Row 'properties', 'securityRules')) { $r = & $C.At $rule 'properties' if ([string](& $C.At $r 'direction') -ne 'Inbound' -or [string](& $C.At $r 'access') -ne 'Allow') { continue } $sources = @(@(& $C.At $r 'sourceAddressPrefix') + @(& $C.At $r 'sourceAddressPrefixes') | Where-Object { $_ } | ForEach-Object { [string]$_ }) if (-not @($sources | Where-Object { $_ -in '*', 'Internet', 'Any', '0.0.0.0/0' }).Count) { continue } foreach ($spec in @(@(& $C.At $r 'destinationPortRange') + @(& $C.At $r 'destinationPortRanges') | Where-Object { $_ } | ForEach-Object { [string]$_ })) { if ($spec -eq '*') { return $true } $low, $high = if ($spec -match '^(\d+)-(\d+)$') { [int]$Matches[1], [int]$Matches[2] } elseif ($spec -match '^\d+$') { [int]$spec, [int]$spec } else { 1, 0 } foreach ($port in 22, 3389) { if ($port -ge $low -and $port -le $high) { return $true } } } } $false } $hasTag = { param($Row, $C, [string[]] $Names) $tags = & $C.At $Row 'tags'; $tags -is [System.Collections.IDictionary] -and @($tags.Keys | Where-Object { $k = $_; @($Names | Where-Object { $k -eq $_ }).Count }).Count -gt 0 } $anyOfType = { param($Row, $C, [string[]] $Types) $counts = $C.TypeCount[[string](& $C.At $Row 'subscriptionId')]; $counts -and @($Types | Where-Object { $counts.Contains($_) -and $counts[$_] -gt 0 }).Count -gt 0 } $oldSeries = '^(Basic_A|Standard_A\d|Standard_D\d+$|Standard_DS\d+$|Standard_D\d+_v2|Standard_DS\d+_v2|Standard_F\d+$|Standard_G)' $re = "$learn/reliability" $check = { param($Id, $Pillar, $Principle, $Title, $Severity, $Type, $Test, $Recommendation, $Link, $Applies) @{ Id = $Id; Pillar = $Pillar; Principle = $Principle; Title = $Title; Severity = $Severity; Type = $Type; Test = $Test; Recommendation = $Recommendation; Link = $Link; Applies = $Applies } } $checks = @( # --- Reliability -------------------------------------------------------------------------------------- & $check 'RE-01' 'Reliability' 'Design for resilience' 'VM in an availability zone, availability set or scale set' 'High' 'microsoft.compute/virtualmachines' { param($Row, $C) (& $count (& $C.At $Row 'zones')) -gt 0 -or (& $C.At $Row 'properties', 'availabilitySet', 'id') -or (& $C.At $Row 'properties', 'virtualMachineScaleSet', 'id') } 'Deploy the VM into an availability zone (or a scale set across zones), so one datacentre failing doesn''t take it down.' "$re/regions-availability-zones" & $check 'RE-02' 'Reliability' 'Design for recovery' 'VM backed up' 'High' 'microsoft.compute/virtualmachines' { param($Row, $C) $C.Backup.Contains([string](& $C.At $Row 'id')) } 'Protect the VM with Azure Backup in a Recovery Services vault, with a policy that meets the workload''s recovery point objective.' "$re/backups" & $check 'RE-03' 'Reliability' 'Keep it simple' 'VM uses managed disks' 'Medium' 'microsoft.compute/virtualmachines' { param($Row, $C) [bool](& $C.At $Row 'properties', 'storageProfile', 'osDisk', 'managedDisk') } 'Move the VM to managed disks: Azure handles the storage accounts, their limits and their resilience.' 'https://learn.microsoft.com/azure/virtual-machines/managed-disks-overview' & $check 'RE-04' 'Reliability' 'Design for resilience' 'Storage account zone- or geo-redundant' 'Medium' 'microsoft.storage/storageaccounts' { param($Row, $C) [string](& $C.At $Row 'sku', 'name') -match 'ZRS|GRS' } 'Use ZRS, GZRS or RA-GZRS so the data survives a zone (or region) outage.' 'https://learn.microsoft.com/azure/storage/common/storage-redundancy' & $check 'RE-05' 'Reliability' 'Design for resilience' 'SQL database zone-redundant' 'Medium' 'microsoft.sql/servers/databases' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'zoneRedundant') } 'Turn on zone redundancy (General Purpose, Business Critical, Premium or Hyperscale).' 'https://learn.microsoft.com/azure/azure-sql/database/high-availability-sla' { param($Row, $C) (& $C.At $Row 'name') -ne 'master' -and [string](& $C.At $Row 'sku', 'tier') -notin 'Basic', 'Standard', 'System' } & $check 'RE-06' 'Reliability' 'Design for recovery' 'SQL database backups geo-redundant' 'Low' 'microsoft.sql/servers/databases' { param($Row, $C) [string](& $C.At $Row 'properties', 'currentBackupStorageRedundancy') -in 'Geo', 'GeoZone' } 'Store backups geo-redundantly, so a database can be restored in another region.' 'https://learn.microsoft.com/azure/azure-sql/database/automated-backups-overview' { param($Row, $C) (& $C.At $Row 'name') -ne 'master' } & $check 'RE-07' 'Reliability' 'Design for resilience' 'App Service plan zone-redundant' 'Medium' 'microsoft.web/serverfarms' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'zoneRedundant') } 'Make the plan zone-redundant (Premium v3 and up), so its instances spread across zones.' 'https://learn.microsoft.com/azure/reliability/reliability-app-service' $paidPlan & $check 'RE-08' 'Reliability' 'Design for resilience' 'App Service plan runs 2+ instances' 'Medium' 'microsoft.web/serverfarms' { param($Row, $C) [int](& $C.At $Row 'sku', 'capacity') -ge 2 } 'Run at least two instances, so one restarting or failing doesn''t stop the apps.' 'https://learn.microsoft.com/azure/reliability/reliability-app-service' $paidPlan & $check 'RE-09' 'Reliability' 'Design for resilience' 'Load balancer is Standard (not Basic)' 'High' 'microsoft.network/loadbalancers' { param($Row, $C) [string](& $C.At $Row 'sku', 'name') -ne 'Basic' } 'Upgrade to a Standard load balancer: Basic is retired, has no SLA and no zone redundancy.' 'https://learn.microsoft.com/azure/load-balancer/load-balancer-basic-upgrade-guidance' & $check 'RE-10' 'Reliability' 'Design for operations' 'Load balancer has health probes' 'Medium' 'microsoft.network/loadbalancers' { param($Row, $C) (& $count (& $C.At $Row 'properties', 'probes')) -gt 0 } 'Add a health probe to each rule, so traffic stops going to an instance that is down.' 'https://learn.microsoft.com/azure/load-balancer/load-balancer-custom-probe-overview' { param($Row, $C) (& $count (& $C.At $Row 'properties', 'loadBalancingRules')) -gt 0 } & $check 'RE-11' 'Reliability' 'Design for resilience' 'Public IP is Standard (not Basic)' 'High' 'microsoft.network/publicipaddresses' { param($Row, $C) [string](& $C.At $Row 'sku', 'name') -ne 'Basic' } 'Upgrade to a Standard public IP: Basic is retired and not zone-redundant.' 'https://learn.microsoft.com/azure/virtual-network/ip-services/public-ip-basic-upgrade-guidance' & $check 'RE-12' 'Reliability' 'Design for resilience' 'AKS node pools span availability zones' 'High' 'microsoft.containerservice/managedclusters' { param($Row, $C) $pools = @(& $C.At $Row 'properties', 'agentPoolProfiles'); $pools.Count -and -not @($pools | Where-Object { (& $count (& $C.At $_ 'availabilityZones')) -lt 2 }).Count } 'Spread every node pool across two or more availability zones.' 'https://learn.microsoft.com/azure/aks/availability-zones' & $check 'RE-13' 'Reliability' 'Design for business requirements' 'AKS on a tier with an uptime SLA' 'Medium' 'microsoft.containerservice/managedclusters' { param($Row, $C) [string](& $C.At $Row 'sku', 'tier') -in 'Standard', 'Premium' } 'Use the Standard (or Premium) tier for production: the Free tier has no financially backed SLA for the API server.' 'https://learn.microsoft.com/azure/aks/free-standard-pricing-tiers' & $check 'RE-14' 'Reliability' 'Design for resilience' 'Application Gateway zone-redundant with 2+ instances' 'Medium' 'microsoft.network/applicationgateways' { param($Row, $C) (& $count (& $C.At $Row 'zones')) -ge 2 -and ([int](& $C.At $Row 'properties', 'autoscaleConfiguration', 'minCapacity') -ge 2 -or [int](& $C.At $Row 'properties', 'sku', 'capacity') -ge 2) } 'Deploy a v2 Application Gateway across zones, with a minimum of two instances.' 'https://learn.microsoft.com/azure/reliability/reliability-application-gateway-v2' & $check 'RE-15' 'Reliability' 'Design for resilience' 'Cosmos DB multi-region or zone-redundant' 'Medium' 'microsoft.documentdb/databaseaccounts' { param($Row, $C) $locations = @(& $C.At $Row 'properties', 'locations'); $locations.Count -gt 1 -or @($locations | Where-Object { & $true1 (& $C.At $_ 'isZoneRedundant') }).Count } 'Add a second region, or turn on zone redundancy in the account''s region.' 'https://learn.microsoft.com/azure/reliability/reliability-cosmos-db-nosql' & $check 'RE-16' 'Reliability' 'Design for recovery' 'Cosmos DB continuous backup' 'Low' 'microsoft.documentdb/databaseaccounts' { param($Row, $C) [string](& $C.At $Row 'properties', 'backupPolicy', 'type') -eq 'Continuous' } 'Use continuous backup, for point-in-time restore.' 'https://learn.microsoft.com/azure/cosmos-db/continuous-backup-restore-introduction' & $check 'RE-17' 'Reliability' 'Design for recovery' 'Recovery Services vault geo-redundant' 'Medium' 'microsoft.recoveryservices/vaults' { param($Row, $C) [string](& $C.At $Row 'properties', 'redundancySettings', 'standardTierStorageRedundancy') -eq 'GeoRedundant' } 'Use geo-redundant storage (with cross-region restore) so backups survive a regional outage.' 'https://learn.microsoft.com/azure/backup/backup-create-recovery-services-vault' & $check 'RE-18' 'Reliability' 'Design for recovery' 'Key Vault soft delete on' 'Medium' 'microsoft.keyvault/vaults' { param($Row, $C) [string](& $C.At $Row 'properties', 'enableSoftDelete') -ne 'False' } 'Keep soft delete on, so a deleted vault, key or secret can be recovered.' 'https://learn.microsoft.com/azure/key-vault/general/soft-delete-overview' # --- Security ----------------------------------------------------------------------------------------- & $check 'SE-01' 'Security' 'Plan your security readiness' 'Defender for Servers on' 'High' 'microsoft.resources/subscriptions' { param($Row, $C) & $planOn $Row $C 'VirtualMachines' } 'Turn on Microsoft Defender for Servers for the subscription.' 'https://learn.microsoft.com/azure/defender-for-cloud/plan-defender-for-servers' { param($Row, $C) $C.Pricing.Contains([string](& $C.At $Row 'subscriptionId')) } & $check 'SE-02' 'Security' 'Plan your security readiness' 'Defender for Storage on' 'Medium' 'microsoft.resources/subscriptions' { param($Row, $C) & $planOn $Row $C 'StorageAccounts' } 'Turn on Microsoft Defender for Storage for the subscription.' 'https://learn.microsoft.com/azure/defender-for-cloud/defender-for-storage-introduction' { param($Row, $C) $C.Pricing.Contains([string](& $C.At $Row 'subscriptionId')) } & $check 'SE-03' 'Security' 'Plan your security readiness' 'Defender CSPM on' 'Medium' 'microsoft.resources/subscriptions' { param($Row, $C) & $planOn $Row $C 'CloudPosture' } 'Turn on Defender Cloud Security Posture Management for attack paths and governance.' 'https://learn.microsoft.com/azure/defender-for-cloud/concept-cloud-security-posture-management' { param($Row, $C) $C.Pricing.Contains([string](& $C.At $Row 'subscriptionId')) } & $check 'SE-04' 'Security' 'Sustain and evolve your security posture' 'Secure score 70% or more' 'Medium' 'microsoft.resources/subscriptions' { param($Row, $C) $C.SecureScore[[string](& $C.At $Row 'subscriptionId')] -ge 70 } 'Work through Defender for Cloud''s recommendations, highest impact first, to raise the secure score.' 'https://learn.microsoft.com/azure/defender-for-cloud/secure-score-security-controls' { param($Row, $C) $C.SecureScore.Contains([string](& $C.At $Row 'subscriptionId')) } & $check 'SE-05' 'Security' 'Design to protect confidentiality' 'Storage account accepts HTTPS only' 'High' 'microsoft.storage/storageaccounts' { param($Row, $C) [string](& $C.At $Row 'properties', 'supportsHttpsTrafficOnly') -ne 'False' } 'Require secure transfer (HTTPS only).' 'https://learn.microsoft.com/azure/storage/common/storage-require-secure-transfer' & $check 'SE-06' 'Security' 'Design to protect confidentiality' 'Storage account requires TLS 1.2+' 'High' 'microsoft.storage/storageaccounts' { param($Row, $C) [string](& $C.At $Row 'properties', 'minimumTlsVersion') -in 'TLS1_2', 'TLS1_3' } 'Set the minimum TLS version to 1.2.' 'https://learn.microsoft.com/azure/storage/common/transport-layer-security-configure-minimum-version' & $check 'SE-07' 'Security' 'Design to protect confidentiality' 'Storage account blocks anonymous blob access' 'High' 'microsoft.storage/storageaccounts' { param($Row, $C) [string](& $C.At $Row 'properties', 'allowBlobPublicAccess') -ne 'True' } 'Turn off anonymous (public) blob access.' 'https://learn.microsoft.com/azure/storage/blobs/anonymous-read-access-prevent' & $check 'SE-08' 'Security' 'Design to protect integrity' 'Storage account shared keys off' 'Medium' 'microsoft.storage/storageaccounts' { param($Row, $C) [string](& $C.At $Row 'properties', 'allowSharedKeyAccess') -eq 'False' } 'Turn off shared key access and use Microsoft Entra ID (RBAC) instead.' 'https://learn.microsoft.com/azure/storage/common/shared-key-authorization-prevent' & $check 'SE-09' 'Security' 'Design to protect confidentiality' 'Storage account network-restricted' 'Medium' 'microsoft.storage/storageaccounts' $restricted 'Disable public network access, or deny by default and allow only the networks that need it (or private endpoints).' 'https://learn.microsoft.com/azure/storage/common/storage-network-security' & $check 'SE-10' 'Security' 'Design to protect integrity' 'Key Vault purge protection on' 'Medium' 'microsoft.keyvault/vaults' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'enablePurgeProtection') } 'Turn on purge protection, so deleted keys and secrets can''t be purged before the retention period ends.' 'https://learn.microsoft.com/azure/key-vault/general/soft-delete-overview' & $check 'SE-11' 'Security' 'Design to protect integrity' 'Key Vault uses Azure RBAC' 'Low' 'microsoft.keyvault/vaults' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'enableRbacAuthorization') } 'Use the Azure RBAC permission model instead of access policies.' 'https://learn.microsoft.com/azure/key-vault/general/rbac-migration' & $check 'SE-12' 'Security' 'Design to protect confidentiality' 'Key Vault network-restricted' 'Medium' 'microsoft.keyvault/vaults' $restricted 'Disable public network access, or deny by default and use private endpoints.' 'https://learn.microsoft.com/azure/key-vault/general/network-security' & $check 'SE-13' 'Security' 'Design to protect confidentiality' 'SQL server public network access off' 'Medium' 'microsoft.sql/servers' { param($Row, $C) [string](& $C.At $Row 'properties', 'publicNetworkAccess') -eq 'Disabled' } 'Disable public network access and connect through private endpoints.' 'https://learn.microsoft.com/azure/azure-sql/database/connectivity-settings' & $check 'SE-14' 'Security' 'Design to protect confidentiality' 'SQL server requires TLS 1.2+' 'High' 'microsoft.sql/servers' { param($Row, $C) [string](& $C.At $Row 'properties', 'minimalTlsVersion') -in '1.2', '1.3' } 'Set the minimum TLS version to 1.2.' 'https://learn.microsoft.com/azure/azure-sql/database/connectivity-settings#minimal-tls-version' & $check 'SE-15' 'Security' 'Design to protect integrity' 'SQL server Microsoft Entra-only authentication' 'Medium' 'microsoft.sql/servers' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'administrators', 'azureADOnlyAuthentication') } 'Turn on Microsoft Entra-only authentication, so SQL logins with passwords can''t be used.' 'https://learn.microsoft.com/azure/azure-sql/database/authentication-azure-ad-only-authentication' & $check 'SE-16' 'Security' 'Design to protect confidentiality' 'App Service HTTPS only' 'High' 'microsoft.web/sites' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'httpsOnly') } 'Turn on HTTPS only, so HTTP requests are redirected to HTTPS.' 'https://learn.microsoft.com/azure/app-service/configure-ssl-bindings#enforce-https' & $check 'SE-17' 'Security' 'Design to protect integrity' 'App Service uses a managed identity' 'Low' 'microsoft.web/sites' { param($Row, $C) [string](& $C.At $Row 'identity', 'type') -notin '', 'None' } 'Give the app a managed identity and use it, not secrets, to reach other Azure services.' 'https://learn.microsoft.com/azure/app-service/overview-managed-identity' & $check 'SE-18' 'Security' 'Design to protect confidentiality' 'NSG blocks SSH and RDP from the internet' 'High' 'microsoft.network/networksecuritygroups' { param($Row, $C) -not (& $managementOpen $Row $C) } 'Remove inbound rules that allow 22 or 3389 from anywhere; use Azure Bastion or just-in-time VM access.' 'https://learn.microsoft.com/azure/bastion/bastion-overview' & $check 'SE-19' 'Security' 'Design to protect confidentiality' 'VM network interface without a public IP' 'Medium' 'microsoft.network/networkinterfaces' { param($Row, $C) -not @(@(& $C.At $Row 'properties', 'ipConfigurations') | Where-Object { & $C.At $_ 'properties', 'publicIPAddress', 'id' }).Count } 'Remove the public IP; reach the VM through Bastion, a VPN or a load balancer.' 'https://learn.microsoft.com/azure/well-architected/security/networking' { param($Row, $C) [bool](& $C.At $Row 'properties', 'virtualMachine', 'id') } & $check 'SE-20' 'Security' 'Design to protect confidentiality' 'VM encryption at host' 'Low' 'microsoft.compute/virtualmachines' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'securityProfile', 'encryptionAtHost') } 'Turn on encryption at host, so temp disks and caches are encrypted too.' 'https://learn.microsoft.com/azure/virtual-machines/disk-encryption-overview' & $check 'SE-21' 'Security' 'Design to protect integrity' 'Container registry admin user off' 'Medium' 'microsoft.containerregistry/registries' { param($Row, $C) -not (& $true1 (& $C.At $Row 'properties', 'adminUserEnabled')) } 'Turn off the admin user; pull and push with Microsoft Entra identities.' 'https://learn.microsoft.com/azure/container-registry/container-registry-authentication' & $check 'SE-22' 'Security' 'Design to protect integrity' 'AKS local accounts off' 'Medium' 'microsoft.containerservice/managedclusters' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'disableLocalAccounts') } 'Disable local accounts, so every sign-in goes through Microsoft Entra ID.' 'https://learn.microsoft.com/azure/aks/manage-local-accounts-managed-azure-ad' & $check 'SE-23' 'Security' 'Design to protect integrity' 'AKS integrated with Microsoft Entra ID' 'Medium' 'microsoft.containerservice/managedclusters' { param($Row, $C) [bool](& $C.At $Row 'properties', 'aadProfile') } 'Turn on Microsoft Entra integration with Azure RBAC for Kubernetes authorization.' 'https://learn.microsoft.com/azure/aks/enable-authentication-microsoft-entra-id' & $check 'SE-24' 'Security' 'Design to protect availability' 'Virtual network DDoS protection' 'Low' 'microsoft.network/virtualnetworks' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'enableDdosProtection') } 'Protect networks with public endpoints with Azure DDoS Network Protection.' 'https://learn.microsoft.com/azure/ddos-protection/ddos-protection-overview' & $check 'SE-25' 'Security' 'Design to protect availability' 'Application Gateway has a WAF' 'Medium' 'microsoft.network/applicationgateways' { param($Row, $C) [string](& $C.At $Row 'properties', 'sku', 'tier') -match 'WAF' -or (& $C.At $Row 'properties', 'firewallPolicy', 'id') } 'Use the WAF v2 tier with a WAF policy in prevention mode.' 'https://learn.microsoft.com/azure/web-application-firewall/ag/ag-overview' # --- Cost Optimization -------------------------------------------------------------------------------- & $check 'CO-01' 'Cost Optimization' 'Develop cost-management discipline' 'Resource tagged for cost allocation' 'Medium' 'microsoft.compute/virtualmachines' { param($Row, $C) & $hasTag $Row $C $C.CostTag } 'Tag resources with a cost centre, project or application (Azure Policy can require and inherit them) so costs can be allocated.' 'https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-tagging' & $check 'CO-02' 'Cost Optimization' 'Develop cost-management discipline' 'Storage account tagged for cost allocation' 'Low' 'microsoft.storage/storageaccounts' { param($Row, $C) & $hasTag $Row $C $C.CostTag } 'Tag resources with a cost centre, project or application so costs can be allocated.' 'https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-tagging' & $check 'CO-03' 'Cost Optimization' 'Design for usage optimization' 'Managed disk attached to something' 'Medium' 'microsoft.compute/disks' { param($Row, $C) [string](& $C.At $Row 'properties', 'diskState') -ne 'Unattached' } 'Delete the unattached disk (snapshot it first if it may be needed): it is billed in full while unused.' 'https://learn.microsoft.com/azure/virtual-machines/disks-find-unattached-portal' & $check 'CO-04' 'Cost Optimization' 'Design for usage optimization' 'Public IP associated with something' 'Low' 'microsoft.network/publicipaddresses' { param($Row, $C) (& $C.At $Row 'properties', 'ipConfiguration', 'id') -or (& $C.At $Row 'properties', 'natGateway', 'id') } 'Delete the unassociated public IP: Standard public IPs are billed whether used or not.' 'https://learn.microsoft.com/azure/virtual-network/ip-services/public-ip-addresses' & $check 'CO-05' 'Cost Optimization' 'Design for usage optimization' 'VM not stopped while still billed' 'High' 'microsoft.compute/virtualmachines' { param($Row, $C) [string](& $C.At $Row 'properties', 'extended', 'instanceView', 'powerState', 'code') -ne 'PowerState/stopped' } 'Deallocate the VM (Stop in the portal, or Stop-AzVM) - a VM stopped from inside the OS keeps billing for its compute.' 'https://learn.microsoft.com/azure/virtual-machines/states-billing' & $check 'CO-06' 'Cost Optimization' 'Design for usage optimization' 'App Service plan hosts apps' 'Medium' 'microsoft.web/serverfarms' { param($Row, $C) [int](& $C.At $Row 'properties', 'numberOfSites') -gt 0 } 'Delete the empty plan, or move apps onto it: a plan is billed with no apps on it.' 'https://learn.microsoft.com/azure/app-service/overview-manage-costs' $paidPlan & $check 'CO-07' 'Cost Optimization' 'Design for usage optimization' 'Snapshot younger than 90 days' 'Low' 'microsoft.compute/snapshots' { param($Row, $C) $created = & $C.At $Row 'properties', 'timeCreated'; -not $created -or ([datetime]$created) -gt $C.Now.AddDays(-90) } 'Delete snapshots you no longer need, or keep backups in Azure Backup with a retention policy.' 'https://learn.microsoft.com/azure/virtual-machines/snapshot-copy-managed-disk' & $check 'CO-08' 'Cost Optimization' 'Design for rate optimization' 'Windows VM uses Azure Hybrid Benefit' 'Medium' 'microsoft.compute/virtualmachines' { param($Row, $C) [string](& $C.At $Row 'properties', 'licenseType') -in 'Windows_Server', 'Windows_Client' } 'If you have Windows Server licences with Software Assurance, apply Azure Hybrid Benefit to pay only the base compute rate.' 'https://learn.microsoft.com/azure/virtual-machines/windows/hybrid-use-benefit-licensing' { param($Row, $C) [string](& $C.At $Row 'properties', 'storageProfile', 'osDisk', 'osType') -eq 'Windows' } & $check 'CO-09' 'Cost Optimization' 'Design for rate optimization' 'SQL database uses Azure Hybrid Benefit' 'Low' 'microsoft.sql/servers/databases' { param($Row, $C) [string](& $C.At $Row 'properties', 'licenseType') -eq 'BasePrice' } 'If you have SQL Server licences with Software Assurance, set the licence type to BasePrice (Azure Hybrid Benefit).' 'https://learn.microsoft.com/azure/azure-sql/azure-hybrid-benefit' { param($Row, $C) (& $C.At $Row 'name') -ne 'master' -and [string](& $C.At $Row 'properties', 'licenseType') -ne '' } & $check 'CO-10' 'Cost Optimization' 'Design with a cost-efficiency mindset' 'VM on a current-generation size' 'Low' 'microsoft.compute/virtualmachines' { param($Row, $C) [string](& $C.At $Row 'properties', 'hardwareProfile', 'vmSize') -notmatch $oldSeries } 'Move to a current-generation size (v5 or later): newer series give more performance for the price.' 'https://learn.microsoft.com/azure/virtual-machines/sizes/overview' & $check 'CO-11' 'Cost Optimization' 'Develop cost-management discipline' 'Resource group tagged for cost allocation' 'Low' 'microsoft.resources/subscriptions/resourcegroups' { param($Row, $C) & $hasTag $Row $C $C.CostTag } 'Tag the resource group with a cost centre or project, and let resources inherit it with Azure Policy.' 'https://learn.microsoft.com/azure/cost-management-billing/costs/enable-tag-inheritance' # --- Operational Excellence ----------------------------------------------------------------------------- & $check 'OE-01' 'Operational Excellence' 'Establish development standards' 'Resource group has owner and environment tags' 'Medium' 'microsoft.resources/subscriptions/resourcegroups' { param($Row, $C) (& $hasTag $Row $C $C.OwnerTag) -and (& $hasTag $Row $C @('Environment', 'Env')) } 'Tag every resource group with its owner and environment, and require the tags with Azure Policy.' 'https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-tagging' & $check 'OE-02' 'Operational Excellence' 'Evolve operations with observability' 'VM has the Azure Monitor agent' 'Medium' 'microsoft.compute/virtualmachines' { param($Row, $C) @($C.Extensions[[string](& $C.At $Row 'id')] | Where-Object { $_ -in 'AzureMonitorWindowsAgent', 'AzureMonitorLinuxAgent' }).Count -gt 0 } 'Install the Azure Monitor agent with a data collection rule, so the VM''s logs and performance counters reach Log Analytics.' 'https://learn.microsoft.com/azure/azure-monitor/agents/azure-monitor-agent-overview' & $check 'OE-03' 'Operational Excellence' 'Evolve operations with observability' 'VM boot diagnostics on' 'Low' 'microsoft.compute/virtualmachines' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'diagnosticsProfile', 'bootDiagnostics', 'enabled') } 'Turn on boot diagnostics (with managed storage), to troubleshoot a VM that won''t start.' 'https://learn.microsoft.com/azure/virtual-machines/boot-diagnostics' & $check 'OE-04' 'Operational Excellence' 'Evolve operations with observability' 'App Service connected to Application Insights' 'Medium' 'microsoft.web/sites' { param($Row, $C) $tags = & $C.At $Row 'tags'; $tags -is [System.Collections.IDictionary] -and @($tags.Keys | Where-Object { $_ -like 'hidden-link*app-insights-resource-id*' }).Count -gt 0 } 'Connect the app to a workspace-based Application Insights resource for requests, failures and dependencies.' 'https://learn.microsoft.com/azure/azure-monitor/app/codeless-app-service' & $check 'OE-05' 'Operational Excellence' 'Evolve operations with observability' 'Subscription has alert rules' 'High' 'microsoft.resources/subscriptions' { param($Row, $C) & $anyOfType $Row $C @('microsoft.insights/metricalerts', 'microsoft.insights/scheduledqueryrules', 'microsoft.insights/activitylogalerts') } 'Create alert rules for the signals that matter (Azure Monitor baseline alerts are a start), routed to an action group.' 'https://learn.microsoft.com/azure/azure-monitor/alerts/alerts-overview' & $check 'OE-06' 'Operational Excellence' 'Evolve operations with observability' 'Subscription has an action group' 'Medium' 'microsoft.resources/subscriptions' { param($Row, $C) & $anyOfType $Row $C @('microsoft.insights/actiongroups') } 'Create an action group (e-mail, Teams, ITSM) so alerts reach the people who act on them.' 'https://learn.microsoft.com/azure/azure-monitor/alerts/action-groups' & $check 'OE-07' 'Operational Excellence' 'Deploy with confidence' 'Web app has a deployment slot' 'Low' 'microsoft.web/sites' { param($Row, $C) [int]$C.Slots[[string](& $C.At $Row 'id')] -gt 0 } 'Deploy to a staging slot and swap, for zero-downtime releases and an instant rollback.' 'https://learn.microsoft.com/azure/app-service/deploy-staging-slots' { param($Row, $C) [string](& $C.At $Row 'kind') -notmatch 'functionapp|workflowapp' } & $check 'OE-08' 'Operational Excellence' 'Automate for efficiency' 'VM patched by the platform' 'Medium' 'microsoft.compute/virtualmachines' { param($Row, $C) $os = & $C.At $Row 'properties', 'osProfile'; $patch = if (& $C.At $os 'windowsConfiguration') { & $C.At $os 'windowsConfiguration', 'patchSettings' } else { & $C.At $os 'linuxConfiguration', 'patchSettings' } [string](& $C.At $patch 'patchMode') -eq 'AutomaticByPlatform' -or [string](& $C.At $patch 'assessmentMode') -eq 'AutomaticByPlatform' } 'Use Azure Update Manager: periodic assessment and platform-orchestrated patching on a maintenance schedule.' 'https://learn.microsoft.com/azure/update-manager/overview' & $check 'OE-09' 'Operational Excellence' 'Automate for efficiency' 'AKS automatic upgrades on' 'Medium' 'microsoft.containerservice/managedclusters' { param($Row, $C) [string](& $C.At $Row 'properties', 'autoUpgradeProfile', 'upgradeChannel') -notin '', 'none' } 'Set an auto-upgrade channel (patch or stable) and a planned maintenance window.' 'https://learn.microsoft.com/azure/aks/auto-upgrade-cluster' # --- Performance Efficiency ----------------------------------------------------------------------------- & $check 'PE-01' 'Performance Efficiency' 'Design to meet capacity requirements' 'App Service plan scales automatically' 'Medium' 'microsoft.web/serverfarms' { param($Row, $C) $C.Autoscale.Contains([string](& $C.At $Row 'id')) -or (& $true1 (& $C.At $Row 'properties', 'elasticScaleEnabled')) } 'Turn on autoscale (rules or automatic scaling), so the plan adds instances under load.' 'https://learn.microsoft.com/azure/app-service/manage-automatic-scaling' { param($Row, $C) [string](& $C.At $Row 'sku', 'tier') -notin 'Free', 'Shared', 'Basic', 'Dynamic', 'FlexConsumption', '' } & $check 'PE-02' 'Performance Efficiency' 'Design to meet capacity requirements' 'Scale set scales automatically' 'Medium' 'microsoft.compute/virtualmachinescalesets' { param($Row, $C) $C.Autoscale.Contains([string](& $C.At $Row 'id')) } 'Add an autoscale setting, so the scale set grows and shrinks with demand.' 'https://learn.microsoft.com/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-autoscale-overview' { param($Row, $C) [string](& $C.At $Row 'tags', 'aks-managed-poolName') -eq '' } & $check 'PE-03' 'Performance Efficiency' 'Design to meet capacity requirements' 'AKS cluster autoscaler on' 'Medium' 'microsoft.containerservice/managedclusters' { param($Row, $C) @(@(& $C.At $Row 'properties', 'agentPoolProfiles') | Where-Object { & $true1 (& $C.At $_ 'enableAutoScaling') }).Count -gt 0 } 'Turn on the cluster autoscaler on the node pools that run workloads.' 'https://learn.microsoft.com/azure/aks/cluster-autoscaler' & $check 'PE-04' 'Performance Efficiency' 'Design to meet capacity requirements' 'Application Gateway autoscales' 'Low' 'microsoft.network/applicationgateways' { param($Row, $C) [bool](& $C.At $Row 'properties', 'autoscaleConfiguration') } 'Use v2 with an autoscale configuration (minimum and maximum instances).' 'https://learn.microsoft.com/azure/application-gateway/application-gateway-autoscaling-zone-redundant' & $check 'PE-05' 'Performance Efficiency' 'Achieve and sustain performance' 'VM network interface has accelerated networking' 'Low' 'microsoft.network/networkinterfaces' { param($Row, $C) & $true1 (& $C.At $Row 'properties', 'enableAcceleratedNetworking') } 'Turn on accelerated networking (on sizes that support it) for lower latency and higher throughput.' 'https://learn.microsoft.com/azure/virtual-network/accelerated-networking-overview' { param($Row, $C) [bool](& $C.At $Row 'properties', 'virtualMachine', 'id') } & $check 'PE-06' 'Performance Efficiency' 'Achieve and sustain performance' 'VM OS disk is SSD' 'Medium' 'microsoft.compute/virtualmachines' { param($Row, $C) [string](& $C.At $Row 'properties', 'storageProfile', 'osDisk', 'managedDisk', 'storageAccountType') -notmatch '^Standard_LRS$' } 'Use Premium SSD (or at least Standard SSD) for OS disks: Standard HDD has the lowest IOPS and no single-instance SLA.' 'https://learn.microsoft.com/azure/virtual-machines/disks-types' & $check 'PE-07' 'Performance Efficiency' 'Achieve and sustain performance' 'SQL database above the Basic tier' 'Low' 'microsoft.sql/servers/databases' { param($Row, $C) [string](& $C.At $Row 'sku', 'tier') -ne 'Basic' } 'Basic (5 DTUs) suits only tiny workloads: size production databases on vCore or Standard/Premium.' 'https://learn.microsoft.com/azure/azure-sql/database/service-tiers-sql-database-vcore' { param($Row, $C) (& $C.At $Row 'name') -ne 'master' } & $check 'PE-08' 'Performance Efficiency' 'Achieve and sustain performance' 'Azure Cache for Redis above Basic' 'Medium' 'microsoft.cache/redis' { param($Row, $C) [string](& $C.At $Row 'properties', 'sku', 'name') -ne 'Basic' } 'Basic is a single node with no SLA: use Standard or Premium for production.' 'https://learn.microsoft.com/azure/azure-cache-for-redis/cache-overview#service-tiers' ) # The tests run later, elsewhere: bind them to the helpers above. foreach ($c in $checks) { $c.Test = $c.Test.GetNewClosure() if ($c.Applies) { $c.Applies = $c.Applies.GetNewClosure() } } # Advisor's and Defender's wording for what a catalogue check already # tests. Best effort, by text: extend it when a duplicate slips through. $overlaps = @( @{ Pattern = 'availability zone|availability set'; Checks = @('RE-01', 'RE-12', 'RE-14') } @{ Pattern = 'enable backup|backup should be enabled|azure backup'; Checks = @('RE-02') } @{ Pattern = 'managed disk'; Checks = @('RE-03') } @{ Pattern = 'zone.?redundant storage|geo.?redundant storage|storage account.*redundan|\b(zrs|grs|gzrs)\b'; Checks = @('RE-04') } @{ Pattern = 'sql.*zone.?redundan|zone.?redundan.*sql'; Checks = @('RE-05') } @{ Pattern = 'app service.*zone.?redundan|zone.?redundan.*app service'; Checks = @('RE-07') } @{ Pattern = 'scale out.*app service|app service.*(two|2|multiple) instances|more than one instance'; Checks = @('RE-08') } @{ Pattern = 'basic.*load balancer|load balancer.*basic|standard (sku )?load balancer'; Checks = @('RE-09') } @{ Pattern = 'health probe'; Checks = @('RE-10') } @{ Pattern = 'basic.*public ip|public ip.*basic|standard sku public ip'; Checks = @('RE-11') } @{ Pattern = 'uptime sla|standard tier.*(aks|kubernetes)|(aks|kubernetes).*standard tier'; Checks = @('RE-13') } @{ Pattern = 'cosmos.*(multi|additional|second).?region|cosmos.*zone'; Checks = @('RE-15') } @{ Pattern = 'continuous backup'; Checks = @('RE-16') } @{ Pattern = 'cross.?region restore|geo.?redundan.*vault|vault.*geo.?redundan'; Checks = @('RE-17') } @{ Pattern = 'key vault.*soft delete|soft delete.*key vault'; Checks = @('RE-18') } @{ Pattern = 'secure transfer|storage.*https'; Checks = @('SE-05') } @{ Pattern = 'storage.*tls|tls.*storage'; Checks = @('SE-06') } @{ Pattern = 'blob public access|anonymous.*(blob|read access)|public access should be disallowed'; Checks = @('SE-07') } @{ Pattern = 'shared key'; Checks = @('SE-08') } @{ Pattern = 'storage.*(private link|network access|virtual network rules|public network)'; Checks = @('SE-09') } @{ Pattern = 'purge protection|deletion protection'; Checks = @('SE-10') } @{ Pattern = 'key vault.*rbac|rbac permission model'; Checks = @('SE-11') } @{ Pattern = 'key vault.*(private link|public network|firewall)'; Checks = @('SE-12') } @{ Pattern = 'sql.*public network access'; Checks = @('SE-13') } @{ Pattern = 'sql.*tls'; Checks = @('SE-14') } @{ Pattern = '(entra|azure ad|azure active directory)[- ]only authentication'; Checks = @('SE-15') } @{ Pattern = '(app service|web app|function app|web application).*https'; Checks = @('SE-16') } @{ Pattern = 'managed identity.*(web app|app service|function)|(web app|app service|function app).*managed identity'; Checks = @('SE-17') } @{ Pattern = 'management ports|just-in-time|internet-facing virtual machines'; Checks = @('SE-18') } @{ Pattern = 'public ip.*virtual machine|virtual machine.*public ip'; Checks = @('SE-19') } @{ Pattern = 'encrypt temp disks|encryption at host'; Checks = @('SE-20') } @{ Pattern = 'container registr.*admin|admin (user|account).*container registr'; Checks = @('SE-21') } @{ Pattern = 'kubernetes.*local auth|local (accounts|authentication).*kubernetes|aks.*local account'; Checks = @('SE-22') } @{ Pattern = 'kubernetes.*(entra|azure ad|aad)|(entra|azure ad|aad).*kubernetes'; Checks = @('SE-23') } @{ Pattern = 'ddos'; Checks = @('SE-24') } @{ Pattern = 'web application firewall|\bwaf\b'; Checks = @('SE-25') } @{ Pattern = 'unassociated public ip|public ip addresses? (not|that are not) associated|delete .*public ip'; Checks = @('CO-04') } @{ Pattern = 'hybrid benefit'; Checks = @('CO-08', 'CO-09') } @{ Pattern = 'azure monitor agent'; Checks = @('OE-02') } @{ Pattern = 'boot diagnostics'; Checks = @('OE-03') } @{ Pattern = 'application insights'; Checks = @('OE-04') } @{ Pattern = 'deployment slot'; Checks = @('OE-07') } @{ Pattern = 'update manager|patch orchestration|periodically check for missing system updates'; Checks = @('OE-08') } @{ Pattern = 'auto.?upgrade'; Checks = @('OE-09') } @{ Pattern = 'cluster autoscaler'; Checks = @('PE-03') } @{ Pattern = 'autoscal'; Checks = @('PE-01', 'PE-02', 'PE-04') } @{ Pattern = 'accelerated networking'; Checks = @('PE-05') } @{ Pattern = 'premium (ssd|storage|disk)'; Checks = @('PE-06') } @{ Pattern = 'redis.*(standard|premium)|basic.*redis'; Checks = @('PE-08') } ) @{ Principles = $principles; Checks = $checks; Overlaps = $overlaps } } |