diff --git a/policyAssignments/dev/pa-d-pedns.json b/policyAssignments/dev/pa-d-pedns.json index 05f0d28..085019d 100644 --- a/policyAssignments/dev/pa-d-pedns.json +++ b/policyAssignments/dev/pa-d-pedns.json @@ -61,6 +61,9 @@ "PEDNS-017_Effect": { "value": "DeployIfNotExists" }, + "PEDNS-018_Effect": { + "value": "DeployIfNotExists" + }, "evaluationDelay": { "value": "AfterProvisioning" }, @@ -139,6 +142,10 @@ { "policyDefinitionReferenceId": "PEDNS-017", "message": "PolicyID: PEDNS-017 Violation in polset-pedns Initiative - 'The Private Endpoint Private DNS Zone group for Cosmos DB SQL Private Endpoint must be configured'" + }, + { + "policyDefinitionReferenceId": "PEDNS-018", + "message": "PolicyID: PEDNS-018 Violation in polset-pedns Initiative - 'The Private Endpoint Private DNS Zone group for Azure Search Services Private Endpoint must be configured'" } ] }, diff --git a/policyAssignments/dev/pa-d-search.json b/policyAssignments/dev/pa-d-search.json new file mode 100644 index 0000000..8372bef --- /dev/null +++ b/policyAssignments/dev/pa-d-search.json @@ -0,0 +1,32 @@ +{ + "$schema": "../policyAssignment.schema.json", + "policyAssignment": { + "name": "pa-d-search", + "displayName": "Azure Search Services Policies Dev", + "description": "Policy Assignment for Azure Search Services - Dev", + "metadata": { + "category": "Azure Search Services" + }, + "policyDefinitionId": "{policyLocationResourceId}/providers/Microsoft.Authorization/policySetDefinitions/polset-search", + "identity": "SystemAssigned", + "parameters": { + "SRCH-001_Effect": { + "value": "Deny" + }, + "SRCH-002_Effect": { + "value": "Modify" + }, + "SRCH-003_Effect": { + "value": "Deny" + } + + }, + "nonComplianceMessages": [ + ], + "roleDefinitionIds": [ + "/providers/microsoft.authorization/roleDefinitions/7ca78c08-252a-4471-8644-bb5ff32d4ba0" + ] + }, + "definitionSourceManagementGroupId": "/providers/Microsoft.Management/managementGroups/CONTOSO-DEV", + "managementGroupId": "CONTOSO-DEV" +} diff --git a/policyAssignments/prod/pa-p-pedns.json b/policyAssignments/prod/pa-p-pedns.json index 94ac9bc..85b070b 100644 --- a/policyAssignments/prod/pa-p-pedns.json +++ b/policyAssignments/prod/pa-p-pedns.json @@ -61,6 +61,9 @@ "PEDNS-017_Effect": { "value": "DeployIfNotExists" }, + "PEDNS-018_Effect": { + "value": "DeployIfNotExists" + }, "evaluationDelay": { "value": "AfterProvisioning" }, @@ -139,6 +142,10 @@ { "policyDefinitionReferenceId": "PEDNS-017", "message": "PolicyID: PEDNS-017 Violation in polset-pedns Initiative - 'The Private Endpoint Private DNS Zone group for Cosmos DB SQL Private Endpoint must be configured'" + }, + { + "policyDefinitionReferenceId": "PEDNS-018", + "message": "PolicyID: PEDNS-018 Violation in polset-pedns Initiative - 'The Private Endpoint Private DNS Zone group for Azure Search Services Private Endpoint must be configured'" } ] }, diff --git a/policyAssignments/prod/pa-p-search.json b/policyAssignments/prod/pa-p-search.json new file mode 100644 index 0000000..787b32b --- /dev/null +++ b/policyAssignments/prod/pa-p-search.json @@ -0,0 +1,32 @@ +{ + "$schema": "../policyAssignment.schema.json", + "policyAssignment": { + "name": "pa-p-search", + "displayName": "Azure Search Services Policies Prod", + "description": "Policy Assignment for Azure Search Services - Prod", + "metadata": { + "category": "Azure Search Services" + }, + "policyDefinitionId": "{policyLocationResourceId}/providers/Microsoft.Authorization/policySetDefinitions/polset-search", + "identity": "SystemAssigned", + "parameters": { + "SRCH-001_Effect": { + "value": "Deny" + }, + "SRCH-002_Effect": { + "value": "Modify" + }, + "SRCH-003_Effect": { + "value": "Deny" + } + + }, + "nonComplianceMessages": [ + ], + "roleDefinitionIds": [ + "/providers/microsoft.authorization/roleDefinitions/7ca78c08-252a-4471-8644-bb5ff32d4ba0" + ] + }, + "definitionSourceManagementGroupId": "/providers/Microsoft.Management/managementGroups/CONTOSO", + "managementGroupId": "CONTOSO" +} diff --git a/policyInitiatives/polset-pedns.json b/policyInitiatives/polset-pedns.json index ace7d2d..eea9ee0 100755 --- a/policyInitiatives/polset-pedns.json +++ b/policyInitiatives/polset-pedns.json @@ -214,6 +214,18 @@ ], "defaultValue": "DeployIfNotExists" }, + "PEDNS-018_Effect": { + "type": "string", + "metadata": { + "displayName": "PEDNS-018 Effect: Azure AI Search", + "description": "Enable or disable the execution of the policy" + }, + "allowedValues": [ + "DeployIfNotExists", + "Disabled" + ], + "defaultValue": "DeployIfNotExists" + }, "evaluationDelay": { "type": "string", "metadata": { @@ -659,6 +671,30 @@ "groupNames": [ "ISO27001-2013_A.13.1.3" ] + }, + { + "policyDefinitionReferenceId": "PEDNS-018", + "policyDefinitionId": "{policyLocationResourceId}/providers/Microsoft.Authorization/policyDefinitions/pol-deploy-pe-dns-records-single-dns-zone-all-locations", + "parameters": { + "Effect": { + "value": "[parameters('PEDNS-018_Effect')]" + }, + "evaluationDelay": { + "value": "[parameters('evaluationDelay')]" + }, + "groupId": { + "value": "searchService" + }, + "privateDnsZoneId": { + "value": "[concat(parameters('privateDnsZoneResourceGroup'), '/providers/Microsoft.Network/privateDnsZones/', 'privatelink.search.windows.net')]" + }, + "privateLinkServiceResourceType": { + "value": "Microsoft.Search/searchServices" + } + }, + "groupNames": [ + "ISO27001-2013_A.13.1.3" + ] } ] } diff --git a/policyInitiatives/polset-search.json b/policyInitiatives/polset-search.json new file mode 100644 index 0000000..679c138 --- /dev/null +++ b/policyInitiatives/polset-search.json @@ -0,0 +1,108 @@ +{ + "name": "polset-search", + "properties": { + "displayName": "Azure Search Services Policy Initiative", + "description": "This policy initiative defines the foundation security requirements for Azure Search Services", + "metadata": { + "category": "Search", + "version": "1.0.0", + "preview": false, + "deprecated": false + }, + "parameters": { + "SRCH-001_Effect": { + "type": "String", + "metadata": { + "displayName": "SRCH-001 Effect: Azure AI Search service should use a SKU that supports private link", + "description": "Enable or disable the execution of the policy" + }, + "allowedValues": [ + "Audit", + "Deny", + "Disabled" + ], + "defaultValue": "Deny" + }, + "SRCH-002_Effect": { + "type": "String", + "metadata": { + "displayName": "SRCH-002 Effect: Configure Azure AI Search services to disable local authentication", + "description": "Enable or disable the execution of the policy" + }, + "allowedValues": [ + "Modify", + "Disabled" + ], + "defaultValue": "Modify" + }, + "SRCH-003_Effect": { + "type": "String", + "metadata": { + "displayName": "SRCH-003 Effect: Azure AI Search services should restrict public network access", + "description": "Enable or disable the execution of the policy" + }, + "allowedValues": [ + "Audit", + "Deny", + "Disabled" + ], + "defaultValue": "Deny" + } + }, + "policyDefinitionGroups": [ + { + "name": "ISO27001-2013_A.9.2.3", + "additionalMetadataId": "/providers/Microsoft.PolicyInsights/policyMetadata/ISO27001-2013_A.9.2.3" + }, + { + "name": "ISO27001-2013_A.13.1.1", + "additionalMetadataId": "/providers/Microsoft.PolicyInsights/policyMetadata/ISO27001-2013_A.13.1.1" + }, + { + "name": "ISO27001-2013_A.13.1.3", + "additionalMetadataId": "/providers/Microsoft.PolicyInsights/policyMetadata/ISO27001-2013_A.13.1.3" + } + ], + "policyDefinitions": [ + { + "policyDefinitionReferenceId": "SRCH-001", + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/a049bf77-880b-470f-ba6d-9f21c530cf83", + "definitionVersion": "1.0.*", + "parameters": { + "effect": { + "value": "[parameters('SRCH-001_Effect')]" + } + }, + "groupNames": [ + "ISO27001-2013_A.13.1.1" + ] + }, + { + "policyDefinitionReferenceId": "SRCH-002", + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/4eb216f2-9dba-4979-86e6-5d7e63ce3b75", + "definitionVersion": "2.0.*", + "parameters": { + "effect": { + "value": "[parameters('SRCH-002_Effect')]" + } + }, + "groupNames": [ + "ISO27001-2013_A.9.2.3" + ] + }, + { + "policyDefinitionReferenceId": "SRCH-003", + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/ee980b6d-0eca-4501-8d54-f6290fd512c3", + "definitionVersion": "1.0.*", + "parameters": { + "effect": { + "value": "[parameters('SRCH-003_Effect')]" + } + }, + "groupNames": [ + "ISO27001-2013_A.13.1.3" + ] + } + ] + } +} diff --git a/tests/policy-integration-tests/search/README.md b/tests/policy-integration-tests/search/README.md new file mode 100644 index 0000000..80482ce --- /dev/null +++ b/tests/policy-integration-tests/search/README.md @@ -0,0 +1,23 @@ +# Policy Integration Test - Policy Integration Test Cases for xxx + +## Introduction + +This folder contains a sample test case for xxx related policies. + +The test case is designed to test the following policy assignments: + +| Policy Assignment Name | Policy Assignment Scope | Description | +| :--------------------- | :--------------------- | :---------- | +| `pa-d-search` | `/providers/Microsoft.Management/managementGroups/CONTOSO-DEV` | Policy Assignment for the Azure Search Service initiative | +| `pa-d-pedns` | `/providers/Microsoft.Management/managementGroups/CONTOSO-DEV` | Policy Assignment for Azure Private Endpoint DNS Records Policy Initiative (deploy DNS records for Private Endpoints) | +| `pa-d-diag-settings` | `/providers/Microsoft.Management/managementGroups/CONTOSO-DEV` | Policy Assignment for Azure Diagnostic Settings Policy Initiative (deploy diagnostic settings for all applicable Azure resources) | + +The following policies are in scope for testing: + +| Policy Assignment | Policy Reference ID | Policy Name | Policy Effect | +| :---------------- | :---------------- | :------------ | :------------ | +| `pa-d-search` | `SRCH-001` | Azure AI Search service should use a SKU that supports private link | Deny | +| `pa-d-search` | `SRCH-002` | Configure Azure AI Search services to disable local authentication | Modify | +| `pa-d-search` | `SRCH-003` | Azure AI Search services should restrict public network access | Deny | +| `pa-d-diag-settings` | `DS-045` | Configure Diagnostic Setting for Azure Search Service | DeployIfNotExists | +| `pa-d-pedns` | `PEDNS-017` | Private DNS Record for Azure Search Service Private Endpoint must exist | DeployIfNotExists | diff --git a/tests/policy-integration-tests/search/config.json b/tests/policy-integration-tests/search/config.json new file mode 100644 index 0000000..d1fe59c --- /dev/null +++ b/tests/policy-integration-tests/search/config.json @@ -0,0 +1,16 @@ +{ + "policyAssignmentIds": [ + "/providers/Microsoft.Management/managementGroups/CONTOSO-DEV/providers/Microsoft.Authorization/policyAssignments/pa-d-pedns", + "/providers/Microsoft.Management/managementGroups/CONTOSO-DEV/providers/Microsoft.Authorization/policyAssignments/pa-d-search", + "/providers/Microsoft.Management/managementGroups/CONTOSO-DEV/providers/Microsoft.Authorization/policyAssignments/pa-d-diag-settings" + ], + "testName": "SearchService", + "searchServiceAssignmentName": "pa-d-search", + "diagSettingsAssignmentName": "pa-d-diag-settings", + "peDNSAssignmentName": "pa-d-pedns", + "testSubscription": "sub-d-lz-corp-01", + "testResourceGroup": "rg-ae-d-policy-test-search-001", + "location": "australiaeast", + "tagsForResourceGroup": false, + "removeTestResourceGroup": true +} diff --git a/tests/policy-integration-tests/search/main.bad.bicep b/tests/policy-integration-tests/search/main.bad.bicep new file mode 100644 index 0000000..c09eebc --- /dev/null +++ b/tests/policy-integration-tests/search/main.bad.bicep @@ -0,0 +1,34 @@ +metadata itemDisplayName = 'Test Template for AI Search Service' +metadata description = 'This template deploys the testing resource for AI Search Service.' +metadata summary = 'Deploys test AI Search Service resources that should violate some policy assignments.' + +// ============ // +// variables // +// ============ // +// Load the configuration file +var globalConfig = loadJsonContent('../.shared/policy_integration_test_config.jsonc') +var localConfig = loadJsonContent('config.json') + +var location = localConfig.location +var namePrefix = globalConfig.namePrefix + +// define template specific variables +var serviceShort = 'srch3' + +resource searchService 'Microsoft.Search/searchServices@2026-03-01-preview' = { + name: '${namePrefix}${serviceShort}01' + location: location + sku: { + name: 'free' //This should violate policy SRCH-001: Azure AI Search service should use a SKU that supports private link, since free SKU does not support private link + } + identity: { + type: 'SystemAssigned' + } + properties: { + hostingMode: 'Default' + publicNetworkAccess: 'Enabled' //This should violate policy SRCH-003: Azure AI Search services should restrict public network access + replicaCount: 1 + partitionCount: 1 + computeType: 'Default' + } +} diff --git a/tests/policy-integration-tests/search/main.good.bicep b/tests/policy-integration-tests/search/main.good.bicep new file mode 100644 index 0000000..4aaff9a --- /dev/null +++ b/tests/policy-integration-tests/search/main.good.bicep @@ -0,0 +1,34 @@ +metadata itemDisplayName = 'Test Template for AI Search Service' +metadata description = 'This template deploys the testing resource for AI Search Service.' +metadata summary = 'Deploys test AI Search Service resources that should comply with all policy assignments.' + +// ============ // +// variables // +// ============ // +// Load the configuration file +var globalConfig = loadJsonContent('../.shared/policy_integration_test_config.jsonc') +var localConfig = loadJsonContent('config.json') + +var location = localConfig.location +var namePrefix = globalConfig.namePrefix + +// define template specific variables +var serviceShort = 'srch2' + +resource searchService 'Microsoft.Search/searchServices@2026-03-01-preview' = { + name: '${namePrefix}${serviceShort}01' + location: location + sku: { + name: 'standard' //This should comply with policy SRCH-001: Azure AI Search service should use a SKU that supports private link, since Basic SKU does not support private link + } + identity: { + type: 'SystemAssigned' + } + properties: { + hostingMode: 'Default' + publicNetworkAccess: 'Disabled' //This should comply with policy SRCH-003: Azure AI Search services should restrict public network access + replicaCount: 1 + partitionCount: 1 + computeType: 'Default' + } +} diff --git a/tests/policy-integration-tests/search/main.test.bicep b/tests/policy-integration-tests/search/main.test.bicep new file mode 100644 index 0000000..cee47c2 --- /dev/null +++ b/tests/policy-integration-tests/search/main.test.bicep @@ -0,0 +1,77 @@ +metadata itemDisplayName = 'Test Template for AI Search Service' +metadata description = 'This template deploys the testing resource for AI Search Service.' +metadata summary = 'Deploys test AI Search Service resources.' + +// ============ // +// variables // +// ============ // +// Load the configuration file +var globalConfig = loadJsonContent('../.shared/policy_integration_test_config.jsonc') +var localConfig = loadJsonContent('config.json') +//Define required variables from the configuration files - change these based on your requirements +var tags = globalConfig.tags +var location = localConfig.location +var namePrefix = globalConfig.namePrefix +var subName = localConfig.testSubscription +var vnetResourceGroup = globalConfig.subscriptions[subName].networkResourceGroup +var vnetName = globalConfig.subscriptions[subName].vNet +var peSubnetName = globalConfig.subscriptions[subName].peSubnet +var serviceShort = 'srch1' //use this to form the name of the resources deployed by this template. This is helpful to identify the resource in the portal and also useful if you want to have a policy that targets specific resources by name. For example, if you have a policy that audits whether storage accounts have secure transfer enabled, you can set serviceShort to 'st' and then in the policy definition, you can target resources with name starting with 'st' to only audit the storage accounts deployed by this test template. + +// ============ // +// resources // +// ============ // +resource vnet 'Microsoft.Network/virtualNetworks@2025-05-01' existing = { + name: vnetName + scope: az.resourceGroup(vnetResourceGroup) + + resource peSubnet 'subnets' existing = { name: peSubnetName } +} + +resource searchService 'Microsoft.Search/searchServices@2026-03-01-preview' = { + name: '${namePrefix}${serviceShort}01' + location: location + tags: tags + sku: { + name: 'standard' + } + properties: { + hostingMode: 'Default' + publicNetworkAccess: 'Disabled' + replicaCount: 1 + partitionCount: 1 + computeType: 'Default' + disableLocalAuth: false //This should be modified to true by the policy SRCH-002: Configure Azure AI Search services to disable local authentication + } +} + +resource pe 'Microsoft.Network/privateEndpoints@2025-05-01' = { + name: 'pe-${namePrefix}${serviceShort}01' + location: location + tags: tags + properties: { + subnet: { + id: vnet::peSubnet.id + } + privateLinkServiceConnections: [ + { + name: 'pe-${namePrefix}${serviceShort}01' + properties: { + privateLinkServiceId: searchService.id + groupIds: [ + 'searchService' + ] + } + } + ] + } +} + +// ============ // +// outputs // +// ============ // +//Specify the outputs that are required for the test +output name string = searchService.name +output resourceId string = searchService.id +output privateEndpointResourceId string = pe.id +output location string = searchService.location diff --git a/tests/policy-integration-tests/search/tests.ps1 b/tests/policy-integration-tests/search/tests.ps1 new file mode 100644 index 0000000..b29f881 --- /dev/null +++ b/tests/policy-integration-tests/search/tests.ps1 @@ -0,0 +1,75 @@ +#region generic sections for all tests +#Requires -Modules Az.Accounts, Az.PolicyInsights, Az.Resources +#Requires -Version 7.0 + +using module AzResourceTest + +$helperFunctionScriptPath = (resolve-path -relativeBasePath $PSScriptRoot -path '../../../scripts/pipelines/helper/helper-functions.ps1').Path + +#load helper +. $helperFunctionScriptPath + +#Run initiate-test script to set environment variables for test configuration and deployment +$globalConfigFilePath = (resolve-path -RelativeBasePath $PSScriptRoot -path '../.shared/policy_integration_test_config.jsonc').Path +$TestDirectory = $PSScriptRoot +Write-Output "Initiating test with global config file: $globalConfigFilePath and test directory: $TestDirectory" +$initiateTestScriptPath = (resolve-path -RelativeBasePath $PSScriptRoot -path '../.shared/initiate-test.ps1').Path +. $initiateTestScriptPath -globalConfigFilePath $globalConfigFilePath -TestDirectory $TestDirectory + +# Refer to the ../../docs/policy-integration-test-get-started.md for details on the expected variables to be set by the initiate-test script and the structure of those variables. +#endregion + +#region defining tests +<# +The list of tested policies are documented in the ./README.md file. +#> + + +#Parse deployment outputs +$resourceId = $script:bicepDeploymentOutputs.resourceId.value +$diagSettingsPolicyAssignmentId = $script:LocalConfig_policyAssignmentIds | Where-Object { $_ -imatch "$script:LocalConfig_diagSettingsAssignmentName`$" } +$peDNSPolicyAssignmentId = $script:LocalConfig_policyAssignmentIds | Where-Object { $_ -imatch "$script:LocalConfig_peDNSAssignmentName`$" } +$diagnosticSettingsId = "{0}{1}" -f $resourceId, $script:GlobalConfig_diagnosticSettingsIdSuffix +$searchServicePolicyAssignmentId = $script:LocalConfig_policyAssignmentIds | Where-Object { $_ -imatch "$script:LocalConfig_searchServiceAssignmentName`$" } +$privateEndpointResourceId = $script:bicepDeploymentOutputs.privateEndpointResourceId.value +$privateEndpointPrivateDNSZoneGroupId = '{0}{1}' -f $privateEndpointResourceId, $script:GlobalConfig_privateEndpointPrivateDNSZoneGroupIdSuffix +$violatingPolicies = @( + @{ + policyAssignmentId = $searchServicePolicyAssignmentId + policyDefinitionReferenceId = 'SRCH-001' + } + @{ + policyAssignmentId = $searchServicePolicyAssignmentId + policyDefinitionReferenceId = 'SRCH-003' + } +) +#define tests +$tests = @() +#Modify / Append Policies +$tests += New-ARTPropertyValueTestConfig 'SRCH-002: Local authentication should be disabled' $script:token $resourceId 'boolean' 'properties.disableLocalAuth' 'equals' $true + +#DeployIfNotExists Policies +$tests += New-ARTResourceExistenceTestConfig 'DS-045: Deploy Diagnostic Settings for Azure Search Service to Log Analytics workspace.' $script:token $diagnosticSettingsId 'exists' $script:GlobalConfig_diagnosticSettingsAPIVersion +$tests += New-ARTPolicyStateTestConfig 'DS-045: Diagnostic Settings Policy Must Be Compliant' $script:token $resourceId $diagSettingsPolicyAssignmentId 'Compliant' 'DS-045' +$tests += New-ARTResourceExistenceTestConfig 'PEDNS-018: Private DNS Record for Azure Search Service Private Endpoint must exist' $script:token $privateEndpointPrivateDNSZoneGroupId 'exists' $script:GlobalConfig_privateDNSZoneGroupAPIVersion +$tests += New-ARTPolicyStateTestConfig 'PEDNS-018: Private DNS Record Policy Must Be Compliant' $script:token $privateEndpointResourceId $peDNSPolicyAssignmentId 'Compliant' 'PEDNS-018' + + +#Deny policies (testing both positive and negative scenarios) +$tests += New-ARTWhatIfDeploymentTestConfig 'Policy abiding deployment should succeed' $script:token $script:whatIfComplyBicepTemplatePath $script:bicepDeploymentResult.bicepDeploymentTarget 'Succeeded' -maxRetry $script:GlobalConfig_whatIfMaxRetry +$tests += New-ARTWhatIfDeploymentTestConfig 'Policy violating deployment should fail' $script:token $script:whatIfViolateBicepTemplatePath $script:bicepDeploymentResult.bicepDeploymentTarget 'Failed' $violatingPolicies -maxRetry $script:GlobalConfig_whatIfMaxRetry + +#endregion + +#region Invoke tests - do not modify +$params = @{ + tests = $tests + testTitle = $script:testTitle + contextTitle = $script:contextTitle + testSuiteName = $script:testSuiteName + OutputFile = $script:outputFilePath + OutputFormat = $script:GlobalConfig_testOutputFormat +} +Test-ARTResourceConfiguration @params + +#endregion