Public/Install-NSPConditionalAccessCoverageTask.ps1

function Install-NSPConditionalAccessCoverageTask {
    <#
    .SYNOPSIS
        Opts a tenant into scheduled coverage and registers the task that runs it. Plan-only by default.
    .DESCRIPTION
        Without -Execute, reports what it would do and whether the prerequisites are in place:
        the coverage app is registered on this host (Register-NSPConditionalAccessCoverageApp)
        when the Cloud side runs, and the ActiveDirectory module is present when the AD side
        runs. Without the module, AD adds go to a script and the run reports OperatorActionRequired.
 
        With -Execute and confirmation:
          - sets Coverage.Scheduled in the tenant profile (Enabled, Sides, MaxAdds, TaskName);
          - registers the task: Windows PowerShell runs Invoke-NSPConditionalAccessCoverageJob
            every -EveryHours hours from -At, as -RunAs (default SYSTEM, which needs the
            certificate in Cert:\LocalMachine\My, and Microsoft.Graph.Authentication installed
            for all users). A Held or Failed run exits non-zero.
        Stop it with Set-NSPConditionalAccessCoverageSetting -ScheduledEnabled $false: the next
        run does nothing and reports Disabled. Unregister-ScheduledTask removes the task.
    .EXAMPLE
        Install-NSPConditionalAccessCoverageTask -TenantProfile $profilePath
    .EXAMPLE
        Install-NSPConditionalAccessCoverageTask -TenantProfile $profilePath -EveryHours 4 -MaxAdds 10 -Execute
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory)][string]$TenantProfile,
        [ValidateSet('Cloud', 'ActiveDirectory')][string[]]$Side,
        [ValidateRange(1, 1000)][int]$MaxAdds = 10,
        [ValidateRange(1, 24)][int]$EveryHours = 4,
        [datetime]$At = (Get-Date).Date.AddDays(1).AddHours(6),
        [string]$RunAs = 'SYSTEM',
        [switch]$Execute,
        [string]$ConfigRoot
    )

    $profilePath = (Resolve-Path -LiteralPath $TenantProfile).Path
    $content = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
    $tenantId = $content.TenantId
    if (-not $Side) { $Side = if ($content.Hybrid) { @('Cloud', 'ActiveDirectory') } else { @('Cloud') } }
    $settings = Get-NSPConditionalAccessCoverageSetting -TenantProfile $content
    if ($MaxAdds -gt $settings.MaxAdds) { throw "The scheduled cap ($MaxAdds) cannot exceed the profile's MaxAdds ($($settings.MaxAdds))." }

    $taskName = "NSP Conditional Access coverage - $(if ($content.TenantDomain) { $content.TenantDomain } else { $tenantId })"
    $modulePath = Join-Path $script:ModuleRoot 'NSP.M365.ConditionalAccess.psd1'
    # The task account would resolve its own default root (another user's LOCALAPPDATA), so the
    # job is always given this session's.
    if (-not $ConfigRoot) { $ConfigRoot = Get-NSPConditionalAccessStateRoot }
    $jobCommand = "Import-Module '$($modulePath -replace "'", "''")' -ErrorAction Stop; " +
    "`$r = Invoke-NSPConditionalAccessCoverageJob -TenantProfile '$($profilePath -replace "'", "''")' -ConfigRoot '$($ConfigRoot -replace "'", "''")'; " +
    "if (`$r.Status -in 'Held', 'Failed') { exit 1 }"
    $arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command `"$($jobCommand -replace '"', '\"')`""

    $problems = [System.Collections.Generic.List[string]]::new()
    if ($Side -contains 'Cloud' -and -not (Test-Path -LiteralPath (Join-Path (Get-NSPConditionalAccessLocalPath -Kind CoverageApps -ConfigRoot $ConfigRoot) "$tenantId.json"))) {
        $problems.Add('No coverage app is registered on this host. Run Register-NSPConditionalAccessCoverageApp -Execute first.')
    }
    $notes = @(if ($Side -contains 'ActiveDirectory' -and -not (Test-NSPConditionalAccessAdModule)) {
            'This host has no ActiveDirectory module: on-premises adds will be written to a script for a domain controller.'
        })

    $result = [ordered]@{
        Status = $null; TenantId = $tenantId; TaskName = $taskName; RunAs = $RunAs; Sides = @($Side)
        MaxAdds = $MaxAdds; FirstRun = $At.ToString('yyyy-MM-dd HH:mm'); EveryHours = $EveryHours
        Command = $jobCommand; Problems = @($problems); Notes = $notes
    }
    if ($problems.Count) { $result.Status = 'NotReady'; return [pscustomobject]$result }
    if (-not $Execute) { $result.Status = 'PlanOnly'; return [pscustomobject]$result }
    if (-not $PSCmdlet.ShouldProcess("tenant $tenantId", "Enable scheduled coverage (cap $MaxAdds adds) and register task '$taskName' as $RunAs every $EveryHours h")) { return }

    Register-NSPConditionalAccessScheduledTask -TaskName $taskName -Arguments $arguments -RunAs $RunAs -At $At -EveryHours $EveryHours -Confirm:$false
    $coverage = [ordered]@{
        ExemptGroup = $settings.ExemptGroup; IncludeGuests = $settings.IncludeGuests; MaxAdds = $settings.MaxAdds; MaxAddsPercent = $settings.MaxAddsPercent
        Scheduled = [ordered]@{ Enabled = $true; Sides = @($Side); MaxAdds = $MaxAdds; TaskName = $taskName; InstalledAtUtc = (Get-Date).ToUniversalTime().ToString('o') }
    }
    $content | Add-Member -NotePropertyName Coverage -NotePropertyValue ([pscustomobject]$coverage) -Force
    Save-NSPConditionalAccessJson -InputObject $content -Path $profilePath
    $result.Status = 'Installed'
    [pscustomobject]$result
}