param( [string]$StagingRoot = "", [ValidateSet("systemd", "buildroot")] [string]$InitSystem = "systemd", [switch]$KeepStaging ) $ErrorActionPreference = "Stop" function Assert-FileNonEmpty { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { throw "Expected staged file is missing: $Path" } $item = Get-Item -LiteralPath $Path if ($item.Length -le 0) { throw "Expected staged file is empty: $Path" } } function Assert-Contains { param( [string]$Text, [string]$Expected, [string]$Message ) if (-not $Text.Contains($Expected)) { throw $Message } } function Assert-NoSecretEnvironmentValues { param([string]$Text) foreach ($line in ($Text -split "`r?`n")) { if ($line -notmatch "^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$") { continue } $key = $Matches[1] $value = $Matches[2].Trim() if ($value -eq "") { continue } if ($key -eq "TERA_API_PASSWORD_KDF" -or $key -eq "TERA_API_MIN_PASSWORD_LENGTH" -or $key -eq "TERA_API_SECRET_STORE") { continue } if ($key -match "(?i)(PASSWORD|TOKEN|SECRET|PRIVATE_KEY)") { throw "target env must not contain secret material in $key" } } } $scriptApiRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $workingApiRoot = Resolve-Path (Get-Location) if (Test-Path -LiteralPath (Join-Path $workingApiRoot "go.mod")) { $apiRoot = $workingApiRoot } elseif (Test-Path -LiteralPath (Join-Path $scriptApiRoot "go.mod")) { $apiRoot = $scriptApiRoot } else { throw "Run this script from services\tera-api, or place it under services\tera-api\scripts." } $repoRoot = Resolve-Path (Join-Path $apiRoot "..\..") $projectRoot = Resolve-Path (Join-Path $apiRoot "..") $buildScript = Join-Path $apiRoot "scripts\check-linux-arm64-build.ps1" $unitSource = Join-Path $apiRoot "deploy\systemd\tera-api.service" $envSource = Join-Path $apiRoot "deploy\etc\tera\tera-api.env" $accessdUnitSource = Join-Path $apiRoot "deploy\systemd\tera-accessd.service" $accessdEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-accessd.env" $hwdUnitSource = Join-Path $apiRoot "deploy\systemd\tera-hwd.service" $hwdEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-hwd.env" $netdUnitSource = Join-Path $apiRoot "deploy\systemd\tera-netd.service" $netdEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-netd.env" $timedUnitSource = Join-Path $apiRoot "deploy\systemd\tera-timed.service" $timedEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-timed.env" $alarmdUnitSource = Join-Path $apiRoot "deploy\systemd\tera-alarmd.service" $alarmdEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-alarmd.env" $snmpdUnitSource = Join-Path $apiRoot "deploy\systemd\tera-snmpd.service" $snmpdBuildrootEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-snmpd.env" $snmpdSystemdEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-snmpd-systemd.env" $logdUnitSource = Join-Path $apiRoot "deploy\systemd\tera-logd.service" $logdEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-logd.env" $updatedUnitSource = Join-Path $apiRoot "deploy\systemd\tera-updated.service" $updatedEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-updated.env" $serialdUnitSource = Join-Path $apiRoot "deploy\systemd\tera-seriald.service" $serialdEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-seriald.env" $sessiondUnitSource = Join-Path $apiRoot "deploy\systemd\tera-sessiond.service" $sessiondEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-sessiond.env" $webUnitSource = Join-Path $apiRoot "deploy\systemd\tera-web.service" $webEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-web.env" $cloneDemoWebBuildScript = Join-Path $projectRoot "frontend\scripts\check-linux-arm64-build.ps1" $cloneDemoWebUnitSource = Join-Path $apiRoot "deploy\systemd\tera-clone-demo-web.service" $cloneDemoWebEnvSource = Join-Path $apiRoot "deploy\etc\tera\tera-clone-demo-web.env" $webSource = Join-Path $repoRoot "webapp" $serialMapSource = Join-Path $apiRoot "deploy\etc\tera\serial-map.json" $updateTrustSource = Join-Path $apiRoot "deploy\etc\tera\update-trust" $updateTrustReadmeSource = Join-Path $updateTrustSource "README" $updateTrustAnchorSources = @(Get-ChildItem -LiteralPath $updateTrustSource -Force -File -Filter "*.json") if ($updateTrustAnchorSources.Count -eq 0) { throw "At least one reviewed public update trust anchor is required for target staging." } $buildrootSerialdSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S40tera-seriald" $buildrootHwdSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S42tera-hwd" $buildrootNetdSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S43tera-netd" $buildrootTimedSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S45tera-timed" $buildrootAlarmdSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S47tera-alarmd" $buildrootSNMPDSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S46tera-snmpd" $buildrootLogdSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S44tera-logd" $buildrootUpdatedSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S48tera-updated" $buildrootAPISource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S50tera-api" $buildrootAccessdSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S49tera-accessd" $buildrootSessiondSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S55tera-sessiond" $buildrootWebSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S60tera-web" $buildrootCloneDemoWebSource = Join-Path $apiRoot "deploy\buildroot\etc\init.d\S61tera-clone-demo-web" $buildrootUdevSource = Join-Path $apiRoot "deploy\buildroot\etc\udev\rules.d\99-tera-serial.rules" $requiredPaths = @($buildScript, $cloneDemoWebBuildScript, $envSource, $accessdEnvSource, $hwdEnvSource, $netdEnvSource, $timedEnvSource, $alarmdEnvSource, $snmpdBuildrootEnvSource, $snmpdSystemdEnvSource, $logdEnvSource, $updatedEnvSource, $serialdEnvSource, $sessiondEnvSource, $webEnvSource, $cloneDemoWebEnvSource, $webSource, $serialMapSource, $updateTrustReadmeSource) + $updateTrustAnchorSources.FullName if ($InitSystem -eq "systemd") { $requiredPaths += @($unitSource, $accessdUnitSource, $hwdUnitSource, $netdUnitSource, $timedUnitSource, $alarmdUnitSource, $snmpdUnitSource, $logdUnitSource, $updatedUnitSource, $serialdUnitSource, $sessiondUnitSource, $webUnitSource, $cloneDemoWebUnitSource) } else { $requiredPaths += @($buildrootSerialdSource, $buildrootHwdSource, $buildrootNetdSource, $buildrootLogdSource, $buildrootTimedSource, $buildrootAlarmdSource, $buildrootSNMPDSource, $buildrootUpdatedSource, $buildrootAccessdSource, $buildrootAPISource, $buildrootSessiondSource, $buildrootWebSource, $buildrootCloneDemoWebSource, $buildrootUdevSource) } foreach ($requiredPath in $requiredPaths) { if (-not (Test-Path -LiteralPath $requiredPath)) { throw "Required packaging input is missing: $requiredPath" } } $createdTempRoot = $false if ($StagingRoot -eq "") { $StagingRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("tera-api-rootfs-" + [System.Guid]::NewGuid().ToString("N")) $createdTempRoot = $true } if (Test-Path -LiteralPath $StagingRoot) { throw "Staging root already exists; choose an empty path: $StagingRoot" } $binTarget = Join-Path $StagingRoot "usr\bin\tera-api" $accessdBinTarget = Join-Path $StagingRoot "usr\bin\tera-accessd" $hwdBinTarget = Join-Path $StagingRoot "usr\bin\tera-hwd" $netdBinTarget = Join-Path $StagingRoot "usr\bin\tera-netd" $timedBinTarget = Join-Path $StagingRoot "usr\bin\tera-timed" $alarmdBinTarget = Join-Path $StagingRoot "usr\bin\tera-alarmd" $snmpdBinTarget = Join-Path $StagingRoot "usr\bin\tera-snmpd" $logdBinTarget = Join-Path $StagingRoot "usr\bin\tera-logd" $updatedBinTarget = Join-Path $StagingRoot "usr\bin\tera-updated" $serialdBinTarget = Join-Path $StagingRoot "usr\bin\tera-seriald" $sessiondBinTarget = Join-Path $StagingRoot "usr\bin\tera-sessiond" $webBinTarget = Join-Path $StagingRoot "usr\bin\tera-web" $cloneDemoWebBinTarget = Join-Path $StagingRoot "usr\bin\tera-clone-demo-web" $serialCheckBinTarget = Join-Path $StagingRoot "usr\bin\tera-serial-check" $serialLineCheckBinTarget = Join-Path $StagingRoot "usr\bin\tera-serial-line-check" $unitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-api.service" $accessdUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-accessd.service" $hwdUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-hwd.service" $netdUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-netd.service" $timedUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-timed.service" $alarmdUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-alarmd.service" $snmpdUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-snmpd.service" $logdUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-logd.service" $updatedUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-updated.service" $serialdUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-seriald.service" $sessiondUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-sessiond.service" $webUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-web.service" $cloneDemoWebUnitTarget = Join-Path $StagingRoot "etc\systemd\system\tera-clone-demo-web.service" $envTarget = Join-Path $StagingRoot "etc\tera\tera-api.env" $accessdEnvTarget = Join-Path $StagingRoot "etc\tera\tera-accessd.env" $hwdEnvTarget = Join-Path $StagingRoot "etc\tera\tera-hwd.env" $netdEnvTarget = Join-Path $StagingRoot "etc\tera\tera-netd.env" $timedEnvTarget = Join-Path $StagingRoot "etc\tera\tera-timed.env" $alarmdEnvTarget = Join-Path $StagingRoot "etc\tera\tera-alarmd.env" $snmpdEnvTarget = Join-Path $StagingRoot "etc\tera\tera-snmpd.env" $logdEnvTarget = Join-Path $StagingRoot "etc\tera\tera-logd.env" $updatedEnvTarget = Join-Path $StagingRoot "etc\tera\tera-updated.env" $serialdEnvTarget = Join-Path $StagingRoot "etc\tera\tera-seriald.env" $sessiondEnvTarget = Join-Path $StagingRoot "etc\tera\tera-sessiond.env" $webEnvTarget = Join-Path $StagingRoot "etc\tera\tera-web.env" $cloneDemoWebEnvTarget = Join-Path $StagingRoot "etc\tera\tera-clone-demo-web.env" $webRootTarget = Join-Path $StagingRoot "usr\share\tera-web" $serialMapTarget = Join-Path $StagingRoot "etc\tera\serial-map.json" $updateTrustReadmeTarget = Join-Path $StagingRoot "etc\tera\update-trust\README" $updateTrustAnchorTargets = @($updateTrustAnchorSources | ForEach-Object { Join-Path (Split-Path -Parent $updateTrustReadmeTarget) $_.Name }) $buildrootSerialdTarget = Join-Path $StagingRoot "etc\init.d\S40tera-seriald" $buildrootHwdTarget = Join-Path $StagingRoot "etc\init.d\S42tera-hwd" $buildrootNetdTarget = Join-Path $StagingRoot "etc\init.d\S43tera-netd" $buildrootTimedTarget = Join-Path $StagingRoot "etc\init.d\S45tera-timed" $buildrootAlarmdTarget = Join-Path $StagingRoot "etc\init.d\S47tera-alarmd" $buildrootSNMPDTarget = Join-Path $StagingRoot "etc\init.d\S46tera-snmpd" $buildrootLogdTarget = Join-Path $StagingRoot "etc\init.d\S44tera-logd" $buildrootUpdatedTarget = Join-Path $StagingRoot "etc\init.d\S48tera-updated" $buildrootAPITarget = Join-Path $StagingRoot "etc\init.d\S50tera-api" $buildrootAccessdTarget = Join-Path $StagingRoot "etc\init.d\S49tera-accessd" $buildrootSessiondTarget = Join-Path $StagingRoot "etc\init.d\S55tera-sessiond" $buildrootWebTarget = Join-Path $StagingRoot "etc\init.d\S60tera-web" $buildrootCloneDemoWebTarget = Join-Path $StagingRoot "etc\init.d\S61tera-clone-demo-web" $buildrootUdevTarget = Join-Path $StagingRoot "etc\udev\rules.d\99-tera-serial.rules" $stateDir = Join-Path $StagingRoot "var\lib\tera-api" $logDir = Join-Path $StagingRoot "var\log\tera" try { foreach ($directory in @( (Split-Path -Parent $binTarget), (Split-Path -Parent $unitTarget), (Split-Path -Parent $envTarget), $webRootTarget, (Split-Path -Parent $buildrootSerialdTarget), (Split-Path -Parent $buildrootUdevTarget), $stateDir, $logDir )) { New-Item -ItemType Directory -Force $directory | Out-Null } & $buildScript -OutputPath $binTarget -Command tera-api if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $accessdBinTarget -Command tera-accessd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $hwdBinTarget -Command tera-hwd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $netdBinTarget -Command tera-netd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $timedBinTarget -Command tera-timed if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $alarmdBinTarget -Command tera-alarmd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $snmpdBinTarget -Command tera-snmpd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $logdBinTarget -Command tera-logd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $updatedBinTarget -Command tera-updated if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $serialdBinTarget -Command tera-seriald if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $sessiondBinTarget -Command tera-sessiond if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $webBinTarget -Command tera-web if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $cloneDemoWebBuildScript -OutputPath $cloneDemoWebBinTarget if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $serialCheckBinTarget -Command tera-serial-check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $buildScript -OutputPath $serialLineCheckBinTarget -Command tera-serial-line-check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Copy-Item -LiteralPath $envSource -Destination $envTarget -Force Copy-Item -LiteralPath $accessdEnvSource -Destination $accessdEnvTarget -Force Copy-Item -LiteralPath $hwdEnvSource -Destination $hwdEnvTarget -Force Copy-Item -LiteralPath $netdEnvSource -Destination $netdEnvTarget -Force Copy-Item -LiteralPath $timedEnvSource -Destination $timedEnvTarget -Force Copy-Item -LiteralPath $alarmdEnvSource -Destination $alarmdEnvTarget -Force Copy-Item -LiteralPath $logdEnvSource -Destination $logdEnvTarget -Force Copy-Item -LiteralPath $updatedEnvSource -Destination $updatedEnvTarget -Force Copy-Item -LiteralPath $serialdEnvSource -Destination $serialdEnvTarget -Force Copy-Item -LiteralPath $sessiondEnvSource -Destination $sessiondEnvTarget -Force Copy-Item -LiteralPath $webEnvSource -Destination $webEnvTarget -Force Copy-Item -LiteralPath $cloneDemoWebEnvSource -Destination $cloneDemoWebEnvTarget -Force Copy-Item -LiteralPath (Join-Path $webSource "index.html") -Destination (Join-Path $webRootTarget "index.html") -Force Copy-Item -LiteralPath (Join-Path $webSource "src") -Destination (Join-Path $webRootTarget "src") -Recurse -Force Copy-Item -LiteralPath $serialMapSource -Destination $serialMapTarget -Force New-Item -ItemType Directory -Force (Split-Path -Parent $updateTrustReadmeTarget) | Out-Null Copy-Item -LiteralPath $updateTrustReadmeSource -Destination $updateTrustReadmeTarget -Force foreach ($anchorSource in $updateTrustAnchorSources) { if (($anchorSource.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $anchorSource.Name.EndsWith(".private.json", [StringComparison]::OrdinalIgnoreCase)) { throw "Update trust anchor must be a public regular file: $($anchorSource.FullName)" } $anchor = Get-Content -LiteralPath $anchorSource.FullName -Raw -Encoding UTF8 | ConvertFrom-Json if ($anchor.format_version -ne 1 -or $anchor.algorithm -ne "ed25519" -or $anchorSource.Name -cne "$($anchor.key_id).json" -or $anchor.allowed_products -notcontains "tera-core-x48-pro" -or $anchor.allowed_profiles -notcontains "fet3568j-c") { throw "Update trust anchor policy is invalid: $($anchorSource.FullName)" } Copy-Item -LiteralPath $anchorSource.FullName -Destination (Join-Path (Split-Path -Parent $updateTrustReadmeTarget) $anchorSource.Name) -Force } if ($InitSystem -eq "systemd") { Copy-Item -LiteralPath $snmpdSystemdEnvSource -Destination $snmpdEnvTarget -Force Copy-Item -LiteralPath $unitSource -Destination $unitTarget -Force Copy-Item -LiteralPath $accessdUnitSource -Destination $accessdUnitTarget -Force Copy-Item -LiteralPath $hwdUnitSource -Destination $hwdUnitTarget -Force Copy-Item -LiteralPath $netdUnitSource -Destination $netdUnitTarget -Force Copy-Item -LiteralPath $timedUnitSource -Destination $timedUnitTarget -Force Copy-Item -LiteralPath $alarmdUnitSource -Destination $alarmdUnitTarget -Force Copy-Item -LiteralPath $snmpdUnitSource -Destination $snmpdUnitTarget -Force Copy-Item -LiteralPath $logdUnitSource -Destination $logdUnitTarget -Force Copy-Item -LiteralPath $updatedUnitSource -Destination $updatedUnitTarget -Force Copy-Item -LiteralPath $serialdUnitSource -Destination $serialdUnitTarget -Force Copy-Item -LiteralPath $sessiondUnitSource -Destination $sessiondUnitTarget -Force Copy-Item -LiteralPath $webUnitSource -Destination $webUnitTarget -Force Copy-Item -LiteralPath $cloneDemoWebUnitSource -Destination $cloneDemoWebUnitTarget -Force } else { Copy-Item -LiteralPath $snmpdBuildrootEnvSource -Destination $snmpdEnvTarget -Force Copy-Item -LiteralPath $buildrootSerialdSource -Destination $buildrootSerialdTarget -Force Copy-Item -LiteralPath $buildrootHwdSource -Destination $buildrootHwdTarget -Force Copy-Item -LiteralPath $buildrootNetdSource -Destination $buildrootNetdTarget -Force Copy-Item -LiteralPath $buildrootTimedSource -Destination $buildrootTimedTarget -Force Copy-Item -LiteralPath $buildrootAlarmdSource -Destination $buildrootAlarmdTarget -Force Copy-Item -LiteralPath $buildrootSNMPDSource -Destination $buildrootSNMPDTarget -Force Copy-Item -LiteralPath $buildrootLogdSource -Destination $buildrootLogdTarget -Force Copy-Item -LiteralPath $buildrootUpdatedSource -Destination $buildrootUpdatedTarget -Force Copy-Item -LiteralPath $buildrootAPISource -Destination $buildrootAPITarget -Force Copy-Item -LiteralPath $buildrootAccessdSource -Destination $buildrootAccessdTarget -Force Copy-Item -LiteralPath $buildrootSessiondSource -Destination $buildrootSessiondTarget -Force Copy-Item -LiteralPath $buildrootWebSource -Destination $buildrootWebTarget -Force Copy-Item -LiteralPath $buildrootCloneDemoWebSource -Destination $buildrootCloneDemoWebTarget -Force Copy-Item -LiteralPath $buildrootUdevSource -Destination $buildrootUdevTarget -Force } Assert-FileNonEmpty $binTarget Assert-FileNonEmpty $accessdBinTarget Assert-FileNonEmpty $hwdBinTarget Assert-FileNonEmpty $netdBinTarget Assert-FileNonEmpty $timedBinTarget Assert-FileNonEmpty $alarmdBinTarget Assert-FileNonEmpty $snmpdBinTarget Assert-FileNonEmpty $logdBinTarget Assert-FileNonEmpty $updatedBinTarget Assert-FileNonEmpty $serialdBinTarget Assert-FileNonEmpty $sessiondBinTarget Assert-FileNonEmpty $webBinTarget Assert-FileNonEmpty $cloneDemoWebBinTarget Assert-FileNonEmpty $serialCheckBinTarget Assert-FileNonEmpty $serialLineCheckBinTarget Assert-FileNonEmpty $envTarget Assert-FileNonEmpty $accessdEnvTarget Assert-FileNonEmpty $hwdEnvTarget Assert-FileNonEmpty $netdEnvTarget Assert-FileNonEmpty $timedEnvTarget Assert-FileNonEmpty $alarmdEnvTarget Assert-FileNonEmpty $snmpdEnvTarget Assert-FileNonEmpty $logdEnvTarget Assert-FileNonEmpty $updatedEnvTarget Assert-FileNonEmpty $serialdEnvTarget Assert-FileNonEmpty $sessiondEnvTarget Assert-FileNonEmpty $webEnvTarget Assert-FileNonEmpty $cloneDemoWebEnvTarget Assert-FileNonEmpty (Join-Path $webRootTarget "index.html") Assert-FileNonEmpty (Join-Path $webRootTarget "src\direct-stream.js") Assert-FileNonEmpty $serialMapTarget Assert-FileNonEmpty $updateTrustReadmeTarget foreach ($anchorTarget in $updateTrustAnchorTargets) { Assert-FileNonEmpty $anchorTarget } if ($InitSystem -eq "systemd") { Assert-FileNonEmpty $unitTarget Assert-FileNonEmpty $accessdUnitTarget Assert-FileNonEmpty $hwdUnitTarget Assert-FileNonEmpty $netdUnitTarget Assert-FileNonEmpty $timedUnitTarget Assert-FileNonEmpty $alarmdUnitTarget Assert-FileNonEmpty $snmpdUnitTarget Assert-FileNonEmpty $logdUnitTarget Assert-FileNonEmpty $updatedUnitTarget Assert-FileNonEmpty $serialdUnitTarget Assert-FileNonEmpty $sessiondUnitTarget Assert-FileNonEmpty $webUnitTarget Assert-FileNonEmpty $cloneDemoWebUnitTarget } else { Assert-FileNonEmpty $buildrootSerialdTarget Assert-FileNonEmpty $buildrootHwdTarget Assert-FileNonEmpty $buildrootNetdTarget Assert-FileNonEmpty $buildrootTimedTarget Assert-FileNonEmpty $buildrootAlarmdTarget Assert-FileNonEmpty $buildrootSNMPDTarget Assert-FileNonEmpty $buildrootLogdTarget Assert-FileNonEmpty $buildrootUpdatedTarget Assert-FileNonEmpty $buildrootAPITarget Assert-FileNonEmpty $buildrootAccessdTarget Assert-FileNonEmpty $buildrootSessiondTarget Assert-FileNonEmpty $buildrootWebTarget Assert-FileNonEmpty $buildrootCloneDemoWebTarget Assert-FileNonEmpty $buildrootUdevTarget } $envText = Get-Content -Raw -LiteralPath $envTarget $accessdEnvText = Get-Content -Raw -LiteralPath $accessdEnvTarget $hwdEnvText = Get-Content -Raw -LiteralPath $hwdEnvTarget $netdEnvText = Get-Content -Raw -LiteralPath $netdEnvTarget $timedEnvText = Get-Content -Raw -LiteralPath $timedEnvTarget $alarmdEnvText = Get-Content -Raw -LiteralPath $alarmdEnvTarget $snmpdEnvText = Get-Content -Raw -LiteralPath $snmpdEnvTarget $logdEnvText = Get-Content -Raw -LiteralPath $logdEnvTarget $updatedEnvText = Get-Content -Raw -LiteralPath $updatedEnvTarget $serialdEnvText = Get-Content -Raw -LiteralPath $serialdEnvTarget $sessiondEnvText = Get-Content -Raw -LiteralPath $sessiondEnvTarget $webEnvText = Get-Content -Raw -LiteralPath $webEnvTarget $cloneDemoWebEnvText = Get-Content -Raw -LiteralPath $cloneDemoWebEnvTarget $serialMapText = Get-Content -Raw -LiteralPath $serialMapTarget Assert-Contains $envText "TERA_API_LISTEN_ADDR=127.0.0.1:8082" "target API must listen only behind the same-origin gateway" Assert-Contains $envText "TERA_API_GREEN_LED_SOCKET=/run/tera-hwd/green-leds.sock" "target env must configure the hardware LED socket" Assert-Contains $envText "TERA_API_SERIAL_SOCKET=/run/tera-seriald/serial-ports.sock" "target env must configure the serial controller socket" Assert-Contains $envText "TERA_API_TIME_SOCKET=/run/tera-timed/time.sock" "target env must configure the system time socket" Assert-Contains $envText "TERA_API_NETWORK_SOCKET=/run/tera-netd/control.sock" "target env must configure the privileged network socket" Assert-Contains $envText "TERA_API_MANAGEMENT_SOCKET=/run/tera-accessd/control.sock" "target env must configure the management access socket" Assert-Contains $accessdEnvText "TERA_ACCESSD_BIND_HOST=0.0.0.0" "management access listeners must bind the target interfaces" Assert-Contains $envText "TERA_API_TLS_STORE=/var/lib/tera-api/tls/library" "TLS inventory must stay in protected API service state" Assert-Contains $envText "TERA_API_TLS_ACTIVE_KEY=/var/lib/tera-api/tls/active.key" "active TLS private key must stay in protected API service state" Assert-Contains $accessdEnvText "TERA_ACCESSD_TLS_KEY=/var/lib/tera-api/tls/active.key" "management listener must use the API-managed active TLS private key" Assert-Contains $envText "TERA_API_ALARM_SOCKET=/run/tera-alarmd/alarm-delivery.sock" "target env must configure the alarm delivery socket" Assert-Contains $envText "TERA_API_LOG_SOCKET=/run/tera-logd/log-store.sock" "target env must configure the authoritative log socket" Assert-Contains $envText "TERA_API_LOG_SCHEDULER_ENABLED=true" "target env must enable configured status-event scheduling" Assert-Contains $envText "TERA_API_LOG_ETHERNET_INTERFACES=Interface-1=eth0,Interface-2=eth1" "target env must bind logical Ethernet identities before reading carrier" Assert-Contains $envText "TERA_API_UPDATE_SOCKET=/run/tera-updated/control.sock" "target env must configure the protected update socket" Assert-Contains $envText "TERA_API_UPDATE_MAX_BYTES=1342177280" "target API must enforce the update package maximum" Assert-Contains $envText "TERA_API_FIRMWARE_TIMEOUT_SECONDS=1800" "target API must use a route-specific bounded firmware timeout" if ($envText.Contains("TERA_API_READ_TIMEOUT_SECONDS=1800") -or $envText.Contains("TERA_API_WRITE_TIMEOUT_SECONDS=1800")) { throw "firmware upload must not expand the global API read/write timeouts" } Assert-Contains $envText "TERA_API_DIAGNOSTICS_ENABLED=true" "target env must enable the bounded diagnostics executor" Assert-Contains $envText "TERA_API_DIAGNOSTICS_PING_PATH=/bin/ping" "target env must use a fixed ping executable" Assert-Contains $envText "TERA_API_DIAGNOSTICS_WORKERS=2" "target diagnostics workers must remain bounded" Assert-Contains $envText "TERA_API_DIAGNOSTICS_QUEUE=16" "target diagnostics queue must remain bounded" Assert-Contains $envText "TERA_API_SUPPORT_BUNDLE_DIR=/var/lib/tera-api/support-bundles" "target support bundles must use the protected API state directory" Assert-Contains $envText "TERA_API_SUPPORT_BUNDLE_MAX_ARTIFACTS=4" "target support bundle retention count must remain bounded" Assert-Contains $envText "TERA_API_SUPPORT_BUNDLE_MAX_BYTES=8388608" "target support bundle artifact size must remain bounded" Assert-Contains $envText "TERA_API_SUPPORT_BUNDLE_MAX_SECTION_BYTES=2097152" "target support bundle section size must remain bounded" Assert-Contains $envText "TERA_API_AUTH_STORE=/var/lib/tera-api/auth.json" "target local users must persist across service restarts" Assert-Contains $envText "TERA_API_SECRET_STORE=/var/lib/tera-api/secrets.json" "protected integration secrets must use the service-owned store path" Assert-Contains $envText "TERA_API_MIN_PASSWORD_LENGTH=8" "target local password policy must enforce the approved eight-character minimum" Assert-Contains $envText "TERA_API_PASSWORD_KDF=argon2id" "target env must default to Argon2id" Assert-Contains $envText "TERA_API_ALLOW_MOCK_ACTORS=false" "target env must disable mock actors" Assert-Contains $hwdEnvText "TERA_HWD_GREEN_LED_SYSFS_ROOT=/sys/class/leds" "hardware env must use kernel gpio-leds entries" Assert-Contains $hwdEnvText "TERA_HWD_RESET_BUTTON_VALUE_PATH=/sys/class/gpio/manual_KeyReset/value" "hardware env must bind the active-low SW2 reset input" Assert-Contains $envText "TERA_API_RESET_BUTTON_POLL_MILLISECONDS=100" "API env must poll the SW2 reset input promptly" Assert-Contains $envText "TERA_API_RESET_BUTTON_HOLD_MILLISECONDS=1000" "API env must debounce SW2 before resetting settings" Assert-Contains $netdEnvText "TERA_NETD_INTERFACES=/etc/network/interfaces" "network adapter must own the target interface file" Assert-Contains $netdEnvText "TERA_NETD_PENDING=/var/lib/tera-network/pending.json" "network adapter must persist rollback state" Assert-Contains $timedEnvText "TERA_TIMED_ZONEINFO_ROOT=/usr/share/zoneinfo" "time adapter must use the target zoneinfo database" Assert-Contains $timedEnvText "TERA_TIMED_LOCALTIME_PATH=/etc/localtime" "time adapter must own only the product localtime path" Assert-Contains $alarmdEnvText "TERA_ALARMD_STATE=/var/lib/tera-alarmd/deliveries.json" "alarm delivery retries must use the protected service state path" Assert-Contains $alarmdEnvText "TERA_ALARMD_MAX_JOBS=256" "alarm delivery history and queue must remain bounded" Assert-Contains $envText "TERA_API_SNMP_SOCKET=/run/tera-snmpd/control.sock" "API must use the privileged SNMP control socket" Assert-Contains $snmpdEnvText "TERA_SNMPD_CONFIG=/etc/snmp/snmpd.conf" "SNMP adapter must use the Net-SNMP configuration path" Assert-Contains $snmpdEnvText "TERA_SNMPD_PERSISTENT_CONFIG=/var/lib/net-snmp/snmpd.conf" "SNMPv3 bootstrap credentials must use the protected Net-SNMP state path" Assert-Contains $logdEnvText "TERA_LOGD_ROOT=/userdata/log" "log service must use only the userdata log root" Assert-Contains $logdEnvText "TERA_LOGD_STORAGE_POLICY=/userdata/storage-layout.json" "log service must consume the shared capacity policy" Assert-Contains $logdEnvText "TERA_LOGD_SEGMENT_BYTES=4194304" "log service closed segments must remain bounded" Assert-Contains $updatedEnvText "TERA_UPDATED_STORAGE_POLICY=/userdata/storage-layout.json" "updater must consume the shared capacity policy" Assert-Contains $updatedEnvText "TERA_UPDATED_TRUST_DIR=/etc/tera/update-trust" "updater trust anchors must remain outside userdata" Assert-Contains $updatedEnvText "TERA_UPDATED_PROFILE=fet3568j-c" "updater must bind packages to the target profile" Assert-Contains $updatedEnvText "TERA_UPDATED_RECOVERY_APPLY_ENABLED=false" "recovery Apply must remain default-off until target evidence promotion" Assert-Contains $updatedEnvText "TERA_UPDATED_RECOVERY_GATE=/etc/tera/recovery-apply-enabled.json" "recovery Apply must require the target evidence gate" Assert-Contains $updatedEnvText "TERA_UPDATED_MISC_DEVICE=/dev/mmcblk0p2" "updater must bind the BCB request to the fixed misc partition" Assert-Contains $serialdEnvText "TERA_SERIALD_MAPPING=/etc/tera/serial-map.json" "serial controller must require an explicit mapping artifact" Assert-Contains $serialdEnvText "TERA_SERIALD_STREAM_SOCKET=/run/tera-seriald/serial-stream.sock" "serial controller must configure the separate data-plane socket" Assert-Contains $serialdEnvText "TERA_SERIALD_EVENT_QUEUE=1024" "serial metadata observation must use a bounded queue" Assert-Contains $serialdEnvText "TERA_SERIALD_REPORT_INTERVAL_SECONDS=10" "serial metadata observation must use a bounded report interval" Assert-Contains $serialdEnvText "TERA_SERIALD_LOG_SOCKET=/run/tera-logd/log-store.sock" "serial payload capture must use the authoritative bounded log service" Assert-Contains $envText "TERA_API_DIRECT_TICKET_SOCKET=/run/tera-api/direct-tickets.sock" "API must configure the Direct Mode ticket socket" Assert-Contains $envText "TERA_API_SESSIOND_STATUS_URL=http://127.0.0.1:8081/readyz" "API must read actual session runtime health over loopback" Assert-Contains $envText "TERA_API_SERIAL_RECONCILE_INTERVAL_SECONDS=30" "API must repair serial profiles after seriald restarts" Assert-Contains $sessiondEnvText "TERA_SESSIOND_TICKET_SOCKET=/run/tera-api/direct-tickets.sock" "session broker must consume API tickets locally" Assert-Contains $sessiondEnvText "TERA_SESSIOND_SERIAL_STREAM_SOCKET=/run/tera-seriald/serial-stream.sock" "session broker must use the serial data-plane socket" Assert-Contains $webEnvText "TERA_WEB_LISTEN_ADDR=0.0.0.0:8080" "same-origin gateway must own the public listener" Assert-Contains $webEnvText "TERA_WEB_API_UPSTREAM=http://127.0.0.1:8082" "same-origin gateway must route REST to loopback tera-api" Assert-Contains $webEnvText "TERA_WEB_SESSION_UPSTREAM=http://127.0.0.1:8081" "same-origin gateway must route streams to loopback tera-sessiond" if ($webEnvText -match "(?m)^PORT=" -or $webEnvText -match "(?m)^TERACLONE_DB_PATH=") { throw "static tera-web env must not contain Clone Demo runtime settings" } Assert-Contains $cloneDemoWebEnvText "PORT=8083" "Clone Demo web must use its dedicated public port" Assert-Contains $cloneDemoWebEnvText "TERACLONE_API_UPSTREAM=http://127.0.0.1:8082" "Clone Demo web must route REST to loopback tera-api" Assert-Contains $cloneDemoWebEnvText "TERACLONE_SESSION_UPSTREAM=http://127.0.0.1:8081" "Clone Demo web must route streams to loopback tera-sessiond" Assert-Contains $cloneDemoWebEnvText "TERACLONE_DB_PATH=/var/lib/tera-clone-demo-web/teraclone.db" "Clone Demo web must use isolated state" $serialMap = $serialMapText | ConvertFrom-Json if ($serialMap.format_version -ne 1 -or $serialMap.hardware_profile -ne "fet3568j-c" -or -not $serialMap.validated) { throw "serial mapping metadata is invalid" } if ($serialMap.channels.Count -ne 48) { throw "serial mapping must contain exactly 48 channels" } for ($index = 1; $index -le 48; $index++) { $entry = $serialMap.channels[$index - 1] $expectedChannel = "CH{0:d2}" -f $index $expectedTTY = if ($index -le 24) { $index + 3 } else { $index + 7 } $expectedDevice = "/dev/ttyWCH$expectedTTY" $expectedBDF = if ($index -le 24) { "0000:01:00.0" } else { "0002:21:00.0" } if ($entry.channel -ne $expectedChannel -or $entry.device -ne $expectedDevice -or $entry.pci_bdf -ne $expectedBDF) { throw "serial mapping entry $expectedChannel does not match the canonical 24+24 layout" } } if ($InitSystem -eq "systemd") { $unitText = Get-Content -Raw -LiteralPath $unitTarget $accessdUnitText = Get-Content -Raw -LiteralPath $accessdUnitTarget $hwdUnitText = Get-Content -Raw -LiteralPath $hwdUnitTarget $netdUnitText = Get-Content -Raw -LiteralPath $netdUnitTarget $timedUnitText = Get-Content -Raw -LiteralPath $timedUnitTarget $alarmdUnitText = Get-Content -Raw -LiteralPath $alarmdUnitTarget $snmpdUnitText = Get-Content -Raw -LiteralPath $snmpdUnitTarget $logdUnitText = Get-Content -Raw -LiteralPath $logdUnitTarget $updatedUnitText = Get-Content -Raw -LiteralPath $updatedUnitTarget $serialdUnitText = Get-Content -Raw -LiteralPath $serialdUnitTarget $sessiondUnitText = Get-Content -Raw -LiteralPath $sessiondUnitTarget $webUnitText = Get-Content -Raw -LiteralPath $webUnitTarget $cloneDemoWebUnitText = Get-Content -Raw -LiteralPath $cloneDemoWebUnitTarget Assert-Contains $unitText "ExecStart=/usr/bin/tera-api" "systemd unit must execute the installed API binary" Assert-Contains $unitText "EnvironmentFile=-/etc/tera/tera-api.env" "systemd unit must load the target environment file" Assert-Contains $unitText "User=tera" "systemd unit must run tera-api as the product service user" Assert-Contains $unitText "NoNewPrivileges=true" "systemd unit must enable basic privilege hardening" Assert-Contains $unitText "UMask=0007" "API ticket socket must remain group-restricted" Assert-Contains $unitText "Wants=network-online.target tera-hwd.service tera-seriald.service tera-netd.service tera-logd.service tera-timed.service tera-alarmd.service tera-snmpd.service tera-updated.service tera-accessd.service" "tera-api must request its local service adapters" Assert-Contains $accessdUnitText "ExecStart=/usr/bin/tera-accessd" "management access unit must execute the bounded listener service" Assert-Contains $accessdUnitText "CapabilityBoundingSet=CAP_NET_BIND_SERVICE" "management access unit must retain only privileged-port binding capability" Assert-Contains $accessdUnitText "ReadWritePaths=/run/tera-accessd /var/lib/tera-api/tls" "management access unit writes must stay in declared runtime and TLS state paths" Assert-Contains $hwdUnitText "ExecStart=/usr/bin/tera-hwd" "hardware unit must execute the installed helper" Assert-Contains $hwdUnitText "User=root" "hardware unit must own privileged gpio-led writes" Assert-Contains $hwdUnitText "Group=tera" "hardware unit socket must be accessible to tera-api" Assert-Contains $hwdUnitText "NoNewPrivileges=true" "hardware unit must enable basic privilege hardening" Assert-Contains $hwdUnitText "ReadWritePaths=/run/tera-hwd /sys/class/leds" "hardware unit must only write its socket and gpio-led entries" Assert-Contains $netdUnitText "ExecStart=/usr/bin/tera-netd" "network unit must execute the privileged adapter" Assert-Contains $netdUnitText "CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW" "network unit must retain only network administration capabilities" Assert-Contains $netdUnitText "ReadWritePaths=/run/tera-netd /var/lib/tera-network /etc/network" "network unit must restrict declared write paths" Assert-Contains $timedUnitText "ExecStart=/usr/bin/tera-timed" "time unit must execute the installed helper" Assert-Contains $timedUnitText "User=root" "time unit must own privileged clock changes" Assert-Contains $timedUnitText "Group=tera" "time unit socket must be accessible to tera-api" Assert-Contains $timedUnitText "CapabilityBoundingSet=CAP_SYS_TIME" "time unit must retain only the system-time capability" Assert-Contains $timedUnitText "ReadWritePaths=/run/tera-timed /etc/localtime" "time unit must restrict declared write paths" Assert-Contains $alarmdUnitText "ExecStart=/usr/bin/tera-alarmd" "alarm unit must execute the bounded delivery service" Assert-Contains $alarmdUnitText "User=tera" "alarm delivery must run without root privileges" Assert-Contains $alarmdUnitText "ReadWritePaths=/var/lib/tera-alarmd /run/tera-alarmd" "alarm unit must restrict declared write paths" Assert-Contains $snmpdUnitText "ExecStart=/usr/bin/tera-snmpd" "SNMP unit must execute the privileged adapter" Assert-Contains $snmpdUnitText "User=root" "SNMP adapter must own protected Net-SNMP configuration" Assert-Contains $snmpdUnitText "ReadWritePaths=/run/tera-snmpd /etc/snmp /var/lib/net-snmp" "SNMP adapter writes must stay in declared paths" Assert-Contains $logdUnitText "ExecStart=/usr/bin/tera-logd" "log unit must execute the authoritative storage service" Assert-Contains $logdUnitText "User=tera-logd" "log unit must use the dedicated userdata owner" Assert-Contains $logdUnitText "ReadWritePaths=/userdata/log /run/tera-logd" "log unit must never spill into rootfs" Assert-Contains $updatedUnitText "ExecStart=/usr/bin/tera-updated" "update unit must execute the signed package validator" Assert-Contains $updatedUnitText "User=tera-updated" "update unit must use the dedicated storage owner" Assert-Contains $updatedUnitText "ReadWritePaths=/userdata/update /run/tera-updated" "update unit must restrict write paths" Assert-Contains $serialdUnitText "ExecStart=/usr/bin/tera-seriald" "serial unit must execute the installed controller" Assert-Contains $serialdUnitText "User=tera" "serial controller must run without root privileges" Assert-Contains $serialdUnitText "DevicePolicy=closed" "serial controller must close unrelated devices" Assert-Contains $serialdUnitText "DeviceAllow=/dev/ttyWCH* rw" "serial controller must only access WCH tty devices" Assert-Contains $serialdUnitText "UMask=0007" "serial controller sockets must remain group-restricted" Assert-Contains $serialdUnitText "Wants=tera-logd.service" "serial payload capture must request the authoritative log service" Assert-Contains $sessiondUnitText "ExecStart=/usr/bin/tera-sessiond" "session unit must execute the bounded broker" Assert-Contains $sessiondUnitText "User=tera" "session broker must run without root privileges" Assert-Contains $sessiondUnitText "UMask=0007" "session broker local IPC must remain group-restricted" Assert-Contains $webUnitText "ExecStart=/usr/bin/tera-web" "web unit must execute the static same-origin gateway" Assert-Contains $webUnitText "ReadOnlyPaths=/usr/share/tera-web" "web unit must treat static assets as read-only" Assert-Contains $webUnitText "UMask=0027" "static gateway files must not become world-writable" Assert-Contains $cloneDemoWebUnitText "ExecStart=/usr/bin/tera-clone-demo-web" "Clone Demo unit must execute its dedicated binary" Assert-Contains $cloneDemoWebUnitText "EnvironmentFile=-/etc/tera/tera-clone-demo-web.env" "Clone Demo unit must load its dedicated environment" Assert-Contains $cloneDemoWebUnitText "ReadWritePaths=/var/lib/tera-clone-demo-web" "Clone Demo unit must isolate its writable state" } else { $buildrootSerialdText = Get-Content -Raw -LiteralPath $buildrootSerialdTarget $buildrootHwdText = Get-Content -Raw -LiteralPath $buildrootHwdTarget $buildrootNetdText = Get-Content -Raw -LiteralPath $buildrootNetdTarget $buildrootTimedText = Get-Content -Raw -LiteralPath $buildrootTimedTarget $buildrootAlarmdText = Get-Content -Raw -LiteralPath $buildrootAlarmdTarget $buildrootSNMPDText = Get-Content -Raw -LiteralPath $buildrootSNMPDTarget $buildrootLogdText = Get-Content -Raw -LiteralPath $buildrootLogdTarget $buildrootUpdatedText = Get-Content -Raw -LiteralPath $buildrootUpdatedTarget $buildrootAPIText = Get-Content -Raw -LiteralPath $buildrootAPITarget $buildrootAccessdText = Get-Content -Raw -LiteralPath $buildrootAccessdTarget $buildrootSessiondText = Get-Content -Raw -LiteralPath $buildrootSessiondTarget $buildrootWebText = Get-Content -Raw -LiteralPath $buildrootWebTarget $buildrootCloneDemoWebText = Get-Content -Raw -LiteralPath $buildrootCloneDemoWebTarget $buildrootUdevText = Get-Content -Raw -LiteralPath $buildrootUdevTarget Assert-Contains $buildrootSerialdText "DAEMON=/usr/bin/tera-seriald" "Buildroot serial init must execute tera-seriald" Assert-Contains $buildrootSerialdText "RUN_AS=tera:tera" "Buildroot serial init must drop privileges" Assert-Contains $buildrootSerialdText "normalize_service_account_password" "Buildroot service account setup must avoid BusyBox bad-salt sentinels" Assert-Contains $buildrootSerialdText 'mkpasswd -m sha-256' "Buildroot service account sentinel must use a supported non-plaintext hash" Assert-Contains $buildrootSerialdText '-s /bin/false' "Buildroot tera service account must remain non-interactive" Assert-Contains $buildrootSerialdText "start-stop-daemon" "Buildroot serial init must use BusyBox supervision" Assert-Contains $buildrootHwdText "DAEMON=/usr/bin/tera-hwd" "Buildroot hardware init must execute tera-hwd" Assert-Contains $buildrootHwdText "RUN_AS=root:tera" "Buildroot hardware init must retain root UID with only the shared service group" Assert-Contains $buildrootHwdText "umask 0007" "Buildroot hardware socket must not be world accessible" Assert-Contains $buildrootNetdText "DAEMON=/usr/bin/tera-netd" "Buildroot network init must execute tera-netd" Assert-Contains $buildrootNetdText "RUN_AS=root:tera" "Buildroot network adapter must retain root UID with only the shared service group" Assert-Contains $buildrootNetdText "chmod 0700 /var/lib/tera-network" "Buildroot rollback state must be root-only" Assert-Contains $buildrootTimedText "DAEMON=/usr/bin/tera-timed" "Buildroot time init must execute tera-timed" Assert-Contains $buildrootTimedText "chown root:tera /run/tera-timed" "Buildroot time socket directory must be group restricted" Assert-Contains $buildrootTimedText "RUN_AS=root:tera" "Buildroot time adapter must retain root UID with only the shared service group" Assert-Contains $buildrootTimedText "umask 0007" "Buildroot time socket must not be world accessible" Assert-Contains $buildrootAlarmdText "DAEMON=/usr/bin/tera-alarmd" "Buildroot alarm init must execute tera-alarmd" Assert-Contains $buildrootAlarmdText "start-stop-daemon" "Buildroot alarm init must use BusyBox supervision" Assert-Contains $buildrootSNMPDText "DAEMON=/usr/bin/tera-snmpd" "Buildroot SNMP init must execute tera-snmpd" Assert-Contains $buildrootSNMPDText "RUN_AS=root:tera" "Buildroot SNMP adapter must retain root UID with the shared service group" Assert-Contains $buildrootSNMPDText "chmod 0700 /var/lib/net-snmp" "Buildroot SNMPv3 state must remain root-only" Assert-Contains $buildrootLogdText "DAEMON=/usr/bin/tera-logd" "Buildroot log init must execute tera-logd" Assert-Contains $buildrootLogdText "RUN_AS=tera-logd:tera" "Buildroot log init must isolate storage ownership while sharing the API socket group" Assert-Contains $buildrootLogdText "missing /userdata/log" "Buildroot log init must fail closed without userdata logging" Assert-Contains $buildrootUpdatedText "DAEMON=/usr/bin/tera-updated" "Buildroot update init must execute tera-updated" Assert-Contains $buildrootUpdatedText "RUN_AS=tera-updated:tera" "Buildroot update init must isolate storage ownership while sharing the API socket group" Assert-Contains $buildrootUpdatedText "/usr/libexec/tera-storage-init --check" "Buildroot update init must fail closed when storage policy is invalid" Assert-Contains $buildrootAPIText "DAEMON=/usr/bin/tera-api" "Buildroot API init must execute tera-api" Assert-Contains $buildrootAPIText "RUN_AS=tera:tera" "Buildroot API init must drop privileges" Assert-Contains $buildrootAPIText "chown -R tera:tera /var/lib/tera-api /var/log/tera" "Buildroot API init must repair restored state ownership" Assert-Contains $buildrootAPIText 'wait_for_socket "$TERA_API_LOG_SOCKET"' "Buildroot API init must wait for the authoritative log service before startup" Assert-Contains $buildrootAccessdText "DAEMON=/usr/bin/tera-accessd" "Buildroot management access init must execute tera-accessd" Assert-Contains $buildrootAccessdText "chown -R tera:tera /var/lib/tera-api/tls" "Buildroot TLS state must be owned by the restricted service account" Assert-Contains $buildrootAccessdText "chmod 0700 /var/lib/tera-api/tls /var/lib/tera-api/tls/library" "Buildroot TLS private state must remain service-account-only" Assert-Contains $buildrootAccessdText "chown root:tera /run/tera-accessd" "Buildroot management control directory must be group traversable" Assert-Contains $buildrootSessiondText "DAEMON=/usr/bin/tera-sessiond" "Buildroot session init must execute tera-sessiond" Assert-Contains $buildrootSessiondText "RUN_AS=tera:tera" "Buildroot session init must drop privileges" Assert-Contains $buildrootWebText "DAEMON=/usr/bin/tera-web" "Buildroot web init must execute tera-web" Assert-Contains $buildrootWebText "RUN_AS=tera:tera" "Buildroot web init must drop privileges" Assert-Contains $buildrootCloneDemoWebText "DAEMON=/usr/bin/tera-clone-demo-web" "Buildroot Clone Demo init must execute its dedicated binary" Assert-Contains $buildrootCloneDemoWebText "ENVFILE=/etc/tera/tera-clone-demo-web.env" "Buildroot Clone Demo init must load its dedicated environment" Assert-Contains $buildrootCloneDemoWebText "STATE_DIR=/var/lib/tera-clone-demo-web" "Buildroot Clone Demo init must isolate its writable state" Assert-Contains $buildrootUdevText 'KERNEL=="ttyWCH[0-9]*", GROUP="tera", MODE="0660"' "Buildroot udev rule must restrict WCH access to the tera group" foreach ($initText in @($buildrootSerialdText, $buildrootHwdText, $buildrootNetdText, $buildrootLogdText, $buildrootTimedText, $buildrootAlarmdText, $buildrootSNMPDText, $buildrootUpdatedText, $buildrootAccessdText, $buildrootAPIText, $buildrootSessiondText, $buildrootWebText, $buildrootCloneDemoWebText)) { Assert-Contains $initText 'rm -f "$PIDFILE"' "Buildroot service init must remove stale PID files before start" } } if ($envText -match "(?im)^\s*TERA_API_CORS_ORIGINS\s*=\s*\*") { throw "target env must not enable wildcard CORS" } Assert-NoSecretEnvironmentValues $envText Assert-NoSecretEnvironmentValues $accessdEnvText Assert-NoSecretEnvironmentValues $hwdEnvText Assert-NoSecretEnvironmentValues $netdEnvText Assert-NoSecretEnvironmentValues $timedEnvText Assert-NoSecretEnvironmentValues $alarmdEnvText Assert-NoSecretEnvironmentValues $snmpdEnvText Assert-NoSecretEnvironmentValues $logdEnvText Assert-NoSecretEnvironmentValues $updatedEnvText Assert-NoSecretEnvironmentValues $serialdEnvText Assert-NoSecretEnvironmentValues $sessiondEnvText Assert-NoSecretEnvironmentValues $webEnvText Assert-NoSecretEnvironmentValues $cloneDemoWebEnvText $artifact = Get-Item -LiteralPath $binTarget $accessdArtifact = Get-Item -LiteralPath $accessdBinTarget $hwdArtifact = Get-Item -LiteralPath $hwdBinTarget $netdArtifact = Get-Item -LiteralPath $netdBinTarget $timedArtifact = Get-Item -LiteralPath $timedBinTarget $alarmdArtifact = Get-Item -LiteralPath $alarmdBinTarget $snmpdArtifact = Get-Item -LiteralPath $snmpdBinTarget $logdArtifact = Get-Item -LiteralPath $logdBinTarget $updatedArtifact = Get-Item -LiteralPath $updatedBinTarget $serialdArtifact = Get-Item -LiteralPath $serialdBinTarget $sessiondArtifact = Get-Item -LiteralPath $sessiondBinTarget $webArtifact = Get-Item -LiteralPath $webBinTarget $cloneDemoWebArtifact = Get-Item -LiteralPath $cloneDemoWebBinTarget $serialCheckArtifact = Get-Item -LiteralPath $serialCheckBinTarget $serialLineCheckArtifact = Get-Item -LiteralPath $serialLineCheckBinTarget if ($createdTempRoot -and -not $KeepStaging) { Write-Host "[check-target-rootfs-staging] Verified temporary tera-api $InitSystem rootfs staging at $StagingRoot" } else { Write-Host "[check-target-rootfs-staging] Staged tera-api $InitSystem rootfs at $StagingRoot" } Write-Host "[check-target-rootfs-staging] Binary size: $($artifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Management access service size: $($accessdArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Hardware helper size: $($hwdArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Network helper size: $($netdArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] System time helper size: $($timedArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Alarm delivery service size: $($alarmdArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] SNMP agent adapter size: $($snmpdArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Authoritative log service size: $($logdArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Firmware validation service size: $($updatedArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Serial controller size: $($serialdArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Session broker size: $($sessiondArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Static gateway size: $($webArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Clone Demo web size: $($cloneDemoWebArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Read-only serial checker size: $($serialCheckArtifact.Length) bytes" Write-Host "[check-target-rootfs-staging] Restoring serial line checker size: $($serialLineCheckArtifact.Length) bytes" } finally { if ($createdTempRoot -and -not $KeepStaging -and (Test-Path -LiteralPath $StagingRoot)) { Remove-Item -LiteralPath $StagingRoot -Recurse -Force } }