internal/functions/Build-AssignmentDefinitionNode.ps1
#Requires -PSEdition Core 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 # Returns a list os completed assignmentValues ) # Each tree branch needs a private copy $definition = Get-DeepClone -InputObject $assignmentDefinition -AsHashTable $pacEnvironmentSelector = $pacEnvironment.pacSelector #region nodeName (required) $nodeName = "" if ($definitionNode.nodeName) { $nodeName += $definitionNode.nodeName $definition.nodeName += $nodeName # ignore "comment" field Write-Debug " nodePath = $($nodeName):" } else { $nodeName = "$($nodeName)//Unknown//" Write-Error " Missing nodeName at child of $($nodeName)" $definition.hasErrors = $true } #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-Verbose " Ignore branch at $($nodeName) reason ignore branch" $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. It 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 $hasErrors = $false if ($null -ne $definitionEntry) { # Convert to list $definitionEntryList = @( $definitionEntry ) } # Validate list $normalizedDefinitionEntryList = @() $mustDefineAssignment = $definitionEntryList.Count -gt 1 foreach ($definitionEntry in $definitionEntryList) { $isValid, $normalizedEntry = Build-AssignmentDefinitionEntry ` -definitionEntry $definitionEntry ` -nodeName $nodeName ` -policyDefinitionsScopes $pacEnvironment.policyDefinitionsScopes ` -combinedPolicyDetails $combinedPolicyDetails ` -mustDefineAssignment:$mustDefineAssignment if ($isValid) { $normalizedDefinitionEntryList += $normalizedEntry } else { $hasErrors = $true } } if ($hasErrors) { $definition.hasErrors = $true } $definition.definitionEntryList = $normalizedDefinitionEntryList } 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) { if ($definition.metadata) { # merge metadata $metadata = $definition.metadata $merge = Get-DeepClone $definitionNode.metadata -AsHashTable foreach ($key in $merge) { $metadata[$key] = $merge.$key } } else { $definition.metadata = Get-DeepClone $definitionNode.metadata -AsHashTable } } #endregion metadata #region parameters # parameterSuppressDefaultValues if ($definitionNode.parameterSuppressDefaultValues) { $definition.parameterSuppressDefaultValues = $definitionNode.parameterSuppressDefaultValues } # 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-DeepClone $rawParameterValue -AsHashTable $allParameters[$parameterName] = $parameterValue } } # Process parameterFileName and parameterSelector if ($definitionNode.parameterSelector) { if ($definition.ContainsKey("parameterSelector")) { Write-Error " Node $($nodeName): multiple parameterFileName definitions at different tree levels are not allowed" $definition.hasErrors = $true } else { $definition.parameterSelector = $definitionNode.parameterSelector } } if ($definitionNode.parameterFile) { if ($definition.ContainsKey("parameterFileName")) { Write-Error " Node $($nodeName): multiple parameterFileName definitions at different tree levels are not allowed." $definition.hasErrors = $true } else { $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-DeepClone $xlsArray -AsHashTable $definition.parameterFileName = $parameterFileName $definition.csvParameterArray = $csvParameterArray 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 } } } if (!$definition.effectColumn -and $definition.ContainsKey("parameterFileName") -and $definition.ContainsKey("parameterSelector")) { # Collected a parameterFileName and a parameterSelector, not yet validated column in CSV file $csvParameterArray = $definition.csvParameterArray $row = $csvParameterArray[0] $parameterFileName = $definition.parameterFileName $parameterSelector = $definition.parameterSelector $effectColumn = "$($parameterSelector)Effect" $parametersColumn = "$($parameterSelector)Parameters" if (-not ($row.ContainsKey("name") -and $row.ContainsKey("referencePath") -and $row.ContainsKey($effectColumn) -and $row.ContainsKey($parametersColumn))) { Write-Error " Node $($nodeName): CSV parameterFile ($parameterFileName) must contain the following columns: name, referencePath, $effectColumn, $parametersColumn." $hasErrors = $true } $definition.effectColumn = $effectColumn $definition.parametersColumn = $parametersColumn # $parameterFileNonComplianceMessage = $firstRow.ContainsKey("nonComplianceMessage") # if ($definition.ContainsKey("nonComplianceMessage") -and $parameterFileNonComplianceMessage) { # Write-Error " Node $($nodeName): specifying nonComplianceMessage in JSON and nonComplianceMessage in CSV parameter file is not allowed." # $definition.hasErrors = $true # } # else { # $definition.parameterFileNonComplianceMessage = $parameterFileNonComplianceMessage } #endregion parameters #region scopes, notScopes if ($definition.scopeCollection) { # 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) { 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 if ($definitionNode.notScope) { $notScope = $definitionNode.notScope Write-Debug " notScope defined at $($nodeName) = $($notScope | ConvertTo-Json -Depth 100)" foreach ($selector in $notScope.Keys) { if ($selector -eq "*" -or $selector -eq $pacEnvironmentSelector) { $notScopeList = $notScope.$selector if ($definition.notScope) { $definition.notScope += $notScopeList } else { $definition.notScope = @() + $notScopeList } } } } if ($definitionNode.scope) { ## Found a scope list - process notScope $scopeList = $null $scope = $definitionNode.scope foreach ($selector in $scope.Keys) { if ($selector -eq "*" -or $selector -eq $pacEnvironmentSelector) { $scopeList = @() + $scope.$selector break } } if ($null -eq $scopeList) { # This branch does not have a scope for this assignment's pacSelector; ignore branch $definition.hasOnlyNotSelectedEnvironments = $true } else { if ($scopeList -is [array] -and $scopeList.Length -gt 0) { $scopeCollection = @() if ($definition.notScope) { $uniqueNotScope = @() + ($definition.notScope | Sort-Object | Get-Unique) $scopeCollection = Build-NotScopes -scopeList $scopeList -notScope $uniqueNotScope -scopeTable $scopeTable } else { foreach ($scope in $scopeList) { $scopeCollection += @{ scope = $scope notScope = @() } } } $definition.scopeCollection = $scopeCollection } else { Write-Error " Node $($nodeName): scope array must not be empty" $definition.hasErrors = $true } } } } #endregion scopes, notScopes #region additionalRoleAssignments (optional, cumulative) if ($definitionNode.additionalRoleAssignments) { # Process additional permissions needed to execute remediations; for example permissions to log to Event Hub, Storage Account or Log Analytics $additionalRoleAssignments = $definitionNode.additionalRoleAssignments foreach ($selector in $additionalRoleAssignments.Keys) { if ($selector -eq "*" -or $selector -eq $pacEnvironmentSelector) { $additionalRoleAssignmentsList = Get-DeepClone $additionalRoleAssignments.$selector -AsHashTable if ($definition.additionalRoleAssignments) { $definition.additionalRoleAssignments += $additionalRoleAssignmentsList } else { $definition.additionalRoleAssignments = @() + $additionalRoleAssignmentsList } } } } #endregion additionalRoleAssignments (optional, cumulative) #region Managed Identity if ($definitionNode.managedIdentityLocations) { # Process managedIdentityLocation; can be overridden $managedIdentityLocationValue = $null $managedIdentityLocations = $definitionNode.managedIdentityLocations foreach ($selector in $managedIdentityLocations.Keys) { if ($selector -eq "*" -or $selector -eq $pacEnvironmentSelector) { $managedIdentityLocationValue = $managedIdentityLocation.$selector break } } if ($null -ne $managedIdentityLocationValue) { $definition.managedIdentityLocation = $managedIdentityLocationValue } } #endregion Managed Identity #region nonComplianceMessage # TODO # if ($definition.ContainsKey("parameterFileNonComplianceMessage")) { # } if ($definitionNode.nonComplianceMessages) { $definition.nonComplianceMessages += $definitionNode.nonComplianceMessages } #endregion nonComplianceMessage #region children $assignmentsList = @() if ($definitionNode.children) { # Process child nodes Write-Debug " $($definitionNode.children.Count) children below at $($nodeName)" $hasErrors = $false foreach ($child in $definitionNode.children) { $hasErrorsLocal, $assignmentsListLocal = Build-AssignmentDefinitionNode ` -pacEnvironment $pacEnvironment ` -scopeTable $scopeTable ` -parameterFilesCsv $parameterFilesCsv ` -definitionNode $child ` -assignmentDefinition $definition ` -combinedPolicyDetails $combinedPolicyDetails ` -policyRoleIds $policyRoleIds if ($hasErrorsLocal) { $hasErrors = $true } elseif ($null -ne $assignmentsListLocal) { $assignmentsList += $assignmentsListLocal } } } 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) { return $definition.hasErrors, @() } else { $hasErrors, $assignmentsList = Build-AssignmentDefinitionAtLeaf ` -pacEnvironment $pacEnvironment ` -assignmentDefinition $definition ` -combinedPolicyDetails $combinedPolicyDetails ` -policyRoleIds $policyRoleIds } } #endregion children if ($hasErrors) { return $true, $null } elseif (($assignmentsList -is [array])) { return $false, $assignmentsList } else { return $false, $( $assignmentsList ) } } |