diff --git a/.vscode/settings.json b/.vscode/settings.json index d93dfee379d..54480b39836 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,10 @@ "[yaml]": { "editor.tabSize": 2 }, + "[json]": { + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, "[markdown]": { "editor.tabSize": 2 }, @@ -32,6 +36,7 @@ "agentpool", "APIM", "apiserver", + "APIVERSION", "APPGW", "Architected", "AUTOMATIONACCOUNT", @@ -39,18 +44,29 @@ "cmdlet", "cmdlets", "Concat", + "DEFAULTVALUE", + "DISPLAYNAME", "endregion", "failover", + "GREATEROREQUAL", + "GREATEROREQUALS", "Hashtable", "kube", "kubelet", "kubenet", "Kubernetes", + "LESSOREQUAL", + "LESSOREQUALS", "lifecycle", + "Newtonsoft", "nics", + "NOTCOUNT", + "NOTEQUALS", + "NOTIN", "NSGs", "OWASP", "POLICYDEFINITIONID", + "POLICYRULE", "psarm", "PUBLICIP", "pwsh", diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 4e0372412c8..6d56b4362d0 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -25,6 +25,9 @@ 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. + [#181](https://github.com/Azure/PSRule.Rules.Azure/issues/181) ## v1.16.1 diff --git a/src/PSRule.Rules.Azure/Common/StringExtensions.cs b/src/PSRule.Rules.Azure/Common/StringExtensions.cs index 4de880ebe93..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 @@ -16,7 +17,17 @@ 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 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) 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/PolicyAssignmentHelper.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentHelper.cs index 6b4fbf31367..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) { @@ -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 e5f8410e63c..1d37954cf94 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; +using System.Threading; using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.Configuration; using PSRule.Rules.Azure.Data.Template; @@ -14,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"; @@ -28,12 +30,35 @@ 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"; + 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"; 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 = "&&"; + 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 = ')'; public sealed class PolicyAssignmentContext : ITemplateContext { @@ -217,37 +242,294 @@ private static void RemovePolicyRuleDeployment(JObject policyRule) } } + private static string ExpressionToObjectPathComparisonOperator(string expression) => expression switch + { + 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 + }; + + 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 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)) + { + subProperty = $".{PROPERTY_TYPE}"; + SetPolicyRuleType(fieldType); + } + else + { + var fieldAliasPath = ResolvePolicyAliasPath(fieldProperty); + if (fieldAliasPath != null) + { + var splitAliasPath = fieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); + subProperty = splitAliasPath[1]; + } + } + + var comparisonExpression = obj + .Children() + .FirstOrDefault(prop => !prop.Name.Equals(PROPERTY_FIELD, StringComparison.OrdinalIgnoreCase)); + + if (comparisonExpression != null) + { + 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); + + if (objectPathComparisonOperator != null) + { + return FormatObjectPathArrayFilter( + subProperty, + objectPathComparisonOperator, + comparisonValue); + } + else + { + // Convert in expression + if (comparisonExpression.Name.Equals(FIELD_IN, StringComparison.OrdinalIgnoreCase) + && comparisonValue.Type == JTokenType.Array) + { + var filters = comparisonValue + .Select(val => FormatObjectPathArrayFilter(subProperty, EQUALITY_OPERATOR, 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 + .Select(val => FormatObjectPathArrayFilter(subProperty, INEQUALITY_OPERATOR, 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; + } + + 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) + { + return value == null + ? string.Format( + Thread.CurrentThread.CurrentCulture, + "@{0} {1} null", + subProperty, + comparisonOperator) + : string.Format( + Thread.CurrentThread.CurrentCulture, + value.Type == JTokenType.String ? "@{0} {1} '{2}'" : "@{0} {1} {2}", + subProperty, + comparisonOperator, + value); + } + + /// + /// Comparer class which orders certain properties before others + /// + private sealed class PropertyNameComparer : IComparer + { + 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) { if (policyRule.Type == JTokenType.Object) { var hasFieldType = false; - foreach (var child in policyRule.Children()) + var hasFieldCount = false; + + // Go through each property and make sure fields and counts are sorted first + foreach (var child in policyRule.Children().OrderBy(prop => prop, new PropertyNameComparer())) { // 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; + } } - else if (hasFieldType && child.Name.Equals(FIELD_EQUALS, StringComparison.OrdinalIgnoreCase)) + // Set policy rule type + else if (hasFieldType + && child.Name.Equals(FIELD_EQUALS, StringComparison.OrdinalIgnoreCase) + && child.Value.Type == JTokenType.String) { var field = child.Value.Value(); - if (field.CountCharacterOccurrences(SLASH) == 1) + SetPolicyRuleType(field); + } + + // 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; + } + + // 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)) + { + 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)) + { + // field in where expression + var fieldFilter = GetFieldObjectPathArrayFilter(whereExpression); + if (fieldFilter != null) + { + var splitAliasPath = outerFieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); + policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], fieldFilter); + } + + // nested allOf in where expression + else if (whereExpression.TryArrayProperty(PROPERTY_ALL_OF, out var allofExpression)) + { + var splitAliasPath = outerFieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); + var filter = new StringBuilder(); + ExpressionToObjectPathArrayFilter(allofExpression, AND_CLAUSE, filter); + policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); + } + + // nested anyOf in where expression + else if (whereExpression.TryArrayProperty(PROPERTY_ANY_OF, out var anyOfExpression)) + { + var splitAliasPath = outerFieldAliasPath.SplitByLastSubstring(COLLECTION_ALIAS); + var filter = new StringBuilder(); + ExpressionToObjectPathArrayFilter(anyOfExpression, OR_CLAUSE, filter); + policyRule[PROPERTY_FIELD] = FormatObjectPathArrayExpression(splitAliasPath[0], filter.ToString()); + } + } + + // Single field in count expression + else + policyRule[PROPERTY_FIELD] = outerFieldAliasPath; + + // 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) + policyRule[child.Name] = child.Value.Value(); + // Expand string expressions else if (child.Value.Type == JTokenType.String) { @@ -419,8 +701,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()); } } diff --git a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 index 9241f519483..ef03ad4bde7 100644 --- a/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1 @@ -680,6 +680,41 @@ 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') + }, + @{ + 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) $result = @(Export-AzPolicyAssignmentRuleData -Name $Name -AssignmentFile $AssignmentFile -OutputPath $outputPath); @@ -705,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; diff --git a/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc b/tests/PSRule.Rules.Azure.Tests/emittedJsonRulesData.jsonc index 62ed9f2734a..f7ec64a5b8a 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", @@ -361,5 +361,212 @@ ] } } + }, + { + // 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')]" + } + ] + } + ] + } + } + }, + { + // 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" + }, + { + "field": "properties.subnets[?@.properties.routeTable.id == null && (@.name != 'AzureFirewallManagementSubnet' && @.name != 'AzureFirewallSubnet')]", + "notCount": 0 + } + ] + } + ] + } + } + }, + { + // 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 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", 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..b8982622ba7 --- /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" + } + ] + } +] \ No newline at end of file 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 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..8c492c09687 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/test7.assignment.json @@ -0,0 +1,99 @@ +[ + { + "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": { + "portNumber": { + "type": "String", + "metadata": { + "displayName": "Port Number", + "description": "The port number which to block inbound access" + }, + "defaultValue": "3389" + } + }, + "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": "[parameters('portNumber')]" + } + ] + } + }, + "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 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