Files
WELA/tests/GpoCreation.Windows.Tests.ps1
田中ザック Isaac Mathis f1ed90d189 Create new disabled, unlinked GPOs from reviewed native audit backups (#427)
* Add guarded creation of disabled unlinked audit GPOs

* Reference PR 427 in GPO creation changelogs

* Accept only inert native ADM placeholders and fix PS5 JSON fixture

* Preserve fractional UTC strings in existing probe fixtures
2026-09-20 22:53:00 +09:00

73 lines
6.1 KiB
PowerShell

# Genuine Microsoft backup read + native workgroup refusal; no domain creation/import is attempted.
param([Parameter(Mandatory)][string]$OutputPath,[switch]$AllowHostedGpmcInstall)
$ErrorActionPreference='Stop'
if($env:OS -ne 'Windows_NT') {throw 'Windows is required.'}
$repo=Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/GpoAuditPackages.ps1')
. (Join-Path $repo 'scripts/EvtxRecovery.ps1')
. (Join-Path $repo 'scripts/AdObjectSacl.ps1')
. (Join-Path $repo 'scripts/GpoCreation.ps1')
$script:checks=0
function Assert($Value,[string]$Message){if(-not $Value){throw "FAIL: $Message"};$script:checks++}
function Reject([scriptblock]$Code,[string]$Pattern){$message='';try{& $Code|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, got $message"}
function New-GPO {throw 'Forbidden domain mutation in read-only native test.'}
function Import-WelaGpoNativeTarget {throw 'Forbidden domain import in read-only native test.'}
$computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
Assert ($computer.PartOfDomain -eq $false) 'Native negative test requires an actual workgroup host'
if($AllowHostedGpmcInstall) {
if($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted') {throw 'GPMC installation is restricted to explicitly opted-in disposable GitHub-hosted runners.'}
Import-Module ServerManager -ErrorAction Stop
if((Get-WindowsFeature GPMC).Installed -ne $true) {
$installed=Install-WindowsFeature GPMC -ErrorAction Stop
if(-not $installed.Success -or [string]$installed.RestartNeeded -ne 'No') {throw 'GPMC installation requires successful completion without a pending restart; native evidence is blocked.'}
}
}
$null=New-WelaGpm
$before=Get-WelaEffectiveAuditPolicy;$precedenceBefore=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy
$root=Resolve-WelaEvtxPath $OutputPath
if(Test-Path $root){throw 'Use a fresh native artifact directory.'}
$null=New-Item -ItemType Directory $root;Protect-WelaGpoOutput $root
try {
$url='https://download.microsoft.com/download/8/5/c/85c25433-a1b0-4ffa-9429-7e023e7da8d8/Windows%20Server%202022%20Security%20Baseline.zip'
$expectedHash='49590cc694626d171fc934fafea6494f13ecd3843086704b7a5b98355909b8e0'
$zipPath=Join-Path $root 'sct.zip'
[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing -ErrorAction Stop
Assert ((Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant() -ceq $expectedHash) 'Official SCT archive matches the reviewed SHA-256 pin'
Add-Type -AssemblyName System.IO.Compression.FileSystem
$extract=Join-Path $root 'sct';$null=New-Item -ItemType Directory $extract
$zip=[IO.Compression.ZipFile]::OpenRead($zipPath)
try {
foreach($entry in $zip.Entries) {
$destination=[IO.Path]::GetFullPath((Join-Path $extract $entry.FullName))
if(-not $destination.StartsWith($extract+[IO.Path]::DirectorySeparatorChar,[StringComparison]::OrdinalIgnoreCase)) {throw 'Archive traversal refused.'}
}
} finally {$zip.Dispose()}
[IO.Compression.ZipFile]::ExtractToDirectory($zipPath,$extract)
$backupRoot=Join-Path $extract 'Windows Server-2022-Security-Baseline-FINAL/GPOs'
$backupId='20fad6fb-7c6d-496e-801c-0434769847ff'
$native=Get-WelaGpoNativeBackup $backupRoot $backupId
$doc=Read-WelaGpoXml $native.Xml
$sourceId=Get-WelaGpoGuid (Get-WelaGpoText $doc.DocumentElement.Identifier 'Identifier' 'http://www.microsoft.com/GroupPolicy/Types')
Assert ($sourceId -eq 'fa0f36d8-14ce-4d94-90f7-66a01ddb07c4' -and $sourceId -ne $backupId) 'Native GPMC reads the genuine selected backup instance and distinct source GPO GUID'
Assert ((Get-WelaGpoText $doc.DocumentElement 'Name') -ceq 'MSFT Windows Server 2022 - Member Server') 'Native report retains official source identity'
[IO.File]::WriteAllText((Join-Path $root 'native-backup-report.xml'),$native.Xml,[Text.Encoding]::Unicode)
$package=Join-Path $root 'package';$null=Export-WelaGpoPackage (Get-WelaGpoPackagePlan wela-2.2.0 MemberServer 20348) $package
$config=[ordered]@{SchemaVersion=1;PackagePath=$package;BackupRoot=$backupRoot;BackupId=$backupId;Domain='example.test';DomainGuid='dddddddd-dddd-dddd-dddd-dddddddddddd';Dc='dc.example.test';Name='Read Only Native Refusal';ReviewedSha256=''}
$configPath=Join-Path $root 'create.json';[IO.File]::WriteAllText($configPath,($config|ConvertTo-Json),[Text.UTF8Encoding]::new($false))
Reject {Invoke-WelaGpoCreateCommand -ConfigPath $configPath} 'narrow audit payload'
Reject {Open-WelaGpoCreationSession ([pscustomobject]$config)} 'workgroup'
Reject {Get-WelaGpoNativeBackup $backupRoot '11111111-1111-1111-1111-111111111111'} '.'
# The source files and overall-status method are actual native evidence; nothing is imported.
Write-WelaGpoReceipt $root 'native-evidence.json' ([ordered]@{ArchiveUrl=$url;ArchiveSha256=$expectedHash;BackupId=$backupId;SourceGpoId=$sourceId;PowerShell=$PSVersionTable.PSVersion.ToString();NativeBackupReportObserved=$true;BroadPayloadRefused=$true;WorkgroupGuardObserved=$true;DomainCreationTested=$false;DomainImportTested=$false;DeploymentVerified=$false})
} finally {
$after=Get-WelaEffectiveAuditPolicy;$precedenceAfter=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy
$beforeKey=@($before.Keys|Sort-Object|ForEach-Object {$_+'='+$before[$_]}) -join ';';$afterKey=@($after.Keys|Sort-Object|ForEach-Object {$_+'='+$after[$_]}) -join ';'
Assert ($beforeKey -ceq $afterKey) 'All actual native audit masks remain unchanged'
Assert (($precedenceBefore|ConvertTo-Json -Compress) -ceq ($precedenceAfter|ConvertTo-Json -Compress)) 'Actual precedence state remains unchanged'
}
Write-Host "GPO creation native checks: $script:checks passed. Genuine GPMC backup read and negative workgroup/broad-policy checks only; positive domain import remains pending."
$global:LASTEXITCODE=0