Public/Import-NSPADGpoRecipe.ps1

function Import-NSPADGpoRecipe {
    <#
    .SYNOPSIS
        Deploys a GPO recipe (Get-NSPADGpoRecipe) to this domain, run on the client's DC: prepares
        the file-share folder, writes the recipe's script to it, imports the recipe's GPO pointing at
        that script, and links it. Asks for anything not passed in.
    .DESCRIPTION
        -Action Deploy, in order:
          1. Share: creates <FileShareRoot> and its report folders, Kickstart-style - Authenticated
             Users read the folder (inheritance broken, Users removed) and may create/write files in
             the report folders, so the computers report back there.
          2. Script: renders the recipe's NSP.ClientScripts recipe to <FileShareRoot>\<file>, readable
             only by SYSTEM, administrators, domain computers and domain controllers (it can carry
             secrets, e.g. the Huntress account key).
          3. GPO: imports the recipe's portable backup (Import-NSPADGpoBackup - this domain's names
             filled in, the script path put into its preference XML) as the recipe's GPO, creating it
             or updating it in place, and links it per -Scope.
        Re-running updates everything in place: the script is regenerated, the GPO re-imported.
        Values are checked before anything changes; one confirmation covers all three steps
        (-Confirm:$false skips it, -WhatIf shows it).
    .PARAMETER Recipe
        The recipe name, e.g. Huntress (Get-NSPADGpoRecipe lists them).
    .PARAMETER Action
        Deploy (the only action so far).
    .PARAMETER Scope
        All - link at the domain root, every computer including domain controllers (the only scope
        so far).
    .PARAMETER FileShareRoot
        The deployment folder on a file share, e.g. \\fileserver\GPO_Share$\Huntress. Its parent must
        exist. Asked for when not given.
    .PARAMETER Values
        Answers to the recipe's prompts (e.g. @{ OrganizationKey = 'Contoso'; AccountKey = ... }); a
        secret may be a SecureString. Anything missing is asked for.
    .EXAMPLE
        Import-NSPADGpoRecipe -Recipe Huntress -Action Deploy -Scope All
    .EXAMPLE
        Import-NSPADGpoRecipe Huntress -FileShareRoot '\\fileserver\GPO_Share$\Huntress' -Values @{ OrganizationKey = 'Contoso' }
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ArgumentCompleter({ param($c, $p, $word) (Get-NSPADGpoRecipe).Name | Where-Object { $_ -like "$word*" } })]
        [string]$Recipe,
        [ValidateSet('Deploy')][string]$Action = 'Deploy',
        [ValidateSet('All')][string]$Scope = 'All',
        [string]$FileShareRoot,
        [hashtable]$Values = @{}
    )

    $r = @(Get-NSPADGpoRecipe | Where-Object { $_.Name -eq $Recipe })
    if ($r.Count -ne 1) { throw "No GPO recipe named '$Recipe'. Recipes: $(@(Get-NSPADGpoRecipe).Name -join ', ')." }
    $r = $r[0]
    $promptNames = @($r.Prompts | ForEach-Object { $_.Name })
    foreach ($key in $Values.Keys) {
        if ($promptNames -notcontains $key) { throw "Recipe '$($r.Name)' has no value '$key'. Values: $($promptNames -join ', ')." }
    }

    Use-NSPADDependency -Console
    Assert-NSPADCommand Import-GPO -ModuleHint 'GroupPolicy (RSAT)'
    Assert-NSPADCommand Get-ADDomain
    Import-NSPToolkitModule -Name NSP.ClientScripts -MinimumVersion 0.1.4
    $domain = Get-ADDomain -ErrorAction Stop

    Write-Host "`n$($r.Name) GPO recipe - $Action, scope $Scope, domain $($domain.DNSRoot)" -ForegroundColor Cyan
    if (-not $FileShareRoot) {
        $FileShareRoot = Read-NSPADGpoRecipeValue -Prompt 'Deployment folder on a file share' -Pattern $script:NSPADUncPattern -Hint "\\fileserver\GPO_Share`$\$($r.ShareFolder)"
    }
    $FileShareRoot = $FileShareRoot.Trim().TrimEnd('\')
    if ($FileShareRoot -notmatch $script:NSPADUncPattern) { throw "-FileShareRoot must be a UNC path (\\server\share\folder): $FileShareRoot" }

    $answers = [ordered]@{}
    foreach ($p in $r.Prompts) {
        if ($Values.ContainsKey($p.Name)) {
            $v = $Values[$p.Name]
            if ($v -is [Security.SecureString]) { $v = ConvertFrom-NSPADSecureString $v }
            $v = "$v".Trim()
            if ($p.Pattern -and $v -notmatch $p.Pattern) { throw "-Values $($p.Name) is not valid for recipe '$($r.Name)'." }
        } else {
            $v = Read-NSPADGpoRecipeValue -Prompt $p.Prompt -Pattern $p.Pattern -Secret:([bool]$p.Secret)
        }
        $answers[$p.Name] = $v
    }

    $payloadPath = Join-Path $FileShareRoot $r.Payload.FileName
    $all = [ordered]@{}
    foreach ($k in $answers.Keys) { $all[$k] = $answers[$k] }
    $all['ShareRoot'] = $FileShareRoot; $all['PayloadPath'] = $payloadPath; $all['GpoName'] = $r.GpoName

    # Rendered (and so validated) before anything changes.
    $scriptParams = @{ GeneratedBy = "Import-NSPADGpoRecipe $($r.Name) v$($r.Version)" }
    foreach ($k in $r.Payload.Parameters.Keys) { $scriptParams[$k] = Expand-NSPADGpoRecipeText -Text $r.Payload.Parameters[$k] -Values $all }
    $scriptText = ConvertTo-NSPClientScript -Recipe $r.Payload.ClientScriptRecipe -Parameters $scriptParams
    $tokens = @{}
    foreach ($k in $r.GpoTokens.Keys) { $tokens[$k] = [Security.SecurityElement]::Escape((Expand-NSPADGpoRecipeText -Text $r.GpoTokens[$k] -Values $all)) }

    $reportList = @($r.ReportFolders | ForEach-Object { Join-Path $FileShareRoot $_ })
    Write-Host " Share folder : $FileShareRoot"
    Write-Host " Script : $payloadPath"
    if ($reportList) { Write-Host " Reports : $($reportList -join ', ')" }
    Write-Host " GPO : $($r.GpoName), linked at $($domain.DistinguishedName) (all computers)"
    if (-not $PSCmdlet.ShouldProcess($domain.DNSRoot, "Deploy GPO recipe '$($r.Name)': share folder $FileShareRoot, GPO '$($r.GpoName)' linked at the domain root")) { return }

    Write-Host "`n[1/3] Share folder" -ForegroundColor Cyan
    Initialize-NSPADGpoRecipeShare -ShareRoot $FileShareRoot -ReportFolders @($r.ReportFolders)

    Write-Host "[2/3] Script" -ForegroundColor Cyan
    Write-NSPADGpoRecipePayload -Path $payloadPath -Text $scriptText -DomainSid "$($domain.DomainSID)"
    Write-Host " Wrote $payloadPath (SYSTEM, administrators, domain computers and DCs only)" -ForegroundColor Gray

    Write-Host "[3/3] GPO" -ForegroundColor Cyan
    # The confirmation above is this command's gate, so NSP.Toolkit's session dry run (on by
    # default) must not quietly skip the link step; the caller's setting comes back afterwards.
    $wasDryRun = Get-NSPDryRun
    Set-NSPDryRun -Enabled $false -Confirm:$false
    try {
        $import = Import-NSPADGpoBackup -Path $r.BackupPath -TargetName $r.GpoName -Replace $tokens -LinkAtDomainRoot:($Scope -eq 'All') -Confirm:$false
    } finally {
        Set-NSPDryRun -Enabled $wasDryRun -Confirm:$false
    }
    # Import-NSPADGpoBackup's own Linked flag is not trusted (see the backlog: a dry run used to skip
    # the link and still say True) - the link is read back from AD instead.
    $linked = $false
    if ($Scope -eq 'All') {
        $linked = Test-NSPADGpoLinked -GpoName $import.GpoName -TargetDN "$($domain.DistinguishedName)"
        if (-not $linked) { throw "GPO '$($import.GpoName)' was imported but is not linked at $($domain.DistinguishedName). Link it with: New-NSPADGpoLink -GpoName '$($import.GpoName)' -TargetDN '$($domain.DistinguishedName)'" }
    }
    Write-Host " Imported '$($r.GpoName)'$(if ($linked) { ' - linked at the domain root (checked in AD)' })" -ForegroundColor Gray
    Write-Host "`nDone. Computers pick it up at their next policy refresh (gpupdate /force to hurry one); results land in $(if ($reportList) { $reportList -join ' and ' } else { $FileShareRoot })." -ForegroundColor Green

    [pscustomobject]@{
        PSTypeName    = 'NSP.ActiveDirectory.GpoRecipeDeployment'
        Recipe        = $r.Name
        Action        = $Action
        Scope         = $Scope
        GpoName       = $r.GpoName
        Linked        = $linked
        FileShareRoot = $FileShareRoot
        PayloadPath   = $payloadPath
        ReportFolders = $reportList
    }
}