functions/Build-PolicyDocumentation.ps1

function Build-PolicyDocumentation {

[CmdletBinding()]
param (
    [Parameter(Mandatory = $false, HelpMessage = "Definitions folder path. Defaults to environment variable `$env:PAC_DEFINITIONS_FOLDER or './Definitions'.")]
    [string]$definitionsRootFolder,

    [Parameter(Mandatory = $false, HelpMessage = "Output Folder. Defaults to environment variable `$env:PAC_OUTPUT_FOLDER or './Outputs'.")]
    [string] $outputFolder,

    [Parameter(Mandatory = $false, HelpMessage = "Formats CSV multi-object cells to use new lines and saves it as UTF-8 with BOM - works only fro Excel in Windows. Default uses commas to separate array elements within a cell")]
    [switch] $windowsNewLineCells,

    [Parameter(Mandatory = $false, HelpMessage = "Set to false if used non-interactive")]
    [bool] $interactive = $true,

    [Parameter(Mandatory = $false, HelpMessage = "Suppresses prompt for confirmation of each file in interactive mode")]
    [switch] $suppressConfirmation
)

#region Script Dot sourcing

# Common Functions

# Documentation Functions

#endregion dot sourcing

#region Initialize

$InformationPreference = 'Continue'
$globalSettings = Get-GlobalSettings -definitionsRootFolder $definitionsRootFolder -outputFolder $outputFolder
$definitionsFolder = $globalSettings.policyDocumentationsFolder
$pacEnvironments = $globalSettings.pacEnvironments
$outputPath = "$($globalSettings.outputFolder)/PolicyDocumentation"
if (-not (Test-Path $outputPath)) {
    New-Item $outputPath -Force -ItemType directory
}


# Caching information to optimize different outputs
$cachedPolicyResourceDetails = @{}
$cachedAssignmentsDetails = @{}
$currentPacEnvironmentSelector = ""
$pacEnvironment = $null

#endregion Initialize

Write-Information ""
Write-Information "==================================================================================================="
Write-Information "Reading documentation definitions in folder '$definitionsFolder'"
Write-Information "==================================================================================================="
$filesRaw = @()
$filesRaw += Get-ChildItem -Path $definitionsFolder -Recurse -File -Filter "*.jsonc"
$filesRaw += Get-ChildItem -Path $definitionsFolder -Recurse -File -Filter "*.json"
$files = @()
$files += ($filesRaw  | Sort-Object -Property Name)
if ($files.Length -gt 0) {
    Write-Information "Number of documentation definition files = $($files.Length)"
}
else {
    Write-Information "There aren't any documentation definition files in the folder provided!"
}

$processAllFiles = -not $interactive -or $suppressConfirmation -or $files.Length -eq 1
foreach ($file in $files) {
    Write-Information ""
    Write-Information "==================================================================================================="
    Write-Information "Reading and Processing '$($file.Name)'"
    Write-Information "==================================================================================================="

    $processThisFile = $processAllFiles
    if (-not $processAllFiles) {
        $title = "Process documentation definition file '$($file.Name)'"
        $message = "Do you want to process the file?"

        $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", `
            "Process the current file."
        $all = New-Object System.Management.Automation.Host.ChoiceDescription "&All", `
            "Process remaining files files in folder '$definitionsFolder'."
        $skip = New-Object System.Management.Automation.Host.ChoiceDescription "&Skip", `
            "Skip processing this file."
        $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $skip, $all)
        $result = $Host.UI.PromptForChoice($title, $message, $options, 0)
        switch ($result) {
            0 {
                $processThisFile = $true
            }
            1 {
                $processThisFile = $false
                Write-Information "***************************************************************************************************"
                Write-Information "***************************************************************************************************"
                Write-Information "** Skipping file '$($file.Name)'"
                Write-Information "***************************************************************************************************"
                Write-Information "***************************************************************************************************"
                Write-Information ""
                Write-Information ""
            }
            2 {
                $processThisFile = $true
                $processAllFiles = $true
            }
        }
    }

    if ($processThisFile) {
        $json = Get-Content -Path $file.FullName -Raw -ErrorAction Stop
        if (-not (Test-Json $json)) {
            Write-Error "The JSON file '$($file.Name)' is not valid." -ErrorAction Stop
        }
        $documentationSpec = $json | ConvertFrom-Json

        if (-not ($documentationSpec.documentAssignments -or $documentationSpec.documentPolicySets)) {
            Write-Error "JSON document must contain 'documentAssignments' and/or 'documentPolicySets' element(s)." -ErrorAction Stop
        }

        $documentPolicySets = $documentationSpec.documentPolicySets
        if ($documentPolicySets -and $documentPolicySets.Count -gt 0) {
            $pacEnvironment = $null
            foreach ($documentPolicySetEntry in $documentPolicySets) {
                $pacEnvironmentSelector = $documentPolicySetEntry.pacEnvironment
                if (-not $pacEnvironmentSelector) {
                    Write-Error "documentPolicySet entry does not specify pacEnvironment" -ErrorAction Stop
                }

                $fileNameStem = $documentPolicySetEntry.fileNameStem
                if (-not $fileNameStem) {
                    Write-Error "documentPolicySet entry does not specify fileNameStem" -ErrorAction Stop
                }

                $title = $documentPolicySetEntry.title
                if (-not $title) {
                    Write-Error "documentPolicySet entry does not specify title" -ErrorAction Stop
                }

                $policySets = $null
                if ($documentPolicySetEntry.initiatives -and !($documentPolicySetEntry.policySets)) {
                    $policySets = $documentPolicySetEntry.initiatives # legacy
                    Write-Warning "Legacy field `"initiatives`" used, change to `"policySets`"" -WarningAction Continue
                }
                elseif (!($documentPolicySetEntry.initiatives) -and !($documentPolicySetEntry.policySets)) {
                    $policySets = $documentPolicySetEntry.policySets
                }

                $itemArrayList = [System.Collections.ArrayList]::new()
                if ($null -ne $policySets -and $policySets.Count -gt 0) {
                    foreach ($policySet in $policySets) {
                        $itemEntry = @{
                            shortName    = $policySet.shortName
                            itemId       = $policySet.id
                            policySetId  = $policySet.id
                            assignmentId = $null
                        }
                        $null = $itemArrayList.Add($itemEntry)
                    }
                }
                else {
                    Write-Error "documentPolicySet entry does not specify an policySets array or policySets array is empty" -ErrorAction Stop
                }
                $itemList = $itemArrayList.ToArray()

                $environmentColumnsInCsv = $documentPolicySetEntry.environmentColumnsInCsv

                if (-not $cachedPolicyResourceDetails.ContainsKey($pacEnvironmentSelector)) {
                    if ($currentPacEnvironmentSelector -ne $pacEnvironmentSelector) {
                        $currentPacEnvironmentSelector = $pacEnvironmentSelector
                        $pacEnvironment = Switch-PacEnvironment `
                            -pacEnvironmentSelector $currentPacEnvironmentSelector `
                            -pacEnvironments $pacEnvironments `
                            -interactive $interactive

                    }
                }

                # Retrieve Policies and PolicySets for current pacEnvironment from cache or from Azure
                $policyResourceDetails = Get-PolicyResourceDetails `
                    -pacEnvironmentSelector $pacEnvironmentSelector `
                    -pacEnvironment $pacEnvironment `
                    -cachedPolicyResourceDetails $cachedPolicyResourceDetails
                $policySetDetails = $policyResourceDetails.policySets

                $flatPolicyList = Convert-PolicySetsToFlatList `
                    -itemList $itemList `
                    -details $policySetDetails

                # Print documentation
                Out-PolicySetsDocumentationToFile `
                    -outputPath $outputPath `
                    -fileNameStem $fileNameStem `
                    -windowsNewLineCells:$windowsNewLineCells `
                    -title $title `
                    -itemList $itemList `
                    -environmentColumnsInCsv $environmentColumnsInCsv `
                    -policySetDetails $policySetDetails `
                    -flatPolicyList $flatPolicyList
            }
        }

        # Process instructions to document Assignments
        if ($documentationSpec.documentAssignments) {
            $documentAssignments = $documentationSpec.documentAssignments
            $environmentCategories = $documentAssignments.environmentCategories

            # Process assignments for every environmentCategory specified
            [hashtable] $assignmentsByEnvironment = @{}
            foreach ($environmentCategoryEntry in $environmentCategories) {
                if (-not $environmentCategoryEntry.pacEnvironment) {
                    Write-Error "JSON document does not contain the required 'pacEnvironment' element." -ErrorAction Stop
                }
                # Load pacEnvironment
                $pacEnvironmentSelector = $environmentCategoryEntry.pacEnvironment
                if ($currentPacEnvironmentSelector -ne $pacEnvironmentSelector) {
                    $currentPacEnvironmentSelector = $pacEnvironmentSelector
                    $pacEnvironment = Switch-PacEnvironment `
                        -pacEnvironmentSelector $currentPacEnvironmentSelector `
                        -pacEnvironments $pacEnvironments `
                        -interactive $interactive
                }

                # Retrieve Policies and PolicySets for current pacEnvironment from cache or from Azure
                $policyResourceDetails = Get-PolicyResourceDetails `
                    -pacEnvironmentSelector $currentPacEnvironmentSelector `
                    -pacEnvironment $pacEnvironment `
                    -cachedPolicyResourceDetails $cachedPolicyResourceDetails

                # Retrieve assignments and process information or retrieve from cache is assignment previously processed
                $assignmentArray = $environmentCategoryEntry.representativeAssignments

                $itemList, $assignmentsDetails = Get-AssignmentsDetails `
                    -pacEnvironmentSelector $currentPacEnvironmentSelector `
                    -assignmentArray $assignmentArray `
                    -policyResourceDetails $policyResourceDetails `
                    -cachedAssignmentsDetails $cachedAssignmentsDetails

                # Flatten Policy lists in Assignments and reconcile the most restrictive effect for each Policy
                $flatPolicyList = Convert-PolicySetsToFlatList `
                    -itemList $itemList `
                    -details $assignmentsDetails

                # Store results of processing and flattening for use in document generation
                $null = $assignmentsByEnvironment.Add($environmentCategoryEntry.environmentCategory, @{
                        pacEnvironmentSelector = $currentPacEnvironmentSelector
                        scopes                 = $environmentCategoryEntry.scopes
                        itemList               = $itemList
                        assignmentsDetails     = $assignmentsDetails
                        flatPolicyList         = $flatPolicyList
                    }
                )
            }

            # Build documents
            $documentationSpecifications = $documentAssignments.documentationSpecifications
            foreach ($documentationSpecification in $documentationSpecifications) {
                $documentationType = $documentationSpecification.type
                if ($null -ne $documentationType) {
                    if ($documentationType -eq "effectsPerEnvironment") {
                        Write-Error "Field documentationType ($($documentationType)) is deprecated, effectsPerEnvironment is not supported." -ErrorAction Stop
                    }
                    else {
                        Write-Information "Field documentationType ($($documentationType)) is deprecated"
                    }
                }
                Out-PolicyAssignmentDocumentationToFile `
                    -outputPath $outputPath `
                    -windowsNewLineCells:$windowsNewLineCells `
                    -documentationSpecification $documentationSpecification `
                    -assignmentsByEnvironment $assignmentsByEnvironment
            }
        }

    }
}
}