Public/Test-IncidentCapsuleIntegrity.ps1

function Test-IncidentCapsuleIntegrity {
    <#
    .SYNOPSIS
    Verifies an Incident Capsule directory or ZIP archive.

    .DESCRIPTION
    Validates the embedded SHA-256 manifest, conventional checksum list, and file set.
    ZIP archives are inspected for unsafe paths, link metadata, duplicate entries, and
    bounded entry count, per-entry size, expanded size, and compression ratio before
    extraction. An adjacent .sha256 sidecar is verified when present.

    .PARAMETER Path
    Path to a capsule directory or ZIP archive.

    .PARAMETER RequireSidecar
    Reject a ZIP archive when its adjacent .sha256 sidecar is missing.

    .PARAMETER RequireSignature
    Reject a capsule when the detached CMS manifest signature
    metadata/manifest.sha256.p7s is missing. When the signature is present it is
    always verified; chain trust is reported separately from cryptographic validity.

    .PARAMETER MaximumArchiveEntries
    Maximum number of ZIP entries inspected and extracted.

    .PARAMETER MaximumArchiveEntryBytes
    Maximum declared and extracted size of one ZIP entry.

    .PARAMETER MaximumArchiveExpandedBytes
    Maximum total expanded ZIP size. MaximumArchiveUncompressedBytes remains an alias.

    .PARAMETER MaximumArchiveCompressionRatio
    Maximum per-entry and aggregate ZIP compression ratio.

    .EXAMPLE
    Test-IncidentCapsuleIntegrity -Path 'E:\Evidence\IC_IR-2026-0042_WS-042_20260712T184233Z_a1b2c3d4.zip' -RequireSidecar

    .EXAMPLE
    $result = Test-IncidentCapsuleIntegrity -Path $capsule.WorkingDirectory
    $result.FileResults | Where-Object Status -ne 'Valid'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [string]$Path,

        [switch]$RequireSidecar,

        [switch]$RequireSignature,

        [ValidateRange(1, 50000)]
        [int]$MaximumArchiveEntries = 20000,

        [ValidateRange(1, 8796093022208L)]
        [int64]$MaximumArchiveEntryBytes = 1073741824L,

        [Alias('MaximumArchiveUncompressedBytes')]
        [ValidateRange(1, 8796093022208L)]
        [int64]$MaximumArchiveExpandedBytes = 21474836480L,

        [ValidateRange(1, 1000)]
        [double]$MaximumArchiveCompressionRatio = 250
    )

    process {
        if ($MaximumArchiveEntryBytes -gt $MaximumArchiveExpandedBytes) {
            throw 'MaximumArchiveEntryBytes cannot exceed MaximumArchiveExpandedBytes.'
        }

        $resolved = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
        if (Test-Path -LiteralPath $resolved -PathType Container) {
            $root = Find-ICManifestRoot -Path $resolved
            $result = Test-ICDirectoryIntegrity -CapsuleRoot $root
            if ($RequireSignature -and -not $result.SignaturePresent) {
                throw "Required manifest signature 'metadata/manifest.sha256.p7s' was not found in '$root'."
            }
            $result
            return
        }

        if ([System.IO.Path]::GetExtension($resolved) -ne '.zip') {
            throw "Path '$resolved' is neither a directory nor a ZIP archive."
        }

        Test-ICArchiveIntegrity `
            -ArchivePath $resolved `
            -RequireSidecar:$RequireSidecar `
            -RequireSignature:$RequireSignature `
            -MaximumArchiveEntries $MaximumArchiveEntries `
            -MaximumArchiveEntryBytes $MaximumArchiveEntryBytes `
            -MaximumArchiveExpandedBytes $MaximumArchiveExpandedBytes `
            -MaximumArchiveCompressionRatio $MaximumArchiveCompressionRatio
    }
}