Private/Presets.ps1

# Private\Presets.ps1
# ---------------------------------------------------------------------------
# PRIVATE helpers for JSON-backed resource presets used by New-UTCMSnapshot
# - Get-UTCMPresetSearchPaths (where to read JSON presets from)
# - Get-UTCMResourcePresets (load + merge presets)
# - Resolve-UTCMResources (resolve explicit list OR a preset)
# Optional (warn-only validator, useful during preview):
# - Get-UTCMAllowListPath
# - Test-UTCMResourceTypes
# ---------------------------------------------------------------------------

# Returns the search order for the JSON preset file(s):
# Module-shipped -> Machine override -> User override
function Get-UTCMPresetSearchPaths {
    [CmdletBinding()]
    param()

    # This script is in <ModuleRoot>\Private, so module root is the parent folder
    $moduleRoot = Split-Path -Path $PSScriptRoot -Parent
    @(
        (Join-Path $moduleRoot 'Presets\resource-presets.json'),
        (Join-Path $env:ProgramData 'UTCM.Tools\resource-presets.json'),
        (Join-Path $env:AppData     'UTCM.Tools\resource-presets.json')
    )
}

# OPTIONAL: return the allow-list file for warn-only validation
function Get-UTCMAllowListPath {
    [CmdletBinding()]
    param()
    $moduleRoot = Split-Path -Path $PSScriptRoot -Parent
    (Join-Path $moduleRoot 'Presets\supported-resource-types.json')
}

# Built-in tiny fallback if JSON is missing (keeps module functional)
# Feel free to adjust the default preset names/contents.
$script:BuiltInResourcePresets = [ordered]@{
    ExchangeCore = @(
        'microsoft.exchange.sharedmailbox',
        'microsoft.exchange.transportrule'
    )
    TenantCore = @(
        'microsoft.entra.securitydefaults',
        'microsoft.entra.namedlocationpolicy',
        'microsoft.entra.authorizationpolicy',
        'microsoft.entra.tenantdetails',
        'microsoft.entra.conditionalaccesspolicy',
        'microsoft.exchange.sharedmailbox',
        'microsoft.exchange.transportrule',
        'microsoft.teams.meetingpolicy',
        'microsoft.teams.messagingpolicy',
        'microsoft.teams.appsetuppolicy',
        'microsoft.intune.devicecategory',
        'microsoft.intune.policysets',
        'microsoft.securityandcompliance.labelpolicy',
        'microsoft.securityandcompliance.dlpcompliancepolicy',
        'microsoft.securityandcompliance.protectionalert'
    )
}

# Load and merge resource presets from JSON (module, machine, user) with built-in defaults.
# OUTPUT: OrderedDictionary Name -> string[] resource identifiers
function Get-UTCMResourcePresets {
    [CmdletBinding()]
    param(
        [string[]] $SearchPaths = (Get-UTCMPresetSearchPaths)
    )

    # Keep insertion order; OrderedDictionary exposes .Contains(key), not .ContainsKey()
    $presets = [ordered]@{} + $script:BuiltInResourcePresets

    foreach ($path in $SearchPaths) {
        if (Test-Path $path) {
            try {
                $json = Get-Content -Path $path -Raw | ConvertFrom-Json -Depth 16
                if ($null -ne $json -and $null -ne $json.presets) {
                    $p = $json.presets

                    # If it's already a dictionary, use .Keys; otherwise iterate PSCustomObject properties.
                    if ($p -is [System.Collections.IDictionary]) {
                        foreach ($key in $p.Keys) {
                            $presets[$key] = @(
                                $p[$key] | ForEach-Object { [string]$_ } | Select-Object -Unique
                            )
                        }
                    } else {
                        foreach ($prop in $p.PSObject.Properties) {
                            $presets[$prop.Name] = @(
                                $prop.Value | ForEach-Object { [string]$_ } | Select-Object -Unique
                            )
                        }
                    }
                }
            } catch {
                Write-Warning "Failed to load UTCM presets from '$path': $($_.Exception.Message)"
            }
        }
    }

    return $presets
}

# Resolve effective resources from an explicit list or a named preset (backed by JSON).
# OUTPUT: string[] resource identifiers (non-empty)
function Resolve-UTCMResources {
    [CmdletBinding()]
    param(
        [string[]] $Resources,
        [string]   $Preset = 'TenantCore',
        [string[]] $PresetSearchPaths = (Get-UTCMPresetSearchPaths)
    )

    if ($Resources -and $Resources.Count -gt 0) {
        return @($Resources | ForEach-Object { [string]$_ })
    }

    $presets = Get-UTCMResourcePresets -SearchPaths $PresetSearchPaths

    # OrderedDictionary has .Contains(key)
    if (-not $presets.Contains($Preset)) {
        $known = ($presets.Keys | Sort-Object) -join ', '
        throw "Unknown preset '$Preset'. Known presets: $known"
    }

    $resolved = @($presets[$Preset] | ForEach-Object { [string]$_ })
    if (-not $resolved -or $resolved.Count -eq 0) {
        throw "Preset '$Preset' resolved to an empty resource list."
    }

    return $resolved
}

# OPTIONAL (warn-only): validate resource identifiers against a local allow-list JSON
function Test-UTCMResourceTypes {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string[]] $Resources,
        [string] $AllowListPath = (Get-UTCMAllowListPath)
    )

    if (Test-Path $AllowListPath) {
        try {
            $allow   = Get-Content -Path $AllowListPath -Raw | ConvertFrom-Json -Depth 8
            $allowed = @([string[]]$allow.resourceTypes)
            if ($allowed.Count -gt 0) {
                $invalid = $Resources | Where-Object { $_ -notin $allowed }
                if ($invalid) {
                    Write-Warning ("The following UTCM resource type(s) may not be supported in this preview: {0}" -f ($invalid -join ', '))
                    return $false
                }
            }
        } catch {
            Write-Warning "Failed to parse allow-list '$AllowListPath': $($_.Exception.Message)"
        }
    } else {
        Write-Verbose "Allow-list not found at '$AllowListPath'; skipping validation."
    }

    return $true
}