298 lines
15 KiB
PowerShell
298 lines
15 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][ValidateSet('purchase', 'leave')][string]$Workflow,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{1,64}$')][string]$ModuleCode,
|
|
[Parameter(Mandatory = $true)][string]$AccountBook,
|
|
[Parameter(Mandatory = $true)][string]$SubSystemId,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{40}$')][string]$SourceCommit,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$PackageSha256,
|
|
[Parameter(Mandatory = $true)][string]$RuntimeConfigurationFile,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$')][string]$RolloutCustomerId,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{8,128}$')][string]$EnvironmentId,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{1,128}$')][string]$TestedBy,
|
|
[Parameter(Mandatory = $true)][string]$CasesFile,
|
|
[Parameter(Mandatory = $true)][string]$UatAuthorizationFile,
|
|
[Parameter(Mandatory = $true)][string]$VerifierCliPath,
|
|
[Parameter(Mandatory = $true)][string]$OutputPath
|
|
)
|
|
|
|
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Get-RegularFile([string]$Path, [long]$MaximumBytes, [string]$Label) {
|
|
$full = [IO.Path]::GetFullPath($Path)
|
|
if (-not [IO.File]::Exists($full)) { throw "$Label file does not exist." }
|
|
$info = Get-Item -LiteralPath $full -Force
|
|
if ($info.Length -le 0 -or $info.Length -gt $MaximumBytes -or
|
|
(($info.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
throw "$Label must be a non-empty ordinary file within the size limit."
|
|
}
|
|
return $full
|
|
}
|
|
|
|
function Get-Sha256([byte[]]$Bytes) {
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
return ([BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant()
|
|
}
|
|
finally { $sha.Dispose() }
|
|
}
|
|
|
|
function Test-ExactProperties([object]$Value, [string[]]$Expected) {
|
|
if ($null -eq $Value) { return $false }
|
|
$names = @($Value.PSObject.Properties | ForEach-Object { $_.Name })
|
|
if ($names.Count -ne $Expected.Count) { return $false }
|
|
foreach ($name in $Expected) { if ($names -cnotcontains $name) { return $false } }
|
|
return $true
|
|
}
|
|
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$runtimePath = Get-RegularFile $RuntimeConfigurationFile 64KB 'Runtime configuration'
|
|
$casesPath = Get-RegularFile $CasesFile 2MB 'Cases'
|
|
$uatAuthorizationPath = Get-RegularFile `
|
|
$UatAuthorizationFile 512KB 'UAT authorization'
|
|
$cliPath = Get-RegularFile $VerifierCliPath 64MB 'Verifier CLI'
|
|
if ([IO.Path]::GetFileName($cliPath) -ne 'lserp-cli.exe') {
|
|
throw 'Verifier CLI filename must be lserp-cli.exe.'
|
|
}
|
|
$runtimeBytes = [IO.File]::ReadAllBytes($runtimePath)
|
|
$runtimeHash = Get-Sha256 $runtimeBytes
|
|
$uatAuthorizationHash = Get-Sha256 `
|
|
([IO.File]::ReadAllBytes($uatAuthorizationPath))
|
|
$cliHash = Get-Sha256 ([IO.File]::ReadAllBytes($cliPath))
|
|
$uatOutput = @(& $cliPath 'acceptance' 'verify-uat-authorization' `
|
|
'--input' $uatAuthorizationPath '--json' 2>&1)
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'UAT authorization failed final CLI verification.'
|
|
}
|
|
try {
|
|
$uatVerification = (($uatOutput | ForEach-Object { [string]$_ }) -join `
|
|
[Environment]::NewLine) | ConvertFrom-Json
|
|
}
|
|
catch { throw 'UAT authorization verifier returned invalid JSON.' }
|
|
if (-not (Test-ExactProperties $uatVerification @('ok', 'correlationId', 'data')) -or
|
|
-not (Test-ExactProperties $uatVerification.data.erpScope @(
|
|
'accountBook', 'subSystemId', 'userIdSha256', 'userNameSha256',
|
|
'databaseScopeFingerprint', 'isAdministrator')) -or
|
|
-not (Test-ExactProperties $uatVerification.data.runtimeCli @(
|
|
'fileName', 'version', 'sha256', 'signerThumbprint',
|
|
'requiresElevation', 'bridgeOnly', 'databaseDirectAccess',
|
|
'sessionSource')) -or
|
|
-not (Test-ExactProperties $uatVerification.data.verifierCli @(
|
|
'fileName', 'sha256', 'signerThumbprint',
|
|
'requiresElevation')) -or
|
|
$uatVerification.ok -ne $true -or
|
|
$uatVerification.data.packageType -cne 'workflow_write_uat_authorization' -or
|
|
$uatVerification.data.schemaVersion -cne '1.2' -or
|
|
$uatVerification.data.sourceSha256 -cne $uatAuthorizationHash -or
|
|
$uatVerification.data.runtimeConfigurationSha256 -cne $runtimeHash -or
|
|
$uatVerification.data.sourceCommit -cne $SourceCommit.ToLowerInvariant() -or
|
|
$uatVerification.data.packageSha256 -cne $PackageSha256.ToLowerInvariant() -or
|
|
$uatVerification.data.customerId -cne $RolloutCustomerId -or
|
|
$uatVerification.data.environmentId -cne $EnvironmentId -or
|
|
$uatVerification.data.erpScope.accountBook -cne $AccountBook -or
|
|
$uatVerification.data.erpScope.subSystemId -cne $SubSystemId -or
|
|
$uatVerification.data.runtimeCli.fileName -cne 'lserp-agent-cli.exe' -or
|
|
[string]$uatVerification.data.runtimeCli.version -cnotmatch
|
|
'^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$' -or
|
|
[string]$uatVerification.data.runtimeCli.sha256 -cnotmatch
|
|
'^[a-f0-9]{64}$' -or
|
|
[string]$uatVerification.data.runtimeCli.signerThumbprint -cnotmatch
|
|
'^[A-F0-9]{40}$' -or
|
|
$uatVerification.data.runtimeCli.requiresElevation -ne $false -or
|
|
$uatVerification.data.runtimeCli.bridgeOnly -ne $true -or
|
|
$uatVerification.data.runtimeCli.databaseDirectAccess -ne $false -or
|
|
$uatVerification.data.runtimeCli.sessionSource -cne
|
|
'current_logged_in_erp_process' -or
|
|
$uatVerification.data.verifierCli.fileName -cne 'lserp-cli.exe' -or
|
|
$uatVerification.data.verifierCli.sha256 -cne $cliHash -or
|
|
$uatVerification.data.verifierCli.requiresElevation -ne $true -or
|
|
$uatVerification.data.signatureVerified -ne $true -or
|
|
$uatVerification.data.uatAuthorized -ne $true -or
|
|
$uatVerification.data.productionReady -ne $false) {
|
|
throw 'UAT authorization does not bind the final test scope and verifier.'
|
|
}
|
|
$authorizedWorkflows = @($uatVerification.data.workflows | Where-Object {
|
|
[string]$_.workflow -ceq $Workflow -and
|
|
[string]$_.moduleCode -ceq $ModuleCode
|
|
})
|
|
if ($authorizedWorkflows.Count -ne 1) {
|
|
throw 'UAT authorization does not contain the exact workflow and module.'
|
|
}
|
|
$casesText = [IO.File]::ReadAllText($casesPath, $strictUtf8)
|
|
$casesHash = Get-Sha256 ($strictUtf8.GetBytes($casesText))
|
|
try { $cases = @($casesText | ConvertFrom-Json) }
|
|
catch { throw 'Cases file must be a UTF-8 JSON array.' }
|
|
if ($cases.Count -le 0 -or $cases.Count -gt 32 -or $casesText.TrimStart()[0] -ne '[') {
|
|
throw 'Cases file must contain a JSON array with 1-32 items.'
|
|
}
|
|
$runtimeCliVersion = [string]$uatVerification.data.runtimeCli.version
|
|
$runtimeCliSha256 = [string]$uatVerification.data.runtimeCli.sha256
|
|
$runtimeCliSigner = [string]$uatVerification.data.runtimeCli.signerThumbprint
|
|
foreach ($case in $cases) {
|
|
if ([string]$case.runtimeCliVersion -cne $runtimeCliVersion -or
|
|
[string]$case.runtimeCliSha256 -cne $runtimeCliSha256 -or
|
|
[string]$case.runtimeCliSignerThumbprint -cne $runtimeCliSigner) {
|
|
throw 'A projected case does not bind the authorized runtime CLI.'
|
|
}
|
|
}
|
|
|
|
$testedAt = [DateTime]::UtcNow
|
|
$content = [ordered]@{
|
|
evidenceType = 'workflow_write_integration'
|
|
workflow = $Workflow
|
|
moduleCode = $ModuleCode
|
|
erpScope = [ordered]@{
|
|
accountBook = $AccountBook
|
|
subSystemId = $SubSystemId
|
|
}
|
|
sourceCommit = $SourceCommit.ToLowerInvariant()
|
|
packageSha256 = $PackageSha256.ToLowerInvariant()
|
|
runtimeConfigurationSha256 = $runtimeHash
|
|
runtimeCli = [ordered]@{
|
|
fileName = 'lserp-agent-cli.exe'
|
|
version = $runtimeCliVersion
|
|
sha256 = $runtimeCliSha256
|
|
signerThumbprint = $runtimeCliSigner
|
|
requiresElevation = $false
|
|
bridgeOnly = $true
|
|
databaseDirectAccess = $false
|
|
sessionSource = 'current_logged_in_erp_process'
|
|
}
|
|
uatAuthorizationSourceSha256 = [string]$uatVerification.data.sourceSha256
|
|
uatAuthorizationContentSha256 = [string]$uatVerification.data.contentSha256
|
|
uatAuthorizationIdSha256 = [string]$uatVerification.data.authorizationIdSha256
|
|
environmentId = $EnvironmentId
|
|
testedAtUtc = $testedAt.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
|
testedBy = $TestedBy
|
|
cases = $cases
|
|
}
|
|
$canonical = $content | ConvertTo-Json -Compress -Depth 12
|
|
$contentHash = Get-Sha256 $strictUtf8.GetBytes($canonical)
|
|
$envelope = [ordered]@{
|
|
schemaVersion = '1.6'
|
|
contentSha256 = $contentHash
|
|
content = $content
|
|
}
|
|
$body = $strictUtf8.GetBytes(($envelope | ConvertTo-Json -Depth 12) + [Environment]::NewLine)
|
|
$bodyHash = Get-Sha256 $body
|
|
$target = [IO.Path]::GetFullPath($OutputPath)
|
|
$directory = [IO.Path]::GetDirectoryName($target)
|
|
if ([string]::IsNullOrWhiteSpace($directory) -or -not [IO.Directory]::Exists($directory) -or
|
|
[IO.File]::Exists($target) -or [IO.Directory]::Exists($target)) {
|
|
throw 'Output must be a new file in an existing directory.'
|
|
}
|
|
$temporary = Join-Path $directory ('.lserp-write-evidence-' + [Guid]::NewGuid().ToString('N') + '.tmp')
|
|
$published = $false
|
|
try {
|
|
$stream = [IO.File]::Open(
|
|
$temporary, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
|
try { $stream.Write($body, 0, $body.Length); $stream.Flush() }
|
|
finally { $stream.Dispose() }
|
|
|
|
$arguments = @(
|
|
'adapters', 'verify-write-integration-evidence', '--input', $temporary,
|
|
'--workflow', $Workflow, '--module', $ModuleCode,
|
|
'--account-book', $AccountBook, '--subsystem', $SubSystemId,
|
|
'--runtime-sha256', $runtimeHash,
|
|
'--source-commit', $SourceCommit.ToLowerInvariant(),
|
|
'--package-sha256', $PackageSha256.ToLowerInvariant()
|
|
)
|
|
$verificationOutput = @(& $cliPath @arguments 2>&1)
|
|
if ($LASTEXITCODE -ne 0) { throw 'Generated write integration evidence failed CLI verification.' }
|
|
$verificationText = (($verificationOutput | ForEach-Object { [string]$_ }) -join [Environment]::NewLine)
|
|
try { $verification = $verificationText | ConvertFrom-Json }
|
|
catch { throw 'Verifier CLI returned invalid JSON.' }
|
|
$expectedData = @(
|
|
'evidenceType', 'schemaVersion', 'contentSha256', 'workflow', 'moduleCode',
|
|
'erpScope', 'sourceCommit', 'packageSha256', 'runtimeConfigurationSha256',
|
|
'runtimeCli',
|
|
'uatAuthorizationSourceSha256', 'uatAuthorizationContentSha256',
|
|
'uatAuthorizationIdSha256',
|
|
'environmentId', 'testedAtUtc', 'testedBy', 'caseCount', 'verified',
|
|
'registrationReady'
|
|
)
|
|
if (-not (Test-ExactProperties $verification @('ok', 'correlationId', 'data')) -or
|
|
-not (Test-ExactProperties $verification.data $expectedData) -or
|
|
-not (Test-ExactProperties $verification.data.erpScope @(
|
|
'accountBook', 'subSystemId', 'userIdSha256', 'userNameSha256',
|
|
'databaseScopeFingerprint', 'isAdministrator')) -or
|
|
-not (Test-ExactProperties $verification.data.runtimeCli @(
|
|
'fileName', 'version', 'sha256', 'signerThumbprint',
|
|
'requiresElevation', 'bridgeOnly', 'databaseDirectAccess',
|
|
'sessionSource')) -or
|
|
$verification.ok -ne $true -or $verification.data.verified -ne $true -or
|
|
$verification.data.schemaVersion -ne '1.6' -or
|
|
$verification.data.registrationReady -ne $false -or
|
|
$verification.data.contentSha256 -ne $contentHash -or
|
|
$verification.data.workflow -ne $Workflow -or
|
|
$verification.data.moduleCode -ne $ModuleCode -or
|
|
$verification.data.erpScope.accountBook -ne $AccountBook -or
|
|
$verification.data.erpScope.subSystemId -ne $SubSystemId -or
|
|
$verification.data.erpScope.userIdSha256 -cne
|
|
$uatVerification.data.erpScope.userIdSha256 -or
|
|
$verification.data.erpScope.userNameSha256 -cne
|
|
$uatVerification.data.erpScope.userNameSha256 -or
|
|
$verification.data.erpScope.databaseScopeFingerprint -cne
|
|
$uatVerification.data.erpScope.databaseScopeFingerprint -or
|
|
$verification.data.erpScope.isAdministrator -ne
|
|
$uatVerification.data.erpScope.isAdministrator -or
|
|
$verification.data.runtimeConfigurationSha256 -ne $runtimeHash -or
|
|
$verification.data.runtimeCli.fileName -cne 'lserp-agent-cli.exe' -or
|
|
$verification.data.runtimeCli.version -cne $runtimeCliVersion -or
|
|
$verification.data.runtimeCli.sha256 -cne $runtimeCliSha256 -or
|
|
$verification.data.runtimeCli.signerThumbprint -cne $runtimeCliSigner -or
|
|
$verification.data.runtimeCli.requiresElevation -ne $false -or
|
|
$verification.data.runtimeCli.bridgeOnly -ne $true -or
|
|
$verification.data.runtimeCli.databaseDirectAccess -ne $false -or
|
|
$verification.data.runtimeCli.sessionSource -cne
|
|
'current_logged_in_erp_process' -or
|
|
$verification.data.uatAuthorizationSourceSha256 -ne
|
|
$uatVerification.data.sourceSha256 -or
|
|
$verification.data.uatAuthorizationContentSha256 -ne
|
|
$uatVerification.data.contentSha256 -or
|
|
$verification.data.uatAuthorizationIdSha256 -ne
|
|
$uatVerification.data.authorizationIdSha256 -or
|
|
$verification.data.sourceCommit -ne $SourceCommit.ToLowerInvariant() -or
|
|
$verification.data.packageSha256 -ne $PackageSha256.ToLowerInvariant() -or
|
|
[int]$verification.data.caseCount -ne $cases.Count -or
|
|
(Get-Sha256 ([IO.File]::ReadAllBytes($temporary))) -ne $bodyHash) {
|
|
throw 'Verifier CLI response is not bound to the generated evidence.'
|
|
}
|
|
if ((Get-Sha256 ([IO.File]::ReadAllBytes($uatAuthorizationPath))) -ne
|
|
$uatAuthorizationHash -or
|
|
(Get-Sha256 ([IO.File]::ReadAllBytes($runtimePath))) -ne $runtimeHash -or
|
|
(Get-Sha256 ([IO.File]::ReadAllBytes($casesPath))) -ne $casesHash -or
|
|
(Get-Sha256 ([IO.File]::ReadAllBytes($cliPath))) -ne $cliHash) {
|
|
throw 'A locked evidence input changed during final verification.'
|
|
}
|
|
[IO.File]::Move($temporary, $target)
|
|
$published = $true
|
|
}
|
|
finally {
|
|
if (-not $published -and [IO.File]::Exists($temporary)) {
|
|
[IO.File]::Delete($temporary)
|
|
}
|
|
}
|
|
|
|
[ordered]@{
|
|
outputFile = $target
|
|
workflow = $Workflow
|
|
moduleCode = $ModuleCode
|
|
accountBook = $AccountBook
|
|
subSystemId = $SubSystemId
|
|
sourceCommit = $SourceCommit.ToLowerInvariant()
|
|
packageSha256 = $PackageSha256.ToLowerInvariant()
|
|
runtimeConfigurationSha256 = $runtimeHash
|
|
runtimeCliVersion = $runtimeCliVersion
|
|
runtimeCliSha256 = $runtimeCliSha256
|
|
runtimeCliSignerThumbprint = $runtimeCliSigner
|
|
uatAuthorizationSourceSha256 = [string]$uatVerification.data.sourceSha256
|
|
uatAuthorizationContentSha256 = [string]$uatVerification.data.contentSha256
|
|
uatAuthorizationIdSha256 = [string]$uatVerification.data.authorizationIdSha256
|
|
contentSha256 = $contentHash
|
|
caseCount = $cases.Count
|
|
testedAtUtc = $testedAt.ToString('o')
|
|
nextStep = 'Run lserp-cli adapters verify-write-integration-evidence, then sign the workflow acceptance manifest.'
|
|
} | ConvertTo-Json -Depth 5
|