191 lines
7.9 KiB
PowerShell
191 lines
7.9 KiB
PowerShell
$ErrorActionPreference = "Stop"
|
|
$installer = Join-Path (Split-Path $PSScriptRoot -Parent) "install.ps1"
|
|
$tokens = $null
|
|
$parseErrors = $null
|
|
[System.Management.Automation.Language.Parser]::ParseFile(
|
|
$installer, [ref]$tokens, [ref]$parseErrors
|
|
) | Out-Null
|
|
if ($parseErrors.Count -gt 0) {
|
|
throw "install.ps1 parser errors: $($parseErrors.Message -join '; ')"
|
|
}
|
|
|
|
$source = Get-Content -LiteralPath $installer -Raw
|
|
function Assert-SourceContains {
|
|
param([Parameter(Mandatory)][string]$Pattern, [Parameter(Mandatory)][string]$Message)
|
|
if ($source -notmatch $Pattern) { throw $Message }
|
|
}
|
|
|
|
$runtimeMatch = [regex]::Match(
|
|
$source,
|
|
'(?s)\$RuntimeFiles\s*=\s*@\((?<files>.*?)\)'
|
|
)
|
|
if (!$runtimeMatch.Success) { throw "Runtime file allowlist was not found." }
|
|
$runtimeFiles = $runtimeMatch.Groups["files"].Value
|
|
foreach ($forbidden in @("agent-config.json", "agent-state.json", "logs", "backups")) {
|
|
if ($runtimeFiles -match [regex]::Escape($forbidden)) {
|
|
throw "$forbidden must not be a managed deployment target."
|
|
}
|
|
}
|
|
|
|
$preflight = $source.IndexOf('# Validate the complete source')
|
|
$firstTargetChange = $source.IndexOf('New-Item -ItemType Directory')
|
|
if ($preflight -lt 0 -or $preflight -gt $firstTargetChange) {
|
|
throw "Source preflight must happen before target changes."
|
|
}
|
|
|
|
Assert-SourceContains '\$existingTask\s*=\s*Get-MinerOpsScheduledTask' `
|
|
"Existing task preservation guard is missing."
|
|
Assert-SourceContains 'Preserving existing Scheduled Task' `
|
|
"Existing task preservation is not explicit."
|
|
Assert-SourceContains 'ExtraArguments\s*=\s*"-Limit 0 -TimeoutMs 8000"' `
|
|
"Fresh enrichment task arguments are missing."
|
|
Assert-SourceContains 'Stop-MinerOpsScheduledTaskForUpgrade' `
|
|
"Bounded main Agent stop behavior is missing."
|
|
Assert-SourceContains '\$mainTaskWasRunning' `
|
|
"Main Agent prior running state is not represented."
|
|
Assert-SourceContains 'Copy-Item[^\r\n]*\(Join-Path \$Backup \$name\)' `
|
|
"Rollback does not restore managed files from backup."
|
|
Assert-SourceContains 'Unregister-ScheduledTask' `
|
|
"Rollback does not remove tasks created by a failed install."
|
|
Assert-SourceContains 'could not restart restored MinerOpsAgent' `
|
|
"Rollback restart failure is not surfaced."
|
|
Assert-SourceContains 'Assert-MinerOpsInstallerTrustedAncestors' `
|
|
"Installer does not validate ancestor namespace protection."
|
|
|
|
foreach ($functionName in @(
|
|
"Assert-MinerOpsInstallerNoReparsePath",
|
|
"Assert-MinerOpsInstallerTrustedAncestors"
|
|
)) {
|
|
$functionSource = [regex]::Match(
|
|
$source,
|
|
"(?s)function $functionName \{.*?\n\}"
|
|
).Value
|
|
if ([string]::IsNullOrWhiteSpace($functionSource)) {
|
|
throw "$functionName source was not found."
|
|
}
|
|
if ($functionSource -notmatch '\$current\s*=\s*\$root') {
|
|
throw "$functionName does not preserve the filesystem root."
|
|
}
|
|
if ($functionSource -notmatch '\$fullPath\.Equals\(\s*\$root') {
|
|
throw "$functionName does not protect a root-valued full path from trimming."
|
|
}
|
|
if ($functionSource -match '\$root\.TrimEnd\(') {
|
|
throw "$functionName converts a root-qualified path to drive-relative form."
|
|
}
|
|
if ($functionSource -match 'GetFullPath\(\$Path\)\.TrimEnd\(') {
|
|
throw "$functionName trims the filesystem root before identifying it."
|
|
}
|
|
if ($functionName -eq "Assert-MinerOpsInstallerTrustedAncestors" -and
|
|
$functionSource -notmatch '\$parent\.Equals\(\s*\$root') {
|
|
throw "$functionName does not protect a root-valued parent from trimming."
|
|
}
|
|
}
|
|
|
|
Assert-SourceContains 'Open-MinerOpsInstallerDirectoryLock' `
|
|
"Installer does not lock an insecure target against replacement."
|
|
Assert-SourceContains 'minerops-filesystem-trust\.blocked-' `
|
|
"Installer directory repair lacks an external durable provenance blocker."
|
|
Assert-SourceContains 'ApproveFilesystemTrustResetAfterReboot' `
|
|
"Installer lacks an explicit post-reboot trust-reset gate."
|
|
Assert-SourceContains 'DirectoryInfo\(\$staging\)' `
|
|
"Installer does not stage a protected new directory before publication."
|
|
Assert-SourceContains 'Assert-MinerOpsInstallerDirectory' `
|
|
"Installer does not read back the final directory security invariant."
|
|
Assert-SourceContains 'Protect-MinerOpsInstallerExistingConfig' `
|
|
"Installer does not contain the existing active-config ACL."
|
|
Assert-SourceContains 'GetNumberOfLinks' `
|
|
"Installer does not reject hard-linked runtime, config, or backup files."
|
|
Assert-SourceContains 'Assert-MinerOpsInstallerTrustedRuntimeFile' `
|
|
"Installer does not verify existing runtime rollback provenance."
|
|
Assert-SourceContains 'Protect-MinerOpsInstallerRuntimeFile' `
|
|
"Installer does not secure and verify published runtime and rollback copies."
|
|
Assert-SourceContains 'Pre-reset runtime is evidence, never rollback authority' `
|
|
"Installer trust reset can adopt pre-reset runtime as rollback authority."
|
|
Assert-SourceContains 'parent-owned filesystem trust blocker remains' `
|
|
"Installer does not explicitly retain the parent-owned trust blocker."
|
|
Assert-SourceContains 'windows-full-boot-event-v1:' `
|
|
"Installer blockers do not use canonical full-kernel-boot records."
|
|
if ($source -match 'Remove-MinerOpsInstallerTrustBlocker|Remove-Item[^\r\n]*TrustBlocker') {
|
|
throw "Installer must have no filesystem trust-blocker deletion path."
|
|
}
|
|
if ($source -match 'LastBootUpTime') {
|
|
throw "Installer must not create wall-clock boot-epoch records."
|
|
}
|
|
|
|
$createStreamFunction = [regex]::Match(
|
|
$source,
|
|
'(?s)function New-MinerOpsInstallerProtectedFileCreateNewStream \{.*?\n\}'
|
|
).Value
|
|
foreach ($requiredCreateContract in @(
|
|
'\[IO\.FileStream\]::new',
|
|
'FileMode\]::CreateNew',
|
|
'FileSystemRights\]::WriteData',
|
|
'FileSystemRights\]::ReadPermissions',
|
|
'FileSystemRights\]::Synchronize',
|
|
'FileShare\]::None',
|
|
'\$Acl'
|
|
)) {
|
|
if ($createStreamFunction -notmatch $requiredCreateContract) {
|
|
throw "Installer protected CreateNew stream contract is incomplete."
|
|
}
|
|
}
|
|
if ($createStreamFunction -match 'FullControl|SetAccessControl|Set-Acl') {
|
|
throw "Installer protected creation requests excess rights or repairs by path."
|
|
}
|
|
|
|
$trustBlockerFunction = [regex]::Match(
|
|
$source,
|
|
'(?s)function New-MinerOpsInstallerTrustBlocker \{.*?\n\}'
|
|
).Value
|
|
$trustCreateIndex = $trustBlockerFunction.IndexOf(
|
|
'New-MinerOpsInstallerProtectedFileCreateNewStream'
|
|
)
|
|
$trustVerifyIndex = $trustBlockerFunction.IndexOf(
|
|
'Test-MinerOpsInstallerProtectedFileAcl'
|
|
)
|
|
$trustContentIndex = $trustBlockerFunction.IndexOf('$record =')
|
|
if (
|
|
$trustCreateIndex -lt 0 -or
|
|
$trustVerifyIndex -le $trustCreateIndex -or
|
|
$trustContentIndex -le $trustVerifyIndex
|
|
) {
|
|
throw "Installer trust blockers must be protected and verified before content."
|
|
}
|
|
|
|
$configRepairFunction = [regex]::Match(
|
|
$source,
|
|
'(?s)function Protect-MinerOpsInstallerExistingConfig \{.*?\n\}'
|
|
).Value
|
|
foreach ($requiredRepairRight in @(
|
|
'ReadPermissions',
|
|
'ChangePermissions',
|
|
'TakeOwnership',
|
|
'Synchronize'
|
|
)) {
|
|
if ($configRepairFunction -notmatch $requiredRepairRight) {
|
|
throw "Installer existing-config repair is missing $requiredRepairRight."
|
|
}
|
|
}
|
|
if ($configRepairFunction -match 'Set-Acl|FileAccess') {
|
|
throw "Installer existing-config repair must remain handle-bound."
|
|
}
|
|
|
|
$repairFunction = [regex]::Match(
|
|
$source,
|
|
'(?s)function Initialize-MinerOpsInstallerDirectory \{.*?\n\}'
|
|
).Value
|
|
$repairIndex = $repairFunction.IndexOf('Set-Acl -LiteralPath $fullPath')
|
|
$abortIndex = $repairFunction.IndexOf('this invocation will not back up')
|
|
if ($repairIndex -lt 0 -or $abortIndex -le $repairIndex) {
|
|
throw "Insecure existing-directory containment must abort before publication."
|
|
}
|
|
|
|
if ($source -match 'Register-ScheduledTask[^\r\n]*-Force') {
|
|
throw "Installer must not force-overwrite Scheduled Tasks."
|
|
}
|
|
if ($source -match '(?i)agent_secret|api3_password|api3_account') {
|
|
throw "Installer must not inspect or print credentials."
|
|
}
|
|
|
|
Write-Host "MinerOps installer source safety tests passed."
|