internal/functions/Build-AssignmentDefinitionNode.ps1
function Build-AssignmentDefinitionNode { # Recursive Function param( [hashtable] $PacEnvironment, [hashtable] $ScopeTable, [hashtable] $ParameterFilesCsv, [hashtable] $DefinitionNode, # Current node [hashtable] $AssignmentDefinition, # Collected values in tree branch [hashtable] $CombinedPolicyDetails, [hashtable] $PolicyRoleIds, [hashtable] $RoleDefinitions # Returns a list os completed assignmentValues ) # Each tree branch needs a private copy $definition = Get-DeepCloneAsOrderedHashtable -InputObject $AssignmentDefinition $pacSelector = $PacEnvironment.pacSelector #region nodeName (required) $nodeName = $definition.nodeName if ($DefinitionNode.nodeName) { $nodeName += $DefinitionNode.nodeName } else { $nodeName = "$($nodeName)//Unknown//" Write-Error " Missing nodeName at child of $($nodeName)" $definition.hasErrors = $true } $definition.nodeName = $nodeName #endregion nodeName (required) #region ignoreBranch and enforcementMode # Ignoring a branch can be useful for prep work to an future state # Due to the history of EPAC, there are two ways ignoreBranch and enforcementMode if ($DefinitionNode.ignoreBranch) { # Does not deploy assignment(s), precedes Azure Policy feature enforcementMode Write-Warning " Node $($nodeName): ignoreBranch is legacy, consider using enforcementMode instead." $definition.ignoreBranch = $DefinitionNode.ignoreBranch } if ($DefinitionNode.enforcementMode) { # Does deploy assignment(s), Azure Policy Engine will not evaluate the Policy Assignment $enforcementMode = $DefinitionNode.enforcementMode if ("Default", "DoNotEnforce" -contains $enforcementMode) { $definition.enforcementMode = $enforcementMode } else { Write-Error " Node $($nodeName): enforcementMode must be Default or DoNotEnforce (actual is ""$($enforcementMode))." $definition.hasErrors = $true } } #endregion ignoreBranch and enforcementMode #region assignment (required at least once per branch, concatenate strings) # name (required) # displayName (required) # description (optional) if ($null -ne $DefinitionNode.assignment) { $assignment = $DefinitionNode.assignment if ($null -ne $assignment.name -and ($assignment.name).Length -gt 0 -and $null -ne $assignment.displayName -and ($assignment.displayName).Length -gt 0) { $normalizedAssignment = ConvertTo-HashTable $assignment if (!$normalizedAssignment.ContainsKey("description")) { $normalizedAssignment.description = "" } # Concatenate information $definition.assignment.name += $normalizedAssignment.name $definition.assignment.displayName += $normalizedAssignment.displayName $definition.assignment.description += $normalizedAssignment.description } else { Write-Error " Node $($nodeName): each assignment field must define an assignment name and displayName." $definition.hasErrors = $true } } #endregion assignment (required at least once per branch, concatenate strings) #region definitionEntry or definitionEntryList (required exactly once per branch) $definitionEntry = $DefinitionNode.definitionEntry $definitionEntryList = $DefinitionNode.definitionEntryList $defEntryList = $definition.definitionEntryList if ($null -ne $definitionEntry -or $null -ne $definitionEntryList) { if ($null -eq $defEntryList -and ($null -ne $definitionEntry -xor $null -ne $definitionEntryList)) { # OK; first and only occurrence in tree branch #region validate and normalize definitionEntryList if ($null -ne $definitionEntry) { # Convert to list $definitionEntryList = @( $definitionEntry ) } $normalizedDefinitionEntryList = @() $mustDefineAssignment = $definitionEntryList.Count -gt 1 $itemList = @{} $perEntryNonComplianceMessages = $false foreach ($definitionEntry in $definitionEntryList) { $isValid, $normalizedEntry = Build-AssignmentDefinitionEntry ` -DefinitionEntry $definitionEntry ` -NodeName $nodeName ` -PolicyDefinitionsScopes $PacEnvironment.policyDefinitionsScopes ` -CombinedPolicyDetails $CombinedPolicyDetails ` -MustDefineAssignment:$mustDefineAssignment if ($isValid) { $policyDefinitionId = $normalizedEntry.policyDefinitionId $isPolicySet = $normalizedEntry.isPolicySet if ($isPolicySet) { $itemEntry = @{ shortName = $policyDefinitionId itemId = $policyDefinitionId policySetId = $policyDefinitionId assignmentId = $null } if ($itemList.ContainsKey($policyDefinitionId)) { Write-Error " Node $($nodeName): policySet '$($policyDefinitionId)' is defined more than once in this definitionEntryList." -ErrorAction Stop $definition.hasErrors = $true } else { $null = $itemList.Add($policyDefinitionId, $itemEntry) } } if ($null -ne $normalizedEntry.nonComplianceMessages -and $normalizedEntry.nonComplianceMessages.Count -gt 0) { $perEntryNonComplianceMessages = $true } $normalizedDefinitionEntryList += $normalizedEntry } else { $definition.hasErrors = $true } } #endregion validate and normalize definitionEntryList #region compile flat Policy List for all Policy Sets used in this branch $flatPolicyList = $null $hasPolicySets = $itemList.Count -gt 0 if ($hasPolicySets) { $flatPolicyList = Convert-PolicyResourcesDetailsToFlatList ` -ItemList $itemList.Values ` -Details $CombinedPolicyDetails.policySets } #endregion compile flat Policy List for all Policy Sets used in this branch $definition.definitionEntryList = $normalizedDefinitionEntryList $definition.hasPolicySets = $hasPolicySets $definition.flatPolicyList = $flatPolicyList $definition.perEntryNonComplianceMessages = $perEntryNonComplianceMessages } else { Write-Error " Node $($nodeName): only one definitionEntry or definitionEntryList can appear in any branch." $definition.hasErrors = $true } } #endregion definitionEntry or definitionEntryList (required exactly once per branch) #region metadata if ($DefinitionNode.metadata) { # merge metadata $metadata = $definition.metadata $merge = Get-DeepCloneAsOrderedHashtable $DefinitionNode.metadata foreach ($key in $merge.Keys) { $metadata[$key] = $merge.$key } } #endregion metadata #region parameters in JSON; parameters defined at a deeper level override previous parameters (union operator) if ($DefinitionNode.parameters) { $allParameters = $definition.parameters $addedParameters = $DefinitionNode.parameters foreach ($parameterName in $addedParameters.Keys) { $rawParameterValue = $addedParameters.$parameterName $parameterValue = Get-DeepCloneAsOrderedHashtable $rawParameterValue $allParameters[$parameterName] = $parameterValue } } #endregion parameters in JSON; parameters defined at a deeper level override previous parameters (union operator) #region process parameterFileName and parameterSelector if ($DefinitionNode.parameterSelector) { $parameterSelector = $DefinitionNode.parameterSelector $definition.parameterSelector = $parameterSelector $definition.effectColumn = "$($parameterSelector)Effect" $definition.parametersColumn = "$($parameterSelector)Parameters" } if ($DefinitionNode.parameterFile) { $parameterFileName = $DefinitionNode.parameterFile if ($ParameterFilesCsv.ContainsKey($parameterFileName)) { $fullName = $ParameterFilesCsv.$parameterFileName $content = Get-Content -Path $fullName -Raw -ErrorAction Stop $xlsArray = @() + ($content | ConvertFrom-Csv -ErrorAction Stop) $csvParameterArray = Get-DeepCloneAsOrderedHashtable $xlsArray $definition.parameterFileName = $parameterFileName $definition.csvParameterArray = $csvParameterArray $definition.csvRowsValidated = $false if ($csvParameterArray.Count -eq 0) { Write-Error " Node $($nodeName): CSV parameterFile '$parameterFileName' is empty (zero rows)." $definition.hasErrors = $true } } else { Write-Error " Node $($nodeName): CSV parameterFileName '$parameterFileName' does not exist." $definition.hasErrors = $true } } #endregion process parameterFileName and parameterSelector #region validate CSV rows if (!($definition.csvRowsValidated) -and $definition.hasPolicySets -and $definition.parameterFileName -and $definition.definitionEntryList) { $csvParameterArray = $definition.csvParameterArray $parameterFileName = $definition.parameterFileName $definition.csvRowsValidated = $true $rowHashtable = @{} foreach ($row in $csvParameterArray) { # Ignore empty lines with a warning $name = $row.name if ($null -eq $name -or $name -eq "") { Write-Verbose " Node $($nodeName): CSV parameterFile '$parameterFileName' has an empty row." continue } # generate the key into the flatPolicyList $policyId = Confirm-PolicyDefinitionUsedExists -Name $name -PolicyDefinitionsScopes $PacEnvironment.policyDefinitionsScopes -AllDefinitions $CombinedPolicyDetails.policies -SuppressErrorMessage if ($null -eq $policyId) { Write-Error " Node $($nodeName): CSV parameterFile '$parameterFileName' has a row containing an unknown Policy name '$name'." $definition.hasErrors = $true continue } $flatPolicyEntryKey = $policyId $flatPolicyReferencePath = $row.referencePath if ($null -ne $flatPolicyReferencePath -and $flatPolicyReferencePath -ne "") { $flatPolicyEntryKey = "$policyId\\$flatPolicyReferencePath" $null = $rowHashtable.Add($flatPolicyEntryKey, "$($row.displayName) ($name -- $flatPolicyReferencePath)") } else { $null = $rowHashtable.Add($flatPolicyEntryKey, "$($row.displayName) ($name)") } $row.policyId = $policyId $row.flatPolicyEntryKey = $flatPolicyEntryKey } $missingInCsv = [System.Collections.ArrayList]::new() $flatPolicyList = $definition.flatPolicyList foreach ($flatPolicyEntryKey in $flatPolicyList.Keys) { if ($rowHashtable.ContainsKey($flatPolicyEntryKey)) { $rowHashtable.Remove($flatPolicyEntryKey) } else { $flatPolicyEntry = $flatPolicyList.$flatPolicyEntryKey if ($VerbosePreference -eq "Continue" -or ($flatPolicyEntry.effectDefault -ne "Manual" -and $flatPolicyEntry.effectDefault -ne "Disabled")) { # Complain only about Policies NOT with Manual or Disabled effect default or when Verbose is on if ($flatPolicyEntry.referencePath) { $null = $missingInCsv.Add("$($flatPolicyEntry.displayName) ($($flatPolicyEntry.name) -- $($flatPolicyEntry.referencePath))") } else { $null = $missingInCsv.Add("$($flatPolicyEntry.displayName) ($($flatPolicyEntry.name))") } } } } if ($rowHashtable.Count -gt 0) { Write-Warning "Node $($nodeName): CSV parameterFile '$parameterFileName' contains rows for Policies not included in any of the Policy Sets. Remove the obsolete rows or regenerate the CSV file." foreach ($displayString in $rowHashtable.Values) { Write-Information " $($displayString)" } } if ($missingInCsv.Count -gt 0) { Write-Warning "Node $($nodeName): CSV parameterFile '$parameterFileName' is missing rows for Policies included in the Policy Sets. Regenerate the CSV file." foreach ($missing in $missingInCsv) { Write-Information " $($missing)" } } } #endregion validate CSV rows #region advanced - overrides, resourceSelectors and nonComplianceMessages if ($DefinitionNode.overrides) { # Cumulative in branch # overrides behave like parameters, we define them similarly (simplified from Azure Policy) $definition.overrides += $DefinitionNode.overrides } if ($DefinitionNode.resourceSelectors) { # Cumulative in branch # resourceSelectors behave like parameters, we define them similarly (simplified from Azure Policy) $definition.resourceSelectors += $DefinitionNode.resourceSelectors } if ($DefinitionNode.nonComplianceMessageColumn) { # nonComplianceMessages are in a column in the parameters csv file $definition.nonComplianceMessageColumn = $DefinitionNode.nonComplianceMessageColumn } if ($DefinitionNode.nonComplianceMessages) { $definition.nonComplianceMessages += $DefinitionNode.nonComplianceMessages } #endregion advanced parameters - overrides and resourceSelectors #region scopes, notScopes if ($definition.scopeCollection -or $definition.hasOnlyNotSelectedEnvironments) { # Once a scopeList is defined at a parent, no descendant may define scopeList or notScope if ($DefinitionNode.scope) { Write-Error " Node $($nodeName): multiple scope definitions at different tree levels are not allowed" $definition.hasErrors = $true } if ($DefinitionNode.notScope -or $DefinitionNode.notScopes) { Write-Error " Node $($nodeName): detected notScope definition in in a child node when the scope was already defined" $definition.hasErrors = $true } } else { # may define notScope or notScopes $definitionNotScopesList = $definition.notScopesList if ($DefinitionNode.notScope) { Add-SelectedPacArray -InputObject $DefinitionNode.notScope -PacSelector $pacSelector -OutputArrayList $definitionNotScopesList } if ($DefinitionNode.notScopes) { Add-SelectedPacArray -InputObject $DefinitionNode.notScopes -PacSelector $pacSelector -OutputArrayList $definitionNotScopesList } if ($DefinitionNode.scope) { ## Found a scope list - process scope notScopes $scopeList = [System.Collections.ArrayList]::new() Add-SelectedPacArray -InputObject $DefinitionNode.scope -PacSelector $pacSelector -OutputArrayList $scopeList if ($scopeList.Count -eq 0) { # This branch does not have a scope for this assignment's pacSelector; ignore branch $definition.hasOnlyNotSelectedEnvironments = $true } else { $scopeCollection = [System.Collections.ArrayList]::new() foreach ($scope in $scopeList) { $thisScopeDetails = $ScopeTable.$scope if ($null -eq $thisScopeDetails) { Write-Error " Node $($nodeName): scope '$scope' is not defined in the ScopeTable." $definition.hasErrors = $true continue } elseif ($thisScopeDetails.isExcluded) { Write-Error " Node $($nodeName): scope '$scope' is excluded in the ScopeTable." $definition.hasErrors = $true continue } $thisNotScopeList = [System.Collections.ArrayList]::new() $thisScopeChildren = $thisScopeDetails.childrenTable $thisScopeGlobalNotScopeList = $thisScopeDetails.notScopesList $thisScopeGlobalNotScopeTable = $thisScopeDetails.notScopesTable foreach ($notScope in $definitionNotScopesList) { if (-not $thisScopeGlobalNotScopeTable.ContainsKey($notScope)) { if ($thisScopeChildren.ContainsKey($notScope)) { $null = $thisNotScopeList.Add($notScope) } elseif ($notScope.Contains("*")) { foreach ($scopeChildId in $thisScopeChildren.Keys) { if ($scopeChildId -like $notScope) { $null = $thisNotScopeList.Add($scopeChildId) } } } else { continue } } } $null = $thisNotScopeList.AddRange($thisScopeGlobalNotScopeList) $thisNotScopeListUnique = $thisNotScopeList | Select-Object -Unique if ($null -eq $thisNotScopeListUnique) { $thisNotScopeListUnique = @() } elseif ($thisNotScopeListUnique -isnot [array]) { $thisNotScopeListUnique = @($thisNotScopeListUnique) } $scopeResult = @{ scope = $scope notScopesList = $thisNotScopeListUnique } $null = $scopeCollection.Add($scopeResult) } $definition.scopeCollection = $scopeCollection } } } #endregion scopes, notScopes #region identity and additionalRoleAssignments (optional, specific to an EPAC environment) if ($DefinitionNode.additionalRoleAssignments) { # Process additional permissions needed to execute remediations; for example permissions to log to Event Hub, Storage Account or Log Analytics Add-SelectedPacArray -InputObject $DefinitionNode.additionalRoleAssignments -PacSelector $pacSelector -OutputArrayList $definition.additionalRoleAssignments } if ($DefinitionNode.managedIdentityLocations) { # Process managedIdentityLocation; can be overridden Add-SelectedPacValue -InputObject $DefinitionNode.managedIdentityLocations -PacSelector $pacSelector -OutputObject $definition -OutputKey "managedIdentityLocation" } if ($DefinitionNode.userAssignedIdentity) { # Process userAssignedIdentity; can be overridden Add-SelectedPacValue -InputObject $DefinitionNode.userAssignedIdentity -PacSelector $pacSelector -OutputObject $definition -OutputKey "userAssignedIdentity" } #endregion identity and additionalRoleAssignments (optional, specific to an EPAC environment) #region children and the leaf node if ($DefinitionNode.children) { # Process child nodes Write-Debug " $($DefinitionNode.children.Count) children below at $($nodeName)" $hasErrors = $false $assignmentsList = [System.Collections.ArrayList]::new() foreach ($child in $DefinitionNode.children) { $hasErrorsLocal, $assignmentsListLocal = Build-AssignmentDefinitionNode ` -PacEnvironment $PacEnvironment ` -ScopeTable $ScopeTable ` -ParameterFilesCsv $ParameterFilesCsv ` -DefinitionNode $child ` -AssignmentDefinition $definition ` -CombinedPolicyDetails $CombinedPolicyDetails ` -PolicyRoleIds $PolicyRoleIds ` -RoleDefinitions $RoleDefinitions if ($hasErrorsLocal) { $hasErrors = $true } elseif ($null -ne $assignmentsListLocal) { $null = $assignmentsList.AddRange($assignmentsListLocal) } } return $hasErrors, $assignmentsList } else { # Arrived at a leaf node - return the values collected in this branch after checking validity if ($definition.ignoreBranch -or $definition.hasOnlyNotSelectedEnvironments -or $definition.hasErrors) { # Empty collection return $definition.hasErrors, [System.Collections.ArrayList]::new() } else { $hasErrors, $assignmentsListAtLeaf = Build-AssignmentDefinitionAtLeaf ` -PacEnvironment $PacEnvironment ` -AssignmentDefinition $definition ` -CombinedPolicyDetails $CombinedPolicyDetails ` -PolicyRoleIds $PolicyRoleIds ` -RoleDefinitions $RoleDefinitions return $hasErrors, $assignmentsListAtLeaf } } #endregion children and the leaf node } |