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: $_"
            }
        }
    }
}