From 01a38b8c8e5bc303e9d84a04e99afe4799139d9f Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Tue, 10 May 2022 23:33:58 +1000 Subject: [PATCH 01/16] Adding intial changes --- .editorconfig | 5 +- .../Data/Policy/PolicyAssignmentVisitor.cs | 137 ++++++++++++++++-- .../test4.assignment.json | 78 ++++++++++ .../test5.assignment.json | 74 ++++++++++ .../test6.assignment.json | 78 ++++++++++ 5 files changed, 358 insertions(+), 14 deletions(-) create mode 100644 tests/PSRule.Rules.Azure.Tests/test4.assignment.json create mode 100644 tests/PSRule.Rules.Azure.Tests/test5.assignment.json create mode 100644 tests/PSRule.Rules.Azure.Tests/test6.assignment.json diff --git a/.editorconfig b/.editorconfig index 2ca08167a3b..50ede30e533 100644 --- a/.editorconfig +++ b/.editorconfig @@ -44,4 +44,7 @@ csharp_prefer_simple_default_expression = true:suggestion # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion \ No newline at end of file +csharp_style_var_elsewhere = true:suggestion + +# Disable rules +dotnet_diagnostic.CA1508.severity = none \ No newline at end of file diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index c4bb6bbf6b7..1173011c3c4 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.Configuration; using PSRule.Rules.Azure.Data.Template; @@ -29,10 +30,18 @@ internal abstract class PolicyAssignmentVisitor private const string PROPERTY_DEFAULTVALUE = "defaultValue"; private const string PROPERTY_ALL_OF = "allOf"; private const string FIELD_EQUALS = "equals"; + private const string FIELD_NOTEQUALS = "notEquals"; + private const string FIELD_GREATER = "greater"; + private const string FIELD_GREATEROREQUALS = "greaterOrEquals"; + private const string FIELD_LESS = "less"; + private const string FIELD_LESSOREQUALS = "lessOrEquals"; private const string PROPERTY_DISPLAYNAME = "displayName"; private const string PROPERTY_DESCRIPTION = "description"; private const string PROPERTY_DEPLOYMENT = "deployment"; private const string PROPERTY_VALUE = "value"; + private const string PROPERTY_COUNT = "count"; + private const string PROPERTY_WHERE = "where"; + private const string COLLECTION_ALIAS = "[*]"; private const char SLASH = '/'; public sealed class PolicyAssignmentContext : ITemplateContext @@ -217,34 +226,136 @@ private static void RemovePolicyRuleDeployment(JObject policyRule) } } + private static string ExpressionToObjectPathComparisonOperator(string expression) => expression switch + { + FIELD_EQUALS => "==", + FIELD_NOTEQUALS => "!=", + FIELD_GREATER => ">", + FIELD_GREATEROREQUALS => ">=", + FIELD_LESS => "<", + FIELD_LESSOREQUALS => "<=", + _ => null + }; + private void ExpandPolicyRule(JToken policyRule) { if (policyRule.Type == JTokenType.Object) { var hasFieldType = false; - foreach (var child in policyRule.Children()) + var hasFieldCount = false; + + foreach (var child in policyRule.Children().ToList()) { // Expand field aliases if (child.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)) { - var field = child.Value.Value(); - if (field.Equals(PROPERTY_TYPE, StringComparison.OrdinalIgnoreCase)) - hasFieldType = true; + if (child.Value.Type == JTokenType.String) + { + var field = child.Value.Value(); + if (field.Equals(PROPERTY_TYPE, StringComparison.OrdinalIgnoreCase)) + hasFieldType = true; - var aliasPath = ResolvePolicyAliasPath(field); - if (aliasPath != null) - policyRule[child.Name] = aliasPath; + var aliasPath = ResolvePolicyAliasPath(field); + if (aliasPath != null) + policyRule[child.Name] = aliasPath; + } } + // Set policy rule type else if (hasFieldType && child.Name.Equals(FIELD_EQUALS, StringComparison.OrdinalIgnoreCase)) { - var field = child.Value.Value(); - if (field.CountCharacterOccurrences(SLASH) == 1) + if (child.Value.Type == JTokenType.String) + { + var field = child.Value.Value(); + if (field.CountCharacterOccurrences(SLASH) == 1) + { + var contents = field.Split(SLASH); + var providerNamespace = contents[0]; + var resourceType = contents[1]; + _PolicyAliasProviderHelper.SetPolicyRuleType(providerNamespace, resourceType); + } + } + } + + // Replace equals with count if field count expression is currently being visited + else if (hasFieldCount && child.Name.Equals(FIELD_EQUALS, StringComparison.OrdinalIgnoreCase)) + { + policyRule[FIELD_EQUALS].Parent.Remove(); + policyRule[PROPERTY_COUNT] = child.Value; + } + + // Expand field count expressions + else if (child.Name.Equals(PROPERTY_COUNT, StringComparison.OrdinalIgnoreCase)) + { + hasFieldCount = true; + if (child.Value.Type == JTokenType.Object) { - var contents = field.Split(SLASH); - var providerNamespace = contents[0]; - var resourceType = contents[1]; - _PolicyAliasProviderHelper.SetPolicyRuleType(providerNamespace, resourceType); + var countObject = child.Value.ToObject(); + if (countObject.TryStringProperty(PROPERTY_FIELD, out var outerFieldAlias)) + { + var outerFieldAliasPath = ResolvePolicyAliasPath(outerFieldAlias); + + if (outerFieldAliasPath != null) + { + if (countObject.TryObjectProperty(PROPERTY_WHERE, out var whereExpression)) + { + // Single field in where expression + if (whereExpression.TryStringProperty(PROPERTY_FIELD, out var innerFieldAlias)) + { + var innerFieldAliasPath = ResolvePolicyAliasPath(innerFieldAlias); + + if (innerFieldAliasPath != null && innerFieldAliasPath.Contains(outerFieldAliasPath)) + { + // Get first property that is not a field expression + var comparisonExpression = whereExpression + .Children() + .FirstOrDefault(prop => !prop.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)); + + if (comparisonExpression != null) + { + var objectPathComparisonOperator = ExpressionToObjectPathComparisonOperator(comparisonExpression.Name); + + var splitAliasPath = innerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + + // Surround right hand side with quotes if string + var normalizedFormattedExpression = comparisonExpression.Value.Type == JTokenType.String + ? "?@{0} {1} '{2}'" + : "?@{0} {1} {2}"; + + var filter = string.Format( + Thread.CurrentThread.CurrentCulture, + normalizedFormattedExpression, + splitAliasPath[1], + objectPathComparisonOperator, + comparisonExpression.Value); + + policyRule[PROPERTY_FIELD] = string.Format( + Thread.CurrentThread.CurrentCulture, + "{0}[{1}]", + splitAliasPath[0], + filter); + + policyRule[PROPERTY_COUNT].Parent.Remove(); + } + } + } + + // nested allOf in where expression + else if (whereExpression.TryObjectProperty(PROPERTY_ALL_OF, out var allofExpression)) + { + + } + + } + + // Single field in count expression + else + { + policyRule[PROPERTY_FIELD] = outerFieldAliasPath; + policyRule[PROPERTY_COUNT].Parent.Remove(); + } + } + } } } diff --git a/tests/PSRule.Rules.Azure.Tests/test4.assignment.json b/tests/PSRule.Rules.Azure.Tests/test4.assignment.json new file mode 100644 index 00000000000..65830e1f0a1 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test4.assignment.json @@ -0,0 +1,78 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "000000000000000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "ResourceName": "000000000000000000000000", + "ResourceGroupName": null, + "ResourceType": "Microsoft.Authorization/policyAssignments", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Sku": null, + "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "DisableLBRuleSNAT", + "Description": null, + "Metadata": { + "assignedBy": "Armaan Dhaliwal-McLeod", + "parameterScopes": {}, + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:46:07.0986544Z", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 0, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "Parameters": {}, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "00000000-0000-0000-0000-000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "ResourceName": "00000000-0000-0000-0000-000000000000", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Properties": { + "Description": "Enforce disabling of SNAT on load balancing rules", + "DisplayName": "DisableLBRuleSNAT", + "Metadata": { + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:35:25.9960771Z", + "updatedBy": null, + "updatedOn": null + }, + "Mode": "All", + "Parameters": {}, + "PolicyRule": { + "if": { + "allOf": [ + { + "equals": "Microsoft.Network/loadBalancers", + "field": "type" + }, + { + "count": { + "field": "Microsoft.Network/loadBalancers/loadBalancingRules[*]", + "where": { + "field": "Microsoft.Network/loadBalancers/loadBalancingRules[*].disableOutboundSnat", + "equals": false + } + }, + "greaterOrEquals": 1 + } + ] + }, + "then": { + "effect": "deny" + } + }, + "PolicyType": 1 + }, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000" + } + ] + } +] diff --git a/tests/PSRule.Rules.Azure.Tests/test5.assignment.json b/tests/PSRule.Rules.Azure.Tests/test5.assignment.json new file mode 100644 index 00000000000..4111c7ca1f8 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test5.assignment.json @@ -0,0 +1,74 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "000000000000000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "ResourceName": "000000000000000000000000", + "ResourceGroupName": null, + "ResourceType": "Microsoft.Authorization/policyAssignments", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Sku": null, + "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "EnsureAtleastOneLBRule", + "Description": null, + "Metadata": { + "assignedBy": "Armaan Dhaliwal-McLeod", + "parameterScopes": {}, + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T13:24:28.6044957Z", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 0, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "Parameters": {}, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "00000000-0000-0000-0000-000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "ResourceName": "00000000-0000-0000-0000-000000000000", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Properties": { + "Description": "Enforce atleast more than one LB rule", + "DisplayName": "EnsureAtleastOneLBRule", + "Metadata": { + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T13:24:18.8003947Z", + "updatedBy": null, + "updatedOn": null + }, + "Mode": "All", + "Parameters": {}, + "PolicyRule": { + "if": { + "allOf": [ + { + "equals": "Microsoft.Network/loadBalancers", + "field": "type" + }, + { + "count": { + "field": "Microsoft.Network/loadBalancers/loadBalancingRules[*]" + }, + "greaterOrEquals": 1 + } + ] + }, + "then": { + "effect": "deny" + } + }, + "PolicyType": 1 + }, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000" + } + ] + } +] diff --git a/tests/PSRule.Rules.Azure.Tests/test6.assignment.json b/tests/PSRule.Rules.Azure.Tests/test6.assignment.json new file mode 100644 index 00000000000..870c0552369 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test6.assignment.json @@ -0,0 +1,78 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "000000000000000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "ResourceName": "000000000000000000000000", + "ResourceGroupName": null, + "ResourceType": "Microsoft.Authorization/policyAssignments", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Sku": null, + "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "UniqueDescriptionNSG", + "Description": null, + "Metadata": { + "assignedBy": "Armaan Dhaliwal-McLeod", + "parameterScopes": {}, + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:46:07.0986544Z", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 0, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "Parameters": {}, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "00000000-0000-0000-0000-000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "ResourceName": "00000000-0000-0000-0000-000000000000", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Properties": { + "Description": "Enforce unique description on one NSG rule", + "DisplayName": "UniqueDescriptionNSG", + "Metadata": { + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:35:25.9960771Z", + "updatedBy": null, + "updatedOn": null + }, + "Mode": "All", + "Parameters": {}, + "PolicyRule": { + "if": { + "allOf": [ + { + "equals": "Microsoft.Network/networkSecurityGroups", + "field": "type" + }, + { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].description", + "equals": "My unique description" + } + }, + "equals": 1 + } + ] + }, + "then": { + "effect": "deny" + } + }, + "PolicyType": 1 + }, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000" + } + ] + } +] \ No newline at end of file From 2f91ae96351342f2ab24a65a7092874c26fe35b6 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 15 May 2022 21:52:29 +1000 Subject: [PATCH 02/16] Added more changes --- .vscode/settings.json | 3 + .../Common/StringExtensions.cs | 4 +- .../Data/Policy/PolicyAliasProviderHelper.cs | 9 +- .../Data/Policy/PolicyAssignmentVisitor.cs | 183 +++++++++++++----- .../Cmdlet.Common.Tests.ps1 | 25 +++ .../emittedJsonRulesData.jsonc | 136 +++++++++++++ .../test4.assignment.json | 2 +- .../test7.assignment.json | 90 +++++++++ .../test8.assignment.json | 124 ++++++++++++ 9 files changed, 522 insertions(+), 54 deletions(-) create mode 100644 tests/PSRule.Rules.Azure.Tests/test7.assignment.json create mode 100644 tests/PSRule.Rules.Azure.Tests/test8.assignment.json diff --git a/.vscode/settings.json b/.vscode/settings.json index bd7a1895e99..309ec4a4923 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,9 @@ "[yaml]": { "editor.tabSize": 2 }, + "[json]": { + "editor.formatOnSave": true + }, "[markdown]": { "editor.tabSize": 2 }, diff --git a/src/PSRule.Rules.Azure/Common/StringExtensions.cs b/src/PSRule.Rules.Azure/Common/StringExtensions.cs index 4de880ebe93..3231aa9f66b 100644 --- a/src/PSRule.Rules.Azure/Common/StringExtensions.cs +++ b/src/PSRule.Rules.Azure/Common/StringExtensions.cs @@ -16,7 +16,9 @@ internal static string ToCamelCase(this string str) internal static int CountCharacterOccurrences(this string str, char chr) { - return str.Count(c => c == chr); + return !string.IsNullOrEmpty(str) + ? str.Count(c => c == chr) + : 0; } internal static bool IsExpressionString(this string str) diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAliasProviderHelper.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAliasProviderHelper.cs index 518aa75bf27..fbca5e284e1 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAliasProviderHelper.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAliasProviderHelper.cs @@ -34,11 +34,10 @@ internal bool ResolvePolicyAliasPath(string aliasName, out string aliasPath) // Handle aliases like Microsoft.Compute/imageId with only one slash if (slashOccurrences == 1) { - if (_DefaultRuleType != null && _Providers.TryResourceType(_DefaultRuleType, out var type2)) - return type2.Aliases != null && - type2.Aliases.TryGetValue(aliasName, out aliasPath); - - return false; + return _DefaultRuleType != null + && _Providers.TryResourceType(_DefaultRuleType, out var type2) + && type2.Aliases != null + && type2.Aliases.TryGetValue(aliasName, out aliasPath); } // Any aliases with two slashes or more will be resolved here diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index 1173011c3c4..c9938a9814d 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.Configuration; @@ -29,6 +30,7 @@ internal abstract class PolicyAssignmentVisitor private const string PROPERTY_TYPE = "type"; private const string PROPERTY_DEFAULTVALUE = "defaultValue"; private const string PROPERTY_ALL_OF = "allOf"; + private const string PROPERTY_ANY_OF = "anyOf"; private const string FIELD_EQUALS = "equals"; private const string FIELD_NOTEQUALS = "notEquals"; private const string FIELD_GREATER = "greater"; @@ -42,7 +44,11 @@ internal abstract class PolicyAssignmentVisitor private const string PROPERTY_COUNT = "count"; private const string PROPERTY_WHERE = "where"; private const string COLLECTION_ALIAS = "[*]"; + private const string AND_CLAUSE = "&&"; + private const string OR_CLAUSE = "||"; private const char SLASH = '/'; + private const char GROUP_OPEN = '('; + private const char GROUP_CLOSE = ')'; public sealed class PolicyAssignmentContext : ITemplateContext { @@ -237,6 +243,112 @@ private static void RemovePolicyRuleDeployment(JObject policyRule) _ => null }; + private void SetPolicyRuleType(string type) + { + if (type.CountCharacterOccurrences(SLASH) > 0) + { + var contents = type.Split(new char[] { SLASH }, count: 2); + var providerNamespace = contents[0]; + var resourceType = contents[1]; + _PolicyAliasProviderHelper.SetPolicyRuleType(providerNamespace, resourceType); + } + } + + private string GetFieldObjectPathArrayFilter(JObject obj) + { + if (obj.TryStringProperty(PROPERTY_FIELD, out var fieldProperty)) + { + var subProperty = string.Empty; + + if (fieldProperty.Equals(PROPERTY_TYPE, StringComparison.OrdinalIgnoreCase) + && obj.TryStringProperty(FIELD_EQUALS, out var fieldType)) + { + subProperty = $".{PROPERTY_TYPE}"; + SetPolicyRuleType(fieldType); + } + + var fieldAliasPath = ResolvePolicyAliasPath(fieldProperty); + if (fieldAliasPath != null) + { + var splitAliasPath = fieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + subProperty = splitAliasPath[1]; + } + + var comparisonExpression = obj + .Children() + .FirstOrDefault(prop => !prop.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)); + + if (comparisonExpression != null) + { + var objectPathComparisonOperator = ExpressionToObjectPathComparisonOperator(comparisonExpression.Name); + + if (objectPathComparisonOperator != null) + { + return FormatObjectPathArrayFilter( + subProperty, + objectPathComparisonOperator, + comparisonExpression.Value); + } + } + } + return null; + } + + private void ExpressionToObjectPathArrayFilter(JArray expression, string clause, StringBuilder objectPath) + { + var clauseSeparator = string.Empty; + foreach (var obj in expression.Children()) + { + var filter = GetFieldObjectPathArrayFilter(obj); + if (filter != null) + { + objectPath.Append(clauseSeparator); + objectPath.Append(filter); + clauseSeparator = $" {clause} "; + } + + else if (obj.TryArrayProperty(PROPERTY_ALL_OF, out var allOfExpression)) + { + objectPath.Append($" {clause} "); + objectPath.Append(GROUP_OPEN); + ExpressionToObjectPathArrayFilter(allOfExpression, AND_CLAUSE, objectPath); + objectPath.Append(GROUP_CLOSE); + } + + else if (obj.TryArrayProperty(PROPERTY_ANY_OF, out var anyOfExpression)) + { + objectPath.Append($" {clause} "); + objectPath.Append(GROUP_OPEN); + ExpressionToObjectPathArrayFilter(anyOfExpression, OR_CLAUSE, objectPath); + objectPath.Append(GROUP_CLOSE); + } + } + } + + private static string FormatObjectPathArrayExpression(string array, string filter) + { + return string.Format( + Thread.CurrentThread.CurrentCulture, + "{0}[?{1}]", + array, + filter); + } + + private static string FormatObjectPathArrayFilter(string subProperty, string comparisonOperator, JToken value) + { + // Surround right hand side with quotes if string + var normalizedFormattedExpression = value.Type == JTokenType.String + ? "@{0} {1} '{2}'" + : "@{0} {1} {2}"; + + return string.Format( + Thread.CurrentThread.CurrentCulture, + normalizedFormattedExpression, + subProperty, + comparisonOperator, + value); + } + private void ExpandPolicyRule(JToken policyRule) { if (policyRule.Type == JTokenType.Object) @@ -267,13 +379,7 @@ private void ExpandPolicyRule(JToken policyRule) if (child.Value.Type == JTokenType.String) { var field = child.Value.Value(); - if (field.CountCharacterOccurrences(SLASH) == 1) - { - var contents = field.Split(SLASH); - var providerNamespace = contents[0]; - var resourceType = contents[1]; - _PolicyAliasProviderHelper.SetPolicyRuleType(providerNamespace, resourceType); - } + SetPolicyRuleType(field); } } @@ -288,9 +394,11 @@ private void ExpandPolicyRule(JToken policyRule) else if (child.Name.Equals(PROPERTY_COUNT, StringComparison.OrdinalIgnoreCase)) { hasFieldCount = true; + if (child.Value.Type == JTokenType.Object) { var countObject = child.Value.ToObject(); + if (countObject.TryStringProperty(PROPERTY_FIELD, out var outerFieldAlias)) { var outerFieldAliasPath = ResolvePolicyAliasPath(outerFieldAlias); @@ -299,53 +407,34 @@ private void ExpandPolicyRule(JToken policyRule) { if (countObject.TryObjectProperty(PROPERTY_WHERE, out var whereExpression)) { - // Single field in where expression - if (whereExpression.TryStringProperty(PROPERTY_FIELD, out var innerFieldAlias)) + // field in where expression + var fieldFilter = GetFieldObjectPathArrayFilter(whereExpression); + if (fieldFilter != null) { - var innerFieldAliasPath = ResolvePolicyAliasPath(innerFieldAlias); - - if (innerFieldAliasPath != null && innerFieldAliasPath.Contains(outerFieldAliasPath)) - { - // Get first property that is not a field expression - var comparisonExpression = whereExpression - .Children() - .FirstOrDefault(prop => !prop.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)); - - if (comparisonExpression != null) - { - var objectPathComparisonOperator = ExpressionToObjectPathComparisonOperator(comparisonExpression.Name); - - var splitAliasPath = innerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); - - // Surround right hand side with quotes if string - var normalizedFormattedExpression = comparisonExpression.Value.Type == JTokenType.String - ? "?@{0} {1} '{2}'" - : "?@{0} {1} {2}"; - - var filter = string.Format( - Thread.CurrentThread.CurrentCulture, - normalizedFormattedExpression, - splitAliasPath[1], - objectPathComparisonOperator, - comparisonExpression.Value); - - policyRule[PROPERTY_FIELD] = string.Format( - Thread.CurrentThread.CurrentCulture, - "{0}[{1}]", - splitAliasPath[0], - filter); - - policyRule[PROPERTY_COUNT].Parent.Remove(); - } - } + var splitAliasPath = outerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], fieldFilter); + policyRule[PROPERTY_COUNT].Parent.Remove(); } // nested allOf in where expression - else if (whereExpression.TryObjectProperty(PROPERTY_ALL_OF, out var allofExpression)) + else if (whereExpression.TryArrayProperty(PROPERTY_ALL_OF, out var allofExpression)) { - + var splitAliasPath = outerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + var filter = new StringBuilder(); + ExpressionToObjectPathArrayFilter(allofExpression, AND_CLAUSE, filter); + policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); + policyRule[PROPERTY_COUNT].Parent.Remove(); } + // nested anyOf in where expression + else if (whereExpression.TryArrayProperty(PROPERTY_ANY_OF, out var anyOfExpression)) + { + var splitAliasPath = outerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + var filter = new StringBuilder(); + ExpressionToObjectPathArrayFilter(anyOfExpression, OR_CLAUSE, filter); + policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); + policyRule[PROPERTY_COUNT].Parent.Remove(); + } } // Single field in count expression diff --git a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 index 9241f519483..94a74f19f28 100644 --- a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 @@ -680,6 +680,31 @@ Describe 'Export-AzPolicyAssignmentRuleData' -Tag 'Cmdlet', 'Export-AzPolicyAssi Index = 2 AssignmentFile = (Join-Path -Path $here -ChildPath 'test3.assignment.json') } + @{ + Name = 'test4' + Index = 3 + AssignmentFile = (Join-Path -Path $here -ChildPath 'test4.assignment.json') + }, + @{ + Name = 'test5' + Index = 4 + AssignmentFile = (Join-Path -Path $here -ChildPath 'test5.assignment.json') + }, + @{ + Name = 'test6' + Index = 5 + AssignmentFile = (Join-Path -Path $here -ChildPath 'test6.assignment.json') + }, + @{ + Name = 'test7' + Index = 6 + AssignmentFile = (Join-Path -Path $here -ChildPath 'test7.assignment.json') + }, + @{ + Name = 'test8' + Index = 7 + AssignmentFile = (Join-Path -Path $here -ChildPath 'test8.assignment.json') + } ) { param($Name, $Index, $AssignmentFile) $result = @(Export-AzPolicyAssignmentRuleData -Name $Name -AssignmentFile $AssignmentFile -OutputPath $outputPath); diff --git a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc index 62ed9f2734a..3bf1a992ff2 100644 --- a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc +++ b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc @@ -361,5 +361,141 @@ ] } } + }, + { + // Synopsis: Enforce disabling of SNAT on load balancing rules + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "DisableLBRuleSNAT" + }, + "spec": { + "condition": { + "allOf": [ + { + "equals": "Microsoft.Network/loadBalancers", + "field": "type" + }, + { + "greaterOrEquals": 1, + "field": "properties.loadBalancingRules[?@.properties.disableOutboundSnat == False]" + } + ] + } + } + }, + { + // Synopsis: Enforce atleast more than one LB rule + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "EnsureAtleastOneLBRule" + }, + "spec": { + "condition": { + "allOf": [ + { + "equals": "Microsoft.Network/loadBalancers", + "field": "type" + }, + { + "greaterOrEquals": 1, + "field": "properties.loadBalancingRules[*]" + } + ] + } + } + }, + { + // Synopsis: Enforce unique description on one NSG rule + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "UniqueDescriptionNSG" + }, + "spec": { + "condition": { + "allOf": [ + { + "equals": "Microsoft.Network/networkSecurityGroups", + "field": "type" + }, + { + "field": "properties.securityRules[?@.properties.description == 'My unique description']", + "count": 1 + } + ] + } + } + }, + { + // Synopsis: Denies RDP port on inbound NSG rules + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "DenyNSGRDPInboundPort" + }, + "spec": { + "condition": { + "allOf": [ + { + "greater": 0, + "field": "properties.securityRules[?@.type == 'Microsoft.Network/networkSecurityGroups/securityRules' && @.properties.direction == 'Inbound' && @.properties.access == 'Allow' && @.properties.destinationPortRange == '3389']" + } + ] + } + } + }, + { + // Synopsis: Deny common ports on NSG rules + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "DenyPortsNSG" + }, + "spec": { + "condition": { + "anyOf": [ + { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups/securityRules" + }, + { + "not": { + "field": "properties.sourceAddressPrefix", + "notEquals": "*" + } + }, + { + "anyOf": [ + { + "field": "properties.destinationPortRange", + "equals": "22" + }, + { + "field": "properties.destinationPortRange", + "equals": "3389" + } + ] + } + ] + }, + { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups" + }, + { + "greater": 0, + "field": "properties.securityRules[?@.properties.sourceAddressPrefix == '*' && (@.properties.destinationPortRange == '22' || @.properties.destinationPortRange == '3389')]" + } + ] + } + ] + } + } } ] \ No newline at end of file diff --git a/tests/PSRule.Rules.Azure.Tests/test4.assignment.json b/tests/PSRule.Rules.Azure.Tests/test4.assignment.json index 65830e1f0a1..b8982622ba7 100644 --- a/tests/PSRule.Rules.Azure.Tests/test4.assignment.json +++ b/tests/PSRule.Rules.Azure.Tests/test4.assignment.json @@ -75,4 +75,4 @@ } ] } -] +] \ No newline at end of file diff --git a/tests/PSRule.Rules.Azure.Tests/test7.assignment.json b/tests/PSRule.Rules.Azure.Tests/test7.assignment.json new file mode 100644 index 00000000000..383a30636ed --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test7.assignment.json @@ -0,0 +1,90 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "000000000000000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "ResourceName": "000000000000000000000000", + "ResourceGroupName": null, + "ResourceType": "Microsoft.Authorization/policyAssignments", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Sku": null, + "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "DenyNSGRDPInboundPort", + "Description": null, + "Metadata": { + "assignedBy": "Armaan Dhaliwal-McLeod", + "parameterScopes": {}, + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:46:07.0986544Z", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 0, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "Parameters": {}, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "00000000-0000-0000-0000-000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "ResourceName": "00000000-0000-0000-0000-000000000000", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Properties": { + "Description": "Denies RDP port on inbound NSG rules", + "DisplayName": "DenyNSGRDPInboundPort", + "Metadata": { + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:35:25.9960771Z", + "updatedBy": null, + "updatedOn": null + }, + "Mode": "All", + "Parameters": {}, + "PolicyRule": { + "if": { + "allOf": [ + { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups/securityRules" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", + "equals": "Inbound" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "equals": "Allow" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "equals": "3389" + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { + "effect": "deny" + } + }, + "PolicyType": 1 + }, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000" + } + ] + } +] \ No newline at end of file diff --git a/tests/PSRule.Rules.Azure.Tests/test8.assignment.json b/tests/PSRule.Rules.Azure.Tests/test8.assignment.json new file mode 100644 index 00000000000..c244ed78e25 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test8.assignment.json @@ -0,0 +1,124 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "000000000000000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "ResourceName": "000000000000000000000000", + "ResourceGroupName": null, + "ResourceType": "Microsoft.Authorization/policyAssignments", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Sku": null, + "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "DenyPortsNSG", + "Description": null, + "Metadata": { + "assignedBy": "Armaan Dhaliwal-McLeod", + "parameterScopes": {}, + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:46:07.0986544Z", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 0, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "Parameters": {}, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "00000000-0000-0000-0000-000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "ResourceName": "00000000-0000-0000-0000-000000000000", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Properties": { + "Description": "Deny common ports on NSG rules", + "DisplayName": "DenyPortsNSG", + "Metadata": { + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:35:25.9960771Z", + "updatedBy": null, + "updatedOn": null + }, + "Mode": "All", + "Parameters": {}, + "PolicyRule": { + "if": { + "anyOf": [ + { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups/securityRules" + }, + { + "not": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefix", + "notEquals": "*" + } + }, + { + "anyOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRange", + "equals": "22" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRange", + "equals": "3389" + } + ] + } + ] + }, + { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups" + }, + { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "allOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", + "equals": "*" + }, + { + "anyOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "equals": "22" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "equals": "3389" + } + ] + } + ] + } + }, + "greater": 0 + } + ] + } + ] + }, + "then": { + "effect": "deny" + } + }, + "PolicyType": 1 + }, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000" + } + ] + } +] \ No newline at end of file From c719904677effc749681a46ef6b614128455bc75 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 15 May 2022 23:09:53 +1000 Subject: [PATCH 03/16] Order properties --- .../Data/Policy/PolicyAssignmentVisitor.cs | 10 +++++++++- tests/PSRule.Rules.Azure.Tests/test3.assignment.json | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index c9938a9814d..a76918d2553 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -349,6 +349,14 @@ private static string FormatObjectPathArrayFilter(string subProperty, string com value); } + private static int OrderPropertySelector(JProperty property) + { + return property.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase) + || property.Name.Equals(PROPERTY_COUNT, StringComparison.OrdinalIgnoreCase) + ? 0 + : 1; + } + private void ExpandPolicyRule(JToken policyRule) { if (policyRule.Type == JTokenType.Object) @@ -356,7 +364,7 @@ private void ExpandPolicyRule(JToken policyRule) var hasFieldType = false; var hasFieldCount = false; - foreach (var child in policyRule.Children().ToList()) + foreach (var child in policyRule.Children().OrderBy(OrderPropertySelector)) { // Expand field aliases if (child.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)) diff --git a/tests/PSRule.Rules.Azure.Tests/test3.assignment.json b/tests/PSRule.Rules.Azure.Tests/test3.assignment.json index db861a96388..df36c0362a7 100644 --- a/tests/PSRule.Rules.Azure.Tests/test3.assignment.json +++ b/tests/PSRule.Rules.Azure.Tests/test3.assignment.json @@ -90,8 +90,8 @@ "if": { "allOf": [ { - "field": "type", - "equals": "Microsoft.Web/sites" + "equals": "Microsoft.Web/sites", + "field": "type" }, { "field": "kind", From b6be9caa21b3af188e2e74c9cb399d089ba3ddbe Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Mon, 16 May 2022 23:21:12 +1000 Subject: [PATCH 04/16] Minor fixes --- .../Data/Policy/PolicyAssignmentVisitor.cs | 8 ++++++-- tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index a76918d2553..838c47d4d78 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -278,16 +278,20 @@ private string GetFieldObjectPathArrayFilter(JObject obj) .Children() .FirstOrDefault(prop => !prop.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)); - if (comparisonExpression != null) + if (comparisonExpression != null && !string.IsNullOrEmpty(subProperty)) { var objectPathComparisonOperator = ExpressionToObjectPathComparisonOperator(comparisonExpression.Name); + var comparisonValue = comparisonExpression.Value; + if (comparisonValue.Type == JTokenType.String) + comparisonValue = TemplateVisitor.ExpandPropertyToken(this, comparisonValue); + if (objectPathComparisonOperator != null) { return FormatObjectPathArrayFilter( subProperty, objectPathComparisonOperator, - comparisonExpression.Value); + comparisonValue); } } } diff --git a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc index 3bf1a992ff2..2d562e88ca8 100644 --- a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc +++ b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc @@ -329,8 +329,8 @@ { "allOf": [ { - "field": "type", - "equals": "Microsoft.Web/sites" + "equals": "Microsoft.Web/sites", + "field": "type" }, { "field": "kind", From 328cab1d2d7b137c4bb54037eb251b16cc8f769c Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Tue, 17 May 2022 19:48:51 +1000 Subject: [PATCH 05/16] Add parameter to assignment --- .../PSRule.Rules.Azure.Tests/test7.assignment.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/PSRule.Rules.Azure.Tests/test7.assignment.json b/tests/PSRule.Rules.Azure.Tests/test7.assignment.json index 383a30636ed..8c492c09687 100644 --- a/tests/PSRule.Rules.Azure.Tests/test7.assignment.json +++ b/tests/PSRule.Rules.Azure.Tests/test7.assignment.json @@ -45,7 +45,16 @@ "updatedOn": null }, "Mode": "All", - "Parameters": {}, + "Parameters": { + "portNumber": { + "type": "String", + "metadata": { + "displayName": "Port Number", + "description": "The port number which to block inbound access" + }, + "defaultValue": "3389" + } + }, "PolicyRule": { "if": { "allOf": [ @@ -68,7 +77,7 @@ }, { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", - "equals": "3389" + "equals": "[parameters('portNumber')]" } ] } From 519d15365511a4a5cc155e03508ebac2f8fe41e8 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Wed, 18 May 2022 07:38:47 +1000 Subject: [PATCH 06/16] Add conversion for in & exists operators --- .../Data/Policy/PolicyAssignmentVisitor.cs | 90 +++++++++++--- .../Cmdlet.Common.Tests.ps1 | 20 +++- .../emittedJsonRulesData.jsonc | 45 +++++++ .../test9.assignment.json | 112 ++++++++++++++++++ 4 files changed, 244 insertions(+), 23 deletions(-) create mode 100644 tests/PSRule.Rules.Azure.Tests/test9.assignment.json diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index 838c47d4d78..1907079108e 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -37,6 +37,9 @@ internal abstract class PolicyAssignmentVisitor private const string FIELD_GREATEROREQUALS = "greaterOrEquals"; private const string FIELD_LESS = "less"; private const string FIELD_LESSOREQUALS = "lessOrEquals"; + private const string FIELD_IN = "in"; + private const string FIELD_NOTIN = "notIn"; + private const string FIELD_EXISTS = "exists"; private const string PROPERTY_DISPLAYNAME = "displayName"; private const string PROPERTY_DESCRIPTION = "description"; private const string PROPERTY_DEPLOYMENT = "deployment"; @@ -46,6 +49,12 @@ internal abstract class PolicyAssignmentVisitor private const string COLLECTION_ALIAS = "[*]"; private const string AND_CLAUSE = "&&"; private const string OR_CLAUSE = "||"; + private const string EQUALITY_OPERATOR = "=="; + private const string INEQUALITY_OPERATOR = "!="; + private const string LESS_OPERATOR = "<"; + private const string LESSOREQUAL_OPERATOR = "<="; + private const string GREATER_OPERATOR = ">"; + private const string GREATEROREQUAL_OPERATOR = ">="; private const char SLASH = '/'; private const char GROUP_OPEN = '('; private const char GROUP_CLOSE = ')'; @@ -234,12 +243,12 @@ private static void RemovePolicyRuleDeployment(JObject policyRule) private static string ExpressionToObjectPathComparisonOperator(string expression) => expression switch { - FIELD_EQUALS => "==", - FIELD_NOTEQUALS => "!=", - FIELD_GREATER => ">", - FIELD_GREATEROREQUALS => ">=", - FIELD_LESS => "<", - FIELD_LESSOREQUALS => "<=", + FIELD_EQUALS => EQUALITY_OPERATOR, + FIELD_NOTEQUALS => INEQUALITY_OPERATOR, + FIELD_GREATER => GREATER_OPERATOR, + FIELD_GREATEROREQUALS => GREATEROREQUAL_OPERATOR, + FIELD_LESS => LESS_OPERATOR, + FIELD_LESSOREQUALS => LESSOREQUAL_OPERATOR, _ => null }; @@ -266,12 +275,14 @@ private string GetFieldObjectPathArrayFilter(JObject obj) subProperty = $".{PROPERTY_TYPE}"; SetPolicyRuleType(fieldType); } - - var fieldAliasPath = ResolvePolicyAliasPath(fieldProperty); - if (fieldAliasPath != null) + else { - var splitAliasPath = fieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); - subProperty = splitAliasPath[1]; + var fieldAliasPath = ResolvePolicyAliasPath(fieldProperty); + if (fieldAliasPath != null) + { + var splitAliasPath = fieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + subProperty = splitAliasPath[1]; + } } var comparisonExpression = obj @@ -293,6 +304,41 @@ private string GetFieldObjectPathArrayFilter(JObject obj) objectPathComparisonOperator, comparisonValue); } + else + { + // Convert in expression + if (comparisonExpression.Name.Equals(FIELD_IN, StringComparison.OrdinalIgnoreCase) + && comparisonValue.Type == JTokenType.Array) + { + var filters = comparisonValue + .ToObject() + .Select(val => FormatObjectPathArrayFilter(subProperty, "==", val)); + + return string.Concat(GROUP_OPEN, string.Join($" {OR_CLAUSE} ", filters), GROUP_CLOSE); + } + + // Convert notIn expression + else if (comparisonExpression.Name.Equals(FIELD_NOTIN, StringComparison.OrdinalIgnoreCase) + && comparisonValue.Type == JTokenType.Array) + { + var filters = comparisonValue + .ToObject() + .Select(val => FormatObjectPathArrayFilter(subProperty, "!=", val)); + + return string.Concat(GROUP_OPEN, string.Join($" {AND_CLAUSE} ", filters), GROUP_CLOSE); + } + + // Convert exists expression + else if (comparisonExpression.Name.Equals(FIELD_EXISTS, StringComparison.OrdinalIgnoreCase)) + { + var existsValue = comparisonValue.Value(); + + return FormatObjectPathArrayFilter( + subProperty, + existsValue ? INEQUALITY_OPERATOR : EQUALITY_OPERATOR, + null); + } + } } } return null; @@ -340,14 +386,15 @@ private static string FormatObjectPathArrayExpression(string array, string filte private static string FormatObjectPathArrayFilter(string subProperty, string comparisonOperator, JToken value) { - // Surround right hand side with quotes if string - var normalizedFormattedExpression = value.Type == JTokenType.String - ? "@{0} {1} '{2}'" - : "@{0} {1} {2}"; - - return string.Format( + return value == null + ? string.Format( + Thread.CurrentThread.CurrentCulture, + "@{0} {1} null", + subProperty, + comparisonOperator) + : string.Format( Thread.CurrentThread.CurrentCulture, - normalizedFormattedExpression, + value.Type == JTokenType.String ? "@{0} {1} '{2}'" : "@{0} {1} {2}", subProperty, comparisonOperator, value); @@ -460,6 +507,13 @@ private void ExpandPolicyRule(JToken policyRule) } } + // Convert string booleans for exists expression + else if (child.Name.Equals(FIELD_EXISTS, StringComparison.OrdinalIgnoreCase) + && child.Value.Type == JTokenType.String) + { + policyRule[child.Name] = child.Value.Value(); + } + // Expand string expressions else if (child.Value.Type == JTokenType.String) { diff --git a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 index 94a74f19f28..a7df749c7ee 100644 --- a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 @@ -13,7 +13,8 @@ BeforeAll { $ErrorActionPreference = 'Stop'; Set-StrictMode -Version latest; - if ($Env:SYSTEM_DEBUG -eq 'true') { + if ($Env:SYSTEM_DEBUG -eq 'true') + { $VerbosePreference = 'Continue'; } @@ -30,8 +31,10 @@ BeforeAll { #region Mocks - function MockContext { - process { + function MockContext + { + process + { return @( (New-Object -TypeName Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext -ArgumentList @( [PSCustomObject]@{ @@ -73,8 +76,10 @@ BeforeAll { } } - function MockSingleSubscription { - process { + function MockSingleSubscription + { + process + { return @( (New-Object -TypeName Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext -ArgumentList @( [PSCustomObject]@{ @@ -704,6 +709,11 @@ Describe 'Export-AzPolicyAssignmentRuleData' -Tag 'Cmdlet', 'Export-AzPolicyAssi Name = 'test8' Index = 7 AssignmentFile = (Join-Path -Path $here -ChildPath 'test8.assignment.json') + }, + @{ + Name = 'test9' + Index = 8 + AssignmentFile = (Join-Path -Path $here -ChildPath 'test9.assignment.json') } ) { param($Name, $Index, $AssignmentFile) diff --git a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc index 2d562e88ca8..97674168b00 100644 --- a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc +++ b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc @@ -497,5 +497,50 @@ ] } } + }, + { + // Synopsis: Prevent subnets without NSG + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "PreventSubnetsWithoutNSG" + }, + "spec": { + "condition": { + "anyOf": [ + { + "allOf": [ + { + "equals": "Microsoft.Network/virtualNetworks/subnets", + "field": "type" + }, + { + "exists": false, + "field": "properties.routeTable.id" + }, + { + "field": "name", + "notIn": [ + "AzureFirewallSubnet", + "AzureFirewallManagementSubnet" + ] + } + ] + }, + { + "allOf": [ + { + "equals": "Microsoft.Network/virtualNetworks", + "field": "type" + }, + { + "notEquals": 0, + "field": "properties.subnets[?@.properties.routeTable.id == null && (@.name != 'AzureFirewallManagementSubnet' && @.name != 'AzureFirewallSubnet')]" + } + ] + } + ] + } + } } ] \ No newline at end of file diff --git a/tests/PSRule.Rules.Azure.Tests/test9.assignment.json b/tests/PSRule.Rules.Azure.Tests/test9.assignment.json new file mode 100644 index 00000000000..2611d99e440 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test9.assignment.json @@ -0,0 +1,112 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "000000000000000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "ResourceName": "000000000000000000000000", + "ResourceGroupName": null, + "ResourceType": "Microsoft.Authorization/policyAssignments", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Sku": null, + "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "PreventSubnetsWithoutNSG", + "Description": null, + "Metadata": { + "assignedBy": "Armaan Dhaliwal-McLeod", + "parameterScopes": {}, + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:46:07.0986544Z", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 0, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "Parameters": {}, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "00000000-0000-0000-0000-000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "ResourceName": "00000000-0000-0000-0000-000000000000", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Properties": { + "Description": "Prevent subnets without NSG", + "DisplayName": "PreventSubnetsWithoutNSG", + "Metadata": { + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:35:25.9960771Z", + "updatedBy": null, + "updatedOn": null + }, + "Mode": "All", + "Parameters": {}, + "PolicyRule": { + "if": { + "anyOf": [ + { + "allOf": [ + { + "equals": "Microsoft.Network/virtualNetworks/subnets", + "field": "type" + }, + { + "exists": "false", + "field": "Microsoft.Network/virtualNetworks/subnets/routeTable.id" + }, + { + "field": "name", + "notIn": [ + "AzureFirewallSubnet", + "AzureFirewallManagementSubnet" + ] + } + ] + }, + { + "allOf": [ + { + "equals": "Microsoft.Network/virtualNetworks", + "field": "type" + }, + { + "count": { + "field": "Microsoft.Network/virtualNetworks/subnets[*]", + "where": { + "allOf": [ + { + "exists": "false", + "field": "Microsoft.Network/virtualNetworks/subnets[*].routeTable.id" + }, + { + "field": "Microsoft.Network/virtualNetworks/subnets[*].name", + "notIn": [ + "AzureFirewallManagementSubnet", + "AzureFirewallSubnet" + ] + } + ] + } + }, + "notEquals": 0 + } + ] + } + ] + }, + "then": { + "effect": "deny" + } + }, + "PolicyType": 1 + }, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000" + } + ] + } +] \ No newline at end of file From 5c7251c32eb8104532a8915ed0d23a9e8cbff9c2 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 22 May 2022 21:57:28 +1000 Subject: [PATCH 07/16] Split from last collection alias substring --- .vscode/settings.json | 3 +- .../Common/StringExtensions.cs | 9 ++ .../Data/Policy/PolicyAssignmentVisitor.cs | 10 +- .../Cmdlet.Common.Tests.ps1 | 5 + .../emittedJsonRulesData.jsonc | 26 +++++ .../test10.assignment.json | 107 ++++++++++++++++++ 6 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 tests/PSRule.Rules.Azure.Tests/test10.assignment.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 309ec4a4923..6578bb1cf57 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,8 @@ "editor.tabSize": 2 }, "[json]": { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.tabSize": 2 }, "[markdown]": { "editor.tabSize": 2 diff --git a/src/PSRule.Rules.Azure/Common/StringExtensions.cs b/src/PSRule.Rules.Azure/Common/StringExtensions.cs index 3231aa9f66b..1a7544a74e6 100644 --- a/src/PSRule.Rules.Azure/Common/StringExtensions.cs +++ b/src/PSRule.Rules.Azure/Common/StringExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Linq; namespace PSRule.Rules.Azure @@ -21,6 +22,14 @@ internal static int CountCharacterOccurrences(this string str, char chr) : 0; } + internal static string[] SplitByLastSubstring(this string str, string substring) + { + var lastSubstringIndex = str.LastIndexOf(substring, StringComparison.OrdinalIgnoreCase); + var firstPart = str.Substring(0, lastSubstringIndex); + var secondPart = str.Substring(lastSubstringIndex + substring.Length); + return new string[] { firstPart, secondPart }; + } + internal static bool IsExpressionString(this string str) { return str != null && diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index 1907079108e..76605ba3870 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -280,7 +280,7 @@ private string GetFieldObjectPathArrayFilter(JObject obj) var fieldAliasPath = ResolvePolicyAliasPath(fieldProperty); if (fieldAliasPath != null) { - var splitAliasPath = fieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + var splitAliasPath = fieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); subProperty = splitAliasPath[1]; } } @@ -289,7 +289,7 @@ private string GetFieldObjectPathArrayFilter(JObject obj) .Children() .FirstOrDefault(prop => !prop.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)); - if (comparisonExpression != null && !string.IsNullOrEmpty(subProperty)) + if (comparisonExpression != null) { var objectPathComparisonOperator = ExpressionToObjectPathComparisonOperator(comparisonExpression.Name); @@ -470,7 +470,7 @@ private void ExpandPolicyRule(JToken policyRule) var fieldFilter = GetFieldObjectPathArrayFilter(whereExpression); if (fieldFilter != null) { - var splitAliasPath = outerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + var splitAliasPath = outerFieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], fieldFilter); policyRule[PROPERTY_COUNT].Parent.Remove(); } @@ -478,7 +478,7 @@ private void ExpandPolicyRule(JToken policyRule) // nested allOf in where expression else if (whereExpression.TryArrayProperty(PROPERTY_ALL_OF, out var allofExpression)) { - var splitAliasPath = outerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + var splitAliasPath = outerFieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); var filter = new StringBuilder(); ExpressionToObjectPathArrayFilter(allofExpression, AND_CLAUSE, filter); policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); @@ -488,7 +488,7 @@ private void ExpandPolicyRule(JToken policyRule) // nested anyOf in where expression else if (whereExpression.TryArrayProperty(PROPERTY_ANY_OF, out var anyOfExpression)) { - var splitAliasPath = outerFieldAliasPath.Split(new string[] { COLLECTION_ALIAS }, StringSplitOptions.None); + var splitAliasPath = outerFieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); var filter = new StringBuilder(); ExpressionToObjectPathArrayFilter(anyOfExpression, OR_CLAUSE, filter); policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); diff --git a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 index a7df749c7ee..570adece1c3 100644 --- a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 @@ -714,6 +714,11 @@ Describe 'Export-AzPolicyAssignmentRuleData' -Tag 'Cmdlet', 'Export-AzPolicyAssi Name = 'test9' Index = 8 AssignmentFile = (Join-Path -Path $here -ChildPath 'test9.assignment.json') + }, + @{ + Name = 'test10' + Index = 9 + AssignmentFile = (Join-Path -Path $here -ChildPath 'test10.assignment.json') } ) { param($Name, $Index, $AssignmentFile) diff --git a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc index 97674168b00..5ec9f4e0fc5 100644 --- a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc +++ b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc @@ -542,5 +542,31 @@ ] } } + }, + { + // Synopsis: Prevent private endpoint being created in specific subnet + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "DenyPrivateEndpointSpecificSubnet" + }, + "spec": { + "condition": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/privateEndpoints" + }, + { + "field": "properties.subnet.id", + "notContains": "pls" + }, + { + "greaterOrEquals": 1, + "field": "properties.privateLinkServiceConnections[*].properties.groupIds[?(@ != 'blob' && @ != 'sqlServer')]" + } + ] + } + } } ] \ No newline at end of file diff --git a/tests/PSRule.Rules.Azure.Tests/test10.assignment.json b/tests/PSRule.Rules.Azure.Tests/test10.assignment.json new file mode 100644 index 00000000000..1fefc9b8e88 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test10.assignment.json @@ -0,0 +1,107 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "000000000000000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "ResourceName": "000000000000000000000000", + "ResourceGroupName": null, + "ResourceType": "Microsoft.Authorization/policyAssignments", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Sku": null, + "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/000000000000000000000000", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "DenyPrivateEndpointSpecificSubnet", + "Description": null, + "Metadata": { + "assignedBy": "Armaan Dhaliwal-McLeod", + "parameterScopes": {}, + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:46:07.0986544Z", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 0, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "Parameters": { + "subnetName": { + "value": "pls" + }, + "exemptedGroupIds": { + "value": [ + "blob", + "sqlServer" + ] + } + }, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "00000000-0000-0000-0000-000000000000", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000", + "ResourceName": "00000000-0000-0000-0000-000000000000", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "Properties": { + "Description": "Prevent private endpoint being created in specific subnet", + "DisplayName": "DenyPrivateEndpointSpecificSubnet", + "Metadata": { + "createdBy": "15bd0bad-d884-4479-9860-a3afffae4132", + "createdOn": "2022-05-09T11:35:25.9960771Z", + "updatedBy": null, + "updatedOn": null + }, + "Mode": "All", + "Parameters": { + "subnetName": { + "type": "string", + "metadata": { + "displayName": "Allowed Subnet prefix name (i.e. pls)", + "description": "Name of subnet where Private Endpoints are allowed to be deployed into." + } + }, + "exemptedGroupIds": { + "type": "array", + "metadata": { + "displayName": "Exempted Private Endpoint Group IDs", + "description": "The Group IDs that are exempted from this Policy (i.e. blob)" + } + } + }, + "PolicyRule": { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/privateEndpoints" + }, + { + "field": "Microsoft.Network/privateEndpoints/subnet.id", + "notContains": "[parameters('subnetName')]" + }, + { + "count": { + "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].groupIds[*]", + "where": { + "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].groupIds[*]", + "notIn": "[parameters('exemptedGroupIds')]" + } + }, + "greaterOrEquals": 1 + } + ] + }, + "then": { + "effect": "deny" + } + }, + "PolicyType": 1 + }, + "PolicyDefinitionId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000" + } + ] + } +] \ No newline at end of file From fefca7d51853a1370106045ad1742712532c055d Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 22 May 2022 22:12:24 +1000 Subject: [PATCH 08/16] More cleanup --- .../Data/Policy/PolicyAssignmentHelper.cs | 10 +++------- .../Data/Policy/PolicyAssignmentVisitor.cs | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs index 6b4fbf31367..38302e38364 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs @@ -60,13 +60,9 @@ internal PolicyDefinition[] ProcessAssignment(string assignmentFile, out PolicyA private static JArray ReadFileArray(string path) { - using (var stream = new StreamReader(path)) - { - using (var reader = new CamelCasePropertyNameJsonTextReader(stream)) - { - return JArray.Load(reader); - } - } + using var stream = new StreamReader(path); + using var reader = new CamelCasePropertyNameJsonTextReader(stream); + return JArray.Load(reader); } private sealed class CamelCasePropertyNameJsonTextReader : JsonTextReader diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index 76605ba3870..beea5d3047b 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -312,7 +312,7 @@ private string GetFieldObjectPathArrayFilter(JObject obj) { var filters = comparisonValue .ToObject() - .Select(val => FormatObjectPathArrayFilter(subProperty, "==", val)); + .Select(val => FormatObjectPathArrayFilter(subProperty, EQUALITY_OPERATOR, val)); return string.Concat(GROUP_OPEN, string.Join($" {OR_CLAUSE} ", filters), GROUP_CLOSE); } @@ -323,7 +323,7 @@ private string GetFieldObjectPathArrayFilter(JObject obj) { var filters = comparisonValue .ToObject() - .Select(val => FormatObjectPathArrayFilter(subProperty, "!=", val)); + .Select(val => FormatObjectPathArrayFilter(subProperty, INEQUALITY_OPERATOR, val)); return string.Concat(GROUP_OPEN, string.Join($" {AND_CLAUSE} ", filters), GROUP_CLOSE); } From e0d10dbd98a9961fca76dd90c6d5aacf5746e0c7 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 12 Jun 2022 21:05:30 +1000 Subject: [PATCH 09/16] Convert notEquals to notCount --- .vscode/settings.json | 6 ++++++ .../Data/Policy/PolicyAssignmentVisitor.cs | 12 ++++++++++++ .../emittedJsonRulesData.jsonc | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6578bb1cf57..917c05192cf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,13 +45,19 @@ "Concat", "endregion", "failover", + "GREATEROREQUAL", + "GREATEROREQUALS", "Hashtable", "kube", "kubelet", "kubenet", "Kubernetes", + "LESSOREQUAL", + "LESSOREQUALS", "lifecycle", "nics", + "NOTCOUNT", + "NOTEQUALS", "NSGs", "OWASP", "POLICYDEFINITIONID", diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index beea5d3047b..56fcd2f59a4 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -45,6 +45,7 @@ internal abstract class PolicyAssignmentVisitor private const string PROPERTY_DEPLOYMENT = "deployment"; private const string PROPERTY_VALUE = "value"; private const string PROPERTY_COUNT = "count"; + private const string PROPERTY_NOTCOUNT = "notCount"; private const string PROPERTY_WHERE = "where"; private const string COLLECTION_ALIAS = "[*]"; private const string AND_CLAUSE = "&&"; @@ -269,6 +270,8 @@ private string GetFieldObjectPathArrayFilter(JObject obj) { var subProperty = string.Empty; + // If we come across a type, set the .type sub property in the object path + // Also set the current type for any further alias expansion if (fieldProperty.Equals(PROPERTY_TYPE, StringComparison.OrdinalIgnoreCase) && obj.TryStringProperty(FIELD_EQUALS, out var fieldType)) { @@ -293,6 +296,7 @@ private string GetFieldObjectPathArrayFilter(JObject obj) { var objectPathComparisonOperator = ExpressionToObjectPathComparisonOperator(comparisonExpression.Name); + // Expand string values if we come across any var comparisonValue = comparisonExpression.Value; if (comparisonValue.Type == JTokenType.String) comparisonValue = TemplateVisitor.ExpandPropertyToken(this, comparisonValue); @@ -415,6 +419,7 @@ private void ExpandPolicyRule(JToken policyRule) var hasFieldType = false; var hasFieldCount = false; + // Go through each property and make sure fields and counts are sorted first foreach (var child in policyRule.Children().OrderBy(OrderPropertySelector)) { // Expand field aliases @@ -449,6 +454,13 @@ private void ExpandPolicyRule(JToken policyRule) policyRule[PROPERTY_COUNT] = child.Value; } + // Replace notEquals with notCount if field count expression is currently being visited + else if (hasFieldCount && child.Name.Equals(FIELD_NOTEQUALS, StringComparison.OrdinalIgnoreCase)) + { + policyRule[FIELD_NOTEQUALS].Parent.Remove(); + policyRule[PROPERTY_NOTCOUNT] = child.Value; + } + // Expand field count expressions else if (child.Name.Equals(PROPERTY_COUNT, StringComparison.OrdinalIgnoreCase)) { diff --git a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc index 5ec9f4e0fc5..f7ec64a5b8a 100644 --- a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc +++ b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc @@ -534,8 +534,8 @@ "field": "type" }, { - "notEquals": 0, - "field": "properties.subnets[?@.properties.routeTable.id == null && (@.name != 'AzureFirewallManagementSubnet' && @.name != 'AzureFirewallSubnet')]" + "field": "properties.subnets[?@.properties.routeTable.id == null && (@.name != 'AzureFirewallManagementSubnet' && @.name != 'AzureFirewallSubnet')]", + "notCount": 0 } ] } From d2cfeb747a0b3bea42fd7cdadda5968cc4113602 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 12 Jun 2022 21:41:20 +1000 Subject: [PATCH 10/16] Undo formatting --- .../Cmdlet.Common.Tests.ps1 | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 index 570adece1c3..ff960654b4e 100644 --- a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 @@ -13,8 +13,7 @@ BeforeAll { $ErrorActionPreference = 'Stop'; Set-StrictMode -Version latest; - if ($Env:SYSTEM_DEBUG -eq 'true') - { + if ($Env:SYSTEM_DEBUG -eq 'true') { $VerbosePreference = 'Continue'; } @@ -31,10 +30,8 @@ BeforeAll { #region Mocks - function MockContext - { - process - { + function MockContext { + process { return @( (New-Object -TypeName Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext -ArgumentList @( [PSCustomObject]@{ @@ -76,10 +73,8 @@ BeforeAll { } } - function MockSingleSubscription - { - process - { + function MockSingleSubscription { + process { return @( (New-Object -TypeName Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext -ArgumentList @( [PSCustomObject]@{ From f9e25c790dd857d3b8481a57af86f356b2330e7b Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 12 Jun 2022 23:13:19 +1000 Subject: [PATCH 11/16] Minor fixes --- .editorconfig | 5 +---- src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.editorconfig b/.editorconfig index 50ede30e533..2ca08167a3b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -44,7 +44,4 @@ csharp_prefer_simple_default_expression = true:suggestion # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion - -# Disable rules -dotnet_diagnostic.CA1508.severity = none \ No newline at end of file +csharp_style_var_elsewhere = true:suggestion \ No newline at end of file diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs index 38302e38364..9f2accccd3b 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs @@ -40,8 +40,8 @@ internal PolicyDefinition[] ProcessAssignment(string assignmentFile, out PolicyA { var assignmentArray = ReadFileArray(rootedAssignmentFile); - foreach (JObject assignment in assignmentArray) - visitor.Visit(assignmentContext, assignment); + foreach (var assignment in assignmentArray) + visitor.Visit(assignmentContext, assignment.ToObject()); } catch (Exception inner) { From e158c8318760c19b5abe1e71d6cbda1a17cf3316 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Sun, 12 Jun 2022 23:29:41 +1000 Subject: [PATCH 12/16] More fixes --- .vscode/settings.json | 5 +++++ .../Data/Policy/PolicyAssignmentVisitor.cs | 20 +++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 917c05192cf..dc109a4cc5b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -43,6 +43,8 @@ "cmdlet", "cmdlets", "Concat", + "DEFAULTVALUE", + "DISPLAYNAME", "endregion", "failover", "GREATEROREQUAL", @@ -55,12 +57,15 @@ "LESSOREQUAL", "LESSOREQUALS", "lifecycle", + "Newtonsoft", "nics", "NOTCOUNT", "NOTEQUALS", + "NOTIN", "NSGs", "OWASP", "POLICYDEFINITIONID", + "POLICYRULE", "psarm", "PUBLICIP", "pwsh", diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index 56fcd2f59a4..e018f42f351 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -16,7 +16,7 @@ namespace PSRule.Rules.Azure.Data.Policy internal abstract class PolicyAssignmentVisitor { private const string PROPERTY_PARAMETERS = "parameters"; - private const string PROPERTY_DEFINTIONS = "policyDefinitions"; + private const string PROPERTY_DEFINITIONS = "policyDefinitions"; private const string PROPERTY_PROPERTIES = "properties"; private const string PROPERTY_POLICYRULE = "policyRule"; private const string PROPERTY_CONDITION = "if"; @@ -484,7 +484,6 @@ private void ExpandPolicyRule(JToken policyRule) { var splitAliasPath = outerFieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], fieldFilter); - policyRule[PROPERTY_COUNT].Parent.Remove(); } // nested allOf in where expression @@ -494,7 +493,6 @@ private void ExpandPolicyRule(JToken policyRule) var filter = new StringBuilder(); ExpressionToObjectPathArrayFilter(allofExpression, AND_CLAUSE, filter); policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); - policyRule[PROPERTY_COUNT].Parent.Remove(); } // nested anyOf in where expression @@ -504,27 +502,23 @@ private void ExpandPolicyRule(JToken policyRule) var filter = new StringBuilder(); ExpressionToObjectPathArrayFilter(anyOfExpression, OR_CLAUSE, filter); policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); - policyRule[PROPERTY_COUNT].Parent.Remove(); } } // Single field in count expression else - { policyRule[PROPERTY_FIELD] = outerFieldAliasPath; - policyRule[PROPERTY_COUNT].Parent.Remove(); - } + + // Remove the count property when we're done + policyRule[PROPERTY_COUNT].Parent.Remove(); } } } } // Convert string booleans for exists expression - else if (child.Name.Equals(FIELD_EXISTS, StringComparison.OrdinalIgnoreCase) - && child.Value.Type == JTokenType.String) - { + else if (child.Name.Equals(FIELD_EXISTS, StringComparison.OrdinalIgnoreCase) && child.Value.Type == JTokenType.String) policyRule[child.Name] = child.Value.Value(); - } // Expand string expressions else if (child.Value.Type == JTokenType.String) @@ -692,8 +686,8 @@ protected virtual void Assignment(PolicyAssignmentContext context, JObject assig VisitAssignmentParameters(context, parameters); } - // Assignment Defintions - if (assignment.TryArrayProperty(PROPERTY_DEFINTIONS, out var definitions)) + // Assignment Definitions + if (assignment.TryArrayProperty(PROPERTY_DEFINITIONS, out var definitions)) VisitDefinitions(context, definitions.Values()); } } From 1883f22b9fdb6d4bd39a90cbccfd10c1cd84d208 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Mon, 13 Jun 2022 19:18:42 +1000 Subject: [PATCH 13/16] Added refactoring --- .vscode/settings.json | 1 + .../Data/Policy/PolicyAssignmentVisitor.cs | 38 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1efc70bf2ff..54480b39836 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,6 +36,7 @@ "agentpool", "APIM", "apiserver", + "APIVERSION", "APPGW", "Architected", "AUTOMATIONACCOUNT", diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index e018f42f351..b3c70e97f7d 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -315,7 +315,6 @@ private string GetFieldObjectPathArrayFilter(JObject obj) && comparisonValue.Type == JTokenType.Array) { var filters = comparisonValue - .ToObject() .Select(val => FormatObjectPathArrayFilter(subProperty, EQUALITY_OPERATOR, val)); return string.Concat(GROUP_OPEN, string.Join($" {OR_CLAUSE} ", filters), GROUP_CLOSE); @@ -326,7 +325,6 @@ private string GetFieldObjectPathArrayFilter(JObject obj) && comparisonValue.Type == JTokenType.Array) { var filters = comparisonValue - .ToObject() .Select(val => FormatObjectPathArrayFilter(subProperty, INEQUALITY_OPERATOR, val)); return string.Concat(GROUP_OPEN, string.Join($" {AND_CLAUSE} ", filters), GROUP_CLOSE); @@ -404,12 +402,25 @@ private static string FormatObjectPathArrayFilter(string subProperty, string com value); } - private static int OrderPropertySelector(JProperty property) + /// + /// Comparer class which orders certain properties before others + /// + private sealed class PropertyNameComparer : IComparer { - return property.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase) - || property.Name.Equals(PROPERTY_COUNT, StringComparison.OrdinalIgnoreCase) - ? 0 - : 1; + public int Compare(JProperty x, JProperty y) + { + return OrderFirst(y) + ? 1 + : OrderFirst(x) + ? -1 + : string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); + } + + private static bool OrderFirst(JProperty prop) + { + return prop.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase) + || prop.Name.Equals(PROPERTY_COUNT, StringComparison.OrdinalIgnoreCase); + } } private void ExpandPolicyRule(JToken policyRule) @@ -420,7 +431,7 @@ private void ExpandPolicyRule(JToken policyRule) var hasFieldCount = false; // Go through each property and make sure fields and counts are sorted first - foreach (var child in policyRule.Children().OrderBy(OrderPropertySelector)) + foreach (var child in policyRule.Children().OrderBy(prop => prop, new PropertyNameComparer())) { // Expand field aliases if (child.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)) @@ -438,13 +449,12 @@ private void ExpandPolicyRule(JToken policyRule) } // Set policy rule type - else if (hasFieldType && child.Name.Equals(FIELD_EQUALS, StringComparison.OrdinalIgnoreCase)) + else if (hasFieldType + && child.Name.Equals(FIELD_EQUALS, StringComparison.OrdinalIgnoreCase) + && child.Value.Type == JTokenType.String) { - if (child.Value.Type == JTokenType.String) - { - var field = child.Value.Value(); - SetPolicyRuleType(field); - } + var field = child.Value.Value(); + SetPolicyRuleType(field); } // Replace equals with count if field count expression is currently being visited From 2c859ce16156c47373391fe6d424e479647a67c8 Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Mon, 13 Jun 2022 20:01:04 +1000 Subject: [PATCH 14/16] Fix tests --- .../Cmdlet.Common.Tests.ps1 | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 index ff960654b4e..ef03ad4bde7 100644 --- a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 @@ -740,23 +740,37 @@ Describe 'Get-AzPolicyAssignmentDataSource' -Tag 'Cmdlet', 'Get-AzPolicyAssignme } It 'Get assignment sources from current working directory' { - $sources = Get-AzPolicyAssignmentDataSource | Sort-Object -Property AssignmentFile - $sources.Length | Should -Be 3; + $sources = Get-AzPolicyAssignmentDataSource | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') } + $sources.Length | Should -Be 10; $sources[0].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test.assignment.json'); $sources[1].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test2.assignment.json'); $sources[2].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test3.assignment.json'); + $sources[3].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test4.assignment.json'); + $sources[4].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test5.assignment.json'); + $sources[5].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test6.assignment.json'); + $sources[6].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test7.assignment.json'); + $sources[7].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test8.assignment.json'); + $sources[8].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test9.assignment.json'); + $sources[9].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test10.assignment.json'); } It 'Get assignment sources from tests folder' { - $sources = Get-AzPolicyAssignmentDataSource -Path $here | Sort-Object -Property AssignmentFile; - $sources.Length | Should -Be 3; + $sources = Get-AzPolicyAssignmentDataSource -Path $here | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') } + $sources.Length | Should -Be 10; $sources[0].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test.assignment.json'); $sources[1].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test2.assignment.json'); $sources[2].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test3.assignment.json'); + $sources[3].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test4.assignment.json'); + $sources[4].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test5.assignment.json'); + $sources[5].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test6.assignment.json'); + $sources[6].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test7.assignment.json'); + $sources[7].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test8.assignment.json'); + $sources[8].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test9.assignment.json'); + $sources[9].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test10.assignment.json'); } It 'Pipe to Export-AzPolicyAssignmentRuleData and generate JSON rules' { - $result = @(Get-AzPolicyAssignmentDataSource | Sort-Object -Property AssignmentFile | Export-AzPolicyAssignmentRuleData -Name 'tests' -OutputPath $outputPath); + $result = @(Get-AzPolicyAssignmentDataSource | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') } | Export-AzPolicyAssignmentRuleData -Name 'tests' -OutputPath $outputPath); $result.Length | Should -Be 1; $result | Should -BeOfType System.IO.FileInfo; $filename = Split-Path -Path $result.FullName -Leaf; From c8691b1e08929a278de2906a2233ce103c6824cd Mon Sep 17 00:00:00 2001 From: ArmaanMcleod Date: Mon, 13 Jun 2022 20:32:21 +1000 Subject: [PATCH 15/16] Updated changelog --- docs/CHANGELOG-v1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 4e0372412c8..ee6b2e3b47c 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -25,6 +25,8 @@ What's changed since v1.16.1: - Deployment: - Check for secure values in outputs by @BernieWhite. [#297](https://github.com/Azure/PSRule.Rules.Azure/issues/297) +- New features: + - Added more field count expression support for Azure Policy JSON rules by @ArmaanMcleod. [microsoft/PSRule#1024](https://github.com/microsoft/PSRule/issues/1024) ## v1.16.1 From 0e0255d02c38a1efb7c296a84cddecebc0b30a7a Mon Sep 17 00:00:00 2001 From: Armaan Mcleod Date: Mon, 13 Jun 2022 23:16:38 +1000 Subject: [PATCH 16/16] Update docs/CHANGELOG-v1.md Co-authored-by: Bernie White --- docs/CHANGELOG-v1.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index ee6b2e3b47c..6d56b4362d0 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -26,7 +26,8 @@ What's changed since v1.16.1: - Check for secure values in outputs by @BernieWhite. [#297](https://github.com/Azure/PSRule.Rules.Azure/issues/297) - New features: - - Added more field count expression support for Azure Policy JSON rules by @ArmaanMcleod. [microsoft/PSRule#1024](https://github.com/microsoft/PSRule/issues/1024) + - Added more field count expression support for Azure Policy JSON rules by @ArmaanMcleod. + [#181](https://github.com/Azure/PSRule.Rules.Azure/issues/181) ## v1.16.1