mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
e86f7efed3
The SHA-256 check proves the ZIP is the one version.json names, but that manifest lives in the same container as the ZIP: it cannot outlive a compromise of the download host. The expected publisher travels inside this script, which the loader verified before running it, so the chain from the file an administrator assigns to the process that starts is publisher-anchored end to end. Install-AutopilotMonitor.ps1 v2.4 -> v2.5: - Get-UnverifiedAgentBinaries checks every AutopilotMonitor.*.exe/.dll in the extracted payload for status Valid and the same subject pin the loader carries; the gate runs immediately before --install and throws on any rejection, exactly like the hash check. - Third-party assemblies keep their own publisher's signature and are not checked: System.Management.Automation.dll ships unsigned, so a gate over all files would refuse the live payload. - An extracted folder with no binary of ours is a rejection, not a pass. - The SHA-256 manifest check is unchanged and still runs first: it proves the bytes, the signature proves the author. Verified against the live payload 2.0.1454 (all five of our PEs Valid and glueckkanja AG, gate passes); appending one byte to AutopilotMonitor.Shared.dll makes it reject. Pester 55/55. The portal privacy and about pages carried the pre-signing wording and are corrected in the same change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
414 lines
19 KiB
PowerShell
414 lines
19 KiB
PowerShell
# Pester tests (Pester 5+) for the bootstrap guard/relax logic in
|
|
# Install-AutopilotMonitor.ps1.
|
|
#
|
|
# The script under test is dot-sourced; its entry guard prevents the bootstrap
|
|
# from executing, so loading it here is side-effect free. External state (WMI,
|
|
# registry, filesystem, WinRT) is mocked; profile directories are real folders
|
|
# under Pester's $TestDrive with a manipulated CreationTimeUtc.
|
|
#
|
|
# Run under Windows PowerShell 5.1 to match the IME runtime:
|
|
# powershell.exe -NoProfile -Command "& { $c = New-PesterConfiguration; $c.Run.Path = 'scripts\Bootstrap'; $c.Output.Verbosity = 'Detailed'; Invoke-Pester -Configuration $c }"
|
|
#
|
|
# This file MUST remain pure ASCII (PS 5.1 reads BOM-less files as ANSI).
|
|
|
|
BeforeAll {
|
|
. (Join-Path $PSScriptRoot 'Install-AutopilotMonitor.ps1')
|
|
|
|
function New-FakeSignature {
|
|
param([string]$Status, [string]$Subject)
|
|
$cert = if ($Subject) {
|
|
[pscustomobject]@{ Subject = $Subject; Thumbprint = '0123456789ABCDEF' }
|
|
} else { $null }
|
|
return [pscustomobject]@{ Status = $Status; SignerCertificate = $cert }
|
|
}
|
|
|
|
function New-FakePayload {
|
|
param([string[]]$Files)
|
|
$dir = Join-Path $TestDrive ([guid]::NewGuid().ToString())
|
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
foreach ($f in $Files) { Set-Content -Path (Join-Path $dir $f) -Value 'x' }
|
|
return $dir
|
|
}
|
|
|
|
function New-FakeProfile {
|
|
param([string]$Name, [double]$AgeMinutes)
|
|
$p = Join-Path $TestDrive $Name
|
|
New-Item -ItemType Directory -Path $p -Force | Out-Null
|
|
(Get-Item $p).CreationTimeUtc = (Get-Date).ToUniversalTime().AddMinutes(-$AgeMinutes)
|
|
return $p
|
|
}
|
|
}
|
|
|
|
Describe 'Test-IsCloudPc' {
|
|
# Local WMI on a Cloud PC is generic ('Virtual Machine'), so detection rests on
|
|
# the Windows365 registry key AND the CloudManagedDesktopExtension service
|
|
# (both required).
|
|
BeforeEach {
|
|
Mock Write-Log { }
|
|
Mock Get-CimInstance { [pscustomobject]@{ Manufacturer = 'Microsoft Corporation'; Model = 'Virtual Machine' } }
|
|
}
|
|
|
|
It 'detects a Cloud PC when both markers are present' {
|
|
Mock Test-Path { $true } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
|
|
Mock Get-Service { [pscustomobject]@{ Name = 'CloudManagedDesktopExtension'; Status = 'Running' } }
|
|
Test-IsCloudPc | Should -BeTrue
|
|
}
|
|
|
|
It 'does not detect with the registry key alone (e.g. W365-Boot physical client)' {
|
|
Mock Test-Path { $true } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
|
|
Mock Get-Service { $null }
|
|
Test-IsCloudPc | Should -BeFalse
|
|
}
|
|
|
|
It 'does not detect with the service alone' {
|
|
Mock Test-Path { $false } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
|
|
Mock Get-Service { [pscustomobject]@{ Name = 'CloudManagedDesktopExtension'; Status = 'Running' } }
|
|
Test-IsCloudPc | Should -BeFalse
|
|
}
|
|
|
|
It 'does not detect a plain device (no markers)' {
|
|
Mock Test-Path { $false } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
|
|
Mock Get-Service { $null }
|
|
Test-IsCloudPc | Should -BeFalse
|
|
}
|
|
|
|
It 'still evaluates markers when the WMI identity query fails' {
|
|
Mock Get-CimInstance { throw 'RPC server unavailable' }
|
|
Mock Test-Path { $true } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
|
|
Mock Get-Service { [pscustomobject]@{ Name = 'CloudManagedDesktopExtension'; Status = 'Running' } }
|
|
Test-IsCloudPc | Should -BeTrue
|
|
}
|
|
}
|
|
|
|
Describe 'Get-OobeState' {
|
|
It 'returns a non-empty state string and never throws' {
|
|
Get-OobeState | Should -Not -BeNullOrEmpty
|
|
}
|
|
}
|
|
|
|
Describe 'Get-RealUserProfilePaths' {
|
|
BeforeEach {
|
|
Mock Write-Log { }
|
|
}
|
|
|
|
It 'merges WMI and filesystem views, drops special/system profiles, dedupes' {
|
|
Mock Get-CimInstance {
|
|
@(
|
|
[pscustomobject]@{ Special = $false; LocalPath = 'C:\Users\UserOne' }
|
|
[pscustomobject]@{ Special = $true; LocalPath = 'C:\Users\systemprofile' }
|
|
[pscustomobject]@{ Special = $false; LocalPath = 'D:\Profiles\Elsewhere' }
|
|
)
|
|
}
|
|
Mock Get-ChildItem {
|
|
@(
|
|
[pscustomobject]@{ FullName = 'C:\Users\UserOne' }
|
|
[pscustomobject]@{ FullName = 'C:\Users\Public' }
|
|
[pscustomobject]@{ FullName = 'C:\Users\defaultuser0' }
|
|
[pscustomobject]@{ FullName = 'C:\Users\WDAGUtilityAccount' }
|
|
)
|
|
}
|
|
$result = @(Get-RealUserProfilePaths)
|
|
$result | Should -Be @('C:\Users\UserOne')
|
|
}
|
|
|
|
It 'falls back to the filesystem view when the WMI query fails' {
|
|
Mock Get-CimInstance { throw 'WMI broken' }
|
|
Mock Get-ChildItem {
|
|
@(
|
|
[pscustomobject]@{ FullName = 'C:\Users\SomeUser' }
|
|
[pscustomobject]@{ FullName = 'C:\Users\Default' }
|
|
)
|
|
}
|
|
$result = @(Get-RealUserProfilePaths)
|
|
$result | Should -Be @('C:\Users\SomeUser')
|
|
}
|
|
|
|
It 'returns an empty result when only special/system profiles exist' {
|
|
Mock Get-CimInstance { @() }
|
|
Mock Get-ChildItem {
|
|
@(
|
|
[pscustomobject]@{ FullName = 'C:\Users\Public' }
|
|
[pscustomobject]@{ FullName = 'C:\Users\defaultuser1' }
|
|
)
|
|
}
|
|
@(Get-RealUserProfilePaths).Count | Should -Be 0
|
|
}
|
|
}
|
|
|
|
Describe 'Get-RelaxDecision' {
|
|
BeforeEach {
|
|
Mock Write-Log { }
|
|
# Defaults: physical device, OOBE long done. Individual tests override.
|
|
Mock Get-OobeState { 'Completed' }
|
|
Mock Test-IsCloudPc { $false }
|
|
}
|
|
|
|
Context 'Windows 365 Cloud PC' {
|
|
BeforeEach {
|
|
Mock Test-IsCloudPc { $true }
|
|
}
|
|
|
|
It 'relaxes for a single fresh profile although OOBE is Completed (first connect)' {
|
|
$p = New-FakeProfile 'CloudPcUser' 0.6
|
|
$d = Get-RelaxDecision -ProfilePaths @($p) -OobeProfileMaxAgeMinutes 15
|
|
$d.Active | Should -BeTrue
|
|
$d.IsCloudPc | Should -BeTrue
|
|
}
|
|
|
|
It 'does not relax for an old profile (productive Cloud PC)' {
|
|
$p = New-FakeProfile 'CloudPcUser' 30
|
|
(Get-RelaxDecision -ProfilePaths @($p) -OobeProfileMaxAgeMinutes 15).Active | Should -BeFalse
|
|
}
|
|
|
|
It 'does not relax for two profiles' {
|
|
$p1 = New-FakeProfile 'UserA' 2
|
|
$p2 = New-FakeProfile 'UserB' 400
|
|
(Get-RelaxDecision -ProfilePaths @($p1, $p2) -OobeProfileMaxAgeMinutes 15).Active | Should -BeFalse
|
|
}
|
|
|
|
It 'relaxes even when the WinRT OOBE API is unavailable (old build)' {
|
|
Mock Get-OobeState { 'Unavailable' }
|
|
$p = New-FakeProfile 'CpcUser' 1
|
|
(Get-RelaxDecision -ProfilePaths @($p) -OobeProfileMaxAgeMinutes 15).Active | Should -BeTrue
|
|
}
|
|
|
|
It 'does not relax for a profile with a future creation time (clock skew)' {
|
|
$p = New-FakeProfile 'SkewUser' -10
|
|
(Get-RelaxDecision -ProfilePaths @($p) -OobeProfileMaxAgeMinutes 15).Active | Should -BeFalse
|
|
}
|
|
}
|
|
|
|
Context 'OOBE InProgress (Windows Backup for Organizations)' {
|
|
It 'relaxes for a single fresh profile during OOBE' {
|
|
Mock Get-OobeState { 'InProgress' }
|
|
$p = New-FakeProfile 'RestoreUser' 3
|
|
$d = Get-RelaxDecision -ProfilePaths @($p) -OobeProfileMaxAgeMinutes 15
|
|
$d.Active | Should -BeTrue
|
|
}
|
|
}
|
|
|
|
Context 'no trigger' {
|
|
It 'does not relax on a physical device with a fresh profile and OOBE Completed' {
|
|
$p = New-FakeProfile 'SomeUser' 2
|
|
(Get-RelaxDecision -ProfilePaths @($p) -OobeProfileMaxAgeMinutes 15).Active | Should -BeFalse
|
|
}
|
|
|
|
It 'stays inactive with no profiles at all' {
|
|
(Get-RelaxDecision -ProfilePaths @() -OobeProfileMaxAgeMinutes 15).Active | Should -BeFalse
|
|
}
|
|
}
|
|
}
|
|
|
|
Describe 'Get-BootstrapDecision' {
|
|
BeforeEach {
|
|
Mock Write-Log { }
|
|
# Baseline: clean device during Device ESP -- no marker, no profiles, no
|
|
# logged-on user, 1h uptime, agent not installed. Tests override selectively.
|
|
Mock Get-ItemProperty { $null }
|
|
Mock Get-RealUserProfilePaths { @() }
|
|
Mock Get-OobeState { 'Completed' }
|
|
Mock Test-IsCloudPc { $false }
|
|
Mock Get-CimInstance { [pscustomobject]@{ LastBootUpTime = (Get-Date).AddHours(-1) } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
|
|
$script:cleanAgentBin = Join-Path $TestDrive 'AgentBin'
|
|
}
|
|
|
|
It 'installs on a clean ESP device' {
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeTrue
|
|
$d.ReasonCode | Should -BeNullOrEmpty
|
|
$d.RelaxActive | Should -BeFalse
|
|
}
|
|
|
|
It 'skips when the Deployed marker exists (guard 1)' {
|
|
Mock Get-ItemProperty { [pscustomobject]@{ Deployed = '2026-08-01T10:00:00Z' } } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\AutopilotMonitor' }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeFalse
|
|
$d.ReasonCode | Should -Be 'AlreadyDeployed'
|
|
}
|
|
|
|
It 'skips a productive device with an old profile (guard 2)' {
|
|
$script:fakeProfile = New-FakeProfile 'OldUser' 5000
|
|
Mock Get-RealUserProfilePaths { @($script:fakeProfile) }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeFalse
|
|
$d.ReasonCode | Should -Be 'ProfilesFound'
|
|
}
|
|
|
|
It 'installs on a freshly provisioned Cloud PC with one fresh profile' {
|
|
$script:fakeProfile = New-FakeProfile 'CloudPcUser' 0.6
|
|
Mock Get-RealUserProfilePaths { @($script:fakeProfile) }
|
|
Mock Test-IsCloudPc { $true }
|
|
# First-connect user is already visible in LogonUI on W365 -- must not block either.
|
|
Mock Get-ItemProperty { [pscustomobject]@{ LastLoggedOnUser = 'AzureAD\CloudPcUser' } } -ParameterFilter { $Path -like '*LogonUI*' }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeTrue
|
|
$d.RelaxActive | Should -BeTrue
|
|
}
|
|
|
|
It 'skips when a real user has logged on and no relax applies (guard 3)' {
|
|
Mock Get-ItemProperty { [pscustomobject]@{ LastLoggedOnUser = 'AzureAD\SomeUser' } } -ParameterFilter { $Path -like '*LogonUI*' }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeFalse
|
|
$d.ReasonCode | Should -Be 'LastLoggedOnUser'
|
|
}
|
|
|
|
It 'does not skip for the defaultuser0 OOBE account (guard 3)' {
|
|
Mock Get-ItemProperty { [pscustomobject]@{ LastLoggedOnUser = 'defaultuser0' } } -ParameterFilter { $Path -like '*LogonUI*' }
|
|
(Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin).Install | Should -BeTrue
|
|
}
|
|
|
|
It 'skips when uptime exceeds the bootstrap window (guard 4)' {
|
|
Mock Get-CimInstance { [pscustomobject]@{ LastBootUpTime = (Get-Date).AddHours(-20) } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeFalse
|
|
$d.ReasonCode | Should -Be 'UptimeExceeded'
|
|
}
|
|
|
|
It 'installs on a Cloud PC despite uptime over the window when the relax is active (guard 4 exemption)' {
|
|
# W365 reality: provisioned days ago, running headless ever since; the user's
|
|
# first connect just created the profile. Uptime is meaningless here.
|
|
$script:fakeProfile = New-FakeProfile 'FirstConnectUser' 0.6
|
|
Mock Get-RealUserProfilePaths { @($script:fakeProfile) }
|
|
Mock Test-IsCloudPc { $true }
|
|
Mock Get-CimInstance { [pscustomobject]@{ LastBootUpTime = (Get-Date).AddHours(-90) } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeTrue
|
|
$d.RelaxActive | Should -BeTrue
|
|
}
|
|
|
|
It 'still skips an over-uptime Cloud PC when the relax is NOT active (no fresh profile)' {
|
|
Mock Test-IsCloudPc { $true }
|
|
Mock Get-CimInstance { [pscustomobject]@{ LastBootUpTime = (Get-Date).AddHours(-90) } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeFalse
|
|
$d.ReasonCode | Should -Be 'UptimeExceeded'
|
|
}
|
|
|
|
It 'keeps guard 4 for the OOBE-restore relax (physical device, uptime is meaningful)' {
|
|
$script:fakeProfile = New-FakeProfile 'RestoreUser' 3
|
|
Mock Get-RealUserProfilePaths { @($script:fakeProfile) }
|
|
Mock Get-OobeState { 'InProgress' }
|
|
Mock Get-CimInstance { [pscustomobject]@{ LastBootUpTime = (Get-Date).AddHours(-20) } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
|
|
$d.Install | Should -BeFalse
|
|
$d.ReasonCode | Should -Be 'UptimeExceeded'
|
|
}
|
|
|
|
It 'skips when the agent binary is already present (guard 5)' {
|
|
$binPath = Join-Path $TestDrive 'AgentBinExisting'
|
|
New-Item -ItemType Directory -Path $binPath -Force | Out-Null
|
|
Set-Content -Path (Join-Path $binPath 'AutopilotMonitor.Agent.exe') -Value 'stub'
|
|
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $binPath
|
|
$d.Install | Should -BeFalse
|
|
$d.ReasonCode | Should -Be 'AgentPresent'
|
|
}
|
|
}
|
|
|
|
Describe 'logging purity' {
|
|
# Regression guard: Write-Log runs inside functions that RETURN decision
|
|
# objects. If it ever emits to the output stream again (Write-Output), the
|
|
# log lines leak into the return values and turn them into arrays. This
|
|
# test deliberately does NOT mock Write-Log.
|
|
It 'Get-BootstrapDecision returns exactly one object with real logging' {
|
|
Mock Get-ItemProperty { [pscustomobject]@{ Deployed = '2026-08-01T10:00:00Z' } } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\AutopilotMonitor' }
|
|
# Plain local assignment: PowerShell's dynamic scoping makes Write-Log
|
|
# (called from Get-BootstrapDecision) resolve this $LogFile instead of
|
|
# the ProgramData path defined by the dot-sourced script.
|
|
$LogFile = Join-Path $TestDrive 'bootstrap_test.log'
|
|
$d = @(Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath (Join-Path $TestDrive 'NoAgent'))
|
|
$d.Count | Should -Be 1
|
|
$d[0].ReasonCode | Should -Be 'AlreadyDeployed'
|
|
Get-Content $LogFile -Raw | Should -Match 'SKIP: Agent was previously deployed'
|
|
}
|
|
}
|
|
|
|
Describe 'Get-UnverifiedAgentBinaries' {
|
|
# The publisher gate on the extracted payload. What matters is the DECISION: which
|
|
# payload is accepted, which is refused, and which files are looked at at all.
|
|
BeforeEach {
|
|
Mock Write-Log { }
|
|
$script:ours = 'CN=glueckkanja AG, O=glueckkanja AG, C=DE'
|
|
}
|
|
|
|
It 'accepts a payload where every binary of ours is validly signed by us' {
|
|
$dir = New-FakePayload -Files @('AutopilotMonitor.Agent.exe', 'AutopilotMonitor.Shared.dll')
|
|
Mock Get-AuthenticodeSignature { New-FakeSignature -Status 'Valid' -Subject $script:ours }
|
|
Get-UnverifiedAgentBinaries -Path $dir | Should -BeNullOrEmpty
|
|
}
|
|
|
|
It 'checks the binaries we author and leaves third-party assemblies alone' {
|
|
$dir = New-FakePayload -Files @(
|
|
'AutopilotMonitor.Agent.exe',
|
|
'AutopilotMonitor.Agent.exe.config',
|
|
'AutopilotMonitor.Shared.dll',
|
|
'Newtonsoft.Json.dll',
|
|
'System.Management.Automation.dll'
|
|
)
|
|
$script:checked = @()
|
|
Mock Get-AuthenticodeSignature {
|
|
$script:checked += (Split-Path $FilePath -Leaf)
|
|
New-FakeSignature -Status 'Valid' -Subject $script:ours
|
|
}
|
|
|
|
Get-UnverifiedAgentBinaries -Path $dir | Should -BeNullOrEmpty
|
|
$script:checked | Should -Be @('AutopilotMonitor.Agent.exe', 'AutopilotMonitor.Shared.dll')
|
|
}
|
|
|
|
It 'refuses an unsigned agent executable' {
|
|
$dir = New-FakePayload -Files @('AutopilotMonitor.Agent.exe')
|
|
Mock Get-AuthenticodeSignature { New-FakeSignature -Status 'NotSigned' -Subject $null }
|
|
(Get-UnverifiedAgentBinaries -Path $dir).Count | Should -Be 1
|
|
}
|
|
|
|
It 'refuses a tampered binary (hash mismatch)' {
|
|
$dir = New-FakePayload -Files @('AutopilotMonitor.Agent.exe')
|
|
Mock Get-AuthenticodeSignature { New-FakeSignature -Status 'HashMismatch' -Subject $script:ours }
|
|
(Get-UnverifiedAgentBinaries -Path $dir).Count | Should -Be 1
|
|
}
|
|
|
|
It 'refuses a signature whose chain does not validate' {
|
|
$dir = New-FakePayload -Files @('AutopilotMonitor.Agent.exe')
|
|
Mock Get-AuthenticodeSignature { New-FakeSignature -Status 'UnknownError' -Subject $script:ours }
|
|
(Get-UnverifiedAgentBinaries -Path $dir).Count | Should -Be 1
|
|
}
|
|
|
|
It 'refuses a validly signed binary from a different publisher' {
|
|
$dir = New-FakePayload -Files @('AutopilotMonitor.Agent.exe')
|
|
Mock Get-AuthenticodeSignature { New-FakeSignature -Status 'Valid' -Subject 'CN=Somebody Else, O=Somebody Else Ltd, C=US' }
|
|
(Get-UnverifiedAgentBinaries -Path $dir).Count | Should -Be 1
|
|
}
|
|
|
|
It 'refuses the whole payload when a single DLL of ours is swapped' {
|
|
$dir = New-FakePayload -Files @('AutopilotMonitor.Agent.exe', 'AutopilotMonitor.Shared.dll')
|
|
Mock Get-AuthenticodeSignature {
|
|
if ((Split-Path $FilePath -Leaf) -eq 'AutopilotMonitor.Shared.dll') {
|
|
New-FakeSignature -Status 'Valid' -Subject 'CN=Somebody Else, O=Somebody Else Ltd, C=US'
|
|
} else {
|
|
New-FakeSignature -Status 'Valid' -Subject $script:ours
|
|
}
|
|
}
|
|
$rejected = Get-UnverifiedAgentBinaries -Path $dir
|
|
$rejected.Count | Should -Be 1
|
|
$rejected[0] | Should -BeLike 'AutopilotMonitor.Shared.dll*'
|
|
}
|
|
|
|
It 'refuses when the signature check itself throws' {
|
|
$dir = New-FakePayload -Files @('AutopilotMonitor.Agent.exe')
|
|
Mock Get-AuthenticodeSignature { throw 'access denied' }
|
|
(Get-UnverifiedAgentBinaries -Path $dir).Count | Should -Be 1
|
|
}
|
|
|
|
It 'refuses an empty payload instead of passing it' {
|
|
$dir = New-FakePayload -Files @('Newtonsoft.Json.dll')
|
|
Mock Get-AuthenticodeSignature { New-FakeSignature -Status 'Valid' -Subject $script:ours }
|
|
(Get-UnverifiedAgentBinaries -Path $dir).Count | Should -Be 1
|
|
}
|
|
}
|
|
|
|
Describe 'publisher pin' {
|
|
It 'pins the expected publisher by subject, not by thumbprint' {
|
|
$ExpectedPublisher | Should -Be '*O=glueckkanja AG*'
|
|
$ExpectedPublisher | Should -Not -Match '^[0-9A-Fa-f]{40}$'
|
|
}
|
|
}
|