Private/Readiness.ps1

function New-ICReadinessCheck {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Code,

        [Parameter(Mandatory)]
        [ValidateSet('Platform', 'Output', 'Storage', 'Privileges', 'Commands', 'CIM', 'EventLogs')]
        [string]$Category,

        [Parameter(Mandatory)]
        [ValidateSet('Passed', 'Warning', 'Failed')]
        [string]$Status,

        [Parameter(Mandatory)]
        [ValidateSet('Information', 'Warning', 'Error')]
        [string]$Severity,

        [Parameter(Mandatory)]
        [string]$Message,

        [AllowNull()]
        [object]$Details
    )

    return [pscustomobject][ordered]@{
        PSTypeName = 'IncidentCapsule.ReadinessCheck'
        Code       = $Code
        Category   = $Category
        Status     = $Status
        Severity   = $Severity
        Message    = $Message
        Details    = $Details
    }
}

function Get-ICOutputProbeDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $candidate = $Path
    while (-not [string]::IsNullOrWhiteSpace($candidate)) {
        if (Test-Path -LiteralPath $candidate) {
            if (Test-Path -LiteralPath $candidate -PathType Container) {
                return [pscustomobject]@{
                    Directory    = $candidate
                    BlockingPath = $null
                }
            }

            return [pscustomobject]@{
                Directory    = $null
                BlockingPath = $candidate
            }
        }

        $parent = Split-Path -Path $candidate -Parent
        if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $candidate) {
            break
        }
        $candidate = $parent
    }

    return [pscustomobject]@{
        Directory    = $null
        BlockingPath = $null
    }
}

function Test-ICOutputWritable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $probePath = Join-Path $Path ('.incident-capsule-readiness-{0}.tmp' -f [guid]::NewGuid().ToString('N'))
    $stream = $null
    try {
        $stream = [System.IO.File]::Open(
            $probePath,
            [System.IO.FileMode]::CreateNew,
            [System.IO.FileAccess]::Write,
            [System.IO.FileShare]::None
        )
        $stream.WriteByte(0)
        return [pscustomobject]@{
            IsWritable = $true
            Error      = $null
        }
    }
    catch {
        return [pscustomobject]@{
            IsWritable = $false
            Error      = $_.Exception.Message
        }
    }
    finally {
        if ($null -ne $stream) {
            $stream.Dispose()
        }
        if (Test-Path -LiteralPath $probePath -PathType Leaf) {
            Remove-Item -LiteralPath $probePath -Force -ErrorAction SilentlyContinue
        }
    }
}

function Get-ICAvailableStorageByte {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    try {
        $fullPath = [System.IO.Path]::GetFullPath($Path)
        $matchingDrive = Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue |
            Where-Object {
                -not [string]::IsNullOrWhiteSpace([string]$_.Root) -and
                $fullPath.StartsWith([string]$_.Root, [System.StringComparison]::OrdinalIgnoreCase)
            } |
            Sort-Object { ([string]$_.Root).Length } -Descending |
            Select-Object -First 1

        if ($null -ne $matchingDrive -and $null -ne $matchingDrive.Free) {
            return [int64]$matchingDrive.Free
        }

        $pathRoot = [System.IO.Path]::GetPathRoot($fullPath)
        if (-not [string]::IsNullOrWhiteSpace($pathRoot)) {
            $driveInfo = New-Object System.IO.DriveInfo($pathRoot)
            if ($driveInfo.IsReady) {
                return [int64]$driveInfo.AvailableFreeSpace
            }
        }
    }
    catch {
        return $null
    }

    return $null
}

function Get-ICReadinessCommandName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Configuration
    )

    $commandsByCollector = [ordered]@{
        System                = @('whoami.exe', 'w32tm.exe')
        Storage               = @('Get-BitLockerVolume', 'Get-SmbShare')
        Network               = @('Get-NetAdapter', 'Get-NetIPAddress', 'Get-NetRoute', 'Get-NetTCPConnection', 'Get-NetUDPEndpoint', 'Get-NetNeighbor', 'ipconfig.exe', 'route.exe', 'arp.exe', 'netstat.exe')
        Sessions              = @('quser.exe', 'qwinsta.exe')
        LocalAccounts         = @('Get-LocalUser', 'Get-LocalGroup', 'Get-LocalGroupMember')
        ScheduledTasks        = @('Get-ScheduledTask', 'Get-ScheduledTaskInfo')
        Defender              = @('Get-MpComputerStatus')
        SecurityConfiguration = @('Get-NetFirewallProfile', 'Get-NetFirewallRule', 'auditpol.exe', 'netsh.exe', 'secedit.exe')
        Drivers               = @('driverquery.exe')
        EventLogs             = @('Get-WinEvent')
    }

    $commandNames = New-Object System.Collections.ArrayList
    foreach ($collectorName in @($Configuration.Collectors)) {
        if ($commandsByCollector.Contains($collectorName)) {
            foreach ($commandName in @($commandsByCollector[$collectorName])) {
                if ($commandName -notin $commandNames) {
                    [void]$commandNames.Add($commandName)
                }
            }
        }
    }

    if ('EventLogs' -in @($Configuration.Collectors) -and $Configuration.ExportEvtx -and 'wevtutil.exe' -notin $commandNames) {
        [void]$commandNames.Add('wevtutil.exe')
    }

    return @($commandNames)
}

function Get-ICCommandReadinessCheck {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Configuration
    )

    $commandNames = @(Get-ICReadinessCommandName -Configuration $Configuration)
    if ($commandNames.Count -eq 0) {
        return @(New-ICReadinessCheck -Code 'COMMANDS_NOT_REQUIRED' -Category Commands -Status Passed -Severity Information -Message 'The selected collectors have no additional command prerequisites.' -Details $null)
    }

    $missing = @($commandNames | Where-Object { $null -eq (Get-Command -Name $_ -ErrorAction SilentlyContinue) })
    if ($missing.Count -eq 0) {
        return @(New-ICReadinessCheck -Code 'COMMANDS_AVAILABLE' -Category Commands -Status Passed -Severity Information -Message 'All checked collector commands are available.' -Details ([pscustomobject][ordered]@{
            CheckedCommands = @($commandNames)
        }))
    }

    return @(New-ICReadinessCheck -Code 'OPTIONAL_COMMANDS_MISSING' -Category Commands -Status Warning -Severity Warning -Message 'Some collector integrations are unavailable; affected collectors can complete partially.' -Details ([pscustomobject][ordered]@{
        CheckedCommands = @($commandNames)
        MissingCommands = @($missing)
    }))
}

function Get-ICCimReadinessCheck {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Configuration
    )

    $cimCollectors = @(
        'System', 'Storage', 'Processes', 'Services', 'Sessions', 'LocalAccounts',
        'Persistence', 'SecurityConfiguration', 'Hotfixes', 'Drivers'
    )
    $selectedCimCollectors = @($Configuration.Collectors | Where-Object { $_ -in $cimCollectors })
    if ($selectedCimCollectors.Count -eq 0) {
        return @(New-ICReadinessCheck -Code 'CIM_NOT_REQUIRED' -Category CIM -Status Passed -Severity Information -Message 'The selected collectors do not require CIM.' -Details $null)
    }

    if ($null -eq (Get-Command -Name 'Get-CimInstance' -ErrorAction SilentlyContinue)) {
        return @(New-ICReadinessCheck -Code 'CIM_COMMAND_UNAVAILABLE' -Category CIM -Status Warning -Severity Warning -Message 'Get-CimInstance is unavailable; CIM-backed collectors can complete partially.' -Details ([pscustomobject][ordered]@{
            AffectedCollectors = @($selectedCimCollectors)
        }))
    }

    try {
        $sample = Get-CimInstance -ClassName Win32_OperatingSystem -Property Caption -ErrorAction Stop | Select-Object -First 1
        if ($null -eq $sample) {
            return @(New-ICReadinessCheck -Code 'CIM_QUERY_EMPTY' -Category CIM -Status Warning -Severity Warning -Message 'The CIM readiness query returned no operating-system record.' -Details $null)
        }

        return @(New-ICReadinessCheck -Code 'CIM_QUERY_SUCCEEDED' -Category CIM -Status Passed -Severity Information -Message 'A local CIM query completed successfully.' -Details $null)
    }
    catch {
        return @(New-ICReadinessCheck -Code 'CIM_QUERY_FAILED' -Category CIM -Status Warning -Severity Warning -Message 'The local CIM readiness query failed; CIM-backed collectors can complete partially.' -Details ([pscustomobject][ordered]@{
            Error = $_.Exception.Message
        }))
    }
}

function Test-ICNoMatchingEventsError {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    return [string]$ErrorRecord.FullyQualifiedErrorId -like '*NoMatchingEventsFound*'
}

function Test-ICAccessDeniedError {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    if ($ErrorRecord.Exception -is [System.UnauthorizedAccessException] -or $ErrorRecord.Exception -is [System.Security.SecurityException]) {
        return $true
    }

    $identity = '{0} {1}' -f [string]$ErrorRecord.FullyQualifiedErrorId, [string]$ErrorRecord.Exception.Message
    return $identity -match '(?i)unauthorized|access.?denied|zugriff.?verweigert'
}

function Get-ICEventLogReadinessCheck {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Configuration
    )

    if ('EventLogs' -notin @($Configuration.Collectors)) {
        return @(New-ICReadinessCheck -Code 'EVENT_LOGS_NOT_SELECTED' -Category EventLogs -Status Passed -Severity Information -Message 'The EventLogs collector is not selected.' -Details $null)
    }

    if ($null -eq (Get-Command -Name 'Get-WinEvent' -ErrorAction SilentlyContinue)) {
        return @(New-ICReadinessCheck -Code 'EVENT_LOG_PROVIDER_UNAVAILABLE' -Category EventLogs -Status Warning -Severity Warning -Message 'Get-WinEvent is unavailable; event channels cannot be inspected or collected.' -Details $null)
    }

    $checks = New-Object System.Collections.ArrayList
    foreach ($logName in @($Configuration.EventLogs)) {
        try {
            $logInfo = Get-WinEvent -ListLog $logName -ErrorAction Stop | Select-Object -First 1
            if ($null -eq $logInfo) {
                [void]$checks.Add((New-ICReadinessCheck -Code 'EVENT_LOG_UNAVAILABLE' -Category EventLogs -Status Warning -Severity Warning -Message "Event channel '$logName' was not found." -Details ([pscustomobject][ordered]@{ EventLog = $logName })))
                continue
            }

            $isEnabled = $true
            if ($null -ne $logInfo.PSObject.Properties['IsEnabled']) {
                $isEnabled = [bool]$logInfo.IsEnabled
            }
            if (-not $isEnabled) {
                [void]$checks.Add((New-ICReadinessCheck -Code 'EVENT_LOG_DISABLED' -Category EventLogs -Status Warning -Severity Warning -Message "Event channel '$logName' exists but is disabled." -Details ([pscustomobject][ordered]@{
                    EventLog = $logName
                    IsEnabled = $false
                })))
                continue
            }

            try {
                $null = Get-WinEvent -LogName $logName -MaxEvents 1 -ErrorAction Stop
                [void]$checks.Add((New-ICReadinessCheck -Code 'EVENT_LOG_READY' -Category EventLogs -Status Passed -Severity Information -Message "Event channel '$logName' is enabled and readable." -Details ([pscustomobject][ordered]@{
                    EventLog = $logName
                    IsEnabled = $true
                })))
            }
            catch {
                if (Test-ICNoMatchingEventsError -ErrorRecord $_) {
                    [void]$checks.Add((New-ICReadinessCheck -Code 'EVENT_LOG_READY' -Category EventLogs -Status Passed -Severity Information -Message "Event channel '$logName' is enabled and readable but currently empty." -Details ([pscustomobject][ordered]@{
                        EventLog = $logName
                        IsEnabled = $true
                    })))
                }
                elseif (Test-ICAccessDeniedError -ErrorRecord $_) {
                    [void]$checks.Add((New-ICReadinessCheck -Code 'EVENT_LOG_ACCESS_DENIED' -Category EventLogs -Status Warning -Severity Warning -Message "Event channel '$logName' exists but cannot be read by the current identity." -Details ([pscustomobject][ordered]@{
                        EventLog = $logName
                        Error = $_.Exception.Message
                    })))
                }
                else {
                    [void]$checks.Add((New-ICReadinessCheck -Code 'EVENT_LOG_READ_FAILED' -Category EventLogs -Status Warning -Severity Warning -Message "Event channel '$logName' exists but its readability check failed." -Details ([pscustomobject][ordered]@{
                        EventLog = $logName
                        Error = $_.Exception.Message
                    })))
                }
            }
        }
        catch {
            $code = 'EVENT_LOG_UNAVAILABLE'
            $message = "Event channel '$logName' is unavailable."
            if (Test-ICAccessDeniedError -ErrorRecord $_) {
                $code = 'EVENT_LOG_ACCESS_DENIED'
                $message = "Event channel '$logName' cannot be inspected by the current identity."
            }
            [void]$checks.Add((New-ICReadinessCheck -Code $code -Category EventLogs -Status Warning -Severity Warning -Message $message -Details ([pscustomobject][ordered]@{
                EventLog = $logName
                Error = $_.Exception.Message
            })))
        }
    }

    return @($checks)
}