packs/zero-trust-assessment/helpers/Get-MtZtaWorkshopPlan.ps1

function Get-MtZtaWorkshopPlan {
    <#
    .SYNOPSIS
    Builds the Zero Trust plan that the report's Zero Trust plan view shows, from the pack's workshop folder.

    .DESCRIPTION
    Reads the Microsoft Zero Trust Workshop files that build/zero-trust-workshop/Update-MtZtWorkshopData.ps1 writes
    into the pack's workshop folder (one per pillar, and icons.json), Maester's additions in workshop/maester.json
    and the pack's taxonomy.json, and returns one object in the shape the report reads
    (Schema 'maester.zero-trust-workshop/1'): the pillars, their functional areas and tasks, each task with the
    checks that measure it, and the icons the plan uses.

    Each task lists its checks in this order, each ID once:
    - Source 'microsoft': the assessment tests Microsoft maps to the task (ztw-task-mapping.json);
    - Source 'same-check': for each of those, the built-in Maester tests that taxonomy.json says check the same
      setting (AlsoCheckedBy), with Via naming the assessment test;
    - Source 'maester': the Maester tests that maester.json adds to the task.
    Tasks that maester.json adds are appended to their pillar with Origin 'maester'.

    It reads files only, so it can be tested on its own. Returns $null when the folder has no pillar files.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        # The pack's workshop folder.
        [Parameter(Mandatory)] [string] $WorkshopPath,

        # The pack's taxonomy.json, for AlsoCheckedBy. Optional.
        [Parameter()] [string] $TaxonomyPath
    )

    # Built-in Maester tests that check the same setting as an assessment test, by assessment test ID.
    $sameCheck = @{}
    $taxonomy = Read-MtZtaWorkshopFile -Path $TaxonomyPath
    foreach ($entry in @((Get-MtZtaWorkshopValue $taxonomy 'Tests').PSObject.Properties | Where-Object { $_ -and $_.MemberType -eq 'NoteProperty' })) {
        $also = @(Get-MtZtaWorkshopValue $entry.Value 'AlsoCheckedBy' | Where-Object { $_ } | ForEach-Object { [string]$_ })
        if ($also.Count -gt 0) { $sameCheck[$entry.Name] = $also }
    }

    $overlay = Read-MtZtaWorkshopFile -Path (Join-Path $WorkshopPath 'maester.json')
    $overlayTasks = Get-MtZtaWorkshopValue $overlay 'Tasks'
    $extraTasks = @(Get-MtZtaWorkshopValue $overlay 'ExtraTasks' | Where-Object { $_ })
    $allIcons = Get-MtZtaWorkshopValue (Read-MtZtaWorkshopFile -Path (Join-Path $WorkshopPath 'icons.json')) 'Icons'
    $iconNames = [System.Collections.Generic.List[string]]::new()

    $pillarOrder = @('identity', 'devices', 'data', 'network', 'infrastructure', 'security-operations', 'ai')
    $updated = $null
    $sourceUrl = $null
    $pillars = [System.Collections.Generic.List[object]]::new()
    foreach ($pillarId in $pillarOrder) {
        $pillar = Read-MtZtaWorkshopFile -Path (Join-Path $WorkshopPath "$pillarId.json")
        if (-not $pillar) { Write-Verbose "The workshop folder has no $pillarId.json."; continue }
        $source = Get-MtZtaWorkshopValue $pillar 'Source'
        $date = [string](Get-MtZtaWorkshopValue $source 'Updated')
        if ($date -and (-not $updated -or [string]::CompareOrdinal($date, $updated) -gt 0)) { $updated = $date }
        if (-not $sourceUrl) { $sourceUrl = Get-MtZtaWorkshopValue $source 'Url' }

        $areas = @(foreach ($area in @(Get-MtZtaWorkshopValue $pillar 'Areas' | Where-Object { $_ })) {
                $row = [ordered]@{ Id = [string]$area.Id }
                foreach ($name in 'Name', 'Description', 'Icon') { $row[$name] = Get-MtZtaWorkshopValue $area $name }
                foreach ($key in @($row.Keys)) { if ($null -eq $row[$key]) { $row.Remove($key) } }
                if ($row.Icon) { $iconNames.Add([string]$row.Icon) }
                [pscustomobject]$row
            })

        $tasks = [System.Collections.Generic.List[object]]::new()
        foreach ($task in @(Get-MtZtaWorkshopValue $pillar 'Tasks' | Where-Object { $_ })) {
            $checks = [System.Collections.Generic.List[object]]::new()
            $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
            $microsoft = @(Get-MtZtaWorkshopValue $task 'Checks' | Where-Object { $_ } | ForEach-Object { [string]$_ })
            foreach ($id in $microsoft) {
                if ($seen.Add($id)) { $checks.Add([pscustomobject][ordered]@{ Id = $id; Source = 'microsoft' }) }
            }
            foreach ($id in $microsoft) {
                foreach ($also in @($sameCheck[($id -replace '^ZTA\.', '')] | Where-Object { $_ })) {
                    if ($seen.Add($also)) { $checks.Add([pscustomobject][ordered]@{ Id = $also; Source = 'same-check'; Via = $id }) }
                }
            }
            $added = Get-MtZtaWorkshopValue $overlayTasks ([string]$task.Id)
            foreach ($id in @(Get-MtZtaWorkshopValue $added 'Tests' | Where-Object { $_ } | ForEach-Object { [string]$_ })) {
                if ($seen.Add($id)) { $checks.Add([pscustomobject][ordered]@{ Id = $id; Source = 'maester' }) }
            }
            $tasks.Add((ConvertTo-MtZtaWorkshopTask -Task $task -Origin 'workshop' -Checks $checks.ToArray() -Note (Get-MtZtaWorkshopValue $added 'Note')))
        }
        foreach ($task in ($extraTasks | Where-Object { [string](Get-MtZtaWorkshopValue $_ 'Pillar') -eq $pillarId })) {
            $checks = @(Get-MtZtaWorkshopValue $task 'Tests' | Where-Object { $_ } | Select-Object -Unique | ForEach-Object { [pscustomobject][ordered]@{ Id = [string]$_; Source = 'maester' } })
            $tasks.Add((ConvertTo-MtZtaWorkshopTask -Task $task -Origin 'maester' -Checks $checks -Note (Get-MtZtaWorkshopValue $task 'Note')))
        }
        foreach ($task in $tasks) { if ($task.PSObject.Properties['Icon']) { $iconNames.Add([string]$task.Icon) } }

        $pillars.Add([pscustomobject][ordered]@{
                Id    = $pillarId
                Name  = [string](Get-MtZtaWorkshopValue $pillar 'Name')
                Areas = $areas
                Tasks = $tasks.ToArray()
            })
    }
    if ($pillars.Count -eq 0) { return $null }

    # Only the icons the plan uses, in the order it first uses them.
    $icons = [ordered]@{}
    foreach ($name in $iconNames) {
        if ($icons.Contains($name)) { continue }
        $svg = Get-MtZtaWorkshopValue $allIcons $name
        if ($svg) { $icons[$name] = [string]$svg }
    }

    $source = [ordered]@{ Name = 'Microsoft Zero Trust Workshop'; Url = $sourceUrl; Updated = $updated }
    foreach ($key in @($source.Keys)) { if (-not $source[$key]) { $source.Remove($key) } }
    [pscustomobject][ordered]@{
        Schema  = 'maester.zero-trust-workshop/1'
        Source  = [pscustomobject]$source
        Pillars = $pillars.ToArray()
        Icons   = [pscustomobject]$icons
    }
}