1254 lines
49 KiB
PowerShell
1254 lines
49 KiB
PowerShell
#Requires -RunAsAdministrator
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$ApproveFilesystemTrustResetAfterReboot
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$Target = "C:\MinerOpsAgent"
|
|
$BackupRoot = Join-Path $Target "backups"
|
|
$BackupTimestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
$BackupSuffix = [guid]::NewGuid().ToString("N").Substring(0, 8)
|
|
$Backup = Join-Path $BackupRoot "$BackupTimestamp-$BackupSuffix"
|
|
$RuntimeFiles = @(
|
|
"MinerOpsAgentService.ps1", "MinerOpsAgentTray.ps1", "MinerOpsAgentGUI.ps1",
|
|
"MinerOpsApi3EnrichmentPoller.ps1", "MinerOpsPoolPoller.ps1",
|
|
"MinerOpsPoolPollerTask.ps1", "EnrichOneMiner.ps1", "MinerOpsControl.ps1",
|
|
"VERSION"
|
|
)
|
|
|
|
function Get-MinerOpsInstallerDirectorySids {
|
|
return @(@(
|
|
"S-1-5-18",
|
|
"S-1-5-32-544"
|
|
) | Select-Object -Unique)
|
|
}
|
|
|
|
function New-MinerOpsInstallerDirectoryAcl {
|
|
$acl = New-Object System.Security.AccessControl.DirectorySecurity
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$administrators = New-Object System.Security.Principal.SecurityIdentifier(
|
|
"S-1-5-32-544"
|
|
)
|
|
$acl.SetOwner($administrators)
|
|
foreach ($sidValue in (Get-MinerOpsInstallerDirectorySids)) {
|
|
$sid = New-Object System.Security.Principal.SecurityIdentifier($sidValue)
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
$sid,
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($rule)
|
|
}
|
|
$users = New-Object System.Security.Principal.SecurityIdentifier(
|
|
"S-1-5-32-545"
|
|
)
|
|
$readRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
$users,
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute,
|
|
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($readRule)
|
|
return $acl
|
|
}
|
|
|
|
function New-MinerOpsInstallerProtectedFileAcl {
|
|
$acl = New-Object System.Security.AccessControl.FileSecurity
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$acl.SetOwner((New-Object `
|
|
System.Security.Principal.SecurityIdentifier("S-1-5-32-544")))
|
|
foreach ($sidValue in (Get-MinerOpsInstallerDirectorySids)) {
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
(New-Object System.Security.Principal.SecurityIdentifier($sidValue)),
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($rule)
|
|
}
|
|
return $acl
|
|
}
|
|
|
|
function New-MinerOpsInstallerRuntimeFileAcl {
|
|
$acl = New-Object System.Security.AccessControl.FileSecurity
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$acl.SetOwner((New-Object `
|
|
System.Security.Principal.SecurityIdentifier("S-1-5-32-544")))
|
|
foreach ($sidValue in (Get-MinerOpsInstallerDirectorySids)) {
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
(New-Object System.Security.Principal.SecurityIdentifier($sidValue)),
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($rule)
|
|
}
|
|
$users = New-Object System.Security.Principal.SecurityIdentifier(
|
|
"S-1-5-32-545"
|
|
)
|
|
$readRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
$users,
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute,
|
|
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($readRule)
|
|
return $acl
|
|
}
|
|
|
|
function Test-MinerOpsInstallerRuntimeFileAcl {
|
|
param([Parameter(Mandatory)]$Acl)
|
|
|
|
$expected = @{
|
|
"S-1-5-18" = "full"
|
|
"S-1-5-32-544" = "full"
|
|
"S-1-5-32-545" = "read"
|
|
}
|
|
$seen = @{}
|
|
try {
|
|
$owner = $Acl.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier]
|
|
).Value
|
|
}
|
|
catch { return $false }
|
|
$trustedOwners = @(
|
|
"S-1-5-18",
|
|
"S-1-5-32-544",
|
|
[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
) | Select-Object -Unique
|
|
if ($owner -notin $trustedOwners) { return $false }
|
|
$rules = @($Acl.GetAccessRules(
|
|
$true, $false, [System.Security.Principal.SecurityIdentifier]
|
|
))
|
|
if ($rules.Count -ne $expected.Count) { return $false }
|
|
foreach ($rule in $rules) {
|
|
$sid = $rule.IdentityReference.Value
|
|
if (
|
|
-not $expected.ContainsKey($sid) -or
|
|
$rule.AccessControlType -ne
|
|
[System.Security.AccessControl.AccessControlType]::Allow -or
|
|
$rule.InheritanceFlags -ne
|
|
[System.Security.AccessControl.InheritanceFlags]::None -or
|
|
$rule.PropagationFlags -ne
|
|
[System.Security.AccessControl.PropagationFlags]::None
|
|
) { return $false }
|
|
if ($expected[$sid] -eq "full" -and
|
|
($rule.FileSystemRights -band
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl) -ne
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl) {
|
|
return $false
|
|
}
|
|
if ($expected[$sid] -eq "read") {
|
|
$writeLike = [System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::Write -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Delete -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
)
|
|
if (
|
|
($rule.FileSystemRights -band $writeLike) -ne 0 -or
|
|
($rule.FileSystemRights -band
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute) -ne
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute
|
|
) { return $false }
|
|
}
|
|
$seen[$sid] = $true
|
|
}
|
|
return $seen.Count -eq $expected.Count
|
|
}
|
|
|
|
function Protect-MinerOpsInstallerRuntimeFile {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$Description
|
|
)
|
|
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $Path -Description $Description
|
|
Set-Acl -LiteralPath $Path `
|
|
-AclObject (New-MinerOpsInstallerRuntimeFileAcl) `
|
|
-ErrorAction Stop
|
|
if (-not (Test-MinerOpsInstallerRuntimeFileAcl `
|
|
-Acl (Get-Acl -LiteralPath $Path -ErrorAction Stop))) {
|
|
throw "$Description ACL could not be verified."
|
|
}
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $Path -Description $Description
|
|
}
|
|
|
|
function Assert-MinerOpsInstallerTrustedRuntimeFile {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$Description
|
|
)
|
|
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $Path -Description $Description
|
|
if (-not (Test-MinerOpsInstallerRuntimeFileAcl `
|
|
-Acl (Get-Acl -LiteralPath $Path -ErrorAction Stop))) {
|
|
throw "$Description owner or DACL is not trusted rollback provenance."
|
|
}
|
|
}
|
|
|
|
function Test-MinerOpsInstallerProtectedFileAcl {
|
|
param([Parameter(Mandatory)]$Acl)
|
|
|
|
if (-not $Acl.AreAccessRulesProtected) { return $false }
|
|
try {
|
|
if ($Acl.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier]
|
|
).Value -ne "S-1-5-32-544") { return $false }
|
|
}
|
|
catch { return $false }
|
|
$expected = @{}
|
|
foreach ($sid in (Get-MinerOpsInstallerDirectorySids)) {
|
|
$expected[$sid] = $false
|
|
}
|
|
$rules = @($Acl.GetAccessRules(
|
|
$true, $false, [System.Security.Principal.SecurityIdentifier]
|
|
))
|
|
if ($rules.Count -ne $expected.Count) { return $false }
|
|
foreach ($rule in $rules) {
|
|
$sid = $rule.IdentityReference.Value
|
|
if (
|
|
-not $expected.ContainsKey($sid) -or
|
|
$rule.IsInherited -or
|
|
$rule.AccessControlType -ne
|
|
[System.Security.AccessControl.AccessControlType]::Allow -or
|
|
$rule.FileSystemRights -ne
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl
|
|
) { return $false }
|
|
$expected[$sid] = $true
|
|
}
|
|
return -not ($expected.Values -contains $false)
|
|
}
|
|
|
|
function Set-MinerOpsInstallerStreamAcl {
|
|
param([Parameter(Mandatory)]$Stream, [Parameter(Mandatory)]$Acl)
|
|
if ($Stream.PSObject.Methods.Name -contains "SetAccessControl") {
|
|
$Stream.SetAccessControl($Acl)
|
|
}
|
|
else {
|
|
[System.IO.FileSystemAclExtensions]::SetAccessControl($Stream, $Acl)
|
|
}
|
|
}
|
|
|
|
function New-MinerOpsInstallerProtectedFileCreateNewStream {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)]
|
|
[System.Security.AccessControl.FileSecurity]$Acl
|
|
)
|
|
|
|
$rights = [System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::WriteData -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Synchronize
|
|
)
|
|
return [IO.FileStream]::new(
|
|
$Path,
|
|
[IO.FileMode]::CreateNew,
|
|
$rights,
|
|
[IO.FileShare]::None,
|
|
4096,
|
|
[IO.FileOptions]::None,
|
|
$Acl
|
|
)
|
|
}
|
|
|
|
function Open-MinerOpsInstallerFileStreamWithRights {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)]
|
|
[System.Security.AccessControl.FileSystemRights]$Rights
|
|
)
|
|
|
|
return [IO.FileStream]::new(
|
|
$Path,
|
|
[IO.FileMode]::Open,
|
|
$Rights,
|
|
[IO.FileShare]::None,
|
|
4096,
|
|
[IO.FileOptions]::None
|
|
)
|
|
}
|
|
|
|
function Get-MinerOpsInstallerStreamAcl {
|
|
param([Parameter(Mandatory)]$Stream)
|
|
if ($Stream.PSObject.Methods.Name -contains "GetAccessControl") {
|
|
return $Stream.GetAccessControl()
|
|
}
|
|
return [System.IO.FileSystemAclExtensions]::GetAccessControl($Stream)
|
|
}
|
|
|
|
function Assert-MinerOpsInstallerSingleLinkStream {
|
|
param(
|
|
[Parameter(Mandatory)]$Stream,
|
|
[Parameter(Mandatory)][string]$Description
|
|
)
|
|
|
|
if (-not ("MinerOpsFileIdentityNative" -as [type])) {
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using Microsoft.Win32.SafeHandles;
|
|
public static class MinerOpsFileIdentityNative
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct ByHandleFileInformation
|
|
{
|
|
public uint FileAttributes;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
|
|
public uint VolumeSerialNumber;
|
|
public uint FileSizeHigh;
|
|
public uint FileSizeLow;
|
|
public uint NumberOfLinks;
|
|
public uint FileIndexHigh;
|
|
public uint FileIndexLow;
|
|
}
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool GetFileInformationByHandle(
|
|
SafeFileHandle file, out ByHandleFileInformation information);
|
|
public static uint GetNumberOfLinks(SafeFileHandle file)
|
|
{
|
|
ByHandleFileInformation information;
|
|
if (!GetFileInformationByHandle(file, out information))
|
|
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
return information.NumberOfLinks;
|
|
}
|
|
}
|
|
'@
|
|
}
|
|
if ([MinerOpsFileIdentityNative]::GetNumberOfLinks(
|
|
$Stream.SafeFileHandle
|
|
) -ne 1) {
|
|
throw "$Description must have exactly one filesystem link."
|
|
}
|
|
}
|
|
|
|
function Assert-MinerOpsInstallerSingleLinkPath {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$Description
|
|
)
|
|
|
|
$item = Get-Item -LiteralPath $Path -Force -ErrorAction Stop
|
|
if (
|
|
$item.PSIsContainer -or
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
|
($item.PSObject.Properties.Name -contains "LinkType" -and
|
|
[string]$item.LinkType)
|
|
) {
|
|
throw "$Description is not a regular single-link file."
|
|
}
|
|
$stream = $null
|
|
try {
|
|
$stream = [IO.File]::Open(
|
|
[IO.Path]::GetFullPath($Path),
|
|
[IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read
|
|
)
|
|
Assert-MinerOpsInstallerSingleLinkStream `
|
|
-Stream $stream -Description $Description
|
|
}
|
|
finally {
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
function Get-MinerOpsInstallerTrustBlockerPaths {
|
|
param([Parameter(Mandatory)][string]$Path)
|
|
|
|
$fullPath = [IO.Path]::GetFullPath($Path).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$parent = [IO.Directory]::GetParent($fullPath)
|
|
if ($null -eq $parent) { throw "Agent directory cannot be a filesystem root." }
|
|
$pattern = ([IO.Path]::GetFileName($fullPath)) +
|
|
".minerops-filesystem-trust.blocked-*.state"
|
|
return @(Get-ChildItem -LiteralPath $parent.FullName `
|
|
-Filter $pattern -File -Force -ErrorAction Stop)
|
|
}
|
|
|
|
function ConvertFrom-MinerOpsInstallerKernelBootEventRecord {
|
|
param([Parameter(Mandatory)]$EventRecord)
|
|
|
|
if (
|
|
$null -eq $EventRecord -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "ProviderName" -or
|
|
$EventRecord.ProviderName -isnot [string] -or
|
|
[string]$EventRecord.ProviderName -cne
|
|
"Microsoft-Windows-Kernel-Boot" -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "Id" -or
|
|
$EventRecord.Id -isnot [int] -or
|
|
[int]$EventRecord.Id -ne 27 -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "RecordId" -or
|
|
$EventRecord.RecordId -isnot [long] -or
|
|
[long]$EventRecord.RecordId -le 0 -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "Version" -or
|
|
$EventRecord.Version -isnot [byte] -or
|
|
[byte]$EventRecord.Version -notin @([byte]0, [byte]1) -or
|
|
$EventRecord.PSObject.Methods.Name -cnotcontains "ToXml"
|
|
) {
|
|
throw "Kernel-Boot event metadata is invalid or unsupported."
|
|
}
|
|
|
|
$xmlText = $EventRecord.ToXml()
|
|
if ($xmlText -isnot [string] -or
|
|
[string]::IsNullOrWhiteSpace([string]$xmlText)) {
|
|
throw "Kernel-Boot event XML is missing."
|
|
}
|
|
try { [xml]$eventXml = [string]$xmlText }
|
|
catch { throw "Kernel-Boot event XML is malformed." }
|
|
|
|
$eventNamespace = "http://schemas.microsoft.com/win/2004/08/events/event"
|
|
if (
|
|
$null -eq $eventXml.DocumentElement -or
|
|
$eventXml.DocumentElement.LocalName -cne "Event" -or
|
|
$eventXml.DocumentElement.NamespaceURI -cne $eventNamespace
|
|
) {
|
|
throw "Kernel-Boot event XML root is invalid."
|
|
}
|
|
$namespaceManager = [Xml.XmlNamespaceManager]::new($eventXml.NameTable)
|
|
$namespaceManager.AddNamespace("evt", $eventNamespace)
|
|
$eventNodes = @($eventXml.SelectNodes("/evt:Event", $namespaceManager))
|
|
$systemNodes = @($eventXml.SelectNodes(
|
|
"/evt:Event/evt:System", $namespaceManager
|
|
))
|
|
$eventDataNodes = @($eventXml.SelectNodes(
|
|
"/evt:Event/evt:EventData", $namespaceManager
|
|
))
|
|
$bootTypeNodes = @($eventXml.SelectNodes(
|
|
"/evt:Event/evt:EventData/evt:Data[@Name='BootType']",
|
|
$namespaceManager
|
|
))
|
|
if (
|
|
$eventNodes.Count -ne 1 -or
|
|
$systemNodes.Count -ne 1 -or
|
|
$eventDataNodes.Count -ne 1 -or
|
|
$bootTypeNodes.Count -ne 1 -or
|
|
$bootTypeNodes[0].ChildNodes.Count -ne 1 -or
|
|
$bootTypeNodes[0].FirstChild.NodeType -ne [Xml.XmlNodeType]::Text
|
|
) {
|
|
throw "Kernel-Boot event XML schema is invalid or ambiguous."
|
|
}
|
|
|
|
$bootTypeText = [string]$bootTypeNodes[0].InnerText
|
|
if ($bootTypeText -cnotmatch '^(0|[1-9][0-9]*)$') {
|
|
throw "Kernel-Boot event BootType is not canonical."
|
|
}
|
|
$bootType = [UInt32]0
|
|
if (-not [UInt32]::TryParse(
|
|
$bootTypeText,
|
|
[Globalization.NumberStyles]::None,
|
|
[Globalization.CultureInfo]::InvariantCulture,
|
|
[ref]$bootType
|
|
)) {
|
|
throw "Kernel-Boot event BootType is invalid."
|
|
}
|
|
return [pscustomobject]@{
|
|
RecordId = [long]$EventRecord.RecordId
|
|
Version = [byte]$EventRecord.Version
|
|
BootType = [UInt32]$bootType
|
|
}
|
|
}
|
|
|
|
function Get-MinerOpsInstallerBootIdentifier {
|
|
try {
|
|
$events = @(Get-WinEvent -FilterHashtable @{
|
|
LogName = "System"
|
|
ProviderName = "Microsoft-Windows-Kernel-Boot"
|
|
Id = 27
|
|
} -ErrorAction Stop)
|
|
$fullBootRecordIds = @($events | ForEach-Object {
|
|
$record = ConvertFrom-MinerOpsInstallerKernelBootEventRecord `
|
|
-EventRecord $_
|
|
if ($record.BootType -eq 0) { [long]$record.RecordId }
|
|
} | Sort-Object -Descending)
|
|
if ($fullBootRecordIds.Count -eq 0) {
|
|
throw "Windows full-kernel-boot event is missing."
|
|
}
|
|
for ($index = 1; $index -lt $fullBootRecordIds.Count; $index++) {
|
|
if ([long]$fullBootRecordIds[$index - 1] -eq
|
|
[long]$fullBootRecordIds[$index]) {
|
|
throw "Windows full-kernel-boot event is ambiguous."
|
|
}
|
|
}
|
|
return "windows-full-boot-event-v1:{0:x16}" -f `
|
|
[UInt64]$fullBootRecordIds[0]
|
|
}
|
|
catch { throw "Windows boot identity could not be verified." }
|
|
}
|
|
|
|
function New-MinerOpsInstallerTrustBlocker {
|
|
param([Parameter(Mandatory)][string]$TargetRoot)
|
|
|
|
$fullTarget = [IO.Path]::GetFullPath($TargetRoot).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$parent = [IO.Directory]::GetParent($fullTarget).FullName
|
|
$path = Join-Path $parent (
|
|
(([IO.Path]::GetFileName($fullTarget)) +
|
|
".minerops-filesystem-trust.blocked-{0}.state") -f
|
|
[guid]::NewGuid().ToString("N")
|
|
)
|
|
$stream = $null
|
|
try {
|
|
# This external sibling is durable even if a pre-repair handle can
|
|
# still mutate children of the contaminated Agent directory.
|
|
$stream = New-MinerOpsInstallerProtectedFileCreateNewStream `
|
|
-Path $path -Acl (New-MinerOpsInstallerProtectedFileAcl)
|
|
if (-not (Test-MinerOpsInstallerProtectedFileAcl `
|
|
-Acl (Get-MinerOpsInstallerStreamAcl -Stream $stream))) {
|
|
throw "Installer filesystem trust blocker ACL could not be verified."
|
|
}
|
|
Assert-MinerOpsInstallerSingleLinkStream -Stream $stream `
|
|
-Description "Installer filesystem trust blocker"
|
|
$record = [ordered]@{
|
|
version = 2
|
|
state = "blocked"
|
|
reason = "existing-agent-directory-was-untrusted"
|
|
target_path = [IO.Path]::GetFullPath($TargetRoot)
|
|
detected_boot_id = Get-MinerOpsInstallerBootIdentifier
|
|
reset_boot_id = $null
|
|
original_task_running = $null
|
|
created_at = [DateTime]::UtcNow.ToString("o")
|
|
}
|
|
$bytes = (New-Object Text.UTF8Encoding($false)).GetBytes(
|
|
(($record | ConvertTo-Json -Depth 10) + [Environment]::NewLine)
|
|
)
|
|
$stream.Write($bytes, 0, $bytes.Length)
|
|
$stream.Flush($true)
|
|
}
|
|
finally {
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
return $path
|
|
}
|
|
|
|
function Assert-MinerOpsInstallerNoReparsePath {
|
|
param([Parameter(Mandatory)][string]$Path)
|
|
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$root = [IO.Path]::GetPathRoot($fullPath)
|
|
if (-not $root) { throw "Agent directory must use an absolute path." }
|
|
if (-not $fullPath.Equals(
|
|
$root, [StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
$fullPath = $fullPath.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
}
|
|
$current = $root
|
|
$rootItem = Get-Item -LiteralPath $current -Force -ErrorAction Stop
|
|
if (
|
|
-not $rootItem.PSIsContainer -or
|
|
($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
) {
|
|
throw "Agent directory path root is a reparse point or non-directory component."
|
|
}
|
|
$relative = $fullPath.Substring($root.Length)
|
|
foreach ($segment in @($relative.Split(
|
|
@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar),
|
|
[StringSplitOptions]::RemoveEmptyEntries
|
|
))) {
|
|
$current = Join-Path $current $segment
|
|
$item = Get-Item -LiteralPath $current -Force -ErrorAction Stop
|
|
if (
|
|
-not $item.PSIsContainer -or
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
) {
|
|
throw "Agent directory path contains a reparse point or non-directory component."
|
|
}
|
|
}
|
|
return $fullPath
|
|
}
|
|
|
|
function Assert-MinerOpsInstallerTrustedAncestors {
|
|
param([Parameter(Mandatory)][string]$Path)
|
|
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$root = [IO.Path]::GetPathRoot($fullPath)
|
|
if (-not $fullPath.Equals(
|
|
$root, [StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
$fullPath = $fullPath.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
}
|
|
$parentInfo = [IO.Directory]::GetParent($fullPath)
|
|
if ($null -eq $parentInfo) { throw "Agent directory cannot be a filesystem root." }
|
|
$parent = $parentInfo.FullName
|
|
$root = [IO.Path]::GetPathRoot($parent)
|
|
if (-not $parent.Equals(
|
|
$root, [StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
$parent = $parent.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
}
|
|
$current = $root
|
|
$components = [System.Collections.Generic.List[string]]::new()
|
|
$components.Add($current)
|
|
foreach ($segment in @($parent.Substring($root.Length).Split(
|
|
@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar),
|
|
[StringSplitOptions]::RemoveEmptyEntries
|
|
))) {
|
|
$current = Join-Path $current $segment
|
|
$components.Add($current)
|
|
}
|
|
$trusted = @{}
|
|
foreach ($sid in @(
|
|
"S-1-5-18",
|
|
"S-1-5-32-544",
|
|
"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464",
|
|
[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
)) { $trusted[$sid] = $true }
|
|
$dangerous = [System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::Delete -bor
|
|
[System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
)
|
|
foreach ($component in $components) {
|
|
$item = Get-Item -LiteralPath $component -Force -ErrorAction Stop
|
|
if (
|
|
-not $item.PSIsContainer -or
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
) { throw "Agent directory ancestor is not trusted." }
|
|
$acl = Get-Acl -LiteralPath $component -ErrorAction Stop
|
|
try {
|
|
$owner = $acl.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier]
|
|
).Value
|
|
}
|
|
catch { throw "Agent directory ancestor owner could not be verified." }
|
|
if (-not $trusted.ContainsKey($owner)) {
|
|
throw "Agent directory ancestor has an untrusted owner."
|
|
}
|
|
foreach ($rule in @($acl.GetAccessRules(
|
|
$true, $true, [System.Security.Principal.SecurityIdentifier]
|
|
))) {
|
|
if (
|
|
$rule.AccessControlType -eq
|
|
[System.Security.AccessControl.AccessControlType]::Allow -and
|
|
-not $trusted.ContainsKey($rule.IdentityReference.Value) -and
|
|
($rule.PropagationFlags -band
|
|
[System.Security.AccessControl.PropagationFlags]::InheritOnly) -eq 0 -and
|
|
($rule.FileSystemRights -band $dangerous) -ne 0
|
|
) {
|
|
throw "Agent directory ancestor permits untrusted namespace replacement."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Open-MinerOpsInstallerDirectoryLock {
|
|
param([Parameter(Mandatory)][string]$Path)
|
|
|
|
if (-not ("MinerOpsDirectoryLockNative" -as [type])) {
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
using Microsoft.Win32.SafeHandles;
|
|
public static class MinerOpsDirectoryLockNative
|
|
{
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
public static extern SafeFileHandle CreateFile(string name, uint access,
|
|
uint share, IntPtr security, uint creation, uint flags,
|
|
IntPtr templateFile);
|
|
}
|
|
'@
|
|
}
|
|
$handle = [MinerOpsDirectoryLockNative]::CreateFile(
|
|
[IO.Path]::GetFullPath($Path), 0, 3, [IntPtr]::Zero, 3,
|
|
0x02200000, [IntPtr]::Zero
|
|
)
|
|
if ($handle.IsInvalid) {
|
|
$code = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
$handle.Dispose()
|
|
throw (New-Object ComponentModel.Win32Exception(
|
|
$code, "Agent directory could not be locked against replacement."
|
|
))
|
|
}
|
|
return $handle
|
|
}
|
|
|
|
function Test-MinerOpsInstallerDirectoryAcl {
|
|
param([Parameter(Mandatory)]$Acl)
|
|
|
|
if (-not $Acl.AreAccessRulesProtected) { return $false }
|
|
$trusted = @{}
|
|
foreach ($sid in (Get-MinerOpsInstallerDirectorySids)) {
|
|
$trusted[$sid] = $false
|
|
}
|
|
$usersRead = $false
|
|
try {
|
|
$owner = $Acl.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier]
|
|
).Value
|
|
}
|
|
catch { return $false }
|
|
if (-not $trusted.ContainsKey($owner)) { return $false }
|
|
$writeLike = [System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::Write -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Delete -bor
|
|
[System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
)
|
|
foreach ($rule in @($Acl.GetAccessRules(
|
|
$true, $false, [System.Security.Principal.SecurityIdentifier]
|
|
))) {
|
|
$sid = $rule.IdentityReference.Value
|
|
if (
|
|
$rule.IsInherited -or
|
|
$rule.AccessControlType -ne
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
) { return $false }
|
|
if ($trusted.ContainsKey($sid)) {
|
|
if (($rule.FileSystemRights -band
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl) -ne
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl) {
|
|
return $false
|
|
}
|
|
$trusted[$sid] = $true
|
|
}
|
|
elseif ($sid -eq "S-1-5-32-545") {
|
|
if (
|
|
($rule.FileSystemRights -band $writeLike) -ne 0 -or
|
|
($rule.FileSystemRights -band
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute) -ne
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute
|
|
) { return $false }
|
|
$usersRead = $true
|
|
}
|
|
else { return $false }
|
|
}
|
|
return $usersRead -and -not ($trusted.Values -contains $false)
|
|
}
|
|
|
|
function Assert-MinerOpsInstallerDirectory {
|
|
param([Parameter(Mandatory)][string]$Path)
|
|
|
|
$fullPath = Assert-MinerOpsInstallerNoReparsePath -Path $Path
|
|
[void](Assert-MinerOpsInstallerTrustedAncestors -Path $fullPath)
|
|
if (-not (Test-MinerOpsInstallerDirectoryAcl `
|
|
-Acl (Get-Acl -LiteralPath $fullPath -ErrorAction Stop))) {
|
|
throw "Agent directory owner or DACL is not trusted."
|
|
}
|
|
return $fullPath
|
|
}
|
|
|
|
function Get-MinerOpsInstallerTransitionArtifacts {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[string]$ExceptPath = ""
|
|
)
|
|
|
|
$exceptFull = if ($ExceptPath) { [IO.Path]::GetFullPath($ExceptPath) } else { "" }
|
|
return @(Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop |
|
|
Where-Object {
|
|
$full = [IO.Path]::GetFullPath($_.FullName)
|
|
-not ($exceptFull -and $full.Equals(
|
|
$exceptFull, [StringComparison]::OrdinalIgnoreCase
|
|
)) -and (
|
|
$_.Name -eq "agent-credential-transition.recovery.json" -or
|
|
$_.Name -eq "agent-config.json.pending" -or
|
|
$_.Name -like "agent-config.pre-credential-transition*" -or
|
|
$_.Name -like "agent-config.json.*" -or
|
|
$_.Name -like "minerops-quarantine-credential-transition-*" -or
|
|
$_.Name -eq "minerops-credential-transition-quarantine" -or
|
|
$_.Name -like "minerops-credential-transition.blocked*.state"
|
|
)
|
|
})
|
|
}
|
|
|
|
function Assert-MinerOpsInstallerChildPath {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$ExpectedParent,
|
|
[switch]$Directory
|
|
)
|
|
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$parent = [IO.Directory]::GetParent($fullPath).FullName.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$expected = [IO.Path]::GetFullPath($ExpectedParent).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
if (-not $parent.Equals($expected, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Installer target is outside the trusted Agent directory."
|
|
}
|
|
if (-not (Test-Path -LiteralPath $fullPath)) { return $fullPath }
|
|
$item = Get-Item -LiteralPath $fullPath -Force -ErrorAction Stop
|
|
if (
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
|
($Directory -and -not $item.PSIsContainer) -or
|
|
(-not $Directory -and $item.PSIsContainer)
|
|
) {
|
|
throw "Installer target is a reparse point or has the wrong type."
|
|
}
|
|
return $fullPath
|
|
}
|
|
|
|
function Protect-MinerOpsInstallerExistingConfig {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$ExpectedParent
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) { return }
|
|
[void](Assert-MinerOpsInstallerDirectory -Path $ExpectedParent)
|
|
$fullPath = Assert-MinerOpsInstallerChildPath `
|
|
-Path $Path -ExpectedParent $ExpectedParent
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $fullPath -Description "Existing Agent config"
|
|
$stream = $null
|
|
try {
|
|
$stream = Open-MinerOpsInstallerFileStreamWithRights `
|
|
-Path $fullPath `
|
|
-Rights ([System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Synchronize
|
|
))
|
|
Assert-MinerOpsInstallerSingleLinkStream -Stream $stream `
|
|
-Description "Existing Agent config"
|
|
Set-MinerOpsInstallerStreamAcl -Stream $stream `
|
|
-Acl (New-MinerOpsInstallerProtectedFileAcl)
|
|
if (-not (Test-MinerOpsInstallerProtectedFileAcl `
|
|
-Acl (Get-MinerOpsInstallerStreamAcl -Stream $stream))) {
|
|
throw "Existing Agent config ACL could not be verified."
|
|
}
|
|
}
|
|
finally {
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
function Initialize-MinerOpsInstallerDirectory {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[switch]$ApproveFilesystemTrustResetAfterReboot
|
|
)
|
|
|
|
$requestedPath = [IO.Path]::GetFullPath($Path)
|
|
$createdNew = $false
|
|
if (-not (Test-Path -LiteralPath $requestedPath)) {
|
|
$parent = [IO.Directory]::GetParent($requestedPath).FullName
|
|
[void](Assert-MinerOpsInstallerNoReparsePath -Path $parent)
|
|
[void](Assert-MinerOpsInstallerTrustedAncestors -Path $requestedPath)
|
|
$staging = Join-Path $parent (
|
|
".minerops-agent-create-{0}" -f [guid]::NewGuid().ToString("N")
|
|
)
|
|
$stagingCreated = $false
|
|
try {
|
|
$directoryInfo = New-Object IO.DirectoryInfo($staging)
|
|
$directoryInfo.Create((New-MinerOpsInstallerDirectoryAcl))
|
|
$stagingCreated = $true
|
|
if (-not (Test-MinerOpsInstallerDirectoryAcl `
|
|
-Acl (Get-Acl -LiteralPath $staging -ErrorAction Stop))) {
|
|
throw "Staged Agent directory DACL could not be verified."
|
|
}
|
|
[void](Assert-MinerOpsInstallerTrustedAncestors -Path $requestedPath)
|
|
[IO.Directory]::Move($staging, $requestedPath)
|
|
$stagingCreated = $false
|
|
$createdNew = $true
|
|
}
|
|
finally {
|
|
if ($stagingCreated -and (Test-Path -LiteralPath $staging)) {
|
|
Remove-Item -LiteralPath $staging -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
$fullPath = Assert-MinerOpsInstallerNoReparsePath -Path $requestedPath
|
|
[void](Assert-MinerOpsInstallerTrustedAncestors -Path $fullPath)
|
|
$wasTrusted = Test-MinerOpsInstallerDirectoryAcl `
|
|
-Acl (Get-Acl -LiteralPath $fullPath -ErrorAction Stop)
|
|
if (-not $wasTrusted) {
|
|
$directoryLock = $null
|
|
$trustBlockerPath = New-MinerOpsInstallerTrustBlocker `
|
|
-TargetRoot $fullPath
|
|
try {
|
|
$directoryLock = Open-MinerOpsInstallerDirectoryLock -Path $fullPath
|
|
[void](Assert-MinerOpsInstallerNoReparsePath -Path $fullPath)
|
|
[void](Assert-MinerOpsInstallerTrustedAncestors -Path $fullPath)
|
|
Set-Acl -LiteralPath $fullPath `
|
|
-AclObject (New-MinerOpsInstallerDirectoryAcl)
|
|
[void](Assert-MinerOpsInstallerDirectory -Path $fullPath)
|
|
}
|
|
finally {
|
|
if ($null -ne $directoryLock) { $directoryLock.Dispose() }
|
|
}
|
|
throw @"
|
|
The existing Agent directory was untrusted. It was hardened for containment,
|
|
but this invocation will not back up, restore, or publish runtime content.
|
|
Reboot and use the documented explicit trust-reset procedure. The external
|
|
filesystem trust blocker remains in force.
|
|
"@
|
|
}
|
|
[void](Assert-MinerOpsInstallerDirectory -Path $fullPath)
|
|
$trustResetMode = $false
|
|
$trustBlockers = @(
|
|
Get-MinerOpsInstallerTrustBlockerPaths -Path $fullPath
|
|
)
|
|
if ($trustBlockers.Count -gt 0) {
|
|
if (-not $ApproveFilesystemTrustResetAfterReboot) {
|
|
throw "Installer is blocked pending an explicit post-reboot filesystem trust reset."
|
|
}
|
|
# This mode is publication only. The credential-transition parent is
|
|
# solely responsible for boot authorization, strict task validation,
|
|
# convergence, and eventual blocker deletion. A direct installer run
|
|
# can therefore never complete the filesystem trust reset.
|
|
$trustResetMode = $true
|
|
}
|
|
Protect-MinerOpsInstallerExistingConfig `
|
|
-Path (Join-Path $fullPath "agent-config.json") `
|
|
-ExpectedParent $fullPath
|
|
if (@(Get-ChildItem -LiteralPath $fullPath `
|
|
-Filter "minerops-credential-transition.blocked*.state" `
|
|
-Force -ErrorAction Stop).Count -gt 0) {
|
|
throw "Credential transition is blocked pending operator review."
|
|
}
|
|
return [pscustomobject]@{
|
|
Path = $fullPath
|
|
CreatedNew = $createdNew
|
|
TrustResetMode = $trustResetMode
|
|
TrustBlockerPaths = @($trustBlockers.FullName)
|
|
}
|
|
}
|
|
|
|
function Get-MinerOpsScheduledTask {
|
|
param([Parameter(Mandatory)][string]$Name)
|
|
|
|
$tasks = @(Get-ScheduledTask -ErrorAction Stop | Where-Object {
|
|
[string]$_.TaskName -ceq $Name
|
|
})
|
|
if ($tasks.Count -gt 1) {
|
|
throw "Scheduled Task query returned multiple results for $Name."
|
|
}
|
|
if ($tasks.Count -eq 0) { return $null }
|
|
return $tasks[0]
|
|
}
|
|
|
|
function Test-MinerOpsInstallerShouldCreateMissingTask {
|
|
param(
|
|
[Parameter(Mandatory)]$Definition,
|
|
[Parameter(Mandatory)][bool]$TrustResetMode
|
|
)
|
|
|
|
if (-not $TrustResetMode) { return $true }
|
|
if ($null -eq $Definition.CreateWhenMissingDuringTrustReset) {
|
|
throw "Scheduled Task trust-reset creation policy is missing."
|
|
}
|
|
return [bool]$Definition.CreateWhenMissingDuringTrustReset
|
|
}
|
|
|
|
function Stop-MinerOpsScheduledTaskForUpgrade {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Name,
|
|
[TimeSpan]$Timeout = [TimeSpan]::FromSeconds(30)
|
|
)
|
|
|
|
Stop-ScheduledTask -TaskName $Name -ErrorAction Stop
|
|
$deadline = [DateTime]::UtcNow.Add($Timeout)
|
|
do {
|
|
Start-Sleep -Milliseconds 500
|
|
$task = Get-MinerOpsScheduledTask -Name $Name
|
|
if ($null -eq $task) {
|
|
throw "Scheduled Task $Name disappeared while stopping it."
|
|
}
|
|
if ([string]$task.State -eq "Ready") {
|
|
return
|
|
}
|
|
if ([string]$task.State -ne "Running") {
|
|
throw "Scheduled Task $Name entered invalid state '$($task.State)' while stopping."
|
|
}
|
|
} while ([DateTime]::UtcNow -lt $deadline)
|
|
|
|
throw "Scheduled Task $Name did not stop within $($Timeout.TotalSeconds) seconds."
|
|
}
|
|
|
|
function Start-MinerOpsScheduledTask {
|
|
param([Parameter(Mandatory)][string]$Name)
|
|
|
|
Start-ScheduledTask -TaskName $Name -ErrorAction Stop
|
|
}
|
|
|
|
# Validate the complete source before creating directories, stopping tasks, or
|
|
# replacing any target file.
|
|
foreach ($name in $RuntimeFiles) {
|
|
$source = Join-Path $PSScriptRoot $name
|
|
if (!(Test-Path -LiteralPath $source -PathType Leaf)) {
|
|
throw "Deployment source is missing $name"
|
|
}
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $source -Description "Deployment source $name"
|
|
}
|
|
|
|
$mainTask = Get-MinerOpsScheduledTask -Name "MinerOpsAgent"
|
|
$mainTaskExisted = $null -ne $mainTask
|
|
$mainTaskWasRunning = $mainTaskExisted -and [string]$mainTask.State -eq "Running"
|
|
$mainTaskStoppedForUpgrade = $false
|
|
$deploymentStarted = $false
|
|
$trustResetMode = $false
|
|
$existing = @()
|
|
$createdTaskNames = [System.Collections.Generic.List[string]]::new()
|
|
|
|
try {
|
|
$directoryState = Initialize-MinerOpsInstallerDirectory `
|
|
-Path $Target `
|
|
-ApproveFilesystemTrustResetAfterReboot:$ApproveFilesystemTrustResetAfterReboot
|
|
$Target = [string]$directoryState.Path
|
|
$trustResetMode = [bool]$directoryState.TrustResetMode
|
|
$BackupRoot = Join-Path $Target "backups"
|
|
[void](Assert-MinerOpsInstallerDirectory -Path $Target)
|
|
$existing = @($RuntimeFiles | Where-Object {
|
|
$candidate = Join-Path $Target $_
|
|
[void](Assert-MinerOpsInstallerChildPath `
|
|
-Path $candidate -ExpectedParent $Target)
|
|
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $candidate -Description "Installed runtime $_"
|
|
if (-not $trustResetMode) {
|
|
Assert-MinerOpsInstallerTrustedRuntimeFile `
|
|
-Path $candidate -Description "Installed runtime $_"
|
|
}
|
|
return $true
|
|
}
|
|
return $false
|
|
})
|
|
if (-not $trustResetMode) {
|
|
[void](Assert-MinerOpsInstallerChildPath `
|
|
-Path $BackupRoot -ExpectedParent $Target -Directory)
|
|
if (-not (Test-Path -LiteralPath $BackupRoot)) {
|
|
New-Item -ItemType Directory -Path $BackupRoot | Out-Null
|
|
}
|
|
Set-Acl -LiteralPath $BackupRoot `
|
|
-AclObject (New-MinerOpsInstallerDirectoryAcl)
|
|
if (-not (Test-MinerOpsInstallerDirectoryAcl `
|
|
-Acl (Get-Acl -LiteralPath $BackupRoot -ErrorAction Stop))) {
|
|
throw "Backup directory owner or DACL could not be verified."
|
|
}
|
|
if ($existing.Count -gt 0) {
|
|
[void](Assert-MinerOpsInstallerDirectory -Path $Target)
|
|
if (Test-Path -LiteralPath $Backup) {
|
|
throw "Random installer backup destination already exists."
|
|
}
|
|
New-Item -ItemType Directory -Path $Backup | Out-Null
|
|
Set-Acl -LiteralPath $Backup `
|
|
-AclObject (New-MinerOpsInstallerDirectoryAcl)
|
|
foreach ($name in $existing) {
|
|
$backupPath = Join-Path $Backup $name
|
|
Copy-Item -LiteralPath (Join-Path $Target $name) `
|
|
-Destination $backupPath
|
|
Protect-MinerOpsInstallerRuntimeFile `
|
|
-Path $backupPath -Description "Installer rollback copy $name"
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($mainTaskWasRunning) {
|
|
$mainTaskStoppedForUpgrade = $true
|
|
Stop-MinerOpsScheduledTaskForUpgrade -Name "MinerOpsAgent"
|
|
}
|
|
|
|
$deploymentStarted = $true
|
|
foreach ($name in $RuntimeFiles) {
|
|
[void](Assert-MinerOpsInstallerDirectory -Path $Target)
|
|
[void](Assert-MinerOpsInstallerChildPath `
|
|
-Path (Join-Path $Target $name) -ExpectedParent $Target)
|
|
$targetPath = Join-Path $Target $name
|
|
if ($trustResetMode -and (Test-Path -LiteralPath $targetPath)) {
|
|
# Pre-reset runtime is evidence, never rollback authority. Reject
|
|
# aliases, then remove it without reading before publishing trusted
|
|
# package content into the now handle-clean namespace.
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $targetPath -Description "Pre-reset runtime $name"
|
|
Remove-Item -LiteralPath $targetPath -Force -ErrorAction Stop
|
|
}
|
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) `
|
|
-Destination $targetPath -Force
|
|
Protect-MinerOpsInstallerRuntimeFile `
|
|
-Path $targetPath -Description "Published runtime $name"
|
|
}
|
|
|
|
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
$system = New-ScheduledTaskPrincipal -UserId "SYSTEM" `
|
|
-LogonType ServiceAccount -RunLevel Highest
|
|
$startup = New-ScheduledTaskTrigger -AtStartup
|
|
$fiveMinutes = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
|
|
-RepetitionInterval (New-TimeSpan -Minutes 5)
|
|
$commonSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew `
|
|
-StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
|
-ExecutionTimeLimit (New-TimeSpan -Hours 72)
|
|
$agentSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew `
|
|
-StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
|
-ExecutionTimeLimit (New-TimeSpan -Hours 72) -RestartCount 3 `
|
|
-RestartInterval (New-TimeSpan -Minutes 1)
|
|
|
|
$definitions = @(
|
|
@{
|
|
Name = "MinerOpsAgent"; Script = "MinerOpsAgentService.ps1"
|
|
Trigger = $startup; Principal = $system; Settings = $agentSettings
|
|
ExtraArguments = ""
|
|
CreateWhenMissingDuringTrustReset = $true
|
|
},
|
|
@{
|
|
Name = "MinerOpsAgentTray"; Script = "MinerOpsAgentTray.ps1"
|
|
Trigger = (New-ScheduledTaskTrigger -AtLogOn); Principal = $null
|
|
Settings = $commonSettings; ExtraArguments = "-WindowStyle Hidden"
|
|
CreateWhenMissingDuringTrustReset = $false
|
|
},
|
|
@{
|
|
Name = "MinerOpsApi3Enrichment"
|
|
Script = "MinerOpsApi3EnrichmentPoller.ps1"
|
|
Trigger = $fiveMinutes; Principal = $system; Settings = $commonSettings
|
|
ExtraArguments = "-Limit 0 -TimeoutMs 8000"
|
|
CreateWhenMissingDuringTrustReset = $true
|
|
},
|
|
@{
|
|
Name = "MinerOpsPoolPoller"; Script = "MinerOpsPoolPollerTask.ps1"
|
|
Trigger = $fiveMinutes; Principal = $system; Settings = $commonSettings
|
|
ExtraArguments = ""
|
|
CreateWhenMissingDuringTrustReset = $true
|
|
}
|
|
)
|
|
foreach ($definition in $definitions) {
|
|
$existingTask = Get-MinerOpsScheduledTask -Name $definition.Name
|
|
if ($null -ne $existingTask) {
|
|
# Existing definitions remain authoritative and are never silently
|
|
# rewritten under SYSTEM. A suspect auxiliary task requires
|
|
# separate operator review; it is not credential authority.
|
|
Write-Host "Preserving existing Scheduled Task $($definition.Name)."
|
|
continue
|
|
}
|
|
|
|
if (-not (Test-MinerOpsInstallerShouldCreateMissingTask `
|
|
-Definition $definition -TrustResetMode $trustResetMode)) {
|
|
# Trust-reset publication can run noninteractively as SYSTEM. A
|
|
# missing per-user tray has no safe interactive principal in that
|
|
# context. Its runtime file is already published; task creation is
|
|
# intentionally deferred to a later interactive maintenance run.
|
|
Write-Host "Deferring missing per-user Scheduled Task $($definition.Name) until an interactive install."
|
|
continue
|
|
}
|
|
|
|
$arguments = '-NoProfile -ExecutionPolicy Bypass -File "{0}"' -f `
|
|
(Join-Path $Target $definition.Script)
|
|
if ($definition.ExtraArguments) {
|
|
$arguments = "$arguments $($definition.ExtraArguments)"
|
|
}
|
|
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments
|
|
$registration = @{
|
|
TaskName = $definition.Name
|
|
Action = $action
|
|
Trigger = $definition.Trigger
|
|
Settings = $definition.Settings
|
|
}
|
|
if ($definition.Principal) {
|
|
$registration.Principal = $definition.Principal
|
|
}
|
|
Register-ScheduledTask @registration | Out-Null
|
|
$createdTaskNames.Add($definition.Name)
|
|
}
|
|
|
|
if ($mainTaskWasRunning -and -not $trustResetMode) {
|
|
Start-MinerOpsScheduledTask -Name "MinerOpsAgent"
|
|
$mainTaskStoppedForUpgrade = $false
|
|
}
|
|
elseif ($trustResetMode) {
|
|
# A trust reset never starts an Agent against a merely contained
|
|
# pre-reset config. The credential-transition workflow positively
|
|
# verifies that config and restores the durable task intent later.
|
|
$mainTaskStoppedForUpgrade = $false
|
|
}
|
|
|
|
if ($trustResetMode) {
|
|
Write-Host "MinerOps Windows Agent runtime was published; the parent-owned filesystem trust blocker remains."
|
|
}
|
|
else {
|
|
Write-Host "MinerOps Windows Agent installed. Existing task definitions, config, state, logs, and backups were preserved."
|
|
}
|
|
} catch {
|
|
$deploymentError = $_
|
|
$rollbackErrors = [System.Collections.Generic.List[string]]::new()
|
|
|
|
foreach ($taskName in $createdTaskNames) {
|
|
try {
|
|
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false `
|
|
-ErrorAction Stop
|
|
} catch {
|
|
$rollbackErrors.Add("could not remove new task ${taskName}: $($_.Exception.Message)")
|
|
}
|
|
}
|
|
|
|
if ($deploymentStarted -and -not $trustResetMode) {
|
|
foreach ($name in $RuntimeFiles) {
|
|
try {
|
|
$targetPath = Join-Path $Target $name
|
|
if ($existing -contains $name) {
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path (Join-Path $Backup $name) `
|
|
-Description "Installer rollback source $name"
|
|
Assert-MinerOpsInstallerTrustedRuntimeFile `
|
|
-Path (Join-Path $Backup $name) `
|
|
-Description "Installer rollback source $name"
|
|
if (Test-Path -LiteralPath $targetPath) {
|
|
Assert-MinerOpsInstallerSingleLinkPath `
|
|
-Path $targetPath `
|
|
-Description "Failed published runtime $name"
|
|
}
|
|
Copy-Item -LiteralPath (Join-Path $Backup $name) `
|
|
-Destination $targetPath -Force
|
|
Protect-MinerOpsInstallerRuntimeFile `
|
|
-Path $targetPath `
|
|
-Description "Restored runtime $name"
|
|
} elseif (Test-Path -LiteralPath $targetPath) {
|
|
Remove-Item -LiteralPath $targetPath -Force
|
|
}
|
|
} catch {
|
|
$rollbackErrors.Add("could not restore ${name}: $($_.Exception.Message)")
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($mainTaskWasRunning -and $mainTaskStoppedForUpgrade -and
|
|
-not $trustResetMode) {
|
|
try {
|
|
Start-MinerOpsScheduledTask -Name "MinerOpsAgent"
|
|
$mainTaskStoppedForUpgrade = $false
|
|
} catch {
|
|
$rollbackErrors.Add("could not restart restored MinerOpsAgent: $($_.Exception.Message)")
|
|
}
|
|
}
|
|
|
|
if ($rollbackErrors.Count -gt 0) {
|
|
$rollbackSummary = $rollbackErrors -join "; "
|
|
throw "Deployment failed: $($deploymentError.Exception.Message). Rollback errors: $rollbackSummary"
|
|
}
|
|
throw $deploymentError
|
|
}
|