diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 200773ac102..e571edbc3a2 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -37,6 +37,8 @@ What's changed since v1.32.0: - Bug fixes: - Fixed quotes get incorrectly duplicated by @BernieWhite. [#2593](https://github.com/Azure/PSRule.Rules.Azure/issues/2593) + - Fixed failure to expand copy loop in a Azure Policy deployment by @BernieWhite. + [#2605](https://github.com/Azure/PSRule.Rules.Azure/issues/2605) ## v1.32.0 diff --git a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs index 4c03b1974cc..f131cdb435a 100644 --- a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs @@ -1764,10 +1764,9 @@ private static JToken ResolveToken(ITemplateContext context, JToken token) /// private static TemplateContext.CopyIndexState[] GetPropertyIterator(ITemplateContext context, JObject value) { - if (value.ContainsKey(PROPERTY_COPY)) + if (value.TryArrayProperty(PROPERTY_COPY, out var copyObjectArray)) { var result = new List(); - var copyObjectArray = value[PROPERTY_COPY].Value(); for (var i = 0; i < copyObjectArray.Count; i++) { var copyObject = copyObjectArray[i] as JObject; @@ -1792,13 +1791,12 @@ private static TemplateContext.CopyIndexState[] GetPropertyIterator(ITemplateCon /// private static TemplateContext.CopyIndexState[] GetOutputIterator(ITemplateContext context, JObject value) { - if (value.ContainsKey(PROPERTY_COPY)) + if (value.TryObjectProperty(PROPERTY_COPY, out var copyObject)) { var result = new List(); - var copyObject = value[PROPERTY_COPY].Value(); var state = new TemplateContext.CopyIndexState { - Name = "", + Name = string.Empty, Input = copyObject[PROPERTY_INPUT], Count = ExpandPropertyInt(context, copyObject, PROPERTY_COUNT) }; @@ -1813,9 +1811,8 @@ private static TemplateContext.CopyIndexState[] GetOutputIterator(ITemplateConte private static IEnumerable GetVariableIterator(ITemplateContext context, JObject value, bool pushToStack = true) { - if (value.ContainsKey(PROPERTY_COPY)) + if (value.TryArrayProperty(PROPERTY_COPY, out var copyObjectArray)) { - var copyObjectArray = value[PROPERTY_COPY].Value(); for (var i = 0; i < copyObjectArray.Count; i++) { var copyObject = copyObjectArray[i] as JObject; @@ -1848,9 +1845,8 @@ private static TemplateContext.CopyIndexState GetResourceIterator(TemplateContex { Input = value }; - if (value.ContainsKey(PROPERTY_COPY)) + if (value.TryObjectProperty(PROPERTY_COPY, out var copyObject)) { - var copyObject = value[PROPERTY_COPY].Value(); result.Name = ExpandProperty(context, copyObject, PROPERTY_NAME); result.Count = ExpandPropertyInt(context, copyObject, PROPERTY_COUNT); context.CopyIndex.PushResourceType(result); diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj index 202daa85316..e0848d79e3e 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -233,6 +233,9 @@ PreserveNewest + + PreserveNewest + diff --git a/tests/PSRule.Rules.Azure.Tests/Template.Policy.WithDeployment.json b/tests/PSRule.Rules.Azure.Tests/Template.Policy.WithDeployment.json new file mode 100644 index 00000000000..187094de7e8 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Template.Policy.WithDeployment.json @@ -0,0 +1,228 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/managementGroupDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "policyName": { + "metadata": { + "description": "Policy name" + }, + "type": "string", + "defaultValue": "Policy-example" + } + }, + "resources": [ + { + "name": "[parameters('policyName')]", + "type": "Microsoft.Authorization/policyDefinitions", + "apiVersion": "2021-06-01", + "properties": { + "policyType": "Custom", + "mode": "Indexed", + "displayName": "Deploy a route table with specific user defined routes", + "description": "Deploys a route table with specific user defined routes when one does not exist. The route table deployed by the policy must be manually associated to subnet(s)", + "metadata": { + "version": "1.0.0", + "category": "Network", + "source": "https://github.com/Azure/Enterprise-Scale/", + "alzCloudEnvironments": [ + "AzureCloud", + "AzureChinaCloud", + "AzureUSGovernment" + ] + }, + "parameters": { + "effect": { + "type": "String", + "metadata": { + "displayName": "Effect", + "description": "Enable or disable the execution of the policy" + }, + "allowedValues": [ + "DeployIfNotExists", + "Disabled" + ], + "defaultValue": "DeployIfNotExists" + }, + "requiredRoutes": { + "type": "Array", + "metadata": { + "displayName": "requiredRoutes", + "description": "Routes that must exist in compliant route tables deployed by this policy" + } + }, + "vnetRegion": { + "type": "String", + "metadata": { + "displayName": "vnetRegion", + "description": "Only VNets in this region will be evaluated against this policy" + } + }, + "routeTableName": { + "type": "String", + "metadata": { + "displayName": "routeTableName", + "description": "Name of the route table automatically deployed by this policy" + } + }, + "disableBgpPropagation": { + "type": "Boolean", + "metadata": { + "displayName": "DisableBgpPropagation", + "description": "Disable BGP Propagation" + }, + "defaultValue": false + } + }, + "policyRule": { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/virtualNetworks" + }, + { + "field": "location", + "equals": "[[parameters('vnetRegion')]" + } + ] + }, + "then": { + "effect": "[[parameters('effect')]", + "details": { + "type": "Microsoft.Network/routeTables", + "existenceCondition": { + "allOf": [ + { + "field": "name", + "equals": "[[parameters('routeTableName')]" + }, + { + "count": { + "field": "Microsoft.Network/routeTables/routes[*]", + "where": { + "value": "[[concat(current('Microsoft.Network/routeTables/routes[*].addressPrefix'), ';', current('Microsoft.Network/routeTables/routes[*].nextHopType'), if(equals(toLower(current('Microsoft.Network/routeTables/routes[*].nextHopType')),'virtualappliance'), concat(';', current('Microsoft.Network/routeTables/routes[*].nextHopIpAddress')), ''))]", + "in": "[[parameters('requiredRoutes')]" + } + }, + "equals": "[[length(parameters('requiredRoutes'))]" + } + ] + }, + "roleDefinitionIds": [ + "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-0000-0000-000000000000" + ], + "deployment": { + "properties": { + "mode": "incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "routeTableName": { + "type": "string" + }, + "vnetRegion": { + "type": "string" + }, + "requiredRoutes": { + "type": "array" + }, + "disableBgpPropagation": { + "type": "bool" + } + }, + "variables": { + "copyLoop": [ + { + "name": "routes", + "count": "[[[length(parameters('requiredRoutes'))]", + "input": { + "name": "[[[concat('route-',copyIndex('routes'))]", + "properties": { + "addressPrefix": "[[[split(parameters('requiredRoutes')[copyIndex('routes')], ';')[0]]", + "nextHopType": "[[[split(parameters('requiredRoutes')[copyIndex('routes')], ';')[1]]", + "nextHopIpAddress": "[[[if(equals(toLower(split(parameters('requiredRoutes')[copyIndex('routes')], ';')[1]),'virtualappliance'),split(parameters('requiredRoutes')[copyIndex('routes')], ';')[2], null())]" + } + } + } + ] + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2021-04-01", + "name": "routeTableDepl", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "routeTableName": { + "type": "string" + }, + "vnetRegion": { + "type": "string" + }, + "requiredRoutes": { + "type": "array" + }, + "disableBgpPropagation": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.Network/routeTables", + "apiVersion": "2021-02-01", + "name": "[[[parameters('routeTableName')]", + "location": "[[[parameters('vnetRegion')]", + "properties": { + "disableBgpRoutePropagation": "[[[parameters('disableBgpPropagation')]", + "copy": "[[variables('copyLoop')]" + } + } + ] + }, + "parameters": { + "routeTableName": { + "value": "[[parameters('routeTableName')]" + }, + "vnetRegion": { + "value": "[[parameters('vnetRegion')]" + }, + "requiredRoutes": { + "value": "[[parameters('requiredRoutes')]" + }, + "disableBgpPropagation": { + "value": "[[parameters('disableBgpPropagation')]" + } + } + } + } + ] + }, + "parameters": { + "routeTableName": { + "value": "[[parameters('routeTableName')]" + }, + "vnetRegion": { + "value": "[[parameters('vnetRegion')]" + }, + "requiredRoutes": { + "value": "[[parameters('requiredRoutes')]" + }, + "disableBgpPropagation": { + "value": "[[parameters('disableBgpPropagation')]" + } + } + } + } + } + } + } + } + } + ], + "outputs": {} +} diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs index e24d84e9976..34ff95d32b0 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs @@ -1057,6 +1057,13 @@ public void Quoting() Assert.Equal(10, result["value"][0]["parameters"]["debugLength"].Value()); } + [Fact] + public void PolicyCopyLoop() + { + var resources = ProcessTemplate(GetSourcePath("Template.Policy.WithDeployment.json"), null, out var templateContext); + Assert.Equal(2, resources.Length); + } + #region Helper methods private static string GetSourcePath(string fileName)