PackageModel/Support/Package/Eigenverft.Manifested.Sandbox.PackageModel.Validation.ps1

<#
    Eigenverft.Manifested.Sandbox.PackageModel.Validation
#>


function Get-PackageModelEntryPointDefinition {
<#
.SYNOPSIS
Returns an entry-point definition by name.
 
.DESCRIPTION
Searches the definition provided-tool collections and returns the first matching
command or app entry by name.
 
.PARAMETER Definition
The PackageModel definition object.
 
.PARAMETER EntryPointName
The entry-point name to resolve.
 
.EXAMPLE
Get-PackageModelEntryPointDefinition -Definition $definition -EntryPointName code
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition,

        [Parameter(Mandatory = $true)]
        [string]$EntryPointName
    )

    foreach ($entryPoint in @($Definition.providedTools.commands)) {
        if ([string]::Equals([string]$entryPoint.name, $EntryPointName, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $entryPoint
        }
    }
    foreach ($entryPoint in @($Definition.providedTools.apps)) {
        if ([string]::Equals([string]$entryPoint.name, $EntryPointName, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $entryPoint
        }
    }

    return $null
}

function Get-PackageModelCommandCheckPath {
<#
.SYNOPSIS
Resolves the command path used for a validation command check.
 
.DESCRIPTION
Uses an explicit relative path when provided, otherwise resolves the path from
the named PackageModel entry point.
 
.PARAMETER PackageModelResult
The current PackageModel result object.
 
.PARAMETER CommandCheck
The validation command-check definition.
 
.EXAMPLE
Get-PackageModelCommandCheckPath -PackageModelResult $result -CommandCheck $check
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageModelResult,

        [Parameter(Mandatory = $true)]
        [psobject]$CommandCheck
    )

    if ($CommandCheck.PSObject.Properties['relativePath'] -and -not [string]::IsNullOrWhiteSpace([string]$CommandCheck.relativePath)) {
        return (Join-Path $PackageModelResult.InstallDirectory (([string]$CommandCheck.relativePath) -replace '/', '\'))
    }

    if ($CommandCheck.PSObject.Properties['entryPoint'] -and -not [string]::IsNullOrWhiteSpace([string]$CommandCheck.entryPoint)) {
        $entryPoint = Get-PackageModelEntryPointDefinition -Definition $PackageModelResult.PackageModelConfig.Definition -EntryPointName ([string]$CommandCheck.entryPoint)
        if (-not $entryPoint -or -not $entryPoint.PSObject.Properties['relativePath']) {
            throw "PackageModel validation entry point '$($CommandCheck.entryPoint)' was not found."
        }

        return (Join-Path $PackageModelResult.InstallDirectory (([string]$entryPoint.relativePath) -replace '/', '\'))
    }

    throw 'PackageModel command checks require either relativePath or entryPoint.'
}

function Get-PackageModelJsonValue {
<#
.SYNOPSIS
Reads a dotted property path from an object.
 
.DESCRIPTION
Walks a dotted property path such as `version` or `metadata.name` and returns
the current value when every path segment exists.
 
.PARAMETER InputObject
The object to read from.
 
.PARAMETER PropertyPath
The dotted property path.
 
.EXAMPLE
Get-PackageModelJsonValue -InputObject $document -PropertyPath version
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$InputObject,

        [Parameter(Mandatory = $true)]
        [string]$PropertyPath
    )

    $current = $InputObject
    foreach ($segment in @($PropertyPath -split '\.')) {
        if ($null -eq $current -or -not $current.PSObject.Properties[$segment]) {
            return $null
        }

        $current = $current.$segment
    }

    return $current
}

function Test-PackageModelValidationValueComparison {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [AllowNull()]
        [object]$ActualValue,

        [AllowNull()]
        [object]$ExpectedValue,

        [string]$Operator = '=='
    )

    $actualText = [string]$ActualValue
    $expectedText = [string]$ExpectedValue
    $operatorText = if ([string]::IsNullOrWhiteSpace($Operator)) { '==' } else { $Operator }

    if ($operatorText -in @('=', '==')) {
        return [string]::Equals($actualText, $expectedText, [System.StringComparison]::OrdinalIgnoreCase)
    }
    if ($operatorText -eq '!=') {
        return (-not [string]::Equals($actualText, $expectedText, [System.StringComparison]::OrdinalIgnoreCase))
    }

    $actualVersion = ConvertTo-PackageModelVersion -VersionText $actualText
    $expectedVersion = ConvertTo-PackageModelVersion -VersionText $expectedText
    switch -Exact ($operatorText) {
        '>' { return $actualVersion -gt $expectedVersion }
        '>=' { return $actualVersion -ge $expectedVersion }
        '<' { return $actualVersion -lt $expectedVersion }
        '<=' { return $actualVersion -le $expectedVersion }
        default { throw "Unsupported PackageModel validation comparison operator '$operatorText'." }
    }
}

function Test-PackageModelInstalledPackage {
<#
.SYNOPSIS
Validates an installed package against its PackageModel rules.
 
.DESCRIPTION
Runs file, directory, command, metadata, signature, file-details, and registry
checks for the current install directory and attaches the validation result to
the PackageModel result object.
 
.PARAMETER PackageModelResult
The PackageModel result object to validate.
 
.EXAMPLE
Test-PackageModelInstalledPackage -PackageModelResult $result
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageModelResult
    )

    $package = $PackageModelResult.Package
    if (-not $package -or -not $package.PSObject.Properties['validation'] -or $null -eq $package.validation) {
        $PackageModelResult.Validation = [pscustomobject]@{
            Status       = 'Ready'
            Accepted     = $true
            InstallDirectory = $PackageModelResult.InstallDirectory
            Files        = @()
            Directories  = @()
            Commands     = @()
            MetadataFiles = @()
            Signatures   = @()
            FileDetails  = @()
            Registry     = @()
        }
        return $PackageModelResult
    }

    $installDirectory = $PackageModelResult.InstallDirectory
    $validation = $package.validation
    $requiresInstallDirectory = (@($validation.files).Count -gt 0) -or
        (@($validation.directories).Count -gt 0) -or
        (@($validation.commandChecks).Count -gt 0) -or
        (@($validation.metadataFiles).Count -gt 0) -or
        (@($validation.signatures).Count -gt 0) -or
        (@($validation.fileDetails).Count -gt 0)
    if ($requiresInstallDirectory -and ([string]::IsNullOrWhiteSpace($installDirectory) -or -not (Test-Path -LiteralPath $installDirectory))) {
        $PackageModelResult.Validation = [pscustomobject]@{
            Status           = 'Failed'
            Accepted         = $false
            FailureReason    = 'InstallDirectoryMissing'
            InstallDirectory = $installDirectory
            Files            = @()
            Directories      = @()
            Commands         = @()
            MetadataFiles    = @()
            Signatures       = @()
            FileDetails      = @()
            Registry         = @()
        }
        return $PackageModelResult
    }

    $fileResults = New-Object System.Collections.Generic.List[object]
    foreach ($relativePath in @($validation.files)) {
        if ($null -eq $relativePath) {
            continue
        }
        $path = Join-Path $installDirectory (([string]$relativePath) -replace '/', '\')
        $fileResults.Add([pscustomobject]@{
            RelativePath = $relativePath
            Path         = $path
            Exists       = (Test-Path -LiteralPath $path -PathType Leaf)
            Status       = if (Test-Path -LiteralPath $path -PathType Leaf) { 'Ready' } else { 'Missing' }
        }) | Out-Null
    }

    $directoryResults = New-Object System.Collections.Generic.List[object]
    foreach ($relativePath in @($validation.directories)) {
        if ($null -eq $relativePath) {
            continue
        }
        $path = Join-Path $installDirectory (([string]$relativePath) -replace '/', '\')
        $directoryResults.Add([pscustomobject]@{
            RelativePath = $relativePath
            Path         = $path
            Exists       = (Test-Path -LiteralPath $path -PathType Container)
            Status       = if (Test-Path -LiteralPath $path -PathType Container) { 'Ready' } else { 'Missing' }
        }) | Out-Null
    }

    $commandResults = New-Object System.Collections.Generic.List[object]
    foreach ($commandCheck in @($validation.commandChecks)) {
        if ($null -eq $commandCheck) {
            continue
        }
        $commandPath = Get-PackageModelCommandCheckPath -PackageModelResult $PackageModelResult -CommandCheck $commandCheck
        $arguments = @()
        foreach ($argument in @($commandCheck.arguments)) {
            $arguments += (Resolve-PackageModelTemplateText -Text ([string]$argument) -PackageModelConfig $PackageModelResult.PackageModelConfig -Package $package)
        }

        $outputLines = @()
        $exitCode = $null
        try {
            $outputLines = @(& $commandPath @arguments 2>&1)
            $exitCode = $LASTEXITCODE
            if ($null -eq $exitCode) {
                $exitCode = 0
            }
        }
        catch {
            $outputLines = @($_.Exception.Message)
            $exitCode = 1
        }

        $combinedOutput = (($outputLines | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine)
        $pattern = if ($commandCheck.PSObject.Properties['outputPattern']) { [string]$commandCheck.outputPattern } else { '(?m)^(?<value>.+)$' }
        $match = [regex]::Match($combinedOutput, $pattern)
        $actualValue = if ($match.Success) {
            if ($match.Groups['value'] -and $match.Groups['value'].Success) {
                $match.Groups['value'].Value.Trim()
            }
            else {
                $match.Value.Trim()
            }
        }
        else {
            $null
        }

        $expectedValue = if ($commandCheck.PSObject.Properties['expectedValue'] -and -not [string]::IsNullOrWhiteSpace([string]$commandCheck.expectedValue)) {
            Resolve-PackageModelTemplateText -Text ([string]$commandCheck.expectedValue) -PackageModelConfig $PackageModelResult.PackageModelConfig -Package $package
        }
        else {
            $null
        }

        $isReady = ($exitCode -eq 0) -and ($null -ne $actualValue) -and (([string]::IsNullOrWhiteSpace($expectedValue)) -or ([string]::Equals([string]$actualValue, [string]$expectedValue, [System.StringComparison]::OrdinalIgnoreCase)))
        $commandResults.Add([pscustomobject]@{
            EntryPoint    = if ($commandCheck.PSObject.Properties['entryPoint']) { $commandCheck.entryPoint } else { $null }
            Path          = $commandPath
            ExitCode      = $exitCode
            ActualValue   = $actualValue
            ExpectedValue = $expectedValue
            Status        = if ($isReady) { 'Ready' } else { 'Failed' }
            Output        = $combinedOutput
        }) | Out-Null
    }

    $metadataResults = New-Object System.Collections.Generic.List[object]
    foreach ($metadataCheck in @($validation.metadataFiles)) {
        if ($null -eq $metadataCheck) {
            continue
        }
        $path = Join-Path $installDirectory (([string]$metadataCheck.relativePath) -replace '/', '\')
        $exists = Test-Path -LiteralPath $path -PathType Leaf
        $value = $null
        $status = 'Missing'

        if ($exists) {
            try {
                $document = Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
                $value = Get-PackageModelJsonValue -InputObject $document -PropertyPath ([string]$metadataCheck.jsonPath)
                $expectedValue = Resolve-PackageModelTemplateText -Text ([string]$metadataCheck.expectedValue) -PackageModelConfig $PackageModelResult.PackageModelConfig -Package $package
                $status = if ([string]::Equals([string]$value, [string]$expectedValue, [System.StringComparison]::OrdinalIgnoreCase)) { 'Ready' } else { 'Failed' }
            }
            catch {
                $status = 'Failed'
            }
        }
        else {
            $expectedValue = Resolve-PackageModelTemplateText -Text ([string]$metadataCheck.expectedValue) -PackageModelConfig $PackageModelResult.PackageModelConfig -Package $package
        }

        $metadataResults.Add([pscustomobject]@{
            RelativePath = $metadataCheck.relativePath
            Path         = $path
            Exists       = $exists
            JsonPath     = $metadataCheck.jsonPath
            Value        = $value
            ExpectedValue = $expectedValue
            Status       = $status
        }) | Out-Null
    }

    $signatureResults = New-Object System.Collections.Generic.List[object]
    foreach ($signatureCheck in @($validation.signatures)) {
        if ($null -eq $signatureCheck) {
            continue
        }
        $path = Join-Path $installDirectory (([string]$signatureCheck.relativePath) -replace '/', '\')
        $status = 'Missing'
        $signatureStatus = $null
        $signerSubject = $null

        if (Test-Path -LiteralPath $path -PathType Leaf) {
            try {
                $signature = Get-AuthenticodeSignature -FilePath $path
                $signatureStatus = $signature.Status.ToString()
                $signerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { $null }
                $requiresValid = $true
                if ($signatureCheck.PSObject.Properties['requireValid']) {
                    $requiresValid = [bool]$signatureCheck.requireValid
                }
                $status = 'Ready'
                if ($requiresValid -and $signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) {
                    $status = 'Failed'
                }
                if ($status -eq 'Ready' -and $signatureCheck.PSObject.Properties['subjectContains'] -and
                    -not [string]::IsNullOrWhiteSpace([string]$signatureCheck.subjectContains) -and
                    ($null -eq $signerSubject -or $signerSubject -notmatch [regex]::Escape([string]$signatureCheck.subjectContains))) {
                    $status = 'Failed'
                }
            }
            catch {
                $status = 'Failed'
            }
        }

        $signatureResults.Add([pscustomobject]@{
            RelativePath     = $signatureCheck.relativePath
            Path             = $path
            SignatureStatus  = $signatureStatus
            SignerSubject    = $signerSubject
            Status           = $status
        }) | Out-Null
    }

    $fileDetailResults = New-Object System.Collections.Generic.List[object]
    foreach ($detailCheck in @($validation.fileDetails)) {
        if ($null -eq $detailCheck) {
            continue
        }
        $path = Join-Path $installDirectory (([string]$detailCheck.relativePath) -replace '/', '\')
        $productName = $null
        $fileDescription = $null
        $status = 'Missing'

        if (Test-Path -LiteralPath $path -PathType Leaf) {
            try {
                $item = Get-Item -LiteralPath $path -ErrorAction Stop
                $productName = if ($item.PSObject.Properties['VersionInfo'] -and $item.VersionInfo) { $item.VersionInfo.ProductName } else { $null }
                $fileDescription = if ($item.PSObject.Properties['VersionInfo'] -and $item.VersionInfo) { $item.VersionInfo.FileDescription } else { $null }
                $status = 'Ready'
                if ($detailCheck.PSObject.Properties['productName'] -and
                    -not [string]::IsNullOrWhiteSpace([string]$detailCheck.productName) -and
                    -not [string]::Equals([string]$productName, [string]$detailCheck.productName, [System.StringComparison]::OrdinalIgnoreCase)) {
                    $status = 'Failed'
                }
                if ($status -eq 'Ready' -and $detailCheck.PSObject.Properties['fileDescription'] -and
                    -not [string]::IsNullOrWhiteSpace([string]$detailCheck.fileDescription) -and
                    -not [string]::Equals([string]$fileDescription, [string]$detailCheck.fileDescription, [System.StringComparison]::OrdinalIgnoreCase)) {
                    $status = 'Failed'
                }
            }
            catch {
                $status = 'Failed'
            }
        }

        $fileDetailResults.Add([pscustomobject]@{
            RelativePath    = $detailCheck.relativePath
            Path            = $path
            ProductName     = $productName
            FileDescription = $fileDescription
            Status          = $status
        }) | Out-Null
    }

    $registryResults = New-Object System.Collections.Generic.List[object]
    foreach ($registryCheck in @($validation.registryChecks)) {
        if ($null -eq $registryCheck) {
            continue
        }
        $registryPaths = if ($registryCheck.PSObject.Properties['paths'] -and @($registryCheck.paths).Count -gt 0) {
            @($registryCheck.paths | ForEach-Object { [string]$_ })
        }
        elseif ($registryCheck.PSObject.Properties['path'] -and -not [string]::IsNullOrWhiteSpace([string]$registryCheck.path)) {
            @([string]$registryCheck.path)
        }
        else {
            @()
        }
        $expectedValue = if ($registryCheck.PSObject.Properties['expectedValue']) {
            Resolve-PackageModelTemplateText -Text ([string]$registryCheck.expectedValue) -PackageModelConfig $PackageModelResult.PackageModelConfig -Package $package
        }
        else {
            $null
        }
        $operator = if ($registryCheck.PSObject.Properties['operator'] -and -not [string]::IsNullOrWhiteSpace([string]$registryCheck.operator)) {
            [string]$registryCheck.operator
        }
        else {
            '=='
        }
        $registryPath = if ($registryPaths.Count -gt 0) { $registryPaths[0] } else { $null }
        $actualValue = $null
        $status = 'Missing'

        foreach ($candidateRegistryPath in $registryPaths) {
            try {
                if (Test-Path -LiteralPath $candidateRegistryPath) {
                    $registryPath = $candidateRegistryPath
                    $status = 'Ready'
                    if ($registryCheck.PSObject.Properties['valueName'] -and -not [string]::IsNullOrWhiteSpace([string]$registryCheck.valueName)) {
                        $actualValue = (Get-ItemProperty -LiteralPath $candidateRegistryPath -Name ([string]$registryCheck.valueName) -ErrorAction Stop).$($registryCheck.valueName)
                        if ($registryCheck.PSObject.Properties['expectedValue']) {
                            $status = if (Test-PackageModelValidationValueComparison -ActualValue $actualValue -ExpectedValue $expectedValue -Operator $operator) { 'Ready' } else { 'Failed' }
                        }
                        else {
                            $status = 'Ready'
                        }
                    }
                    else {
                        $status = 'Ready'
                    }
                }
            }
            catch {
                $registryPath = $candidateRegistryPath
                $status = 'Failed'
            }

            if ($status -eq 'Ready') {
                break
            }
        }

        $registryResults.Add([pscustomobject]@{
            Path         = $registryPath
            Paths        = @($registryPaths)
            ValueName    = if ($registryCheck.PSObject.Properties['valueName']) { $registryCheck.valueName } else { $null }
            ActualValue  = $actualValue
            ExpectedValue = $expectedValue
            Operator     = if ($registryCheck.PSObject.Properties['expectedValue']) { $operator } else { $null }
            Status       = $status
        }) | Out-Null
    }

    $allResults = @($fileResults.ToArray()) + @($directoryResults.ToArray()) + @($commandResults.ToArray()) + @($metadataResults.ToArray()) + @($signatureResults.ToArray()) + @($fileDetailResults.ToArray()) + @($registryResults.ToArray())
    $accepted = (@($allResults | Where-Object { $_.Status -ne 'Ready' }).Count -eq 0)

    $PackageModelResult.Validation = [pscustomobject]@{
        Status           = if ($accepted) { 'Ready' } else { 'Failed' }
        Accepted         = $accepted
        FailureReason    = if ($accepted) { $null } else { 'InstalledPackageValidationFailed' }
        InstallDirectory = $installDirectory
        Files            = @($fileResults.ToArray())
        Directories      = @($directoryResults.ToArray())
        Commands         = @($commandResults.ToArray())
        MetadataFiles    = @($metadataResults.ToArray())
        Signatures       = @($signatureResults.ToArray())
        FileDetails      = @($fileDetailResults.ToArray())
        Registry         = @($registryResults.ToArray())
    }

    $failedCount = @($allResults | Where-Object { $_.Status -ne 'Ready' }).Count
    Write-PackageModelExecutionMessage -Message ("[STATE] Validation completed for '{0}' with accepted='{1}', failedChecks={2}." -f $installDirectory, $accepted, $failedCount)

    return $PackageModelResult
}