internal/functions/Copy-DefinitionByAssignment.ps1
function Copy-DefinitionByAssignment { <# .SYNOPSIS This function creates a copy of a policy or policySet definition by assignment in a specified location based on the location of the Assignment that references it. .DESCRIPTION The Copy-DefinitionByAssignment function processes a definition from a specified path, copies it based on its assignment, and logs the changes. If multiple assignments are found, the highest priority folder will be used as specified in the FolderOrder hashtable. This is generally used to aid in securing the repository, such as for use with GitHub Code Owners. This can also be used to identify unused definitions, as they will be placed in the Unused subfolder. .PARAMETER PolicyPath The path to the policy or policySet definition file. This parameter is mandatory. .PARAMETER FolderOrder An ordered hashtable specifying the order of the folders. .PARAMETER Definitions The path to the directory containing the policy definitions. The default is "./Definitions". .PARAMETER Output The path to the directory where the output will be saved. The default is "./Output". .PARAMETER SuppressFileCopy A switch parameter. If specified, the function will not copy the policy definition file. .PARAMETER ChangeLogData An ordered hashtable for logging changes. This parameter is optional, but is necessary for the review against decisions made on previously processed policySetDefinitions when processing policyDefinitions. .EXAMPLE $myFolderOder = [ordered]@{ SecurityOperations = "security" HRProtected = "hr" FinanceTracking = "finance" PlatformOperations = "platform" } Copy-DefinitionByAssignment -PolicyPath "./Definitions/policySetDefinitions/MyPolicy.json" -FolderOrder $myFolderOrder -ChangeLogData:$ChangeLogData Processes the policySet definition in the "MyPolicy.json", and copies it to a location in ./Output/NewFolderStructure based on its assignment (or lack thereof) according to the $myFolderOrder hashtable. If it is found in a policyAssignment, the containing folder for the assignment in $myFolderOrder will be used. If it is not found, it will be placed in the Unused subfolder. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $PolicyPath, [System.Management.Automation.OrderedHashtable] $FolderOrder, [string] $Definitions = "./Definitions", [string] $Output = "./Output", [switch] $SuppressFileCopy, [System.Management.Automation.OrderedHashtable] [Parameter(Mandatory = $false)] $ChangeLogData ) $InformationPreference = "Continue" $defaultCategory = "Unused" $OutputPath = Join-Path $Output "NewFolderStructure" if (!(Test-Path $OutputPath)) { $null = New-Item -Path $OutputPath -ItemType Directory -Force } $OutputPath = Resolve-Path $OutputPath if ($PolicyPath -like "*policySetDefinitions*") { $definitionType = "policySetDefinitions" } elseif ($PolicyPath -like "*policyDefinitions*") { $definitionType = "policyDefinitions" } else { Write-Error "PolicySetPath $($PolicyPath) is not part of a valid folder structure for EPAC Definitions." return } If (Test-Path $PolicyPath) { $PolicyContent = Get-Content -Path $PolicyPath | ConvertFrom-Json -Depth 100 } else { Write-Error "PolicySetPath $($PolicyPath) does not exist." return } Write-Debug "Processing $($PolicyPath)..." Write-Debug " PolicySet.Name $($PolicyContent.Name)" if ($PolicyPath -like "*policySetDefinitions*") { $definitionType = "policySetDefinitions" } elseif ($PolicyPath -like "*policyDefinitions*") { $definitionType = "policyDefinitions" } else { Write-Error "PolicySetPath $($PolicyPath) is not part of a valid folder structure for EPAC Definitions." return } Write-Debug " DefinitionType $definitionType" $pAssignmentCopyRecord = [ordered]@{ SourceFile = $PolicyPath NewFilePath = "" AssignmentFile = "" ParentPolicySetFile = "" Status = "" RecordType = $definitionType Found = "False" AlreadyCategorized = "False" AssignmentSubfolder = "" SecurityRootPath = "" CurrentOutputPath = "" InitialRelativePath = "" } ## PROCESS POLICY SETS ## Test all assignments that are in this category to see if they contain the policySet. foreach ($f2 in $FolderOrder.Values) { ## Confirm the path doesn't already include a folder in the FolderOrder list, must be accounted for in relative path string. foreach ($f3 in $FolderOrder.Values) { if ($PolicyPath -like "*/$f3/*" -or $PolicyPath -like "*\$f3\*") { $pAssignmentCopyRecord.AlreadyCategorized = $f3 } } Remove-Variable f3 Write-Debug " Testing folder $f2 for assignments..." $assignmentRoot = @{ Path = $Definitions ChildPath = "policyAssignments" AdditionalChildPath = $f2 } $assignmentRootPath = Join-Path @assignmentRoot if (!(Test-Path $assignmentRootPath)) { Write-Warning "The path $assignmentRootPath is invalid, and will occur if you do not have all items in FolderOrder represented in policySetDefinitions. This can be normal while the initial buildout is being completed." if (!($(Test-Path (Split-Path (Split-Path $assignmentRootPath))) -eq "Definitions")) { Write-Error "No Definitions folder found. Location `"$Definitions`" is not part of a valid EPAC repository structure." break } else { Write-Debug "No assignments found in $assignmentRootPath" continue } } else { $policyAssignments = Get-ChildItem -Path $assignmentRootPath -Recurse -File -Include "*.json", "*.jsonc" -ErrorAction SilentlyContinue } if (!($policyAssignments) -or !($policyAssignments.count -gt 0)) { Write-Debug "No assignments found in $assignmentRootPath" continue } foreach ($a in $policyAssignments) { $pAssignmentCopyRecord.AssignmentSubfolder = Get-HydrationDefinitionSubfolderByContentId -ContentId $PolicyContent.Name -ContainerDefinitionPath $a.FullName -CategoryList:$ChangeLogData if ($pAssignmentCopyRecord.AssignmentSubfolder -ne "NotFound") { $pAssignmentCopyRecord.AssignmentFile = $a.FullName $pAssignmentCopyRecord.Status = "Active" $pAssignmentCopyRecord.Found = "True" break } } remove-variable a if ($PolicyPath -like "*\*") { $pAssignmentCopyRecord.InitialRelativePath = $(Split-Path $PolicyPath) -replace ".*\\$($pAssignmentCopyRecord.RecordType)\\", '' } else { $pAssignmentCopyRecord.InitialRelativePath = $(Split-Path $PolicyPath) -replace ".*\/$($pAssignmentCopyRecord.RecordType)\/", '' } $currentOutputPath = @{ Path = $OutputPath ChildPath = $pAssignmentCopyRecord.RecordType AdditionalChildPath = $pAssignmentCopyRecord.AssignmentSubfolder } $pAssignmentCopyRecord.CurrentOutputPath = Join-Path @currentOutputPath if ($pAssignmentCopyRecord.Found -ne "True") { $pAssignmentCopyRecord.Status = "Inactive" $pAssignmentCopyRecord.Found = "False" $pAssignmentCopyRecord.AssignmentSubfolder = $defaultCategory $pAssignmentCopyRecord.AssignmentFile = "None" } if ($pAssignmentCopyRecord.ParentPolicySetFile -eq "") { $pAssignmentCopyRecord.ParentPolicySetFile = "None" } Write-Debug " Copy Source: $($PolicyPath)" if ($pAssignmentCopyRecord.Found -eq "True") { break } # TODO: INSERT DEBUG BLOCK } if ($definitionType -eq "policySetDefinitions") { $securityRootPath = @{ Path = $OutputPath ChildPath = $pAssignmentCopyRecord.RecordType AdditionalChildPath = $pAssignmentCopyRecord.AssignmentSubfolder } $pAssignmentCopyRecord.SecurityRootPath = Join-Path @securityRootPath $currentOutputPath = @{ Path = $pAssignmentCopyRecord.SecurityRootPath ChildPath = $pAssignmentCopyRecord.InitialRelativePath } $pAssignmentCopyRecord.CurrentOutputPath = Join-Path @currentOutputPath $newFilePath = @{ Path = $pAssignmentCopyRecord.CurrentOutputPath ChildPath = $pAssignmentCopyRecord.Name } $pAssignmentCopyRecord.NewFilePath = Join-Path @NewFilePath Remove-Variable securityRootPath Remove-Variable currentOutputPath Remove-Variable newFilePath Write-Debug " Copy Destination: $($pAssignmentCopyRecord.NewFilePath)" Write-Debug " Assignment File: $($pAssignmentCopyRecord.AssignmentFile)" if (!($SuppressFileCopy)) { if (!(Test-Path $pAssignmentCopyRecord.CurrentOutputPath)) { $null = New-Item -Path $pAssignmentCopyRecord.CurrentOutputPath -ItemType Directory -Force } Copy-Item -Path $PolicyPath -Destination $pAssignmentCopyRecord.CurrentOutputPath } return $pAssignmentCopyRecord # END PROCESSING OF POLICYSETS } else { # We search all policySetDefinitions as a group, and do not use folder structure as it has no RoI here. # We have already processed them, and will use the data collected from that process if a link is found. $psdRootPath = Join-Path $Definitions "policySetDefinitions" $policySetDefinitions = Get-ChildItem -Path $psdRootPath -Recurse -File -Include "*.json", "*.jsonc" -ErrorAction SilentlyContinue if (!($policySetDefinitions) -or !($policySetDefinitions.count -gt 0)) { Write-Information "No Policy Set Definitions found in $psdRootPath" } else { if ($PolicyPath -like "*\*") { $pdInitialRelativePath = $(Split-Path $PolicyPath) -replace ".*\\$($pAssignmentCopyRecord.RecordType)\\", '' } else { $pdInitialRelativePath = $(Split-Path $PolicyPath) -replace ".*\/$($pAssignmentCopyRecord.RecordType)\/", '' } foreach ($psd in $policySetDefinitions) { $psdContent = Get-Content -Path $psd.FullName | ConvertFrom-Json -Depth 100 $parentCopyRecord = [ordered]@{ SourceFile = $PolicyPath NewFilePath = "" AssignmentFile = "" ParentPolicySetFile = "" Status = "" RecordType = $definitionType Found = "False" AlreadyCategorized = "False" AssignmentSubfolder = "" SecurityRootPath = "" CurrentOutputPath = "" InitialRelativePath = $pdInitialRelativePath } if ($psdContent.properties.policyDefinitions.policyDefinitionName -contains $PolicyContent.Name -and $ChangeLogData) { foreach ($c in $ChangeLogData.GetEnumerator()) { if ($c.Value.SourceFile -eq $psd.FullName) { $parentCopyRecord.SourceFile = $PolicyPath $parentCopyRecord.AssignmentFile = $c.Value.AssignmentFile $parentPolicySetFile = @{ Path = $c.Value.NewFilePath ChildPath = $(Split-Path $c.Value.SourceFile -Leaf) } $parentCopyRecord.ParentPolicySetFile = Join-Path @parentPolicySetFile $parentCopyRecord.Status = $c.Value.Status $parentCopyRecord.Found = $c.Value.Found $parentCopyRecord.AssignmentSubfolder = $c.Value.AssignmentSubfolder break } } Write-Debug " ParentPolicySetFile $($parentCopyRecord.ParentPolicySetFile)" } } Remove-Variable psd ## TODO: INSERT DEBUG BLOCK } if (($pAssignmentCopyRecord.Found -eq "True" -and $parentCopyRecord.Found -eq "True")) { Write-Debug "PolicySet Assignment and Policy Assignment Found" if ($pAssignmentCopyRecord.OutputPath -eq $parentCopyRecord.OutputPath) { # If BOTH are found, and equal in terms of security, one must be chosen arbitrarily $useRecord = $pAssignmentCopyRecord } elseif ($parentCopyRecord.AssignmentSubfolder -eq $defaultCategory) { $useRecord = $pAssignmentCopyRecord } elseif ($pAssignmentCopyRecord.AssignmentSubfolder -eq $defaultCategory) { $useRecord = $parentCopyRecord } else { if ([array]::IndexOf($($FolderOrder.Values, $pAssignmentCopyRecord.AssignmentSubfolder)) -gt $([array]::IndexOf($FolderOrder.Values, $parentCopyRecord.AssignmentSubfolder))) { $useRecord = $pAssignmentCopyRecord } else { $useRecord = $parentCopyRecord } } } elseif ($pAssignmentCopyRecord.Found -eq "True" -or ($parentCopyRecord.Found -ne "True" -and $pAssignmentCopyRecord.Found -ne "True")) { # If Neither are found, one must be chosen arbitrarily $useRecord = $pAssignmentCopyRecord } elseif ($parentCopyRecord.Found -eq "True") { $useRecord = $parentCopyRecord } else { Write-Error "This should never happen. $PolicyPath was neither found, nor unfound." } $securityRootPath = @{ "Path" = $OutputPath "ChildPath" = $useRecord.RecordType "AdditionalChildPath" = $useRecord.AssignmentSubfolder } $useRecord.SecurityRootPath = Join-Path @securityRootPath $currentOutputPath = @{ "Path" = $useRecord.SecurityRootPath "ChildPath" = $useRecord.InitialRelativePath } $useRecord.CurrentOutputPath = Join-Path @currentOutputPath $NewFilePath = @{ "Path" = $useRecord.CurrentOutputPath "ChildPath" = $useRecord.Name } $useRecord.NewFilePath = Join-Path @NewFilePath Write-Debug " Copy Source: $($PolicyPath)" Write-Debug " Creating Destination: $($useRecord.NewFilePath)" Write-Debug " Assignment File: $($useRecord.AssignmentFile)" if (!($SuppressFileCopy)) { if (!(Test-Path $useRecord.CurrentOutputPath)) { $null = New-Item -Path $useRecord.CurrentOutputPath -ItemType Directory -Force } Copy-Item -Path $PolicyPath -Destination $useRecord.CurrentOutputPath } return $useRecord } } |