Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Azure.APIM.PolicyBase #2140

Merged
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)
BenjaminEngeset marked this conversation as resolved.
Show resolved Hide resolved
- [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',
BenjaminEngeset marked this conversation as resolved.
Show resolved Hide resolved
'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"
}
]