Files
minerops/agents/windows/tests/Test-MinerOpsCredentialTransition.ps1
T

2016 lines
85 KiB
PowerShell

$ErrorActionPreference = "Stop"
$root = Split-Path $PSScriptRoot -Parent
$scriptPath = Join-Path $root "Upgrade-MinerOpsAgentCredential.ps1"
$tokens = $null
$parseErrors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$scriptPath,
[ref]$tokens,
[ref]$parseErrors
) | Out-Null
if ($parseErrors.Count -gt 0) {
throw "Credential transition parser errors: $($parseErrors.Message -join '; ')"
}
$source = Get-Content -LiteralPath $scriptPath -Raw
function Assert-True {
param(
[Parameter(Mandatory = $true)][bool]$Condition,
[Parameter(Mandatory = $true)][string]$Message
)
if (-not $Condition) { throw $Message }
}
function Assert-SourceContains {
param(
[Parameter(Mandatory = $true)][string]$Pattern,
[Parameter(Mandatory = $true)][string]$Message
)
if ($source -notmatch $Pattern) { throw $Message }
}
function Assert-Throws {
param(
[Parameter(Mandatory = $true)][scriptblock]$Action,
[string]$Pattern = ""
)
try {
& $Action | Out-Null
}
catch {
if ($Pattern -and $_.Exception.Message -notmatch $Pattern) {
throw "Expected error '$Pattern', got '$($_.Exception.Message)'."
}
return
}
throw "Expected action to fail."
}
$script:Passed = 0
function Invoke-BehaviorTest {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][scriptblock]$Action
)
& $Action
$script:Passed++
Write-Host "PASS: $Name"
}
# Dot-sourcing loads functions without executing the real transition.
. $scriptPath
function Get-SimPathKey {
param([Parameter(Mandatory = $true)][string]$Path)
return [IO.Path]::GetFullPath($Path)
}
function New-TransitionSimulation {
param([Parameter(Mandatory = $true)][bool]$OriginallyRunning)
$testRoot = Join-Path ([IO.Path]::GetTempPath()) (
"minerops-transition-test-" + [guid]::NewGuid().ToString("N")
)
$targetRoot = Join-Path $testRoot "agent"
$packageRoot = Join-Path $testRoot "package"
[void](New-Item -ItemType Directory -Path $targetRoot, $packageRoot)
foreach ($name in $RequiredFiles) {
[IO.File]::WriteAllText((Join-Path $packageRoot $name), "test`n")
}
$config = [ordered]@{
hub_url = "https://hub.example.test"
agent_id = "00000000-0000-4000-8000-000000000111"
site_id = "00000000-0000-4000-8000-000000000222"
agent_zone_id = "00000000-0000-4000-8000-000000000333"
site_name = "Test Site"
agent_secret = "legacy-shared-secret"
api3_account = "test-account"
api3_password = "test-password"
poll_seconds = 30
}
$configPath = Join-Path $targetRoot "agent-config.json"
[IO.File]::WriteAllText(
$configPath,
($config | ConvertTo-Json -Depth 20) + [Environment]::NewLine
)
$token = "transition-token-123456789012345678901234567890"
$enrollment = [ordered]@{
workflow = "existing_agent_credential_transition"
hub_url = "https://hub.example.test"
token = $token
}
[IO.File]::WriteAllText(
(Join-Path $packageRoot "enrollment.json"),
($enrollment | ConvertTo-Json) + [Environment]::NewLine
)
$script:Sim = @{
Root = $testRoot
Target = $targetRoot
Package = $packageRoot
Config = $configPath
Recovery = Join-Path $targetRoot "agent-credential-transition.recovery.json"
Pending = Join-Path $targetRoot "agent-config.json.pending"
Prior = Join-Path $targetRoot "agent-config.pre-credential-transition.json"
Enrollment = Join-Path $packageRoot "enrollment.json"
Token = $token
Secure = @{}
Events = [System.Collections.Generic.List[string]]::new()
Redemptions = [System.Collections.Generic.List[object]]::new()
TaskRunning = $OriginallyRunning
TaskVerificationFailureAfterCommit = $false
ResponseMode = "success"
SwapMode = "success"
FailAclLeaf = ""
FailTaskRestore = $false
InstallerFailure = $false
TaskDefinitionInvalid = $false
TaskStateOverride = $null
CleanupSafe = $false
SwapCalls = 0
DirectoryTrusted = $true
DirectoryTrustedAtDiscovery = $true
TrustEpochBlocked = $false
TrustResetAuthorized = $false
TrustResetRuntimePublished = $false
TrustResetOriginalTaskRunning = $null
BootChanged = $false
TrustBlocker = Join-Path $testRoot `
"agent.minerops-filesystem-trust.blocked-simulated.state"
DirectoryReparse = $false
AttemptSwap = $false
}
$script:Sim.Secure[(Get-SimPathKey -Path $configPath)] = $false
Set-TransitionSimulationMocks
return $script:Sim
}
function Remove-TransitionSimulation {
if ($null -ne $script:Sim -and (Test-Path -LiteralPath $script:Sim.Root)) {
Remove-Item -LiteralPath $script:Sim.Root -Recurse -Force
}
$script:Sim = $null
}
function Set-TransitionSimulationMocks {
function script:Initialize-MinerOpsCredentialDirectory {
param(
[Parameter(Mandatory = $true)][string]$Path,
[scriptblock]$CheckpointAction,
[switch]$ApproveFilesystemTrustResetAfterReboot
)
if ($script:Sim.DirectoryReparse) {
throw "Agent credential directory path contains a reparse point."
}
$wasTrusted = $script:Sim.DirectoryTrusted
$script:Sim.DirectoryTrustedAtDiscovery = $wasTrusted
if (-not $wasTrusted) {
if (-not $script:Sim.TrustEpochBlocked) {
[IO.File]::WriteAllText($script:Sim.TrustBlocker, "blocked")
$script:Sim.TrustEpochBlocked = $true
Invoke-MinerOpsTransitionCheckpoint `
-Name "directory-repair-blocker-written" `
-Action $CheckpointAction
}
$script:Sim.Events.Add("repair-directory")
$script:Sim.DirectoryTrusted = $true
Invoke-MinerOpsTransitionCheckpoint `
-Name "directory-trust-established" `
-Action $CheckpointAction
}
$newlyBlocked = -not $wasTrusted
$approved = $false
if ($script:Sim.TrustEpochBlocked -and -not $newlyBlocked) {
if (-not $ApproveFilesystemTrustResetAfterReboot) {
throw "filesystem trust epoch is blocked"
}
if (-not $script:Sim.BootChanged) {
throw "without a verified reboot"
}
$approved = $true
}
return [pscustomobject]@{
Path = [IO.Path]::GetFullPath($Path)
WasTrustedAtDiscovery = $wasTrusted
WasRepaired = -not $wasTrusted
TrustEpochBlocked = $script:Sim.TrustEpochBlocked
TrustEpochNewlyBlocked = $newlyBlocked
TrustResetApproved = $approved
TrustResetAuthorized = $script:Sim.TrustResetAuthorized
TrustResetRuntimePublished = `
$script:Sim.TrustResetRuntimePublished
TrustResetOriginalTaskRunning = `
$script:Sim.TrustResetOriginalTaskRunning
TrustBlockerPaths = @($script:Sim.TrustBlocker)
}
}
function script:Assert-MinerOpsCredentialDirectory {
param([Parameter(Mandatory = $true)][string]$Path)
if ($script:Sim.DirectoryReparse) {
throw "Agent credential directory path contains a reparse point."
}
if (-not $script:Sim.DirectoryTrusted) {
throw "Agent credential directory owner or DACL is not trusted."
}
$script:Sim.Events.Add("assert-directory")
if ($script:Sim.AttemptSwap) {
$script:Sim.Events.Add("swap-blocked-by-directory")
}
return [IO.Path]::GetFullPath($Path)
}
function script:Initialize-MinerOpsQuarantineDirectory {
param([Parameter(Mandatory = $true)][string]$TargetRoot)
$path = Join-Path $TargetRoot `
"minerops-credential-transition-quarantine"
if (-not (Test-Path -LiteralPath $path)) {
[void](New-Item -ItemType Directory -Path $path)
}
return [IO.Path]::GetFullPath($path)
}
function script:Protect-MinerOpsQuarantineEvidence {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$QuarantineRoot
)
$script:Sim.Secure[(Get-SimPathKey -Path $Path)] = $true
}
function script:Get-MinerOpsCredentialArtifactProvenance {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedParent,
[Parameter(Mandatory = $true)][string]$Role,
[Parameter(Mandatory = $true)][bool]$DirectoryWasTrustedAtDiscovery,
[switch]$CreatedByCurrentTransition
)
if ($Role -eq "Active") { return "UnknownUntrusted" }
if ($Role -eq "Quarantine") {
return "ProtectedButUntrustedQuarantined"
}
$key = Get-SimPathKey -Path $Path
if (
$CreatedByCurrentTransition -or
($DirectoryWasTrustedAtDiscovery -and
$script:Sim.Secure.ContainsKey($key) -and
$script:Sim.Secure[$key])
) {
return "ProtectedTransitionCandidate"
}
return "UnknownUntrusted"
}
function script:Move-MinerOpsArtifactToQuarantine {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedParent,
[Parameter(Mandatory = $true)][string]$Role,
[scriptblock]$CheckpointAction
)
if (-not (Test-Path -LiteralPath $Path)) { return $null }
$sourceKey = Get-SimPathKey -Path $Path
$script:Sim.Events.Add("contain-without-read:$([IO.Path]::GetFileName($Path))")
$quarantineRoot = Initialize-MinerOpsQuarantineDirectory `
-TargetRoot $ExpectedParent
$destination = Join-Path $quarantineRoot (
"minerops-quarantine-credential-transition-$Role-" +
[guid]::NewGuid().ToString("N") + ".evidence"
)
[IO.File]::Move($Path, $destination)
Invoke-MinerOpsTransitionCheckpoint `
-Name "artifact-moved-to-quarantine" `
-Action $CheckpointAction
$script:Sim.Secure.Remove($sourceKey)
$script:Sim.Secure[(Get-SimPathKey -Path $destination)] = $true
$script:Sim.Events.Add("quarantine:$([IO.Path]::GetFileName($Path))")
return $destination
}
function script:Read-MinerOpsProtectedCredentialText {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedParent,
[switch]$AllowAclRepair
)
$key = Get-SimPathKey -Path $Path
if (-not $script:Sim.Secure.ContainsKey($key) -or
-not $script:Sim.Secure[$key]) {
$script:Sim.Events.Add("repair:$([IO.Path]::GetFileName($Path))")
$script:Sim.Secure[$key] = $true
if (-not $AllowAclRepair) {
throw "Credential transition artifact had an insecure owner or DACL."
}
}
$script:Sim.Events.Add("read:$([IO.Path]::GetFileName($Path))")
return [IO.File]::ReadAllText($Path)
}
function script:Write-MinerOpsProtectedCredentialJson {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedParent,
[Parameter(Mandatory = $true)]$Value
)
$leaf = [IO.Path]::GetFileName($Path)
$key = Get-SimPathKey -Path $Path
if (Test-Path -LiteralPath $Path) {
throw "CreateNew refused existing artifact."
}
[IO.File]::Create($Path).Dispose()
$script:Sim.Events.Add("create-empty:$leaf")
$script:Sim.Events.Add("protect:$leaf")
if ($script:Sim.FailAclLeaf -eq $leaf) {
Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
throw "simulated ACL establishment failure"
}
$script:Sim.Secure[$key] = $true
$script:Sim.Events.Add("verify-acl:$leaf")
[IO.File]::WriteAllText(
$Path,
($Value | ConvertTo-Json -Depth 50) + [Environment]::NewLine
)
$script:Sim.Events.Add("write-secret:$leaf")
}
function script:Open-MinerOpsProtectedCredentialFile {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedParent,
[switch]$AllowAclRepair
)
$key = Get-SimPathKey -Path $Path
if (-not $script:Sim.Secure.ContainsKey($key) -or
-not $script:Sim.Secure[$key]) {
$script:Sim.Secure[$key] = $true
if (-not $AllowAclRepair) {
throw "Credential transition artifact had an insecure owner or DACL."
}
}
return [IO.File]::Open(
$Path, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite,
[IO.FileShare]::None
)
}
function script:Remove-MinerOpsProtectedCredentialArtifact {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedParent
)
if (-not (Test-Path -LiteralPath $Path)) { return }
$leaf = [IO.Path]::GetFileName($Path)
if ($leaf -eq "agent-credential-transition.recovery.json") {
$active = [IO.File]::ReadAllText($script:Sim.Config) |
ConvertFrom-Json
$script:Sim.CleanupSafe = (
[string]$active.agent_secret -ne "legacy-shared-secret" -and
$script:Sim.TaskRunning -eq
[bool](ConvertFrom-Json ([IO.File]::ReadAllText($Path))).original_task_running
)
}
Remove-Item -LiteralPath $Path -Force
$script:Sim.Secure.Remove((Get-SimPathKey -Path $Path))
$script:Sim.Events.Add("remove:$leaf")
}
function script:Invoke-MinerOpsRuntimeInstaller {
param(
[Parameter(Mandatory = $true)][string]$Path,
[switch]$ApproveFilesystemTrustResetAfterReboot
)
if ($ApproveFilesystemTrustResetAfterReboot) {
$script:Sim.Events.Add("trust-reset-runtime-publication")
if ($script:Sim.InstallerFailure) {
throw "simulated runtime publication failure"
}
$script:Sim.TaskRunning = $false
}
$script:Sim.Events.Add("install")
}
function script:Set-MinerOpsFilesystemTrustBlockerRuntimePublished {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$TargetRoot,
[Parameter(Mandatory = $true)][bool]$OriginalTaskRunning
)
if (-not (Test-Path -LiteralPath $Path) -or
-not $script:Sim.TrustResetAuthorized) {
throw "simulated blocker disappeared before publication state"
}
$script:Sim.TrustResetRuntimePublished = $true
$script:Sim.TrustResetOriginalTaskRunning = $OriginalTaskRunning
$script:Sim.Events.Add("trust-blocker:runtime-published")
}
function script:Set-MinerOpsFilesystemTrustBlockerResetAuthorized {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$TargetRoot,
[Parameter(Mandatory = $true)][bool]$OriginalTaskRunning
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "simulated blocker disappeared before reset authorization"
}
$script:Sim.TrustResetAuthorized = $true
$script:Sim.TrustResetOriginalTaskRunning = $OriginalTaskRunning
$script:Sim.Events.Add("trust-blocker:reset-authorized")
}
function script:Remove-MinerOpsFilesystemTrustBlockerAfterConvergence {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$TargetRoot
)
if (-not $script:Sim.TrustResetRuntimePublished) {
throw "simulated blocker removed before runtime publication"
}
Remove-Item -LiteralPath $Path -Force -ErrorAction Stop
$script:Sim.TrustEpochBlocked = $false
$script:Sim.Events.Add("trust-blocker:removed-after-convergence")
}
function script:Get-MinerOpsVerifiedScheduledTask {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$TargetRoot
)
if ($script:Sim.TaskVerificationFailureAfterCommit -and
$script:Sim.Redemptions.Count -gt 0) {
throw "simulated strict task verification failure"
}
if ($script:Sim.TaskDefinitionInvalid) {
throw "simulated strict task definition failure"
}
$state = if ($null -ne $script:Sim.TaskStateOverride) {
[string]$script:Sim.TaskStateOverride
}
elseif ($script:Sim.TaskRunning) { "Running" } else { "Ready" }
return [pscustomobject]@{
Task = [pscustomobject]@{ TaskName = $Name }
State = $state
}
}
function script:Set-MinerOpsScheduledTaskState {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$TargetRoot,
[Parameter(Mandatory = $true)][bool]$ShouldRun,
[TimeSpan]$Timeout = [TimeSpan]::FromSeconds(30)
)
if ($script:Sim.FailTaskRestore -and $ShouldRun) {
throw "simulated task restart failure"
}
$script:Sim.TaskRunning = $ShouldRun
$script:Sim.Events.Add("task:$ShouldRun")
}
function script:Stop-MinerOpsAgentTask {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$TargetRoot
)
$script:Sim.TaskRunning = $false
$script:Sim.Events.Add("task:stopped-for-transition")
}
function script:Invoke-MinerOpsCredentialRedemption {
param(
[Parameter(Mandatory = $true)][string]$HubUrl,
[Parameter(Mandatory = $true)][string]$Token,
[Parameter(Mandatory = $true)][string]$SecretHash
)
$script:Sim.Redemptions.Add([pscustomobject]@{
Token = $Token
SecretHash = $SecretHash
})
if ($script:Sim.ResponseMode -eq "lost-once" -and
$script:Sim.Redemptions.Count -eq 1) {
throw "simulated lost HTTP response after commit"
}
return [pscustomobject]@{
data = [pscustomobject]@{
agent_id = "00000000-0000-4000-8000-000000000111"
site_id = "00000000-0000-4000-8000-000000000222"
site_name = "Test Site"
agent_zone_id = "00000000-0000-4000-8000-000000000333"
agent_zone_name = "Test Zone"
}
}
}
function script:Invoke-MinerOpsRecoverableConfigInstall {
param(
[Parameter(Mandatory = $true)][string]$PendingPath,
[Parameter(Mandatory = $true)][string]$ConfigPath,
[Parameter(Mandatory = $true)][string]$PriorPath,
[Parameter(Mandatory = $true)][string]$TargetRoot,
[scriptblock]$CheckpointAction
)
$script:Sim.SwapCalls++
if ($script:Sim.SwapMode -eq "fail-before-once" -and
$script:Sim.SwapCalls -eq 1) {
throw "simulated replacement failure"
}
if ($script:Sim.SwapMode -eq "active-missing-pending-once" -and
$script:Sim.SwapCalls -eq 1) {
Remove-Item -LiteralPath $ConfigPath -Force
$script:Sim.Secure.Remove((Get-SimPathKey -Path $ConfigPath))
throw "simulated ERROR_UNABLE_TO_MOVE_REPLACEMENT"
}
if (Test-Path -LiteralPath $ConfigPath) {
[void](Assert-MinerOpsCredentialDirectory -Path $TargetRoot)
$script:Sim.Events.Add("directory-verified-before-active-move")
[IO.File]::Move($ConfigPath, $PriorPath)
$script:Sim.Secure[(Get-SimPathKey -Path $PriorPath)] = `
$script:Sim.Secure[(Get-SimPathKey -Path $ConfigPath)]
$script:Sim.Secure.Remove((Get-SimPathKey -Path $ConfigPath))
Invoke-MinerOpsTransitionCheckpoint `
-Name "active-moved-to-prior" -Action $CheckpointAction
}
if ($script:Sim.SwapMode -eq "active-to-prior-fail-once" -and
$script:Sim.SwapCalls -eq 1) {
throw "simulated ERROR_UNABLE_TO_MOVE_REPLACEMENT_2"
}
[void](Assert-MinerOpsCredentialDirectory -Path $TargetRoot)
$script:Sim.Events.Add("directory-verified-before-pending-move")
[IO.File]::Move($PendingPath, $ConfigPath)
$script:Sim.Secure[(Get-SimPathKey -Path $ConfigPath)] = $true
$script:Sim.Secure.Remove((Get-SimPathKey -Path $PendingPath))
$script:Sim.Events.Add("recoverable-install")
Invoke-MinerOpsTransitionCheckpoint `
-Name "pending-moved-to-active" -Action $CheckpointAction
if ($script:Sim.SwapMode -eq "move-then-fail-once" -and
$script:Sim.SwapCalls -eq 1) {
throw "simulated ambiguous post-replacement failure"
}
}
}
function Invoke-SimulatedTransition {
param(
[scriptblock]$Checkpoint,
[switch]$ApproveFilesystemTrustResetAfterReboot
)
Invoke-MinerOpsAgentCredentialTransition `
-TargetRoot $script:Sim.Target `
-PackageRoot $script:Sim.Package `
-ScheduledTaskName "MinerOpsAgent-Test" `
-CheckpointAction $Checkpoint `
-ApproveFilesystemTrustResetAfterReboot:$ApproveFilesystemTrustResetAfterReboot
}
function Get-SimRecovery {
return [IO.File]::ReadAllText($script:Sim.Recovery) | ConvertFrom-Json
}
function Set-SimPendingObject {
param([Parameter(Mandatory = $true)]$Value)
[IO.File]::WriteAllText(
$script:Sim.Pending,
($Value | ConvertTo-Json -Depth 50) + [Environment]::NewLine
)
}
# Source invariants complement the behavioral harness. In particular, JSON is
# not even serialized until after the empty file's DACL has been read back.
Assert-SourceContains 'existing_agent_credential_transition' `
"Transition package workflow is not validated."
Assert-SourceContains '/api/agent/credential-transition' `
"Dedicated credential transition endpoint is missing."
Assert-SourceContains 'FileMode\]::CreateNew' `
"Secret artifacts are not created without overwrite."
Assert-SourceContains 'Test-MinerOpsCredentialAcl' `
"Credential artifact DACL verification is missing."
Assert-SourceContains 'original_task_running' `
"Original task state is not durable."
Assert-SourceContains 'transition_token_hash' `
"Recovery does not bind the transition token."
Assert-SourceContains 'agent_secret_hash' `
"Recovery does not bind the generated credential."
Assert-SourceContains 'Assert-MinerOpsNoReparseDirectoryPath' `
"Full directory-path reparse validation is missing."
Assert-SourceContains 'Test-MinerOpsAgentDirectoryAcl' `
"Credential directory DACL verification is missing."
Assert-SourceContains 'TrustedCurrentTransitionState' `
"Trusted transition provenance is not explicit."
Assert-SourceContains 'TrustedActiveConfig' `
"Trusted active-config provenance is not explicit."
Assert-SourceContains 'ProtectedButUntrustedQuarantined' `
"Quarantined provenance is not explicit."
Assert-SourceContains 'UnknownUntrusted' `
"Unknown artifact provenance is not explicit."
Assert-SourceContains 'minerops-quarantine-credential-transition' `
"Non-authoritative quarantine naming is missing."
Assert-SourceContains 'minerops-credential-transition\.blocked' `
"Durable quarantine blocked-state naming is missing."
Assert-SourceContains 'minerops-credential-transition-quarantine' `
"Protected quarantine directory naming is missing."
Assert-SourceContains 'marker exists before ACL repair' `
"ACL-repair laundering protection is not documented in source."
Assert-SourceContains 'Invoke-MinerOpsRecoverableConfigInstall' `
"Recoverable config installation is missing."
Assert-SourceContains 'active-moved-to-prior' `
"Known active-to-prior intermediate state is missing."
Assert-SourceContains 'pending-moved-to-active' `
"Known pending-to-active intermediate state is missing."
Assert-SourceContains 'before-recovery-cleanup' `
"Success cleanup checkpoint is missing."
Assert-SourceContains 'Remove-MinerOpsFilesystemTrustBlockerAfterConvergence' `
"Parent-owned post-convergence trust-blocker removal is missing."
Assert-SourceContains 'windows-full-boot-event-v1:' `
"Canonical Windows full-kernel-boot representation is missing."
Assert-True ($source -notmatch 'LastBootUpTime') `
"Wall-clock boot timestamps must not authorize a trust reset."
foreach ($functionName in @(
"Assert-MinerOpsNoReparseDirectoryPath",
"Assert-MinerOpsTrustedAncestorNamespace"
)) {
$functionSource = [regex]::Match(
$source,
"(?s)function $functionName \{.*?\n\}"
).Value
Assert-True (-not [string]::IsNullOrWhiteSpace($functionSource)) `
"$functionName source was not found."
Assert-True ($functionSource -match '\$current\s*=\s*\$root') `
"$functionName does not preserve the filesystem root."
Assert-True ($functionSource -match `
'\$fullPath\.Equals\(\s*\$root') `
"$functionName does not protect a root-valued full path from trimming."
Assert-True ($functionSource -notmatch '\$root\.TrimEnd\(') `
"$functionName converts a root-qualified path to drive-relative form."
Assert-True ($functionSource -notmatch `
'GetFullPath\(\$Path\)\.TrimEnd\(') `
"$functionName trims the filesystem root before identifying it."
if ($functionName -eq "Assert-MinerOpsTrustedAncestorNamespace") {
Assert-True ($functionSource -match `
'\$parent\.Equals\(\s*\$root') `
"$functionName does not protect a root-valued parent from trimming."
}
}
$taskRestoreIndex = $source.LastIndexOf('-Name "task-restored"')
$nonRecoveryCleanupIndex = $source.LastIndexOf('-Path $pendingPath')
$blockerRemovalIndex = $source.LastIndexOf(
'Remove-MinerOpsFilesystemTrustBlockerAfterConvergence'
)
$recoveryCleanupIndex = $source.LastIndexOf('-Path $recoveryPath')
Assert-True (
$taskRestoreIndex -ge 0 -and
$nonRecoveryCleanupIndex -gt $taskRestoreIndex -and
$blockerRemovalIndex -gt $nonRecoveryCleanupIndex -and
$recoveryCleanupIndex -gt $blockerRemovalIndex
) "Blocker deletion must follow convergence and non-recovery cleanup while recovery remains last."
function New-TestTrustBlockerRecord {
return [pscustomobject][ordered]@{
version = 2
state = "blocked"
reason = "existing-agent-directory-was-untrusted"
target_path = [IO.Path]::GetFullPath($script:VerifierTarget)
detected_boot_id = "windows-full-boot-event-v1:000000000000002a"
reset_boot_id = $null
original_task_running = $null
created_at = "2026-09-19T00:00:00.0000000Z"
}
}
$script:VerifierTarget = Join-Path ([IO.Path]::GetTempPath()) `
"minerops-verifier-target"
Invoke-BehaviorTest "production blocker parser accepts canonical boot epoch" {
$json = (New-TestTrustBlockerRecord) | ConvertTo-Json -Depth 10
$record = ConvertFrom-MinerOpsJson `
-Json $json -Description "test filesystem trust blocker"
[void](Assert-MinerOpsFilesystemTrustBlockerRecord `
-Record $record -TargetRoot $script:VerifierTarget)
}
Invoke-BehaviorTest "malformed stored BootId fails closed" {
$record = New-TestTrustBlockerRecord
$record.detected_boot_id = "not-a-boot-id"
Assert-Throws { Assert-MinerOpsFilesystemTrustBlockerRecord `
-Record $record -TargetRoot $script:VerifierTarget } "canonical"
}
Invoke-BehaviorTest "missing stored BootId fails closed" {
$record = New-TestTrustBlockerRecord
$record.PSObject.Properties.Remove("detected_boot_id")
Assert-Throws { Assert-MinerOpsFilesystemTrustBlockerRecord `
-Record $record -TargetRoot $script:VerifierTarget } "invalid"
}
Invoke-BehaviorTest "empty stored BootId fails closed" {
$record = New-TestTrustBlockerRecord
$record.detected_boot_id = ""
Assert-Throws { Assert-MinerOpsFilesystemTrustBlockerRecord `
-Record $record -TargetRoot $script:VerifierTarget } "canonical"
}
Invoke-BehaviorTest "wrong-type stored BootId fails closed" {
$record = New-TestTrustBlockerRecord
$record.detected_boot_id = 42
Assert-Throws { Assert-MinerOpsFilesystemTrustBlockerRecord `
-Record $record -TargetRoot $script:VerifierTarget } "canonical"
}
Invoke-BehaviorTest "additional blocker representation fails closed" {
$record = New-TestTrustBlockerRecord
$record | Add-Member -NotePropertyName boot_timestamp `
-NotePropertyValue "2026-09-19T00:00:00Z"
Assert-Throws { Assert-MinerOpsFilesystemTrustBlockerRecord `
-Record $record -TargetRoot $script:VerifierTarget } "invalid"
}
Invoke-BehaviorTest "current boot-epoch query failure fails closed" {
Assert-Throws { Get-MinerOpsBootIdentifier -QueryAction {
throw "simulated registry failure"
} } "could not be verified"
}
Invoke-BehaviorTest "malformed current boot-epoch query fails closed" {
Assert-Throws { Get-MinerOpsBootIdentifier -QueryAction {
[pscustomobject]@{
RecordId = "42"
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = [int]0
}
} } "could not be verified"
}
Invoke-BehaviorTest "duplicated current full-boot identity fails closed" {
Assert-Throws { Get-MinerOpsBootIdentifier -QueryAction {
1..2 | ForEach-Object {
[pscustomobject]@{
RecordId = [long]42
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = [int]0
}
}
} } "could not be verified"
}
Invoke-BehaviorTest "wrong-type current BootType fails closed" {
Assert-Throws { Get-MinerOpsBootIdentifier -QueryAction {
[pscustomobject]@{
RecordId = [long]42
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = "0"
}
} } "could not be verified"
}
Invoke-BehaviorTest "canonical full-kernel-boot event query succeeds" {
$bootId = Get-MinerOpsBootIdentifier -QueryAction {
[pscustomobject]@{
RecordId = [long]42
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = [int]0
}
}
Assert-True (
$bootId -ceq "windows-full-boot-event-v1:000000000000002a"
) "Full-kernel-boot event was not canonicalized."
}
Invoke-BehaviorTest "Fast Startup event does not advance full-boot epoch" {
$bootId = Get-MinerOpsBootIdentifier -QueryAction {
[pscustomobject]@{
RecordId = [long]43
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = [int]1
}
[pscustomobject]@{
RecordId = [long]42
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = [int]0
}
}
Assert-True (
$bootId -ceq "windows-full-boot-event-v1:000000000000002a"
) "Fast Startup advanced the full-kernel-boot epoch."
}
Invoke-BehaviorTest "hibernate event does not advance full-boot epoch" {
$bootId = Get-MinerOpsBootIdentifier -QueryAction {
[pscustomobject]@{
RecordId = [long]44
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = [int]2
}
[pscustomobject]@{
RecordId = [long]42
EventId = [int]27
ProviderName = "Microsoft-Windows-Kernel-Boot"
Version = [byte]1
BootType = [int]0
}
}
Assert-True (
$bootId -ceq "windows-full-boot-event-v1:000000000000002a"
) "Hibernate advanced the full-kernel-boot epoch."
}
Invoke-BehaviorTest "same canonical boot epoch denies reset" {
Assert-Throws { Assert-MinerOpsBootEpochChanged `
-StoredBootIdentifier "windows-full-boot-event-v1:000000000000002a" `
-CurrentBootIdentifier "windows-full-boot-event-v1:000000000000002a" } `
"without a verified full Restart"
}
Invoke-BehaviorTest "different canonical boot epoch passes epoch check only" {
Assert-MinerOpsBootEpochChanged `
-StoredBootIdentifier "windows-full-boot-event-v1:000000000000002a" `
-CurrentBootIdentifier "windows-full-boot-event-v1:000000000000002b"
}
Invoke-BehaviorTest "older canonical boot epoch denies reset" {
Assert-Throws { Assert-MinerOpsBootEpochChanged `
-StoredBootIdentifier "windows-full-boot-event-v1:000000000000002a" `
-CurrentBootIdentifier "windows-full-boot-event-v1:0000000000000029" } `
"older than the"
}
$bootQueryFunction = [regex]::Match(
$source,
'(?s)function Get-MinerOpsBootIdentifier \{.*?\n\}'
).Value
Assert-True (
$bootQueryFunction -notmatch 'Get-Date|DateTime|LastBootUpTime' -and
$bootQueryFunction -match 'Get-WinEvent' -and
$bootQueryFunction -match 'BootType'
) "Boot epoch comparison must not derive from the wall clock."
$writeFunction = [regex]::Match(
$source,
'(?s)function Write-MinerOpsProtectedCredentialJson \{.*?\n\}'
).Value
$createIndex = $writeFunction.IndexOf(
"New-MinerOpsProtectedFileCreateNewStream"
)
$verifyIndex = $writeFunction.IndexOf("Test-MinerOpsCredentialAcl")
$jsonIndex = $writeFunction.IndexOf('$json =')
Assert-True (
$createIndex -ge 0 -and $verifyIndex -gt $createIndex -and
$jsonIndex -gt $verifyIndex -and
$writeFunction -notmatch 'Set-MinerOpsStreamAcl'
) "Secret serialization must follow protected creation and ACL verification."
$createStreamFunction = [regex]::Match(
$source,
'(?s)function New-MinerOpsProtectedFileCreateNewStream \{.*?\n\}'
).Value
foreach ($requiredCreateContract in @(
'\[IO\.FileStream\]::new',
'FileMode\]::CreateNew',
'FileSystemRights\]::WriteData',
'FileSystemRights\]::ReadPermissions',
'FileSystemRights\]::Synchronize',
'FileShare\]::None',
'\$Acl'
)) {
Assert-True ($createStreamFunction -match $requiredCreateContract) `
"Protected CreateNew stream contract is incomplete."
}
Assert-True (
$createStreamFunction -notmatch 'FullControl|SetAccessControl|Set-Acl'
) "Protected creation must use minimum rights and creation-time FileSecurity."
$openStreamFunction = [regex]::Match(
$source,
'(?s)function Open-MinerOpsFileStreamWithRights \{.*?\n\}'
).Value
Assert-True (
$openStreamFunction -match 'FileMode\]::Open' -and
$openStreamFunction -match 'FileShare\]::None' -and
$openStreamFunction -match '\$Rights'
) "Existing protected artifacts must use an exclusive explicit-rights handle."
$repairOpenFunction = [regex]::Match(
$source,
'(?s)function Open-MinerOpsProtectedCredentialFile \{.*?\n\}'
).Value
foreach ($requiredRepairRight in @(
'ReadData',
'ReadPermissions',
'ChangePermissions',
'TakeOwnership',
'Synchronize'
)) {
Assert-True ($repairOpenFunction -match $requiredRepairRight) `
"Existing-artifact repair is missing $requiredRepairRight."
}
Assert-True (
$repairOpenFunction -match 'Set-MinerOpsStreamAcl' -and
$repairOpenFunction -match 'will not be trusted' -and
$repairOpenFunction -notmatch 'Set-Acl'
) "Existing-artifact repair must remain handle-bound and fail closed."
$trustBlockerCreateFunction = [regex]::Match(
$source,
'(?s)function New-MinerOpsFilesystemTrustBlocker \{.*?\n\}'
).Value
$blockerCreateIndex = $trustBlockerCreateFunction.IndexOf(
'New-MinerOpsProtectedFileCreateNewStream'
)
$blockerVerifyIndex = $trustBlockerCreateFunction.IndexOf(
'Test-MinerOpsCredentialAcl'
)
$blockerContentIndex = $trustBlockerCreateFunction.IndexOf('$record =')
Assert-True (
$blockerCreateIndex -ge 0 -and
$blockerVerifyIndex -gt $blockerCreateIndex -and
$blockerContentIndex -gt $blockerVerifyIndex
) "Filesystem trust blockers must be protected and verified before content."
Assert-True ($source -notmatch '\[IO\.File\]::Open') `
"Credential transition stream security must use explicit FileSystemRights."
$quarantineFunction = [regex]::Match(
$source,
'(?s)function Move-MinerOpsArtifactToQuarantine \{.*?\n\}'
).Value
$quarantineMoveIndex = $quarantineFunction.IndexOf('[IO.File]::Move')
$quarantineAclIndex = $quarantineFunction.IndexOf('Set-Acl')
Assert-True (
$quarantineMoveIndex -ge 0 -and
$quarantineAclIndex -gt $quarantineMoveIndex -and
$quarantineFunction -notmatch 'Read(AllText|-MinerOpsProtectedCredentialText)'
) "Untrusted evidence must leave authority before ACL mutation and must never be read."
foreach ($sensitiveVariable in @(
'enrollmentToken', 'candidateSecret', 'transition_token', 'agent_secret'
)) {
Assert-True (
$source -notmatch (
'(?im)Write-(Host|Output|Verbose|Information).*?\$' +
$sensitiveVariable
)
) "$sensitiveVariable must not be written to output."
}
foreach ($productionValue in @('plexus-windows-agent', 'Plexus Mining Site')) {
Assert-True (
$source -notmatch [regex]::Escape($productionValue)
) "Production-specific value found in generic transition source."
}
Assert-True ($source -notmatch 'Get-ScheduledTask[^\r\n]*SilentlyContinue') `
"Scheduled Task queries must use terminating error behavior."
Assert-SourceContains 'function Get-MinerOpsVerifiedScheduledTask' `
"The strict Scheduled Task verifier is missing."
Assert-SourceContains 'Settings\.Enabled' `
"The strict verifier does not require an enabled task."
Assert-SourceContains 'Principal\.UserId' `
"The strict verifier does not validate the task principal."
Assert-SourceContains 'Actions' `
"The strict verifier does not validate the task action."
# Exercise the real strict verifier with deterministic Scheduled Task provider
# results before the transition simulation replaces it with a filesystem-free
# mock. These cases prove that only a valid expected task in exactly Running or
# Ready can satisfy durable intent.
$script:VerifierTasks = @()
$script:VerifierQueryFailure = ""
function script:Get-ScheduledTask {
param($ErrorAction)
if ($script:VerifierQueryFailure -eq "provider") {
throw "provider failure"
}
if ($script:VerifierQueryFailure -eq "access") {
throw (New-Object UnauthorizedAccessException("access denied"))
}
return $script:VerifierTasks
}
function script:New-VerifierTask {
param(
[string]$State = "Ready",
[string]$Execute = "",
[string]$Arguments = "",
[string]$UserId = "SYSTEM",
[bool]$Enabled = $true,
[switch]$Malformed,
[string]$Name = "MinerOpsAgent-Test"
)
if (-not $Execute) {
$Execute = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
}
if (-not $Arguments) {
$Arguments = '-NoProfile -ExecutionPolicy Bypass -File "{0}"' -f
(Join-Path $script:VerifierTarget "MinerOpsAgentService.ps1")
}
$task = [pscustomobject]@{
TaskName = $Name
TaskPath = "\"
State = $State
Settings = [pscustomobject]@{ Enabled = $Enabled }
Actions = @([pscustomobject]@{
Execute = $Execute
Arguments = $Arguments
})
Principal = [pscustomobject]@{
UserId = $UserId
LogonType = "ServiceAccount"
RunLevel = "Highest"
}
}
if ($Malformed) { $task.Actions = @() }
return $task
}
function script:Set-VerifierTask {
param([Parameter(Mandatory)]$Task)
$script:VerifierQueryFailure = ""
$script:VerifierTasks = @($Task)
}
function script:Start-ScheduledTask {
param([string]$TaskName, $ErrorAction)
$script:VerifierTasks[0].State = "Running"
}
function script:Stop-ScheduledTask {
param([string]$TaskName, $ErrorAction)
$script:VerifierTasks[0].State = "Ready"
}
function script:Start-Sleep { param([int]$Milliseconds) }
$strictName = "MinerOpsAgent-Test"
Invoke-BehaviorTest "running intent accepts exactly Running" {
Set-VerifierTask (New-VerifierTask -State "Running")
[void](Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $true)
}
Invoke-BehaviorTest "running intent rejects Ready" {
Set-VerifierTask (New-VerifierTask -State "Ready")
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $true }
}
Invoke-BehaviorTest "running intent rejects missing task" {
$script:VerifierTasks = @()
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $true } "missing"
}
Invoke-BehaviorTest "running intent rejects query exception" {
$script:VerifierQueryFailure = "provider"
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $true } "query failed"
$script:VerifierQueryFailure = ""
}
Invoke-BehaviorTest "stopped intent accepts exactly Ready" {
Set-VerifierTask (New-VerifierTask -State "Ready")
[void](Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false)
}
Invoke-BehaviorTest "stopped intent stops Running then verifies Ready" {
Set-VerifierTask (New-VerifierTask -State "Running")
[void](Set-MinerOpsScheduledTaskState `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false)
[void](Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false)
}
Invoke-BehaviorTest "stopped intent rejects missing task" {
$script:VerifierTasks = @()
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false } "missing"
}
Invoke-BehaviorTest "stopped intent rejects provider exception" {
$script:VerifierQueryFailure = "provider"
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false } "query failed"
$script:VerifierQueryFailure = ""
}
Invoke-BehaviorTest "stopped intent rejects access denied" {
$script:VerifierQueryFailure = "access"
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false } "query failed"
$script:VerifierQueryFailure = ""
}
Invoke-BehaviorTest "stopped intent rejects empty state" {
Set-VerifierTask (New-VerifierTask -State "")
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false } "malformed"
}
Invoke-BehaviorTest "stopped intent rejects Disabled" {
Set-VerifierTask (New-VerifierTask -State "Disabled" -Enabled:$false)
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false } "disabled"
}
foreach ($invalidState in @("Unknown", "Queued")) {
Invoke-BehaviorTest "stopped intent rejects $invalidState" {
Set-VerifierTask (New-VerifierTask -State $invalidState)
Assert-Throws { Assert-MinerOpsScheduledTaskIntent `
-Name $strictName -TargetRoot $script:VerifierTarget -ShouldRun $false }
}
}
Invoke-BehaviorTest "strict verifier rejects malformed task object" {
Set-VerifierTask (New-VerifierTask -Malformed)
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "exactly one"
}
Invoke-BehaviorTest "strict verifier rejects multiple results" {
$script:VerifierTasks = @(
(New-VerifierTask),
(New-VerifierTask)
)
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "multiple"
}
Invoke-BehaviorTest "strict verifier rejects wrong executable" {
Set-VerifierTask (New-VerifierTask -Execute "C:\Windows\System32\cmd.exe")
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "executable"
}
Invoke-BehaviorTest "strict verifier rejects wrong script arguments" {
Set-VerifierTask (New-VerifierTask -Arguments '-NoProfile -File "C:\wrong.ps1"')
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "arguments"
}
Invoke-BehaviorTest "strict verifier rejects wrong principal" {
Set-VerifierTask (New-VerifierTask -UserId "BUILTIN\Users")
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "principal"
}
Invoke-BehaviorTest "strict verifier rejects missing TaskPath" {
$task = New-VerifierTask
$task.PSObject.Properties.Remove("TaskPath")
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "task path"
}
Invoke-BehaviorTest "strict verifier rejects null TaskPath" {
$task = New-VerifierTask
$task.TaskPath = $null
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "task path"
}
Invoke-BehaviorTest "strict verifier rejects empty TaskPath" {
$task = New-VerifierTask
$task.TaskPath = ""
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "task path"
}
Invoke-BehaviorTest "strict verifier rejects wrong-type TaskPath" {
$task = New-VerifierTask
$task.TaskPath = 1
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "task path"
}
Invoke-BehaviorTest "strict verifier rejects non-root TaskPath" {
$task = New-VerifierTask
$task.TaskPath = "\MinerOps\"
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "task path"
}
Invoke-BehaviorTest "strict verifier rejects missing Settings" {
$task = New-VerifierTask
$task.PSObject.Properties.Remove("Settings")
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "enabled"
}
Invoke-BehaviorTest "strict verifier rejects missing Enabled" {
$task = New-VerifierTask
$task.Settings.PSObject.Properties.Remove("Enabled")
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "enabled"
}
Invoke-BehaviorTest "strict verifier rejects Boolean false Enabled" {
Set-VerifierTask (New-VerifierTask -Enabled:$false)
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "enabled"
}
Invoke-BehaviorTest "strict verifier rejects string False Enabled" {
$task = New-VerifierTask
$task.Settings.Enabled = "False"
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "enabled"
}
Invoke-BehaviorTest "strict verifier rejects numeric Enabled" {
$task = New-VerifierTask
$task.Settings.Enabled = 1
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget } "enabled"
}
Invoke-BehaviorTest "strict verifier accepts actual Boolean true Enabled" {
Set-VerifierTask (New-VerifierTask -Enabled:$true)
[void](Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget)
}
foreach ($malformation in @(
"numeric-state", "string-actions", "numeric-execute", "array-arguments",
"string-principal", "numeric-user", "numeric-logon", "numeric-runlevel"
)) {
Invoke-BehaviorTest "strict verifier rejects $malformation property" {
$task = New-VerifierTask
switch ($malformation) {
"numeric-state" { $task.State = 3 }
"string-actions" { $task.Actions = "powershell.exe" }
"numeric-execute" { $task.Actions[0].Execute = 1 }
"array-arguments" { $task.Actions[0].Arguments = @("arg1", "arg2") }
"string-principal" { $task.Principal = "SYSTEM" }
"numeric-user" { $task.Principal.UserId = 18 }
"numeric-logon" { $task.Principal.LogonType = 5 }
"numeric-runlevel" { $task.Principal.RunLevel = 1 }
}
Set-VerifierTask $task
Assert-Throws { Get-MinerOpsVerifiedScheduledTask `
-Name $strictName -TargetRoot $script:VerifierTarget }
}
}
try {
Invoke-BehaviorTest "normal successful transition" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Invoke-SimulatedTransition | Out-Null
$active = [IO.File]::ReadAllText($sim.Config) | ConvertFrom-Json
Assert-True ($active.agent_secret -ne "legacy-shared-secret") `
"Permanent secret was not installed."
Assert-True $sim.TaskRunning "Originally running task did not end running."
Assert-True (-not (Test-Path $sim.Recovery)) "Recovery was not cleaned."
Assert-True $sim.CleanupSafe "Cleanup occurred before success criteria."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "protected-before-secret writes" {
$sim = New-TransitionSimulation -OriginallyRunning $false
Invoke-SimulatedTransition | Out-Null
foreach ($leaf in @(
"agent-credential-transition.recovery.json",
"agent-config.json.pending"
)) {
$create = $sim.Events.IndexOf("create-empty:$leaf")
$protect = $sim.Events.IndexOf("protect:$leaf")
$verify = $sim.Events.IndexOf("verify-acl:$leaf")
$write = $sim.Events.IndexOf("write-secret:$leaf")
Assert-True (
$create -ge 0 -and $protect -gt $create -and
$verify -gt $protect -and $write -gt $verify
) "ACL ordering failed for $leaf."
}
Remove-TransitionSimulation
}
Invoke-BehaviorTest "ACL failure blocks redemption without leaked content" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.FailAclLeaf = "agent-credential-transition.recovery.json"
Assert-Throws { Invoke-SimulatedTransition } "ACL establishment failure"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Redemption proceeded after ACL failure."
Assert-True (-not (Test-Path $sim.Recovery)) `
"Insecure recovery artifact remained after handled ACL failure."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "insecure recovery is quarantined and blocked on two runs" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "recovery-written") { throw "stop" }
}
} "stop"
$malicious = Get-SimRecovery
$malicious.agent_secret = "attacker-provided-secret"
$malicious.agent_secret_hash = Get-MinerOpsSha256 `
-Value $malicious.agent_secret
$malicious.original_task_running = $false
[IO.File]::WriteAllText(
$sim.Recovery,
($malicious | ConvertTo-Json -Depth 50) + [Environment]::NewLine
)
$sim.Events.Clear()
$sim.Secure[(Get-SimPathKey $sim.Recovery)] = $false
Assert-Throws { Invoke-SimulatedTransition } "evidence is quarantined"
Assert-True (
$sim.Events -contains "quarantine:agent-credential-transition.recovery.json"
) "Insecure recovery was not moved out of authority."
Assert-True (
$sim.Events -notcontains "read:agent-credential-transition.recovery.json"
) "Insecure recovery contents were read."
Assert-True ($sim.Redemptions.Count -eq 0) `
"Recovery credential was used after an insecure read."
Assert-Throws { Invoke-SimulatedTransition } "evidence is quarantined"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Second invocation used quarantined recovery authority."
Assert-True $sim.TaskRunning `
"Attacker-provided task intent changed the Scheduled Task."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "blocked recovery cannot be laundered by later ACL repair" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "recovery-written") { throw "stop" }
}
} "stop"
$malicious = Get-SimRecovery
$malicious.agent_secret = "attacker-laundering-secret"
$malicious.agent_secret_hash = Get-MinerOpsSha256 `
-Value $malicious.agent_secret
$malicious.original_task_running = $false
[IO.File]::WriteAllText(
$sim.Recovery,
($malicious | ConvertTo-Json -Depth 50) + [Environment]::NewLine
)
$sim.Secure[(Get-SimPathKey $sim.Recovery)] = $false
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "quarantine-blocker-written") {
throw "termination after quarantine blocker"
}
}
} "termination after quarantine blocker"
Assert-True (Test-Path -LiteralPath $sim.Recovery) `
"Injected interruption did not leave the authority pathname."
Assert-True (
@(Get-ChildItem -LiteralPath $sim.Target `
-Filter "minerops-credential-transition.blocked*.state").Count -gt 0
) "Durable blocker was not present before containment."
# Model an external/legacy ACL repair after interruption. ACL
# correctness must not override the already-durable blocked state.
$sim.Secure[(Get-SimPathKey $sim.Recovery)] = $true
Assert-Throws { Invoke-SimulatedTransition } "evidence is quarantined"
Assert-True (-not (Test-Path -LiteralPath $sim.Recovery)) `
"Second invocation retained repaired recovery authority."
Assert-True ($sim.Redemptions.Count -eq 0) `
"Repaired malicious recovery reached redemption on a later run."
Assert-True $sim.TaskRunning `
"Repaired malicious recovery changed Scheduled Task intent."
Remove-TransitionSimulation
}
foreach ($originallyRunning in @($true, $false)) {
$label = if ($originallyRunning) { "running" } else { "stopped" }
Invoke-BehaviorTest "lost response retry restores originally $label task" {
$sim = New-TransitionSimulation -OriginallyRunning $originallyRunning
$sim.ResponseMode = "lost-once"
Assert-Throws { Invoke-SimulatedTransition } "lost HTTP response"
Assert-True (-not $sim.TaskRunning) `
"Task should remain stopped after ambiguous redemption."
$recoverySecretHash = (Get-SimRecovery).agent_secret_hash
Invoke-SimulatedTransition | Out-Null
Assert-True ($sim.TaskRunning -eq $originallyRunning) `
"Original task intent was not restored."
Assert-True ($sim.Redemptions.Count -eq 2) `
"Retry did not redeem twice."
Assert-True (
$sim.Redemptions[0].SecretHash -eq $recoverySecretHash -and
$sim.Redemptions[1].SecretHash -eq $recoverySecretHash
) "Retry changed the committed credential."
Remove-TransitionSimulation
}
}
Invoke-BehaviorTest "crash after server commit before pending converges" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "server-committed") { throw "crash-after-commit" }
}
} "crash-after-commit"
Assert-True (-not (Test-Path $sim.Pending)) `
"Pending unexpectedly existed at injected checkpoint."
Invoke-SimulatedTransition | Out-Null
Assert-True $sim.TaskRunning "Retry did not restore task."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "valid protected pending resumes on rerun" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "pending-written") { throw "crash-after-pending" }
}
} "crash-after-pending"
$writesBefore = @($sim.Events | Where-Object {
$_ -eq "write-secret:agent-config.json.pending"
}).Count
Invoke-SimulatedTransition | Out-Null
$writesAfter = @($sim.Events | Where-Object {
$_ -eq "write-secret:agent-config.json.pending"
}).Count
Assert-True ($writesAfter -eq $writesBefore) `
"Valid pending config was overwritten instead of resumed."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "failure immediately before replacement converges" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "before-replace") { throw "before-replace" }
}
} "before-replace"
Assert-True (Test-Path $sim.Pending) "Pending config was lost."
Invoke-SimulatedTransition | Out-Null
Assert-True $sim.TaskRunning "Retry did not finish replacement."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "pre-rename replacement failure converges" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.SwapMode = "fail-before-once"
Assert-Throws { Invoke-SimulatedTransition } "replacement failure"
Assert-True (Test-Path $sim.Pending) `
"Pending config was lost on replacement failure."
Invoke-SimulatedTransition | Out-Null
Assert-True $sim.TaskRunning "Replacement retry did not restore task."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "post-replacement ambiguity converges" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.SwapMode = "move-then-fail-once"
Assert-Throws { Invoke-SimulatedTransition } "post-replacement"
Assert-True (-not (Test-Path $sim.Pending)) `
"Recoverable installation should consume pending config."
Assert-True (Test-Path $sim.Prior) `
"Known prior remnant was not retained."
Invoke-SimulatedTransition | Out-Null
Assert-True $sim.TaskRunning `
"Already-replaced config was not recovered."
Remove-TransitionSimulation
}
foreach ($originallyRunning in @($true, $false)) {
$label = if ($originallyRunning) { "running" } else { "stopped" }
Invoke-BehaviorTest "active-missing pending recovery restores originally $label task" {
$sim = New-TransitionSimulation -OriginallyRunning $originallyRunning
$sim.SwapMode = "active-missing-pending-once"
Assert-Throws { Invoke-SimulatedTransition } `
"ERROR_UNABLE_TO_MOVE_REPLACEMENT"
Assert-True (-not (Test-Path $sim.Config)) `
"Active config unexpectedly survived the injected partial state."
Assert-True (Test-Path $sim.Pending) `
"Authenticated pending config was lost."
$recovery = Get-SimRecovery
$first = $sim.Redemptions[0]
Invoke-SimulatedTransition | Out-Null
Assert-True ($sim.Redemptions.Count -eq 2) `
"Active-missing recovery did not retry idempotent redemption."
Assert-True (
$sim.Redemptions[1].Token -eq $first.Token -and
$sim.Redemptions[1].SecretHash -eq $first.SecretHash -and
$sim.Redemptions[1].Token -eq $recovery.transition_token -and
$sim.Redemptions[1].SecretHash -eq $recovery.agent_secret_hash
) "Active-missing recovery changed token or permanent credential."
$active = [IO.File]::ReadAllText($sim.Config) | ConvertFrom-Json
Assert-True (
[string]$active.agent_id -eq
"00000000-0000-4000-8000-000000000111" -and
[string]$active.site_id -eq
"00000000-0000-4000-8000-000000000222" -and
[string]$active.agent_zone_id -eq
"00000000-0000-4000-8000-000000000333"
) "Active-missing recovery changed Agent routing."
Assert-True ($sim.TaskRunning -eq $originallyRunning) `
"Active-missing recovery lost original task intent."
Remove-TransitionSimulation
}
}
Invoke-BehaviorTest "known active-missing pending prior intermediate converges" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.SwapMode = "active-to-prior-fail-once"
Assert-Throws { Invoke-SimulatedTransition } `
"ERROR_UNABLE_TO_MOVE_REPLACEMENT_2"
Assert-True (-not (Test-Path $sim.Config)) `
"Active config should be absent at the known rename midpoint."
Assert-True (Test-Path $sim.Pending) `
"Pending config should remain at the known rename midpoint."
Assert-True (Test-Path $sim.Prior) `
"Authenticated prior remnant should represent the moved active file."
Invoke-SimulatedTransition | Out-Null
Assert-True $sim.TaskRunning `
"Known replacement midpoint did not converge."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "active-missing unknown pending fails closed" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Remove-Item -LiteralPath $sim.Config -Force
$sim.Secure.Remove((Get-SimPathKey -Path $sim.Config))
[IO.File]::WriteAllText(
$sim.Pending,
'{"agent_secret":"unknown-pending-secret"}'
)
$sim.Secure[(Get-SimPathKey -Path $sim.Pending)] = $false
$captured = & {
try { Invoke-SimulatedTransition } catch { $_.Exception.Message }
} *>&1 | Out-String
Assert-True ($sim.Redemptions.Count -eq 0) `
"Unknown active-missing pending state reached redemption."
Assert-True (-not $captured.Contains("unknown-pending-secret")) `
"Unknown active-missing pending secret reached output."
Assert-Throws { Invoke-SimulatedTransition } "evidence is quarantined"
Remove-TransitionSimulation
}
foreach ($activeMissingMutation in @("malformed", "agent", "secret")) {
Invoke-BehaviorTest "active-missing $activeMissingMutation pending fails closed" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "pending-written") { throw "stop" }
}
} "stop"
Remove-Item -LiteralPath $sim.Config -Force
$sim.Secure.Remove((Get-SimPathKey -Path $sim.Config))
if ($activeMissingMutation -eq "malformed") {
[IO.File]::WriteAllText($sim.Pending, '{"agent_secret":')
}
else {
$pending = [IO.File]::ReadAllText($sim.Pending) |
ConvertFrom-Json
if ($activeMissingMutation -eq "agent") {
$pending.agent_id = "00000000-0000-4000-8000-000000009999"
}
else {
$pending.agent_secret = "mismatched-active-missing-secret"
}
Set-SimPendingObject -Value $pending
}
$calls = $sim.Redemptions.Count
Assert-Throws { Invoke-SimulatedTransition }
Assert-True ($sim.Redemptions.Count -eq $calls) `
"Invalid active-missing pending state reached redemption."
Remove-TransitionSimulation
}
}
Invoke-BehaviorTest "prior remnant alone cannot restore active authority" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "active-moved-to-prior") {
throw "stop-at-rename-midpoint"
}
}
} "stop-at-rename-midpoint"
Remove-Item -LiteralPath $sim.Pending -Force
$sim.Secure.Remove((Get-SimPathKey -Path $sim.Pending))
$calls = $sim.Redemptions.Count
Assert-Throws { Invoke-SimulatedTransition } `
"no authenticated pending config"
Assert-True (-not (Test-Path -LiteralPath $sim.Config)) `
"Prior remnant was promoted into active authority."
Assert-True ($sim.Redemptions.Count -eq $calls) `
"Prior-only state reached redemption."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "directory junction fails before credential operations" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryReparse = $true
Assert-Throws { Invoke-SimulatedTransition } "reparse point"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Reparse directory reached redemption."
Assert-True (-not (Test-Path $sim.Recovery)) `
"Reparse directory received new secret state."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "directory repair quarantines authority and blocks three invocations" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "recovery-written") { throw "stop" }
}
} "stop"
$sim.DirectoryTrusted = $false
Assert-Throws { Invoke-SimulatedTransition } "existing Agent directory was untrusted"
Assert-True ($sim.Events -contains "repair-directory") `
"Permissive target directory was not repaired."
Assert-True (
$sim.Events -contains "quarantine:agent-credential-transition.recovery.json"
) "Pre-repair recovery was promoted instead of quarantined."
Assert-True ($sim.Redemptions.Count -eq 0) `
"Directory repair promoted pre-existing recovery authority."
Assert-Throws { Invoke-SimulatedTransition } "filesystem trust epoch is blocked"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Later rerun promoted a pre-repair artifact."
Assert-Throws { Invoke-SimulatedTransition } "filesystem trust epoch is blocked"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Third invocation escaped the durable trust-epoch blocker."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "clean directory repair contains but never continues automatically" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryTrusted = $false
Assert-Throws { Invoke-SimulatedTransition } `
"existing Agent directory was untrusted"
$repairIndex = $sim.Events.IndexOf("repair-directory")
$recoveryWriteIndex = $sim.Events.IndexOf(
"write-secret:agent-credential-transition.recovery.json"
)
Assert-True ($repairIndex -ge 0 -and $recoveryWriteIndex -lt 0) `
"Same-epoch repair created transition credential state."
Assert-True ($sim.Redemptions.Count -eq 0) `
"Same-epoch repair redeemed a transition token."
Assert-Throws { Invoke-SimulatedTransition } "filesystem trust epoch is blocked"
Assert-Throws { Invoke-SimulatedTransition } "filesystem trust epoch is blocked"
Assert-True (Test-Path -LiteralPath $sim.TrustBlocker) `
"Ordinary reruns removed the external trust blocker."
Remove-TransitionSimulation
}
foreach ($lateLeaf in @(
"agent-credential-transition.recovery.json",
"agent-config.json.pending"
)) {
Invoke-BehaviorTest "post-scan $lateLeaf stays non-authoritative" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryTrusted = $false
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "directory-containment-scan-complete") {
$latePath = Join-Path $sim.Target $lateLeaf
[IO.File]::WriteAllText(
$latePath,
'{"original_task_running":false,"agent_secret":"late"}'
)
$sim.Secure[(Get-SimPathKey $latePath)] = $true
}
}
} "existing Agent directory was untrusted"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Post-scan artifact reached redemption in the repair epoch."
Assert-Throws { Invoke-SimulatedTransition } `
"filesystem trust epoch is blocked"
Assert-Throws { Invoke-SimulatedTransition } `
"filesystem trust epoch is blocked"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Correct-ACL late artifact became authority on an ordinary rerun."
Assert-True $sim.TaskRunning `
"Late artifact changed durable Scheduled Task intent."
$active = [IO.File]::ReadAllText($sim.Config) | ConvertFrom-Json
Assert-True (
[string]$active.agent_secret -eq "legacy-shared-secret"
) "Late pending state was activated while the blocker existed."
Remove-TransitionSimulation
}
}
Invoke-BehaviorTest "explicit post-reboot trust reset republishes runtime then resumes" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryTrusted = $false
Assert-Throws { Invoke-SimulatedTransition } `
"existing Agent directory was untrusted"
$sim.BootChanged = $true
$script:BlockerPresentBeforeRemoval = $false
Invoke-SimulatedTransition `
-ApproveFilesystemTrustResetAfterReboot `
-Checkpoint {
param($name)
if ($name -eq "before-trust-blocker-removal") {
$script:BlockerPresentBeforeRemoval = Test-Path `
-LiteralPath $sim.TrustBlocker
}
} | Out-Null
Assert-True $script:BlockerPresentBeforeRemoval `
"Parent removed the blocker before final convergence."
Assert-True (-not (Test-Path -LiteralPath $sim.TrustBlocker)) `
"Approved post-reboot reset did not remove the external blocker."
Assert-True (
$sim.Events -contains "trust-reset-runtime-publication"
) "Trust reset did not publish fresh runtime before continuing."
Assert-True ($sim.Redemptions.Count -eq 1) `
"Approved clean trust reset did not resume normal transition."
$publishIndex = $sim.Events.IndexOf("trust-reset-runtime-publication")
$restoreIndex = $sim.Events.IndexOf("task:True")
$removeIndex = $sim.Events.IndexOf(
"trust-blocker:removed-after-convergence"
)
Assert-True (
$publishIndex -ge 0 -and $restoreIndex -gt $publishIndex -and
$removeIndex -gt $restoreIndex
) "Trust blocker removal did not follow runtime and task convergence."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "installer failure leaves trust blocker present" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryTrusted = $false
Assert-Throws { Invoke-SimulatedTransition } `
"existing Agent directory was untrusted"
$sim.BootChanged = $true
$sim.InstallerFailure = $true
Assert-Throws { Invoke-SimulatedTransition `
-ApproveFilesystemTrustResetAfterReboot } `
"runtime publication failure"
Assert-True (Test-Path -LiteralPath $sim.TrustBlocker) `
"Installer failure removed the trust blocker."
Assert-True ($sim.Redemptions.Count -eq 0) `
"Installer failure reached credential redemption."
Assert-True (
$sim.TrustResetAuthorized -and
$sim.TrustResetOriginalTaskRunning
) "Installer failure lost the strictly validated task intent."
$sim.InstallerFailure = $false
Invoke-SimulatedTransition `
-ApproveFilesystemTrustResetAfterReboot | Out-Null
Assert-True $sim.TaskRunning `
"Installer-failure retry did not restore original running intent."
Assert-True (-not (Test-Path -LiteralPath $sim.TrustBlocker)) `
"Installer-failure retry did not converge blocker removal."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "published reset marker resumes without early deletion" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryTrusted = $false
Assert-Throws { Invoke-SimulatedTransition } `
"existing Agent directory was untrusted"
$sim.BootChanged = $true
Assert-Throws {
Invoke-SimulatedTransition `
-ApproveFilesystemTrustResetAfterReboot `
-Checkpoint {
param($name)
if ($name -eq "recovery-written") {
throw "stop-after-reset-publication"
}
}
} "stop-after-reset-publication"
Assert-True (Test-Path -LiteralPath $sim.TrustBlocker) `
"Interrupted parent removed the trust blocker."
Assert-True $sim.TrustResetRuntimePublished `
"Interrupted parent did not persist runtime publication state."
Invoke-SimulatedTransition `
-ApproveFilesystemTrustResetAfterReboot | Out-Null
Assert-True (-not (Test-Path -LiteralPath $sim.TrustBlocker)) `
"Resumed converged parent did not remove the blocker."
Assert-True ($sim.Redemptions.Count -eq 1) `
"Resumed trust reset did not complete exactly one redemption."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "wrong task definition cannot remove trust blocker" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryTrusted = $false
Assert-Throws { Invoke-SimulatedTransition } `
"existing Agent directory was untrusted"
$sim.BootChanged = $true
$sim.TaskDefinitionInvalid = $true
Assert-Throws { Invoke-SimulatedTransition `
-ApproveFilesystemTrustResetAfterReboot } `
"strict task definition"
Assert-True (Test-Path -LiteralPath $sim.TrustBlocker) `
"Wrong task definition indirectly removed the trust blocker."
Assert-True (
$sim.Events -notcontains "trust-reset-runtime-publication"
) "Wrong task definition reached runtime publication."
Remove-TransitionSimulation
}
foreach ($blockedTaskState in @("Disabled", "Unknown", "Queued")) {
Invoke-BehaviorTest "$blockedTaskState task cannot remove trust blocker" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.DirectoryTrusted = $false
Assert-Throws { Invoke-SimulatedTransition } `
"existing Agent directory was untrusted"
$sim.BootChanged = $true
$sim.TaskStateOverride = $blockedTaskState
Assert-Throws { Invoke-SimulatedTransition `
-ApproveFilesystemTrustResetAfterReboot } `
"no valid pre-reset intent"
Assert-True (Test-Path -LiteralPath $sim.TrustBlocker) `
"$blockedTaskState task indirectly removed the trust blocker."
Remove-TransitionSimulation
}
}
Invoke-BehaviorTest "directory is revalidated and simulated swap is blocked" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.AttemptSwap = $true
Invoke-SimulatedTransition | Out-Null
Assert-True (
$sim.Events -contains "directory-verified-before-active-move" -and
$sim.Events -contains "directory-verified-before-pending-move"
) "Directory invariant was not revalidated before both renames."
Assert-True (
$sim.Events -contains "swap-blocked-by-directory"
) "Simulated untrusted pathname swap was not blocked."
$active = [IO.File]::ReadAllText($sim.Config) | ConvertFrom-Json
Assert-True ([string]$active.agent_secret -ne "legacy-shared-secret") `
"Simulated swap prevented installation of the protected config."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "unknown replacement remnant is quarantined and blocks rerun" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$unknown = Join-Path $sim.Target "agent-config.json.unknown-replacement"
[IO.File]::WriteAllText($unknown, '{"agent_secret":"do-not-log-this"}')
$captured = & {
try { Invoke-SimulatedTransition } catch { $_.Exception.Message }
} *>&1 | Out-String
Assert-True (-not $captured.Contains("do-not-log-this")) `
"Unknown remnant contents reached output."
Assert-True ($sim.Redemptions.Count -eq 0) `
"Unknown replacement remnant reached redemption."
Assert-Throws { Invoke-SimulatedTransition } "evidence is quarantined"
Assert-True ($sim.Redemptions.Count -eq 0) `
"Quarantined unknown remnant was later trusted."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "wrong committed active config never converges" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "pending-moved-to-active") {
throw "stop-after-active-install"
}
}
} "stop-after-active-install"
$active = [IO.File]::ReadAllText($sim.Config) | ConvertFrom-Json
$active.agent_id = "00000000-0000-4000-8000-000000009999"
[IO.File]::WriteAllText(
$sim.Config,
($active | ConvertTo-Json -Depth 50) + [Environment]::NewLine
)
$calls = $sim.Redemptions.Count
Assert-Throws { Invoke-SimulatedTransition } "does not match this Agent"
Assert-True ($sim.Redemptions.Count -eq $calls) `
"Wrong active Agent identity reached redemption."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "malformed pending fails closed before redemption" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "pending-written") { throw "stop" }
}
} "stop"
[IO.File]::WriteAllText($sim.Pending, '{"agent_secret":')
$calls = $sim.Redemptions.Count
Assert-Throws { Invoke-SimulatedTransition } "malformed JSON"
Assert-True ($sim.Redemptions.Count -eq $calls) `
"Malformed pending state reached redemption."
Remove-TransitionSimulation
}
foreach ($mutation in @("agent", "secret")) {
Invoke-BehaviorTest "mismatched pending $mutation fails closed" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "pending-written") { throw "stop" }
}
} "stop"
$pending = [IO.File]::ReadAllText($sim.Pending) | ConvertFrom-Json
if ($mutation -eq "agent") {
$pending.agent_id = "00000000-0000-4000-8000-000000009999"
}
else {
$pending.agent_secret = "attacker-secret"
}
Set-SimPendingObject -Value $pending
$calls = $sim.Redemptions.Count
Assert-Throws { Invoke-SimulatedTransition } `
"does not match protected recovery"
Assert-True ($sim.Redemptions.Count -eq $calls) `
"Mismatched pending state reached redemption."
Remove-TransitionSimulation
}
}
Invoke-BehaviorTest "insecure pending is quarantined and blocked on two runs" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "pending-written") { throw "stop" }
}
} "stop"
$sim.Events.Clear()
$sim.Secure[(Get-SimPathKey $sim.Pending)] = $false
$calls = $sim.Redemptions.Count
Assert-Throws { Invoke-SimulatedTransition } "evidence is quarantined"
Assert-True (
$sim.Events -notcontains "read:agent-config.json.pending"
) "Insecure pending contents were read."
Assert-True ($sim.Redemptions.Count -eq $calls) `
"Insecure pending state reached redemption."
Assert-Throws { Invoke-SimulatedTransition } "evidence is quarantined"
Assert-True ($sim.Redemptions.Count -eq $calls) `
"Second invocation used quarantined pending authority."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "task restart failure retains recovery and no success cleanup" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.FailTaskRestore = $true
Assert-Throws { Invoke-SimulatedTransition } "task restart failure"
Assert-True (Test-Path $sim.Recovery) `
"Recovery was deleted before task verification."
Assert-True (-not $sim.CleanupSafe) `
"Cleanup was marked safe after restart failure."
$sim.FailTaskRestore = $false
Invoke-SimulatedTransition | Out-Null
Assert-True $sim.TaskRunning "Task recovery retry failed."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "strict task verification failure preserves committed recovery for retry" {
$sim = New-TransitionSimulation -OriginallyRunning $false
$sim.TaskVerificationFailureAfterCommit = $true
Assert-Throws { Invoke-SimulatedTransition } `
"strict task verification failure"
Assert-True (Test-Path -LiteralPath $sim.Recovery) `
"Recovery was deleted after strict final task verification failed."
Assert-True (-not $sim.CleanupSafe) `
"Task verification failure reached success cleanup."
$recovery = Get-SimRecovery
$first = $sim.Redemptions[0]
$sim.TaskVerificationFailureAfterCommit = $false
Invoke-SimulatedTransition | Out-Null
Assert-True ($sim.Redemptions.Count -eq 2) `
"Task-repair retry did not repeat idempotent redemption."
Assert-True (
$sim.Redemptions[1].Token -eq $first.Token -and
$sim.Redemptions[1].SecretHash -eq $first.SecretHash -and
$sim.Redemptions[1].Token -eq $recovery.transition_token -and
$sim.Redemptions[1].SecretHash -eq $recovery.agent_secret_hash
) "Task-repair retry changed the committed credential."
Assert-True (-not (Test-Path -LiteralPath $sim.Recovery)) `
"Successful task-repair retry did not complete cleanup."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "cleanup interruption resumes from recovery token" {
$sim = New-TransitionSimulation -OriginallyRunning $true
Assert-Throws {
Invoke-SimulatedTransition -Checkpoint {
param($name)
if ($name -eq "before-recovery-cleanup") {
throw "cleanup-interrupted"
}
}
} "cleanup-interrupted"
Assert-True (-not (Test-Path $sim.Enrollment)) `
"Enrollment token file should already be removed at this checkpoint."
Assert-True (Test-Path $sim.Recovery) `
"Recovery must remain last during cleanup."
Invoke-SimulatedTransition | Out-Null
Assert-True (-not (Test-Path $sim.Recovery)) `
"Recovery-token retry did not finish cleanup."
Assert-True $sim.TaskRunning `
"Cleanup retry changed the intended task state."
Remove-TransitionSimulation
}
Invoke-BehaviorTest "ordinary output and errors omit token and secret" {
$sim = New-TransitionSimulation -OriginallyRunning $true
$sim.ResponseMode = "lost-once"
$captured = & {
try { Invoke-SimulatedTransition } catch { $_.Exception.Message }
} *>&1 | Out-String
$recovery = Get-SimRecovery
Assert-True (-not $captured.Contains([string]$recovery.agent_secret)) `
"Generated Agent secret appeared in output."
Assert-True (-not $captured.Contains([string]$recovery.transition_token)) `
"Transition token appeared in output."
Remove-TransitionSimulation
}
}
finally {
Remove-TransitionSimulation
}
Write-Host "$script:Passed credential-transition behavioral tests passed."