Reporting/Export-EscCaData.ps1
|
function Export-EscCaData { <# .SYNOPSIS Exports the CA registry configuration and the issued-certificate list to a timestamped folder (read-only; live/Windows only). .DESCRIPTION Collects two read-only artifacts from a Certification Authority and writes them under <OutputRoot>\CA_Assesment_<yyyyMMdd_HHmmss>\ : 1. The CertSvc Configuration registry subtree (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration, confirmed in Microsoft's "Migrate a Certification Authority" guidance). * Local target -> a real importable '.reg' file via 'reg export'. * Remote target -> a structured '.json' dump via the remote-registry API, plus a 'certutil -config <host\CA> -getreg' text dump. 2. The issued certificates (Disposition = 20) via 'certutil -view ... csv'. 3. Published CRLs (base + delta) into a 'CRLs' sub-folder - copied from the local CertEnroll folder and/or read from AD CDP objects over LDAP. Never publishes a new CRL (no 'certutil -crl'). 4. Optional (-IncludeBackup): a full CA backup into a 'CA_Backup' sub-folder (CAName.p12 + Database\) via Backup-CARoleService (certutil -backup fallback). With -BackupPassword the private key is included; without it, database only. Everything is read-only: reg export, certutil -getreg/-view, the CRL read and the backup only READ CA state; no certificate is issued, no CRL is published, no object is modified, and the CA service is never stopped. All Windows tooling (reg.exe / certutil.exe / ADCSAdministration) is required, so this runs only on a live Windows host. The CA backup additionally must run on the CA host itself (local-only). .PARAMETER Config CA config string 'HostFqdn\CAName'. When given, drives remote certutil calls and the ComputerName / CaName defaults. .PARAMETER ComputerName CA host (defaults to the Config host, else the local machine). .PARAMETER CaName Sanitized CA name = the sub-key under ...\CertSvc\Configuration (defaults to the Config CA, else the local 'Active' value). .PARAMETER OutputRoot Root folder for the export (default: the current directory). A CA_Assesment_<timestamp> sub-folder is created under it. Invoke-ESCAssessment passes its report -OutputPath here so reports and exports stay together. .PARAMETER IncludeIssued Export the issued-certificate list (default: on). Use -IncludeIssued:$false to skip. .PARAMETER MaxRows Cap the issued-certificate rows written (0 = all, the default). .PARAMETER IncludeCrl Export published CRLs (base + delta) into a CRLs sub-folder (default: on). .PARAMETER IncludeBackup Also take a full CA backup (database, plus private key when -BackupPassword is given) into a CA_Backup sub-folder. Local-only; off by default. .PARAMETER BackupPassword SecureString protecting the exported private key (.p12). Omit to back up the database only (no private key). .OUTPUTS [pscustomobject] with FolderPath, Files[], and per-job status. .EXAMPLE Export-EscCaData .EXAMPLE Export-EscCaData -Config 'ca01.corp.local\Corp-Issuing-CA' -OutputRoot .\out -IncludeBackup #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string] $Config, [Parameter(Mandatory = $false)] [string] $ComputerName, [Parameter(Mandatory = $false)] [string] $CaName, [Parameter(Mandatory = $false)] [string] $OutputRoot = '.', [Parameter(Mandatory = $false)] [switch] $IncludeIssued = $true, [Parameter(Mandatory = $false)] [switch] $IncludeCrl = $true, [Parameter(Mandatory = $false)] [int] $MaxRows = 0, [Parameter(Mandatory = $false)] [switch] $IncludeBackup, [Parameter(Mandatory = $false)] [System.Security.SecureString] $BackupPassword ) $regPath = 'SYSTEM\CurrentControlSet\Services\CertSvc\Configuration' $regFull = 'HKEY_LOCAL_MACHINE\{0}' -f $regPath if (-not [string]::IsNullOrWhiteSpace($Config) -and $Config.Contains('\')) { $parts = $Config.Split('\', 2) if ([string]::IsNullOrWhiteSpace($ComputerName)) { $ComputerName = $parts[0] } if ([string]::IsNullOrWhiteSpace($CaName)) { $CaName = $parts[1] } } if ([string]::IsNullOrWhiteSpace($ComputerName)) { $ComputerName = $env:COMPUTERNAME } $localNames = @($env:COMPUTERNAME, 'localhost', '.', '127.0.0.1') try { $localNames += [System.Net.Dns]::GetHostEntry('').HostName } catch { } $short = ($ComputerName -split '\.')[0] $isLocal = [string]::IsNullOrWhiteSpace($ComputerName) -or ($localNames -contains $ComputerName) -or ` ($localNames -contains $short) -or ($short -eq $env:COMPUTERNAME) $ts = (Get-Date).ToString('yyyyMMdd_HHmmss') $folder = Join-Path -Path $OutputRoot -ChildPath ('CA_Assesment_{0}' -f $ts) try { if (-not (Test-Path -LiteralPath $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null } } catch { Write-EscLog -Component 'CaExport' -Level Warning -Message ("Cannot create output folder '{0}': {1}" -f $folder, $_.Exception.Message) return [pscustomobject]@{ FolderPath = $folder; Files = @(); Jobs = @(); Error = $_.Exception.Message } } $files = New-Object System.Collections.Generic.List[string] $jobs = New-Object System.Collections.Generic.List[object] $record = { param($Name, $Path, $Ok, $Detail, $Acl = $null) $jobs.Add([pscustomobject]@{ Job = $Name; Path = $Path; Success = [bool]$Ok; Detail = [string]$Detail; AclHardened = $Acl }) if ($Ok -and $Path -and (Test-Path -LiteralPath $Path)) { $files.Add([string]$Path) } } if ($isLocal) { $regFile = Join-Path $folder ('CertSvc_Configuration_{0}.reg' -f $ts) try { $out = & reg.exe export $regFull $regFile /y 2>&1 $ok = ($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $regFile) & $record 'CertSvc_Configuration (reg export)' $regFile $ok (($out | Out-String).Trim()) } catch { & $record 'CertSvc_Configuration (reg export)' $regFile $false $_.Exception.Message } } else { $jsonFile = Join-Path $folder ('CertSvc_Configuration_{0}.json' -f $ts) try { $dump = ConvertTo-EscRegistryDump -ComputerName $ComputerName -SubKey $regPath ($dump | ConvertTo-Json -Depth 12) | Set-Content -LiteralPath $jsonFile -Encoding UTF8 & $record 'CertSvc_Configuration (remote registry json)' $jsonFile $true '' } catch { & $record 'CertSvc_Configuration (remote registry json)' $jsonFile $false $_.Exception.Message } } $getregFile = Join-Path $folder ('CA_Registry_getreg_{0}.txt' -f $ts) try { if ($isLocal) { $raw = & certutil.exe -getreg 2>&1 } else { $raw = & certutil.exe -config $Config -getreg 2>&1 } ($raw | Out-String) | Set-Content -LiteralPath $getregFile -Encoding UTF8 & $record 'CA_Registry_getreg (certutil)' $getregFile ($LASTEXITCODE -eq 0) '' } catch { & $record 'CA_Registry_getreg (certutil)' $getregFile $false $_.Exception.Message } if ($IncludeIssued) { $csvFile = Join-Path $folder ('Issued_Certificates_{0}.csv' -f $ts) $cols = 'RequestID,Request.RequesterName,CommonName,CertificateTemplate,SerialNumber,NotBefore,NotAfter,CertificateHash' try { $cuArgs = @() if (-not $isLocal -and -not [string]::IsNullOrWhiteSpace($Config)) { $cuArgs += @('-config', $Config) } $cuArgs += @('-view', '-restrict', 'Disposition=20', '-out', $cols, 'csv') $raw = & certutil.exe @cuArgs 2>&1 $text = ($raw | Out-String) if ($MaxRows -gt 0) { $lines = @($text -split "`r?`n") if ($lines.Count -gt ($MaxRows + 1)) { $text = ($lines[0..$MaxRows] -join "`r`n") } } $text | Set-Content -LiteralPath $csvFile -Encoding UTF8 & $record 'Issued_Certificates (certutil -view)' $csvFile ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $csvFile)) '' } catch { & $record 'Issued_Certificates (certutil -view)' $csvFile $false $_.Exception.Message } } if ($IncludeCrl) { $crlDir = Join-Path $folder 'CRLs' try { $crl = Export-EscCrlList -CrlDir $crlDir -IsLocal $isLocal -CaName $CaName & $record 'CRL_Lists' $crlDir ($crl.Count -gt 0) $crl.Detail } catch { & $record 'CRL_Lists' $crlDir $false $_.Exception.Message } } if ($IncludeBackup) { $backupDir = Join-Path $folder 'CA_Backup' if (-not $isLocal) { & $record 'CA_Backup' $null $false 'CA backup is local-only; run this on the CA host.' } else { try { if (-not (Test-Path -LiteralPath $backupDir)) { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null } $hasCmd = $null -ne (Get-Command -Name 'Backup-CARoleService' -ErrorAction SilentlyContinue) if ($hasCmd) { if ($null -ne $BackupPassword) { Backup-CARoleService -Path $backupDir -Password $BackupPassword -ErrorAction Stop $detail = 'Full backup via Backup-CARoleService (database + private key).' } else { Backup-CARoleService -Path $backupDir -DatabaseOnly -ErrorAction Stop $detail = 'Database-only backup (no -BackupPassword supplied; private key NOT included).' } } else { if ($null -ne $BackupPassword) { $plain = (New-Object System.Net.NetworkCredential('', $BackupPassword)).Password $out = & certutil.exe -f -p $plain -backup $backupDir 2>&1 $plain = $null $detail = 'Full backup via certutil (database + private key).' } else { $out = & certutil.exe -f -backupdb $backupDir 2>&1 $detail = 'Database-only backup via certutil (no password; private key NOT included).' } } $hasFiles = @(Get-ChildItem -LiteralPath $backupDir -Recurse -File -ErrorAction SilentlyContinue).Count -gt 0 $aclOk = $false if ($hasFiles) { $aclOk = Protect-EscBackupFolder -Path $backupDir if ($aclOk) { $detail += ' ACL hardened to Administrators/SYSTEM only.' } else { $detail += ' WARNING: ACL hardening failed - secure this folder manually.' } } & $record 'CA_Backup' $backupDir $hasFiles $detail $aclOk } catch { & $record 'CA_Backup' $backupDir $false $_.Exception.Message } } } $fileCount = $files.Count $jobCount = $jobs.Count Write-EscLog -Component 'CaExport' -Message ("CA export -> {0} ({1} file(s), {2} job(s))." -f $folder, $fileCount, $jobCount) return [pscustomobject]@{ FolderPath = $folder ComputerName = $ComputerName CaName = $CaName IsLocal = $isLocal Files = $files.ToArray() Jobs = $jobs.ToArray() } } |