Using $Assert and/or PSRule expressions in Conventions #3316
-
Using PSRule to validate infrastructure dependencies after processing all objects I have a scenario where validating Azure infrastructure requires the full context of resources after the pipeline has completed processing all objects. Specifically, consider cases where examining each Azure resource independently is insufficient—for example, determining whether each Azure Function App has exactly one private endpoint attached, or verifying that private endpoints are correctly integrated with specific virtual networks. This type of validation requires having processed all related resources beforehand to build a complete dependency graph. To accomplish this, I’m currently using Conventions, leveraging the -Process block to incrementally construct a graph as each resource object is processed, and the -End block to perform validations once all resources are available. However, at the point when the -End block runs, it appears we no longer have access to $Assert methods, meaning we can’t directly pass or fail rules based on the completed graph. Is there a recommended or supported way in PSRule to execute validations at the end of pipeline execution with access to $Assert, or is there an alternative best practice for this type of scenario? What would be the ideal approach here? You can look at the code in this repo, or try it in the rules look like this Import-Module PSQuickGraph
$global:vnetPrefix = '10.9.0.0/16'
$global:subnetNames = "my-vnet/function-integration-subnet", "my-vnet/private-endpoint-subnet"
$global:subnetPrefixes = "10.9.1.0/24", "10.9.2.0/24"
# store VNET Id found in the template
$global:vnetId = $null
$global:webSites = @()
$global:connectionGraph = New-Graph
$global:pvtEndpointGraph = New-Graph
# Synopsis: Infrastructure should include a VNET
Rule 'local.Network.Exists' -Type 'Microsoft.Network/virtualNetworks' {
# this aims to check if network exisists as a resource
$TargetObject | Exists -Field name
$TargetObject | Exists -Field id
$Assert.HasFieldValue($TargetObject, 'properties.addressSpace.addressPrefixes[0]', $global:vnetPrefix)
}
# Synopsis: Infrastructure should include subnets: "my-vnet/function-integration-subnet", "my-vnet/private-endpoint-subnet"
Rule 'local.Network.Subnets.Exist' -Type 'Microsoft.Network/virtualNetworks' {
# this aims to check if network has two subnets with matching names
$subnetChecks = @()
$TargetObject.resources.name | % {
Write-Verbose "Subnet name: $_; Subnet names: $global:subnetNames; Test result: $($_ -in $global:subnetNames)"
$subnetChecks += $_ -in $global:subnetNames
}
if ($subnetChecks -notcontains $false) {
$Assert.Pass()
}
else {
$Assert.Fail()
}
}
# Synopsis: Subnets should have prefixes: "10.9.1.0/24", "10.9.2.0/24"
Rule 'local.Network.Subnets.Prefixes' -Type 'Microsoft.Network/virtualNetworks' {
# this aims to check if network has two subnets with matching names
$subnetChecks = @()
$TargetObject.resources.properties.addressprefix | % {
$subnetChecks += $_ -in $global:subnetPrefixes
}
if ($subnetChecks -notcontains $false) {
$Assert.Pass()
}
else {
$Assert.Fail()
}
}
# Synopsis: Infrastructure should include a ServiceBus
Rule 'local.ServiceBus.Exists' -Type 'Microsoft.ServiceBus/namespaces' {
# this aims to check if network exisists as a resource
# $Assert.NotNull($TargetObject, "name")
$TargetObject | Exists -Field 'name'
}
# Synopsis: The ServiceBus should be of the Standard SKU/Tier
Rule 'local.ServiceBus.Sku' -Type 'Microsoft.ServiceBus/namespaces' {
# this aims to check if network exisists as a resource
$TargetObject | Match 'sku.name' 'Standard'
$TargetObject | Match 'sku.tier' 'Standard'
# $Assert.HasFieldValue($TargetObject, 'sku.name', 'Standard')
# $Assert.HasFieldValue($TargetObject, 'sku.tier', 'Standard')
}
# Synopsis: Web Apps should have Public network access disabled
Rule "local.WebSite.PublicAccess.Disabled" -Type 'Microsoft.Web/sites' {
$TargetObject | Match 'properties.publicNetworkAccess' 'Disabled'
}
# Synopsis: Web Apps should have VNET integration tuned on
Rule "local.WebSite.VnetIntegration.Configured" -Type 'Microsoft.Web/sites' {
$TargetObject | Exists -Field name
$vnetIntegrationObject = $TargetObject.resources |
Where-Object { $_.Type -eq 'Microsoft.Web/sites/networkConfig' }
# $vnetIntegrationObject | ConvertTo-Json -Depth 99 | Out-File xxx.json
$Assert.NotNull($vnetIntegrationObject, 'name')
$Assert.EndsWith($vnetIntegrationObject, 'properties.subnetResourceId', 'function-integration-subnet')
}
# Synopsis: All functions have to have a
Export-PSRuleConvention 'FullConnectivityTest' `
-Process {
Write-Verbose "Convention process block triggered"
if ($TargetObject.type -eq 'Microsoft.Network/virtualNetworks') {
Write-Verbose "Convention triggered for a VNET $($TargetObject.name)"
$global:vnetId = $TargetObject.id
foreach ($subnetResource in $TargetObject.resources) {
Add-Edge -From $subnetResource.id -To $TargetObject.id -Graph $global:connectionGraph
Add-Edge -From $subnetResource.id -To $TargetObject.id -Graph $global:pvtEndpointGraph
}
}
if ($TargetObject.type -eq 'Microsoft.Web/sites') {
$vnetIntegrationObject = $TargetObject.resources |
Where-Object { $_.Type -eq 'Microsoft.Web/sites/networkConfig' }
$global:webSites += $TargetObject.id
Add-Vertex -Graph $global:connectionGraph -Vertex $TargetObject.id
Add-Vertex -Graph $global:pvtEndpointGraph -Vertex $TargetObject.id
Add-Edge -From $TargetObject.id -To $vnetIntegrationObject.properties.subnetResourceId -Graph $global:connectionGraph
}
if ($TargetObject.type -eq 'Microsoft.Network/privateEndpoints') {
foreach ($subnet in $TargetObject.Properties.subnet) {
Write-Verbose "Edge: $($TargetObject.id) -> $($TargetObject.Properties.subnet.id)"
Add-Edge -From $TargetObject.id -To $subnet.id -Graph $global:pvtEndpointGraph
}
foreach ($link in $TargetObject.Properties.privateLinkServiceConnections) {
Write-Verbose "Edge: $($link.properties.privateLinkServiceId) -> $($TargetObject.id)"
Add-Edge -From $link.properties.privateLinkServiceId -To $TargetObject.id -Graph $global:pvtEndpointGraph
}
}
}`
-End {
Write-Verbose "Exporting graph"
Export-Graph -Graph $global:connectionGraph -Format MSAGL_SUGIYAMA -Path ./output/graph.svg
Export-Graph -Graph $global:pvtEndpointGraph -Format MSAGL_SUGIYAMA -Path ./output/pvtEndpointGraph.svg
# There should be only one VNET
if ($global:vnetId.Count -ne 1) {
Throw "There should be exactly 1 VNET"
}
# All web apps should end up in the VNET via VNET Integration Path
foreach($webApp in $global:webSites){
$p = Get-GraphPath -From $webApp -To $global:vnetId -Graph $global:connectionGraph
if ($null -eq $p) {
Throw "There should be a path from a Function App to the VNET"
}
}
# All web apps should have only one Private Endpoint Connection
foreach($webApp in $global:webSites){
$vertex = $global:pvtEndpointGraph.Vertices | ? { $_ -eq $webApp }
$outEdges = $global:pvtEndpointGraph.OutEdges($vertex)
if ($outEdges.Count -ne 1) {
Throw "There should be exactly one Private Endpoint attached to a Web Site: $vertex"
}
}
# All web apps should only have Private Endpoints in the specific VNET
foreach($webApp in $global:webSites){
$p = Get-GraphPath -From $webApp -To $global:vnetId -Graph $global:pvtEndpointGraph
if ($null -eq $p) {
Throw "There should be a path from a Function App to the VNET"
}
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
@eosfor Convention lifecycle is covered here: https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Conventions/ and In short, assertion is intended to be run within a rule, because it controls the pass or fail of a rule. So, you are correct You can generate errors in conventions, but they are treated as errors instead of a rule failure. Within a convention It's non-trivial but you could do something like this: $Global:FunctionMap = @{}
# Synopsis: Build an object to represent the function app and its private endpoints connection
Export-PSRuleConvention 'FunctionConnectivity' -If { $PSRule.TargetType -eq 'Microsoft.Web/sites' -or $PSRule.TargetType -eq 'Microsoft.Network/privateEndpoints' } -Begin {
# Track function connectivity
if ($PSRule.TargetType -eq 'Microsoft.Web/sites') {
$Global:FunctionMap[$TargetObject.id.ToLower()] = [PSCustomObject]@{
PrivateEndpoints = @()
}
$PSRule.ImportWithType('FunctionPrivateEndpoints', [PSCustomObject]@{
Name = $PSRule.TargetName
Id = $TargetObject.id
})
}
elseif ($PSRule.TargetType -eq 'Microsoft.Network/privateEndpoints') {
# Track and link private endpoints to function apps
foreach ($connection in $TargetObject.Properties.privateLinkServiceConnections) {
$functionAppId = $connection.properties.privateLinkServiceId.ToLower()
$connection = [PSCustomObject]@{
Name = $connection.name
SubnetId = $TargetObject.Properties.subnet.id
}
if ($Global:FunctionMap.ContainsKey($functionAppId)) {
$Global:FunctionMap[$functionAppId].PrivateEndpoints += $connection
}
}
}
}
# Synopsis: All functions have to have a private endpoint connection to the VNET
Rule 'local.FunctionApp.PE' -Type 'FunctionPrivateEndpoints' {
$Assert.Count($TargetObject, 'PrivateEndpoints', 1).Reason('The Function App should have exactly one private endpoint connection but found {0}.', $TargetObject.PrivateEndpoints.Length)
foreach ($privateEndpoint in $TargetObject.PrivateEndpoints) {
$Assert.EndsWith($privateEndpoint, 'SubnetId', '/function-integration-subnet').Reason('The Function App should be connected to the function-integration-subnet but found {0}.', $privateEndpoint.SubnetId)
}
} However, the current object model didn't really consider walking a resource graph to related objects, and there have been minimal asks from the community for this feature. There is an open discussion here: #1984 so if you have use cases, you can add them there. |
Beta Was this translation helpful? Give feedback.
@eosfor Convention lifecycle is covered here: https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Conventions/ and
$Assert
is covered here: https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Variables/#assert.In short, assertion is intended to be run within a rule, because it controls the pass or fail of a rule. So, you are correct
$Assert
is not available within a convention, or at other times.You can generate errors in conventions, but they are treated as errors instead of a rule failure.
Throw
is one way, alsoWrite-Error
.Within a convention
-Initialize
and-Begin
blocks you can create new custom objects that will be processed by the pipeline.