Public/Test-IncidentCapsuleReadiness.ps1

function Test-IncidentCapsuleReadiness {
    <#
    .SYNOPSIS
    Checks whether the local host is ready to create an Incident Capsule.

    .DESCRIPTION
    Resolves the complete effective configuration and performs local, non-collecting
    checks for platform support, output-path writability, free space, elevation,
    collector commands, CIM, and selected event channels. No capsule is created.

    .PARAMETER OutputPath
    Proposed parent directory for the capsule output.

    .PARAMETER Profile
    Built-in Minimal, Standard, or Extended profile.

    .PARAMETER ConfigurationPath
    Optional PowerShell data file whose values override profile defaults.

    .PARAMETER Collectors
    Optional explicit collector list. Replaces the profile collector list.

    .PARAMETER ExcludeCollector
    Collector names removed after all other configuration is resolved.

    .PARAMETER NoCompression
    Calculates required free space without ZIP archive headroom.

    .EXAMPLE
    Test-IncidentCapsuleReadiness -OutputPath 'E:\Evidence' -Profile Standard

    .OUTPUTS
    IncidentCapsule.Readiness
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidAssignmentToAutomaticVariable',
        '',
        Justification = 'Profile is part of the established public cmdlet vocabulary.'
    )]
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$OutputPath = (Get-Location).Path,

        [ValidateSet('Minimal', 'Standard', 'Extended')]
        [string]$Profile = 'Standard',

        [string]$ConfigurationPath,

        [ValidateSet(
            'System', 'Storage', 'Processes', 'Services', 'Network', 'Sessions',
            'LocalAccounts', 'ScheduledTasks', 'Persistence', 'Defender', 'PowerShell',
            'SecurityConfiguration', 'Hotfixes', 'Drivers', 'EventLogs'
        )]
        [string[]]$Collectors,

        [ValidateSet(
            'System', 'Storage', 'Processes', 'Services', 'Network', 'Sessions',
            'LocalAccounts', 'ScheduledTasks', 'Persistence', 'Defender', 'PowerShell',
            'SecurityConfiguration', 'Hotfixes', 'Drivers', 'EventLogs'
        )]
        [string[]]$ExcludeCollector,

        [switch]$NoCompression
    )

    $configurationParameters = @{
        Profile = $Profile
    }
    if ($PSBoundParameters.ContainsKey('ConfigurationPath')) {
        $configurationParameters.ConfigurationPath = $ConfigurationPath
    }
    if ($PSBoundParameters.ContainsKey('Collectors')) {
        $configurationParameters.Collectors = $Collectors
    }
    if ($PSBoundParameters.ContainsKey('ExcludeCollector')) {
        $configurationParameters.ExcludeCollector = $ExcludeCollector
    }

    $configuration = Resolve-ICConfiguration @configurationParameters
    $checks = New-Object System.Collections.ArrayList
    $resolvedOutputPath = $null
    $probeDirectory = $null

    $windowsHost = Test-ICWindows
    if ($windowsHost) {
        [void]$checks.Add((New-ICReadinessCheck -Code 'WINDOWS_SUPPORTED' -Category Platform -Status Passed -Severity Information -Message 'The local host is Windows.' -Details $null))
    }
    else {
        [void]$checks.Add((New-ICReadinessCheck -Code 'WINDOWS_REQUIRED' -Category Platform -Status Failed -Severity Error -Message 'Incident Capsule collection is supported only on Windows.' -Details $null))
    }

    if ([string]::IsNullOrWhiteSpace($OutputPath)) {
        [void]$checks.Add((New-ICReadinessCheck -Code 'OUTPUT_PATH_INVALID' -Category Output -Status Failed -Severity Error -Message 'OutputPath cannot be empty.' -Details $null))
    }
    else {
        try {
            $resolvedOutputPath = [System.IO.Path]::GetFullPath($OutputPath)
        }
        catch {
            [void]$checks.Add((New-ICReadinessCheck -Code 'OUTPUT_PATH_INVALID' -Category Output -Status Failed -Severity Error -Message 'OutputPath cannot be resolved to an absolute filesystem path.' -Details ([pscustomobject][ordered]@{
                Error = $_.Exception.Message
            })))
        }
    }

    if ($null -ne $resolvedOutputPath) {
        if (Test-Path -LiteralPath $resolvedOutputPath) {
            if (Test-Path -LiteralPath $resolvedOutputPath -PathType Container) {
                $probeDirectory = $resolvedOutputPath
            }
            else {
                [void]$checks.Add((New-ICReadinessCheck -Code 'OUTPUT_PATH_NOT_DIRECTORY' -Category Output -Status Failed -Severity Error -Message 'OutputPath exists but is not a directory.' -Details ([pscustomobject][ordered]@{
                    OutputPath = $resolvedOutputPath
                })))
            }
        }
        else {
            $probeResult = Get-ICOutputProbeDirectory -Path $resolvedOutputPath
            if ($null -ne $probeResult.BlockingPath) {
                [void]$checks.Add((New-ICReadinessCheck -Code 'OUTPUT_PATH_PARENT_NOT_DIRECTORY' -Category Output -Status Failed -Severity Error -Message 'An existing OutputPath parent is not a directory.' -Details ([pscustomobject][ordered]@{
                    OutputPath = $resolvedOutputPath
                    BlockingPath = $probeResult.BlockingPath
                })))
            }
            elseif ($null -eq $probeResult.Directory) {
                [void]$checks.Add((New-ICReadinessCheck -Code 'OUTPUT_PATH_PARENT_UNAVAILABLE' -Category Output -Status Failed -Severity Error -Message 'No existing parent directory could be found for OutputPath.' -Details ([pscustomobject][ordered]@{
                    OutputPath = $resolvedOutputPath
                })))
            }
            else {
                $probeDirectory = $probeResult.Directory
            }
        }
    }

    if ($null -ne $probeDirectory) {
        $writability = Test-ICOutputWritable -Path $probeDirectory
        if ($writability.IsWritable) {
            $outputCode = 'OUTPUT_PATH_WRITABLE'
            $outputMessage = 'OutputPath exists and is writable.'
            if (-not (Test-Path -LiteralPath $resolvedOutputPath)) {
                $outputCode = 'OUTPUT_PATH_CREATABLE'
                $outputMessage = 'OutputPath does not exist yet, and its nearest existing parent is writable.'
            }
            [void]$checks.Add((New-ICReadinessCheck -Code $outputCode -Category Output -Status Passed -Severity Information -Message $outputMessage -Details ([pscustomobject][ordered]@{
                OutputPath = $resolvedOutputPath
                ProbeDirectory = $probeDirectory
            })))
        }
        else {
            [void]$checks.Add((New-ICReadinessCheck -Code 'OUTPUT_PATH_NOT_WRITABLE' -Category Output -Status Failed -Severity Error -Message 'OutputPath cannot be written by the current identity.' -Details ([pscustomobject][ordered]@{
                OutputPath = $resolvedOutputPath
                ProbeDirectory = $probeDirectory
                Error = $writability.Error
            })))
        }

        $availableBytes = Get-ICAvailableStorageByte -Path $probeDirectory
        $archiveHeadroomBytes = 0L
        if (-not $NoCompression) {
            $archiveHeadroomBytes = [int64]$configuration.MaximumCapsuleBytes
        }
        $requiredBytes = [int64]$configuration.MaximumCapsuleBytes + $archiveHeadroomBytes
        $spaceDetails = [pscustomobject][ordered]@{
            AvailableBytes       = if ($null -ne $availableBytes) { [int64]$availableBytes } else { $null }
            RequiredBytes        = $requiredBytes
            CapsuleBudgetBytes   = [int64]$configuration.MaximumCapsuleBytes
            ArchiveHeadroomBytes = $archiveHeadroomBytes
            CompressionEnabled   = -not [bool]$NoCompression
        }

        if ($null -eq $availableBytes) {
            [void]$checks.Add((New-ICReadinessCheck -Code 'DISK_SPACE_UNKNOWN' -Category Storage -Status Warning -Severity Warning -Message 'Available free space could not be determined.' -Details $spaceDetails))
        }
        elseif ([int64]$availableBytes -lt $requiredBytes) {
            [void]$checks.Add((New-ICReadinessCheck -Code 'INSUFFICIENT_SPACE' -Category Storage -Status Failed -Severity Error -Message 'Available free space is below the configured capsule budget and archive headroom.' -Details $spaceDetails))
        }
        else {
            [void]$checks.Add((New-ICReadinessCheck -Code 'DISK_SPACE_SUFFICIENT' -Category Storage -Status Passed -Severity Information -Message 'Available free space covers the configured capsule budget and archive headroom.' -Details $spaceDetails))
        }
    }

    $isElevated = $false
    if ($windowsHost) {
        $isElevated = Test-ICAdministrator
        if ($isElevated) {
            [void]$checks.Add((New-ICReadinessCheck -Code 'ELEVATION_PRESENT' -Category Privileges -Status Passed -Severity Information -Message 'The current Windows identity is elevated.' -Details $null))
        }
        else {
            [void]$checks.Add((New-ICReadinessCheck -Code 'NOT_ELEVATED' -Category Privileges -Status Warning -Severity Warning -Message 'The current Windows identity is not elevated; protected sources can be partial.' -Details $null))
        }

        foreach ($check in @(Get-ICCommandReadinessCheck -Configuration $configuration)) {
            [void]$checks.Add($check)
        }
        foreach ($check in @(Get-ICCimReadinessCheck -Configuration $configuration)) {
            [void]$checks.Add($check)
        }
        foreach ($check in @(Get-ICEventLogReadinessCheck -Configuration $configuration)) {
            [void]$checks.Add($check)
        }
    }

    $failedCount = @($checks | Where-Object { $_.Status -eq 'Failed' }).Count
    $warningCount = @($checks | Where-Object { $_.Status -eq 'Warning' }).Count
    $passedCount = @($checks | Where-Object { $_.Status -eq 'Passed' }).Count
    $status = 'Ready'
    if ($failedCount -gt 0) {
        $status = 'Blocked'
    }
    elseif ($warningCount -gt 0) {
        $status = 'ReadyWithWarnings'
    }

    return [pscustomobject][ordered]@{
        PSTypeName         = 'IncidentCapsule.Readiness'
        Status             = $status
        IsReady            = $status -ne 'Blocked'
        CheckedAtUtc       = [datetime]::UtcNow
        HostName           = Get-ICHostName
        IsWindows          = [bool]$windowsHost
        IsElevated         = [bool]$isElevated
        Profile            = $Profile
        OutputPath         = $resolvedOutputPath
        CompressionEnabled = -not [bool]$NoCompression
        Configuration      = Copy-ICValue -Value $configuration
        PrivacyScope       = Get-ICPrivacyScope -Configuration $configuration
        ResourceLimits     = Get-ICResourceLimit -Configuration $configuration
        Checks             = @($checks)
        Summary            = [pscustomobject][ordered]@{
            Passed  = $passedCount
            Warnings = $warningCount
            Failed  = $failedCount
        }
    }
}