Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/Deployment/Test-WorkflowWriteUatCampaign.ps1
2026-08-14 14:28:28 +08:00

1204 lines
58 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$CampaignFile,
[Parameter(Mandatory = $true)][string]$UatAuthorizationFile,
[Parameter(Mandatory = $true)][string]$UatTokenVaultPath,
[Parameter(Mandatory = $true)][string]$VerifierCliPath,
[Parameter(Mandatory = $true)][string]$RuntimeCliPath,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
[string]$ExpectedUatAuthorizationSha256,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
[string]$ExpectedVerifierCliSha256,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$')]
[string]$ExpectedRuntimeCliVersion,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
[string]$ExpectedRuntimeCliSha256,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
[string]$ExpectedVerifierSignerThumbprint,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
[string]$ExpectedRuntimeSignerThumbprint,
[string]$CaseCatalogFile = (Join-Path $PSScriptRoot `
'workflow-write-uat-case-catalog.v1.json'),
[Parameter(Mandatory = $true)]
[ValidateRange(1, 2147483647)]
[int]$ErpProcessId,
[ValidateRange(1000, 300000)]
[int]$BridgeTimeoutMilliseconds = 180000
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
if ($PSVersionTable.PSVersion -lt [Version]'5.1' -or
[string]$PSVersionTable.PSEdition -ne 'Desktop' -or
[string]::IsNullOrWhiteSpace($env:SystemRoot)) {
throw 'workflow_uat_campaign_check_failed:windows_powershell_51_required'
}
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'workflow_uat_campaign_check_failed:elevated_operator_required'
}
$utf8 = [Text.UTF8Encoding]::new($false, $true)
$maximumResponseCharacters = 4 * 1024 * 1024
$locks = New-Object System.Collections.Generic.List[IO.FileStream]
$expectedCaseCatalogSha256 = `
'23eb6c4f308d4904bf3920ed37499f05521beebde9422026f9732983c16002d5'
function Throw-CampaignCheckError([string]$Code) {
throw ('workflow_uat_campaign_check_failed:' + $Code)
}
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
}
function Assert-NoReparseDirectoryChain([string]$Directory, [string]$Code) {
try {
$current = [IO.DirectoryInfo]::new([IO.Path]::GetFullPath($Directory))
while ($null -ne $current) {
if (-not $current.Exists -or
(($current.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
Throw-CampaignCheckError $Code
}
$current = $current.Parent
}
}
catch {
if ($_.Exception.Message.StartsWith('workflow_uat_campaign_check_failed:')) { throw }
Throw-CampaignCheckError $Code
}
}
function Open-LockedRegularFile(
[string]$Path,
[long]$MaximumBytes,
[string]$ExpectedFileName,
[string]$Code
) {
try {
$full = [IO.Path]::GetFullPath($Path)
if (-not [IO.File]::Exists($full)) { Throw-CampaignCheckError $Code }
$item = Get-Item -LiteralPath $full -Force
if ($item.Length -le 0 -or $item.Length -gt $MaximumBytes -or
(($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) -or
(-not [string]::IsNullOrWhiteSpace($ExpectedFileName) -and
[IO.Path]::GetFileName($full) -cne $ExpectedFileName)) {
Throw-CampaignCheckError $Code
}
Assert-NoReparseDirectoryChain ([IO.Path]::GetDirectoryName($full)) $Code
$stream = [IO.File]::Open(
$full, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
$script:locks.Add($stream)
return [pscustomobject]@{ Path = $full; Stream = $stream }
}
catch {
if ($_.Exception.Message.StartsWith('workflow_uat_campaign_check_failed:')) { throw }
Throw-CampaignCheckError $Code
}
}
function Get-Sha256Hex([byte[]]$Bytes) {
$sha = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant()
}
finally { $sha.Dispose() }
}
function Get-LockedSha256([IO.FileStream]$Stream) {
$sha = [Security.Cryptography.SHA256]::Create()
try {
$Stream.Position = 0
$value = ([BitConverter]::ToString($sha.ComputeHash($Stream))).Replace('-', '').ToLowerInvariant()
$Stream.Position = 0
return $value
}
finally { $sha.Dispose() }
}
function ConvertTo-UnixSeconds([object]$Value) {
$timestamp = ([DateTimeOffset]$Value).ToUniversalTime()
$epoch = [DateTimeOffset]::new(
1970, 1, 1, 0, 0, 0, [TimeSpan]::Zero)
return [int64][Math]::Floor(($timestamp - $epoch).TotalSeconds)
}
function Read-LockedJson([object]$LockedFile, [string]$Code) {
try {
$LockedFile.Stream.Position = 0
$reader = New-Object IO.StreamReader(
$LockedFile.Stream, $utf8, $true, 4096, $true)
try { $text = $reader.ReadToEnd() } finally { $reader.Dispose() }
$LockedFile.Stream.Position = 0
return $text | ConvertFrom-Json
}
catch {
if ($_.Exception.Message.StartsWith('workflow_uat_campaign_check_failed:')) { throw }
Throw-CampaignCheckError $Code
}
}
function Assert-RestrictedAcl([string]$Path, [bool]$IsDirectory, [string]$Code) {
try {
$sections = [Security.AccessControl.AccessControlSections]::All
$acl = if ($IsDirectory) {
[IO.Directory]::GetAccessControl($Path, $sections)
} else { [IO.File]::GetAccessControl($Path, $sections) }
$owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value
$currentSid = $identity.User.Value
$systemSid = [Security.Principal.SecurityIdentifier]::new(
[Security.Principal.WellKnownSidType]::LocalSystemSid, $null).Value
if (-not $acl.AreAccessRulesProtected -or $owner -cne $currentSid) {
Throw-CampaignCheckError $Code
}
$rules = @($acl.GetAccessRules(
$true, $true, [Security.Principal.SecurityIdentifier]))
if ($rules.Count -ne 2) { Throw-CampaignCheckError $Code }
$seen = @{}
foreach ($rule in $rules) {
$sid = [string]$rule.IdentityReference.Value
if ($rule.IsInherited -or
$rule.AccessControlType -ne
[Security.AccessControl.AccessControlType]::Allow -or
$sid -cnotin @($currentSid, $systemSid) -or
(($rule.FileSystemRights -band
[Security.AccessControl.FileSystemRights]::FullControl) -ne
[Security.AccessControl.FileSystemRights]::FullControl) -or
$seen.ContainsKey($sid)) {
Throw-CampaignCheckError $Code
}
$seen[$sid] = $true
}
$sddl = $acl.GetSecurityDescriptorSddlForm($sections)
$integrityPattern = if ($IsDirectory) {
'S:.*\(ML;(?=[A-Z]*OI)(?=[A-Z]*CI)[A-Z]*;NW;;;HI\)'
} else { 'S:.*\(ML;;NW;;;HI\)' }
if (-not $seen.ContainsKey($currentSid) -or
-not $seen.ContainsKey($systemSid) -or
$sddl -cnotmatch $integrityPattern) {
Throw-CampaignCheckError $Code
}
}
catch {
if ($_.Exception.Message.StartsWith('workflow_uat_campaign_check_failed:')) { throw }
Throw-CampaignCheckError $Code
}
}
function ConvertTo-WindowsProcessArgument([string]$Value) {
if ($null -eq $Value -or $Value.Length -eq 0) { return '""' }
if (-not [Text.RegularExpressions.Regex]::IsMatch($Value, '[\s"]')) { return $Value }
$builder = New-Object Text.StringBuilder
[void]$builder.Append([char]34)
$slashes = 0
foreach ($character in $Value.ToCharArray()) {
if ([int]$character -eq 92) { $slashes++; continue }
if ([int]$character -eq 34) {
for ($index = 0; $index -lt (($slashes * 2) + 1); $index++) {
[void]$builder.Append([char]92)
}
[void]$builder.Append([char]34)
}
else {
for ($index = 0; $index -lt $slashes; $index++) {
[void]$builder.Append([char]92)
}
[void]$builder.Append($character)
}
$slashes = 0
}
for ($index = 0; $index -lt ($slashes * 2); $index++) {
[void]$builder.Append([char]92)
}
[void]$builder.Append([char]34)
return $builder.ToString()
}
function Invoke-TrustedCli([string]$CliPath, [string[]]$Arguments, [int]$Timeout) {
$process = New-Object Diagnostics.Process
try {
$start = New-Object Diagnostics.ProcessStartInfo
$start.FileName = $CliPath
$start.WorkingDirectory = [IO.Path]::GetDirectoryName($CliPath)
$start.UseShellExecute = $false
$start.CreateNoWindow = $true
$start.RedirectStandardOutput = $true
$start.RedirectStandardError = $true
$start.RedirectStandardInput = $true
$start.StandardOutputEncoding = $utf8
$start.StandardErrorEncoding = $utf8
$start.Arguments = (($Arguments | ForEach-Object {
ConvertTo-WindowsProcessArgument ([string]$_)
}) -join ' ')
$process.StartInfo = $start
if (-not $process.Start()) { Throw-CampaignCheckError 'cli_process_start_failed' }
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
$process.StandardInput.Close()
if (-not $process.WaitForExit($Timeout)) {
try { $process.Kill() } catch { }
Throw-CampaignCheckError 'cli_timeout'
}
$process.WaitForExit()
$stdout = $stdoutTask.Result
$stderr = $stderrTask.Result
if ($process.ExitCode -ne 0 -or
[string]::IsNullOrWhiteSpace($stdout) -or
-not [string]::IsNullOrWhiteSpace($stderr) -or
$stdout.Length -gt $maximumResponseCharacters) {
Throw-CampaignCheckError 'cli_verification_failed'
}
try { $envelope = $stdout | ConvertFrom-Json }
catch { Throw-CampaignCheckError 'cli_response_invalid' }
if (-not (Test-ExactProperties $envelope @('ok', 'correlationId', 'data')) -or
$envelope.ok -ne $true -or
([string]$envelope.correlationId) -cnotmatch '^[A-Za-z0-9_.:-]{8,128}$' -or
$null -eq $envelope.data) {
Throw-CampaignCheckError 'cli_response_invalid'
}
return $envelope.data
}
finally { $process.Dispose() }
}
function Assert-PathWithin([string]$Path, [string]$Root, [string]$Code) {
$fullPath = [IO.Path]::GetFullPath($Path)
$fullRoot = [IO.Path]::GetFullPath($Root).TrimEnd(
[IO.Path]::DirectorySeparatorChar,
[IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
if (-not $fullPath.StartsWith($fullRoot, [StringComparison]::OrdinalIgnoreCase)) {
Throw-CampaignCheckError $Code
}
}
$purchaseCases = @(
'purchase_unique_match_commit', 'purchase_ambiguous_match_blocked',
'purchase_overallocation_blocked', 'purchase_permission_denied',
'purchase_database_permission_recheck_denied',
'purchase_currency_field_missing_blocked',
'purchase_currency_crosswalk_unapproved_blocked',
'purchase_row_scope_denied', 'purchase_runtime_recheck_blocked',
'purchase_transaction_rollback', 'purchase_idempotency_replay',
'purchase_idempotency_conflict', 'purchase_audit_correlated'
)
$leaveCases = @(
'leave_natural_language_resolution', 'leave_multi_day_calendar_resolution',
'leave_resolution_proof_bypass_blocked', 'leave_ambiguous_type_blocked',
'leave_ambiguous_flow_type_blocked', 'leave_time_segment_required_blocked',
'leave_local_time_zone_rejected', 'leave_other_employee_denied',
'leave_permission_denied', 'leave_database_permission_recheck_denied',
'leave_create_draft_commit', 'leave_submit_separate_confirmation',
'leave_overlap_blocked', 'leave_stale_flow_type_blocked',
'leave_runtime_recheck_blocked', 'leave_transaction_rollback',
'leave_idempotency_replay', 'leave_idempotency_conflict',
'leave_audit_correlated'
)
$executeCases = @(
'purchase_unique_match_commit',
'purchase_database_permission_recheck_denied',
'purchase_currency_field_missing_blocked',
'purchase_currency_crosswalk_unapproved_blocked',
'purchase_row_scope_denied', 'purchase_runtime_recheck_blocked',
'purchase_transaction_rollback', 'purchase_idempotency_replay',
'purchase_idempotency_conflict',
'leave_database_permission_recheck_denied', 'leave_create_draft_commit',
'leave_submit_separate_confirmation', 'leave_stale_flow_type_blocked',
'leave_runtime_recheck_blocked', 'leave_transaction_rollback',
'leave_idempotency_replay', 'leave_idempotency_conflict'
)
$derivedCases = @('purchase_audit_correlated', 'leave_audit_correlated')
$postPlanStagingCases = @(
'purchase_runtime_recheck_blocked',
'leave_stale_flow_type_blocked',
'leave_runtime_recheck_blocked'
)
function Get-Dependency([string]$CaseCode) {
if ($CaseCode -in @(
'purchase_idempotency_replay', 'purchase_idempotency_conflict',
'purchase_audit_correlated')) {
return 'purchase_unique_match_commit'
}
if ($CaseCode -in @(
'leave_submit_separate_confirmation', 'leave_idempotency_replay',
'leave_idempotency_conflict', 'leave_audit_correlated')) {
return 'leave_create_draft_commit'
}
return $null
}
function Get-IdempotencyPolicy([string]$CaseCode, [string]$CaptureMode) {
if ($CaptureMode -eq 'plan_only') { return 'not_applicable' }
if ($CaptureMode -eq 'derived_audit') { return 'derived_no_execute' }
if ($CaseCode.EndsWith('_idempotency_replay', [StringComparison]::Ordinal)) {
return 'reuse_dependency_key_and_input'
}
if ($CaseCode.EndsWith('_idempotency_conflict', [StringComparison]::Ordinal)) {
return 'reuse_dependency_key_with_different_input'
}
return 'new_unique_key'
}
function Test-SafeCatalogText([object]$Value, [int]$MaximumLength) {
if ($null -eq $Value) { return $false }
$text = [string]$Value
if ([string]::IsNullOrWhiteSpace($text) -or
$text.Length -gt $MaximumLength -or
$text -cne $text.Trim() -or
[Text.RegularExpressions.Regex]::IsMatch($text, '[\x00-\x1f\x7f]') -or
[Text.RegularExpressions.Regex]::IsMatch(
$text,
'(?i)(?:https?|jdbc|file)://|\b(?:password|passwd|secret|api[_ -]?key|token)\b|\b(?:\d{1,3}\.){3}\d{1,3}\b|sk-[A-Za-z0-9_-]{8,}|\b(?:insert\s+into|update\s+\S+\s+set|delete\s+from|drop\s+table|truncate\s+table|alter\s+table)\b')) {
return $false
}
return $true
}
function Test-SafeCatalogTextArray(
[object]$Value,
[int]$MinimumCount,
[int]$MaximumCount
) {
if ($null -eq $Value -or -not ($Value -is [Array])) { return $false }
$items = @($Value)
if ($items.Count -lt $MinimumCount -or $items.Count -gt $MaximumCount) {
return $false
}
foreach ($item in $items) {
if (-not (Test-SafeCatalogText $item 500)) { return $false }
}
return $true
}
function Assert-CaseCatalog([object]$Catalog) {
if (-not (Test-ExactProperties $Catalog @(
'schemaVersion', 'packageType', 'safety', 'workflows')) -or
[string]$Catalog.schemaVersion -cne '1.0' -or
[string]$Catalog.packageType -cne 'workflow_write_uat_case_catalog' -or
-not (Test-ExactProperties $Catalog.safety @(
'productionUseProhibited',
'automaticDatabaseOrConfigurationChanges',
'approvedRestorePointRequired', 'containsCredentials',
'containsBusinessIdentifiers', 'executableInstructionsIncluded')) -or
$Catalog.safety.productionUseProhibited -ne $true -or
$Catalog.safety.automaticDatabaseOrConfigurationChanges -ne $false -or
$Catalog.safety.approvedRestorePointRequired -ne $true -or
$Catalog.safety.containsCredentials -ne $false -or
$Catalog.safety.containsBusinessIdentifiers -ne $false -or
$Catalog.safety.executableInstructionsIncluded -ne $false) {
Throw-CampaignCheckError 'case_catalog_safety_invalid'
}
$workflows = @($Catalog.workflows)
if ($workflows.Count -ne 2) {
Throw-CampaignCheckError 'case_catalog_workflow_coverage_invalid'
}
$lookup = @{}
for ($workflowIndex = 0; $workflowIndex -lt 2; $workflowIndex++) {
$workflowName = if ($workflowIndex -eq 0) { 'purchase' } else { 'leave' }
$expectedCases = if ($workflowName -eq 'purchase') {
$purchaseCases
} else { $leaveCases }
$workflow = $workflows[$workflowIndex]
if (-not (Test-ExactProperties $workflow @(
'workflow', 'caseCount', 'cases')) -or
[string]$workflow.workflow -cne $workflowName -or
[int]$workflow.caseCount -ne $expectedCases.Count) {
Throw-CampaignCheckError 'case_catalog_workflow_contract_invalid'
}
$cases = @($workflow.cases)
if ($cases.Count -ne $expectedCases.Count) {
Throw-CampaignCheckError 'case_catalog_case_coverage_invalid'
}
for ($index = 0; $index -lt $expectedCases.Count; $index++) {
$caseCode = $expectedCases[$index]
$case = $cases[$index]
$expectedCommand = Get-ExpectedCommand $caseCode
$expectedCaptureMode = if ($caseCode -in $derivedCases) {
'derived_audit'
} elseif ($caseCode -in $executeCases) {
'execute'
} else { 'plan_only' }
$expectedMutation = if ($caseCode -in @(
'purchase_unique_match_commit', 'leave_create_draft_commit',
'leave_submit_separate_confirmation')) {
'positive'
} else { 'zero' }
$expectedConfirmation = if ($expectedCaptureMode -eq 'derived_audit') {
'inherited_required'
} elseif ($expectedCaptureMode -eq 'execute') {
'required'
} else { 'prohibited' }
$expectedSourceProof = $caseCode -in @(
'purchase_unique_match_commit', 'purchase_idempotency_replay',
'purchase_audit_correlated')
if (-not (Test-ExactProperties $case @(
'sequence', 'caseCode', 'title', 'commandName',
'captureMode', 'expectedResultCode', 'expectedIssueCode',
'expectedMutationPolicy', 'nativeConfirmationPolicy',
'minimumAuditEventCount', 'sourceDocumentProofRequired',
'primaryRole', 'supportingRoles', 'fixtureCode',
'preconditions', 'operatorSteps', 'dbaReadOnlyChecks',
'cleanupSteps', 'retryPolicy')) -or
[int]$case.sequence -ne ($index + 1) -or
[string]$case.caseCode -cne $caseCode -or
[string]$case.commandName -cne $expectedCommand -or
[string]$case.captureMode -cne $expectedCaptureMode -or
([string]$case.expectedResultCode) -cnotmatch
'^[a-z][a-z0-9_]{2,95}$' -or
($null -ne $case.expectedIssueCode -and
([string]$case.expectedIssueCode) -cnotmatch
'^[a-z][a-z0-9_]{2,95}$') -or
[string]$case.expectedMutationPolicy -cne $expectedMutation -or
[string]$case.nativeConfirmationPolicy -cne $expectedConfirmation -or
[int]$case.minimumAuditEventCount -ne
$(if ($expectedCaptureMode -eq 'plan_only') { 1 } else { 2 }) -or
[bool]$case.sourceDocumentProofRequired -ne $expectedSourceProof -or
-not (Test-SafeCatalogText $case.title 120) -or
([string]$case.primaryRole) -cnotmatch '^[a-z][a-z0-9_]{2,63}$' -or
([string]$case.fixtureCode) -cnotmatch '^[a-z][a-z0-9_]{2,95}$' -or
([string]$case.retryPolicy) -cnotin @(
'single_success_then_relationship_cases_only',
'new_capture_allowed_while_authorization_active',
'new_plan_and_new_key_required',
'dependency_key_relationship_required',
'derived_with_dependency_only') -or
-not (Test-SafeCatalogTextArray $case.preconditions 1 12) -or
-not (Test-SafeCatalogTextArray $case.operatorSteps 1 12) -or
-not (Test-SafeCatalogTextArray $case.dbaReadOnlyChecks 1 12) -or
-not (Test-SafeCatalogTextArray $case.cleanupSteps 1 12) -or
$lookup.ContainsKey($caseCode)) {
Throw-CampaignCheckError 'case_catalog_case_contract_invalid'
}
if (-not ($case.supportingRoles -is [Array])) {
Throw-CampaignCheckError 'case_catalog_case_contract_invalid'
}
$roles = @($case.supportingRoles)
if ($roles.Count -lt 1 -or $roles.Count -gt 8 -or
@($roles | Select-Object -Unique).Count -ne $roles.Count) {
Throw-CampaignCheckError 'case_catalog_case_contract_invalid'
}
foreach ($role in $roles) {
if ([string]$role -cnotmatch '^[a-z][a-z0-9_]{2,63}$' -or
[string]$role -ceq [string]$case.primaryRole) {
Throw-CampaignCheckError 'case_catalog_case_contract_invalid'
}
}
$lookup[$caseCode] = $case
}
}
if ($lookup.Count -ne 32) {
Throw-CampaignCheckError 'case_catalog_case_coverage_invalid'
}
return $lookup
}
try {
$caseCatalog = Open-LockedRegularFile `
$CaseCatalogFile (256KB) 'workflow-write-uat-case-catalog.v1.json' `
'case_catalog_file_invalid'
$campaign = Open-LockedRegularFile `
$CampaignFile (2MB) 'campaign.json' 'campaign_file_invalid'
$authorization = Open-LockedRegularFile `
$UatAuthorizationFile (512KB) '' 'uat_authorization_file_invalid'
$vault = Open-LockedRegularFile `
$UatTokenVaultPath (512KB) '' 'uat_token_vault_file_invalid'
$verifierCli = Open-LockedRegularFile `
$VerifierCliPath (128MB) 'lserp-cli.exe' 'verifier_cli_invalid'
$runtimeCli = Open-LockedRegularFile `
$RuntimeCliPath (128MB) 'lserp-agent-cli.exe' 'runtime_cli_invalid'
if ($verifierCli.Path -ieq $runtimeCli.Path) {
Throw-CampaignCheckError 'cli_role_path_conflict'
}
$campaignRoot = [IO.Path]::GetDirectoryName($campaign.Path)
Assert-RestrictedAcl $campaignRoot $true 'campaign_directory_acl_invalid'
Assert-RestrictedAcl $vault.Path $false 'uat_token_vault_acl_invalid'
Assert-PathWithin $campaign.Path $campaignRoot 'campaign_path_invalid'
$campaignPrefix = $campaignRoot.TrimEnd(
[IO.Path]::DirectorySeparatorChar,
[IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
if ($vault.Path.StartsWith($campaignPrefix, [StringComparison]::OrdinalIgnoreCase)) {
Throw-CampaignCheckError 'uat_token_vault_inside_campaign'
}
$caseCatalogHash = Get-LockedSha256 $caseCatalog.Stream
if ($caseCatalogHash -cne $expectedCaseCatalogSha256) {
Throw-CampaignCheckError 'case_catalog_hash_mismatch'
}
$caseCatalogJson = Read-LockedJson $caseCatalog 'case_catalog_json_invalid'
$catalogCaseByCode = Assert-CaseCatalog $caseCatalogJson
$authorizationHash = Get-LockedSha256 $authorization.Stream
$verifierCliHash = Get-LockedSha256 $verifierCli.Stream
$runtimeCliHash = Get-LockedSha256 $runtimeCli.Stream
if ($authorizationHash -cne
$ExpectedUatAuthorizationSha256.ToLowerInvariant()) {
Throw-CampaignCheckError 'uat_authorization_hash_mismatch'
}
if ($verifierCliHash -cne
$ExpectedVerifierCliSha256.ToLowerInvariant()) {
Throw-CampaignCheckError 'verifier_cli_hash_mismatch'
}
if ($runtimeCliHash -cne $ExpectedRuntimeCliSha256.ToLowerInvariant()) {
Throw-CampaignCheckError 'runtime_cli_hash_mismatch'
}
$verifierSignature = Get-AuthenticodeSignature -LiteralPath $verifierCli.Path
$verifierSigner = if ($null -eq $verifierSignature.SignerCertificate) { '' } else {
([string]$verifierSignature.SignerCertificate.Thumbprint).Replace(' ', '').ToUpperInvariant()
}
if ($verifierSignature.Status -ne
[Management.Automation.SignatureStatus]::Valid -or
$verifierSigner -cne
$ExpectedVerifierSignerThumbprint.ToUpperInvariant()) {
Throw-CampaignCheckError 'verifier_cli_signature_invalid'
}
$runtimeSignature = Get-AuthenticodeSignature -LiteralPath $runtimeCli.Path
$runtimeSigner = if ($null -eq $runtimeSignature.SignerCertificate) { '' } else {
([string]$runtimeSignature.SignerCertificate.Thumbprint).Replace(' ', '').ToUpperInvariant()
}
if ($runtimeSignature.Status -ne
[Management.Automation.SignatureStatus]::Valid -or
$runtimeSigner -cne
$ExpectedRuntimeSignerThumbprint.ToUpperInvariant()) {
Throw-CampaignCheckError 'runtime_cli_signature_invalid'
}
$runtimeIdentity = Invoke-TrustedCli $runtimeCli.Path @(
'version', '--correlation-id',
('campaign-runtime-' + [Guid]::NewGuid().ToString('N'))
) $BridgeTimeoutMilliseconds
if (-not (Test-ExactProperties $runtimeIdentity @(
'component', 'version', 'protocolVersion', 'bridgeOnly',
'databaseDirectAccess', 'sessionSource')) -or
[string]$runtimeIdentity.component -cne 'lserp-agent-cli' -or
[string]$runtimeIdentity.version -cne $ExpectedRuntimeCliVersion -or
[string]$runtimeIdentity.protocolVersion -cne '1.0' -or
$runtimeIdentity.bridgeOnly -ne $true -or
$runtimeIdentity.databaseDirectAccess -ne $false -or
[string]$runtimeIdentity.sessionSource -cne
'current_logged_in_erp_process') {
Throw-CampaignCheckError 'runtime_cli_identity_invalid'
}
$verifiedAuthorization = Invoke-TrustedCli $verifierCli.Path @(
'acceptance', 'verify-uat-authorization', '--input', $authorization.Path,
'--correlation-id', ('campaign-auth-' + [Guid]::NewGuid().ToString('N'))
) $BridgeTimeoutMilliseconds
if ([string]$verifiedAuthorization.packageType -cne
'workflow_write_uat_authorization' -or
[string]$verifiedAuthorization.schemaVersion -cne '1.2' -or
[string]$verifiedAuthorization.sourceSha256 -cne $authorizationHash -or
[string]$verifiedAuthorization.runtimeCli.fileName -cne
'lserp-agent-cli.exe' -or
[string]$verifiedAuthorization.runtimeCli.version -cne
$ExpectedRuntimeCliVersion -or
[string]$verifiedAuthorization.runtimeCli.sha256 -cne
$runtimeCliHash -or
([string]$verifiedAuthorization.runtimeCli.signerThumbprint).
ToUpperInvariant() -cne $runtimeSigner -or
$verifiedAuthorization.runtimeCli.requiresElevation -ne $false -or
$verifiedAuthorization.runtimeCli.bridgeOnly -ne $true -or
$verifiedAuthorization.runtimeCli.databaseDirectAccess -ne $false -or
[string]$verifiedAuthorization.runtimeCli.sessionSource -cne
'current_logged_in_erp_process' -or
[string]$verifiedAuthorization.verifierCli.sha256 -cne
$verifierCliHash -or
([string]$verifiedAuthorization.verifierCli.signerThumbprint).ToUpperInvariant() -cne
$verifierSigner -or
$verifiedAuthorization.signatureVerified -ne $true -or
$verifiedAuthorization.uatAuthorized -ne $true -or
$verifiedAuthorization.productionReady -ne $false) {
Throw-CampaignCheckError 'uat_authorization_contract_mismatch'
}
$campaignJson = Read-LockedJson $campaign 'campaign_json_invalid'
if (-not (Test-ExactProperties $campaignJson @(
'schemaVersion', 'contentSha256', 'content')) -or
[string]$campaignJson.schemaVersion -cne '1.1' -or
([string]$campaignJson.contentSha256) -cnotmatch '^[a-f0-9]{64}$') {
Throw-CampaignCheckError 'campaign_schema_invalid'
}
$campaignContent = $campaignJson.content
$campaignContentJson = $campaignContent | ConvertTo-Json -Depth 14 -Compress
if ((Get-Sha256Hex $utf8.GetBytes($campaignContentJson)) -cne
[string]$campaignJson.contentSha256) {
Throw-CampaignCheckError 'campaign_content_hash_mismatch'
}
if (-not (Test-ExactProperties $campaignContent @(
'packageType', 'campaignId', 'createdAtUnixSeconds',
'caseCatalogSha256', 'authorization', 'safety', 'directories',
'workflows', 'productionReady')) -or
[string]$campaignContent.packageType -cne 'workflow_write_uat_campaign' -or
([string]$campaignContent.campaignId) -cnotmatch
'^[A-Za-z0-9][A-Za-z0-9_.-]{7,63}$' -or
[string]$campaignContent.caseCatalogSha256 -cne $caseCatalogHash -or
$campaignContent.productionReady -ne $false -or
-not (Test-ExactProperties $campaignContent.authorization @(
'sourceSha256', 'contentSha256', 'authorizationIdSha256',
'customerId', 'environmentId', 'environmentClass',
'issuedAtUnixSeconds', 'expiresAtUnixSeconds', 'verifierCliSha256',
'verifierCliSignerThumbprint', 'runtimeCliVersion',
'runtimeCliSha256', 'runtimeCliSignerThumbprint', 'userIdSha256',
'userNameSha256', 'databaseScopeFingerprint')) -or
[string]$campaignContent.authorization.sourceSha256 -cne
[string]$verifiedAuthorization.sourceSha256 -or
[string]$campaignContent.authorization.contentSha256 -cne
[string]$verifiedAuthorization.contentSha256 -or
[string]$campaignContent.authorization.authorizationIdSha256 -cne
[string]$verifiedAuthorization.authorizationIdSha256 -or
[string]$campaignContent.authorization.customerId -cne
[string]$verifiedAuthorization.customerId -or
[string]$campaignContent.authorization.environmentId -cne
[string]$verifiedAuthorization.environmentId -or
[string]$campaignContent.authorization.environmentClass -cne 'recoverable_uat' -or
[int64]$campaignContent.authorization.issuedAtUnixSeconds -ne
(ConvertTo-UnixSeconds $verifiedAuthorization.issuedAtUtc) -or
[int64]$campaignContent.authorization.expiresAtUnixSeconds -ne
(ConvertTo-UnixSeconds $verifiedAuthorization.expiresAtUtc) -or
[string]$campaignContent.authorization.verifierCliSha256 -cne
$verifierCliHash -or
([string]$campaignContent.authorization.verifierCliSignerThumbprint).ToUpperInvariant() -cne
$verifierSigner -or
[string]$campaignContent.authorization.runtimeCliVersion -cne
$ExpectedRuntimeCliVersion -or
[string]$campaignContent.authorization.runtimeCliSha256 -cne
$runtimeCliHash -or
([string]$campaignContent.authorization.runtimeCliSignerThumbprint).
ToUpperInvariant() -cne $runtimeSigner -or
[string]$campaignContent.authorization.runtimeCliVersion -cne
[string]$verifiedAuthorization.runtimeCli.version -or
[string]$campaignContent.authorization.runtimeCliSha256 -cne
[string]$verifiedAuthorization.runtimeCli.sha256 -or
([string]$campaignContent.authorization.runtimeCliSignerThumbprint).
ToUpperInvariant() -cne
([string]$verifiedAuthorization.runtimeCli.signerThumbprint).
ToUpperInvariant() -or
[string]$campaignContent.authorization.userIdSha256 -cne
[string]$verifiedAuthorization.erpScope.userIdSha256 -or
[string]$campaignContent.authorization.userNameSha256 -cne
[string]$verifiedAuthorization.erpScope.userNameSha256 -or
[string]$campaignContent.authorization.databaseScopeFingerprint -cne
[string]$verifiedAuthorization.erpScope.databaseScopeFingerprint) {
Throw-CampaignCheckError 'campaign_authorization_mismatch'
}
$nowUnixSeconds = ConvertTo-UnixSeconds ([DateTimeOffset]::UtcNow)
if ([int64]$campaignContent.createdAtUnixSeconds -lt
[int64]$campaignContent.authorization.issuedAtUnixSeconds -or
[int64]$campaignContent.createdAtUnixSeconds -gt
[int64]$campaignContent.authorization.expiresAtUnixSeconds -or
[int64]$campaignContent.createdAtUnixSeconds -gt ($nowUnixSeconds + 300)) {
Throw-CampaignCheckError 'campaign_created_time_invalid'
}
if (-not (Test-ExactProperties $campaignContent.safety @(
'productionUseProhibited', 'automaticDatabaseWrites',
'oneCaseAtATime', 'operatorStagingRequired',
'authorizationReverificationRequiredBeforeResume',
'tokenMaterialIncluded', 'idempotencyMaterialIncluded',
'privateBusinessInputIncluded',
'resumeStateDerivedOnlyFromEvidence')) -or
$campaignContent.safety.productionUseProhibited -ne $true -or
$campaignContent.safety.automaticDatabaseWrites -ne $false -or
$campaignContent.safety.oneCaseAtATime -ne $true -or
$campaignContent.safety.operatorStagingRequired -ne $true -or
$campaignContent.safety.authorizationReverificationRequiredBeforeResume -ne $true -or
$campaignContent.safety.tokenMaterialIncluded -ne $false -or
$campaignContent.safety.idempotencyMaterialIncluded -ne $false -or
$campaignContent.safety.privateBusinessInputIncluded -ne $false -or
$campaignContent.safety.resumeStateDerivedOnlyFromEvidence -ne $true -or
-not (Test-ExactProperties $campaignContent.directories @(
'privateInput', 'evidence')) -or
[string]$campaignContent.directories.privateInput -cne 'private-input' -or
[string]$campaignContent.directories.evidence -cne 'evidence') {
Throw-CampaignCheckError 'campaign_safety_invalid'
}
$authorizationJson = Read-LockedJson $authorization 'uat_authorization_json_invalid'
$vaultJson = Read-LockedJson $vault 'uat_token_vault_json_invalid'
if ($null -eq $authorizationJson.content -or
-not (Test-ExactProperties $authorizationJson.content.erpScope @(
'accountBook', 'subSystemId', 'userId', 'userName',
'databaseScopeFingerprint'))) {
Throw-CampaignCheckError 'uat_authorization_scope_invalid'
}
$signedScope = $authorizationJson.content.erpScope
foreach ($scopeField in @(
'accountBook', 'subSystemId', 'userId', 'userName')) {
$scopeValue = [string]$signedScope.$scopeField
if ([string]::IsNullOrWhiteSpace($scopeValue) -or
$scopeValue.Length -gt 256 -or
$scopeValue -cne $scopeValue.Trim()) {
Throw-CampaignCheckError 'uat_authorization_scope_invalid'
}
foreach ($character in $scopeValue.ToCharArray()) {
if ([char]::IsControl($character)) {
Throw-CampaignCheckError 'uat_authorization_scope_invalid'
}
}
}
if (([string]$signedScope.databaseScopeFingerprint).ToLowerInvariant() -cne
[string]$verifiedAuthorization.erpScope.databaseScopeFingerprint -or
(Get-Sha256Hex ($utf8.GetBytes(
[string]$signedScope.accountBook))) -cne
[string]$verifiedAuthorization.erpScope.accountBookSha256 -or
(Get-Sha256Hex ($utf8.GetBytes(
[string]$signedScope.subSystemId))) -cne
[string]$verifiedAuthorization.erpScope.subSystemIdSha256 -or
(Get-Sha256Hex ($utf8.GetBytes(
[string]$signedScope.userId))) -cne
[string]$verifiedAuthorization.erpScope.userIdSha256 -or
(Get-Sha256Hex ($utf8.GetBytes(
[string]$signedScope.userName))) -cne
[string]$verifiedAuthorization.erpScope.userNameSha256) {
Throw-CampaignCheckError 'uat_authorization_scope_invalid'
}
if (-not (Test-ExactProperties $vaultJson @(
'schemaVersion', 'authorizationId', 'protectedForUserSid',
'protectionScope', 'createdAtUtc', 'entries')) -or
[string]$vaultJson.schemaVersion -cne '1.0' -or
[string]$vaultJson.authorizationId -cne
[string]$verifiedAuthorization.authorizationId -or
[string]$vaultJson.protectedForUserSid -cne $identity.User.Value -or
[string]$vaultJson.protectionScope -cne
'dpapi_current_user_high_integrity') {
Throw-CampaignCheckError 'uat_token_vault_contract_invalid'
}
$signedCaseByCode = @{}
foreach ($workflow in @($authorizationJson.content.workflows)) {
foreach ($case in @($workflow.cases)) {
$caseCode = [string]$case.caseCode
if ($signedCaseByCode.ContainsKey($caseCode) -or
([string]$case.tokenSha256) -cnotmatch '^[a-f0-9]{64}$') {
Throw-CampaignCheckError 'uat_authorization_case_contract_invalid'
}
$signedCaseByCode[$caseCode] = [pscustomobject]@{
Workflow = [string]$workflow.workflow
CommandName = [string]$case.expectedCommandName
TokenSha256 = [string]$case.tokenSha256
}
}
}
$vaultKeys = @{}
foreach ($entry in @($vaultJson.entries)) {
if (-not (Test-ExactProperties $entry @(
'workflow', 'caseCode', 'protectedTokenBase64')) -or
([string]$entry.protectedTokenBase64) -cnotmatch
'^[A-Za-z0-9+/]{64,4096}={0,2}$') {
Throw-CampaignCheckError 'uat_token_vault_entry_invalid'
}
$key = [string]$entry.workflow + '|' + [string]$entry.caseCode
if ($vaultKeys.ContainsKey($key)) {
Throw-CampaignCheckError 'uat_token_vault_entry_invalid'
}
$vaultKeys[$key] = $true
}
if ($vaultKeys.Count -ne $signedCaseByCode.Count) {
Throw-CampaignCheckError 'uat_token_vault_coverage_invalid'
}
foreach ($code in $signedCaseByCode.Keys) {
$signed = $signedCaseByCode[$code]
if (-not $vaultKeys.ContainsKey($signed.Workflow + '|' + $code)) {
Throw-CampaignCheckError 'uat_token_vault_coverage_invalid'
}
}
$health = Invoke-TrustedCli $runtimeCli.Path @(
'bridge', 'health', '--erp-process-id', [string]$ErpProcessId,
'--expected-database-scope-fingerprint',
([string]$signedScope.databaseScopeFingerprint).ToLowerInvariant(),
'--expected-user-id', [string]$signedScope.userId,
'--expected-user-name', [string]$signedScope.userName,
'--expected-account-book', [string]$signedScope.accountBook,
'--expected-subsystem-id', [string]$signedScope.subSystemId,
'--expected-is-administrator', 'false',
'--timeout-ms', [string]$BridgeTimeoutMilliseconds,
'--correlation-id', ('campaign-health-' + [Guid]::NewGuid().ToString('N'))
) ([Math]::Min(330000, $BridgeTimeoutMilliseconds + 30000))
$healthUat = $health.workflowUat
if ($null -eq $healthUat -or $healthUat.enabled -ne $true -or
[string]$healthUat.sourceSha256 -cne $authorizationHash -or
[string]$healthUat.authorizationIdSha256 -cne
[string]$verifiedAuthorization.authorizationIdSha256 -or
$healthUat.generalCapabilitiesHidden -ne $true) {
Throw-CampaignCheckError 'bridge_uat_authorization_mismatch'
}
$privateInputDirectory = Join-Path $campaignRoot 'private-input'
$evidenceDirectory = Join-Path $campaignRoot 'evidence'
if (-not [IO.Directory]::Exists($privateInputDirectory) -or
-not [IO.Directory]::Exists($evidenceDirectory)) {
Throw-CampaignCheckError 'campaign_directory_layout_invalid'
}
Assert-NoReparseDirectoryChain $privateInputDirectory 'campaign_directory_layout_invalid'
Assert-NoReparseDirectoryChain $evidenceDirectory 'campaign_directory_layout_invalid'
$rootItems = @(Get-ChildItem -LiteralPath $campaignRoot -Force)
if ($rootItems.Count -ne 3) {
Throw-CampaignCheckError 'campaign_directory_layout_invalid'
}
foreach ($item in $rootItems) {
$validRootItem = ($item.Name -ceq 'campaign.json' -and -not $item.PSIsContainer) -or
($item.Name -cin @('private-input', 'evidence') -and $item.PSIsContainer)
if (-not $validRootItem -or
(($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
Throw-CampaignCheckError 'campaign_directory_layout_invalid'
}
}
$expectedInputNames = @{}
$expectedEvidenceNames = @{}
$allowedEvidenceNames = @{}
$completedCodes = @{}
$workflowStatuses = New-Object System.Collections.Generic.List[object]
$nextCase = $null
$campaignCodes = @{}
$workflows = @($campaignContent.workflows)
if ($workflows.Count -lt 1 -or $workflows.Count -gt 2) {
Throw-CampaignCheckError 'campaign_workflow_coverage_invalid'
}
foreach ($workflow in $workflows) {
if (-not (Test-ExactProperties $workflow @(
'workflow', 'moduleCode', 'caseCount', 'caseIndexFile',
'assembledCasesFile', 'cases'))) {
Throw-CampaignCheckError 'campaign_workflow_schema_invalid'
}
$workflowName = [string]$workflow.workflow
$expectedCases = if ($workflowName -eq 'purchase') {
$purchaseCases
} elseif ($workflowName -eq 'leave') {
$leaveCases
} else { Throw-CampaignCheckError 'campaign_workflow_coverage_invalid' }
$authorizedWorkflow = @($verifiedAuthorization.workflows | Where-Object {
[string]$_.workflow -ceq $workflowName
})
if ($authorizedWorkflow.Count -ne 1 -or
[string]$workflow.moduleCode -cne
[string]$authorizedWorkflow[0].moduleCode -or
[int]$authorizedWorkflow[0].caseCount -ne $expectedCases.Count) {
Throw-CampaignCheckError 'campaign_workflow_authorization_mismatch'
}
if ([int]$workflow.caseCount -ne $expectedCases.Count -or
[string]$workflow.caseIndexFile -cne
('evidence/' + $workflowName + '-index.json') -or
[string]$workflow.assembledCasesFile -cne
('evidence/' + $workflowName + '-cases.json')) {
Throw-CampaignCheckError 'campaign_workflow_contract_invalid'
}
$indexPath = Join-Path $evidenceDirectory ($workflowName + '-index.json')
$indexFile = Open-LockedRegularFile `
$indexPath (64KB) ($workflowName + '-index.json') `
'campaign_case_index_invalid'
$indexJson = Read-LockedJson $indexFile 'campaign_case_index_invalid'
if (-not (Test-ExactProperties $indexJson @(
'schemaVersion', 'workflow', 'caseFiles')) -or
[string]$indexJson.schemaVersion -cne '1.0' -or
[string]$indexJson.workflow -cne $workflowName) {
Throw-CampaignCheckError 'campaign_case_index_invalid'
}
$indexCaseFiles = @($indexJson.caseFiles)
if ($indexCaseFiles.Count -ne $expectedCases.Count) {
Throw-CampaignCheckError 'campaign_case_index_invalid'
}
for ($caseIndex = 0; $caseIndex -lt $expectedCases.Count; $caseIndex++) {
if ([string]$indexCaseFiles[$caseIndex] -cne
($expectedCases[$caseIndex] + '.json')) {
Throw-CampaignCheckError 'campaign_case_index_invalid'
}
}
$cases = @($workflow.cases)
if ($cases.Count -ne $expectedCases.Count) {
Throw-CampaignCheckError 'campaign_case_coverage_invalid'
}
$allowedEvidenceNames[$workflowName + '-index.json'] = $true
$allowedEvidenceNames[$workflowName + '-cases.json'] = $true
$completedInWorkflow = 0
for ($index = 0; $index -lt $expectedCases.Count; $index++) {
$case = $cases[$index]
$caseCode = $expectedCases[$index]
$catalogCase = $catalogCaseByCode[$caseCode]
$expectedCaptureMode = if ($caseCode -in $derivedCases) {
'derived_audit'
} elseif ($caseCode -in $executeCases) {
'execute'
} else { 'plan_only' }
$expectedDependency = Get-Dependency $caseCode
$expectedAuditOutput = if ($caseCode -eq 'purchase_unique_match_commit') {
'purchase_audit_correlated.json'
} elseif ($caseCode -eq 'leave_create_draft_commit') {
'leave_audit_correlated.json'
} else { $null }
$expectedOperatorStage = if ($expectedCaptureMode -eq 'derived_audit') {
'derived_from_dependency'
} elseif ($caseCode -in $postPlanStagingCases) {
'post_plan_change_required'
} elseif ($caseCode.EndsWith('_transaction_rollback', [StringComparison]::Ordinal)) {
'controlled_failure_fixture_required'
} elseif ($null -ne $expectedDependency) {
'relationship_fixture_required'
} else { 'case_fixture_required' }
$expectedMutation = if ($caseCode -in @(
'purchase_unique_match_commit', 'leave_create_draft_commit',
'leave_submit_separate_confirmation')) {
'positive'
} else { 'zero' }
if (-not (Test-ExactProperties $case @(
'sequence', 'caseCode', 'commandName', 'captureMode',
'execute', 'pauseAfterPlanForOperatorStaging',
'operatorStage', 'dependencyCaseCode',
'idempotencyPolicy', 'inputFile', 'evidenceFile',
'correlatedAuditEvidenceFile',
'requiresDbaReadOnlyObservation',
'expectedBusinessMutation')) -or
[int]$case.sequence -ne ($index + 1) -or
[string]$case.caseCode -cne $caseCode -or
[string]$case.evidenceFile -cne ($caseCode + '.json') -or
$case.requiresDbaReadOnlyObservation -ne $true -or
-not $signedCaseByCode.ContainsKey($caseCode) -or
[string]$case.commandName -cne
[string]$signedCaseByCode[$caseCode].CommandName -or
[string]$case.commandName -cne [string]$catalogCase.commandName -or
[string]$case.captureMode -cne $expectedCaptureMode -or
[string]$case.captureMode -cne [string]$catalogCase.captureMode -or
[bool]$case.execute -ne ($expectedCaptureMode -eq 'execute') -or
[bool]$case.pauseAfterPlanForOperatorStaging -ne
($caseCode -in $postPlanStagingCases) -or
[string]$case.operatorStage -cne $expectedOperatorStage -or
(($null -eq $case.dependencyCaseCode) -ne
($null -eq $expectedDependency)) -or
($null -ne $expectedDependency -and
[string]$case.dependencyCaseCode -cne $expectedDependency) -or
[string]$case.idempotencyPolicy -cne
(Get-IdempotencyPolicy $caseCode $expectedCaptureMode) -or
(($null -eq $case.correlatedAuditEvidenceFile) -ne
($null -eq $expectedAuditOutput)) -or
($null -ne $expectedAuditOutput -and
[string]$case.correlatedAuditEvidenceFile -cne
$expectedAuditOutput) -or
[string]$case.expectedBusinessMutation -cne $expectedMutation -or
[string]$case.expectedBusinessMutation -cne
[string]$catalogCase.expectedMutationPolicy -or
$campaignCodes.ContainsKey($caseCode)) {
Throw-CampaignCheckError 'campaign_case_contract_invalid'
}
$campaignCodes[$caseCode] = $true
$evidenceName = [string]$case.evidenceFile
$expectedEvidenceNames[$evidenceName] = $true
$allowedEvidenceNames[$evidenceName] = $true
if ($null -ne $case.inputFile) {
$expectedInput = 'private-input/' + $caseCode + '.json'
if ([string]$case.inputFile -cne $expectedInput) {
Throw-CampaignCheckError 'campaign_case_input_invalid'
}
$expectedInputNames[$caseCode + '.json'] = $true
}
elseif ($expectedCaptureMode -cne 'derived_audit') {
Throw-CampaignCheckError 'campaign_case_input_invalid'
}
$evidencePath = Join-Path $evidenceDirectory $evidenceName
if ([IO.File]::Exists($evidencePath)) {
$evidence = Open-LockedRegularFile `
$evidencePath (256KB) $evidenceName 'campaign_evidence_file_invalid'
$verifiedCase = Invoke-TrustedCli $verifierCli.Path @(
'adapters', 'verify-write-observation', '--input', $evidence.Path,
'--correlation-id', ('campaign-case-' + [Guid]::NewGuid().ToString('N'))
) $BridgeTimeoutMilliseconds
$signedCase = $signedCaseByCode[$caseCode]
$observedAtUnixSeconds = ConvertTo-UnixSeconds `
$verifiedCase.observedAtUtc
if ([string]$verifiedCase.packageType -cne
'workflow_write_case_observation' -or
[string]$verifiedCase.caseCode -cne $caseCode -or
[string]$verifiedCase.commandName -cne [string]$case.commandName -or
[string]$verifiedCase.uatAuthorizationSourceSha256 -cne
$authorizationHash -or
[string]$verifiedCase.uatAuthorizationContentSha256 -cne
[string]$verifiedAuthorization.contentSha256 -or
[string]$verifiedCase.uatAuthorizationIdSha256 -cne
[string]$verifiedAuthorization.authorizationIdSha256 -or
[string]$verifiedCase.uatTokenSha256 -cne
[string]$signedCase.TokenSha256 -or
[string]$verifiedCase.schemaVersion -cne '1.1' -or
[string]$verifiedCase.runtimeCliVersion -cne
$ExpectedRuntimeCliVersion -or
[string]$verifiedCase.runtimeCliSha256 -cne
$runtimeCliHash -or
([string]$verifiedCase.runtimeCliSignerThumbprint).
ToUpperInvariant() -cne $runtimeSigner -or
$observedAtUnixSeconds -lt
[int64]$campaignContent.authorization.issuedAtUnixSeconds -or
$observedAtUnixSeconds -gt
[int64]$campaignContent.authorization.expiresAtUnixSeconds -or
$verifiedCase.semanticsVerified -ne $true -or
$verifiedCase.rawIdentifiersEmitted -ne $false -or
$verifiedCase.productionReady -ne $false) {
Throw-CampaignCheckError 'campaign_evidence_binding_invalid'
}
$completedCodes[$caseCode] = $true
$completedInWorkflow++
}
}
$commitCode = if ($workflowName -eq 'purchase') {
'purchase_unique_match_commit'
} else { 'leave_create_draft_commit' }
$auditCode = if ($workflowName -eq 'purchase') {
'purchase_audit_correlated'
} else { 'leave_audit_correlated' }
if ($completedCodes.ContainsKey($commitCode) -ne
$completedCodes.ContainsKey($auditCode)) {
Throw-CampaignCheckError 'campaign_commit_audit_pair_incomplete'
}
$complete = $completedInWorkflow -eq $expectedCases.Count
if ($complete) {
$setVerification = Invoke-TrustedCli $verifierCli.Path @(
'adapters', 'verify-write-observations', '--input', $indexFile.Path,
'--correlation-id', ('campaign-set-' + [Guid]::NewGuid().ToString('N'))
) $BridgeTimeoutMilliseconds
if ([string]$setVerification.schemaVersion -cne '1.1' -or
[string]$setVerification.workflow -cne $workflowName -or
[int]$setVerification.caseCount -ne $expectedCases.Count -or
[string]$setVerification.runtimeCliVersion -cne
$ExpectedRuntimeCliVersion -or
[string]$setVerification.runtimeCliSha256 -cne
$runtimeCliHash -or
([string]$setVerification.runtimeCliSignerThumbprint).
ToUpperInvariant() -cne $runtimeSigner -or
$setVerification.coverageVerified -ne $true -or
$setVerification.crossCaseRelationshipsVerified -ne $true) {
Throw-CampaignCheckError 'campaign_case_set_invalid'
}
}
$workflowStatuses.Add([pscustomobject][ordered]@{
workflow = $workflowName
requiredCaseCount = $expectedCases.Count
completedCaseCount = $completedInWorkflow
complete = $complete
crossCaseRelationshipsVerified = $complete
})
}
if ($workflows.Count -ne @($verifiedAuthorization.workflows).Count -or
$campaignCodes.Count -ne $signedCaseByCode.Count) {
Throw-CampaignCheckError 'campaign_case_coverage_invalid'
}
foreach ($file in @(Get-ChildItem -LiteralPath $evidenceDirectory -Force)) {
if ($file.PSIsContainer -or
(($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) -or
-not $allowedEvidenceNames.ContainsKey($file.Name)) {
Throw-CampaignCheckError 'campaign_evidence_directory_contains_unknown_file'
}
}
foreach ($file in @(Get-ChildItem -LiteralPath $privateInputDirectory -Force)) {
if ($file.PSIsContainer -or
(($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) -or
-not $expectedInputNames.ContainsKey($file.Name) -or
$file.Length -le 0 -or $file.Length -gt 512KB) {
Throw-CampaignCheckError 'campaign_private_input_invalid'
}
}
foreach ($workflow in $workflows) {
foreach ($case in @($workflow.cases)) {
$caseCode = [string]$case.caseCode
if ($completedCodes.ContainsKey($caseCode)) { continue }
$dependency = if ($null -eq $case.dependencyCaseCode) {
$null
} else { [string]$case.dependencyCaseCode }
if ($null -ne $dependency -and
-not $completedCodes.ContainsKey($dependency)) {
continue
}
if ([string]$case.captureMode -ceq 'derived_audit') {
Throw-CampaignCheckError 'campaign_derived_audit_missing'
}
$inputPath = Join-Path $campaignRoot ([string]$case.inputFile)
$catalogCase = $catalogCaseByCode[$caseCode]
$nextCase = [pscustomobject][ordered]@{
workflow = [string]$workflow.workflow
caseCode = $caseCode
commandName = [string]$case.commandName
captureMode = [string]$case.captureMode
inputFile = [string]$case.inputFile
inputReady = [IO.File]::Exists($inputPath)
evidenceFile = 'evidence/' + [string]$case.evidenceFile
correlatedAuditEvidenceFile = $case.correlatedAuditEvidenceFile
execute = [bool]$case.execute
pauseAfterPlanForOperatorStaging =
[bool]$case.pauseAfterPlanForOperatorStaging
operatorStage = [string]$case.operatorStage
idempotencyPolicy = [string]$case.idempotencyPolicy
requiresDbaReadOnlyObservation = $true
operatorGuide = [pscustomobject][ordered]@{
title = [string]$catalogCase.title
expectedResultCode = [string]$catalogCase.expectedResultCode
expectedIssueCode = $catalogCase.expectedIssueCode
expectedMutationPolicy =
[string]$catalogCase.expectedMutationPolicy
nativeConfirmationPolicy =
[string]$catalogCase.nativeConfirmationPolicy
minimumAuditEventCount =
[int]$catalogCase.minimumAuditEventCount
sourceDocumentProofRequired =
[bool]$catalogCase.sourceDocumentProofRequired
primaryRole = [string]$catalogCase.primaryRole
supportingRoles = @($catalogCase.supportingRoles)
fixtureCode = [string]$catalogCase.fixtureCode
preconditions = @($catalogCase.preconditions)
operatorSteps = @($catalogCase.operatorSteps)
dbaReadOnlyChecks = @($catalogCase.dbaReadOnlyChecks)
cleanupSteps = @($catalogCase.cleanupSteps)
retryPolicy = [string]$catalogCase.retryPolicy
}
}
break
}
if ($null -ne $nextCase) { break }
}
$completedCount = $completedCodes.Count
$requiredCount = $campaignCodes.Count
$complete = $completedCount -eq $requiredCount
if (-not $complete -and $null -eq $nextCase) {
Throw-CampaignCheckError 'campaign_dependency_deadlock'
}
[pscustomobject][ordered]@{
packageType = 'workflow_write_uat_campaign_checkpoint'
schemaVersion = '1.1'
campaignId = [string]$campaignContent.campaignId
campaignContentSha256 = [string]$campaignJson.contentSha256
caseCatalogSha256 = $caseCatalogHash
authorizationSignatureVerified = $true
authorizationActive = $true
verifierCliSha256 = $verifierCliHash
runtimeCliVersion = $ExpectedRuntimeCliVersion
runtimeCliSha256 = $runtimeCliHash
runtimeCliSignerThumbprint = $runtimeSigner
bridgeAuthorizationMatched = $true
tokenVaultAclAndCoverageVerified = $true
productionUseProhibited = $true
automaticDatabaseWrites = $false
completedCaseCount = $completedCount
requiredCaseCount = $requiredCount
workflows = @($workflowStatuses)
nextCase = $nextCase
complete = $complete
readyForNextCase = (-not $complete -and $nextCase.inputReady)
productionReady = $false
} | ConvertTo-Json -Depth 10
}
finally {
foreach ($lock in @($locks)) {
if ($null -ne $lock) { $lock.Dispose() }
}
}