Public/Get-EFFleetSummary.ps1

function Get-EFFleetSummary {
    <#
    .SYNOPSIS
    Checks several Windows computers without changing them.
 
    .DESCRIPTION
    Runs the same EndpointForge computer checkup on each named computer and returns one
    combined result. This command never installs EndpointForge, turns on remote
    management, changes a Windows setting, or runs a fix. TcpPort, DnsResolution,
    HttpEndpointHealth, WindowsUpdateAvailable, and LocalGroupMembership can create
    observable activity from every remote computer and are blocked unless
    AllowNetworkChecks is supplied.
 
    Each remote computer must already allow PowerShell remoting and must already have
    EndpointForge 0.6.0 or later installed. Your account must have permission to connect.
    These requirements are intentionally not changed for you.
 
    EndpointForge calls its list of expected settings and everyday checks a baseline in
    scripts. In the menu and documentation, that is described simply as a checklist.
    Matching log lines, event messages, and event data are not included in fleet results.
 
    .PARAMETER ComputerName
    One or more computer names to check. Duplicate names are checked once.
 
    .PARAMETER Baseline
    The checklist to use. Supply the built-in checklist name, a checklist JSON file, or a
    checklist object. The default is EnterpriseRecommended.
 
    .PARAMETER Credential
    An optional account that already has permission to connect. The credential is used
    only for the remote connection and is not included in the returned report.
 
    .PARAMETER ThrottleLimit
    The largest number of remote checks that may run at the same time.
 
    .PARAMETER IncludeSoftware
    Also collects installed-software details. This makes the report larger and can take
    longer. It still does not change a computer.
 
    .PARAMETER AllowNetworkChecks
    Allows the five network-active types after every destination, requested account
    identity, purpose, update-scan option, and source computer has been reviewed. Each
    remote computer performs its own activity. Contacted services, identity providers,
    firewalls, proxies, and monitoring tools may record it. This acknowledgement does not
    grant network authorization.
 
    .PARAMETER MinimumFreeSpacePercent
    The system-drive free-space level that should produce a warning.
 
    .PARAMETER MaximumUptimeDays
    The number of days running without a restart that should produce a warning.
 
    .EXAMPLE
    Get-EFFleetSummary -ComputerName PC-101,PC-102
 
    Checks two computers with the built-in checklist and returns one combined result.
 
    .EXAMPLE
    Get-EFFleetSummary -ComputerName (Get-Content .\computers.txt) -Credential (Get-Credential)
 
    Checks names from a text file using an account that already has remote access.
 
    .EXAMPLE
    Get-EFFleetSummary -ComputerName PC-101,PC-102 -Baseline .\Contoso.EverydayChecks.json -AllowNetworkChecks
 
    Explicitly allows the reviewed network-active checks from each remote computer.
 
    .OUTPUTS
    EndpointForge.FleetSummary
 
    .LINK
    Get-EFEndpointSummary
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter(Position = 1)]
        [AllowNull()]
        [object]$Baseline = 'EnterpriseRecommended',

        [pscredential]$Credential,

        [ValidateRange(1, 128)]
        [int]$ThrottleLimit = 16,

        [switch]$IncludeSoftware,

        [switch]$AllowNetworkChecks,

        [ValidateRange(1, 99)]
        [int]$MinimumFreeSpacePercent = 15,

        [ValidateRange(1, 3650)]
        [int]$MaximumUptimeDays = 30
    )

    $targets = [Collections.Generic.List[string]]::new()
    $seenTargets = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    foreach ($name in $ComputerName) {
        $trimmedName = [string]$name
        if ($null -ne $trimmedName) { $trimmedName = $trimmedName.Trim() }
        if ([string]::IsNullOrWhiteSpace($trimmedName)) {
            throw [System.ArgumentException]::new('ComputerName cannot contain an empty value.')
        }
        if ($seenTargets.Add($trimmedName)) {
            $targets.Add($trimmedName)
        }
    }
    if ($targets.Count -eq 0) {
        throw [System.ArgumentException]::new('Provide at least one computer name.')
    }

    $resolvedBaseline = Resolve-EFBaseline -Baseline $Baseline
    $networkControls = @($resolvedBaseline.Controls | Where-Object { Test-EFControlUsesNetwork -Control $_ })
    if ($networkControls.Count -gt 0 -and -not $AllowNetworkChecks) {
        $networkTypes = @($networkControls.Type | Sort-Object -Unique) -join ', '
        throw [System.InvalidOperationException]::new(
            "This checklist contains $($networkControls.Count) network-active check(s): $networkTypes. " +
            "Across $($targets.Count) computer(s), that activity would originate from every remote computer and may be recorded. " +
            'Review the checklist, then add -AllowNetworkChecks to permit them.'
        )
    }
    $startedAtUtc = [DateTime]::UtcNow
    $remoteScript = {
        param($Checklist, $CollectSoftware, $FreeSpaceThreshold, $UptimeThreshold, $PermitNetworkChecks)

        Import-Module EndpointForge -MinimumVersion 0.6.0 -Force -ErrorAction Stop
        $parameters = @{
            Baseline                = $Checklist
            IncludeSoftware         = [bool]$CollectSoftware
            MinimumFreeSpacePercent = [int]$FreeSpaceThreshold
            MaximumUptimeDays       = [int]$UptimeThreshold
            NoProgress              = $true
            AllowNetworkChecks      = [bool]$PermitNetworkChecks
        }
        $checkup = Get-EFEndpointSummary @parameters
        [pscustomobject]@{
            RemoteComputerName = $env:COMPUTERNAME
            Checkup            = $checkup
        }
    }

    $invokeParameters = @{
        ComputerName  = $targets.ToArray()
        ScriptBlock   = $remoteScript
        ArgumentList  = @($resolvedBaseline, [bool]$IncludeSoftware, $MinimumFreeSpacePercent, $MaximumUptimeDays, [bool]$AllowNetworkChecks)
        ThrottleLimit = $ThrottleLimit
        ErrorAction   = 'SilentlyContinue'
        ErrorVariable = 'fleetRemoteErrors'
    }
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $invokeParameters.Credential = $Credential
    }

    $fleetRemoteErrors = @()
    $responses = @()
    try {
        $responses = @(Invoke-Command @invokeParameters)
    }
    catch {
        $fleetRemoteErrors += $_
    }

    $resultList = [Collections.Generic.List[object]]::new()
    $completedTargets = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    foreach ($response in $responses) {
        if ($null -eq $response) { continue }
        $remoteTarget = [string](Get-EFPropertyValue -InputObject $response -Name 'PSComputerName' -Default '')
        if ([string]::IsNullOrWhiteSpace($remoteTarget)) {
            $remoteTarget = [string](Get-EFPropertyValue -InputObject $response -Name 'RemoteComputerName' -Default '')
        }
        $checkup = Get-EFPropertyValue -InputObject $response -Name 'Checkup'
        if ($null -eq $checkup) { continue }

        $requestedTarget = @($targets | Where-Object { $_ -ieq $remoteTarget } | Select-Object -First 1)
        if ($requestedTarget.Count -eq 0) { $requestedTarget = @($remoteTarget) }
        $requestedName = [string]$requestedTarget[0]
        if (-not [string]::IsNullOrWhiteSpace($requestedName)) {
            $null = $completedTargets.Add($requestedName)
        }
        $resultList.Add([pscustomobject]@{
            PSTypeName            = 'EndpointForge.FleetComputerResult'
            RequestedComputerName = $requestedName
            ComputerName          = [string](Get-EFPropertyValue -InputObject $checkup -Name 'ComputerName' -Default $remoteTarget)
            OverallStatus         = [string](Get-EFPropertyValue -InputObject $checkup -Name 'OverallStatus' -Default 'Incomplete')
            Score                 = Get-EFPropertyValue -InputObject $checkup -Name 'Score'
            IssueCount            = [int](Get-EFPropertyValue -InputObject $checkup -Name 'IssueCount' -Default 0)
            UnknownCount          = [int](Get-EFPropertyValue -InputObject $checkup -Name 'UnknownCount' -Default 0)
            CompletedAtUtc        = Get-EFPropertyValue -InputObject $checkup -Name 'CompletedAtUtc'
            NextStep              = [string](Get-EFPropertyValue -InputObject $checkup -Name 'NextStep' -Default '')
            Checkup               = $checkup
        })
    }

    $failureList = [Collections.Generic.List[object]]::new()
    foreach ($target in $targets) {
        if ($completedTargets.Contains($target)) { continue }

        $matchingErrors = @($fleetRemoteErrors | Where-Object {
            $errorTarget = ''
            if ($null -ne $_.OriginInfo) { $errorTarget = [string]$_.OriginInfo.PSComputerName }
            if ([string]::IsNullOrWhiteSpace($errorTarget)) { $errorTarget = [string]$_.TargetObject }
            $errorTarget -ieq $target -or [string]$_.Exception.Message -match [regex]::Escape($target)
        })
        $message = if ($matchingErrors.Count -gt 0) {
            [string]$matchingErrors[0].Exception.Message
        }
        else {
                'The computer did not return a checkup. Confirm its name, network access, PowerShell remoting permission, and that EndpointForge 0.6.0 or later is already installed there.'
        }
        $failureList.Add([pscustomobject]@{
            PSTypeName   = 'EndpointForge.FleetFailure'
            ComputerName = [string]$target
            Message      = $message
        })
    }

    $healthyCount = @($resultList | Where-Object OverallStatus -eq 'Healthy').Count
    $warningCount = @($resultList | Where-Object OverallStatus -eq 'Warning').Count
    $criticalCount = @($resultList | Where-Object OverallStatus -eq 'Critical').Count
    $incompleteCount = @($resultList | Where-Object OverallStatus -eq 'Incomplete').Count
    $failedCount = $failureList.Count
    $exitCode = if ($failedCount -gt 0 -or $incompleteCount -gt 0) {
        3
    }
    elseif ($criticalCount -gt 0) {
        2
    }
    elseif ($warningCount -gt 0) {
        1
    }
    else {
        0
    }
    $summary = if ($failedCount -gt 0) {
        "$($resultList.Count) of $($targets.Count) computer(s) were checked; $failedCount could not be checked."
    }
    elseif ($criticalCount -gt 0) {
        "All $($targets.Count) computer(s) were checked; $criticalCount need urgent attention."
    }
    elseif ($warningCount -gt 0 -or $incompleteCount -gt 0) {
        "All $($targets.Count) computer(s) were checked; some results need attention or were incomplete."
    }
    else {
        "All $($targets.Count) computer(s) were checked and look good."
    }
    $nextStep = if ($failedCount -gt 0) {
        'Review Failures. EndpointForge will not turn on remote access or install itself on another computer.'
    }
    elseif ($criticalCount -gt 0 -or $warningCount -gt 0) {
        'Review Results, then check an affected computer directly before approving any fixes.'
    }
    elseif ($incompleteCount -gt 0) {
        'Review incomplete results and permissions, then run the check again.'
    }
    else {
        'No action is required.'
    }

    [pscustomobject]@{
        PSTypeName       = 'EndpointForge.FleetSummary'
        SchemaVersion    = '1.0'
        ChecklistName    = [string]$resolvedBaseline.Name
        BaselineName     = [string]$resolvedBaseline.Name
        BaselineVersion  = [string]$resolvedBaseline.Version
        NetworkCheckCount = $networkControls.Count
        NetworkChecksAllowed = [bool]$AllowNetworkChecks
        StartedAtUtc     = $startedAtUtc
        CompletedAtUtc   = [DateTime]::UtcNow
        TargetCount      = $targets.Count
        SucceededCount   = $resultList.Count
        FailedCount      = $failedCount
        HealthyCount     = $healthyCount
        WarningCount     = $warningCount
        CriticalCount    = $criticalCount
        IncompleteCount  = $incompleteCount
        IsComplete       = $failedCount -eq 0 -and $incompleteCount -eq 0
        ExitCode          = $exitCode
        Summary           = $summary
        NextStep          = $nextStep
        Results           = $resultList.ToArray()
        Failures          = $failureList.ToArray()
    }
}