Files
FT546-WCS/tools/shadow/Test-Ft546ShadowPreflight.ps1

195 lines
8.8 KiB
PowerShell

[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