Public/Get-UTCMTenantDriftReport.ps1

function Get-UTCMTenantDriftReport {
    <#
    .SYNOPSIS
        End-to-end drift detection pipeline: select baseline, compare to current, generate report.
 
    .DESCRIPTION
        Orchestrates the full drift workflow:
          1. Select or create a baseline snapshot (interactive menu or -SnapshotId / -CreateSnapshot).
          2. Optionally create a current-state snapshot and compare (-CompareToCurrent).
             Resources for the comparison snapshot default to the same types as the baseline;
             override with -ComparePreset or -CompareResources. In interactive mode a preset
             menu is shown.
          3. Optionally export the baseline to JSON (-ExportJson).
          4. Optionally generate a paginated HTML drift dashboard + CSV (-Dashboard).
             The HTML includes a drift summary on page 1 and the full current configuration
             state on page 2.
 
    .PARAMETER CreateSnapshot
        Create a fresh baseline snapshot before comparing.
 
    .PARAMETER SnapshotId
        GUID of an existing baseline snapshot. If omitted, an interactive menu is shown.
 
    .PARAMETER CompareToCurrent
        Create a current-state snapshot and compare it to the baseline.
 
    .PARAMETER ComparePreset
        Named preset for the current-state snapshot resources (e.g. ExchangeCore, TenantCore).
 
    .PARAMETER CompareResources
        Explicit resource identifiers for the current-state snapshot.
 
    .PARAMETER ExportJson
        Export the baseline snapshot to JSON in the output directory.
 
    .PARAMETER Dashboard
        Generate the HTML drift report and CSV.
 
    .PARAMETER ListSnapshots
        List available snapshots and exit.
 
    .PARAMETER NoPrompt
        Non-interactive mode (CI-friendly). Requires -SnapshotId or -CreateSnapshot.
 
    .PARAMETER OutputPath
        Output directory. Default: current directory.
 
    .PARAMETER PollingIntervalSeconds
        Seconds between polls during snapshot creation. Default: 10.
 
    .PARAMETER GraphScopes
        Graph scopes for the connection. Default: ConfigurationMonitoring.ReadWrite.All.
 
    .OUTPUTS
        The diff array (or $null if no comparison was performed).
 
    .EXAMPLE
        # Interactive: select baseline, choose preset, generate dashboard
        Get-UTCMTenantDriftReport -CompareToCurrent -Dashboard
 
    .EXAMPLE
        # CI pipeline
        Get-UTCMTenantDriftReport -SnapshotId $id -CompareToCurrent -Dashboard -ExportJson -OutputPath .\Reports -NoPrompt
 
    .EXAMPLE
        # Compare using a specific preset
        Get-UTCMTenantDriftReport -SnapshotId $id -CompareToCurrent -ComparePreset ExchangeCore -Dashboard
    #>

    [CmdletBinding()]
    param(
        [switch] $CreateSnapshot,

        # Optional SnapshotId: validate only when supplied (allow null for interactive selection)
        [ValidateScript({
            if ($_ -and -not (Validate-Guid $_)) { throw "SnapshotId '$_' is not a valid GUID." }
            $true
        })]
        [string] $SnapshotId,

        [switch] $CompareToCurrent,
        [switch] $ExportJson,
        [switch] $Dashboard,
        [switch] $ListSnapshots,
        [switch] $NoPrompt,

        [ValidateNotNullOrEmpty()]
        [string] $OutputPath = ".\",

        [ValidateRange(5,300)]
        [int] $PollingIntervalSeconds = 10,

        [string[]] $GraphScopes = @('ConfigurationMonitoring.ReadWrite.All'),

        # Resources for the "current" comparison snapshot.
        # -CompareResources: explicit resource identifiers (overrides preset & baseline).
        # -ComparePreset: named preset from resource-presets.json.
        # If neither is specified, defaults to the same resource types as the baseline.
        [string] $ComparePreset,
        [string[]] $CompareResources
    )

    # Ensure Graph session (scope list is passed through for consistency with your module)
    if (Get-Command -Name Ensure-GraphConnection -ErrorAction SilentlyContinue) {
        Ensure-GraphConnection -Scopes $GraphScopes
    }

    # Quick list mode: leverage the improved listing cmdlet
    if ($ListSnapshots) {
        # Show newest, completed, downloadable jobs for practical use
        return Get-UTCMAvailableSnapshot -OnlyCompleted -DownloadableOnly
    }

    # If no SnapshotId and no CreateSnapshot => optionally prompt to select one (unless -NoPrompt)
    if (-not $SnapshotId -and -not $CreateSnapshot) {
        $snapshots = Get-UTCMAvailableSnapshot -OnlyCompleted -DownloadableOnly
        if (-not $snapshots -or $snapshots.Count -eq 0) {
            throw "No snapshots exist. Use -CreateSnapshot to generate a new baseline."
        }

        if ($NoPrompt) {
            throw "No SnapshotId provided and -NoPrompt is set. Provide -SnapshotId or use -CreateSnapshot."
        }

        Write-Host "`nAvailable Snapshots (Completed & Downloadable):`n" -ForegroundColor Cyan
        $i = 1
        foreach ($s in $snapshots) {
            # default projection has id, displayName, createdDateTime, status
            Write-Host ("[{0}] {1} ({2}) Created: {3} Status: {4}" -f $i, $s.displayName, $s.id, $s.createdDateTime, $s.status)
            $i++
        }

        $selection = Read-Host "`nSelect a snapshot number to use as baseline"
        if (-not ($selection -as [int]) -or $selection -lt 1 -or $selection -gt $snapshots.Count) {
            throw "Invalid snapshot selection."
        }

        $SnapshotId = $snapshots[$selection - 1].id
    }

    # Create a fresh snapshot (uses module defaults, e.g., 'TenantCore' preset).
    if ($CreateSnapshot) {
        # New-UTCMSnapshot returns the final job object (status + resourceLocation).
        $job = New-UTCMSnapshot -PollingIntervalSeconds $PollingIntervalSeconds
        if ($job.status -notin @('succeeded','partiallySuccessful')) {
            $errors = $null
            if ($job.PSObject.Properties.Name -contains 'errorDetails') {
                $errors = $job.errorDetails -join '; '
            }
            throw ("Snapshot creation did not complete successfully. Status: {0}. {1}" -f $job.status, ($errors ?? ''))
        }
        $SnapshotId = $job.id
    }

    # Fetch baseline snapshot metadata (concise by default)
    $baseline = Get-UTCMSnapshot -SnapshotId $SnapshotId

    # Export baseline if requested (downloads from resourceLocation under the hood)
    if ($ExportJson) {
        if (-not (Get-Command -Name Resolve-OutputPath -ErrorAction SilentlyContinue)) {
            throw "Resolve-OutputPath utility is not available. Ensure your Private helpers are loaded."
        }
        $resolvedPath = Resolve-OutputPath -Path $OutputPath
        Export-UTCMSnapshot -Snapshot $baseline -Path $resolvedPath -Format JSON -Overwrite | Out-Null
    }

    # Optionally compare against the current state
    $diff = $null
    if ($CompareToCurrent) {
        if (-not (Get-Command -Name Get-UTCMCurrentStateSnapshot -ErrorAction SilentlyContinue)) {
            throw "Get-UTCMCurrentStateSnapshot is not available in this module."
        }
        if (-not (Get-Command -Name Compare-UTCMConfiguration -ErrorAction SilentlyContinue)) {
            throw "Compare-UTCMConfiguration is not available in this module."
        }

        # ── Resolve resources for the current-state snapshot ──────────────
        $compResources = $null

        if ($CompareResources -and $CompareResources.Count -gt 0) {
            # Explicit resource list wins
            $compResources = $CompareResources
        }
        elseif ($ComparePreset) {
            # Named preset
            $compResources = Resolve-UTCMResources -Preset $ComparePreset
        }

        if (-not $compResources) {
            # Derive from baseline snapshot items
            $baselineDetail = Get-UTCMSnapshot -SnapshotId $SnapshotId -IncludeItems
            $baselineResources = @($baselineDetail.configurationItems |
                ForEach-Object { $_.type } | Select-Object -Unique)

            if (-not $NoPrompt) {
                # Interactive: offer presets or same-as-baseline
                $presets     = Get-UTCMResourcePresets
                $presetNames = @($presets.Keys | Sort-Object)

                Write-Host "`nCompare resources:" -ForegroundColor Cyan
                Write-Host "[Enter] Same as baseline ($($baselineResources.Count) resource types)"
                $i = 1
                foreach ($pn in $presetNames) {
                    Write-Host ("[{0}] {1} ({2} resources)" -f $i, $pn, $presets[$pn].Count)
                    $i++
                }

                $choice = Read-Host "`nPress Enter for same as baseline, or select a preset number"

                if ($choice) {
                    $idx = $choice -as [int]
                    if ($idx -ge 1 -and $idx -le $presetNames.Count) {
                        $selectedPreset = $presetNames[$idx - 1]
                        $compResources  = @($presets[$selectedPreset])
                        Write-Host ("Using preset '{0}' for comparison." -f $selectedPreset) -ForegroundColor Green
                    } else {
                        Write-Warning "Invalid selection — using same resources as baseline."
                    }
                }
            }

            if (-not $compResources) {
                $compResources = $baselineResources
            }
        }

        if (-not $compResources -or $compResources.Count -eq 0) {
            throw "Could not determine resources for current-state comparison."
        }

        Write-Host ("Creating current-state snapshot with {0} resource type(s)..." -f $compResources.Count) -ForegroundColor Cyan
        $current = Get-UTCMCurrentStateSnapshot -Resources $compResources -PollingIntervalSeconds $PollingIntervalSeconds

        $diff = Compare-UTCMConfiguration -BaselineSnapshotId $SnapshotId -CompareSnapshotId $current.id

        # Fetch current items for full-state page in the drift report
        $currentItems = $null
        try {
            $currentSnap = Get-UTCMSnapshot -SnapshotId $current.id -IncludeItems
            if ($currentSnap.PSObject.Properties.Name -contains 'configurationItems') {
                $currentItems = $currentSnap.configurationItems
            }
        } catch {
            Write-Warning "Could not fetch current snapshot items for full-state report: $($_.Exception.Message)"
        }
    }

    # Optional dashboard (render only if we have a diff)
    if ($Dashboard -and -not $diff) {
        Write-Warning "No drift comparison was performed — nothing to render. Use -CompareToCurrent to generate a diff."
    }
    if ($Dashboard -and $diff) {
        if (-not (Get-Command -Name Resolve-OutputPath -ErrorAction SilentlyContinue)) {
            throw "Resolve-OutputPath utility is not available. Ensure your Private helpers are loaded."
        }
        $resolved = Resolve-OutputPath -Path $OutputPath

        if (-not (Get-Command -Name New-UTCMDriftReport -ErrorAction SilentlyContinue)) {
            throw "New-UTCMDriftReport is not available in this module."
        }

        $reportParams = @{
            Diff       = $diff
            SnapshotId = $SnapshotId
            OutputPath = $resolved
        }
        if ($currentItems) {
            $reportParams['CurrentItems'] = $currentItems
        }

        New-UTCMDriftReport @reportParams | Out-Null
    }

    return $diff
}