AzPolicyLens.Wiki.Azure.Helper.psm1
|
<# ===================================================================================================== AUTHOR: Tao Yang DATE: 11/09/2025 NAME: AzPolicyLens.Wiki.Azure.Helper.psm1 VERSION: 2.0.0 COMMENT: this nested module contains the Azure related helper functions for AzPolicyLens.Wiki module ===================================================================================================== #> using module ./AzPolicyLens.Wiki.Utility.Helper.psm1 using module ./AzPolicyLens.Wiki.Generic.Helper.psm1 #function to build initiative and definition json definitions from the ARG search results function buildPolicyDefinitionJson { [CmdletBinding()] [OutputType([string])] param ( [parameter(Mandatory = $true)] [System.Collections.Specialized.IOrderedDictionary] $PolicyDefinition ) Write-Verbose "[$(getCurrentUTCString)]: Building JSON for policy definition '$($PolicyDefinition.name)'." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) $json = $PolicyDefinition | ConvertTo-Json -Depth 99 #tidy up $json = RemoveNullPropertiesFromJson -JsonString $json -Recurse -RemoveEmptyArrays -RemoveEmptyStrings $json } #function to detect the policy effect (borrowed from AzPolicyTest module) function getPolicyEffect { param( [object] $policyObject ) $parameterRegex = "^\[parameters\(\'(\S+)\'\)\]$" $effect = $policyObject.properties.policyRule.then.effect #check if the effect is a parameterised value if ($effect -imatch $parameterRegex) { $effectParameterName = $matches[1] $policyEffectAllowedValues = $policyObject.properties.parameters.$effectParameterName.allowedValues $policyEffectDefaultValue = $policyObject.properties.parameters.$effectParameterName.defaultValue $effects = @() if ($policyEffectAllowedValues) { $effects += $policyEffectAllowedValues } else { $effects += $policyEffectDefaultValue } $result = @{ effects = $effects defaultEffectValue = $policyEffectDefaultValue isHardCoded = $false } } else { $result = @{ effects = @($effect) defaultEffectValue = $null isHardCoded = $true } } $result } #function to get the policy definition group details from initiatives function getPolicyDefinitionGroupsFromInitiatives { [CmdletBinding()] [OutputType([System.Object[]])] param ( [parameter(Mandatory = $true)] [System.Object[]] $initiatives ) $policyDefinitionGroups = [System.Collections.Generic.List[object]]::new() $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($initiative in $initiatives) { if ($initiative.properties.policyDefinitionGroups) { foreach ($pdg in $initiative.properties.policyDefinitionGroups) { $key = "$($pdg.name)|$($pdg.additionalMetadataId)" if ($seen.Add($key)) { $policyDefinitionGroups.Add($pdg) } } } } $policyDefinitionGroups | sort-object -property 'name' } #function to get unused policy definition group from an initiative function getUnusedPolicyDefinitionGroupsFromInitiative { [CmdletBinding()] [OutputType([Array])] param ( [parameter(Mandatory = $true)] [System.Object] $initiative ) $unusedPolicyDefinitionGroupNames = @() $allPolicyDefinitionGroupNames = ($initiative.properties.policyDefinitionGroups).name $usedPolicyDefinitionGroupNames = $initiative.properties.policyDefinitions.groupNames | Get-Unique foreach ($n in $allPolicyDefinitionGroupNames) { if (-not $usedPolicyDefinitionGroupNames.Contains($n)) { $unusedPolicyDefinitionGroupNames += $n } } , $unusedPolicyDefinitionGroupNames } #function to get mapped policies for a policy definition group function getMappedPoliciesForPolicyDefinitionGroup { [CmdletBinding()] [OutputType([hashtable])] param ( [parameter(Mandatory = $true, ParameterSetName = 'ByName')] [string]$policyDefinitionGroupName, [parameter(Mandatory = $true, ParameterSetName = 'ByPolicyMetadataId')] [string]$policyMetadataId, [parameter(Mandatory = $true)] [System.Array] $initiatives ) $builtInDefinitionResourceIdRegex = '(?im)^\/providers\/microsoft\.authorization\/policydefinitions\/' $mapping = @{ definedInitiatives = [System.Collections.Generic.List[object]]::new() mappedPolicies = [System.Collections.Generic.List[object]]::new() } foreach ($initiative in $initiatives) { Write-Verbose "Processing initiative '$($initiative.id)'" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) if ($PSCmdlet.ParameterSetName -eq 'ByName' -and $initiative.properties.policyDefinitionGroups) { Write-Verbose " - searching for policy definition group with name '$policyDefinitionGroupName'." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) $pdg = $initiative.properties.policyDefinitionGroups | Where-Object { $_.name -ieq $policyDefinitionGroupName } } elseif ($PSCmdlet.ParameterSetName -eq 'ByPolicyMetadataId' -and $initiative.properties.policyDefinitionGroups) { Write-Verbose " - searching for policy definition group with additionalMetadataIdId '$policyMetadataId'." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) $pdg = $initiative.properties.policyDefinitionGroups | Where-Object { $_.additionalMetadataId -ieq $policyMetadataId } } if ($pdg) { #Go through the member definitions [void]$mapping.definedInitiatives.Add(@{ policyDefinitionGroupName = $pdg.name policySetDefinitionId = $initiative.id policySetName = $initiative.name policySetDisplayName = $initiative.properties.displayName policySetMetadata = $initiative.properties.metadata type = $initiative.properties.policyType isInUse = $initiative.isInUse }) foreach ($definitionConfig in $initiative.properties.policyDefinitions) { if ($definitionConfig.groupNames -icontains $pdg.name) { #found a mapped policy Write-Verbose " - Found mapped policy '$($definitionConfig.policyDefinitionReferenceId)' in initiative '$($initiative.id)'" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) if ($definitionConfig.policyDefinitionId -match $builtInDefinitionResourceIdRegex) { $policyDefinitionType = 'BuiltIn' } else { $policyDefinitionType = 'Custom' } [void]$mapping.mappedPolicies.Add(@{ policyDefinitionGroupName = $pdg.name policySetName = $initiative.Name policySetDisplayName = $initiative.properties.displayName policySetDefinitionId = $initiative.Id policySetIsInUse = $initiative.isInUse policySetType = $initiative.properties.policyType policyDefinitionReferenceId = $definitionConfig.policyDefinitionReferenceId policyDefinitionId = $definitionConfig.policyDefinitionId policyDefinitionType = $policyDefinitionType }) } } } } $mapping } #Function to get subscription ancestor Management Groups function getSubscriptionMgAncestors { [CmdletBinding()] [OutputType([string])] param ( [parameter(Mandatory = $true)] [System.Object[]] $managementGroups, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Object] $TopLevelManagementGroup, [parameter(Mandatory = $true)] [object] $subscription ) #get all the ancestor management groups for the subscription that's under the top level management group $ancestorManagementGroupNames = @() $ancestorManagementGroupNames += $subscription.managementGroupAncestorsChain.name $ancestorManagementGroups = @() foreach ($mg in $managementGroups) { if ($ancestorManagementGroupNames -contains $mg.name -and $mg.tier -ge $TopLevelManagementGroup.tier) { $ancestorManagementGroups += $mg } } $ancestorManagementGroups = $ancestorManagementGroups | Sort-Object 'tier' -Descending $ancestorManagementGroups } #function to get the managed identity principal IDs for assignments function getPolicyAssignmentManagedIdentityPrincipalIds { [CmdletBinding()] [OutputType([object[]])] param ( [parameter(Mandatory = $true)] [System.Object[]] $assignments ) # Get all managed identity principal IDs from policy assignments $assignmentIdentities = @() foreach ($assignment in $assignments) { if ($assignment.identity) { $assignmentIdentity = @{ assignmentId = $assignment.id } if ($assignment.identity -and $assignment.identity.type -ieq 'SystemAssigned') { $assignmentIdentity.Add('systemAssignedPrincipalId', $assignment.identity.principalId) } elseif ($assignment.identity.type -ieq 'UserAssigned') { $userAssignedIdentities = ConvertToOrderedHashtable -inputObject $assignment.identity.userAssignedIdentities $userAssignedIdentityResourceId = $userAssignedIdentities.Keys $usmi = @() foreach ($id in $userAssignedIdentityResourceId) { $usmi += @{ resourceId = $id principalId = $userAssignedIdentities[$id].principalId } } $assignmentIdentity.Add('userAssignedIdentities', $usmi) } $assignmentIdentities += $assignmentIdentity } } $assignmentIdentities } #function to get role assignment details for a given policy assignment function getRoleAssignmentDetailsForPolicyAssignment { [CmdletBinding()] [OutputType([System.Object[]])] param ( [parameter(Mandatory = $true)] [System.Object] $assignment, [parameter(Mandatory = $true)] [AllowNull()] [System.Object[]] $roleAssignments, [parameter(Mandatory = $true)] [AllowNull()] [System.Object[]] $roleDefinitions ) $roleAssignmentDetails = @() if ($roleAssignments) { # get the managed identity principal IDs for the policy assignment $managedIdentityPrincipalIds = getPolicyAssignmentManagedIdentityPrincipalIds -assignments $assignment $principalIds = @() $assignmentRoleAssignments = @() if ($managedIdentityPrincipalIds.systemAssignedPrincipalId) { $principalIds += $managedIdentityPrincipalIds.systemAssignedPrincipalId } if ($managedIdentityPrincipalIds.userAssignedIdentities) { foreach ($usmi in $managedIdentityPrincipalIds.userAssignedIdentities) { $principalIds += $usmi.principalId } } if (-not $principalIds) { Write-Verbose "[$(getCurrentUTCString)]: No managed identity principal IDs found for policy assignment '$($assignment.name)'." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) return $roleAssignmentDetails } Write-Verbose "[$(getCurrentUTCString)]: Found $($principalIds.Count) managed identity principal IDs for policy assignment '$($assignment.name)'." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) # Get role assignments for the managed identity principal IDs foreach ($principalId in $principalIds) { #find the type of the identity based on the principal ID if ($principalId -ieq $managedIdentityPrincipalIds.systemAssignedPrincipalId) { $identityType = 'SystemAssigned' } else { $identityType = 'UserAssigned' } $assignmentRoleAssignments += $roleAssignments | Where-Object { $_.principalId -ieq $principalId } Write-verbose "[$(getCurrentUTCString)]: Found $($assignmentRoleAssignments.Count) role assignments for principal ID '$principalId'." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) foreach ($roleAssignment in $assignmentRoleAssignments) { #Get the definition $roleDefinition = $roleDefinitions | Where-Object { $_.id -ieq $roleAssignment.roleDefinitionId } if (-not $roleDefinition) { Write-Warning "[$(getCurrentUTCString)]: No role definition found for role assignment '$($roleAssignment.id)'." } else { $roleAssignmentDetails += @{ roleAssignmentId = $roleAssignment.id roleDefinitionId = $roleDefinition.id roleDefinitionDisplayName = $roleDefinition.roleName roleDefinitionName = $roleDefinition.name roleDefinitionType = $roleDefinition.type principalId = $roleAssignment.principalId identityType = $identityType managedIdentityType = $assignment.identity.type roleAssignmentScope = $roleAssignment.scope assignmentName = $assignment.name assignmentId = $assignment.id } } } } } else { Write-Verbose "[$(getCurrentUTCString)]: No role assignments from the discovery data." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) } $roleAssignmentDetails } #function to calculate the compliance percentage function getCompliancePercentage { param ( [Parameter(Mandatory = $true)] [int]$CompliantCount, [Parameter(Mandatory = $true)] [int]$NonCompliantCount, [Parameter(Mandatory = $true)] [int]$ExemptCount, [Parameter(Mandatory = $true)] [int]$ConflictCount, [Parameter(Mandatory = $false, HelpMessage = "Number of fractional digits used to round the compliance percentage.")] [ValidateRange(0, 2)] [int]$NumberOfFractionalDigits = 0 ) $TotalCount = $CompliantCount + $NonCompliantCount + $ExemptCount + $ConflictCount $totalCompliantCount = $CompliantCount + $ExemptCount if ($TotalCount -eq 0) { $compliancePercentage = 100 } else { $compliancePercentage = [math]::round(($totalCompliantCount / $TotalCount) * 100, $NumberOfFractionalDigits) } $compliancePercentage } #function to determine the security framework for a given built-in policy metadata function getSecFrameworkForPolicyMetadata { param ( [Parameter(Mandatory = $true)] [System.Array]$mappings, [Parameter(Mandatory = $true)] [string]$policyMetadataId ) $policyMetadataName = $policyMetadataId.Split('/')[-1] #Write-Verbose "[$(getCurrentUTCString)]: Looking up security framework for policy metadata name '$policyMetadataName'." -Verbose $framework = "Uncategorized" #default value if no match found foreach ($mapping in $mappings) { $regex = $mapping.policyMetadataNameRegex if ($policyMetadataName -imatch $regex) { $framework = $mapping.framework break } } #Write-Verbose " - Mapped policy metadata name '$policyMetadataName' to security framework '$framework'." -Verbose $framework } #function to get unique policy categories from assigned initiatives function getUniqueCategoriesFromAssignedInitiatives { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [parameter(Mandatory = $true, HelpMessage = 'The environment discovery data.')] [system.object] $EnvironmentDiscoveryData ) $assignedInitiatives = $EnvironmentDiscoveryData.initiatives | Where-Object { $_.isInUse -eq $true } $allCategories = @{} foreach ($initiative in $assignedInitiatives) { #initial values for compliance counts $compliantCount = 0 $nonCompliantCount = 0 $exemptCount = 0 $conflictCount = 0 #get assignments for the initiative $initiativeAssignments = $EnvironmentDiscoveryData.assignments | Where-Object { $_.properties.policyDefinitionId -ieq $initiative.id } #calculate compliance counts foreach ($assignment in $initiativeAssignments) { $assignmentComplianceData = $EnvironmentDiscoveryData.assignmentCompliance | Where-Object { $_.policyAssignmentId -ieq $assignment.id } foreach ($complianceRecord in $assignmentComplianceData) { $compliantCount = $compliantCount + $complianceRecord.compliantCount $nonCompliantCount = $nonCompliantCount + $complianceRecord.nonCompliantCount $exemptCount = $exemptCount + $complianceRecord.exemptCount $conflictCount = $conflictCount + $complianceRecord.conflictCount } } if ($initiative.properties.metadata -and $initiative.properties.metadata.category) { $category = $initiative.properties.metadata.category $pdgs = @() if ($initiative.properties.policyDefinitionGroups.count -gt 0) { foreach ($item in $initiative.properties.policyDefinitionGroups) { if ($item.additionalMetadataId.length -gt 0) { $pdgs += $item.additionalMetadataId } else { $pdgs += $item.name } } } if ($allCategories.ContainsKey($category)) { $allCategories[$category].policyInitiativeCount++ $allCategories[$category].resourceIds += $initiative.id $allCategories[$category].policyDefinitionGroups += $pdgs $allCategories[$category].compliantCount += $compliantCount $allCategories[$category].nonCompliantCount += $nonCompliantCount $allCategories[$category].exemptCount += $exemptCount $allCategories[$category].conflictCount += $conflictCount } else { $allCategories[$category] = @{ policyInitiativeCount = 1 resourceIds = @($initiative.id) policyDefinitionGroups = $pdgs compliantCount = $compliantCount nonCompliantCount = $nonCompliantCount exemptCount = $exemptCount conflictCount = $conflictCount } } } else { if ($allCategories.ContainsKey('Uncategorized')) { $pdgs = @() if ($initiative.properties.policyDefinitionGroups.count -gt 0) { foreach ($item in $initiative.properties.policyDefinitionGroups) { if ($item.additionalMetadataId.length -gt 0) { $pdgs += $item.additionalMetadataId } else { $pdgs += $item.name } } } $allCategories['Uncategorized'].policyInitiativeCount++ $allCategories['Uncategorized'].resourceIds += $initiative.id $allCategories['Uncategorized'].policyDefinitionGroups += $pdgs $allCategories['Uncategorized'].compliantCount += $compliantCount $allCategories['Uncategorized'].nonCompliantCount += $nonCompliantCount $allCategories['Uncategorized'].exemptCount += $exemptCount $allCategories['Uncategorized'].conflictCount += $conflictCount } else { $allCategories['Uncategorized'] = @{ policyInitiativeCount = 1 resourceIds = @($initiative.id) policyDefinitionGroups = $pdgs compliantCount = $compliantCount nonCompliantCount = $nonCompliantCount exemptCount = $exemptCount conflictCount = $conflictCount } } } } #deduplicate policy definition groups in each category foreach ($key in $allCategories.Keys) { $allCategories[$key].policyDefinitionGroups = $allCategories[$key].policyDefinitionGroups | Get-Unique } # Convert to ordered hashtable sorted by key, with Uncategorized last $orderedCategories = [ordered]@{} $allCategories.GetEnumerator() | Where-Object { $_.Key -ne 'Uncategorized' } | Sort-Object -Property Key | ForEach-Object { $orderedCategories[$_.Key] = $_.Value } # Add Uncategorized at the end if it exists if ($allCategories.ContainsKey('Uncategorized')) { $orderedCategories['Uncategorized'] = $allCategories['Uncategorized'] } $orderedCategories } |