diff --git a/Docs/operational-scripts.md b/Docs/operational-scripts.md index 110f6ab2..4efccbdb 100644 --- a/Docs/operational-scripts.md +++ b/Docs/operational-scripts.md @@ -4,7 +4,7 @@ The scripts are detailed in the [reference page](operational-scripts-reference.m ## Batch Creation of Remediation Tasks -The script `Create-AzRemediationTasks` creates remediation tasks for all non-compliant resources for EPAC environments in the `global-settings.jsonc` file. +The script `New-AzRemediationTasks` creates remediation tasks for all non-compliant resources for EPAC environments in the `global-settings.jsonc` file. This script executes all remediation tasks in a Policy as Code environment specified with parameter `PacEnvironmentSelector`. The script will interactively prompt for the value if the parameter is not supplied. The script will recurse the Management Group structure and subscriptions from the defined starting point. @@ -34,7 +34,7 @@ The Hydration Kit is a set of scripts that can be used to deploy an EPAC environ ## CI/CD Helpers -The scripts `Create-AzureDevOpsBug` and `Create-GitHubIssue` create a Bug or Issue when there are one or multiple failed Remediation Tasks. +The scripts `New-AzureDevOpsBug` and `New-GitHubIssue` create a Bug or Issue when there are one or multiple failed Remediation Tasks. ## Non-compliance Reports diff --git a/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psd1 b/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psd1 index 2221afb0..ecd7334d 100644 --- a/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psd1 +++ b/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psd1 @@ -34,6 +34,17 @@ # Aliases to export from this module AliasesToExport = '' + + RequiredModules = @( + @{ + ModuleName = 'Az.Accounts' + ModuleVersion = '2.9.1' + }, + @{ + ModuleName = 'Az.ResourceGraph' + ModuleVersion = '0.13.0' + } + ) # List of all files packaged with this module FileList = @() diff --git a/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psm1 b/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psm1 index 952c23b2..51e0717c 100644 --- a/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psm1 +++ b/Module/EnterprisePolicyAsCode/EnterprisePolicyAsCode.psm1 @@ -41,7 +41,7 @@ foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Recurse - foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Recurse -File -Filter "*.ps1")) { . Import-ModuleFile -Path $function.FullName - Export-ModuleMember $function.BaseName -Verbose + Export-ModuleMember $function.BaseName } #endregion Load functions diff --git a/Scripts/Helpers/Add-HelperScripts.ps1 b/Scripts/Helpers/Add-HelperScripts.ps1 index 7aa45e2b..87c4ca1e 100644 --- a/Scripts/Helpers/Add-HelperScripts.ps1 +++ b/Scripts/Helpers/Add-HelperScripts.ps1 @@ -58,8 +58,8 @@ . "$PSScriptRoot/Get-AzPolicyResources.ps1" . "$PSScriptRoot/Get-AzPolicyResourcesDetails.ps1" . "$PSScriptRoot/Get-CalculatedPolicyAssignmentsAndReferenceIds.ps1" -. "$PSScriptRoot/Get-ClonedObject.ps1" . "$PSScriptRoot/Get-CustomMetadata.ps1" +. "$PSScriptRoot/Get-DeepCloneAsOrderedHashtable.ps1" . "$PSScriptRoot/Get-DefinitionsFullPath.ps1" . "$PSScriptRoot/Get-DeploymentPlan.ps1" . "$PSScriptRoot/Get-GlobalSettings.ps1" diff --git a/Scripts/Helpers/Build-AssignmentDefinitionAtLeaf.ps1 b/Scripts/Helpers/Build-AssignmentDefinitionAtLeaf.ps1 index 8d7a3e21..d719602f 100644 --- a/Scripts/Helpers/Build-AssignmentDefinitionAtLeaf.ps1 +++ b/Scripts/Helpers/Build-AssignmentDefinitionAtLeaf.ps1 @@ -164,7 +164,7 @@ function Build-AssignmentDefinitionAtLeaf { continue } $enforcementMode = $AssignmentDefinition.enforcementMode - $metadata = Get-ClonedObject $AssignmentDefinition.metadata -AsHashTable + $metadata = Get-DeepCloneAsOrderedHashtable $AssignmentDefinition.metadata if ($metadata.ContainsKey("pacOwnerId")) { Write-Error " Leaf Node $($nodeName): metadata.pacOwnerId ($($metadata.pacOwnerId)) may not be set explicitly; it is reserved for EPAC usage." $hasErrors = $true @@ -567,7 +567,7 @@ function Build-AssignmentDefinitionAtLeaf { foreach ($scopeEntry in $scopeCollection) { # Clone hashtable - [hashtable] $scopedAssignment = Get-ClonedObject $baseAssignment -AsHashTable + [hashtable] $scopedAssignment = Get-DeepCloneAsOrderedHashtable $baseAssignment # Add scope and if defined notScopes() $scope = $scopeEntry.scope diff --git a/Scripts/Helpers/Build-AssignmentDefinitionNode.ps1 b/Scripts/Helpers/Build-AssignmentDefinitionNode.ps1 index 69924b80..9f6b9370 100644 --- a/Scripts/Helpers/Build-AssignmentDefinitionNode.ps1 +++ b/Scripts/Helpers/Build-AssignmentDefinitionNode.ps1 @@ -14,7 +14,7 @@ function Build-AssignmentDefinitionNode { ) # Each tree branch needs a private copy - $definition = Get-ClonedObject -InputObject $AssignmentDefinition + $definition = Get-DeepCloneAsOrderedHashtable -InputObject $AssignmentDefinition $pacSelector = $PacEnvironment.pacSelector #region nodeName (required) @@ -157,7 +157,8 @@ function Build-AssignmentDefinitionNode { if ($DefinitionNode.metadata) { # merge metadata $metadata = $definition.metadata - $merge = Get-ClonedObject $DefinitionNode.metadata -AsHashTable + $merge = Get-DeepCloneAsOrderedHashtable $DefinitionNode.metadata + foreach ($key in $merge.Keys) { $metadata[$key] = $merge.$key } @@ -170,7 +171,7 @@ function Build-AssignmentDefinitionNode { $addedParameters = $DefinitionNode.parameters foreach ($parameterName in $addedParameters.Keys) { $rawParameterValue = $addedParameters.$parameterName - $parameterValue = Get-ClonedObject $rawParameterValue -AsHashTable + $parameterValue = Get-DeepCloneAsOrderedHashtable $rawParameterValue $allParameters[$parameterName] = $parameterValue } } @@ -189,7 +190,7 @@ function Build-AssignmentDefinitionNode { $fullName = $ParameterFilesCsv.$parameterFileName $content = Get-Content -Path $fullName -Raw -ErrorAction Stop $xlsArray = @() + ($content | ConvertFrom-Csv -ErrorAction Stop) - $csvParameterArray = Get-ClonedObject $xlsArray -AsHashTable + $csvParameterArray = Get-DeepCloneAsOrderedHashtable $xlsArray $definition.parameterFileName = $parameterFileName $definition.csvParameterArray = $csvParameterArray $definition.csvRowsValidated = $false diff --git a/Scripts/Helpers/Build-AssignmentPlan.ps1 b/Scripts/Helpers/Build-AssignmentPlan.ps1 index cd7232d8..fbbf1848 100644 --- a/Scripts/Helpers/Build-AssignmentPlan.ps1 +++ b/Scripts/Helpers/Build-AssignmentPlan.ps1 @@ -35,7 +35,7 @@ function Build-AssignmentPlan { # Cache role assognments and definitions $deployedPolicyAssignments = $deployedPolicyResources.policyassignments.managed $deployedRoleAssignmentsByPrincipalId = $DeployedPolicyResources.roleAssignmentsByPrincipalId - $deleteCandidates = Get-ClonedObject $deployedPolicyAssignments -AsHashTable -AsShallowClone + $deleteCandidates = $deployedPolicyAssignments.Clone() $roleDefinitions = $DeployedPolicyResources.roleDefinitions # Process each assignment file @@ -50,8 +50,9 @@ function Build-AssignmentPlan { } # Write-Information "" + $assignmentObject = $null try { - $assignmentObject = $Json | ConvertFrom-Json -AsHashtable + $assignmentObject = $Json | ConvertFrom-Json -Depth 100 -AsHashtable } catch { Write-Error "Assignment JSON file '$($assignmentFile.FullName)' is not valid." -ErrorAction Stop @@ -189,6 +190,9 @@ function Build-AssignmentPlan { } $changesStrings += ($identityStatus.changedIdentityStrings) } + elseif ($identityStatus.requiresRoleChanges) { + $changesStrings += ($identityStatus.changedIdentityStrings) + } if (!$displayNameMatches) { $changesStrings += "displayName" diff --git a/Scripts/Helpers/Build-ExemptionsPlan.ps1 b/Scripts/Helpers/Build-ExemptionsPlan.ps1 index d87aefa7..13d4fdb7 100644 --- a/Scripts/Helpers/Build-ExemptionsPlan.ps1 +++ b/Scripts/Helpers/Build-ExemptionsPlan.ps1 @@ -3,14 +3,14 @@ function Build-ExemptionsPlan { param ( [string] $ExemptionsRootFolder, [string] $ExemptionsAreNotManagedMessage, - [hashtable] $PacEnvironment, + $PacEnvironment, $ScopeTable, - [hashtable] $AllDefinitions, - [hashtable] $AllAssignments, - [hashtable] $CombinedPolicyDetails, - [hashtable] $Assignments, - [hashtable] $DeployedExemptions, - [hashtable] $Exemptions + $AllDefinitions, + $AllAssignments, + $CombinedPolicyDetails, + $Assignments, + $DeployedExemptions, + $Exemptions ) Write-Information "===================================================================================================" @@ -25,7 +25,7 @@ function Build-ExemptionsPlan { $uniqueIds = @{} $deployedManagedExemptions = $DeployedExemptions.managed - $deleteCandidates = Get-ClonedObject $deployedManagedExemptions -AsHashTable -AsShallowClone + $deleteCandidates = $deployedManagedExemptions.Clone() $replacedAssignments = $Assignments.replace $numberOfFilesWithErrors = 0 $desiredState = $PacEnvironment.desiredState @@ -65,7 +65,7 @@ function Build-ExemptionsPlan { Write-Information "Processing file '$($fullName)'" Write-Information "---------------------------------------------------------------------------------------------------" $errorInfo = New-ErrorInfo -FileName $fullName - $exemptionsArray = @() + $exemptionsArray = [System.Collections.ArrayList]::new() $isCsvFile = $false if ($extension -eq ".json" -or $extension -eq ".jsonc") { $content = Get-Content -Path $fullName -Raw -ErrorAction Stop @@ -79,7 +79,7 @@ function Build-ExemptionsPlan { if ($null -ne $jsonObj) { $jsonExemptions = $jsonObj.exemptions if ($null -ne $jsonExemptions -and $jsonExemptions.Count -gt 0) { - $exemptionsArray += $jsonExemptions + $null = $exemptionsArray.AddRange($jsonExemptions) } } } @@ -87,8 +87,13 @@ function Build-ExemptionsPlan { $isCsvFile = $true $content = Get-Content -Path $fullName -Raw -ErrorAction Stop $xlsExemptions = ($content | ConvertFrom-Csv -ErrorAction Stop) - if ($xlsExemptions.Count -gt 0) { - $exemptionsArray += $xlsExemptions + if ($null -ne $xlsExemptions) { + if ($xlsExemptions -isnot [array]) { + $xlsExemptions = @($xlsExemptions) + } + if ($xlsExemptions.Count -gt 0) { + $null = $exemptionsArray.AddRange($xlsExemptions) + } } } #endregion read each file @@ -178,7 +183,7 @@ function Build-ExemptionsPlan { $step1 = $policyDefinitionReferenceIds if (-not [string]::IsNullOrWhiteSpace($step1)) { $step2 = $step1.Trim() - $step3 = $step2 -split ":" + $step3 = $step2 -split "&" foreach ($item in $step3) { $step4 = $item.Trim() if ($step4.Length -gt 0) { @@ -505,6 +510,7 @@ function Build-ExemptionsPlan { $splits = $currentScope -split "/" $trimmedScope = $splits[0..4] -join "/" if ($validateScope) { + $thisResourceIdExists = $false if ($resourceIdsExist.ContainsKey($currentScope)) { $thisResourceIdExists = $resourceIdsExist.$currentScope } @@ -533,7 +539,7 @@ function Build-ExemptionsPlan { $uniqueAssignmentNames = @{} if ($null -ne $policyAssignmentId -and !$validateScope) { $calculatedPolicyAssignment = $calculatedPolicyAssignments[0] - $clonedCalculatedPolicyAssignment = Get-ClonedObject $calculatedPolicyAssignment -AsShallowClone + $clonedCalculatedPolicyAssignment = $calculatedPolicyAssignment.Clone() $null = $filteredPolicyAssignments.Add($clonedCalculatedPolicyAssignment) } else { @@ -566,7 +572,7 @@ function Build-ExemptionsPlan { $listOfAssignmentsWithSameName = [System.Collections.ArrayList]::new() $null = $uniqueAssignmentNames.Add($calculatedPolicyAssignment.name, $listOfAssignmentsWithSameName) } - $clonedCalculatedPolicyAssignment = Get-ClonedObject $calculatedPolicyAssignment -AsShallowClone + $clonedCalculatedPolicyAssignment = $calculatedPolicyAssignment.Clone() $null = $listOfAssignmentsWithSameName.Add($clonedCalculatedPolicyAssignment) $null = $filteredPolicyAssignments.Add($clonedCalculatedPolicyAssignment) } @@ -719,8 +725,7 @@ function Build-ExemptionsPlan { ordinalString = $ordinalString } $epacMetadata += $epacMetadataDefinitionSpecification - $clonedMetadata = $null - $clonedMetadata = Get-ClonedObject $metadata -AsShallowClone + $clonedMetadata = Get-DeepCloneAsOrderedHashtable $metadata $clonedMetadata.pacOwnerId = $PacEnvironment.pacOwnerId $clonedMetadata.epacMetadata = $epacMetadata if (!$clonedMetadata.ContainsKey("deployedBy")) { diff --git a/Scripts/Helpers/Build-PolicyPlan.ps1 b/Scripts/Helpers/Build-PolicyPlan.ps1 index cf93c867..fe1835bf 100644 --- a/Scripts/Helpers/Build-PolicyPlan.ps1 +++ b/Scripts/Helpers/Build-PolicyPlan.ps1 @@ -26,7 +26,7 @@ function Build-PolicyPlan { } $managedDefinitions = $DeployedDefinitions.managed - $deleteCandidates = Get-ClonedObject $managedDefinitions -AsHashTable -AsShallowClone + $deleteCandidates = $managedDefinitions.Clone() $deploymentRootScope = $PacEnvironment.deploymentRootScope $duplicateDefinitionTracking = @{} $definitionsNew = $Definitions.new @@ -39,8 +39,9 @@ function Build-PolicyPlan { # Write-Information "Processing $($definitionFilesSet.Length) Policy files in this parallel execution." $Json = Get-Content -Path $file.FullName -Raw -ErrorAction Stop + $definitionObject = $null try { - $definitionObject = $Json | ConvertFrom-Json + $definitionObject = ConvertFrom-Json $Json -Depth 100 } catch { Write-Error "Assignment JSON file '$($file.FullName)' is not valid." -ErrorAction Stop @@ -52,12 +53,12 @@ function Build-PolicyPlan { $id = "$deploymentRootScope/providers/Microsoft.Authorization/policyDefinitions/$name" $displayName = $definitionProperties.displayName $description = $definitionProperties.description - $metadata = Get-ClonedObject $definitionProperties.metadata -AsHashTable + $metadata = Get-DeepCloneAsOrderedHashtable $definitionProperties.metadata # $version = $definitionProperties.version $mode = $definitionProperties.mode $parameters = $definitionProperties.parameters $policyRule = $definitionProperties.policyRule - if ($metadata) { + if ($null -ne $metadata) { $metadata.pacOwnerId = $thisPacOwnerId } else { @@ -125,25 +126,25 @@ function Build-PolicyPlan { if ($managedDefinitions.ContainsKey($id)) { # Update and replace scenarios $deployedDefinition = $managedDefinitions[$id] - $deployedDefinition = Get-PolicyResourceProperties -PolicyResource $deployedDefinition + $deployedDefinitionProperties = Get-PolicyResourceProperties -PolicyResource $deployedDefinition # Remove defined Policy entry from deleted hashtable (the hashtable originally contains all custom Policy in the scope) $null = $deleteCandidates.Remove($id) # Check if Policy in Azure is the same as in the JSON file - $displayNameMatches = $deployedDefinition.displayName -eq $displayName - $descriptionMatches = $deployedDefinition.description -eq $description - $modeMatches = $deployedDefinition.mode -eq $definition.Mode + $displayNameMatches = $deployedDefinitionProperties.displayName -eq $displayName + $descriptionMatches = $deployedDefinitionProperties.description -eq $description + $modeMatches = $deployedDefinitionProperties.mode -eq $definition.Mode $metadataMatches, $changePacOwnerId = Confirm-MetadataMatches ` - -ExistingMetadataObj $deployedDefinition.metadata ` + -ExistingMetadataObj $deployedDefinitionProperties.metadata ` -DefinedMetadataObj $metadata - # $versionMatches = $version -eq $deployedDefinition.version + # $versionMatches = $version -eq $deployedDefinitionProperties.version $versionMatches = $true $parametersMatch, $incompatible = Confirm-ParametersDefinitionMatch ` - -ExistingParametersObj $deployedDefinition.parameters ` + -ExistingParametersObj $deployedDefinitionProperties.parameters ` -DefinedParametersObj $parameters $policyRuleMatches = Confirm-ObjectValueEqualityDeep ` - $deployedDefinition.policyRule ` + $deployedDefinitionProperties.policyRule ` $policyRule # Update Policy in Azure if necessary diff --git a/Scripts/Helpers/Build-PolicySetPlan.ps1 b/Scripts/Helpers/Build-PolicySetPlan.ps1 index 3989569b..40b2b199 100644 --- a/Scripts/Helpers/Build-PolicySetPlan.ps1 +++ b/Scripts/Helpers/Build-PolicySetPlan.ps1 @@ -26,7 +26,7 @@ function Build-PolicySetPlan { } $managedDefinitions = $DeployedDefinitions.managed - $deleteCandidates = Get-ClonedObject $managedDefinitions -AsHashTable -AsShallowClone + $deleteCandidates = $managedDefinitions.Clone() $deploymentRootScope = $PacEnvironment.deploymentRootScope $policyDefinitionsScopes = $PacEnvironment.policyDefinitionsScopes $duplicateDefinitionTracking = @{} @@ -35,6 +35,7 @@ function Build-PolicySetPlan { foreach ($file in $definitionFiles) { $Json = Get-Content -Path $file.FullName -Raw -ErrorAction Stop + $definitionObject = $null try { $definitionObject = $Json | ConvertFrom-Json -Depth 100 } @@ -47,7 +48,7 @@ function Build-PolicySetPlan { $id = "$deploymentRootScope/providers/Microsoft.Authorization/policySetDefinitions/$name" $displayName = $definitionProperties.displayName $description = $definitionProperties.description - $metadata = Get-ClonedObject $definitionProperties.metadata -AsHashTable + $metadata = Get-DeepCloneAsOrderedHashtable $definitionProperties.metadata # $version = $definitionProperties.version $parameters = $definitionProperties.parameters $policyDefinitions = $definitionProperties.policyDefinitions @@ -79,8 +80,14 @@ function Build-PolicySetPlan { if ($null -eq $displayName) { Write-Error "Policy Set '$name' from file '$($file.Name)' requires a displayName" -ErrorAction Stop } - if ($null -eq $policyDefinitions -or $policyDefinitions.Count -eq 0) { - Write-Error "Policy Set '$displayName' from file '$($file.Name)' requires a policyDefinitions array with at least one entry" -ErrorAction Stop + if ($null -eq $policyDefinitions) { + Write-Error "Policy Set '$displayName' from file '$($file.Name)' requires a policyDefinitions entry; it is null. Did you misspell policyDefinitions (it is case sensitive)?" -ErrorAction Stop + } + elseif ($policyDefinitions -isnot [System.Collections.IList]) { + Write-Error "Policy Set '$displayName' from file '$($file.Name)' requires a policyDefinitions array; it is not an array." -ErrorAction Stop + } + elseif ($policyDefinitions.Count -eq 0) { + Write-Error "Policy Set '$displayName' from file '$($file.Name)' requires a policyDefinitions array with at least one entry; it has zero entries." -ErrorAction Stop } if ($duplicateDefinitionTracking.ContainsKey($id)) { Write-Error "Duplicate Policy Set with name '$($name)' in '$($duplicateDefinitionTracking[$id])' and '$($file.FullName)'" -ErrorAction Stop diff --git a/Scripts/Helpers/Confirm-ActiveAzExemptions.ps1 b/Scripts/Helpers/Confirm-ActiveAzExemptions.ps1 index 1f237e05..9c47fcfb 100644 --- a/Scripts/Helpers/Confirm-ActiveAzExemptions.ps1 +++ b/Scripts/Helpers/Confirm-ActiveAzExemptions.ps1 @@ -47,7 +47,7 @@ function Confirm-ActiveAzExemptions { $metadata = $null } - $exemptionObj = [pscustomobject][ordered]@{ + $exemptionObj = [ordered]@{ name = $name displayName = $exemption.displayName description = $exemption.description diff --git a/Scripts/Helpers/Confirm-MetadataMatches.ps1 b/Scripts/Helpers/Confirm-MetadataMatches.ps1 index 28df7805..a71767bd 100644 --- a/Scripts/Helpers/Confirm-MetadataMatches.ps1 +++ b/Scripts/Helpers/Confirm-MetadataMatches.ps1 @@ -7,8 +7,8 @@ function Confirm-MetadataMatches { $match = $false $changePacOwnerId = $false - $existingMetadata = Get-ClonedObject $ExistingMetadataObj -AsHashTable - $definedMetadata = Get-ClonedObject $DefinedMetadataObj -AsHashTable + $existingMetadata = Get-DeepCloneAsOrderedHashtable $ExistingMetadataObj + $definedMetadata = Get-DeepCloneAsOrderedHashtable $DefinedMetadataObj # remove system generated metadata from consideration if ($existingMetadata.ContainsKey("createdBy")) { @@ -23,6 +23,9 @@ function Confirm-MetadataMatches { if ($existingMetadata.ContainsKey("updatedOn")) { $existingMetadata.Remove("updatedOn") } + if ($existingMetadata.ContainsKey("lastSyncedToArgOn")) { + $existingMetadata.Remove("lastSyncedToArgOn") + } $existingPacOwnerId = $existingMetadata.pacOwnerId $definedPacOwnerId = $definedMetadata.pacOwnerId @@ -40,5 +43,8 @@ function Confirm-MetadataMatches { $match = Confirm-ObjectValueEqualityDeep $existingMetadata $definedMetadata } + if (!$match) { + $null = $null + } return $match, $changePacOwnerId } diff --git a/Scripts/Helpers/Confirm-ObjectValueEqualityDeep.ps1 b/Scripts/Helpers/Confirm-ObjectValueEqualityDeep.ps1 index c0a74d78..8103c6cf 100644 --- a/Scripts/Helpers/Confirm-ObjectValueEqualityDeep.ps1 +++ b/Scripts/Helpers/Confirm-ObjectValueEqualityDeep.ps1 @@ -107,21 +107,20 @@ function Confirm-ObjectValueEqualityDeep { } else { $normalizedKeys1 = $Object1.PSObject.Properties.Name + if ($normalizedKeys1 -isnot [System.Collections.ICollection]) { + $normalizedKeys1 = @($normalizedKeys1) + } } if ($Object2 -is [System.Collections.IDictionary]) { $normalizedKeys2 = $Object2.Keys } else { $normalizedKeys2 = $Object2.PSObject.Properties.Name + if ($normalizedKeys2 -isnot [System.Collections.ICollection]) { + $normalizedKeys2 = @($normalizedKeys2) + } } - $key1IsNotArray = $normalizedKeys1 -isnot [System.Collections.ICollection] - $key2IsNotArray = $normalizedKeys2 -isnot [System.Collections.ICollection] - if ($key1IsNotArray) { - $normalizedKeys1 = @($normalizedKeys1) - } - if ($key2IsNotArray) { - $normalizedKeys2 = @($normalizedKeys2) - } + $allKeys = $normalizedKeys1 + $normalizedKeys2 $uniqueKeys = $allKeys | Sort-Object -Unique if ($null -eq $uniqueKeys) { @@ -135,13 +134,27 @@ function Confirm-ObjectValueEqualityDeep { # iterate and recurse foreach ($key in $uniqueKeys) { $item1 = $Object1.$key + if ($null -eq $item1) { + # property missing + $key1Array = $normalizedKeys1 -eq $key + if ($key1Array.Count -gt 0) { + # found a matching key (case insensitive) + $key1 = $key1Array[0] + $item1 = $Object1.$key1 + } + } $item2 = $Object2.$key - - if (Confirm-ObjectValueEqualityDeep $item1 $item2) { - # if either the property values are equal or a deep inspection shows equal, continue to the next property + if ($null -eq $item2) { + # property missing + $key2Array = $normalizedKeys2 -eq $key + if ($key2Array.Count -gt 0) { + # found a matching key (case insensitive) + $key2 = $key2Array[0] + $item2 = $Object2.$key2 + } } - else { - # if the property values are not equal and a deep inspection does not show equal, return false + $match = Confirm-ObjectValueEqualityDeep $item1 $item2 + if (!$match) { return $false } } diff --git a/Scripts/Helpers/Confirm-ParametersDefinitionMatch.ps1 b/Scripts/Helpers/Confirm-ParametersDefinitionMatch.ps1 index cf8178f5..c027339a 100644 --- a/Scripts/Helpers/Confirm-ParametersDefinitionMatch.ps1 +++ b/Scripts/Helpers/Confirm-ParametersDefinitionMatch.ps1 @@ -1,15 +1,28 @@ function Confirm-ParametersDefinitionMatch { [CmdletBinding()] param( - [PSCustomObject] $ExistingParametersObj, - [PSCustomObject] $DefinedParametersObj + $ExistingParametersObj, + $DefinedParametersObj ) $match = $true $incompatible = $false - $existingParameters = Get-ClonedObject $ExistingParametersObj -AsHashTable - $definedParameters = Get-ClonedObject $DefinedParametersObj -AsHashTable - $addedParameters = Get-ClonedObject $definedParameters -AsHashTable + $addedParameters = @{} + if ($null -eq $ExistingParametersObj) { + $existingParameters = @{} + } + else { + $existingParameters = Get-DeepCloneAsOrderedHashtable $ExistingParametersObj + } + if ($null -eq $DefinedParametersObj) { + $definedParameters = @{} + } + else { + $definedParameters = Get-DeepCloneAsOrderedHashtable $DefinedParametersObj + foreach ($definedParameterName in $definedParameters.Keys) { + $addedParameters.Add($definedParameterName, $definedParameters.$definedParameterName) + } + } foreach ($existingParameterName in $existingParameters.Keys) { # ignore paramer name case $definedParameterNameArray = $definedParameters.Keys -eq $existingParameterName @@ -27,7 +40,6 @@ function Confirm-ParametersDefinitionMatch { # analyze parameter type if ($existing.type -ne $defined.type) { - $match = $false $incompatible = $true break } diff --git a/Scripts/Helpers/Confirm-ParametersUsageMatches.ps1 b/Scripts/Helpers/Confirm-ParametersUsageMatches.ps1 index 6ab85691..5b00ab71 100644 --- a/Scripts/Helpers/Confirm-ParametersUsageMatches.ps1 +++ b/Scripts/Helpers/Confirm-ParametersUsageMatches.ps1 @@ -21,14 +21,17 @@ function Confirm-ParametersUsageMatches { return $false } foreach ($existingParameterName in $existingParameters.Keys) { - $definedParameterNameArray = $definedParameters.Keys -eq $existingParameterName - if ($definedParameterNameArray.Count -eq 0) { - # No matching parameter name found (case insensitive) - return $false - } - $definedParameterName = $definedParameterNameArray[0] $existingParameter = $existingParameters.$existingParameterName - $definedParameter = $definedParameters.$definedParameterName + $definedParameter = $definedParameters.$existingParameterName + if ($null -eq $definedParameter) { + $definedParameterNameArray = $definedParameters.Keys -eq $existingParameterName + if ($definedParameterNameArray.Count -eq 0) { + # No matching parameter name found (case insensitive) + return $false + } + $definedParameterName = $definedParameterNameArray[0] + $definedParameter = $definedParameters.$definedParameterName + } $existingParameterValue = $existingParameter if ($null -ne $existingParameterValue.value) { diff --git a/Scripts/Helpers/Confirm-PolicyDefinitionsParametersMatch.ps1 b/Scripts/Helpers/Confirm-PolicyDefinitionsParametersMatch.ps1 index 46020cbd..69b4923c 100644 --- a/Scripts/Helpers/Confirm-PolicyDefinitionsParametersMatch.ps1 +++ b/Scripts/Helpers/Confirm-PolicyDefinitionsParametersMatch.ps1 @@ -5,28 +5,28 @@ function Confirm-PolicyDefinitionsParametersMatch { $DefinedParametersObj ) - $existingParameters = ConvertTo-HashTable $ExistingParametersObj - $definedParameters = ConvertTo-HashTable $DefinedParametersObj - $addedParameters = Get-ClonedObject $definedParameters -AsHashTable -AsShallowClone - foreach ($existingParameterName in $existingParameters.Keys) { - $found = $false - foreach ($definedParameterName in $definedParameters.Keys) { - if ($definedParameterName -eq $existingParameterName) { - # remove key from $addedParameters - $addedParameters.Remove($definedParameterName) + if ($null -eq $ExistingParametersObj) { + $ExistingParametersObj = @{} + } + if ($null -eq $DefinedParametersObj) { + $DefinedParametersObj = @{} + } + $addedParameters = $DefinedParametersObj.Clone() + foreach ($existingParameterName in $ExistingParametersObj.Keys) { + $definedParameterNameArray = $DefinedParametersObj.Keys -eq $existingParameterName + if ($definedParameterNameArray.Count -gt 0) { + # remove key from $addedParameters + $addedParameters.Remove($definedParameterName) - # analyze parameter - $existing = $existingParameters.$existingParameterName - $defined = $definedParameters.$definedParameterName - $match = Confirm-ObjectValueEqualityDeep $existing $defined - if (!$match) { - return $false - } - $found = $true - break + # analyze parameter + $existing = $ExistingParametersObj.$existingParameterName + $defined = $DefinedParametersObj.$definedParameterName + $match = Confirm-ObjectValueEqualityDeep $existing $defined + if (!$match) { + return $false } } - if (!$found) { + else { # parameter deleted return $false } diff --git a/Scripts/Helpers/Convert-PolicyResourcesDetailsToFlatList.ps1 b/Scripts/Helpers/Convert-PolicyResourcesDetailsToFlatList.ps1 index 79f3d91f..b0827691 100644 --- a/Scripts/Helpers/Convert-PolicyResourcesDetailsToFlatList.ps1 +++ b/Scripts/Helpers/Convert-PolicyResourcesDetailsToFlatList.ps1 @@ -57,7 +57,7 @@ function Convert-PolicyResourcesDetailsToFlatList { $assignment = $detail.assignment $properties = Get-PolicyResourceProperties -PolicyResource $assignment $assignmentOverrides = $properties.overrides - $assignmentParameters = Get-ClonedObject $properties.parameters -AsHashTable + $assignmentParameters = Get-DeepCloneAsOrderedHashtable $properties.parameters } foreach ($policyInPolicySetInfo in $detail.policyDefinitions) { diff --git a/Scripts/Helpers/Get-AzPolicyAssignments.ps1 b/Scripts/Helpers/Get-AzPolicyAssignments.ps1 index 7de542f5..fa908031 100644 --- a/Scripts/Helpers/Get-AzPolicyAssignments.ps1 +++ b/Scripts/Helpers/Get-AzPolicyAssignments.ps1 @@ -20,10 +20,9 @@ function Get-AzPolicyAssignments { $policyResourcesTable = $DeployedPolicyResources.policyassignments $uniquePrincipalIds = @{} - foreach ($policyResourceRaw in $policyResources) { - $resourceTenantId = $policyResourceRaw.tenantId + foreach ($policyResource in $policyResources) { + $resourceTenantId = $policyResource.tenantId if ($resourceTenantId -in @($null, "", $environmentTenantId)) { - $policyResource = Get-ClonedObject $policyResourceRaw -AsHashTable -AsShallowClone $id = $policyResource.id $testId = $id $properties = Get-PolicyResourceProperties $policyResource diff --git a/Scripts/Helpers/Get-AzPolicyExemptions.ps1 b/Scripts/Helpers/Get-AzPolicyExemptions.ps1 index 7ac2966b..1e0e1b16 100644 --- a/Scripts/Helpers/Get-AzPolicyExemptions.ps1 +++ b/Scripts/Helpers/Get-AzPolicyExemptions.ps1 @@ -58,10 +58,9 @@ function Get-AzPolicyExemptions { $policyResourcesTable = $DeployedPolicyResources.policyexemptions $policyExemptionsCounters = $policyResourcesTable.counters - foreach ($policyResourceRaw in $policyResources) { - $resourceTenantId = $policyResourceRaw.tenantId + foreach ($policyResource in $policyResources) { + $resourceTenantId = $policyResource.tenantId if ($resourceTenantId -in @($null, "", $environmentTenantId)) { - $policyResource = Get-ClonedObject $policyResourceRaw -AsHashTable -AsShallowClone $properties = Get-PolicyResourceProperties $policyResource $id = $policyResource.id @@ -113,7 +112,7 @@ function Get-AzPolicyExemptions { } $resourceSelectors = $properties.resourceSelectors $assignmentScopeValidation = $properties.assignmentScopeValidation - $pacOwner = Confirm-PacOwner -ThisPacOwnerId $thisPacOwnerId -PolicyResource $policyResourceRaw -ManagedByCounters $policyExemptionsCounters.managedBy + $pacOwner = Confirm-PacOwner -ThisPacOwnerId $thisPacOwnerId -PolicyResource $policyResource -ManagedByCounters $policyExemptionsCounters.managedBy $status = "active" $expiresInDays = [Int32]::MaxValue if ($expiresOn) { diff --git a/Scripts/Helpers/Get-AzPolicyOrSetDefinitions.ps1 b/Scripts/Helpers/Get-AzPolicyOrSetDefinitions.ps1 index 9ca0c806..5009d22f 100644 --- a/Scripts/Helpers/Get-AzPolicyOrSetDefinitions.ps1 +++ b/Scripts/Helpers/Get-AzPolicyOrSetDefinitions.ps1 @@ -35,10 +35,9 @@ function Get-AzPolicyOrSetDefinitions { } $policyResources = Search-AzGraphAllItems -Query $query -ProgressItemName $progressItemName - foreach ($policyResourceRaw in $policyResources) { - $resourceTenantId = $policyResourceRaw.tenantId + foreach ($policyResource in $policyResources) { + $resourceTenantId = $policyResource.tenantId if ($resourceTenantId -in @($null, "", $environmentTenantId)) { - $policyResource = Get-ClonedObject $policyResourceRaw -AsHashTable -AsShallowClone $id = $policyResource.id $testId = $id $included, $resourceIdParts = Confirm-PolicyResourceExclusions ` diff --git a/Scripts/Helpers/Get-ClonedObject.ps1 b/Scripts/Helpers/Get-ClonedObject.ps1 deleted file mode 100644 index 546f8414..00000000 --- a/Scripts/Helpers/Get-ClonedObject.ps1 +++ /dev/null @@ -1,76 +0,0 @@ -function Get-ClonedObject { - [CmdletBinding()] - param( - [parameter(Position = 0, ValueFromPipeline = $true)] - $InputObject, - - [switch] $AsHashTable, - [switch] $AsShallowClone - ) - - $clone = $InputObject - if ($AsHashTable) { - # only support deep cloning to hashtable - if ($null -ne $InputObject) { - $json = ConvertTo-Json $InputObject -Depth 100 -Compress - $clone = ConvertFrom-Json $json -NoEnumerate -Depth 100 -AsHashTable - } - else { - $clone = @{} - } - } - else { - if ($null -ne $InputObject) { - if ($AsShallowClone) { - if ($InputObject -is [System.ICloneable]) { - $clone = $InputObject.Clone() - } - } - elseif ($InputObject -is [System.ValueType] -or $InputObject -is [datetime]) { - $clone = $InputObject - } - else { - if ($InputObject -is [System.Collections.IDictionary]) { - $clone = $InputObject.Clone() - foreach ($key in $InputObject.Keys) { - $value = $InputObject[$key] - $isComplex = -not ($null -eq $value -or $value -is [string] -or $value -is [System.ValueType] -or $value -is [datetime]) - if ($isComplex) { - $clone[$key] = Get-ClonedObject -InputObject $value - } - } - } - elseif ($InputObject -is [System.Collections.IList]) { - $clone = $InputObject.Clone() - for ($i = 0; $i -lt $clone.Count; $i++) { - $value = $InputObject[$i] - $isComplex = -not ($null -eq $value -or $value -is [string] -or $value -is [System.ValueType] -or $value -is [datetime]) - if ($isComplex) { - $clone[$i] = Get-ClonedObject -InputObject $value - } - else { - # assumin uniform IList - break - } - } - } - elseif ($InputObject -is [psobject]) { - $clone = $InputObject.PSObject.Copy() - foreach ($propertyName in $InputObject.PSObject.Properties.Name) { - $value = $InputObject.$propertyName - $isComplex = -not ($null -eq $value -or $value -is [string] -or $value -is [System.ValueType] -or $value -is [datetime]) - if ($isComplex) { - $clone.$propertyName = Get-ClonedObject -InputObject $value - } - } - } - } - } - } - if ($clone -is [System.Collections.IList]) { - Write-Output $clone -NoEnumerate - } - else { - Write-Output $clone - } -} diff --git a/Scripts/Helpers/Get-DeepCloneAsOrderedHashtable.ps1 b/Scripts/Helpers/Get-DeepCloneAsOrderedHashtable.ps1 new file mode 100644 index 00000000..461a2229 --- /dev/null +++ b/Scripts/Helpers/Get-DeepCloneAsOrderedHashtable.ps1 @@ -0,0 +1,21 @@ +function Get-DeepCloneAsOrderedHashtable { + [CmdletBinding()] + param( + [parameter(Position = 0, ValueFromPipeline = $true)] + $InputObject + ) + + $clone = $null + # only support deep cloning to hashtable + if ($null -ne $InputObject) { + $json = ConvertTo-Json $InputObject -Depth 100 -Compress + $clone = ConvertFrom-Json $json -NoEnumerate -Depth 100 -AsHashTable + } + + if ($clone -is [System.Collections.IList]) { + Write-Output $clone -NoEnumerate + } + else { + Write-Output $clone + } +} diff --git a/Scripts/Helpers/Get-DeploymentPlan.ps1 b/Scripts/Helpers/Get-DeploymentPlan.ps1 index f8ce2bd6..3a2d7bc4 100644 --- a/Scripts/Helpers/Get-DeploymentPlan.ps1 +++ b/Scripts/Helpers/Get-DeploymentPlan.ps1 @@ -3,9 +3,9 @@ function Get-DeploymentPlan { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "Plan input filename.")] - [string]$PlanFile, + [string] $PlanFile, - [switch] $AsHashtable + [switch] $AsHashTable ) $plan = $null @@ -14,7 +14,7 @@ function Get-DeploymentPlan { $Json = Get-Content -Path $PlanFile -Raw -ErrorAction Stop try { - $plan = $Json | ConvertFrom-Json -AsHashTable:$AsHashtable + $plan = $Json | ConvertFrom-Json -AsHashTable:$AsHashTable } catch { Write-Error "Assignment JSON file '$($PlanFile)' is not valid." -ErrorAction Stop diff --git a/Scripts/Helpers/Get-PolicyAssignmentsDetails.ps1 b/Scripts/Helpers/Get-PolicyAssignmentsDetails.ps1 index c456894a..46ddf3e5 100644 --- a/Scripts/Helpers/Get-PolicyAssignmentsDetails.ps1 +++ b/Scripts/Helpers/Get-PolicyAssignmentsDetails.ps1 @@ -36,7 +36,7 @@ function Get-PolicyAssignmentsDetails { if ($policySetId.Contains("policySetDefinition", [StringComparison]::InvariantCultureIgnoreCase)) { # PolicySet if ($policySetsDetails.ContainsKey($policySetId)) { - $combinedDetail = Get-ClonedObject $policySetsDetails.$policySetId -AsHashTable + $combinedDetail = Get-DeepCloneAsOrderedHashtable $policySetsDetails.$policySetId $combinedDetail.assignmentId = $assignmentId $combinedDetail.assignment = $assignment $combinedDetail.policySetId = $policySetId diff --git a/Scripts/Helpers/Merge-AssignmentParametersEx.ps1 b/Scripts/Helpers/Merge-AssignmentParametersEx.ps1 index 0c97e8d4..5676108f 100644 --- a/Scripts/Helpers/Merge-AssignmentParametersEx.ps1 +++ b/Scripts/Helpers/Merge-AssignmentParametersEx.ps1 @@ -15,7 +15,7 @@ function Merge-AssignmentParametersEx { $nonComplianceMessageColumn = $ParameterInstructions.nonComplianceMessageColumn #region parameters column - $parameters = Get-ClonedObject $BaseAssignment.parameters -AsHashTable + $parameters = Get-DeepCloneAsOrderedHashtable $BaseAssignment.parameters foreach ($row in $csvParameterArray) { if ($row.flatPolicyEntryKey) { $parametersColumnCell = $row[$parametersColumn] diff --git a/Scripts/Helpers/Out-PolicyDefinition.ps1 b/Scripts/Helpers/Out-PolicyDefinition.ps1 index 447ab804..92b08599 100644 --- a/Scripts/Helpers/Out-PolicyDefinition.ps1 +++ b/Scripts/Helpers/Out-PolicyDefinition.ps1 @@ -63,7 +63,7 @@ function Out-PolicyDefinition { # Write the content Remove-NullFields $Definition - $outDefinition = [PSCustomObject]@{'$schema' = "https://raw.githubusercontent.com/Azure/enterprise-azure-policy-as-code/main/Schemas/policy-definition-schema.json" } + $outDefinition = [ordered]@{'$schema' = "https://raw.githubusercontent.com/Azure/enterprise-azure-policy-as-code/main/Schemas/policy-definition-schema.json" } # update schema if policy set if ($Definition.properties.policyDefinitions) { diff --git a/Scripts/Helpers/RestMethods/Set-AzPolicyAssignmentRestMethod.ps1 b/Scripts/Helpers/RestMethods/Set-AzPolicyAssignmentRestMethod.ps1 index 5a72a572..51d59837 100644 --- a/Scripts/Helpers/RestMethods/Set-AzPolicyAssignmentRestMethod.ps1 +++ b/Scripts/Helpers/RestMethods/Set-AzPolicyAssignmentRestMethod.ps1 @@ -1,8 +1,8 @@ function Set-AzPolicyAssignmentRestMethod { [CmdletBinding()] param ( - [PSCustomObject] $AssignmentObj, - [string] $ApiVersion + $AssignmentObj, + $ApiVersion ) # Write log info @@ -11,7 +11,7 @@ function Set-AzPolicyAssignmentRestMethod { Write-Information "$displayName - $id" # Fix parameters to the weird way assignments uses JSON - $parametersTemp = Get-ClonedObject $AssignmentObj.parameters -AsHashTable + $parametersTemp = Get-DeepCloneAsOrderedHashtable $AssignmentObj.parameters $parameters = @{} foreach ($parameterName in $parametersTemp.Keys) { $value = $parametersTemp.$parameterName diff --git a/Scripts/Helpers/RestMethods/Set-AzPolicyDefinitionRestMethod.ps1 b/Scripts/Helpers/RestMethods/Set-AzPolicyDefinitionRestMethod.ps1 index 5b276c2b..ffd82c00 100644 --- a/Scripts/Helpers/RestMethods/Set-AzPolicyDefinitionRestMethod.ps1 +++ b/Scripts/Helpers/RestMethods/Set-AzPolicyDefinitionRestMethod.ps1 @@ -1,8 +1,8 @@ function Set-AzPolicyDefinitionRestMethod { [CmdletBinding()] param ( - [PSCustomObject] $DefinitionObj, - [string] $ApiVersion + $DefinitionObj, + $ApiVersion ) # Write log info diff --git a/Scripts/Helpers/RestMethods/Set-AzPolicyExemptionRestMethod.ps1 b/Scripts/Helpers/RestMethods/Set-AzPolicyExemptionRestMethod.ps1 index c0d628c4..b5ec4c72 100644 --- a/Scripts/Helpers/RestMethods/Set-AzPolicyExemptionRestMethod.ps1 +++ b/Scripts/Helpers/RestMethods/Set-AzPolicyExemptionRestMethod.ps1 @@ -1,8 +1,8 @@ function Set-AzPolicyExemptionRestMethod { [CmdletBinding()] param ( - [PSCustomObject] $ExemptionObj, - [string] $ApiVersion + $ExemptionObj, + $ApiVersion ) # Write log info diff --git a/Scripts/Helpers/Search-AzGraphAllItems.ps1 b/Scripts/Helpers/Search-AzGraphAllItems.ps1 index c3fac121..bd1e9f91 100644 --- a/Scripts/Helpers/Search-AzGraphAllItems.ps1 +++ b/Scripts/Helpers/Search-AzGraphAllItems.ps1 @@ -23,5 +23,6 @@ function Search-AzGraphAllItems { if ($count % $ProgressIncrement -ne 0) { Write-Information "Retrieved $($count) $ProgressItemName" } - Write-Output $data -NoEnumerate + $dataClone = Get-DeepCloneAsOrderedHashtable -InputObject $data + Write-Output $dataClone -NoEnumerate } diff --git a/Scripts/Helpers/Write-AssignmentDetails.ps1 b/Scripts/Helpers/Write-AssignmentDetails.ps1 index e689e022..69ccf1d4 100644 --- a/Scripts/Helpers/Write-AssignmentDetails.ps1 +++ b/Scripts/Helpers/Write-AssignmentDetails.ps1 @@ -15,6 +15,16 @@ function Write-AssignmentDetails { Write-Information "'$($DisplayName)' at $($shortScope)" } if ($IdentityStatus.requiresRoleChanges) { + foreach ($role in $IdentityStatus.updated) { + $roleScope = $role.scope + $roleShortScope = $roleScope -replace "/providers/Microsoft.Management", "" + if (!$role.properties.crossTenant) { + Write-Information " update role assignment description $($role.roleDisplayName) at $($roleShortScope)" + } + else { + Write-Information " update role assignment description $($role.roleDisplayName) at $($roleShortScope) (remote)" + } + } foreach ($role in $IdentityStatus.added) { $roleScope = $role.scope $roleShortScope = $roleScope -replace "/providers/Microsoft.Management", "" diff --git a/Scripts/Operations/Export-AzPolicyResources.ps1 b/Scripts/Operations/Export-AzPolicyResources.ps1 index 4f88c459..fcf31cfb 100644 --- a/Scripts/Operations/Export-AzPolicyResources.ps1 +++ b/Scripts/Operations/Export-AzPolicyResources.ps1 @@ -446,16 +446,16 @@ foreach ($pacSelector in $globalSettings.pacEnvironmentSelectors) { # } # } - $definition = [PSCustomObject]@{ + $definition = [ordered]@{ name = $name - properties = [PSCustomObject]@{ + properties = [ordered]@{ displayName = $properties.displayName description = $properties.description mode = $properties.mode metadata = $metadata version = $version parameters = $properties.parameters - policyRule = [PSCustomObject]@{ + policyRule = [ordered]@{ if = $properties.policyRule.if then = $properties.policyRule.then } @@ -543,20 +543,20 @@ foreach ($pacSelector in $globalSettings.pacEnvironmentSelectors) { # } # Adjust policyDefinitions for EPAC - $policyDefinitionsIn = Get-ClonedObject $properties.policyDefinitions -AsHashTable + $policyDefinitionsIn = $properties.policyDefinitions $policyDefinitionsOut = [System.Collections.ArrayList]::new() foreach ($policyDefinitionIn in $policyDefinitionsIn) { $parts = Split-AzPolicyResourceId -Id $policyDefinitionIn.policyDefinitionId $policyDefinitionOut = $null if ($parts.scopeType -eq "builtin") { - $policyDefinitionOut = [PSCustomObject]@{ + $policyDefinitionOut = [ordered]@{ policyDefinitionReferenceId = $policyDefinitionIn.policyDefinitionReferenceId policyDefinitionId = $policyDefinitionIn.policyDefinitionId parameters = $policyDefinitionIn.parameters } } else { - $policyDefinitionOut = [PSCustomObject]@{ + $policyDefinitionOut = [ordered]@{ policyDefinitionReferenceId = $policyDefinitionIn.policyDefinitionReferenceId policyDefinitionName = $parts.name parameters = $policyDefinitionIn.parameters @@ -572,9 +572,9 @@ foreach ($pacSelector in $globalSettings.pacEnvironmentSelectors) { $null = $policyDefinitionsOut.Add($policyDefinitionOut) } - $definition = [PSCustomObject]@{ + $definition = [ordered]@{ name = $policySetDefinition.name - properties = [PSCustomObject]@{ + properties = [ordered]@{ displayName = $properties.displayName description = $properties.description metadata = $metadata @@ -733,7 +733,7 @@ foreach ($pacSelector in $globalSettings.pacEnvironmentSelectors) { $parameters = @{} if ($null -ne $properties.parameters -and $properties.parameters.psbase.Count -gt 0) { - $parametersClone = Get-ClonedObject $properties.parameters -AsHashTable + $parametersClone = Get-DeepCloneAsOrderedHashtable $properties.parameters foreach ($parameterName in $parametersClone.Keys) { $parameterValue = $parametersClone.$parameterName $parameters[$parameterName] = $parameterValue.value diff --git a/Scripts/Operations/Create-AzRemediationTasks.ps1 b/Scripts/Operations/New-AzRemediationTasks.ps1 similarity index 99% rename from Scripts/Operations/Create-AzRemediationTasks.ps1 rename to Scripts/Operations/New-AzRemediationTasks.ps1 index b4fc8c9c..5128934d 100644 --- a/Scripts/Operations/Create-AzRemediationTasks.ps1 +++ b/Scripts/Operations/New-AzRemediationTasks.ps1 @@ -242,7 +242,7 @@ else { Write-Verbose "Parameters: $($parameters | ConvertTo-Json -Depth 99)" if ($WhatIfPreference) { Write-Information "`tWhatIf: Remediation Task would have been created." - $newPolicyRemediationTask = [PSCustomObject]@{ + $newPolicyRemediationTask = [ordered]@{ Name = $parameters.Name Id = $parameters.Name PolicyAssignmentId = $_.PolicyAssignmentId @@ -257,7 +257,7 @@ else { if ($null -eq $newPolicyRemediationTask) { Write-Information "`tRemediation Task could not be created." - $failedPolicyRemediationTask = [PSCustomObject]@{ + $failedPolicyRemediationTask = [ordered]@{ Name = $parameters.Name Id = "Not created" PolicyAssignmentId = $_.PolicyAssignmentId @@ -334,7 +334,7 @@ else { } elseif ($remediationTaskState -eq 'Failed') { Write-Information "`tRemediation Task '$($runningPolicyRemediationTask.Name)' failed." - $failedPolicyRemediationTask = [PSCustomObject]@{ + $failedPolicyRemediationTask = [ordered]@{ Name = $runningPolicyRemediationTask.Name Id = $runningPolicyRemediationTask.Id PolicyAssignmentId = $runningPolicyRemediationTask.PolicyAssignmentId diff --git a/Scripts/Operations/Create-AzureDevOpsBug.ps1 b/Scripts/Operations/New-AzureDevOpsBug.ps1 similarity index 100% rename from Scripts/Operations/Create-AzureDevOpsBug.ps1 rename to Scripts/Operations/New-AzureDevOpsBug.ps1 diff --git a/Scripts/Operations/Create-GitHubIssue.ps1 b/Scripts/Operations/New-GitHubIssue.ps1 similarity index 100% rename from Scripts/Operations/Create-GitHubIssue.ps1 rename to Scripts/Operations/New-GitHubIssue.ps1