Scripts/Get-CAManagerInventory.ps1
|
<#
.SYNOPSIS Read-only inventory of an existing AD CS deployment - captures every value CA-Manager.ps1 will need to reproduce (or diff against) a hand-built CA/OCSP/App-Proxy/auto-enroll setup. Nothing is created, changed, published, or revoked - safe to run on a production CA. .DESCRIPTION Run this ON the issuing CA (elevated - it self-elevates). If the deployment is two-tier (an online root that issued the issuing CA's own cert - the opt-in "dedicated sub-CA" model), run it a SECOND time on the root and send both files: the root's CRL/AIA config is part of what has to be reachable through the App Proxy. Dumps, in order, to a timestamped .txt under -OutputDir: A. Host / role / CA identity - OS, domain, ADCS role features, certutil -cainfo, certutil -getreg CA (full), CA cert(s) + expiry, and whether this CA is a root or a subordinate (+ its parent). B. AIA / CDP / OCSP publication - Get-CAAuthorityInformationAccess / Get-CACrlDistributionPoint (falls back to certutil -getreg), CRL/delta/overlap periods, the CRL files currently in CertEnroll\ + the newest one's ThisUpdate/NextUpdate/NextCRLPublish, and URL-flag decoding. C. OCSP / Online Responder - role feature, OCSPSvc state, the full HKLM\...\Services\OCSPSvc\Responder registry subtree (revocation configs, signing cert, CRL URLs), OCSP signing certs in LocalMachine\My. D. Certificate templates - certutil -CATemplates (published list), and for the custom template(s) - full AD definition (name flags, enrollment flags, EKU, key usage, validity/overlap, schema version, RA signatures) + the template object's ACL with Enroll / AutoEnroll ACEs called out by name (this is where the umbrella group shows up). E. Auto-enrollment GPO - Default Domain Policy's Public Key Policies section, a scan of every other GPO for AutoEnrollment / AEPolicy, and the live AEPolicy / OfflineExpirationPercent registry values under both HKLM and HKCU policy hives. F. CRL share - if -CrlSharePath is given (or a UNC / file:// entry is found in the CA's CRLPublicationURLs): Get-SmbShare / share ACL / NTFS ACL / the .crl files sitting there + timestamps. G. App Proxy connector - WAPCSvc / WAPCUpdaterSvc state + installed connector version. (The Entra published-app config - App-CRL, App-OCSP: their external/internal URLs and pre-auth mode - is NOT readable locally; pull it from the Entra portal or Graph by hand.) H. Umbrella / master AD group - -MasterGroupName if given, else a wildcard search for *Master* / *IKEv2* groups; direct + recursive membership and any nested groups. Send the .txt file back (and the root's copy, for a two-tier deployment). .PARAMETER OutputDir Where the timestamped inventory .txt is written. Default C:\Admin\CAInventory. .PARAMETER TemplateName Display name(s) or internal name(s) of the custom cert template(s) to deep-dive in section D. Accepts partial / wildcard matches. If omitted, section D deep-dives every template on this CA whose EKU includes Client Authentication (that will include the built-ins - noisy but complete). .PARAMETER CrlSharePath UNC path of the CRL distribution share, if one exists. If omitted, section F still runs against any UNC / file:// entry discovered in the CA's own CRLPublicationURLs. .PARAMETER MasterGroupName Name of the auto-enrollment umbrella group (e.g. "IKEv2_MasterGroup"). If omitted, section H does a wildcard AD search instead. .NOTES Needs, where present: the ADCSAdministration, ActiveDirectory and GroupPolicy modules (all standard on a domain CA). Every block is wrapped - a missing module or cmdlet degrades that block to a note, it does not stop the run. #> [CmdletBinding()] param( [string]$OutputDir = "C:\Admin\CAInventory", [string[]]$TemplateName, [string]$CrlSharePath, [string]$MasterGroupName ) # --- self-elevate (certutil -getreg CA, the OCSPSvc registry subtree, and some template ACLs all # want admin), passing every bound param through so the elevated instance doesn't fall back to # defaults - same pattern as NPS-Manager.ps1 / FortiClient_Kickstart.ps1. --- if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"") if ($PSBoundParameters.ContainsKey('OutputDir')) { $relaunchArgs += @('-OutputDir', "`"$OutputDir`"") } if ($PSBoundParameters.ContainsKey('TemplateName')) { $relaunchArgs += @('-TemplateName', ($TemplateName -join ',')) } if ($PSBoundParameters.ContainsKey('CrlSharePath')) { $relaunchArgs += @('-CrlSharePath', "`"$CrlSharePath`"") } if ($PSBoundParameters.ContainsKey('MasterGroupName')){ $relaunchArgs += @('-MasterGroupName', "`"$MasterGroupName`"") } Start-Process powershell.exe -ArgumentList $relaunchArgs -Verb RunAs exit } $ErrorActionPreference = 'Continue' if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' $hostTag = $env:COMPUTERNAME $outFile = Join-Path $OutputDir "CAInventory_${hostTag}_${stamp}.txt" try { Start-Transcript -Path $outFile -Force | Out-Null } catch { } function Section { param([string]$Text) Write-Host "" Write-Host ("=" * 78) -ForegroundColor Cyan Write-Host " $Text" -ForegroundColor Cyan Write-Host ("=" * 78) -ForegroundColor Cyan } function Sub { param([string]$Text) Write-Host "" Write-Host "--- $Text ---" -ForegroundColor Gray } function Note { param([string]$Text) Write-Host " [note] $Text" -ForegroundColor Yellow } function Try-Block { param([string]$Label, [scriptblock]$Body) Sub $Label try { & $Body } catch { Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red } } # FILETIME-style pKIExpirationPeriod / pKIOverlapPeriod: 8-byte LE, negative 100ns intervals. function ConvertFrom-PkiPeriod { param($ByteArray) if (-not $ByteArray) { return "(none)" } try { $ticks = [BitConverter]::ToInt64([byte[]]$ByteArray, 0) # negative $span = [TimeSpan]::FromSeconds([math]::Abs($ticks) / 1e7) $days = [math]::Round($span.TotalDays, 2) $yrs = [math]::Round($span.TotalDays / 365.25, 3) return "$days days (~$yrs years) [raw ticks $ticks]" } catch { return "(unparseable: $($_.Exception.Message))" } } Write-Host "CA Manager - deployment inventory" -ForegroundColor Green Write-Host "Host : $hostTag" Write-Host "When : $(Get-Date)" Write-Host "Run by : $env:USERDOMAIN\$env:USERNAME" Write-Host "Output : $outFile" Note "Read-only. Nothing is created, changed, published, or revoked by this script." # Module availability (informational - blocks below each guard themselves too) Sub "PowerShell modules present" foreach ($m in 'ADCSAdministration','ActiveDirectory','GroupPolicy','PKI') { $have = [bool](Get-Module -ListAvailable -Name $m -ErrorAction SilentlyContinue) Write-Host (" {0,-22} {1}" -f $m, $(if ($have) { 'yes' } else { 'MISSING' })) } foreach ($m in 'ADCSAdministration','ActiveDirectory','GroupPolicy') { try { Import-Module $m -ErrorAction Stop } catch { } } # ===================================================================================== Section "A. Host / role / CA identity" # ===================================================================================== Try-Block "OS / domain" { Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, OSArchitecture, CSName | Format-List | Out-String | Write-Host Get-CimInstance Win32_ComputerSystem | Select-Object Domain, DomainRole, PartOfDomain | Format-List | Out-String | Write-Host } Try-Block "ADCS role features installed" { if (Get-Command Get-WindowsFeature -ErrorAction SilentlyContinue) { Get-WindowsFeature -Name ADCS-* | Where-Object Installed | Select-Object Name, DisplayName, InstallState | Format-Table -AutoSize | Out-String | Write-Host } else { Note "Get-WindowsFeature not available (not a Windows Server SKU?)." } } Try-Block "certutil -cainfo" { (& certutil -cainfo 2>&1 | Out-String).Trim() | Write-Host } Try-Block "certutil -getreg CA (full CA registry dump)" { (& certutil -getreg CA 2>&1 | Out-String).Trim() | Write-Host } Try-Block "CA certificate(s) in LocalMachine\CA and LocalMachine\My (subject / issuer / validity)" { foreach ($store in 'Cert:\LocalMachine\CA', 'Cert:\LocalMachine\My') { Write-Host " store: $store" Get-ChildItem $store -ErrorAction SilentlyContinue | Select-Object Subject, Issuer, NotBefore, NotAfter, @{n='DaysLeft';e={[math]::Round(($_.NotAfter - (Get-Date)).TotalDays)}}, @{n='IsCA';e={ ($_.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.19' } | ForEach-Object { $_.CertificateAuthority }) -contains $true }}, @{n='SelfSigned';e={$_.Subject -eq $_.Issuer}}, Thumbprint | Format-List | Out-String | Write-Host } Note "SelfSigned=True on the CA cert => this is a ROOT. SelfSigned=False => SUBORDINATE; 'Issuer' above is the parent CA - run this script there too." } # ===================================================================================== Section "B. AIA / CDP / OCSP publication + CRL timing" # ===================================================================================== Try-Block "Get-CAAuthorityInformationAccess" { if (Get-Command Get-CAAuthorityInformationAccess -ErrorAction SilentlyContinue) { Get-CAAuthorityInformationAccess | Format-List * | Out-String | Write-Host } else { Note "cmdlet not present - see certutil CACertPublicationURLs below." } } Try-Block "Get-CACrlDistributionPoint" { if (Get-Command Get-CACrlDistributionPoint -ErrorAction SilentlyContinue) { Get-CACrlDistributionPoint | Format-List * | Out-String | Write-Host } else { Note "cmdlet not present - see certutil CRLPublicationURLs below." } } Try-Block "certutil -getreg CA\CACertPublicationURLs" { (& certutil -getreg CA\CACertPublicationURLs 2>&1 | Out-String).Trim() | Write-Host } Try-Block "certutil -getreg CA\CRLPublicationURLs" { (& certutil -getreg CA\CRLPublicationURLs 2>&1 | Out-String).Trim() | Write-Host } Sub "URL-flag quick reference (the number: prefix on each URL above)" @' 1 = Publish to this location (CDP: publish CRLs / AIA: publish CA cert) 2 = Include in the CDP extension of issued certs 4 = Include in CRLs (to find delta CRL locations) 8 = Include in the CDP extension of issued CRLs 32 = Publish delta CRLs to this location 64 = Include in the IDP extension of issued CRLs 128 = (AIA) Include in the OCSP extension (AuthorityInfoAccess = OCSP) e.g. "65" = 1 + 64, "3" = 1 + 2, "6" = 2 + 4, "134" = 2 + 4 + 128 '@ | Write-Host Try-Block "CRL period / delta / overlap" { foreach ($k in 'CRLPeriod','CRLPeriodUnits','CRLDeltaPeriod','CRLDeltaPeriodUnits','CRLOverlapPeriod','CRLOverlapUnits','CRLNextPublish','ValidityPeriod','ValidityPeriodUnits') { Write-Host (" {0,-22} {1}" -f $k, ((& certutil -getreg "CA\$k" 2>&1 | Select-String -Pattern '=\s*(.+)$' | ForEach-Object { $_.Matches[0].Groups[1].Value }) -join ' ')) } } Try-Block "Published CRL files in CertEnroll\ + newest CRL dump" { $certEnroll = Join-Path $env:SystemRoot 'System32\CertSrv\CertEnroll' if (Test-Path $certEnroll) { $crls = Get-ChildItem $certEnroll -Filter *.crl -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending $crls | Select-Object Name, Length, LastWriteTime | Format-Table -AutoSize | Out-String | Write-Host Get-ChildItem $certEnroll -Filter *.crt -ErrorAction SilentlyContinue | Select-Object Name, Length, LastWriteTime | Format-Table -AutoSize | Out-String | Write-Host if ($crls) { Sub "certutil -dump `"$($crls[0].FullName)`" (ThisUpdate / NextUpdate / NextCRLPublish)" (& certutil -dump $crls[0].FullName 2>&1 | Out-String).Trim() | Write-Host } } else { Note "$certEnroll not found." } } # ===================================================================================== Section "C. OCSP / Online Responder" # ===================================================================================== Try-Block "Online Responder role feature + service state" { if (Get-Command Get-WindowsFeature -ErrorAction SilentlyContinue) { Get-WindowsFeature -Name ADCS-Online-Cert | Select-Object Name, DisplayName, InstallState | Format-List | Out-String | Write-Host } Get-Service -Name OCSPSvc, CertSvc -ErrorAction SilentlyContinue | Select-Object Name, Status, StartType, DisplayName | Format-Table -AutoSize | Out-String | Write-Host } Try-Block "HKLM\SYSTEM\CurrentControlSet\Services\OCSPSvc\Responder (full subtree - revocation configs)" { $ocspKey = 'HKLM:\SYSTEM\CurrentControlSet\Services\OCSPSvc\Responder' if (Test-Path $ocspKey) { Get-ChildItem -Path $ocspKey -Recurse -ErrorAction SilentlyContinue | ForEach-Object { Write-Host "" Write-Host " [key] $($_.PSPath -replace '^.*::','')" $props = Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue if ($props) { $props.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS(Path|ParentPath|ChildName|Provider|Drive)$' } | ForEach-Object { $v = $_.Value if ($v -is [byte[]]) { $v = "<byte[$($v.Length)]> " + ([BitConverter]::ToString($v)) } Write-Host (" {0,-28} {1}" -f $_.Name, $v) } } } # also dump the Responder key's own values (not just children) $root = Get-ItemProperty -Path $ocspKey -ErrorAction SilentlyContinue if ($root) { Write-Host "" Write-Host " [key] $ocspKey (root values)" $root.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS(Path|ParentPath|ChildName|Provider|Drive)$' } | ForEach-Object { Write-Host (" {0,-28} {1}" -f $_.Name, $_.Value) } } } else { Note "$ocspKey not present - Online Responder role not installed on this box (may be on a separate server)." } } Try-Block "OCSP signing certificate(s) in LocalMachine\My (EKU 1.3.6.1.5.5.7.3.9)" { $all = @(Get-ChildItem Cert:\LocalMachine\My -ErrorAction SilentlyContinue) $ocsp = $all | Where-Object { $_.EnhancedKeyUsageList.ObjectId -contains '1.3.6.1.5.5.7.3.9' -or (($_.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.37' } | ForEach-Object { $_.Format($false) }) -match 'OCSP') } if ($ocsp) { $ocsp | Select-Object Subject, Issuer, NotBefore, NotAfter, @{n='DaysLeft';e={[math]::Round(($_.NotAfter - (Get-Date)).TotalDays)}}, Thumbprint | Format-List | Out-String | Write-Host } else { Note "Nothing in LocalMachine\My matched the OCSP-Signing EKU. Full LocalMachine\My inventory (eyeball for a short-lived cert issued by one of the CAs, EKU 'OCSP Signing'):" $all | Select-Object Subject, Issuer, NotAfter, @{n='EKU';e={ ($_.EnhancedKeyUsageList | ForEach-Object { $_.FriendlyName }) -join ', ' }}, Thumbprint | Format-Table -AutoSize | Out-String | Write-Host Note "OCSP signing certs may instead live in a per-service store or be auto-rotated - the OCSPSvc\Responder\*\SigningCertificate blobs above are the authoritative copy." } } # ===================================================================================== Section "D. Certificate templates" # ===================================================================================== Try-Block "certutil -CATemplates (templates published on THIS CA)" { (& certutil -CATemplates 2>&1 | Out-String).Trim() | Write-Host } Try-Block "Get-CATemplate" { if (Get-Command Get-CATemplate -ErrorAction SilentlyContinue) { Get-CATemplate | Format-Table -AutoSize | Out-String | Write-Host } else { Note "Get-CATemplate not present." } } Try-Block "Custom template definition(s) from AD + Enroll / AutoEnroll ACL" { $configNC = $null try { $configNC = (Get-ADRootDSE -ErrorAction Stop).configurationNamingContext } catch { } if (-not $configNC) { Note "Could not read configurationNamingContext (ActiveDirectory module / DC reachability). Skipping AD template dump."; return } $tmplBase = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC" $allTmpl = Get-ADObject -SearchBase $tmplBase -LDAPFilter '(objectClass=pKICertificateTemplate)' -Properties * -ErrorAction Stop $enrollGuid = [guid]'0e10c968-78fb-11d2-90d4-00c04f79dc55' # Enroll extended right $autoEnrollGuid = [guid]'a05b8cc2-17bc-4802-a710-e7c15ab866a2' # AutoEnroll extended right # Which templates to deep-dive if ($TemplateName) { $picked = foreach ($pat in $TemplateName) { $allTmpl | Where-Object { $_.displayName -like "*$pat*" -or $_.Name -like "*$pat*" -or $_.'msPKI-Cert-Template-OID' -eq $pat } } $picked = $picked | Sort-Object DistinguishedName -Unique } else { Note "No -TemplateName given - deep-diving every template whose EKU includes Client Authentication (1.3.6.1.5.5.7.3.2). Expect built-ins in the list." $picked = $allTmpl | Where-Object { $_.pKIExtendedKeyUsage -contains '1.3.6.1.5.5.7.3.2' } } if (-not $picked) { Note "No templates matched."; return } foreach ($t in $picked) { Write-Host "" Write-Host (" ============ {0} (cn={1}) ============" -f $t.displayName, $t.Name) -ForegroundColor White [pscustomobject]@{ displayName = $t.displayName name = $t.Name 'msPKI-Template-Schema-Version' = $t.'msPKI-Template-Schema-Version' revision = $t.revision 'msPKI-Template-Minor-Revision' = $t.'msPKI-Template-Minor-Revision' 'msPKI-Cert-Template-OID' = $t.'msPKI-Cert-Template-OID' 'msPKI-Minimal-Key-Size' = $t.'msPKI-Minimal-Key-Size' 'msPKI-RA-Signature' = $t.'msPKI-RA-Signature' 'pKIDefaultKeySpec' = $t.pKIDefaultKeySpec EKU = ($t.pKIExtendedKeyUsage -join ', ') 'pKIKeyUsage(hex)' = $(if ($t.pKIKeyUsage) { '0x' + ([BitConverter]::ToString([byte[]]$t.pKIKeyUsage) -replace '-','') } else { $null }) 'Certificate-Name-Flag(hex)' = ('0x{0:X8}' -f ([int]$t.'msPKI-Certificate-Name-Flag')) 'Enrollment-Flag(hex)' = ('0x{0:X8}' -f ([int]$t.'msPKI-Enrollment-Flag')) 'Private-Key-Flag(hex)' = ('0x{0:X8}' -f ([int]$t.'msPKI-Private-Key-Flag')) 'msPKI-Certificate-Application-Policy' = ($t.'msPKI-Certificate-Application-Policy' -join ', ') ExpirationPeriod = ConvertFrom-PkiPeriod $t.pKIExpirationPeriod OverlapPeriod = ConvertFrom-PkiPeriod $t.pKIOverlapPeriod } | Format-List | Out-String | Write-Host Sub "ACL for $($t.Name) (Enroll / AutoEnroll ACEs highlighted)" try { $acl = Get-Acl -Path ("AD:\" + $t.DistinguishedName) -ErrorAction Stop foreach ($ace in $acl.Access) { $rightNote = '' if ($ace.ActiveDirectoryRights -match 'ExtendedRight') { if ($ace.ObjectType -eq $enrollGuid) { $rightNote = ' <== ENROLL' } elseif ($ace.ObjectType -eq $autoEnrollGuid) { $rightNote = ' <== AUTOENROLL' } elseif ($ace.ObjectType -eq [guid]::Empty) { $rightNote = ' <== (all extended rights)' } } Write-Host (" {0,-45} {1,-22} {2}{3}" -f $ace.IdentityReference, $ace.ActiveDirectoryRights, $ace.AccessControlType, $rightNote) } } catch { Write-Host " ERROR reading ACL: $($_.Exception.Message)" -ForegroundColor Red } } } # ===================================================================================== Section "E. Auto-enrollment GPO + live policy registry values" # ===================================================================================== Try-Block "Default Domain Policy - Public Key Policies section of the GPO report" { if (-not (Get-Command Get-GPOReport -ErrorAction SilentlyContinue)) { Note "GroupPolicy module not available."; return } $xml = Get-GPOReport -Name 'Default Domain Policy' -ReportType Xml -ErrorAction Stop $m = [regex]::Matches($xml, '(?s)<PublicKeyPolicies>.*?</PublicKeyPolicies>') if ($m.Count) { $m | ForEach-Object { Write-Host $_.Value } } else { Note "No <PublicKeyPolicies> block in Default Domain Policy. Scanning it for AutoEnrollment / AEPolicy anyway:" [regex]::Matches($xml, '(?s).{120}(AutoEnrollment|AEPolicy|Cryptography).{240}') | ForEach-Object { Write-Host "..."; Write-Host $_.Value; Write-Host "..." } } } Try-Block "Every GPO - which ones mention AutoEnrollment / AEPolicy" { if (-not (Get-Command Get-GPO -ErrorAction SilentlyContinue)) { return } foreach ($g in (Get-GPO -All | Sort-Object DisplayName)) { try { $rx = Get-GPOReport -Guid $g.Id -ReportType Xml -ErrorAction Stop if ($rx -match 'AutoEnrollment|AEPolicy|PublicKeyPolicies|Certificate Services Client') { Write-Host (" * {0} {{{1}}}" -f $g.DisplayName, $g.Id) } } catch { } } Note "For each GPO flagged above, CA-Manager will want its full <PublicKeyPolicies> block - re-run Get-GPOReport -Name '<that GPO>' -ReportType Xml if it isn't Default Domain Policy (already dumped above)." } Try-Block "Live policy registry values (this machine / current user)" { $paths = @( 'HKLM:\Software\Policies\Microsoft\Cryptography\AutoEnrollment', 'HKCU:\Software\Policies\Microsoft\Cryptography\AutoEnrollment', 'HKLM:\Software\Policies\Microsoft\Cryptography\PolicyServers', 'HKCU:\Software\Policies\Microsoft\Cryptography\PolicyServers' ) foreach ($p in $paths) { Write-Host " $p" if (Test-Path $p) { Get-ItemProperty -Path $p -ErrorAction SilentlyContinue | Select-Object * -ExcludeProperty PSPath,PSParentPath,PSChildName,PSProvider,PSDrive | Format-List | Out-String | Write-Host Get-ChildItem -Path $p -Recurse -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " [subkey] $($_.PSChildName)" Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue | Select-Object * -ExcludeProperty PSPath,PSParentPath,PSChildName,PSProvider,PSDrive | Format-List | Out-String | Write-Host } } else { Write-Host " (absent)" } } Note "AEPolicy bitmask: 0x1 enrolled + 0x2 renew/update/remove-revoked + 0x4 update-templates => 0x7 (7) = all three boxes. OfflineExpirationPercent = 10 matches 'log expiry events at 10%'." } # ===================================================================================== Section "F. CRL distribution share" # ===================================================================================== Try-Block "Resolve the share path" { $paths = @() if ($CrlSharePath) { $paths += $CrlSharePath } # discover UNC / file:// publication targets from the CA registry $crlUrls = (& certutil -getreg CA\CRLPublicationURLs 2>&1 | Out-String) foreach ($line in ($crlUrls -split "`r?`n")) { if ($line -match '(file://|\\\\)[^\r\n"]+') { $u = $Matches[0].Trim().TrimEnd('\') $u = $u -replace '^file://','' $u = $u -replace '%SystemRoot%', $env:SystemRoot -replace '%windir%', $env:windir # strip the %3/%8-style certutil filename tokens down to a directory $u = $u -replace '\\[^\\]*%\d.*$','' if ($u -match '^\\\\' -and $paths -notcontains $u) { $paths += $u } } } if (-not $paths) { Note "No -CrlSharePath given and no UNC/file:// entry in CRLPublicationURLs. Section F skipped."; return } foreach ($sp in ($paths | Sort-Object -Unique)) { Write-Host "" Write-Host " path: $sp" -ForegroundColor White Sub "reachable?" Write-Host " $([bool](Test-Path -LiteralPath $sp))" Sub "SMB share (if this box hosts it)" try { $leaf = ($sp -replace '^\\\\[^\\]+\\','').TrimEnd('\') # UNC -> share leaf; local path stays as-is $allShares = @(Get-SmbShare -ErrorAction SilentlyContinue) $match = $allShares | Where-Object { $_.Path -and ( ($_.Path.TrimEnd('\') -ieq $sp.TrimEnd('\')) -or # share backs this exact local path ($leaf -ieq $_.Name) -or # UNC leaf == share name ($sp -ilike ("*\" + $_.Name)) ) } if ($match) { $match | ForEach-Object { $_ | Select-Object Name, Path, Description, ScopeName | Format-List | Out-String | Write-Host Sub "share-level ACL (Get-SmbShareAccess $($_.Name))" Get-SmbShareAccess -Name $_.Name -ErrorAction SilentlyContinue | Select-Object Name, AccountName, AccessControlType, AccessRight | Format-Table -AutoSize | Out-String | Write-Host } } else { Write-Host " No share matched '$sp' by name or backing path. All shares on this box:" $allShares | Select-Object Name, Path, Description | Format-Table -AutoSize | Out-String | Write-Host } } catch { Write-Host " (Get-SmbShare unavailable: $($_.Exception.Message))" } Sub "NTFS ACL" try { (Get-Acl -LiteralPath $sp).Access | Select-Object IdentityReference, FileSystemRights, AccessControlType, IsInherited | Format-Table -AutoSize | Out-String | Write-Host } catch { Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red } Sub ".crl / .crt files present" try { Get-ChildItem -LiteralPath $sp -Include *.crl,*.crt -Recurse -ErrorAction SilentlyContinue | Select-Object FullName, Length, LastWriteTime | Format-Table -AutoSize | Out-String | Write-Host } catch { } } } # ===================================================================================== Section "G. Azure App Proxy connector (local view only)" # ===================================================================================== Try-Block "connector services + installed version" { Get-Service -Name 'WAPCSvc','WAPCUpdaterSvc' -ErrorAction SilentlyContinue | Select-Object Name, Status, StartType, DisplayName | Format-Table -AutoSize | Out-String | Write-Host $keys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' Get-ItemProperty $keys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'Application Proxy|App Proxy' } | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Format-List | Out-String | Write-Host } Note "The Entra published-app side (App-CRL: external+internal URL, pre-auth mode; App-OCSP: same) is NOT readable from this box." Note "Pull it by hand: Entra portal > Enterprise apps > (each app) > Application proxy; or Graph: GET /applications + onPremisesPublishing." # ===================================================================================== Section "H. Auto-enrollment umbrella / master AD group" # ===================================================================================== Try-Block "locate the umbrella group + its membership" { if (-not (Get-Command Get-ADGroup -ErrorAction SilentlyContinue)) { Note "ActiveDirectory module unavailable."; return } $groups = @() if ($MasterGroupName) { $groups = @(Get-ADGroup -Filter "Name -eq '$MasterGroupName'" -Properties Description, member, memberOf -ErrorAction SilentlyContinue) if (-not $groups) { Note "No group named exactly '$MasterGroupName' - falling back to wildcard search." } } if (-not $groups) { $groups = @(Get-ADGroup -LDAPFilter '(|(name=*Master*)(name=*IKEv2*)(name=*VPN*Enroll*)(name=*AutoEnroll*))' -Properties Description, member, memberOf -ErrorAction SilentlyContinue) Note "Wildcard match (*Master* / *IKEv2* / *VPN*Enroll* / *AutoEnroll*) - the real umbrella is whichever one is used in the section D template ACLs." } if (-not $groups) { Note "No candidate groups found."; return } foreach ($g in $groups) { Write-Host "" Write-Host " ===== $($g.Name) =====" -ForegroundColor White Write-Host " DN : $($g.DistinguishedName)" Write-Host " Description : $($g.Description)" Write-Host " memberOf : $($g.memberOf -join '; ')" Sub "direct members" Get-ADGroupMember -Identity $g -ErrorAction SilentlyContinue | Select-Object objectClass, name, distinguishedName | Format-Table -AutoSize | Out-String | Write-Host Sub "nested group members only (these are the 'primary' VPN user groups nested into the umbrella)" Get-ADGroupMember -Identity $g -ErrorAction SilentlyContinue | Where-Object objectClass -eq 'group' | Select-Object name, distinguishedName | Format-Table -AutoSize | Out-String | Write-Host Sub "recursive member count" try { Write-Host " $((Get-ADGroupMember -Identity $g -Recursive -ErrorAction SilentlyContinue | Measure-Object).Count) principals (recursive)" } catch { } } } Section "DONE" Write-Host "Inventory written to:" -ForegroundColor Green Write-Host " $outFile" -ForegroundColor Green Write-Host "" Write-Host "Send that file back. For a two-tier (dedicated sub-CA) deployment, run this on the ROOT too." -ForegroundColor Cyan try { Stop-Transcript | Out-Null } catch { } Write-Host "" Write-Host "(press Enter to close)" try { Read-Host | Out-Null } catch { } |