Private/MenuHandlers.ps1

# Start-NSPADManager's menu actions. Each gathers what it needs, then calls the public functions;
# every change goes through Invoke-NSPStep, so DRY RUN is honoured.

function Write-NSPADScreen {
    param([Parameter(Mandatory)][string]$Title)
    Clear-NSPConsole
    Write-NSPConsoleHeader -Title $Title -Subtitle 'AD Manager'
    Write-Host ''
}

function Wait-NSPADReturn {
    Read-Host "`nPress Enter to return to the menu" | Out-Null
}

function Get-NSPADScaffoldAnswers {
    # The answers the scaffold plan reads: this server's saved AD answers when they carry scaffold
    # fields, else optionally a ClientAnswers\<Abbrev>.json file, else $null (generic defaults).
    $saved = $script:ADAnswers
    $scaffoldFields = @('Auth_UserGroup_Value', 'VPNFW_DNS_Mode') + @(Get-NSPADPurposeGroupDefault | ForEach-Object AnswersField)
    if ($saved -and @($scaffoldFields | Where-Object { $saved.PSObject.Properties[$_] }).Count) {
        $label = if ($saved.PSObject.Properties['CompanyName'] -and $saved.CompanyName) { " for $($saved.CompanyName)" } else { '' }
        if (Read-NSPConfirm -Prompt "Use the saved answers$($label)?" -DefaultYes) { return $saved }
    }
    if (-not (Read-NSPConfirm -Prompt "Read defaults from a client's saved answers file (ClientAnswers\<Abbrev>.json)?")) { return $null }
    $path = (Read-NSPNonEmpty -Prompt 'Path to the answers file').Trim('"')
    if (-not (Test-Path -LiteralPath $path)) {
        Write-Host " '$path' not found - using generic defaults instead." -ForegroundColor Yellow
        return $null
    }
    try { return (Get-Content -LiteralPath $path -Raw | ConvertFrom-Json) }
    catch {
        Write-Host " Could not parse '$path' ($($_.Exception.Message)) - using generic defaults instead." -ForegroundColor Yellow
        return $null
    }
}

function Invoke-NSPADMenuPrereqs {
    Write-NSPADScreen 'Install RSAT / management modules'
    $st = Get-NSPADPrerequisiteStatus
    $adText = if ($st.ActiveDirectory) { 'present' } else { 'MISSING - needed for menu 2/3 (group/OU scaffold)' }
    $gpText = if ($st.GroupPolicy) { 'present' } else { 'MISSING - needed for menu 4/5 (GPO, WMI filters)' }
    Write-NSPConsoleLine " ActiveDirectory module : $adText" -Role $(if ($st.ActiveDirectory) { 'Success' } else { 'Warning' })
    Write-NSPConsoleLine " GroupPolicy module : $gpText" -Role $(if ($st.GroupPolicy) { 'Success' } else { 'Warning' })
    Write-Host ''
    if (Read-NSPConfirm -Prompt 'Proceed?' -DefaultYes) { Install-NSPADPrerequisite }
    Wait-NSPADReturn
}

function Invoke-NSPADMenuScaffold {
    Write-NSPADScreen 'Stand up the VPN group/OU scaffold'
    if (-not (Get-Command Get-ADOrganizationalUnit -ErrorAction SilentlyContinue)) {
        Write-NSPConsoleLine ' ActiveDirectory module not available - run menu 1 (Install RSAT) first.' -Role Error
        Wait-NSPADReturn; return
    }
    $clientAnswers = Get-NSPADScaffoldAnswers
    Write-Host ''
    Write-NSPConsoleLine ' Pick the base OU everything gets created under:' -Role Heading
    $baseOU = Select-NSPADOrganizationalUnit
    if (-not $baseOU) {
        Write-NSPConsoleLine ' Cancelled - no OU selected.' -Role Muted
        Wait-NSPADReturn; return
    }
    $plan = Get-NSPADScaffoldPlan -ClientAnswers $clientAnswers -BaseOU $baseOU
    Write-Host "`n About to create:" -ForegroundColor Cyan
    Show-NSPADScaffoldPlan -Plan $plan
    if (Read-NSPConfirm -Prompt 'Create/update this scaffold now?' -DefaultYes) {
        New-NSPADVpnGroupScaffold -Plan $plan
        Write-NSPConsoleLine "`n Done." -Role Success
    }
    Wait-NSPADReturn
}

function Show-NSPADScaffoldPlan {
    param([Parameter(Mandatory)]$Plan)
    Write-Host ''
    Write-Host " $($Plan.BaseOU)" -ForegroundColor Gray
    Write-Host ' VPNGroups\' -ForegroundColor White
    Write-Host " $($Plan.MainGroupName)" -ForegroundColor Green
    Write-Host ' FWRules\' -ForegroundColor White
    foreach ($pg in $Plan.PurposeGroups) { Write-Host " $($pg.Name) ($($pg.Key))" -ForegroundColor Green }
    Write-Host ''
}

function Invoke-NSPADMenuNestGroup {
    Write-NSPADScreen 'Nest a group into the shared VPNFW purpose groups'
    if (-not (Get-Command Get-ADGroup -ErrorAction SilentlyContinue)) {
        Write-NSPConsoleLine ' ActiveDirectory module not available - run menu 1 (Install RSAT) first.' -Role Error
        Wait-NSPADReturn; return
    }
    Write-NSPConsoleLine ' Pick the FWRules\ OU the purpose groups live under:' -Role Heading
    $fwRulesOU = Select-NSPADOrganizationalUnit
    if (-not $fwRulesOU) {
        Write-NSPConsoleLine ' Cancelled - no OU selected.' -Role Muted
        Wait-NSPADReturn; return
    }
    $purposeGroups = @(Get-ADGroup -SearchBase $fwRulesOU -SearchScope OneLevel -Filter * -ErrorAction SilentlyContinue | Sort-Object Name)
    if (-not $purposeGroups.Count) {
        Write-NSPConsoleLine " No groups found directly under '$fwRulesOU' - run menu 2 first, or pick a different OU." -Role Warning
        Wait-NSPADReturn; return
    }
    $memberName = Read-NSPNonEmpty -Prompt 'AD group to nest (exact name)'
    $memberGroup = Get-ADGroup -Filter "Name -eq '$($memberName.Replace("'", "''"))'" -ErrorAction SilentlyContinue
    if (-not $memberGroup) {
        Write-NSPConsoleLine " No group named '$memberName' found - nothing nested." -Role Warning
        Wait-NSPADReturn; return
    }
    Write-Host ''
    for ($i = 0; $i -lt $purposeGroups.Count; $i++) { Write-Host (' {0}. {1}' -f ($i + 1), $purposeGroups[$i].Name) }
    Write-Host ''
    $sel = Read-NSPOptional -Prompt "Numbers to nest '$memberName' into (comma-separated), or blank to cancel"
    $picked = @($sel -split '[,\s]+' | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ - 1 } |
            Where-Object { $_ -ge 0 -and $_ -lt $purposeGroups.Count } | ForEach-Object { $purposeGroups[$_] })
    if (-not $picked.Count) {
        Write-NSPConsoleLine ' Nothing selected - cancelled.' -Role Muted
        Wait-NSPADReturn; return
    }
    foreach ($pg in $picked) { Add-NSPADGroupMember -GroupIdentity $pg.DistinguishedName -MemberIdentity $memberGroup.DistinguishedName }
    Wait-NSPADReturn
}

function Invoke-NSPADMenuAutoEnrollGpo {
    Write-NSPADScreen 'Push the certificate auto-enrollment GPO'
    if (-not (Get-Command Get-GPO -ErrorAction SilentlyContinue)) {
        Write-NSPConsoleLine ' GroupPolicy module not available - run menu 1 (Install RSAT) first.' -Role Error
        Wait-NSPADReturn; return
    }
    $gpoName = Read-NSPNonEmpty -Prompt 'GPO name' -CurrentValue 'NSP - Certificate Auto-Enrollment'
    Write-Host ''
    Write-Host " GPO: $gpoName (dedicated - Default Domain Policy is not touched)" -ForegroundColor White
    Write-Host ' Linked at the domain root.' -ForegroundColor Gray
    foreach ($v in (Get-NSPADAutoEnrollGpoPlan)) {
        Write-Host (' [{0}] {1}\{2} = {3} ({4})' -f $v.Hive, $v.Key, $v.ValueName, $v.Value, $v.Type) -ForegroundColor Gray
    }
    Write-Host ' AEPolicy 7 = enrolled + renew/update/remove-revoked + update-templates' -ForegroundColor DarkGray
    if (Read-NSPConfirm -Prompt "`nProceed?" -DefaultYes) { New-NSPADAutoEnrollGpo -GpoName $gpoName }
    Wait-NSPADReturn
}

function Invoke-NSPADMenuWmiFilters {
    Write-NSPADScreen 'Stand up the well-known WMI filters'
    if (-not (Get-Command Get-GPO -ErrorAction SilentlyContinue)) {
        Write-NSPConsoleLine ' GroupPolicy module not available - run menu 1 (Install RSAT) first.' -Role Error
        Wait-NSPADReturn; return
    }
    $filters = @(Get-NSPADWellKnownWmiFilter)
    Write-NSPConsoleLine ' Will create/update:' -Role Heading
    foreach ($f in $filters) { Write-Host " $($f.Name)" -ForegroundColor Gray }
    if (-not (Read-NSPConfirm -Prompt "`nProceed?" -DefaultYes)) { Wait-NSPADReturn; return }
    try {
        # Written through ADSI (0.1.4) - the BW.Utils module Install-NSPADWmiFilterModule fetched isn't needed.
        foreach ($f in $filters) { New-NSPADWmiFilter -Filter $f }
        Write-NSPConsoleLine "`n Done." -Role Success
    } catch {
        Write-NSPConsoleLine "`n ERROR: $($_.Exception.Message)" -Role Error
    }
    Wait-NSPADReturn
}

function Invoke-NSPADMenuGpoRecipe {
    # Import-NSPADGpoRecipe turns dry run off for its own confirmed run, so the dashboard's DRY RUN
    # is honoured here with -WhatIf instead: it asks everything and shows what it would do.
    Write-NSPADScreen 'Deploy a GPO recipe'
    if (-not (Get-Command Get-GPO -ErrorAction SilentlyContinue)) {
        Write-NSPConsoleLine ' GroupPolicy module not available - run menu 1 (Install RSAT) first.' -Role Error
        Wait-NSPADReturn; return
    }
    $recipes = @(Get-NSPADGpoRecipe)
    $pick = Read-NSPMenu -Prompt 'Recipe' -Choices @($recipes | ForEach-Object { [pscustomobject]@{ Label = "$($_.Name) - $($_.GpoName)"; Value = $_.Name; Help = $_.Description } })
    if ($null -eq $pick) { return }
    $recipe = $recipes | Where-Object Name -eq $pick

    # The share folder and the recipe's non-secret values (Huntress: the organization key) are
    # remembered per recipe in this server's AD answers. Secret values (the account key) are never
    # saved - Import-NSPADGpoRecipe asks for them, masked, every time.
    if (-not $script:ADAnswers) { $script:ADAnswers = [pscustomobject]@{} }
    $remembered = {
        param([string]$Name, [string]$Prompt, [string]$Pattern)
        $field = "GpoRecipe_$($recipe.Name)_$Name"
        $saved = if ($script:ADAnswers.PSObject.Properties[$field]) { [string]$script:ADAnswers.$field } else { '' }
        while ($true) {
            $value = (Read-NSPNonEmpty -Prompt $Prompt -CurrentValue $saved).Trim()
            if ($Name -eq 'ShareRoot') { $value = $value.TrimEnd('\') }
            if (-not $Pattern -or $value -match $Pattern) { break }
            Write-NSPConsoleLine ' That value is not valid - try again.' -Role Warning
        }
        $script:ADAnswers | Add-Member -NotePropertyName $field -NotePropertyValue $value -Force
        $value
    }
    $share = & $remembered 'ShareRoot' "Deployment folder on a file share (e.g. \\fileserver\GPO_Share`$\$($recipe.ShareFolder))" $script:NSPADUncPattern
    $values = @{}
    foreach ($p in @($recipe.Prompts | Where-Object { -not $_.Secret })) { $values[$p.Name] = & $remembered $p.Name $p.Prompt $p.Pattern }

    $dry = Get-NSPDryRun
    if ($dry) { Write-NSPConsoleLine "`n DRY RUN: shows what it would do and changes nothing. Press D on the menu to switch to APPLY, then deploy." -Role Heading }
    try {
        $result = Import-NSPADGpoRecipe -Recipe $recipe.Name -Action Deploy -Scope All -FileShareRoot $share -Values $values -WhatIf:$dry
        if ($result) { Write-NSPConsoleLine "`n Deployed '$($result.GpoName)' - reports in $($result.ReportFolders -join ', ')." -Role Success }
    } catch {
        Write-NSPConsoleLine "`n ERROR: $($_.Exception.Message)" -Role Error
    }
    Wait-NSPADReturn
}

function Invoke-NSPADMenuVpnInventory {
    Write-NSPADScreen 'Export VPN documentation inventory (read-only)'
    Write-NSPConsoleLine ' Collects VPN groups (members, nesting, SIDs), client-auth certificate templates' -Role Muted
    Write-NSPConsoleLine ' (Enroll/AutoEnroll groups), and TameMyCerts OU stamps into one hand-off file.' -Role Muted
    Write-NSPConsoleLine ' Run on the CA/NPS server when possible: ias.xml and the TameMyCerts policy folder are found automatically.' -Role Muted
    Write-Host ''

    $patterns = Read-NSPNonEmpty -Prompt 'Group name patterns (comma-separated)' -CurrentValue 'IKEv2*, VPNFW*, FGT*'
    $groupPattern = @($patterns -split '\s*,\s*' | Where-Object { $_ })

    $npsPath = ''
    if (-not (Test-Path (Join-Path $env:SystemRoot 'System32\ias\ias.xml'))) {
        $answer = (Read-NSPOptional -Prompt "Path to a copy of the NPS server's ias.xml (blank to skip - NPS-named groups then rely on the patterns)").Trim('"')
        if ($answer -and (Test-Path -LiteralPath $answer)) { $npsPath = $answer }
    }
    $policyDirectory = ''
    if (-not (Get-ADInvTameMyCertsDirectory)) {
        $answer = (Read-NSPOptional -Prompt 'TameMyCerts policy folder (blank to skip - OU stamps are then left out)').Trim('"')
        if ($answer -and (Test-Path -LiteralPath $answer)) { $policyDirectory = $answer }
    }
    $savedCompany = if ($script:ADAnswers -and $script:ADAnswers.PSObject.Properties['CompanyName']) { [string]$script:ADAnswers.CompanyName } else { '' }
    $company = Read-NSPNonEmpty -Prompt 'Client (company) name for the file' -CurrentValue $savedCompany
    $outputDirectory = Read-NSPNonEmpty -Prompt 'Output folder' -CurrentValue (Get-NSPToolWorkPath -Tool AD -Kind Responses)

    try {
        $result = Export-NSPADVpnInventory -OutputDirectory $outputDirectory -Company $company -GroupPattern $groupPattern -NpsConfigPath $npsPath -PolicyDirectory $policyDirectory
        Write-Host ''
        Write-NSPConsoleLine (' Groups: {0} Templates: {1} TameMyCerts policies: {2}' -f $result.Groups, $result.Templates, $result.TameMyCertsPolicies) -Role Success
        Write-NSPConsoleLine " Written: $($result.Path)" -Role Success
        Write-NSPConsoleLine " Copy it to the Orchestrator's Staging\<Client>\Inbox\ folder (with the ias.xml for the VPN report)." -Role Heading
        Write-NSPConsoleLine ' Opening the folder in Explorer (accept the access prompt if one appears).' -Role Muted
        Open-NSPOutputFolder -Path $result.Path
    } catch {
        Write-NSPConsoleLine "`n ERROR: $($_.Exception.Message)" -Role Error
    }
    Wait-NSPADReturn
}

function Invoke-NSPADMenuLegacyCleanup {
    Write-NSPADScreen 'Clean up old AD-Manager (zip) files'
    Write-NSPConsoleLine ' Moves answers and hand-off files the zip-era AD-Manager left on this server into' -Role Muted
    Write-NSPConsoleLine " $(Get-NSPToolWorkPath -Tool AD), then removes the old copies (you are asked first)." -Role Muted
    $summary = Move-NSPToolLegacyData -Tool AD
    if ($summary.Found) { $script:ADAnswers = Get-NSPToolAnswers -Tool AD }
    Wait-NSPADReturn
}