Scripts/Remove-CAAppProxySetup.ps1

<#
.SYNOPSIS
    Tears down what CA-Manager menu 7 (App Proxy connector + Entra apps) and menu 5 (URL routing)
    created, so the whole flow can be re-run from a clean slate. Dev / re-test tool - NOT shipped in
    the client zip.
 
.DESCRIPTION
    Undoes, with a confirm before each destructive step:
      1. Entra apps 'CRL' + 'OCSP-Relay' (DELETE /applications, SP cascades)
      2. Entra connector group 'CertChecks' (connector moved back to 'Default' first)
      3. the Entra private-network connector on this box (MSI uninstall; skip with -KeepConnector)
      4. the IIS /CertEnroll/ virtual directory (role + MIME left; skip with -KeepIIS)
      5. the msappproxy CDP/AIA/OCSP entries in the CA's CRLPublicationURLs / CACertPublicationURLs
         (surgical - only the *.msappproxy.net lines are removed), then certutil -crl + restart CertSvc
      6. CA_AppProxyCrlFqdn / CA_AppProxyOcspFqdn / CA_MsAppProxyTenant in CAAnswers.json (blanked)
 
    App Proxy stays *enabled* in the tenant (a one-way first-connector side effect) - that does not
    affect a re-run.
 
.PARAMETER CrlAppName / OcspAppName / ConnectorGroupName
    Override the names to look for (defaults match CA-Manager's).
 
.PARAMETER KeepConnector
    Leave the connector installed + registered (re-run then just skips 7c). Recommended for a quick
    re-test - registration is the one genuinely manual step.
 
.PARAMETER KeepIIS
    Leave the /CertEnroll/ virtual directory in place.
 
.PARAMETER CAAnswersPath
    Path to CAAnswers.json to blank the resolved FQDNs in. Default: next to this script.
 
.PARAMETER WhatIf
    Print what would happen, change nothing.
#>

[CmdletBinding()]
param(
    [string]$CrlAppName          = 'CRL',
    [string]$OcspAppName         = 'OCSP-Relay',
    [string]$ConnectorGroupName  = 'CertChecks',
    [switch]$KeepConnector,
    [switch]$KeepIIS,
    [string]$CAAnswersPath,
    [switch]$WhatIf
)

$ErrorActionPreference = 'Stop'

# $PSScriptRoot is empty when this script is dot-sourced; fall back to the invocation path, then CWD.
$BaseDir = if ($PSScriptRoot) { $PSScriptRoot }
           elseif ($MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path }
           else { (Get-Location).Path }
if (-not $CAAnswersPath) { $CAAnswersPath = Join-Path $BaseDir 'CAAnswers.json' }

. (Join-Path $BaseDir '..\Private\CACore.ps1')   # NSP.PKI: was Modules\
. (Join-Path $BaseDir '..\Private\CAGraph.ps1')   # NSP.PKI: was Modules\
. (Join-Path $BaseDir '..\Private\CAAppProxy.ps1')   # NSP.PKI: was Modules\
Repair-CAModulePath   # the connector installer/uninstaller can drop the CurrentUser module dir

function Confirm-Step {
    param([string]$Prompt)
    if ($WhatIf) { Write-Host " [WHATIF] $Prompt" -ForegroundColor Cyan; return $false }
    $r = Read-Host " $Prompt [y/N]"
    return ($r -match '^(?i)y')
}

Write-Host ""
Write-Host "==== CA-Manager App Proxy teardown ====" -ForegroundColor Cyan
if ($WhatIf) { Write-Host " (WhatIf - nothing will change)" -ForegroundColor Cyan }

# ---------------------------------------------------------------------------
# 1 + 2. Entra: apps, then connector group
# ---------------------------------------------------------------------------
$Beta = 'https://graph.microsoft.com/beta'
Write-Host "`n-- Entra --" -ForegroundColor White
$ctx = Connect-CAGraph
if (-not $ctx) { Write-Host " Graph sign-in failed - skipping the Entra half." -ForegroundColor Yellow }
else {
    # An App Proxy "app" is an application + a servicePrincipal. DELETE /applications does NOT delete
    # the SP, and a lingering tagged SP still counts as "assigned" to the connector group
    # (ApplicationsOrConnectorsAssigned_BadRequest) even after /applications and /members read empty.
    # So: delete the SP, delete the application, then purge BOTH from the 30-day recycle bin.
    $names = @($CrlAppName, $OcspAppName)
    $sps = @((Invoke-CAGraph -Method GET -Uri "$Beta/servicePrincipals?`$select=id,appId,displayName,tags&`$top=999").body.value) |
        Where-Object { $_.tags -contains 'WindowsAzureActiveDirectoryOnPremApp' -and $_.displayName -in $names }
    foreach ($sp in $sps) {
        Write-Host " App Proxy SP '$($sp.displayName)' sp=$($sp.id) appId=$($sp.appId)" -ForegroundColor Gray
        if (Confirm-Step "DELETE servicePrincipal + application for '$($sp.displayName)'?") {
            $r1 = Invoke-CAGraph -Method DELETE -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($sp.id)"
            Write-Host $(if ($r1.ok) { " SP deleted." } else { " SP -> HTTP $($r1.status)" }) -ForegroundColor $(if ($r1.ok) { 'Green' } else { 'Yellow' })
            $appObj = (Invoke-CAGraph -Method GET -Uri "https://graph.microsoft.com/v1.0/applications?`$filter=appId eq '$($sp.appId)'&`$select=id").body.value | Select-Object -First 1
            if ($appObj) {
                $r2 = Invoke-CAGraph -Method DELETE -Uri "https://graph.microsoft.com/v1.0/applications/$($appObj.id)"
                Write-Host $(if ($r2.ok) { " app deleted." } else { " app -> HTTP $($r2.status)" }) -ForegroundColor $(if ($r2.ok) { 'Green' } else { 'Yellow' })
            }
        }
    }

    # purge the recycle bin - both apps AND servicePrincipals
    foreach ($t in @('microsoft.graph.application', 'microsoft.graph.servicePrincipal')) {
        $deleted = @((Invoke-CAGraph -Method GET -Uri "https://graph.microsoft.com/v1.0/directory/deletedItems/$t").body.value) |
            Where-Object { $_.displayName -in $names }
        foreach ($d in $deleted) {
            if (Confirm-Step "PERMANENTLY delete recycled $($t.Split('.')[-1]) '$($d.displayName)' ($($d.id))?") {
                $r = Invoke-CAGraph -Method DELETE -Uri "https://graph.microsoft.com/v1.0/directory/deletedItems/$($d.id)"
                Write-Host $(if ($r.ok) { " purged." } else { " -> HTTP $($r.status)" }) -ForegroundColor $(if ($r.ok) { 'Green' } else { 'Yellow' })
            }
        }
    }

    $grp = @(Get-CAGraphAll "$Beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups" -Quiet) |
        Where-Object { $_.name -eq $ConnectorGroupName -and $_.connectorGroupType -eq 'applicationProxy' } | Select-Object -First 1
    if (-not $grp) { Write-Host " connector group '$ConnectorGroupName' not found - already gone." -ForegroundColor Green }
    else {
        Write-Host " connector group '$ConnectorGroupName' id=$($grp.id)" -ForegroundColor Gray
        # move any members back to Default so the group can be deleted
        $default = @(Get-CAGraphAll "$Beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups" -Quiet) |
            Where-Object { $_.isDefault } | Select-Object -First 1
        $members = @(Get-CAGraphAll "$Beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups/$($grp.id)/members" -Quiet)
        if ($members -and $default -and (Confirm-Step "Move $($members.Count) connector(s) from '$ConnectorGroupName' back to 'Default'?")) {
            foreach ($m in $members) {
                $r = Invoke-CAGraph -Method POST -Uri "$Beta/onPremisesPublishingProfiles/applicationProxy/connectors/$($m.id)/memberOf/`$ref" `
                    -Body @{ '@odata.id' = "$Beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups/$($default.id)" }
                if (-not $r.ok) { $r = Invoke-CAGraph -Method PUT -Uri "$Beta/onPremisesPublishingProfiles/applicationProxy/connectors/$($m.id)/memberOf/`$ref" -Body @{ '@odata.id' = "$Beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups/$($default.id)" } }
                Write-Host $(if ($r.ok) { " moved $($m.machineName)" } else { " move $($m.machineName) -> HTTP $($r.status)" }) -ForegroundColor $(if ($r.ok) { 'Green' } else { 'Yellow' })
            }
        }
        $assigned = @((Invoke-CAGraph -Method GET -Uri "$Beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups/$($grp.id)/applications?`$select=id,displayName").body.value)
        if ($assigned) {
            Write-Host " still assigned to '$ConnectorGroupName': $((($assigned | ForEach-Object { $_.displayName }) -join ', '))" -ForegroundColor Yellow
            Write-Host " (delete + purge those, or move them to 'Default', before the group will delete)" -ForegroundColor DarkGray
        }
        if (Confirm-Step "DELETE connector group '$ConnectorGroupName'?") {
            $r = $null
            for ($i = 0; $i -lt 4; $i++) {
                $r = Invoke-CAGraph -Method DELETE -Uri "$Beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups/$($grp.id)"
                if ($r.ok -or $WhatIf) { break }
                Write-Host " HTTP $($r.status) - the member move may still be propagating; retrying in 5s..." -ForegroundColor DarkYellow
                Start-Sleep -Seconds 5
            }
            Write-Host $(if ($r.ok) { " deleted." } else { " -> HTTP $($r.status) (still has a member / is the default group - uninstall the connector, then re-run this script)" }) -ForegroundColor $(if ($r.ok) { 'Green' } else { 'Red' })
        }
    }

    if (-not $KeepConnector) {
        $me = try { [System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName } catch { $env:COMPUTERNAME }
        $conn = Get-CAAppProxyConnector -MachineName $me
        if (-not $conn) { Write-Host " no tenant connector object for this box - already gone." -ForegroundColor Green }
        elseif (Confirm-Step "DELETE the tenant connector object for '$($conn.machineName)' (works only once the MSI is uninstalled)?") {
            $r = Invoke-CAGraph -Method DELETE -Uri "$Beta/onPremisesPublishingProfiles/applicationProxy/connectors/$($conn.id)"
            Write-Host $(if ($r.ok) { " deleted." } else { " -> HTTP $($r.status) (still active - uninstall the connector below, reboot, then re-run this script)" }) -ForegroundColor $(if ($r.ok) { 'Green' } else { 'Yellow' })
        }
    }
    try { Disconnect-MgGraph 3>$null | Out-Null } catch { }
}

# ---------------------------------------------------------------------------
# 3. connector MSI uninstall
# ---------------------------------------------------------------------------
if (-not $KeepConnector) {
    Write-Host "`n-- connector --" -ForegroundColor White
    $svc = Get-Service -Name WAPCSvc -ErrorAction SilentlyContinue
    if (-not $svc) { Write-Host " WAPCSvc not present - connector already uninstalled." -ForegroundColor Green }
    else {
        $ProgressPreference = 'SilentlyContinue'
        $entries = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
                                    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue |
            Where-Object { $_.DisplayName -match '(?i)(Entra private network connector|Application Proxy Connector)' } |
            Sort-Object DisplayName, PSChildName -Unique
        if (-not $entries) { Write-Host " No connector uninstall entry found - remove it via Settings > Apps by hand." -ForegroundColor Yellow }
        foreach ($e in $entries) {
            Write-Host " $($e.DisplayName) $($e.DisplayVersion)" -ForegroundColor Gray
            if (Confirm-Step "Uninstall '$($e.DisplayName)'?") {
                if ($e.PSChildName -match '^\{[0-9A-Fa-f-]+\}$') {
                    Start-Process msiexec.exe -ArgumentList "/x $($e.PSChildName) /qn /norestart" -Wait
                } elseif ($e.UninstallString) {
                    $u = $e.UninstallString
                    if ($u -match 'msiexec') { $u = ($u -replace '(?i)/I', '/X') + ' /qn /norestart'; Start-Process cmd.exe -ArgumentList "/c $u" -Wait }
                    else { Start-Process cmd.exe -ArgumentList "/c `"$u`" /quiet" -Wait }
                }
                Write-Host " done (a reboot may be pending)." -ForegroundColor Green
            }
        }
        # NOTE: after this, re-run the Entra half (or this script again) to delete the now-inactive
        # tenant connector object.
    }
}

# ---------------------------------------------------------------------------
# 4. IIS /CertEnroll/ virtual directory
# ---------------------------------------------------------------------------
if (-not $KeepIIS) {
    Write-Host "`n-- IIS --" -ForegroundColor White
    $appcmd = Join-Path $env:windir 'system32\inetsrv\appcmd.exe'
    $removed = $false
    try {
        Import-Module WebAdministration -ErrorAction Stop
        $vd = Get-WebVirtualDirectory -Site 'Default Web Site' -Name 'CertEnroll' -ErrorAction SilentlyContinue
        if (-not $vd) { Write-Host " /CertEnroll/ vdir not present - already gone." -ForegroundColor Green; $removed = $true }
        elseif (Confirm-Step "Remove the IIS /CertEnroll/ virtual directory (Web-Server role + MIME maps kept)?") {
            Remove-WebVirtualDirectory -Site 'Default Web Site' -Name 'CertEnroll'
            Write-Host " removed." -ForegroundColor Green; $removed = $true
        } else { $removed = $true }   # user declined
    } catch {
        Write-Host " WebAdministration didn't load - trying appcmd.exe..." -ForegroundColor DarkYellow
    }
    if (-not $removed -and (Test-Path $appcmd)) {
        if (Confirm-Step "Remove /CertEnroll/ via appcmd.exe?") {
            & $appcmd delete vdir "Default Web Site/CertEnroll" 2>&1 | Out-String | Write-Host
        }
    } elseif (-not $removed) {
        Write-Host " Remove it by hand: appcmd delete vdir `"Default Web Site/CertEnroll`" (or IIS Manager)." -ForegroundColor Yellow
    }
}

# ---------------------------------------------------------------------------
# 5. CA URL routing - strip only the msappproxy entries
# ---------------------------------------------------------------------------
Write-Host "`n-- CA URL routing (menu 5 undo) --" -ForegroundColor White
function Get-RegMultiSz {
    param([string]$Key)
    $out = & certutil.exe -getreg $Key 2>&1 | Out-String
    $entries = @()
    foreach ($line in ($out -split "`r?`n")) {
        if ($line -match '^\s*\d+:\s*(\d+:\S.*?)\s*$') { $entries += $Matches[1] }
    }
    return $entries
}
foreach ($key in 'CA\CRLPublicationURLs', 'CA\CACertPublicationURLs') {
    $cur  = Get-RegMultiSz -Key $key
    $keep = $cur | Where-Object { $_ -notmatch '(?i)msappproxy\.net' }
    $drop = $cur | Where-Object { $_ -match  '(?i)msappproxy\.net' }
    if (-not $drop) { Write-Host " $key : no msappproxy entries." -ForegroundColor Green; continue }
    Write-Host " $key - would remove:" -ForegroundColor Gray
    $drop | ForEach-Object { Write-Host " $_" -ForegroundColor DarkYellow }
    if (Confirm-Step "Rewrite $key without the msappproxy entries?") {
        & certutil.exe -setreg $key ($keep -join "\n") | Out-String | Write-Host
    }
}
$certsvc = Get-Service -Name CertSvc -ErrorAction SilentlyContinue
if ($certsvc -and (Confirm-Step "Restart CertSvc + republish the CRL?")) {
    Restart-Service -Name CertSvc -Force -ErrorAction SilentlyContinue
    for ($i = 0; $i -lt 10; $i++) {
        $certsvc = Get-Service -Name CertSvc -ErrorAction SilentlyContinue
        if ($certsvc.Status -eq 'Running') { break }
        Start-Sleep -Seconds 3
    }
    if ($certsvc.Status -eq 'Running') {
        $out = & certutil.exe -crl 2>&1 | Out-String
        Write-Host $out
        if ($LASTEXITCODE -ne 0) { Write-Host " certutil -crl failed - run it by hand once the CA is settled (registry changes ARE applied)." -ForegroundColor Yellow }
    } else {
        Write-Host " CertSvc did not come back Running (reboot pending?). Registry changes are applied - start CertSvc and run 'certutil -crl' after the reboot." -ForegroundColor Yellow
    }
}

# ---------------------------------------------------------------------------
# 6. CAAnswers.json - blank the resolved FQDNs
# ---------------------------------------------------------------------------
Write-Host "`n-- CAAnswers.json --" -ForegroundColor White
if (-not (Test-Path $CAAnswersPath)) { Write-Host " $CAAnswersPath not found - skip." -ForegroundColor Yellow }
else {
    $ans = Get-Content $CAAnswersPath -Raw | ConvertFrom-Json
    $changed = $false
    foreach ($f in 'CA_AppProxyCrlFqdn', 'CA_AppProxyOcspFqdn', 'CA_MsAppProxyTenant') {
        if ($ans.PSObject.Properties[$f] -and -not [string]::IsNullOrWhiteSpace($ans.$f)) {
            Write-Host " $f = '$($ans.$f)'" -ForegroundColor Gray; $changed = $true
        }
    }
    if (-not $changed) { Write-Host " nothing to blank." -ForegroundColor Green }
    elseif (Confirm-Step "Blank those fields in $CAAnswersPath?") {
        foreach ($f in 'CA_AppProxyCrlFqdn', 'CA_AppProxyOcspFqdn', 'CA_MsAppProxyTenant') {
            if ($ans.PSObject.Properties[$f]) { $ans.$f = '' }
        }
        $json = ($ans | ConvertTo-Json -Depth 6) -replace "`r`n", "`n" -replace "`n", "`r`n"
        Set-Content -Path $CAAnswersPath -Value $json -Encoding UTF8
        Write-Host " blanked." -ForegroundColor Green
    }
}

Write-Host "`n==== teardown pass complete ====" -ForegroundColor Cyan
Write-Host "If you uninstalled the connector, re-run this script (Entra half) once WAPCSvc is gone to"  -ForegroundColor Gray
Write-Host "delete the now-inactive tenant connector object, then re-run CA-Manager menu 7 from clean." -ForegroundColor Gray