diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8f32e779..aa76211514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- New rules: + - API Management: + - Check API Management products require a subscription. [#342](https://github.com/Microsoft/PSRule.Rules.Azure/issues/342) + - Check API Management products require approval. [#343](https://github.com/Microsoft/PSRule.Rules.Azure/issues/343) + - Check API Management sample products have been removed. [#344](https://github.com/Microsoft/PSRule.Rules.Azure/issues/344) + - Check API Management uses a managed identity. [#345](https://github.com/Microsoft/PSRule.Rules.Azure/issues/345) + - Check API Management certificates are not expired. [#346](https://github.com/Microsoft/PSRule.Rules.Azure/issues/346) + ## v0.10.1 What's changed since v0.10.0: diff --git a/docs/rules/en/Azure.APIM.CertificateExpiry.md b/docs/rules/en/Azure.APIM.CertificateExpiry.md new file mode 100644 index 0000000000..57ff536ea1 --- /dev/null +++ b/docs/rules/en/Azure.APIM.CertificateExpiry.md @@ -0,0 +1,37 @@ +--- +severity: Important +category: Reliability +resource: API Management +online version: https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/rules/en/Azure.APIM.CertificateExpiry.md +--- + +# API Management uses current certificates + +## SYNOPSIS + +Renew certificates used for custom domain bindings. + +## DESCRIPTION + +When custom domains are configured within an API Management service. +A certificate must be assigned to allow traffic to be transmitted using TLS. + +Each certificate has an expiry date, after which the certificate is not valid. +After expiry, client connections to the API Management service will reject the certificate. + +## RECOMMENDATION + +Consider renewing certificates before expiry to prevent service issues. + +## NOTES + +By default, this rule fails when certificates have less than 30 days remaining before expiry. + +To configure this rule: + +- Override the Azure_MinimumCertificateLifetime configuration value with the minimum number of days until expiry that should pass. + +## LINKS + +- [Configure a custom domain name](https://docs.microsoft.com/en-us/azure/api-management/configure-custom-domain#use-the-azure-portal-to-set-a-custom-domain-name) +- [Azure template reference](https://docs.microsoft.com/en-us/azure/templates/microsoft.apimanagement/2019-12-01/service#hostnameconfiguration-object) diff --git a/docs/rules/en/Azure.APIM.ManagedIdentity.md b/docs/rules/en/Azure.APIM.ManagedIdentity.md new file mode 100644 index 0000000000..aa92d3e468 --- /dev/null +++ b/docs/rules/en/Azure.APIM.ManagedIdentity.md @@ -0,0 +1,28 @@ +--- +severity: Important +category: Security configuration +resource: API Management +online version: https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/rules/en/Azure.APIM.ManagedIdentity.md +--- + +# API Management uses a managed identity + +## SYNOPSIS + +Use managed identities to access Azure resources. + +## DESCRIPTION + +API Management must authenticate to access Azure resources such as Key Vault. +Use Key Vault to store certificates and secrets used within API Management. + +## RECOMMENDATION + +Consider configuring a managed identity for each API Management instance. +Also consider using managed identities to authenticate to related Azure services. + +## LINKS + +- [Use managed identities in Azure API Management](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-use-managed-service-identity) +- [Authenticate with managed identity](https://docs.microsoft.com/en-us/azure/api-management/api-management-authentication-policies#ManagedIdentity) +- [Azure template reference](https://docs.microsoft.com/en-us/azure/templates/microsoft.apimanagement/2019-12-01/service#ApiManagementServiceIdentity) diff --git a/docs/rules/en/Azure.APIM.ProductApproval.md b/docs/rules/en/Azure.APIM.ProductApproval.md new file mode 100644 index 0000000000..24ccf8be51 --- /dev/null +++ b/docs/rules/en/Azure.APIM.ProductApproval.md @@ -0,0 +1,28 @@ +--- +severity: Important +category: Security configuration +resource: API Management +online version: https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/rules/en/Azure.APIM.ProductApproval.md +--- + +# Require approval for products + +## SYNOPSIS + +Configure products to require approval. + +## DESCRIPTION + +When publishing APIs through Azure API Management (APIM), APIs are assigned to products. +Access to use an API is delegated through a product. + +When products do not require approval, users can create a subscription for a product without approval. + +## RECOMMENDATION + +Consider configuring all API Management products to require approval. + +## LINKS + +- [Create and publish a product](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-add-products) +- [Azure template reference](https://docs.microsoft.com/en-us/azure/templates/microsoft.apimanagement/2019-12-01/service/products) diff --git a/docs/rules/en/Azure.APIM.ProductSubscription.md b/docs/rules/en/Azure.APIM.ProductSubscription.md new file mode 100644 index 0000000000..63bdc8463c --- /dev/null +++ b/docs/rules/en/Azure.APIM.ProductSubscription.md @@ -0,0 +1,29 @@ +--- +severity: Important +category: Security configuration +resource: API Management +online version: https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/rules/en/Azure.APIM.ProductSubscription.md +--- + +# Require a subscription for products + +## SYNOPSIS + +Configure products to require a subscription. + +## DESCRIPTION + +When publishing APIs through Azure API Management (APIM), APIs can be secured using subscription keys. +Client applications that consume published APIs must subscribe before making calls to those APIs. + +When combined with policies, subscriptions allow controls such as throttling to be implemented. + +## RECOMMENDATION + +Consider configuring all API Management products to require a subscription. + +## LINKS + +- [Subscriptions in Azure API Management](https://docs.microsoft.com/en-us/azure/api-management/api-management-subscriptions) +- [Create and publish a product](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-add-products) +- [Azure template reference](https://docs.microsoft.com/en-us/azure/templates/microsoft.apimanagement/2019-12-01/service/products) diff --git a/docs/rules/en/Azure.APIM.SampleProducts.md b/docs/rules/en/Azure.APIM.SampleProducts.md new file mode 100644 index 0000000000..2c76f9cbdb --- /dev/null +++ b/docs/rules/en/Azure.APIM.SampleProducts.md @@ -0,0 +1,25 @@ +--- +severity: Awareness +category: Security configuration +resource: API Management +online version: https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/rules/en/Azure.APIM.SampleProducts.md +--- + +# Remove default products + +## SYNOPSIS + +Remove starter and unlimited sample products. + +## DESCRIPTION + +API Management includes two sample products _Starter_ and _Unlimited_. +Accidentally adding APIs to these sample products may expose APIs more than intended. + +## RECOMMENDATION + +Consider removing starter and unlimited sample products from API Management. + +## LINKS + +- [Create and publish a product](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-add-products) diff --git a/docs/rules/en/module.md b/docs/rules/en/module.md index 3c37dd2fbf..7cd24c4919 100644 --- a/docs/rules/en/module.md +++ b/docs/rules/en/module.md @@ -63,6 +63,7 @@ Name | Synopsis | Severity Name | Synopsis | Severity ---- | -------- | -------- [Azure.AKS.MinNodeCount](Azure.AKS.MinNodeCount.md) | AKS clusters should have minimum number of nodes for failover and updates. | Important +[Azure.APIM.CertificateExpiry](Azure.APIM.CertificateExpiry.md) | Renew certificates used for custom domain bindings. | Important [Azure.AppGw.MinInstance](Azure.AppGw.MinInstance.md) | Application Gateways should use a minimum of two instances. | Important [Azure.AppService.PlanInstanceCount](Azure.AppService.PlanInstanceCount.md) | Use an App Service Plan with at least two (2) instances. | Single point of failure [Azure.Monitor.ServiceHealth](Azure.Monitor.ServiceHealth.md) | Configure Service Health alerts to notify administrators. | Important @@ -106,7 +107,11 @@ Name | Synopsis | Severity [Azure.APIM.EncryptValues](Azure.APIM.EncryptValues.md) | API Management named values should be encrypted. | Important [Azure.APIM.HTTPBackend](Azure.APIM.HTTPBackend.md) | Use HTTPS for communication to backend services. | Critical [Azure.APIM.HTTPEndpoint](Azure.APIM.HTTPEndpoint.md) | Enforce HTTPS for communication to API clients. | Important +[Azure.APIM.ManagedIdentity](Azure.APIM.ManagedIdentity.md) | Use managed identities to access Azure resources. | Important +[Azure.APIM.ProductApproval](Azure.APIM.ProductApproval.md) | Configure products to require approval. | Important +[Azure.APIM.ProductSubscription](Azure.APIM.ProductSubscription.md) | Configure products to require a subscription. | Important [Azure.APIM.Protocols](Azure.APIM.Protocols.md) | API Management should only accept a minimum of TLS 1.2. | Important +[Azure.APIM.SampleProducts](Azure.APIM.SampleProducts.md) | Remove starter and unlimited sample products. | Awareness [Azure.AppGw.OWASP](Azure.AppGw.OWASP.md) | Application Gateway Web Application Firewall (WAF) should use OWASP 3.x rules. | Important [Azure.AppGw.Prevention](Azure.AppGw.Prevention.md) | Internet exposed Application Gateways should use prevention mode to protect backend resources. | Critical [Azure.AppGw.SSLPolicy](Azure.AppGw.SSLPolicy.md) | Application Gateway should only accept a minimum of TLS 1.2. | Critical diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index ae8207ab9c..5c8c666cf2 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -196,8 +196,8 @@ task PSScriptAnalyzer NuGet, { # Synopsis: Install PSRule task PSRule NuGet, { - if ($Null -eq (Get-InstalledModule -Name PSRule -MinimumVersion 0.15.0 -ErrorAction Ignore)) { - Install-Module -Name PSRule -Repository PSGallery -MinimumVersion 0.15.0 -Scope CurrentUser -Force; + if ($Null -eq (Get-InstalledModule -Name PSRule -MinimumVersion 0.16.0 -ErrorAction Ignore)) { + Install-Module -Name PSRule -Repository PSGallery -MinimumVersion 0.16.0 -Scope CurrentUser -Force; } Import-Module -Name PSRule -Verbose:$False; } @@ -289,14 +289,15 @@ task Rules PSRule, { $assertParams = @{ Path = './.ps-rule/' Style = $AssertStyle - OutputFormat = 'NUnit3'; + OutputFormat = 'NUnit3' + ErrorAction = 'Stop' } Import-Module (Join-Path -Path $PWD -ChildPath out/modules/PSRule.Rules.Azure) -Force; Get-RepoRuleData -Path $PWD | - Assert-PSRule @assertParams -OutputPath reports/ps-rule-file.xml -ErrorAction Stop; + Assert-PSRule @assertParams -OutputPath reports/ps-rule-file.xml; - $rules = Get-PSRule -Module PSRule.Rules.Azure -Culture 'en-US'; - $rules | Assert-PSRule @assertParams -OutputPath reports/ps-rule-file2.xml -ErrorAction Stop; + $rules = Get-PSRule -Module PSRule.Rules.Azure; + $rules | Assert-PSRule @assertParams -OutputPath reports/ps-rule-file2.xml; } # Synopsis: Run script analyzer diff --git a/ps-rule.yaml b/ps-rule.yaml index 4ac8bb2dc8..39dc4b46aa 100644 --- a/ps-rule.yaml +++ b/ps-rule.yaml @@ -4,3 +4,7 @@ binding: targetName: - RuleName - FullName + +output: + culture: + - en-US diff --git a/src/PSRule.Rules.Azure/PSRule.Rules.Azure.psm1 b/src/PSRule.Rules.Azure/PSRule.Rules.Azure.psm1 index 71c48e2bf4..e9d94191db 100644 --- a/src/PSRule.Rules.Azure/PSRule.Rules.Azure.psm1 +++ b/src/PSRule.Rules.Azure/PSRule.Rules.Azure.psm1 @@ -473,7 +473,7 @@ function GetSubResource { Get-AzResource @getParams -ExpandProperties; } catch { - Write-Verbose -Message "Failed to read: $ResourceType"; + Write-Warning -Message "Failed to read: $ResourceType"; } } } @@ -489,12 +489,28 @@ function VisitAPIManagement { ) process { $resources = @(); - $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/apis' -ApiVersion '2019-01-01'; - $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/backends' -ApiVersion '2019-01-01'; - $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/properties' -ApiVersion '2019-01-01' | ForEach-Object { - $_.properties.value = '*** MASKED ***'; - $_; + $apis += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/apis' -ApiVersion '2019-12-01'; + foreach ($api in $apis) { + $resources += $api; + $apiParams = @{ + Name = "$($Resource.Name)/$($api.Name)" + ResourceType = 'Microsoft.ApiManagement/service/apis/policies' + ResourceGroupName = $Resource.ResourceGroupName + DefaultProfile = $Context + ApiVersion = '2019-12-01' + }; + $resources += Get-AzResource @apiParams; } + + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/backends' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/products' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/policies' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/identityProviders' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/diagnostics' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/loggers' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/certificates' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/namedValues' -ApiVersion '2019-12-01'; + $resources += GetSubResource @PSBoundParameters -ResourceType 'Microsoft.ApiManagement/service/portalsettings' -ApiVersion '2019-12-01'; $Resource | Add-Member -MemberType NoteProperty -Name resources -Value $resources -PassThru; } } diff --git a/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 b/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 index 62cc9d9a3e..fb47a83342 100644 --- a/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 +++ b/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 @@ -20,4 +20,7 @@ SecurityCenterNotConfigured = "Security Center are not configured." LateralTraversalNotRestricted = "A rule to limit lateral traversal was not found." AllInboundRestricted = "The first inbound rule denies traffic from all sources." + APIMProductSubscription = "The product '{0}' does not require a subscription to use." + APIMProductApproval = "The product '{0}' does not require approval." + APIMCertificateExpiry = "The certificate for host name '{0}' expires or expired on '{1}'." } diff --git a/src/PSRule.Rules.Azure/rules/Azure.APIM.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.APIM.Rule.ps1 index 38d6677631..0d63f7145f 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.APIM.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.APIM.Rule.ps1 @@ -68,9 +68,9 @@ Rule 'Azure.APIM.HTTPBackend' -Type 'Microsoft.ApiManagement/service', 'Microsof } # Synopsis: Encrypt all named values -Rule 'Azure.APIM.EncryptValues' -Type 'Microsoft.ApiManagement/service', 'Microsoft.ApiManagement/service/properties' -Tag @{ release = 'GA' } { +Rule 'Azure.APIM.EncryptValues' -Type 'Microsoft.ApiManagement/service', 'Microsoft.ApiManagement/service/properties', 'Microsoft.ApiManagement/service/namedValues' -Tag @{ release = 'GA' } { if ($PSRule.TargetType -eq 'Microsoft.ApiManagement/service') { - $properties = @(GetSubResources -ResourceType 'Microsoft.ApiManagement/service/properties') + $properties = @(GetSubResources -ResourceType 'Microsoft.ApiManagement/service/properties', 'Microsoft.ApiManagement/service/namedValues') if ($properties.Length -eq 0) { $True; } @@ -78,7 +78,76 @@ Rule 'Azure.APIM.EncryptValues' -Type 'Microsoft.ApiManagement/service', 'Micros $Assert.HasFieldValue($prop, 'properties.secret', $True) } } - elseif ($PSRule.TargetType -eq 'Microsoft.ApiManagement/service/properties') { + elseif ($PSRule.TargetType -in 'Microsoft.ApiManagement/service/properties', 'Microsoft.ApiManagement/service/namedValues') { $Assert.HasFieldValue($TargetObject, 'properties.secret', $True) } } + +# Synopsis: Require subscription for products +Rule 'Azure.APIM.ProductSubscription' -Type 'Microsoft.ApiManagement/service', 'Microsoft.ApiManagement/service/products' -Tag @{ release = 'GA' } { + $products = @($TargetObject); + if ($PSRule.TargetType -eq 'Microsoft.ApiManagement/service') { + $products = @(GetSubResources -ResourceType 'Microsoft.ApiManagement/service/products'); + if ($products.Length -eq 0) { + $True; + } + } + foreach ($product in $products) { + $Assert. + HasFieldValue($product, 'Properties.subscriptionRequired', $True). + WithReason(($LocalizedData.APIMProductSubscription -f $product.Name), $True); + } +} + +# Synopsis: Require approval for products +Rule 'Azure.APIM.ProductApproval' -Type 'Microsoft.ApiManagement/service', 'Microsoft.ApiManagement/service/products' -Tag @{ release = 'GA' } { + $products = @($TargetObject); + if ($PSRule.TargetType -eq 'Microsoft.ApiManagement/service') { + $products = @(GetSubResources -ResourceType 'Microsoft.ApiManagement/service/products'); + if ($products.Length -eq 0) { + $True; + } + } + foreach ($product in $products) { + $Assert. + HasFieldValue($product, 'Properties.approvalRequired', $True). + WithReason(($LocalizedData.APIMProductApproval -f $product.Name), $True); + } +} + +# Synopsis: Remove sample products +Rule 'Azure.APIM.SampleProducts' -Type 'Microsoft.ApiManagement/service', 'Microsoft.ApiManagement/service/products' -Tag @{ release = 'GA' } { + $products = @($TargetObject); + if ($PSRule.TargetType -eq 'Microsoft.ApiManagement/service') { + $products = @(GetSubResources -ResourceType 'Microsoft.ApiManagement/service/products'); + if ($products.Length -eq 0) { + $True; + } + } + foreach ($product in $products) { + $product | Within 'Name' -Not 'unlimited', 'starter' + } +} + +# Synopsis: Provision a managed identity +Rule 'Azure.APIM.ManagedIdentity' -Type 'Microsoft.ApiManagement/service' -Tag @{ release = 'GA' } { + Within 'Identity.Type' 'SystemAssigned', 'UserAssigned' +} + +# Synopsis: Renew expired certificates +Rule 'Azure.APIM.CertificateExpiry' -Type 'Microsoft.ApiManagement/service' -Tag @{ release = 'GA' } { + $configurations = @($TargetObject.Properties.hostnameConfigurations | Where-Object { + $Null -ne $_.certificate + }) + if ($configurations.Length -eq 0) { + $True; + } + else { + foreach ($configuration in $configurations) { + $remaining = ($configuration.certificate.expiry - [DateTime]::Now).Days; + $Assert. + GreaterOrEqual($remaining, '.', $Configuration.Azure_MinimumCertificateLifetime). + WithReason(($LocalizedData.APIMCertificateExpiry -f $configuration.hostName, $configuration.certificate.expiry.ToString('yyyy/MM/dd')), $True); + } + } +} -Configure @{ Azure_MinimumCertificateLifetime = 30 } diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.APIM.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.APIM.Tests.ps1 index 5403f0d354..b9887d089b 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.APIM.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.APIM.Tests.ps1 @@ -46,8 +46,8 @@ Describe 'Azure.APIM' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -Be 'apim-B'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-C'; } It 'Azure.APIM.HTTPEndpoint' { @@ -62,8 +62,8 @@ Describe 'Azure.APIM' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -Be 'apim-B'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-C'; } It 'Azure.APIM.HTTPBackend' { @@ -79,13 +79,45 @@ Describe 'Azure.APIM' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -Be 'apim-B'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-C'; } It 'Azure.APIM.EncryptValues' { $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.APIM.EncryptValues' }; + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-A', 'apim-C'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'apim-B'; + } + + It 'Azure.APIM.ProductSubscription' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.APIM.ProductSubscription' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-C'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'apim-A'; + } + + It 'Azure.APIM.ProductApproval' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.APIM.ProductApproval' }; + # Fail $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); $ruleResult | Should -Not -BeNullOrEmpty; @@ -95,8 +127,56 @@ Describe 'Azure.APIM' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-C'; + } + + It 'Azure.APIM.SampleProducts' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.APIM.SampleProducts' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'apim-A'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-C'; + } + + It 'Azure.APIM.ManagedIdentity' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.APIM.ManagedIdentity' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; $ruleResult.Length | Should -Be 1; $ruleResult.TargetName | Should -Be 'apim-B'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-A', 'apim-C'; + } + + It 'Azure.APIM.CertificateExpiry' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.APIM.CertificateExpiry' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'apim-C'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-A'; } } } diff --git a/tests/PSRule.Rules.Azure.Tests/PathBuilderTests.cs b/tests/PSRule.Rules.Azure.Tests/PathBuilderTests.cs index 651bbd5890..f28f2b6281 100644 --- a/tests/PSRule.Rules.Azure.Tests/PathBuilderTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/PathBuilderTests.cs @@ -4,6 +4,7 @@ using PSRule.Rules.Azure.Pipeline; using System; using System.IO; +using System.Linq; using Xunit; namespace PSRule.Rules.Azure @@ -28,14 +29,14 @@ namespace PSRule.Rules.Azure builder.Add(GetSourcePath("*Parameters*.json")); var actual3 = builder.Build(); Assert.Equal(2, actual3.Length); - Assert.Equal(GetSourcePath("Resources.Parameters.json"), actual3[0].FullName); - Assert.Equal(GetSourcePath("Resources.Parameters2.json"), actual3[1].FullName); + Assert.NotNull(actual3.SingleOrDefault(f => f.FullName == GetSourcePath("Resources.Parameters.json"))); + Assert.NotNull(actual3.SingleOrDefault(f => f.FullName == GetSourcePath("Resources.Parameters2.json"))); builder.Add(GetSourcePath("*Parameters?.json")); var actual4 = builder.Build(); Assert.Equal(2, actual4.Length); - Assert.Equal(GetSourcePath("Resources.Parameters.json"), actual4[0].FullName); - Assert.Equal(GetSourcePath("Resources.Parameters2.json"), actual4[1].FullName); + Assert.NotNull(actual4.SingleOrDefault(f => f.FullName == GetSourcePath("Resources.Parameters.json"))); + Assert.NotNull(actual4.SingleOrDefault(f => f.FullName == GetSourcePath("Resources.Parameters2.json"))); } private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.APIM.json b/tests/PSRule.Rules.Azure.Tests/Resources.APIM.json index b3843c18f3..04a2797696 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.APIM.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.APIM.json @@ -2,7 +2,12 @@ { "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A", "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A", - "Identity": null, + "Identity": { + "PrincipalId": "00000000-0000-0000-0000-000000000000", + "TenantId": "00000000-0000-0000-0000-000000000000", + "Type": "SystemAssigned", + "UserAssignedIdentities": null + }, "Kind": null, "Location": "region", "ManagedBy": null, @@ -30,7 +35,35 @@ "certificatePassword": null, "negotiateClientCertificate": false, "certificate": null, + "defaultSslBinding": false + }, + { + "type": "Proxy", + "hostName": "api.contoso.com", + "encodedCertificate": null, + "keyVaultId": "https://vault-A.vault.azure.net/secrets/apicert", + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": { + "expiry": "2099-01-01T12:00:00+00:00", + "thumbprint": "0000000000000000000000000000000000000000", + "subject": "CN=api.contoso.com, OU=A, O=O, L=L, S=S, C=C" + }, "defaultSslBinding": true + }, + { + "type": "Management", + "hostName": "management.contoso.com", + "encodedCertificate": null, + "keyVaultId": "https://vault-A.vault.azure.net/secrets/managementcert", + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": { + "expiry": "2099-01-01T12:00:00+00:00", + "thumbprint": "0000000000000000000000000000000000000000", + "subject": "CN=management.contoso.com, OU=A, O=O, L=L, S=S, C=C" + }, + "defaultSslBinding": false } ], "publicIPAddresses": [ @@ -252,6 +285,116 @@ "Sku": null, "Tags": null, "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/product-A", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/product-A", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "product-A", + "Name": "product-A", + "ExtensionResourceName": null, + "Properties": { + "displayName": "product-A", + "description": "An example product for unit tests.", + "terms": null, + "subscriptionRequired": true, + "approvalRequired": true, + "subscriptionsLimit": null, + "state": "published" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/product-B", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/product-B", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "product-B", + "Name": "product-B", + "ExtensionResourceName": null, + "Properties": { + "displayName": "product-B", + "description": "An example product for unit tests.", + "terms": null, + "subscriptionRequired": true, + "approvalRequired": true, + "subscriptionsLimit": null, + "state": "notPublished" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/starter", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/starter", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "starter", + "Name": "starter", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "displayName": "Starter", + "description": "Subscribers will be able to run 5 calls/minute up to a maximum of 100 calls/week.", + "terms": "", + "subscriptionRequired": true, + "approvalRequired": false, + "subscriptionsLimit": 1, + "state": "notPublished" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/unlimited", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-A/products/unlimited", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "unlimited", + "Name": "unlimited", + "ExtensionResourceName": null, + "Properties": { + "displayName": "Unlimited", + "description": "Subscribers have completely unlimited access to the API. Administrator approval is required.", + "terms": null, + "subscriptionRequired": true, + "approvalRequired": true, + "subscriptionsLimit": 1, + "state": "notPublished" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" } ] }, @@ -401,6 +544,60 @@ "ChangedTime": null, "ETag": null }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/products/product-A", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/products/product-A", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "product-A", + "Name": "product-A", + "ExtensionResourceName": null, + "Properties": { + "displayName": "product-A", + "description": "An example product for unit tests.", + "terms": null, + "subscriptionRequired": true, + "approvalRequired": true, + "subscriptionsLimit": null, + "state": "published" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/products/product-B", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/products/product-B", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "product-B", + "Name": "product-B", + "ExtensionResourceName": null, + "Properties": { + "displayName": "product-B", + "description": "An example product for unit tests.", + "terms": null, + "subscriptionRequired": false, + "approvalRequired": true, + "subscriptionsLimit": null, + "state": "notPublished" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, { "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/backends/endpoint-A", "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/backends/endpoint-A", @@ -483,8 +680,8 @@ "SubscriptionId": "00000000-0000-0000-0000-000000000000" }, { - "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/properties/property-B", - "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/properties/property-B", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/namedValues/property-B", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/namedValues/property-B", "Identity": null, "Kind": null, "Location": null, @@ -501,8 +698,348 @@ "secret": true }, "ResourceGroupName": "rg-test", - "Type": "Microsoft.ApiManagement/service/properties", - "ResourceType": "Microsoft.ApiManagement/service/properties", + "Type": "Microsoft.ApiManagement/service/namedValues", + "ResourceType": "Microsoft.ApiManagement/service/namedValues", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + } + ] + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C", + "Identity": { + "PrincipalId": "00000000-0000-0000-0000-000000000000", + "TenantId": "00000000-0000-0000-0000-000000000000", + "Type": "UserAssigned", + "UserAssignedIdentities": null + }, + "Kind": null, + "Location": "region", + "ManagedBy": null, + "ResourceName": "apim-C", + "Name": "apim-C", + "Plan": null, + "Properties": { + "publisherEmail": "email", + "publisherName": "n/a", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "provisioningState": "Succeeded", + "targetProvisioningState": "", + "gatewayUrl": "https://apim-C.azure-api.net", + "gatewayRegionalUrl": "https://apim-C-region-01.regional.azure-api.net", + "portalUrl": "https://apim-C.portal.azure-api.net", + "developerPortalUrl": "https://apim-C.developer.azure-api.net", + "managementApiUrl": "https://apim-C.management.azure-api.net", + "scmUrl": "https://apim-C.scm.azure-api.net", + "hostnameConfigurations": [ + { + "type": "Proxy", + "hostName": "apim-C.azure-api.net", + "encodedCertificate": null, + "keyVaultId": null, + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": null, + "defaultSslBinding": false + }, + { + "type": "Proxy", + "hostName": "api.contoso.com", + "encodedCertificate": null, + "keyVaultId": "https://vault-A.vault.azure.net/secrets/apicert", + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": { + "expiry": "2020-01-01T12:00:00+00:00", + "thumbprint": "0000000000000000000000000000000000000000", + "subject": "CN=api.contoso.com, OU=A, O=O, L=L, S=S, C=C" + }, + "defaultSslBinding": true + }, + { + "type": "Management", + "hostName": "management.contoso.com", + "encodedCertificate": null, + "keyVaultId": "https://vault-A.vault.azure.net/secrets/managementcert", + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": { + "expiry": "2099-01-01T12:00:00+00:00", + "thumbprint": "0000000000000000000000000000000000000000", + "subject": "CN=management.contoso.com, OU=A, O=O, L=L, S=S, C=C" + }, + "defaultSslBinding": false + } + ], + "publicIPAddresses": [ + "0.0.0.0" + ], + "privateIPAddresses": null, + "additionalLocations": null, + "virtualNetworkConfiguration": null, + "customProperties": { + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Protocols.Server.Http2": "False" + }, + "virtualNetworkType": "None", + "certificates": null + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service", + "ResourceType": "Microsoft.ApiManagement/service", + "Sku": { + "Name": "Developer", + "Tier": null, + "Size": null, + "Family": null, + "Model": null, + "Capacity": 1 + }, + "Tags": {}, + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "resources": [ + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/apis/api-A", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/apis/api-A", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "api-A", + "Name": "api-A", + "Plan": null, + "Properties": { + "displayName": "api-A", + "apiRevision": "1", + "description": "Azure Logic App.", + "serviceUrl": "https://endpoint-a.contoso.com", + "path": "api-a", + "protocols": [ + "https" + ], + "authenticationSettings": { + "oAuth2": null, + "openid": null + }, + "subscriptionKeyParameterNames": { + "header": "Ocp-Apim-Subscription-Key", + "query": "subscription-key" + }, + "isCurrent": true + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/apis", + "ResourceType": "Microsoft.ApiManagement/service/apis", + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/apis/api-B", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/apis/api-B", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "api-B", + "Name": "api-B", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "displayName": "api-B", + "apiRevision": "1", + "description": "", + "subscriptionRequired": false, + "serviceUrl": "https://endpoint-b.contoso.com", + "path": "api-b", + "protocols": [ + "https" + ], + "authenticationSettings": { + "oAuth2": null, + "openid": null + }, + "subscriptionKeyParameterNames": { + "header": "Ocp-Apim-Subscription-Key", + "query": "subscription-key" + }, + "isCurrent": true + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/apis", + "ResourceType": "Microsoft.ApiManagement/service/apis", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "CreatedTime": null, + "ChangedTime": null, + "ETag": null + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/products/product-A", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/products/product-A", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "product-A", + "Name": "product-A", + "ExtensionResourceName": null, + "Properties": { + "displayName": "product-A", + "description": "An example product for unit tests.", + "terms": null, + "subscriptionRequired": true, + "approvalRequired": true, + "subscriptionsLimit": null, + "state": "published" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/products/product-B", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/products/product-B", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "product-B", + "Name": "product-B", + "ExtensionResourceName": null, + "Properties": { + "displayName": "product-B", + "description": "An example product for unit tests.", + "terms": null, + "subscriptionRequired": false, + "approvalRequired": true, + "subscriptionsLimit": null, + "state": "notPublished" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/products", + "ResourceType": "Microsoft.ApiManagement/service/products", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/backends/endpoint-A", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/backends/endpoint-A", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "endpoint-A", + "Name": "endpoint-A", + "Plan": null, + "Properties": { + "title": null, + "description": "endpoint-A", + "url": "https://endpoint-a.contoso.com", + "protocol": "http", + "resourceId": "" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/backends", + "ResourceType": "Microsoft.ApiManagement/service/backends", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/backends/endpoint-B", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/backends/endpoint-B", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "endpoint-B", + "Name": "endpoint-B", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "title": null, + "description": "endpoint-B", + "url": "https://endpoint-b.contoso.com", + "protocol": "http", + "resourceId": "" + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/backends", + "ResourceType": "Microsoft.ApiManagement/service/backends", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "CreatedTime": null, + "ChangedTime": null, + "ETag": null + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/namedValues/property-A", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/namedValues/property-A", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "property-A", + "Name": "property-A", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "displayName": "property-A", + "tags": [], + "secret": true + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/namedValues", + "ResourceType": "Microsoft.ApiManagement/service/namedValues", + "ExtensionResourceType": null, + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/namedValues/property-B", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-C/namedValues/property-B", + "Identity": null, + "Kind": null, + "Location": null, + "ManagedBy": null, + "ResourceName": "property-B", + "Name": "property-B", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "displayName": "property-B", + "tags": null, + "secret": false + }, + "ResourceGroupName": "rg-test", + "Type": "Microsoft.ApiManagement/service/namedValues", + "ResourceType": "Microsoft.ApiManagement/service/namedValues", "ExtensionResourceType": null, "Sku": null, "Tags": null,