Private/Get-NSPConditionalAccessOnPremGroupScript.ps1

function Get-NSPConditionalAccessOnPremGroupScript {
    <#
    .SYNOPSIS
        The text of the script that creates a Conditional Access group in Active Directory, for the operator to run on-premises.
    .DESCRIPTION
        The script uses the public NSP modules, installed from the PowerShell Gallery when missing:
          - NSP.Bootstrap: Invoke-NSPElevated (right-click > Run with PowerShell relaunches it
            elevated; NSP_NO_ELEVATE=1 keeps it in the session) and Install-NSPModule.
          - NSP.ActiveDirectory: Select-NSPADOrganizationalUnit (browse to the OU when
            -OrganizationalUnit is not given) and New-NSPADGroup (idempotent, by sAMAccountName).
          - NSP.Toolkit: its dry-run switch, turned off only for a real run; -WhatIf creates nothing.
        Only loading NSP.Bootstrap itself is inline. Without Gallery access the script stops and
        says what to install. It offers a delta sync on the Entra Connect server, and waits for
        Enter before a window it opened closes.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)][string]$Name,
        [string]$Purpose = 'Conditional Access group managed by NSP.M365.ConditionalAccess.'
    )

    $template = @'
<#
    Generated by NSP.M365.ConditionalAccess on __DATE__.
    Creates the Active Directory group '__NAME__' (global security group) for Conditional Access.
 
    Right-click > Run with PowerShell, or run it from PowerShell. It asks to elevate when needed,
    and lets you browse to the OU unless -OrganizationalUnit is given. -WhatIf shows what it would do.
    It uses NSP.Bootstrap and NSP.ActiveDirectory, installing them from the PowerShell Gallery for
    the current user when missing. Afterwards the group syncs to Entra ID; then, in the dashboard,
    update the policies that use it (Travel > Update travel policies, or plan again).
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    # Distinguished name of the OU, for example OU=Groups,DC=contoso,DC=local.
    [string]$OrganizationalUnit
)
 
$groupName = '__NAME__'
$description = '__PURPOSE__'
 
# Wait before a window this script opened closes (Run with PowerShell, or the elevated relaunch).
$ownWindow = @([Environment]::GetCommandLineArgs() | Where-Object { $_ -match '^-(File|Command|EncodedCommand)$' }).Count -gt 0
function Wait-Close { if ($ownWindow) { Read-Host 'Press Enter to close' | Out-Null } }
function Stop-WithError {
    param([string]$Message)
    Write-Host "Failed: $Message" -ForegroundColor Red
    Wait-Close
    exit 1
}
 
# NSP.Bootstrap brings elevation and module installs; it is the one module loaded by hand.
try {
    if (-not (Get-Module NSP.Bootstrap | Where-Object { $_.Version -ge [version]'0.1.4' })) {
        if (-not (Get-Module -ListAvailable NSP.Bootstrap | Where-Object { $_.Version -ge [version]'0.1.4' })) {
            Write-Host 'Installing NSP.Bootstrap from the PowerShell Gallery (current user)...'
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
            if (-not (Get-PackageProvider -ListAvailable -Name NuGet -ErrorAction SilentlyContinue)) {
                Install-PackageProvider -Name NuGet -MinimumVersion '2.8.5.201' -Scope CurrentUser -Force | Out-Null
            }
            Install-Module -Name NSP.Bootstrap -MinimumVersion '0.1.4' -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
        }
        Import-Module NSP.Bootstrap -MinimumVersion '0.1.4' -ErrorAction Stop
    }
} catch {
    Stop-WithError "NSP.Bootstrap could not be loaded: $($_.Exception.Message). Without PowerShell Gallery access, install NSP.Bootstrap and NSP.ActiveDirectory on this server another way, then run this again."
}
 
$relaunch = @()
if ($OrganizationalUnit) { $relaunch += @('-OrganizationalUnit', $OrganizationalUnit) }
if ($WhatIfPreference) { $relaunch += '-WhatIf' }
try {
    if (Invoke-NSPElevated -ScriptPath $PSCommandPath -ArgumentList $relaunch) { return }
} catch {
    Stop-WithError "Not elevated, and elevation was refused or failed: $($_.Exception.Message)"
}
 
try {
    foreach ($module in @(@('NSP.Toolkit', '0.1.1'), @('NSP.ActiveDirectory', '0.1.4'))) {
        if (-not (Get-Module $module[0] | Where-Object { $_.Version -ge [version]$module[1] })) {
            Install-NSPModule -Name $module[0] -MinimumVersion $module[1]
            Import-Module $module[0] -MinimumVersion $module[1] -Global -ErrorAction Stop
        }
    }
    if (-not (Get-Command Get-ADGroup -ErrorAction SilentlyContinue)) { Import-Module ActiveDirectory -ErrorAction Stop }
 
    $sam = $groupName -replace '[^A-Za-z0-9_\-]', ''
    $existing = Get-ADGroup -LDAPFilter "(sAMAccountName=$sam)" -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host "The group '$groupName' already exists: $($existing.DistinguishedName). Nothing to do." -ForegroundColor Yellow
    } else {
        $chosen = -not $OrganizationalUnit
        if ($chosen) {
            Write-Host "Where should the group '$groupName' go? Browse to its OU." -ForegroundColor Cyan
            $OrganizationalUnit = Select-NSPADOrganizationalUnit
            if (-not $OrganizationalUnit) { Write-Host 'Cancelled. Nothing was created.' -ForegroundColor Yellow; Wait-Close; return }
        } else {
            Get-ADOrganizationalUnit -Identity $OrganizationalUnit -ErrorAction Stop | Out-Null
        }
        if ($chosen -and -not $WhatIfPreference) {
            $confirm = Read-Host "Create '$groupName' in $OrganizationalUnit? [Y/n]"
            if ($confirm -and $confirm -notmatch '^[Yy]') { Write-Host 'Cancelled. Nothing was created.' -ForegroundColor Yellow; Wait-Close; return }
        }
        Set-NSPDryRun -Enabled ([bool]$WhatIfPreference)
        $dn = New-NSPADGroup -Name $groupName -Path $OrganizationalUnit -GroupScope Global -GroupCategory Security -Description $description -WhatIf:$WhatIfPreference
        if (-not $WhatIfPreference) { Write-Host "Created: $dn" -ForegroundColor Green }
    }
    if (-not $WhatIfPreference -and (Get-Command Start-ADSyncSyncCycle -ErrorAction SilentlyContinue)) {
        $sync = Read-Host 'Entra Connect is on this server. Run a delta sync now? [Y/n]'
        if (-not $sync -or $sync -match '^[Yy]') { Start-ADSyncSyncCycle -PolicyType Delta | Out-Null; Write-Host 'Delta sync started.' -ForegroundColor Green }
    } else {
        Write-Host 'Next: run a delta sync on the Entra Connect server (Start-ADSyncSyncCycle -PolicyType Delta), or wait for the next one.'
    }
    Write-Host 'Then, in the dashboard, update the policies that use the group (Travel > Update travel policies, or plan again).'
} catch {
    Stop-WithError $_.Exception.Message
}
Wait-Close
'@

    $template.Replace('__DATE__', (Get-Date).ToString('yyyy-MM-dd')).
    Replace('__NAME__', ($Name -replace "'", "''")).
    Replace('__PURPOSE__', ($Purpose -replace "'", "''"))
}