From 036db3dfaa0a366b992599d5bff909e0409d1d05 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 4 Mar 2024 01:37:43 +1000 Subject: [PATCH] Fixes for policy as rule and policy ignore #1731 #181 #1323 (#2720) --- .vscode/markdown.code-snippets | 10 + .vscode/yaml.code-snippets | 1 + data/policy-ignore.json | 38 ++- docs/CHANGELOG-v1.md | 7 + .../rules/Azure.Cognitive.DisableLocalAuth.md | 8 +- docs/en/rules/Azure.ContainerApp.Insecure.md | 15 +- .../Azure.ContainerApp.ManagedIdentity.md | 20 +- docs/en/rules/Azure.Storage.BlobAccessType.md | 34 ++- .../rules/Azure.Storage.BlobPublicAccess.md | 19 +- docs/en/rules/Azure.Storage.MinTLS.md | 12 +- docs/en/rules/Azure.Storage.SecureTransfer.md | 15 +- .../Data/Policy/PolicyAssignmentVisitor.cs | 112 ++++++- .../Data/Template/TokenStream.cs | 8 + .../PSRule.Rules.Azure.Tests.csproj | 3 + .../Policy.assignment.5.json | 273 ++++++++++++++++++ .../PolicyAssignmentVisitorTests.cs | 23 +- .../PolicyIgnoreDataTests.cs | 4 +- 17 files changed, 548 insertions(+), 54 deletions(-) create mode 100644 tests/PSRule.Rules.Azure.Tests/Policy.assignment.5.json diff --git a/.vscode/markdown.code-snippets b/.vscode/markdown.code-snippets index 5d50858338..7fc4c3250b 100644 --- a/.vscode/markdown.code-snippets +++ b/.vscode/markdown.code-snippets @@ -140,5 +140,15 @@ "", "```" ] + }, + "rule-azure-example-policy": { + "scope": "markdown", + "prefix": "rule-azure-example-policy", + "description": "Example for Azure Policy", + "body": [ + "### Configure with Azure Policy", + "", + "To address this issue at runtime use the following policies:" + ] } } diff --git a/.vscode/yaml.code-snippets b/.vscode/yaml.code-snippets index 8f73ff0228..6d7482cdb7 100644 --- a/.vscode/yaml.code-snippets +++ b/.vscode/yaml.code-snippets @@ -1,5 +1,6 @@ { "Azure rule with type": { + "scope": "yaml", "prefix": "rule-azure-with-type", "description": "Rule definition for Azure", "body": [ diff --git a/data/policy-ignore.json b/data/policy-ignore.json index 731da75371..e219be8457 100644 --- a/data/policy-ignore.json +++ b/data/policy-ignore.json @@ -165,6 +165,42 @@ "/providers/Microsoft.Authorization/policyDefinitions/cfdc5972-75b3-4418-8ae1-7f5c36839390" ], "reason": "Duplicate", - "value": "Azure.Defender.Storage.SensitiveData" + "value": "Azure.Defender.Storage.DataScan" + }, + { + "policyDefinitionIds": [ + "/providers/Microsoft.Authorization/policyDefinitions/0e80e269-43a4-4ae9-b5bc-178126b8a5cb" + ], + "reason": "Duplicate", + "value": "Azure.ContainerApp.Insecure" + }, + { + "policyDefinitionIds": [ + "/providers/Microsoft.Authorization/policyDefinitions/b874ab2d-72dd-47f1-8cb5-4a306478a4e7" + ], + "reason": "Duplicate", + "value": "Azure.ContainerApp.ManagedIdentity" + }, + { + "policyDefinitionIds": [ + "/providers/Microsoft.Authorization/policyDefinitions/13502221-8df0-4414-9937-de9c5c4e396b" + ], + "reason": "Duplicate", + "value": "Azure.Storage.BlobPublicAccess" + }, + { + "policyDefinitionIds": [ + "/providers/Microsoft.Authorization/policyDefinitions/404c3081-a854-4457-ae30-26a93ef643f9", + "/providers/Microsoft.Authorization/policyDefinitions/f81e3117-0093-4b17-8a60-82363134f0eb" + ], + "reason": "Duplicate", + "value": "Azure.Storage.SecureTransfer" + }, + { + "policyDefinitionIds": [ + "/providers/Microsoft.Authorization/policyDefinitions/fe83a0eb-a853-422d-aac2-1bffd182c5d0" + ], + "reason": "Duplicate", + "value": "Azure.Storage.MinTLS" } ] diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index b044784c35..2c33ce19be 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -48,9 +48,16 @@ What's changed since pre-release v1.34.0-B0047: - Renamed `Azure.Storage.DefenderCloud.SensitiveData` to `Azure.Storage.Defender.DataScan`. - Promoted `Azure.Storage.Defender.MalwareScan` to GA rule set by @BernieWhite. [#2590](https://github.com/Azure/PSRule.Rules.Azure/pull/2590) +- General improvements: + - Added duplicate policies to default ignore list by @BernieWhite. + [#1731](https://github.com/Azure/PSRule.Rules.Azure/issues/1731) - Engineering: - Updated resource providers and policy aliases. [#2717](https://github.com/Azure/PSRule.Rules.Azure/pull/2717) +- Bug fixes: + - Fixes for policy as rules by @BernieWhite. + [#181](https://github.com/Azure/PSRule.Rules.Azure/issues/181) + [#1323](https://github.com/Azure/PSRule.Rules.Azure/issues/1323) ## v1.34.0-B0047 (pre-release) diff --git a/docs/en/rules/Azure.Cognitive.DisableLocalAuth.md b/docs/en/rules/Azure.Cognitive.DisableLocalAuth.md index 025a0d0e3a..cb159b4731 100644 --- a/docs/en/rules/Azure.Cognitive.DisableLocalAuth.md +++ b/docs/en/rules/Azure.Cognitive.DisableLocalAuth.md @@ -95,10 +95,10 @@ resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { To address this issue at runtime use the following policies: -```text -/providers/Microsoft.Authorization/policyDefinitions/71ef260a-8f18-47b7-abcb-62d0673d94dc -/providers/Microsoft.Authorization/policyDefinitions/14de9e63-1b31-492e-a5a3-c3f7fd57f555 -``` +- [Azure AI Services resources should have key access disabled (disable local authentication)](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Azure%20Ai%20Services/CognitiveServices_DisableLocalAuth_Audit.json) + `/providers/Microsoft.Authorization/policyDefinitions/71ef260a-8f18-47b7-abcb-62d0673d94dc` +- [Configure Cognitive Services accounts to disable local authentication methods](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Cognitive%20Services/CognitiveServices_DisableLocalAuth_Modify.json) + `/providers/Microsoft.Authorization/policyDefinitions/14de9e63-1b31-492e-a5a3-c3f7fd57f555` ## LINKS diff --git a/docs/en/rules/Azure.ContainerApp.Insecure.md b/docs/en/rules/Azure.ContainerApp.Insecure.md index 2012e543c2..a3fb28e6d1 100644 --- a/docs/en/rules/Azure.ContainerApp.Insecure.md +++ b/docs/en/rules/Azure.ContainerApp.Insecure.md @@ -1,8 +1,8 @@ --- -reviewed: 2023-04-29 +reviewed: 2024-03-04 severity: Important pillar: Security -category: Design +category: SE:07 Encryption resource: Container App online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ContainerApp.Insecure/ --- @@ -98,9 +98,16 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { } ``` +### Configure with Azure Policy + +To address this issue at runtime use the following policies: + +- [Container Apps should only be accessible over HTTPS](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Container%20Apps/ContainerApps_EnableHTTPS_Audit.json) + `/providers/Microsoft.Authorization/policyDefinitions/0e80e269-43a4-4ae9-b5bc-178126b8a5cb` + ## LINKS -- [Data encryption in Azure](https://learn.microsoft.com/azure/architecture/framework/security/design-storage-encryption#data-in-transit) -- [Ingress in Azure Container Apps](https://learn.microsoft.com/azure/container-apps/ingress-overview#configuration) +- [SE:07 Encryption](https://learn.microsoft.com/azure/well-architected/security/encryption#data-in-transit) +- [Ingress in Azure Container Apps](https://learn.microsoft.com/azure/container-apps/ingress-overview) - [Container Apps ARM template API specification](https://learn.microsoft.com/azure/container-apps/azure-resource-manager-api-spec?tabs=arm-template) - [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.app/containerapps) diff --git a/docs/en/rules/Azure.ContainerApp.ManagedIdentity.md b/docs/en/rules/Azure.ContainerApp.ManagedIdentity.md index fd9241351e..f9539bca63 100644 --- a/docs/en/rules/Azure.ContainerApp.ManagedIdentity.md +++ b/docs/en/rules/Azure.ContainerApp.ManagedIdentity.md @@ -1,7 +1,7 @@ --- severity: Important pillar: Security -category: Authentication +category: SE:05 Identity and access management resource: Container App online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ContainerApp.ManagedIdentity/ --- @@ -16,10 +16,13 @@ Ensure managed identity is used for authentication. Using managed identities have the following benefits: -- Your app connects to resources with the managed identity. You don't need to manage credentials in your container app. +- Your app connects to resources with the managed identity. + You don't need to manage credentials in your container app. - You can use role-based access control to grant specific permissions to a managed identity. -- System-assigned identities are automatically created and managed. They're deleted when your container app is deleted. -- You can add and delete user-assigned identities and assign them to multiple resources. They're independent of your container app's life cycle. +- System-assigned identities are automatically created and managed. + They're deleted when your container app is deleted. +- You can add and delete user-assigned identities and assign them to multiple resources. + They're independent of your container app's life cycle. - You can use managed identity to authenticate with a private Azure Container Registry without a username and password to pull containers for your Container App. - You can use managed identity to create connections for Dapr-enabled applications via Dapr components. @@ -102,6 +105,13 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { } ``` +### Configure with Azure Policy + +To address this issue at runtime use the following policies: + +- [Managed Identity should be enabled for Container Apps](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Container%20Apps/ContainerApps_ManagedIdentity_Audit.json) + `/providers/Microsoft.Authorization/policyDefinitions/b874ab2d-72dd-47f1-8cb5-4a306478a4e7` + ## NOTES Using managed identities in scale rules isn't supported. @@ -109,6 +119,6 @@ Init containers can't access managed identities. ## LINKS -- [Use identity-based authentication](https://learn.microsoft.com/azure/well-architected/security/design-identity-authentication#use-identity-based-authentication) +- [SE:05 Identity and access management](https://learn.microsoft.com/azure/well-architected/security/identity-access) - [Managed identities in Azure Container Apps](https://learn.microsoft.com/azure/container-apps/managed-identity) - [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.app/containerapps#managedserviceidentity) diff --git a/docs/en/rules/Azure.Storage.BlobAccessType.md b/docs/en/rules/Azure.Storage.BlobAccessType.md index f3ac8e0153..cdb85d0143 100644 --- a/docs/en/rules/Azure.Storage.BlobAccessType.md +++ b/docs/en/rules/Azure.Storage.BlobAccessType.md @@ -1,8 +1,8 @@ --- -reviewed: 2022-01-20 +reviewed: 2024-03-04 severity: Important pillar: Security -category: Authentication +category: SE:05 Identity and access management resource: Storage Account online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.Storage.BlobAccessType/ --- @@ -40,16 +40,16 @@ For example: ```json { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2021-06-01", - "name": "[format('{0}/{1}/{2}', parameters('name'), 'default', variables('containerName'))]", - "properties": { - "publicAccess": "None" - }, - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('name'), 'default')]", - "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" - ] + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2021-06-01", + "name": "[format('{0}/{1}/{2}', parameters('name'), 'default', variables('containerName'))]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('name'), 'default')]", + "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + ] } ``` @@ -73,8 +73,10 @@ resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@20 ## LINKS -- [Authentication with Azure AD](https://learn.microsoft.com/azure/architecture/framework/security/design-identity-authentication) -- [About anonymous public read access](https://docs.microsoft.com/azure/storage/blobs/anonymous-read-access-configure#about-anonymous-public-read-access) -- [Use Azure Policy to enforce authorized access](https://docs.microsoft.com/azure/storage/blobs/anonymous-read-access-prevent#use-azure-policy-to-enforce-authorized-access) -- [How a shared access signature works](https://docs.microsoft.com/azure/storage/common/storage-sas-overview#how-a-shared-access-signature-works) +- [SE:05 Identity and access management](https://learn.microsoft.com/azure/well-architected/security/identity-access) +- [Use Microsoft Entra ID for storage authentication](https://learn.microsoft.com/azure/security/fundamentals/identity-management-best-practices#use-microsoft-entra-id-for-storage-authentication) +- [Configure anonymous read access for containers and blobs](https://learn.microsoft.com/azure/storage/blobs/anonymous-read-access-configure) +- [Remediate anonymous read access to blob data](https://learn.microsoft.com/azure/storage/blobs/anonymous-read-access-prevent) +- [How a shared access signature works](https://learn.microsoft.com/azure/storage/common/storage-sas-overview#how-a-shared-access-signature-works) +- [Authorize access to blobs using Microsoft Entra ID](https://learn.microsoft.com/azure/storage/blobs/authorize-access-azure-active-directory) - [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.storage/storageaccounts) diff --git a/docs/en/rules/Azure.Storage.BlobPublicAccess.md b/docs/en/rules/Azure.Storage.BlobPublicAccess.md index 02b74e2253..a85e447ed2 100644 --- a/docs/en/rules/Azure.Storage.BlobPublicAccess.md +++ b/docs/en/rules/Azure.Storage.BlobPublicAccess.md @@ -1,7 +1,7 @@ --- severity: Important pillar: Security -category: Authentication +category: SE:05 Identity and access management resource: Storage Account online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.Storage.BlobPublicAccess/ --- @@ -89,11 +89,18 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { } ``` +### Configure with Azure Policy + +To address this issue at runtime use the following policies: + +- [Configure your Storage account public access to be disallowed](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Storage/StorageAccountDisablePublicBlobAccess_Modify.json) + `/providers/Microsoft.Authorization/policyDefinitions/13502221-8df0-4414-9937-de9c5c4e396b` + ## LINKS -- [Use Azure AD for storage authentication](https://docs.microsoft.com/azure/security/fundamentals/identity-management-best-practices#use-azure-ad-for-storage-authentication) -- [Allow or disallow public read access for a storage account](https://docs.microsoft.com/azure/storage/blobs/anonymous-read-access-configure#allow-or-disallow-public-read-access-for-a-storage-account) -- [Remediate anonymous public access](https://docs.microsoft.com/azure/storage/blobs/anonymous-read-access-prevent#remediate-anonymous-public-access) -- [Use Azure Policy to enforce authorized access](https://docs.microsoft.com/azure/storage/blobs/anonymous-read-access-prevent#use-azure-policy-to-enforce-authorized-access) -- [Authorize access to blobs using Azure Active Directory](https://docs.microsoft.com/azure/storage/blobs/authorize-access-azure-active-directory) +- [SE:05 Identity and access management](https://learn.microsoft.com/azure/well-architected/security/identity-access) +- [Use Microsoft Entra ID for storage authentication](https://learn.microsoft.com/azure/security/fundamentals/identity-management-best-practices#use-microsoft-entra-id-for-storage-authentication) +- [Configure anonymous read access for containers and blobs](https://learn.microsoft.com/azure/storage/blobs/anonymous-read-access-configure) +- [Remediate anonymous read access to blob data](https://learn.microsoft.com/azure/storage/blobs/anonymous-read-access-prevent) +- [Authorize access to blobs using Microsoft Entra ID](https://learn.microsoft.com/azure/storage/blobs/authorize-access-azure-active-directory) - [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.storage/storageaccounts) diff --git a/docs/en/rules/Azure.Storage.MinTLS.md b/docs/en/rules/Azure.Storage.MinTLS.md index cc5f14a141..4bc99a4311 100644 --- a/docs/en/rules/Azure.Storage.MinTLS.md +++ b/docs/en/rules/Azure.Storage.MinTLS.md @@ -1,7 +1,8 @@ --- +reviewed: 2024-03-04 severity: Critical pillar: Security -category: Encryption +category: SE:07 Encryption resource: Storage Account online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.Storage.MinTLS/ --- @@ -87,9 +88,16 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { } ``` +### Configure with Azure Policy + +To address this issue at runtime use the following policies: + +- [Storage accounts should have the specified minimum TLS version](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Storage/StorageAccountMinimumTLSVersion_Audit.json) + `/providers/Microsoft.Authorization/policyDefinitions/fe83a0eb-a853-422d-aac2-1bffd182c5d0` + ## LINKS -- [Data encryption in Azure](https://learn.microsoft.com/azure/architecture/framework/security/design-storage-encryption#data-in-transit) +- [SE:07 Encryption](https://learn.microsoft.com/azure/well-architected/security/encryption#data-in-transit) - [TLS encryption in Azure](https://learn.microsoft.com/azure/security/fundamentals/encryption-overview#tls-encryption-in-azure) - [Enforce a minimum required version of Transport Layer Security (TLS) for requests to a storage account](https://learn.microsoft.com/azure/storage/common/transport-layer-security-configure-minimum-version) - [DP-3: Encrypt sensitive data in transit](https://learn.microsoft.com/security/benchmark/azure/baselines/storage-security-baseline#dp-3-encrypt-sensitive-data-in-transit) diff --git a/docs/en/rules/Azure.Storage.SecureTransfer.md b/docs/en/rules/Azure.Storage.SecureTransfer.md index ed19e4749b..c5e2480eed 100644 --- a/docs/en/rules/Azure.Storage.SecureTransfer.md +++ b/docs/en/rules/Azure.Storage.SecureTransfer.md @@ -1,8 +1,8 @@ --- -reviewed: 2023-09-02 +reviewed: 2024-03-04 severity: Important pillar: Security -category: Encryption +category: SE:07 Encryption resource: Storage Account online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.Storage.SecureTransfer/ ms-content-id: 539cb7b9-5510-4aa3-b422-41a049a10a88 @@ -101,9 +101,18 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { } ``` +### Configure with Azure Policy + +To address this issue at runtime use the following policies: + +- [Secure transfer to storage accounts should be enabled](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Storage/Storage_AuditForHTTPSEnabled_Audit.json) + `/providers/Microsoft.Authorization/policyDefinitions/404c3081-a854-4457-ae30-26a93ef643f9` +- [Configure secure transfer of data on a storage account](https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/Storage/StorageAccountSecureTransfer_Modify.json) + `/providers/Microsoft.Authorization/policyDefinitions/f81e3117-0093-4b17-8a60-82363134f0eb` + ## LINKS -- [Data encryption in Azure](https://learn.microsoft.com/azure/architecture/framework/security/design-storage-encryption#data-in-transit) +- [SE:07 Encryption](https://learn.microsoft.com/azure/well-architected/security/encryption#data-in-transit) - [Require secure transfer in Azure Storage](https://learn.microsoft.com/azure/storage/common/storage-require-secure-transfer) - [DP-3: Encrypt sensitive data in transit](https://learn.microsoft.com/security/benchmark/azure/baselines/storage-security-baseline#dp-3-encrypt-sensitive-data-in-transit) - [Sample policy for ensuring https traffic](https://learn.microsoft.com/azure/governance/policy/samples/built-in-policies#storage) diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs index 423c05a73b..9429a29820 100644 --- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs @@ -1001,7 +1001,7 @@ namespace PSRule.Rules.Azure.Data.Policy { if (condition.TryStringProperty(PROPERTY_VALUE, out var s) && s.IsExpressionString()) { - VisitValueExpression(context, condition, s); + condition = VisitValueExpression(context, condition, s); } else if (condition.TryStringProperty(PROPERTY_FIELD, out var field)) { @@ -1248,7 +1248,7 @@ namespace PSRule.Rules.Azure.Data.Policy } } - private static void VisitValueExpression(PolicyAssignmentContext context, JObject condition, string s) + private static JObject VisitValueExpression(PolicyAssignmentContext context, JObject condition, string s) { var tokens = ExpressionParser.Parse(s); @@ -1288,7 +1288,24 @@ namespace PSRule.Rules.Azure.Data.Policy } // Handle [field('string')] - else if (tokens.ConsumeFunction(PROPERTY_FIELD) && + else if (tokens.HasFieldTokens()) + { + condition = VisitFieldTokens(context, condition, tokens); + } + + // Handle runtime token + else if (tokens.HasPolicyRuntimeTokens()) + { + var value = VisitRuntimeTokens(context, tokens); + if (value != null) + condition.ReplaceProperty(PROPERTY_VALUE, value); + } + return condition; + } + + private static JObject VisitFieldTokens(PolicyAssignmentContext context, JObject condition, TokenStream tokens) + { + if (tokens.ConsumeFunction(PROPERTY_FIELD) && tokens.TryTokenType(ExpressionTokenType.GroupStart, out _) && tokens.ConsumeString(out var field) && tokens.TryTokenType(ExpressionTokenType.GroupEnd, out _)) @@ -1302,17 +1319,68 @@ namespace PSRule.Rules.Azure.Data.Policy else { condition.Remove(PROPERTY_VALUE); + + field = context.TryPolicyAliasPath(field, out var aliasPath) ? TrimFieldName(context, aliasPath) : field; condition.Add(PROPERTY_FIELD, field); } } - // Handle runtime token - else if (tokens.HasPolicyRuntimeTokens()) + else if (tokens.ConsumeFunction("if") && + tokens.TryTokenType(ExpressionTokenType.GroupStart, out _)) { - var value = VisitRuntimeTokens(context, tokens); - if (value != null) - condition.ReplaceProperty(PROPERTY_VALUE, value); + var orginal = condition; + + // Condition + var leftCondition = VisitFieldTokens(context, new JObject(), tokens); + var rightCondition = ReverseCondition(Clone(leftCondition)); + + var leftEvaluation = VisitFieldTokens(context, Clone(orginal), tokens); + var rightEvaluation = VisitFieldTokens(context, Clone(orginal), tokens); + + var left = new JObject + { + { PROPERTY_FIELD, DOT }, + { PROPERTY_WHERE, leftCondition }, + { PROPERTY_ALLOF, new JArray(new[] { leftEvaluation }) } + }; + + var right = new JObject + { + { PROPERTY_FIELD, DOT }, + { PROPERTY_WHERE, rightCondition }, + { PROPERTY_ALLOF, new JArray(new[] { rightEvaluation }) } + }; + + var result = OrCondition(left, right); + condition.Replace(result); + tokens.TryTokenType(ExpressionTokenType.GroupEnd, out _); + return result; } + + else if (tokens.ConsumeFunction(PROPERTY_EQUALS) && + tokens.TryTokenType(ExpressionTokenType.GroupStart, out _)) + { + VisitFieldTokens(context, condition, tokens); + if (tokens.ConsumeString(out var s)) + { + condition.Add(PROPERTY_EQUALS, s); + } + + tokens.TryTokenType(ExpressionTokenType.GroupEnd, out _); + } + + else if (tokens.ConsumeFunction("empty") && + tokens.TryTokenType(ExpressionTokenType.GroupStart, out _)) + { + if (condition.TryBoolProperty(PROPERTY_EQUALS, out var emptyEquals)) + { + condition.Remove(PROPERTY_EQUALS); + condition.Add("hasValue", !emptyEquals.Value); + } + VisitFieldTokens(context, condition, tokens); + tokens.TryTokenType(ExpressionTokenType.GroupEnd, out _); + } + return condition; } private static JObject VisitRuntimeTokens(PolicyAssignmentContext context, TokenStream tokens) @@ -1540,6 +1608,11 @@ namespace PSRule.Rules.Azure.Data.Policy return condition; } + private static JObject Clone(JObject o) + { + return o.DeepClone() as JObject; + } + private static JObject AlwaysFail(string effect) { return new JObject @@ -1564,7 +1637,7 @@ namespace PSRule.Rules.Azure.Data.Policy /// private static JObject DefaultEffectConditions(PolicyAssignmentContext context, JObject details) { - return AndNameCondition(details, TypeExpression(context, details)); + return AndNameCondition(context, details, TypeExpression(context, details)); } private static JObject TypeExpression(PolicyAssignmentContext context, JObject details) @@ -1612,11 +1685,13 @@ namespace PSRule.Rules.Azure.Data.Policy return existenceCondition; } - private static JObject AndNameCondition(JObject details, JObject condition) + private static JObject AndNameCondition(PolicyAssignmentContext context, JObject details, JObject condition) { if (details == null || !details.TryStringProperty(PROPERTY_NAME, out var name)) return condition; + name = TemplateVisitor.ExpandString(context, name); + var nameCondition = new JObject { { PROPERTY_NAME, DOT }, { PROPERTY_EQUALS, name } @@ -1641,6 +1716,23 @@ namespace PSRule.Rules.Azure.Data.Policy return left == null || left.Count == 0 ? right : left; } + private static JObject OrCondition(JObject left, JObject right) + { + if (left != null && left.Count > 0 && right != null && right.Count > 0) + { + var allOf = new JArray + { + left, + right + }; + return new JObject + { + { PROPERTY_ANYOF, allOf } + }; + } + return left == null || left.Count == 0 ? right : left; + } + private static bool TryPolicyRuleEffect(PolicyAssignmentContext context, JObject then, out string effect) { ResolveObject(context, then); diff --git a/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs b/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs index cf93ac7ee4..8534ee0a02 100644 --- a/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs +++ b/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs @@ -202,6 +202,14 @@ namespace PSRule.Rules.Azure.Data.Template return true; } + internal static bool HasFieldTokens(this TokenStream stream) + { + return stream.ToArray().Any(t => + t.Type == ExpressionTokenType.Element && + string.Equals("field", t.Content, StringComparison.OrdinalIgnoreCase) + ); + } + internal static bool HasPolicyRuntimeTokens(this TokenStream stream) { return stream.ToArray().Any(t => diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj index c4484667f8..af0b4c54c1 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -41,6 +41,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Rules.Azure.Tests/Policy.assignment.5.json b/tests/PSRule.Rules.Azure.Tests/Policy.assignment.5.json new file mode 100644 index 0000000000..7ee4a33fa5 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Policy.assignment.5.json @@ -0,0 +1,273 @@ +[ + { + "Identity": null, + "Location": null, + "Name": "assignment.5", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/assignment.5", + "ResourceName": "assignment.5", + "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/assignment.5", + "Properties": { + "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000", + "NotScopes": [], + "DisplayName": "Deploy Diagnostic Settings for Key Vault to Log Analytics workspace", + "Description": null, + "Metadata": { + "assignedBy": "", + "parameterScopes": {}, + "createdBy": "", + "createdOn": "", + "updatedBy": null, + "updatedOn": null + }, + "EnforcementMode": 1, + "PolicyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/bef3f64c-5290-43b7-85b0-9b254eef4c47", + "Parameters": { + "logAnalytics": { + "value": "test_workspace_id" + } + }, + "NonComplianceMessages": [] + }, + "PolicyDefinitions": [ + { + "Name": "bef3f64c-5290-43b7-85b0-9b254eef4c47", + "ResourceName": "bef3f64c-5290-43b7-85b0-9b254eef4c47", + "ResourceType": "Microsoft.Authorization/policyDefinitions", + "ResourceId": "/providers/Microsoft.Authorization/policyDefinitions/bef3f64c-5290-43b7-85b0-9b254eef4c47", + "SubscriptionId": null, + "Properties": { + "DisplayName": "Deploy Diagnostic Settings for Key Vault to Log Analytics workspace", + "PolicyType": 2, + "Mode": "Indexed", + "Description": "Deploys the diagnostic settings for Key Vault to stream to a regional Log Analytics workspace when any Key Vault which is missing this diagnostic settings is created or updated.", + "Metadata": { + "version": "3.0.0", + "category": "Monitoring" + }, + "Version": "3.0.0", + "Parameters": { + "effect": { + "type": "String", + "metadata": { + "displayName": "Effect", + "description": "Enable or disable the execution of the policy" + }, + "allowedValues": [ + "DeployIfNotExists", + "Disabled" + ], + "defaultValue": "DeployIfNotExists" + }, + "profileName": { + "type": "String", + "metadata": { + "displayName": "Profile name", + "description": "The diagnostic settings profile name" + }, + "defaultValue": "setbypolicy_logAnalytics" + }, + "logAnalytics": { + "type": "String", + "metadata": { + "displayName": "Log Analytics workspace", + "description": "Select Log Analytics workspace from dropdown list. If this workspace is outside of the scope of the assignment you must manually grant 'Log Analytics Contributor' permissions (or similar) to the policy assignment's principal ID.", + "strongType": "omsWorkspace", + "assignPermissions": true + } + }, + "metricsEnabled": { + "type": "String", + "metadata": { + "displayName": "Enable metrics", + "description": "Whether to enable metrics stream to the Log Analytics workspace - True or False" + }, + "allowedValues": [ + "True", + "False" + ], + "defaultValue": "False" + }, + "logsEnabled": { + "type": "String", + "metadata": { + "displayName": "Enable logs", + "description": "Whether to enable logs stream to the Log Analytics workspace - True or False" + }, + "allowedValues": [ + "True", + "False" + ], + "defaultValue": "True" + }, + "matchWorkspace": { + "type": "Boolean", + "metadata": { + "displayName": "Workspace id must match", + "description": "Whether to require that the workspace of the diagnostic settings matches the workspace deployed by this policy" + }, + "allowedValues": [ + true, + false + ], + "defaultValue": false + } + }, + "PolicyRule": { + "if": { + "field": "type", + "equals": "Microsoft.KeyVault/vaults" + }, + "then": { + "effect": "[parameters('effect')]", + "details": { + "type": "Microsoft.Insights/diagnosticSettings", + "name": "[parameters('profileName')]", + "evaluationDelay": "AfterProvisioning", + "existenceCondition": { + "allOf": [ + { + "count": { + "field": "Microsoft.Insights/diagnosticSettings/logs[*]", + "where": { + "allOf": [ + { + "field": "Microsoft.Insights/diagnosticSettings/logs[*].enabled", + "equals": "[parameters('logsEnabled')]" + }, + { + "field": "Microsoft.Insights/diagnosticSettings/logs[*].category", + "equals": "AuditEvent" + } + ] + } + }, + "greaterOrEquals": 1 + }, + { + "count": { + "field": "Microsoft.Insights/diagnosticSettings/metrics[*]", + "where": { + "field": "Microsoft.Insights/diagnosticSettings/metrics[*].enabled", + "equals": "[parameters('metricsEnabled')]" + } + }, + "greaterOrEquals": 1 + }, + { + "anyOf": [ + { + "value": "[parameters('matchWorkspace')]", + "equals": false + }, + { + "field": "Microsoft.Insights/diagnosticSettings/workspaceId", + "equals": "[parameters('logAnalytics')]" + } + ] + } + ] + }, + "roleDefinitionIds": [ + "/providers/microsoft.authorization/roleDefinitions/749f88d5-cbae-40b8-bcfc-e573ddc772fa", + "/providers/microsoft.authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293" + ], + "deployment": { + "properties": { + "mode": "incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceName": { + "type": "string" + }, + "location": { + "type": "string" + }, + "logAnalytics": { + "type": "string" + }, + "metricsEnabled": { + "type": "string" + }, + "logsEnabled": { + "type": "string" + }, + "profileName": { + "type": "string" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults/providers/diagnosticSettings", + "apiVersion": "2017-05-01-preview", + "name": "[concat(parameters('resourceName'), '/', 'Microsoft.Insights/', parameters('profileName'))]", + "location": "[parameters('location')]", + "dependsOn": [], + "properties": { + "workspaceId": "[parameters('logAnalytics')]", + "metrics": [ + { + "category": "AllMetrics", + "enabled": "[parameters('metricsEnabled')]", + "retentionPolicy": { + "enabled": false, + "days": 0 + } + } + ], + "logs": [ + { + "category": "AuditEvent", + "enabled": "[parameters('logsEnabled')]" + }, + { + "category": "AzurePolicyEvaluationDetails", + "enabled": "[parameters('logsEnabled')]" + } + ] + } + } + ], + "outputs": {} + }, + "parameters": { + "location": { + "value": "[field('location')]" + }, + "resourceName": { + "value": "[field('name')]" + }, + "logAnalytics": { + "value": "[parameters('logAnalytics')]" + }, + "metricsEnabled": { + "value": "[parameters('metricsEnabled')]" + }, + "logsEnabled": { + "value": "[parameters('logsEnabled')]" + }, + "profileName": { + "value": "[parameters('profileName')]" + } + } + } + } + } + } + }, + "Versions": [ + "3.0.0" + ] + }, + "PolicyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/bef3f64c-5290-43b7-85b0-9b254eef4c47" + } + ], + "exemptions": [] + } +] diff --git a/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs index 4de51f8876..4712dd5e27 100644 --- a/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs @@ -35,7 +35,7 @@ namespace PSRule.Rules.Azure var definitions = context.GetDefinitions(); Assert.NotNull(definitions); - Assert.Equal(129, definitions.Length); + Assert.Equal(130, definitions.Length); // Check category and version var actual = definitions.FirstOrDefault(definition => definition.DefinitionId == "/providers/Microsoft.Authorization/policyDefinitions/34c877ad-507e-4c82-993e-3452a6e0ad3c"); @@ -228,6 +228,27 @@ namespace PSRule.Rules.Azure Assert.Equal(new string[] { "PSRule.Rules.Azure\\Azure.Policy.Indexed" }, actual.With); } + [Fact] + public void ExpandDetailsName() + { + var context = new PolicyAssignmentContext(GetContext()); + var visitor = new PolicyAssignmentDataExportVisitor(); + foreach (var assignment in GetAssignmentData("Policy.assignment.5.json").Where(a => a["Name"].Value() == "assignment.5")) + visitor.Visit(context, assignment); + + var definitions = context.GetDefinitions(); + Assert.NotNull(definitions); + Assert.Single(definitions); + + var actual = definitions.FirstOrDefault(definition => definition.DefinitionId == "/providers/Microsoft.Authorization/policyDefinitions/bef3f64c-5290-43b7-85b0-9b254eef4c47"); + Assert.NotNull(actual); + Assert.Single(actual.Types); + Assert.Equal("Microsoft.KeyVault/vaults", actual.Types[0]); + Assert.Null(actual.Where); + Assert.Equal("{\"allOf\":[{\"type\":\".\",\"equals\":\"Microsoft.Insights/diagnosticSettings\"},{\"name\":\".\",\"equals\":\"setbypolicy_logAnalytics\"}]}", actual.Condition["where"].ToString(Formatting.None)); + Assert.Equal(new string[] { "PSRule.Rules.Azure\\Azure.Policy.Indexed" }, actual.With); + } + #region Helper methods private static PipelineContext GetContext(PSRuleOption option = null) diff --git a/tests/PSRule.Rules.Azure.Tests/PolicyIgnoreDataTests.cs b/tests/PSRule.Rules.Azure.Tests/PolicyIgnoreDataTests.cs index 3aa415dad5..49b74af8d4 100644 --- a/tests/PSRule.Rules.Azure.Tests/PolicyIgnoreDataTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/PolicyIgnoreDataTests.cs @@ -25,13 +25,13 @@ namespace PSRule.Rules.Azure Assert.Equal(PolicyIgnoreReason.Duplicate, entry.Reason); Assert.Contains("Azure.Defender.Storage.MalwareScan", entry.Value); Assert.Contains("Azure.Defender.Storage", entry.Value); - Assert.Contains("Azure.Defender.Storage.SensitiveData", entry.Value); + Assert.Contains("Azure.Defender.Storage.DataScan", entry.Value); Assert.True(data.TryGetValue("/providers/Microsoft.Authorization/policyDefinitions/cfdc5972-75b3-4418-8ae1-7f5c36839390", out entry)); Assert.Equal(PolicyIgnoreReason.Duplicate, entry.Reason); Assert.Contains("Azure.Defender.Storage.MalwareScan", entry.Value); Assert.Contains("Azure.Defender.Storage", entry.Value); - Assert.Contains("Azure.Defender.Storage.SensitiveData", entry.Value); + Assert.Contains("Azure.Defender.Storage.DataScan", entry.Value); } #region Helper methods