195 lines
8.8 KiB
PowerShell
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
|