Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
- Defender for Cloud:
- Check that Microsoft Defender Cloud Security Posture Management is using `Standard` plan by @BenjaminEngeset.
[#2151](https://github.com/Azure/PSRule.Rules.Azure/issues/2151)
- API Management:
- Check that base element for any policy element in a section is configured by @BenjaminEngeset.
[#2072](https://github.com/Azure/PSRule.Rules.Azure/issues/2072)
- Updated rules:
- Container Apps:
- Promoted `Azure.ContainerApp.Insecure` to GA rule set by @BernieWhite.
Expand Down
93 changes: 93 additions & 0 deletions docs/en/rules/Azure.APIM.PolicyBase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
severity: Important
pillar: Security
category: Design
resource: API Management
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.APIM.PolicyBase/
---

# Base element

## SYNOPSIS

Base element for any policy element in a section should be configured.

## DESCRIPTION

Determine the policy evaluation order by placement of the base (`<base />`) element in each section in the policy definition at each scope.

API Management supports the following scopes _Global_ (all API), _Workspace_, _Product_, _API_, or _Operation_.

The _base_ element inherits the policies configured in that section at the next broader (parent) scope.
Otherwise inherited security or other controls may not apply.
The _base_ element can be placed before or after any policy element in a section, depending on the wanted evaluation order.
However, if security controls are defined in inherited scopes it may decrease the effectiveness of these controls.
For most cases, unless otherwise specified in the policy reference (such as `cors`) the _base_ element should be specified as the first element in each section.

A specific exception is at the _Global_ scope.
The _Global_ scope does not need the _base_ element because this is the peak scope from which all others inherit.

## RECOMMENDATION

Consider configuring the base element for any policy element in a section.

## EXAMPLES

### Configure with Azure template

To deploy API Management policies that pass this rule:

- Configure an policy sub-resource.
- Configure the base element before or after any policy element in a section in `properties.value` property.

For example an API policy:

```json
{
"type": "Microsoft.ApiManagement/service/apis/policies",
"apiVersion": "2021-08-01",
"name": "[format('{0}/{1}', parameters('name'), 'policy')]",
"properties": {
"value": "<policies><inbound><base /><ip-filter action=\"allow\"><address-range from=\"10.1.0.1\" to=\"10.1.0.255\" /></ip-filter></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>",
"format": "xml"
},
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service/apis', parameters('name'))]"
],
}
```

### Configure with Bicep

To deploy API Management policies that pass this rule:

- Configure an policy sub-resource.
- Configure the base element before or after any policy element in a section in `properties.value` property.

For example an API policy:

```bicep
resource apiName_policy 'Microsoft.ApiManagement/service/apis/policies@2021-08-01' = {
parent: api
name: 'policy'
properties: {
value: '<policies><inbound><base /><ip-filter action=\"allow\"><address-range from=\"10.1.0.1\" to=\"10.1.0.255\" /></ip-filter></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>'
format: 'xml'
}
}
```

## NOTES

The rule only checks against `rawxml` and `xml` policy formatted content. Global policies are excluded since they don't benefit from the base element.

## LINKS

- [Secure application configuration and dependencies](https://learn.microsoft.com/azure/well-architected/security/design-app-dependencies)
- [Things to know](https://learn.microsoft.com/azure/api-management/api-management-howto-policies#things-to-know)
- [Mitigate OWASP API threats](https://learn.microsoft.com/azure/api-management/mitigate-owasp-api-threats#recommendations-6)
- [Apply policies specified at different scopes](https://learn.microsoft.com/azure/api-management/api-management-howto-policies#apply-policies-specified-at-different-scopes)
- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.apimanagement/service/apis/resolvers/policies)
- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.apimanagement/service/products/policies)
- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.apimanagement/service/apis/policies)
- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.apimanagement/service/apis/operations/policies)
19 changes: 17 additions & 2 deletions src/PSRule.Rules.Azure/rules/Azure.APIM.Rule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,19 @@ Rule 'Azure.APIM.CORSPolicy' -Ref 'AZR-000365' -Type 'Microsoft.ApiManagement/se
}
}

# Synopsis: Base element for any policy element in a section should be configured.
Rule 'Azure.APIM.PolicyBase' -Ref 'AZR-000371' -Type 'Microsoft.ApiManagement/service', 'Microsoft.ApiManagement/service/apis', 'Microsoft.ApiManagement/service/apis/resolvers', 'Microsoft.ApiManagement/service/apis/operations', 'Microsoft.ApiManagement/service/apis/resolvers/policies', 'Microsoft.ApiManagement/service/products/policies', 'Microsoft.ApiManagement/service/apis/policies',
'Microsoft.ApiManagement/service/apis/operations/policies' -If { $Null -ne (GetAPIMPolicyNode -Node 'policies' -IgnoreGlobal) } -Tag @{ release = 'GA'; ruleSet = '2023_06'; } {
$policies = GetAPIMPolicyNode -Node 'policies' -IgnoreGlobal
foreach ($policy in $policies) {
Write-Debug "Got policy: $($policy.OuterXml)"

$Assert.HasField($policy.inbound, 'base').PathPrefix('inbound')
$Assert.HasField($policy.backend, 'base').PathPrefix('backend')
$Assert.HasField($policy.outbound, 'base').PathPrefix('outbound')
$Assert.HasField($policy.'on-error', 'base').PathPrefix('on-error')
}
}
#endregion Rules

#region Helper functions
Expand All @@ -336,7 +349,9 @@ function global:GetAPIMPolicyNode {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$Node
[string]$Node,

[switch]$IgnoreGlobal
)
process {
$policies = @($TargetObject)
Expand All @@ -345,7 +360,7 @@ function global:GetAPIMPolicyNode {
Write-Debug "[GetAPIMPolicyNode] - Found $($policies.Count) policy nodes."
}
$policies | ForEach-Object {
if ($_.properties.format -in 'rawxml', 'xml' -and $_.properties.value) {
if (!($IgnoreGlobal -and $_.type -eq 'Microsoft.ApiManagement/service/policies') -and $_.properties.format -in 'rawxml', 'xml' -and $_.properties.value) {
$xml = [Xml]$_.properties.value
$xml.SelectNodes("//${Node}")
}
Expand Down
18 changes: 18 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Azure.APIM.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,24 @@ Describe 'Azure.APIM' -Tag 'APIM' {
$ruleResult.Length | Should -Be 2;
$ruleResult.TargetName | Should -BeIn 'apim-E', 'policy-B';
}

It 'Azure.APIM.PolicyBase' {
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.APIM.PolicyBase' };

# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 4;
$ruleResult.TargetName | Should -BeIn 'apim-B', 'apim-C', 'api-policy-A', 'api-policy-B';

$ruleResult[0].Reason | Should -BeIn "Path inbound.base: Does not exist.", "Path backend.base: Does not exist.", "Path outbound.base: Does not exist.", "Path on-error.base: Does not exist.";

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 2;
$ruleResult.TargetName | Should -BeIn 'apim-D', 'api-policy-C';
}
}

Context 'With Template' {
Expand Down
192 changes: 192 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Resources.APIM.json
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,30 @@
"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/apis/api-B/policies/api-policy-A",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apis/api-B/policies/api-policy-A",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-A",
"Name": "api-policy-A",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><ip-filter action=\"allow\"><address-range from=\"51.175.196.186\" to=\"51.175.196.186\" /></ip-filter></inbound><backend></backend><outbound></outbound><on-error></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"ExtensionResourceType": null,
"Sku": null,
"Tags": null,
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
}
]
},
Expand Down Expand Up @@ -1159,6 +1183,78 @@
"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/policies/api-policy-A",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apim-C/apis/api-B/policies/api-policy-A",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-A",
"Name": "api-policy-A",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><ip-filter action=\"allow\"><address-range from=\"51.175.196.186\" to=\"51.175.196.186\" /></ip-filter></inbound><backend></backend><outbound></outbound><on-error></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"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/apis/api-B/policies/api-policy-B",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apim-C/apis/api-B/policies/api-policy-B",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-B",
"Name": "api-policy-B",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><base /><ip-filter action=\"allow\"><address-range from=\"51.175.196.187\" to=\"51.175.196.187\" /></ip-filter></inbound><backend><base /></backend><outbound></outbound><on-error></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"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/apis/api-B/policies/api-policy-C",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apim-C/apis/api-B/policies/api-policy-C",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-C",
"Name": "api-policy-C",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><base /><ip-filter action=\"allow\"><address-range from=\"51.175.196.188\" to=\"51.175.196.188\" /></ip-filter></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"ExtensionResourceType": null,
"Sku": null,
"Tags": null,
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
}
]
},
Expand Down Expand Up @@ -1320,6 +1416,30 @@
"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-D/apis/api-B/policies/api-policy-A",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apim-D/apis/api-B/policies/api-policy-A",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-A",
"Name": "api-policy-A",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><base /><ip-filter action=\"allow\"><address-range from=\"51.175.196.188\" to=\"51.175.196.188\" /></ip-filter></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"ExtensionResourceType": null,
"Sku": null,
"Tags": null,
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
}
]
},
Expand Down Expand Up @@ -2941,5 +3061,77 @@
"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-P/apis/api-A/policies/api-policy-A",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apim-P/apis/api-A/policies/api-policy-A",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-A",
"Name": "api-policy-A",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><ip-filter action=\"allow\"><address-range from=\"51.175.196.186\" to=\"51.175.196.186\" /></ip-filter></inbound><backend></backend><outbound></outbound><on-error></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"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-P/apis/api-A/policies/api-policy-B",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apim-P/apis/api-A/policies/api-policy-B",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-B",
"Name": "api-policy-B",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><ip-filter action=\"allow\"><address-range from=\"51.175.196.187\" to=\"51.175.196.187\" /></ip-filter></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"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-P/apis/api-A/policies/api-policy-C",
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/apim-B/apim-P/apis/api-A/policies/api-policy-C",
"Identity": null,
"Kind": null,
"Location": null,
"ManagedBy": null,
"ResourceName": "api-policy-C",
"Name": "api-policy-C",
"ExtensionResourceName": null,
"ParentResource": null,
"Plan": null,
"Properties": {
"value": "<policies><inbound><base /><ip-filter action=\"allow\"><address-range from=\"51.175.196.188\" to=\"51.175.196.188\" /></ip-filter></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>",
"format": "xml"
},
"ResourceGroupName": "rg-test",
"Type": "Microsoft.ApiManagement/service/apis/policies",
"ResourceType": "Microsoft.ApiManagement/service/apis/policies",
"ExtensionResourceType": null,
"Sku": null,
"Tags": null,
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
}
]