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
    }
}