Public/Invoke-CertWatchtowerScan.ps1
|
function Invoke-CertWatchtowerScan { <# .SYNOPSIS Scan orchestrator: collects snapshots from one or many servers - standalone via -ComputerName (default: local machine) or multi-tenant via JSON config files. .DESCRIPTION Standalone mode (default) needs no configuration at all: Invoke-CertWatchtowerScan -IncludeCrl -IncludeCA | New-CwReport -OutFile .\cert-watchtower.html scans the local machine; -ComputerName dc01,dc02 scans remote servers over the existing Kerberos/WinRM context (Invoke-Command). The needed cert-watchtower functions are injected into the remote session as text; nothing is installed on the target. Multi-tenant mode reads one JSON file per tenant (glob supported): { "tenant": "Contoso AG", "servers": ["dc01.contoso.local", "srv-web01.contoso.local"], "warnDays": 30, "critDays": 7, "endpoints": ["ldaps://dc01.contoso.local:636"] } Configs contain host lists and thresholds - never secrets. With -SnapshotPath every snapshot is additionally persisted as JSON (the canonical artifact for later reports, diffs and baselines); without it the snapshots are only emitted to the pipeline. .EXAMPLE # simplest possible: scan this machine, render a report Invoke-CertWatchtowerScan | New-CwReport -OutFile .\cert-watchtower.html .EXAMPLE Invoke-CertWatchtowerScan -ComputerName dc01,dc02 -IncludeCrl -IncludeCA -SnapshotPath .\snapshots .EXAMPLE Invoke-CertWatchtowerScan -TenantConfig .\tenants\*.json -SnapshotPath .\snapshots -IncludeCrl | New-CwReport -OutFile .\out\fleet.html #> [CmdletBinding(DefaultParameterSetName = 'Servers')] [OutputType([pscustomobject])] param( [Parameter(Mandatory, ParameterSetName = 'Config')] [string]$TenantConfig, [Parameter(ParameterSetName = 'Servers', Position = 0)] [Alias('Server', 'Servers')] [string[]]$ComputerName = @('localhost'), [Parameter(ParameterSetName = 'Servers')] [string]$Tenant, [Parameter(ParameterSetName = 'Servers')] [int]$WarnDays = 30, [Parameter(ParameterSetName = 'Servers')] [int]$CritDays = 7, [Parameter(ParameterSetName = 'Servers')] [string[]]$ProbeEndpoints, [string]$SnapshotPath, [switch]$IncludeCrl, [switch]$IncludeCA, [pscredential]$Credential ) # --- build the flat work list (tenant, server, thresholds, endpoints) ---- $jobs = [System.Collections.Generic.List[object]]::new() if ($PSCmdlet.ParameterSetName -eq 'Config') { $configFiles = @(Get-ChildItem -Path $TenantConfig -File -ErrorAction Stop) if ($configFiles.Count -eq 0) { throw "No tenant config files match '$TenantConfig'." } foreach ($file in $configFiles) { $config = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json $cfgTenant = if ($config.PSObject.Properties['tenant']) { $config.tenant } else { $file.BaseName } $cfgWarn = if ($config.PSObject.Properties['warnDays']) { [int]$config.warnDays } else { 30 } $cfgCrit = if ($config.PSObject.Properties['critDays']) { [int]$config.critDays } else { 7 } $cfgEndpoints = if ($config.PSObject.Properties['endpoints']) { @($config.endpoints) } else { @() } foreach ($server in @($config.servers)) { $jobs.Add([pscustomobject]@{ Tenant = $cfgTenant; Server = $server WarnDays = $cfgWarn; CritDays = $cfgCrit; Endpoints = $cfgEndpoints }) } } } else { foreach ($server in $ComputerName) { $jobs.Add([pscustomobject]@{ Tenant = $Tenant; Server = $server WarnDays = $WarnDays; CritDays = $CritDays; Endpoints = @($ProbeEndpoints | Where-Object { $_ }) }) } } if ($SnapshotPath -and -not (Test-Path -Path $SnapshotPath)) { $null = New-Item -Path $SnapshotPath -ItemType Directory -Force } # function definitions shipped into remote sessions (no install on target) $remoteDefText = $null $localNames = @('localhost', '127.0.0.1', '.', $env:COMPUTERNAME, "$env:COMPUTERNAME.$env:USERDNSDOMAIN".TrimEnd('.')) foreach ($job in $jobs) { $label = if ($job.Tenant) { "[$($job.Tenant)] " } else { '' } Write-Verbose "$($label)collecting snapshot from $($job.Server)" $isLocal = $job.Server -in $localNames try { $snapshot = if ($isLocal) { Get-CwSnapshot -IncludeCrl:$IncludeCrl -IncludeCA:$IncludeCA ` -Tenant $job.Tenant -WarnDays $job.WarnDays -CritDays $job.CritDays } else { if (-not $remoteDefText) { $remoteDefText = (@( 'ConvertTo-CwSeverity', 'Get-CwCertRole', 'ConvertFrom-CwCrl', 'Get-CwCertInventory', 'Test-CwCrlHealth', 'Get-CwCaHealth', 'Test-CwEndpoint', 'Get-CwSnapshot' ) | ForEach-Object { $fn = Get-Command -Name $_ -CommandType Function -ErrorAction Stop "function $($fn.Name) { $($fn.Definition) }" }) -join "`n" } $icmParams = @{ ComputerName = $job.Server ScriptBlock = { param($defs, $params) . ([scriptblock]::Create($defs)) Get-CwSnapshot @params } ArgumentList = @($remoteDefText, @{ IncludeCrl = [bool]$IncludeCrl IncludeCA = [bool]$IncludeCA Tenant = $job.Tenant WarnDays = $job.WarnDays CritDays = $job.CritDays }) ErrorAction = 'Stop' } if ($Credential) { $icmParams.Credential = $Credential } Invoke-Command @icmParams | Select-Object -Property * -ExcludeProperty PSComputerName, RunspaceId, PSShowComputerName } # endpoint probes always run from the scanning host (outside view) if (@($job.Endpoints).Count -gt 0) { $snapshot.Endpoints = @(Test-CwEndpoint -Uri $job.Endpoints -WarnDays $job.WarnDays -CritDays $job.CritDays) } if ($SnapshotPath) { $stamp = (Get-Date).ToString('yyyyMMdd-HHmmss') $safeTenant = if ($job.Tenant) { ($job.Tenant -replace '[^\w\-]', '_') + '_' } else { '' } $safeHost = $job.Server -replace '[^\w\-.]', '_' $outFile = Join-Path $SnapshotPath "$safeTenant$safeHost`_$stamp.json" $snapshot | ConvertTo-Json -Depth 8 | Set-Content -Path $outFile -Encoding utf8 Write-Verbose "snapshot written: $outFile" } $snapshot } catch { Write-Warning "$label$($job.Server) failed: $_" } } } |