Public/Invoke-CertWatchtowerScan.ps1
|
function Invoke-CertWatchtowerScan { <# .SYNOPSIS Fleet-scan orchestrator: iterates servers/tenants from JSON config, collects snapshots (local or via WinRM) and persists them. .DESCRIPTION Tenant config files (one JSON per tenant, glob supported) look like: { "tenant": "Contoso AG", "servers": ["dc01.contoso.local", "srv-web01.contoso.local"], "warnDays": 30, "critDays": 7, "endpoints": ["ldaps://dc01.contoso.local:636"] } Remote collection runs over the existing Kerberos/WinRM context (Invoke-Command) - no separate credentials, no secrets in the config. The needed cert-watchtower functions are injected into the remote session as text; nothing is installed on the target. Every collected snapshot is written to -SnapshotPath as JSON and emitted to the pipeline (ready to pipe into New-CwReport). .EXAMPLE Invoke-CertWatchtowerScan -TenantConfig .\tenants\*.json -SnapshotPath .\snapshots -IncludeCrl -IncludeCA | New-CwReport -OutFile .\out\fleet.html #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory, Position = 0)] [string]$TenantConfig, [string]$SnapshotPath, [switch]$IncludeCrl, [switch]$IncludeCA, [pscredential]$Credential ) $configFiles = @(Get-ChildItem -Path $TenantConfig -File -ErrorAction Stop) if ($configFiles.Count -eq 0) { throw "No tenant config files match '$TenantConfig'." } if ($SnapshotPath -and -not (Test-Path -Path $SnapshotPath)) { $null = New-Item -Path $SnapshotPath -ItemType Directory -Force } # function definitions shipped into the remote session (no install on target) $remoteDefs = @( '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) }" } $remoteDefText = $remoteDefs -join "`n" foreach ($file in $configFiles) { $config = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json $tenant = if ($config.PSObject.Properties['tenant']) { $config.tenant } else { $file.BaseName } $warnDays = if ($config.PSObject.Properties['warnDays']) { [int]$config.warnDays } else { 30 } $critDays = if ($config.PSObject.Properties['critDays']) { [int]$config.critDays } else { 7 } $endpoints = if ($config.PSObject.Properties['endpoints']) { @($config.endpoints) } else { @() } foreach ($server in @($config.servers)) { Write-Verbose "[$tenant] collecting snapshot from $server" $isLocal = $server -in @('localhost', '127.0.0.1', '.', $env:COMPUTERNAME, "$env:COMPUTERNAME.$env:USERDNSDOMAIN".TrimEnd('.')) try { $snapshot = if ($isLocal) { Get-CwSnapshot -IncludeCrl:$IncludeCrl -IncludeCA:$IncludeCA ` -Tenant $tenant -WarnDays $warnDays -CritDays $critDays } else { $icmParams = @{ ComputerName = $server ScriptBlock = { param($defs, $params) . ([scriptblock]::Create($defs)) Get-CwSnapshot @params } ArgumentList = @($remoteDefText, @{ IncludeCrl = [bool]$IncludeCrl IncludeCA = [bool]$IncludeCA Tenant = $tenant WarnDays = $warnDays CritDays = $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 ($endpoints.Count -gt 0) { $snapshot.Endpoints = @(Test-CwEndpoint -Uri $endpoints -WarnDays $warnDays -CritDays $critDays) } if ($SnapshotPath) { $stamp = (Get-Date).ToString('yyyyMMdd-HHmmss') $safeTenant = $tenant -replace '[^\w\-]', '_' $safeHost = $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 "[$tenant] $server failed: $_" } } } } |