test(ft546): verify read-only shadow operation
This commit is contained in:
194
tools/shadow/Test-Ft546ShadowPreflight.ps1
Normal file
194
tools/shadow/Test-Ft546ShadowPreflight.ps1
Normal file
@@ -0,0 +1,194 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$StableManifest,
|
||||
[Parameter(Mandatory = $true)][string]$ShadowArtifact,
|
||||
[Parameter(Mandatory = $true)][string]$Config,
|
||||
[Parameter(Mandatory = $true)][string]$DeploymentDirectory,
|
||||
[Parameter(Mandatory = $true)][string]$PermissionReport,
|
||||
[Parameter(Mandatory = $true)][string]$NetworkDenyReport,
|
||||
[uri]$StatusUri = 'http://127.0.0.1:18091/shadow/status',
|
||||
[long]$MinimumFreeBytes = 1073741824,
|
||||
[int]$MaximumEvidenceAgeMinutes = 60,
|
||||
[Parameter(Mandatory = $true)][string]$OutputJson
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
. (Join-Path $PSScriptRoot 'ShadowScriptCommon.ps1')
|
||||
|
||||
$checks = New-Object System.Collections.ArrayList
|
||||
function Add-PreflightCheck {
|
||||
param([string]$Name, [bool]$Passed, [string]$Detail)
|
||||
[void]$checks.Add([pscustomobject]@{
|
||||
name = $Name
|
||||
status = if ($Passed) { 'PASS' } else { 'FAIL' }
|
||||
detail = $Detail
|
||||
})
|
||||
}
|
||||
|
||||
$stablePath = Resolve-ShadowFile $StableManifest
|
||||
$stable = Read-ShadowJson $stablePath
|
||||
$artifact = Resolve-ShadowFile $ShadowArtifact
|
||||
$configPath = Resolve-ShadowFile $Config
|
||||
$deployment = Resolve-ShadowDirectory $DeploymentDirectory -Create
|
||||
$permissions = Read-ShadowJson $PermissionReport
|
||||
$network = Read-ShadowJson $NetworkDenyReport
|
||||
$configValues = Read-ShadowYamlScalars $configPath
|
||||
|
||||
$stableAlive = $null -ne (Get-Process -Id ([int]$stable.pid) `
|
||||
-ErrorAction SilentlyContinue)
|
||||
Add-PreflightCheck 'STABLE_PROCESS_HEALTHY' $stableAlive `
|
||||
"pid=$($stable.pid)"
|
||||
$stableArtifactExists = Test-Path -LiteralPath $stable.artifactPath -PathType Leaf
|
||||
$stableHashMatches = $stableArtifactExists -and `
|
||||
((Get-ShadowSha256 $stable.artifactPath) -eq $stable.artifactSha256)
|
||||
Add-PreflightCheck 'STABLE_ARTIFACT_HASH_UNCHANGED' $stableHashMatches `
|
||||
$stable.artifactPath
|
||||
$stableConfigMatches = $true
|
||||
foreach ($stableConfig in @($stable.configuration)) {
|
||||
$stableConfigMatches = $stableConfigMatches -and `
|
||||
(Test-Path -LiteralPath $stableConfig.path -PathType Leaf) -and `
|
||||
((Get-ShadowSha256 $stableConfig.path) -eq $stableConfig.sha256)
|
||||
}
|
||||
Add-PreflightCheck 'STABLE_CONFIG_HASHES_UNCHANGED' $stableConfigMatches `
|
||||
"count=$(@($stable.configuration).Count)"
|
||||
|
||||
$artifactHash = Get-ShadowSha256 $artifact
|
||||
$configHash = Get-ShadowSha256 $configPath
|
||||
$artifactInDeployment = Test-ShadowPathWithin $artifact $deployment
|
||||
$configInDeployment = Test-ShadowPathWithin $configPath $deployment
|
||||
Add-PreflightCheck 'SHADOW_ARTIFACT_IN_DEPLOYMENT' $artifactInDeployment $artifact
|
||||
Add-PreflightCheck 'SHADOW_CONFIG_IN_DEPLOYMENT' $configInDeployment $configPath
|
||||
|
||||
$stableDirectory = Split-Path -Parent $stable.artifactPath
|
||||
$deploymentDistinct = -not ([IO.Path]::GetFullPath($deployment).Equals(
|
||||
[IO.Path]::GetFullPath($stableDirectory),
|
||||
[StringComparison]::OrdinalIgnoreCase))
|
||||
Add-PreflightCheck 'DEPLOYMENT_DISTINCT_FROM_STABLE' $deploymentDistinct `
|
||||
"stable=$stableDirectory;shadow=$deployment"
|
||||
|
||||
$permissionPassed = $permissions.status -eq 'PASS' -and `
|
||||
[bool]$permissions.source.approvedViewsReadable -and `
|
||||
([int]$permissions.source.productionWritePermissionCount -eq 0) -and `
|
||||
[bool]$permissions.evidence.requiredWritesGranted
|
||||
Add-PreflightCheck 'DATABASE_PERMISSIONS_APPROVED' $permissionPassed `
|
||||
"source=$($permissions.source.login);evidence=$($permissions.evidence.login)"
|
||||
$permissionFresh = $false
|
||||
try {
|
||||
$permissionAge = ([DateTime]::UtcNow -
|
||||
[DateTime]::Parse($permissions.generatedAtUtc).ToUniversalTime()).TotalMinutes
|
||||
$permissionFresh = $permissionAge -ge 0 -and `
|
||||
$permissionAge -le $MaximumEvidenceAgeMinutes
|
||||
}
|
||||
catch { $permissionFresh = $false }
|
||||
Add-PreflightCheck 'DATABASE_PERMISSION_REPORT_FRESH' $permissionFresh `
|
||||
"maximumMinutes=$MaximumEvidenceAgeMinutes"
|
||||
|
||||
$configSource = $null
|
||||
$configEvidence = $null
|
||||
$configIdentityError = $null
|
||||
try {
|
||||
$configSource = Get-ShadowSqlServerJdbcIdentity `
|
||||
$configValues['shadow.source.jdbc-url']
|
||||
$configEvidence = Get-ShadowSqlServerJdbcIdentity `
|
||||
$configValues['shadow.evidence.jdbc-url']
|
||||
}
|
||||
catch { $configIdentityError = $_.Exception.Message }
|
||||
$sourceConfigMatches = $null -ne $configSource -and `
|
||||
$configSource.server.Equals([string]$permissions.source.server,
|
||||
[StringComparison]::OrdinalIgnoreCase) -and `
|
||||
$configSource.database.Equals([string]$permissions.source.database,
|
||||
[StringComparison]::OrdinalIgnoreCase) -and `
|
||||
([string]$configValues['shadow.source.username']).Equals(
|
||||
[string]$permissions.source.login,
|
||||
[StringComparison]::OrdinalIgnoreCase)
|
||||
Add-PreflightCheck 'CONFIG_SOURCE_IDENTITY_MATCHES_PERMISSION_REPORT' `
|
||||
$sourceConfigMatches $(if ($configIdentityError) { $configIdentityError } `
|
||||
else { "config=$($configSource.server)|$($configSource.database)" })
|
||||
$evidenceConfigMatches = $null -ne $configEvidence -and `
|
||||
$configEvidence.server.Equals([string]$permissions.evidence.server,
|
||||
[StringComparison]::OrdinalIgnoreCase) -and `
|
||||
$configEvidence.database.Equals([string]$permissions.evidence.database,
|
||||
[StringComparison]::OrdinalIgnoreCase) -and `
|
||||
([string]$configValues['shadow.evidence.username']).Equals(
|
||||
[string]$permissions.evidence.login,
|
||||
[StringComparison]::OrdinalIgnoreCase)
|
||||
Add-PreflightCheck 'CONFIG_EVIDENCE_IDENTITY_MATCHES_PERMISSION_REPORT' `
|
||||
$evidenceConfigMatches $(if ($configIdentityError) { $configIdentityError } `
|
||||
else { "config=$($configEvidence.server)|$($configEvidence.database)" })
|
||||
$sourceIdentity = ("{0}|{1}" -f $permissions.source.server,
|
||||
$permissions.source.database).ToLowerInvariant()
|
||||
$evidenceIdentity = ("{0}|{1}" -f $permissions.evidence.server,
|
||||
$permissions.evidence.database).ToLowerInvariant()
|
||||
$databasesDistinct = $sourceIdentity -ne $evidenceIdentity
|
||||
Add-PreflightCheck 'SOURCE_EVIDENCE_DATABASES_DISTINCT' $databasesDistinct `
|
||||
"source=$sourceIdentity;evidence=$evidenceIdentity"
|
||||
|
||||
$networkPassed = $network.status -eq 'PASS' -and `
|
||||
[bool]$network.plcEgressDenied -and [bool]$network.wmsEgressDenied
|
||||
Add-PreflightCheck 'PLC_WMS_EGRESS_DENIED' $networkPassed `
|
||||
"plc=$($network.plcEgressDenied);wms=$($network.wmsEgressDenied)"
|
||||
$networkFresh = $false
|
||||
try {
|
||||
$networkAge = ([DateTime]::UtcNow -
|
||||
[DateTime]::Parse($network.generatedAtUtc).ToUniversalTime()).TotalMinutes
|
||||
$networkFresh = $networkAge -ge 0 -and `
|
||||
$networkAge -le $MaximumEvidenceAgeMinutes
|
||||
}
|
||||
catch { $networkFresh = $false }
|
||||
Add-PreflightCheck 'NETWORK_DENY_REPORT_FRESH' $networkFresh `
|
||||
"maximumMinutes=$MaximumEvidenceAgeMinutes"
|
||||
|
||||
$loopback = $StatusUri.Host -in @('127.0.0.1', 'localhost', '::1')
|
||||
Add-PreflightCheck 'STATUS_ENDPOINT_LOOPBACK' $loopback $StatusUri.AbsoluteUri
|
||||
$stablePorts = @($stable.listeningPorts | ForEach-Object { [int]$_.port })
|
||||
$portDistinct = $stablePorts -notcontains [int]$StatusUri.Port
|
||||
Add-PreflightCheck 'SHADOW_PORT_DISTINCT_FROM_STABLE' $portDistinct `
|
||||
"shadow=$($StatusUri.Port);stable=$($stablePorts -join ',')"
|
||||
|
||||
$configText = Get-Content -LiteralPath $configPath -Raw
|
||||
$outboundKeyPattern = '(?im)^\s*[^#\r\n]*(plc|wms)[^:\r\n]*(ip|host|port|url)\s*:'
|
||||
$noOutboundEndpoints = -not [regex]::IsMatch($configText, $outboundKeyPattern)
|
||||
Add-PreflightCheck 'NO_PLC_WMS_ENDPOINT_IN_CONFIG' $noOutboundEndpoints $configPath
|
||||
|
||||
$freeBytes = Get-ShadowFreeBytes $deployment
|
||||
$spaceAvailable = $MinimumFreeBytes -ge 0 -and $freeBytes -ge $MinimumFreeBytes
|
||||
Add-PreflightCheck 'OUTPUT_FREE_SPACE' $spaceAvailable `
|
||||
"available=$freeBytes;required=$MinimumFreeBytes"
|
||||
|
||||
$pidFile = Join-Path $deployment 'shadow.pid.json'
|
||||
$noShadowPidFile = -not (Test-Path -LiteralPath $pidFile)
|
||||
Add-PreflightCheck 'NO_EXISTING_SHADOW_PID_FILE' $noShadowPidFile $pidFile
|
||||
|
||||
$failed = @($checks | Where-Object { $_.status -ne 'PASS' })
|
||||
$result = [pscustomobject]@{
|
||||
schemaVersion = 1
|
||||
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
|
||||
success = $failed.Count -eq 0
|
||||
checks = @($checks)
|
||||
stable = $stable
|
||||
shadowArtifact = [pscustomobject]@{ path = $artifact; sha256 = $artifactHash }
|
||||
config = [pscustomobject]@{ path = $configPath; sha256 = $configHash }
|
||||
deploymentDirectory = $deployment
|
||||
pidFile = $pidFile
|
||||
statusUri = $StatusUri.AbsoluteUri
|
||||
sourceIdentity = $permissions.source
|
||||
evidenceIdentity = $permissions.evidence
|
||||
configuredSourceIdentity = $configSource
|
||||
configuredEvidenceIdentity = $configEvidence
|
||||
minimumFreeBytes = $MinimumFreeBytes
|
||||
permissionReport = [pscustomobject]@{
|
||||
path = Resolve-ShadowFile $PermissionReport
|
||||
sha256 = Get-ShadowSha256 $PermissionReport
|
||||
}
|
||||
networkDenyReport = [pscustomobject]@{
|
||||
path = Resolve-ShadowFile $NetworkDenyReport
|
||||
sha256 = Get-ShadowSha256 $NetworkDenyReport
|
||||
}
|
||||
freeBytes = $freeBytes
|
||||
}
|
||||
Write-ShadowJson -Value $result -Path $OutputJson
|
||||
if ($failed.Count -gt 0) {
|
||||
throw "SHADOW_PREFLIGHT_FAILED:$($failed.name -join ',')"
|
||||
}
|
||||
return $result
|
||||
Reference in New Issue
Block a user