mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-23 22:53:00 +02:00
Confirm AD SACL receipt ownership and validate exact PKI parent
This commit is contained in:
1 parent
496423bb8c
commit
cbd1c0643b
4 files changed
+78
-12
No files matched your search
@@ -54,10 +54,14 @@ Both masks include CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, De
|
||||
|
||||
See Microsoft's [certificate template schema](https://learn.microsoft.com/en-us/windows/win32/adschema/c-pkicertificatetemplate) and [enrollment service schema](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adsc/208d42e8-1932-4767-87c5-b8511991e69b). WriteProperty covers changes such as template attributes and an enrollment service's published `certificateTemplates` list; object creation, child objects, enrollment access rights and CA `AuditFilter` are separate. No security enforcement, enrollment permissions, CA settings, DACLs or owners are changed.
|
||||
|
||||
PKI parent membership is proven by a one-level LDAP lookup under the exact approved container using the target's binary object GUID. A matching textual DN suffix, including an escaped comma in an object's name, cannot establish this membership.
|
||||
|
||||
## Verification, concurrency and recovery
|
||||
|
||||
The command reads owner, group, DACL and SACL together using the critical [LDAP security descriptor flags control](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/3888c2b7-35b9-45b7-afeb-b772aa932dd0). A missing object, incomplete descriptor or permission failure is an error. It records the original complete SDDL and binary descriptor, object GUID, same-DC `uSNChanged`, target DN and requested ACEs in `before.jsonl`. A separate `ad-sacl-*.json` receipt records the exact missing ACE additions and expected descriptor **before** the write. Keep these files private and treat receipts as trusted administrator input; they contain directory security configuration.
|
||||
|
||||
Receipts initially have `ReceiptStatus: Pending`, which never authorizes automatic rollback. Only after the LDAP write succeeds and a fresh read verifies the original security information and requested ACEs does WELA persist `Confirmed`, the confirmation time and observed state. Confirmation replaces the intent file while retaining its `.pending` backup. A failed/stale request, failed read-back, interruption or unpersisted confirmation requires manual recovery review; another writer's later matching ACEs do not turn an unconfirmed intention into WELA-owned changes.
|
||||
|
||||
Existing ACEs are retained byte-for-byte. A same-scope ACE with a superset of the rights/outcomes already satisfies the request; WELA adds only missing audit ACEs. After the prompt and durable journal, WELA rereads the same DC's object GUID, USN and complete descriptor and refuses a changed object. The LDAP modify uses a **critical SACL-only** control, preserving owner/group/DACL on the server. Read-back verifies every original ACE, non-SACL security information and requested auditing; a final read detects later drift. Microsoft documents that [`uSNChanged` is local to a DC](https://learn.microsoft.com/en-us/windows/win32/adschema/a-usnchanged), so the command never substitutes another DC for these checks.
|
||||
|
||||
**Use an exclusive maintenance window for SACL changes.** The immediate comparison is not an atomic compare-and-swap: a concurrent writer in the final read/write window can still lose a SACL update. WELA does not claim LDAP transaction protection, lock other writers, or automatically restore a whole descriptor after an uncertain result. Stop other SACL editors/automation, review failures against the journal and current descriptor, and verify again after inheritance/replication have settled.
|
||||
@@ -71,7 +75,7 @@ Existing ACEs are retained byte-for-byte. A same-scope ACE with a superset of th
|
||||
-BackupPath .\new-rollback-backup -ResultsPath rollback-result.json
|
||||
```
|
||||
|
||||
Rollback checks the DC and object identity, validates that the receipt's expected SACL contains precisely the recorded additions plus the old ACEs, and requires the current SACL to match that expected sequence. It removes only those exact additions, using the current descriptor and a SACL-only write; it never restores old owner/group/DACL data. Repeated rollback is idempotent. Reordered/merged ACEs or any intervening SACL edit make automatic ownership ambiguous and are refused. A failed or interrupted apply can leave a receipt without a completed write; inspect current state first. If automatic rollback is refused, compare the original SDDL/ACE bytes, receipt additions and current SACL on the exact DC, identify additions manually, remove only those demonstrably attributable to this run, and preserve all unrelated current entries. Do not restore the complete saved SDDL over later changes.
|
||||
Rollback requires a confirmed receipt, checks the DC and object identity, validates that the receipt's expected SACL contains precisely the recorded additions plus the old ACEs, and requires the current SACL to match that expected sequence. It removes only those exact additions, using the current descriptor and a SACL-only write; it never restores old owner/group/DACL data. Repeated rollback is idempotent. Reordered/merged ACEs or any intervening SACL edit make automatic ownership ambiguous and are refused. Pending receipts are refused even if current ACEs match the intended additions. If automatic rollback is refused, compare the original SDDL/ACE bytes, receipt additions and current SACL on the exact DC, identify additions manually, remove only those demonstrably attributable to this run, and preserve all unrelated current entries. Do not restore the complete saved SDDL over later changes.
|
||||
|
||||
`SaclConfigured`/`Applied`/`AlreadyCompliant` refer to the **selected object's SACL**. `ChangeRequired`, `NotApplicable`, `Unknown`, `Blocked`, runner `Skipped`, `Failed` and `Overridden` remain distinct. Exit 0 means no read/write/verification failures; dry runs and skipped requests do not establish configuration. No Sigma rule uplift is claimed.
|
||||
|
||||
|
||||
@@ -223,6 +223,21 @@ function Write-WelaAdSacl {
|
||||
$null = $Session.Connection.SendRequest($request)
|
||||
}
|
||||
|
||||
function Test-WelaAdPkiContainer {
|
||||
param($Session, $Snapshot, [string]$ContainerDn)
|
||||
# A one-level GUID lookup proves parent membership without interpreting DN
|
||||
# text (escaped commas can otherwise impersonate an approved suffix).
|
||||
$escapedGuid = (([guid]$Snapshot.ObjectGuid).ToByteArray() | ForEach-Object { '\{0:X2}' -f $_ }) -join ''
|
||||
$rows = @(Search-WelaAdDirectory -Session $Session -Dn $ContainerDn -Scope OneLevel `
|
||||
-Filter "(objectGUID=$escapedGuid)" -Attributes @('objectGUID'))
|
||||
if ($rows.Count -eq 0) { return $false }
|
||||
if ($rows.Count -ne 1 -or $rows[0].Dn -ine $Snapshot.Dn -or
|
||||
([guid]::new([byte[]](Get-WelaAdSingleValue $rows[0] 'objectGUID'))).ToString() -ne $Snapshot.ObjectGuid) {
|
||||
throw 'PKI container membership lookup returned an unexpected object identity.'
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-WelaAdSaclPlan {
|
||||
param($Session, [string[]]$Profiles, [string[]]$ObjectDn)
|
||||
$requests = @()
|
||||
@@ -250,9 +265,11 @@ function Get-WelaAdSaclPlan {
|
||||
} else {
|
||||
$before = Get-WelaAdObjectState $Session $request.Dn
|
||||
$class = $null; $guid = $null
|
||||
if ($before.Classes -contains 'pKICertificateTemplate' -and $before.Dn.EndsWith(",CN=Certificate Templates,CN=Public Key Services,CN=Services,$($Session.ConfigurationDn)", [StringComparison]::OrdinalIgnoreCase)) {
|
||||
if ($before.Classes -contains 'pKICertificateTemplate' -and
|
||||
(Test-WelaAdPkiContainer $Session $before "CN=Certificate Templates,CN=Public Key Services,CN=Services,$($Session.ConfigurationDn)")) {
|
||||
$class = 'pKICertificateTemplate'; $guid = 'e5209ca2-3bba-11d2-90cc-00c04fd91ab1'
|
||||
} elseif ($before.Classes -contains 'pKIEnrollmentService' -and $before.Dn.EndsWith(",CN=Enrollment Services,CN=Public Key Services,CN=Services,$($Session.ConfigurationDn)", [StringComparison]::OrdinalIgnoreCase)) {
|
||||
} elseif ($before.Classes -contains 'pKIEnrollmentService' -and
|
||||
(Test-WelaAdPkiContainer $Session $before "CN=Enrollment Services,CN=Public Key Services,CN=Services,$($Session.ConfigurationDn)")) {
|
||||
$class = 'pKIEnrollmentService'; $guid = 'ee4aa692-3bba-11d2-90cc-00c04fd91ab1'
|
||||
} else { throw 'PkiObjects accepts only existing certificate template/enrollment service objects under the selected forest PKI containers.' }
|
||||
if (-not (Test-WelaAdSchemaClass $Session $class $guid)) { throw "PKI schema class missing: $class." }
|
||||
@@ -295,17 +312,30 @@ function Set-WelaAdSaclControls {
|
||||
param($state)
|
||||
$before = $state.Observed
|
||||
$addition = New-WelaAdSaclAddition $before $state.Entry.Definitions
|
||||
$receipt = [ordered]@{ Version = 1; Kind = 'WelaAdSaclAddition'; Server = $state.Session.Server; Dn = $before.Dn;
|
||||
ObjectGuid = $before.ObjectGuid; Before = $before; AddedAces = $addition.AddedAces; ExpectedBinary = $addition.Binary }
|
||||
# A durable receipt precedes the write, including exact additions for
|
||||
# conservative rollback even if the process stops after LDAP success.
|
||||
$receipt = [ordered]@{ Version = 1; Kind = 'WelaAdSaclAddition'; ReceiptStatus = 'Pending'; Server = $state.Session.Server; Dn = $before.Dn;
|
||||
ObjectGuid = $before.ObjectGuid; Before = $before; AddedAces = $addition.AddedAces; ExpectedBinary = $addition.Binary;
|
||||
ConfirmedUtc = $null; ConfirmedAfter = $null }
|
||||
# Durable intent precedes mutation, but cannot authorize rollback.
|
||||
# A failed/stale request may never have written its intended ACEs.
|
||||
$receiptPath = Join-Path $state.Context.BackupPath ('ad-sacl-' + [guid]::NewGuid().ToString('N') + '.json')
|
||||
$receipt | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $receiptPath -Encoding UTF8 -ErrorAction Stop
|
||||
$fresh = Get-WelaAdObjectState $state.Session $state.Entry.Dn
|
||||
if ($fresh.ObjectGuid -ne $before.ObjectGuid -or $fresh.UsnChanged -ne $before.UsnChanged -or $fresh.Descriptor.Binary -ne $before.Descriptor.Binary) { throw 'AD object changed after journaling; no write was sent. Re-audit and retry.' }
|
||||
$state.Baseline = $before
|
||||
Write-WelaAdSacl $state.Session $before.Dn $addition.Binary
|
||||
"SACL-only write sent; recovery receipt: $receiptPath. Event generation and inheritance propagation remain unverified."
|
||||
$verified = Get-WelaAdObjectState $state.Session $before.Dn
|
||||
if (-not (Test-WelaAdPreserved $before $verified)) { throw 'Existing owner, group, DACL or SACL ACE changed after writing; receipt remains Pending and requires manual recovery review.' }
|
||||
foreach ($definition in $state.Entry.Definitions) {
|
||||
if (-not (Test-WelaAdAcePresent $verified.Descriptor $definition)) { throw 'Requested SACL did not verify after writing; receipt remains Pending and requires manual recovery review.' }
|
||||
}
|
||||
$receipt.ReceiptStatus = 'Confirmed'
|
||||
$receipt.ConfirmedUtc = [DateTime]::UtcNow.ToString('o')
|
||||
$receipt.ConfirmedAfter = $verified
|
||||
$confirmedPath = $receiptPath + '.tmp'
|
||||
$receipt | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $confirmedPath -Encoding UTF8 -ErrorAction Stop
|
||||
# Preserve the complete Pending receipt if confirmation cannot persist.
|
||||
[IO.File]::Replace($confirmedPath, $receiptPath, ($receiptPath + '.pending'))
|
||||
"SACL-only write and read-back verified; confirmed recovery receipt: $receiptPath. Event generation and inheritance propagation remain unverified."
|
||||
}
|
||||
Invoke-WelaConfigurationControl -Context $Context -Id $id -Kind AdObjectSacl -Target @{ Server = $Session.Server; Dn = $entry.Dn } `
|
||||
-Desired $entry.Definitions -Read $read -Compliant $test -Apply $apply -CallbackState $state `
|
||||
@@ -318,6 +348,9 @@ function Invoke-WelaAdSaclRollback {
|
||||
$receipt = Get-Content -LiteralPath $ReceiptPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
||||
if ($receipt.Version -ne 1 -or $receipt.Kind -ne 'WelaAdSaclAddition' -or $receipt.Server -ine $Session.Server -or
|
||||
-not $receipt.AddedAces.Count -or $receipt.ObjectGuid -ne $receipt.Before.ObjectGuid -or $receipt.Dn -ine $receipt.Before.Dn) { throw 'Invalid receipt, empty additions, or a different DC target.' }
|
||||
if ($receipt.ReceiptStatus -ne 'Confirmed' -or -not $receipt.ConfirmedUtc -or
|
||||
$receipt.ConfirmedAfter.ObjectGuid -ne $receipt.ObjectGuid -or $receipt.ConfirmedAfter.Server -ine $Session.Server -or
|
||||
$receipt.ConfirmedAfter.Dn -ine $receipt.Dn) { throw 'Unconfirmed receipt: automatic rollback cannot establish ACE ownership. Review Pending/failed/interrupted changes manually.' }
|
||||
$expected = Get-WelaAdDescriptorInfo $receipt.ExpectedBinary
|
||||
# Receipts are recovery evidence, not a general descriptor-restore mechanism.
|
||||
$remaining = New-Object 'System.Collections.Generic.List[string]'
|
||||
|
||||
@@ -20,7 +20,7 @@ $script:originalInfo = ${function:Get-WelaAdDescriptorInfo}
|
||||
function Reset-Mocks {
|
||||
$script:writes = 0; $script:reads = 0; $script:readError = $false; $script:writeError = $false
|
||||
$script:race = $false; $script:finalDrift = $false; $script:badReadback = $false; $script:removeExisting = $false
|
||||
$script:absentClass = ''; $script:dmsa = $true; $script:exchange = $true; $script:onPrompt = $null
|
||||
$script:absentClass = ''; $script:dmsa = $true; $script:exchange = $true; $script:onPrompt = $null; $script:pkiMember = $true; $script:pkiRequest = $null
|
||||
$script:state = [pscustomobject]@{ Server = $session.Server; Dn = $session.DomainDn; ObjectGuid = '01234567-89ab-cdef-0123-456789abcdef'; UsnChanged = '17'; Classes = @('top', 'domainDNS');
|
||||
Descriptor = [pscustomobject]@{ Binary = 'before'; Sddl = 'O:SYG:SYD:(A;;GA;;;SY)S:(AU;SA;WP;;;BA)'; Owner = 'S-1-5-18'; Group = 'S-1-5-18'; Dacl = 'unchanged-dacl'; ControlFlags = 32788; Sacl = @('unrelated') } }
|
||||
$session.Writable = $true
|
||||
@@ -30,6 +30,10 @@ function Test-WelaAdDmsaDomain { param($Session) return $script:dmsa }
|
||||
function Search-WelaAdDirectory {
|
||||
param($Session, $Dn, $Filter, $Scope, $Attributes, [switch]$SecurityDescriptor)
|
||||
if ($Filter -eq '(objectClass=msExchOrganizationContainer)' -and $script:exchange) { [pscustomobject]@{ Dn = 'CN=Exchange'; Values = @{} } }
|
||||
if ($Filter -like '(objectGUID=*') {
|
||||
$script:pkiRequest = @{ Dn = $Dn; Scope = $Scope; Filter = $Filter }
|
||||
if ($script:pkiMember) { [pscustomobject]@{ Dn = $script:state.Dn; Values = @{ objectGUID = @(,([guid]$script:state.ObjectGuid).ToByteArray()) } } }
|
||||
}
|
||||
}
|
||||
function Get-WelaAdObjectState {
|
||||
param($Session, $Dn)
|
||||
@@ -37,7 +41,7 @@ function Get-WelaAdObjectState {
|
||||
if ($script:readError) { throw 'LDAP access denied' }
|
||||
$snapshot = $script:state | ConvertTo-Json -Depth 10 | ConvertFrom-Json
|
||||
if ($script:race -and $script:reads -ge 3) { $snapshot.UsnChanged = '99' }
|
||||
if ($script:finalDrift -and $script:reads -ge 5) { $snapshot.Descriptor.Sacl = @('unrelated'); $snapshot.Descriptor.Binary = 'drift' }
|
||||
if ($script:finalDrift -and $script:reads -ge 6) { $snapshot.Descriptor.Sacl = @('unrelated'); $snapshot.Descriptor.Binary = 'drift' }
|
||||
return $snapshot
|
||||
}
|
||||
function Test-WelaAdAcePresent { param($Descriptor, $Definition) return $Descriptor.Sacl -contains ('added-' + $Definition.Class) }
|
||||
@@ -90,8 +94,13 @@ try {
|
||||
$script:state.Classes = @('top', 'pKICertificateTemplate')
|
||||
$plan = @(Get-WelaAdSaclPlan $session @('PkiObjects') @($script:state.Dn))
|
||||
Assert ($plan[0].Status -eq 'ChangeRequired' -and $plan[0].Definitions[0].AccessMask -eq 852000 -and $plan[0].Definitions[0].AceFlags -eq 64) 'PKI direct-object write/delete/ACL audit only'
|
||||
Assert ($script:pkiRequest.Scope -eq 'OneLevel' -and $script:pkiRequest.Dn -eq "CN=Certificate Templates,CN=Public Key Services,CN=Services,$($session.ConfigurationDn)" -and
|
||||
$script:pkiRequest.Filter -eq '(objectGUID=\67\45\23\01\AB\89\EF\CD\01\23\45\67\89\AB\CD\EF)') 'PKI membership uses exact parent and byte-escaped object GUID'
|
||||
$script:pkiMember = $false
|
||||
$script:state.Dn = "CN=Test,$($session.DomainDn)"
|
||||
Assert ((@(Get-WelaAdSaclPlan $session @('PkiObjects') @($script:state.Dn)))[0].Status -eq 'Unknown') 'PKI class outside trusted forest containers refused'
|
||||
$script:state.Dn = "CN=Foo\,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($session.ConfigurationDn)"
|
||||
Assert ((@(Get-WelaAdSaclPlan $session @('PkiObjects') @($script:state.Dn)))[0].Status -eq 'Unknown') 'escaped RDN suffix cannot impersonate approved PKI parent'
|
||||
Reset-Mocks; $session.Writable = $false
|
||||
Assert ((@(Get-WelaAdSaclPlan $session @('MdiDomain') @()))[0].Status -eq 'Blocked') 'RODC plan explicitly blocked'
|
||||
Reset-Mocks; $script:readError = $true
|
||||
@@ -108,13 +117,26 @@ try {
|
||||
$receipts = @(Get-ChildItem $ctx.BackupPath -Filter 'ad-sacl-*.json')
|
||||
$receipt = Get-Content $receipts[0].FullName -Raw | ConvertFrom-Json
|
||||
Assert ($receipt.AddedAces.Count -eq 6 -and $receipt.Before.Descriptor.Binary -eq 'before') 'receipt stores only intended additions and before binary'
|
||||
Assert ($receipt.ReceiptStatus -eq 'Confirmed' -and $receipt.ConfirmedAfter.Descriptor.Binary -eq 'after' -and $receipt.ConfirmedUtc) 'successful write and readback confirm rollback ownership'
|
||||
$ctx2 = Get-Context; $report2 = Invoke-TestConfigure $ctx2
|
||||
Assert ($script:writes -eq 1 -and $report2.Results[0].Status -eq 'AlreadyCompliant') 'repeat run is idempotent'
|
||||
Reset-Mocks; $script:race = $true; $ctx = Get-Context
|
||||
$report = Invoke-TestConfigure $ctx
|
||||
Assert ($report.ExitCode -eq 1 -and $script:writes -eq 0 -and $report.Results[0].Diagnostic -like '*changed after journaling*') 'USN race fails closed before write'
|
||||
$pendingPath = @(Get-ChildItem $ctx.BackupPath -Filter 'ad-sacl-*.json')[0].FullName
|
||||
Assert ((Get-Content $pendingPath -Raw | ConvertFrom-Json).ReceiptStatus -eq 'Pending') 'stale pre-write receipt remains pending'
|
||||
$script:state.Descriptor.Sacl = @('unrelated') + @(Get-WelaAdAuditDefinitions | ForEach-Object { 'added-' + $_.Class })
|
||||
$script:state.Descriptor.Binary = 'after'
|
||||
Assert-Throws { Invoke-WelaAdSaclRollback $session (Get-Context $true) $pendingPath } 'pending intent cannot authorize rollback of another writers matching ACEs'
|
||||
Reset-Mocks; $script:writeError = $true; $ctx = Get-Context
|
||||
$report = Invoke-TestConfigure $ctx
|
||||
$pendingPath = @(Get-ChildItem $ctx.BackupPath -Filter 'ad-sacl-*.json')[0].FullName
|
||||
Assert ($report.ExitCode -eq 1 -and (Get-Content $pendingPath -Raw | ConvertFrom-Json).ReceiptStatus -eq 'Pending') 'failed LDAP write cannot confirm receipt'
|
||||
Assert-Throws { Invoke-WelaAdSaclRollback $session (Get-Context $true) $pendingPath } 'failed-write receipt cannot authorize automatic rollback'
|
||||
Reset-Mocks; $script:badReadback = $true; $ctx = Get-Context
|
||||
Assert ((Invoke-TestConfigure $ctx).ExitCode -eq 1) 'missing audit ACE readback fails'
|
||||
$pendingPath = @(Get-ChildItem $ctx.BackupPath -Filter 'ad-sacl-*.json')[0].FullName
|
||||
Assert ((Get-Content $pendingPath -Raw | ConvertFrom-Json).ReceiptStatus -eq 'Pending') 'failed readback leaves receipt pending'
|
||||
Reset-Mocks; $script:removeExisting = $true; $ctx = Get-Context
|
||||
$report = Invoke-TestConfigure $ctx
|
||||
Assert ($report.ExitCode -eq 1 -and $report.Results[0].Diagnostic -like '*Existing owner*') 'loss of pre-existing audit ACE fails verification'
|
||||
|
||||
@@ -66,13 +66,15 @@ try {
|
||||
|
||||
# Mock the state/transport boundary, retain real ACL transformations and runner.
|
||||
$script:state = $after; $script:writes = 0
|
||||
function Get-WelaAdObjectState { param($Session, $Dn) return $script:state }
|
||||
function Get-WelaAdObjectState { param($Session, $Dn) return ($script:state | ConvertTo-Json -Depth 12 | ConvertFrom-Json) }
|
||||
function Write-WelaAdSacl { param($Session, $Dn, $Binary)
|
||||
$script:writes++; $script:state.Descriptor = Get-WelaAdDescriptorInfo $Binary; $script:state.UsnChanged = '101'
|
||||
if ($script:corruptRollback) { $script:state.Descriptor.Owner = 'S-1-5-19' }
|
||||
}
|
||||
$session.Writable = $true
|
||||
$receiptPath = Join-Path $root 'receipt.json'
|
||||
[pscustomobject]@{ Version = 1; Kind = 'WelaAdSaclAddition'; Server = $before.Server; Dn = $before.Dn; ObjectGuid = $before.ObjectGuid;
|
||||
[pscustomobject]@{ Version = 1; Kind = 'WelaAdSaclAddition'; ReceiptStatus = 'Confirmed'; ConfirmedUtc = [DateTime]::UtcNow.ToString('o');
|
||||
ConfirmedAfter = $after; Server = $before.Server; Dn = $before.Dn; ObjectGuid = $before.ObjectGuid;
|
||||
Before = $before; AddedAces = $addition.AddedAces; ExpectedBinary = $addition.Binary } | ConvertTo-Json -Depth 12 | Set-Content $receiptPath -Encoding UTF8
|
||||
$ctx = New-WelaConfigurationContext -Auto -BackupPath (Join-Path $root 'rollback')
|
||||
Invoke-WelaAdSaclRollback $session $ctx $receiptPath
|
||||
@@ -89,5 +91,10 @@ try {
|
||||
$ctx = New-WelaConfigurationContext -Auto -BackupPath (Join-Path $root 'rollback-drift')
|
||||
Invoke-WelaAdSaclRollback $session $ctx $receiptPath
|
||||
Assert ($ctx.Results[0].Status -eq 'Failed' -and $script:writes -eq 1) 'rollback refuses ambiguous intervening SACL changes'
|
||||
$script:state.Descriptor = Get-WelaAdDescriptorInfo $addition.Binary
|
||||
$script:corruptRollback = $true
|
||||
$ctx = New-WelaConfigurationContext -Auto -BackupPath (Join-Path $root 'rollback-corruption')
|
||||
Invoke-WelaAdSaclRollback $session $ctx $receiptPath
|
||||
Assert ($ctx.Results[0].Status -eq 'Failed' -and $ctx.Results[0].Diagnostic -like '*Owner/group/DACL*') 'independent rollback snapshot detects owner corruption'
|
||||
Write-Host "Passed $script:checks native Windows AD descriptor/LDAP tests. No AD bind or live mutation occurred."
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
Reference in new issue
Block a user