|
|
|
|
<#
|
|
|
|
|
Tests the audit cmdlets of the module built in NTFSSecurity\bin\Release on files in a sandbox folder. Reading
|
|
|
|
|
and changing audit entries needs the Security privilege; tests that need it skip without it and run in CI,
|
|
|
|
|
whose runners are elevated.
|
|
|
|
|
#>
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
|
|
|
|
|
)]
|
|
|
|
|
param ()
|
|
|
|
|
|
|
|
|
|
BeforeDiscovery {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$canReadAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
# Assigning an owner other than the user or one of its groups needs the Restore privilege.
|
|
|
|
|
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BeforeAll {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
|
|
|
|
|
Import-Module -Name $modulePath -Force -ErrorAction Stop
|
|
|
|
|
$sandbox = New-TestSandbox -Name 'Audit'
|
|
|
|
|
Push-Location -LiteralPath $sandbox
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
|
|
|
|
|
$enablePrivileges = $privateData['EnablePrivileges']
|
|
|
|
|
$sidType = [System.Security.Principal.SecurityIdentifier]
|
|
|
|
|
# An owner that the user can assign only with the Restore privilege
|
|
|
|
|
$trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'
|
|
|
|
|
|
|
|
|
|
function Get-RestorePrivilegeState {
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore').PrivilegeState
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
Pop-Location
|
|
|
|
|
Remove-TestSandbox -Sandbox $sandbox
|
|
|
|
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSAudit' {
|
|
|
|
|
Context 'When the audit entries cannot be read' {
|
|
|
|
|
It 'Should write an error without the Security privilege instead of returning nothing' -Skip:$canReadAudit {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NoPrivilege'
|
|
|
|
|
|
|
|
|
|
$entries = @(Get-NTFSAudit -Path $file -ErrorVariable auditErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$entries | Should -BeNullOrEmpty
|
|
|
|
|
$auditErrors | Should -HaveCount 1
|
|
|
|
|
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write an error for a security descriptor that was read without the audit entries' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AccessOnly'
|
|
|
|
|
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$entries = @(Get-NTFSAudit -SecurityDescriptor $sd -ErrorVariable auditErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$entries | Should -BeNullOrEmpty
|
|
|
|
|
$auditErrors | Should -HaveCount 1
|
|
|
|
|
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When a path fails after a path with audit entries' {
|
|
|
|
|
# Before 5.0.0, the cmdlet wrote the entries of the previous item again for the failing path. In CI, the deny
|
|
|
|
|
# entry made the original implementation, which also read the DACL, fail for the second path. The current one
|
|
|
|
|
# reads only the SACL, which the deny entry doesn't block; Access.Tests.ps1 guards the same loop fix in
|
|
|
|
|
# Get-NTFSAccess with a read that fails without elevation.
|
|
|
|
|
It 'Should return the entries of the first item once' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Audited' -Directory
|
|
|
|
|
$denied = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied'
|
|
|
|
|
Add-NTFSAudit -Path $folder -Account 'Everyone' -AccessRights Delete -AuditFlags Success
|
|
|
|
|
Block-TestReadPermission -Sandbox $sandbox -Path $denied
|
|
|
|
|
|
|
|
|
|
$entries = @(Get-NTFSAudit -Path $folder, $denied -ExcludeInherited -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
@($entries | Where-Object -Property FullName -EQ -Value $folder) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Add-NTFSAudit' {
|
|
|
|
|
Context 'Positional parameters' {
|
|
|
|
|
It 'Should take -Account at position 2 and -AccessRights at position 3 in the <_> parameter set' -ForEach @(
|
|
|
|
|
'PathSimple', 'PathComplex', 'SDSimple', 'SDComplex'
|
|
|
|
|
) {
|
|
|
|
|
$parameterSet = (Get-Command -Name Add-NTFSAudit).ParameterSets | Where-Object -Property Name -EQ -Value $_
|
|
|
|
|
$positions = @{}
|
|
|
|
|
$parameterSet.Parameters | Where-Object -Property Position -GE -Value 0 | ForEach-Object -Process {
|
|
|
|
|
$positions[$_.Name] = $_.Position
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$positions['Account'] | Should -Be 2
|
|
|
|
|
$positions['AccessRights'] | Should -Be 3
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A descriptor from Get-NTFSSecurityDescriptor contains the audit entries only with the Security privilege.
|
|
|
|
|
It 'Should bind an account and access rights that are passed by position' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Positional'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
Add-NTFSAudit -SecurityDescriptor $sd 'Everyone' 'ReadData' -InheritanceFlags None -PropagationFlags None -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$rules = $sd.SecurityDescriptor.GetAuditRules($true, $false, [System.Security.Principal.SecurityIdentifier])
|
|
|
|
|
@($rules | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A descriptor from Get-NTFSSecurityDescriptor contains the audit entries only with the Security privilege.
|
|
|
|
|
Context 'With -PassThru' -Skip:(-not $canReadAudit) {
|
|
|
|
|
It 'Should return the audit entries of a security descriptor, not its access entries' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'PassThru'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
$result = @(Add-NTFSAudit -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -PassThru)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_ | Should -BeOfType [Security2.FileSystemAuditRule2] }
|
|
|
|
|
@($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report the inheritance of the audit entries in InheritanceEnabled, not that of the access entries' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditProtected'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$sd.SecurityDescriptor.SetAuditRuleProtection($true, $false)
|
|
|
|
|
|
|
|
|
|
$result = @(Add-NTFSAudit -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -PassThru)
|
|
|
|
|
|
|
|
|
|
$sd.SecurityDescriptor.AreAccessRulesProtected | Should -BeFalse
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.InheritanceEnabled | Should -BeFalse }
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
test: cover deny entries without rights, a NULL DACL, hard links with read rights denied, and the extension methods
Add-NTFSAccess, Remove-NTFSAccess, and Add-NTFSAudit report an AddAceError or RemoveAceError for each item when .NET refuses an entry without rights, change nothing, and return nothing with -PassThru. Get-NTFSAccess returns the one entry that .NET reports for a NULL DACL without a source; the new helper Set-TestNullDacl writes it through SetNamedSecurityInfo inside the sandbox guard. Get-NTFSHardLink lists the names of a file whose read rights are denied, which is why its UnauthorizedAccessException handler has no trigger. The public Extensions.ForEach and GetParent helpers, which a static scan listed as unused although cmdlets call them, are tested directly.
All of these characterize behavior that was already correct, so none was red before; the mutations that prove their detection run against the frozen measurement commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
# .NET refuses to build an audit entry without rights. The cmdlet reports the exception for the item and goes on.
|
|
|
|
|
Context 'With -AccessRights None' -Skip:(-not $canReadAudit) {
|
|
|
|
|
It 'Should write an AddAceError for each item, change nothing, and return nothing with -PassThru' {
|
|
|
|
|
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditNoneFirst'
|
|
|
|
|
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditNoneSecond'
|
|
|
|
|
$before = @((Get-Acl -LiteralPath $first -Audit).Sddl, (Get-Acl -LiteralPath $second -Audit).Sddl)
|
|
|
|
|
|
|
|
|
|
$result = @(Add-NTFSAudit -Path $first, $second -Account 'Everyone' -AccessRights None -AuditFlags Success -PassThru -ErrorVariable auditErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$auditErrors | Should -HaveCount 2
|
|
|
|
|
for ($index = 0; $index -lt 2; $index++) {
|
|
|
|
|
$auditErrors[$index].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
|
|
|
|
|
$auditErrors[$index].CategoryInfo.Category | Should -Be 'WriteError'
|
|
|
|
|
$auditErrors[$index].TargetObject | Should -BeExactly @($first, $second)[$index]
|
|
|
|
|
$auditErrors[$index].Exception | Should -BeOfType [System.ArgumentException]
|
|
|
|
|
}
|
|
|
|
|
(Get-Acl -LiteralPath $first -Audit).Sddl | Should -BeExactly $before[0]
|
|
|
|
|
(Get-Acl -LiteralPath $second -Audit).Sddl | Should -BeExactly $before[1]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
Context 'When the item has an owner that the user cannot assign' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote the unchanged owner back, which Windows refuses without the Restore
|
|
|
|
|
# privilege (#34).
|
|
|
|
|
It 'Should add the audit entry and keep the owner' -Skip:(-not ($canReadAudit -and $canAssignAnyOwner)) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -ErrorVariable addErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$addErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With inherited access entries' {
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet also read and wrote the DACL. Read together with the SACL, the inherited entries
|
|
|
|
|
# of a DACL without the auto-inherit flag lose their inherited flag when the folder has no SACL, and the cmdlet
|
|
|
|
|
# wrote them back as explicit copies.
|
|
|
|
|
It 'Should leave the access entries unchanged' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Inherited'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$inheritedCount = @((Get-Acl -LiteralPath $file).GetAccessRules($false, $true, $sidType)).Count
|
|
|
|
|
$inheritedCount | Should -BeGreaterThan 0
|
|
|
|
|
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
@($acl.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $inheritedCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSOrphanedAudit' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$orphanedFile = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedAudit'
|
|
|
|
|
$missing = Join-Path -Path $sandbox -ChildPath 'MissingOrphanedAudit.txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With the Security privilege' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
# Two entries of accounts that don't exist and one of Everyone, which resolves. Each test reads them, so
|
|
|
|
|
# none depends on another one.
|
|
|
|
|
foreach ($sid in 'S-1-5-21-1-2-3-1001', 'S-1-5-21-1-2-3-1002', 'S-1-1-0') {
|
|
|
|
|
Add-NTFSAudit -Path $orphanedFile -Account $sid -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -ErrorAction Stop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$orphanedFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedAuditFolder' -Directory
|
|
|
|
|
Add-NTFSAudit -Path $orphanedFolder -Account 'S-1-5-21-1-2-3-1003' -AccessRights Delete -ErrorAction Stop
|
|
|
|
|
$inheritingFile = Join-Path -Path $orphanedFolder -ChildPath 'File.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $inheritingFile
|
|
|
|
|
Set-Content -LiteralPath $inheritingFile -Value 'File'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, the cmdlet wrote the entries of an item as one collection and ignored -Account.
|
|
|
|
|
It 'Should return one object per entry whose account cannot be resolved' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAudit -Path $orphanedFile)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 2
|
|
|
|
|
$result | ForEach-Object -Process { $_ | Should -BeOfType [Security2.FileSystemAuditRule2] }
|
|
|
|
|
$result.Account.Sid | Should -Not -Contain 'S-1-1-0'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return only the entries of -Account' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAudit -Path $orphanedFile -Account 'S-1-5-21-1-2-3-1002')
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].Account.Sid | Should -Be 'S-1-5-21-1-2-3-1002'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the entries of a security descriptor' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$result = @(Get-NTFSSecurityDescriptor -Path $orphanedFile | Get-NTFSOrphanedAudit -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 2
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $orphanedFile }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return an inherited entry, and nothing with -ExcludeInherited' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAudit -Path $inheritingFile -ErrorAction Stop)
|
|
|
|
|
$explicitResult = @(Get-NTFSOrphanedAudit -Path $inheritingFile -ExcludeInherited -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].Account.Sid | Should -Be 'S-1-5-21-1-2-3-1003'
|
|
|
|
|
$result[0].IsInherited | Should -BeTrue
|
|
|
|
|
$explicitResult | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report the number of orphaned entries of each item in a verbose message' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$messages = @(Get-NTFSOrphanedAudit -Path $orphanedFile -Verbose 4>&1 | Where-Object -FilterScript {
|
|
|
|
|
$_ -is [System.Management.Automation.VerboseRecord] })
|
|
|
|
|
|
|
|
|
|
$messages.Message | Should -Contain "Item $orphanedFile knows about 2 orphaned SIDs in its ACL"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write an error for a path that does not exist and continue with the next path' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAudit -Path $missing, $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$orphanedErrors | Should -HaveCount 1
|
|
|
|
|
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*'
|
|
|
|
|
$orphanedErrors[0].TargetObject | Should -Be $missing
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $orphanedFile }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Since 5.0.0-rc7, the next path has an error of its own without the Security privilege, which shows that the cmdlet
|
|
|
|
|
# continued with it.
|
|
|
|
|
It 'Should write an error for a path that does not exist and continue with the next path without the Security privilege' -Skip:$canReadAudit {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAudit -Path $missing, $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$orphanedErrors | Should -HaveCount 2
|
|
|
|
|
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*'
|
|
|
|
|
$orphanedErrors[0].TargetObject | Should -Be $missing
|
|
|
|
|
$orphanedErrors[1].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
$orphanedErrors[1].TargetObject | Should -Be $orphanedFile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write an error for a security descriptor that was read without the audit entries' {
|
|
|
|
|
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $orphanedFile), [System.Security.AccessControl.AccessControlSections]::Access
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$result = @($sd | Get-NTFSOrphanedAudit -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$orphanedErrors | Should -HaveCount 1
|
|
|
|
|
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc7, the cmdlet read the item without its audit entries and returned nothing, as for an item without
|
|
|
|
|
# orphaned entries; it now writes the error of Get-NTFSAudit.
|
|
|
|
|
It 'Should write a ReadSecurityError without the Security privilege instead of returning nothing' -Skip:$canReadAudit {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAudit -Path $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$orphanedErrors | Should -HaveCount 1
|
|
|
|
|
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
$orphanedErrors[0].TargetObject | Should -Be $orphanedFile
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Remove-NTFSAudit' {
|
|
|
|
|
Context 'When a path does not exist' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$missing = Join-Path -Path $sandbox -ChildPath 'Missing.txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, the cmdlet went on with the missing item and wrote a second, misleading RemoveAceError.
|
|
|
|
|
It 'Should write only the read error' {
|
|
|
|
|
Remove-NTFSAudit -Path $missing -Account 'Everyone' -AccessRights ReadData -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -HaveCount 1
|
|
|
|
|
$removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should not stop with -PassThru' {
|
|
|
|
|
{ Remove-NTFSAudit -Path $missing -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorAction SilentlyContinue } |
|
|
|
|
|
Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A descriptor from Get-NTFSSecurityDescriptor contains the audit entries only with the Security privilege.
|
|
|
|
|
Context 'With -RemoveSpecific' -Skip:(-not $canReadAudit) {
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$removeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSpecific' -Directory
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $removeFolder
|
|
|
|
|
Add-NTFSAudit -SecurityDescriptor $sd -Account 'Everyone' -AccessRights Modify
|
|
|
|
|
|
|
|
|
|
function Get-EveryoneAuditRule {
|
|
|
|
|
$sd.SecurityDescriptor.GetAuditRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should keep an audit entry that does not match exactly' {
|
|
|
|
|
Remove-NTFSAudit -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -RemoveSpecific
|
|
|
|
|
|
|
|
|
|
(Get-EveryoneAuditRule).FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::Modify) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove an audit entry that matches exactly' {
|
|
|
|
|
Remove-NTFSAudit -SecurityDescriptor $sd -Account 'Everyone' -AccessRights Modify -RemoveSpecific
|
|
|
|
|
|
|
|
|
|
Get-EveryoneAuditRule | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should keep an audit entry that does not match exactly, given the path' {
|
|
|
|
|
Add-NTFSAudit -Path $removeFolder -Account 'Everyone' -AccessRights Modify
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAudit -Path $removeFolder -Account 'Everyone' -AccessRights ReadData -RemoveSpecific -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$entries = @(Get-NTFSAudit -Path $removeFolder -ExcludeInherited | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' })
|
|
|
|
|
$entries | Should -HaveCount 1
|
|
|
|
|
$entries[0].AccessRights.ToString() | Should -BeLike '*Modify*'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove an audit entry that matches exactly, given the path' {
|
|
|
|
|
Add-NTFSAudit -Path $removeFolder -Account 'Everyone' -AccessRights Modify
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAudit -Path $removeFolder -Account 'Everyone' -AccessRights Modify -RemoveSpecific -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
Get-NTFSAudit -Path $removeFolder -ExcludeInherited | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' } |
|
|
|
|
|
Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With -PassThru' {
|
|
|
|
|
It 'Should return the audit entries of the item, not its access entries' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'PassThru'
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'BUILTIN\Users' -AccessRights Delete -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
|
|
|
|
|
$result = @(Remove-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -PassThru)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_ | Should -BeOfType [Security2.FileSystemAuditRule2] }
|
|
|
|
|
@($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-5-32-545' }) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: drive roots, audit descriptors, -WhatIf conflicts, and small items
- #41: for the root of a drive, the cmdlets read and changed the security
descriptor of the drive, a device object. FileSystemSecurity2 now routes
drive roots through the path-based AlphaFS methods, which keep the
trailing backslash; the removal and inheritance helpers use it too.
- #109: Add-, Remove-, and Clear-NTFSAudit report a security descriptor
without the audit entries like Get-NTFSAudit, through one helper, and
Get-NTFSEffectiveAccess names the cause that Windows reported instead
of a missing Security privilege.
- #108: Copy-Item2 and Move-Item2 check the destination only for an
operation that runs; with -WhatIf, a verbose message names the conflict.
- #111: Disable-Privileges skips the privileges that the token doesn't
hold, the privilege messages are spelled right, and Get-FileHash2
declares the type name of its objects; 05-Releasing.md documents the
release metadata tests.
- rc3 review leftovers: Remove-NTFSAudit writes nothing for an item
without a SACL, the owner retry of Set-NTFSSecurityDescriptor restores
the previous owner in a finally block and keeps an owner that the
descriptor sets, and FileSystemSecurity2.Write with another item writes
only the sections that were read.
Each fix has a test that failed first, in Windows PowerShell 5.1 and
PowerShell 7; writing a drive root was checked once on a temporary VHD.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Context 'When the item has no SACL' {
|
|
|
|
|
# An item without audit entries can have no SACL at all, and Windows denies a write without any section:
|
|
|
|
|
# (5) Access is denied. Before 5.0.0-rc4, the cmdlet failed for such an item, although there was nothing to
|
|
|
|
|
# remove.
|
|
|
|
|
It 'Should write no error' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NoSacl'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$audit = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Audit
|
|
|
|
|
)
|
|
|
|
|
$audit.SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeNullOrEmpty
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Audit cmdlets with a security descriptor without the audit entries' {
|
|
|
|
|
# Before 5.0.0-rc4, only Get-NTFSAudit reported a security descriptor that was read without the audit entries, such
|
|
|
|
|
# as without the Security privilege. The other audit cmdlets changed the missing SACL in memory and wrote no error,
|
|
|
|
|
# and -PassThru returned nothing (#109).
|
|
|
|
|
It '<Command> should write an error and leave the descriptor without audit entries' -ForEach @(
|
|
|
|
|
@{ Command = 'Add-NTFSAudit'; Parameters = @{ Account = 'Everyone'; AccessRights = 'ReadData'; PassThru = $true } }
|
|
|
|
|
@{ Command = 'Remove-NTFSAudit'; Parameters = @{ Account = 'Everyone'; AccessRights = 'ReadData'; PassThru = $true } }
|
|
|
|
|
@{ Command = 'Clear-NTFSAudit'; Parameters = @{ DisableInheritance = $true } }
|
|
|
|
|
) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AccessOnly'
|
|
|
|
|
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$result = @(& $Command -SecurityDescriptor $sd @Parameters -ErrorVariable auditErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$auditErrors | Should -HaveCount 1
|
|
|
|
|
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
# The descriptor was read with the access entries only, not without the Security privilege.
|
|
|
|
|
$auditErrors[0].Exception.Message | Should -Not -BeLike '*because it was read without the Security privilege*'
|
fix: drive roots, audit descriptors, -WhatIf conflicts, and small items
- #41: for the root of a drive, the cmdlets read and changed the security
descriptor of the drive, a device object. FileSystemSecurity2 now routes
drive roots through the path-based AlphaFS methods, which keep the
trailing backslash; the removal and inheritance helpers use it too.
- #109: Add-, Remove-, and Clear-NTFSAudit report a security descriptor
without the audit entries like Get-NTFSAudit, through one helper, and
Get-NTFSEffectiveAccess names the cause that Windows reported instead
of a missing Security privilege.
- #108: Copy-Item2 and Move-Item2 check the destination only for an
operation that runs; with -WhatIf, a verbose message names the conflict.
- #111: Disable-Privileges skips the privileges that the token doesn't
hold, the privilege messages are spelled right, and Get-FileHash2
declares the type name of its objects; 05-Releasing.md documents the
release metadata tests.
- rc3 review leftovers: Remove-NTFSAudit writes nothing for an item
without a SACL, the owner retry of Set-NTFSSecurityDescriptor restores
the previous owner in a finally block and keeps an owner that the
descriptor sets, and FileSystemSecurity2.Write with another item writes
only the sections that were read.
Each fix has a test that failed first, in Windows PowerShell 5.1 and
PowerShell 7; writing a drive root was checked once on a temporary VHD.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
$sd.SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Describe 'Clear-NTFSAudit' {
|
|
|
|
|
Context 'When the item has an owner that the user cannot assign' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote the unchanged owner back, which Windows refuses without the Restore
|
|
|
|
|
# privilege (#34).
|
|
|
|
|
It 'Should remove the audit entries and keep the owner' -Skip:(-not ($canReadAudit -and $canAssignAnyOwner)) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearOtherOwner'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
|
|
|
|
|
Clear-NTFSAudit -Path $file -ErrorVariable clearErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$clearErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the item has no SACL' {
|
|
|
|
|
# An item without audit entries can have no SACL at all, and Windows denies a write without any section.
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet also read and wrote the DACL, and in an elevated session it wrote the inherited
|
|
|
|
|
# access entries back as explicit copies.
|
|
|
|
|
It 'Should write no error and leave the access entries unchanged' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NoSacl'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$audit = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Audit
|
|
|
|
|
)
|
|
|
|
|
$audit.SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeNullOrEmpty
|
|
|
|
|
$inheritedCount = @((Get-Acl -LiteralPath $file).GetAccessRules($false, $true, $sidType)).Count
|
|
|
|
|
# Without inherited entries, the test couldn't see them copied as explicit ones (#110).
|
|
|
|
|
$inheritedCount | Should -BeGreaterThan 0
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Clear-NTFSAudit -Path $file -ErrorVariable clearErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$clearErrors | Should -BeNullOrEmpty
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
@($acl.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $inheritedCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'Without the Security privilege' {
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet read the security descriptor without its SACL, found no audit entries to remove,
|
|
|
|
|
# and finished without an error although nothing was changed; it also wrote the DACL back.
|
|
|
|
|
It 'Should write an error and leave the item unchanged' -Skip:$canReadAudit {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NoPrivilege'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sddl = (Get-Acl -LiteralPath $file).Sddl
|
|
|
|
|
|
|
|
|
|
Clear-NTFSAudit -Path $file -ErrorVariable clearErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$clearErrors | Should -HaveCount 1
|
|
|
|
|
$clearErrors[0].FullyQualifiedErrorId | Should -BeLike 'ClearAclError,*'
|
|
|
|
|
(Get-Acl -LiteralPath $file).Sddl | Should -BeExactly $sddl
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: compare output objects and report InheritedFrom correctly
Equals of FileSystemAccessRule2, FileSystemAuditRule2, and
FileSystemSecurity2 cast its argument to the .NET type, which throws for
the wrapper types themselves: -eq and -contains, and in PowerShell 7 also
Select-Object -Unique and Compare-Object, stopped with an
InvalidCastException. GetHashCode of FileSystemSecurity2 read a field
that is never set and threw a NullReferenceException. Two objects are now
equal when they hold the same entry or descriptor, like the .NET types.
InheritedFrom: Win32.GetInheritedFrom returned the sources of the SACL
whenever the descriptor had one, also for the access entries, and the
callers gave the filtered entries of -ExcludeExplicit the sources of the
first entries of the ACL. Get-NTFSAccess -SecurityDescriptor stopped with
an ArgumentOutOfRangeException for a descriptor with audit entries, as
Get-NTFSSecurityDescriptor reads them in an elevated session. The method
now takes the ACL of the entries, and the callers map the sources before
they filter.
The coverage report of rc6 pointed at both; each test fails without its
fix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, comparing an audit entry with anything threw an InvalidCastException.
|
|
|
|
|
Describe 'Comparing audit entries' {
|
|
|
|
|
It 'Should find an entry equal to itself and not to a string' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Compare'
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'S-1-1-0' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
$entries = @(Get-NTFSAudit -Path $file -ExcludeInherited)
|
|
|
|
|
$entries | Should -HaveCount 1
|
|
|
|
|
|
|
|
|
|
$entries[0] -eq $entries[0] | Should -BeTrue
|
|
|
|
|
$entries -contains $entries[0] | Should -BeTrue
|
|
|
|
|
$entries[0].Equals('S-1-1-0') | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, -ExcludeExplicit gave each inherited audit entry the source of another entry.
|
|
|
|
|
Describe 'InheritedFrom of audit entries' {
|
|
|
|
|
It 'Should name the folder that an inherited entry comes from, also with -ExcludeExplicit' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFrom' -Directory
|
|
|
|
|
Add-NTFSAudit -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None
|
|
|
|
|
$file = Join-Path -Path $folder -ChildPath 'File.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
Set-Content -LiteralPath $file -Value 'File'
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'S-1-5-32-546' -AccessRights Delete -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
|
|
|
|
|
$all = @(Get-NTFSAudit -Path $file)
|
|
|
|
|
$inherited = @(Get-NTFSAudit -Path $file -ExcludeExplicit)
|
|
|
|
|
|
|
|
|
|
@($all | Where-Object -FilterScript { $_.IsInherited }) | Should -HaveCount 1
|
|
|
|
|
@($all | Where-Object -FilterScript { $_.IsInherited })[0].InheritedFrom | Should -Be $folder
|
|
|
|
|
$inherited | Should -HaveCount 1
|
|
|
|
|
$inherited[0].InheritedFrom | Should -Be $folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Windows names the folders of audit entries only for a caller whose Security privilege is enabled, and the cmdlets
|
|
|
|
|
# enable it. A caller of the library that doesn't gets the entries without sources. Before 5.0.0, the text lost its
|
|
|
|
|
# last character, and an explicit entry, which has no source, got it as well.
|
|
|
|
|
It 'Should name an unknown parent for an inherited entry and no source for an explicit entry when the privilege is disabled' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFromDisabled' -Directory
|
|
|
|
|
Add-NTFSAudit -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None
|
|
|
|
|
$file = Join-Path -Path $folder -ChildPath 'File.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
Set-Content -LiteralPath $file -Value 'File'
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'S-1-5-32-546' -AccessRights Delete -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
|
|
|
|
|
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$entries = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true))
|
|
|
|
|
|
|
|
|
|
$inherited = @($entries | Where-Object -FilterScript { $_.IsInherited })
|
|
|
|
|
$inherited | Should -HaveCount 1
|
|
|
|
|
$inherited[0].InheritedFrom | Should -BeExactly 'unknown parent'
|
|
|
|
|
$explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited })
|
|
|
|
|
$explicit | Should -HaveCount 1
|
|
|
|
|
$explicit[0].InheritedFrom | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The module setting GetInheritedFrom turns off the lookup of the sources, which costs a call for each item.
|
|
|
|
|
It 'Should leave InheritedFrom empty when the module setting GetInheritedFrom is off' -Skip:(-not $canReadAudit) {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFromOff' -Directory
|
|
|
|
|
Add-NTFSAudit -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None
|
|
|
|
|
$file = Join-Path -Path $folder -ChildPath 'File.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
Set-Content -LiteralPath $file -Value 'File'
|
|
|
|
|
$saved = $privateData['GetInheritedFrom']
|
|
|
|
|
$privateData['GetInheritedFrom'] = $false
|
|
|
|
|
try {
|
|
|
|
|
$result = @(Get-NTFSAudit -Path $file -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
$privateData['GetInheritedFrom'] = $saved
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$inherited = @($result | Where-Object -FilterScript { $_.IsInherited })
|
|
|
|
|
$inherited | Should -HaveCount 1
|
|
|
|
|
$inherited[0].InheritedFrom | Should -BeNullOrEmpty
|
|
|
|
|
}
|
fix: compare output objects and report InheritedFrom correctly
Equals of FileSystemAccessRule2, FileSystemAuditRule2, and
FileSystemSecurity2 cast its argument to the .NET type, which throws for
the wrapper types themselves: -eq and -contains, and in PowerShell 7 also
Select-Object -Unique and Compare-Object, stopped with an
InvalidCastException. GetHashCode of FileSystemSecurity2 read a field
that is never set and threw a NullReferenceException. Two objects are now
equal when they hold the same entry or descriptor, like the .NET types.
InheritedFrom: Win32.GetInheritedFrom returned the sources of the SACL
whenever the descriptor had one, also for the access entries, and the
callers gave the filtered entries of -ExcludeExplicit the sources of the
first entries of the ACL. Get-NTFSAccess -SecurityDescriptor stopped with
an ArgumentOutOfRangeException for a descriptor with audit entries, as
Get-NTFSSecurityDescriptor reads them in an elevated session. The method
now takes the ACL of the entries, and the callers map the sources before
they filter.
The coverage report of rc6 pointed at both; each test fails without its
fix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Audit changes with the Security privilege disabled' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$holdsSecurityForOperations = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$savedEnablePrivileges = $privateData['EnablePrivileges']
|
|
|
|
|
$securityWasEnabled = (Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Security').PrivilegeState -eq 'Enabled'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterEach {
|
|
|
|
|
$privateData['EnablePrivileges'] = $savedEnablePrivileges
|
|
|
|
|
if ($securityWasEnabled) {
|
|
|
|
|
$null = [ProcessPrivileges.ProcessExtensions]::EnablePrivilege(
|
|
|
|
|
[Diagnostics.Process]::GetCurrentProcess(), [ProcessPrivileges.Privilege]::Security
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$null = [ProcessPrivileges.ProcessExtensions]::DisablePrivilege(
|
|
|
|
|
[Diagnostics.Process]::GetCurrentProcess(), [ProcessPrivileges.Privilege]::Security
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It '<Command> should use a held privilege or report a missing one and continue to the next path' -ForEach @(
|
|
|
|
|
@{ Command = 'Add-NTFSAudit'; ErrorId = 'AddAceError'; Parameters = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData'; PassThru = $true } }
|
|
|
|
|
@{ Command = 'Remove-NTFSAudit'; ErrorId = 'RemoveAceError'; Parameters = @{ Account = 'S-1-1-0'; AccessRights = 'Delete'; PassThru = $true } }
|
|
|
|
|
@{ Command = 'Clear-NTFSAudit'; ErrorId = 'ClearAclError'; Parameters = @{ DisableInheritance = $true } }
|
|
|
|
|
@{ Command = 'Enable-NTFSAuditInheritance'; ErrorId = 'ModifySdError'; Parameters = @{ PassThru = $true; RemoveExplicitAuditRules = $true } }
|
|
|
|
|
@{ Command = 'Disable-NTFSAuditInheritance'; ErrorId = 'ModifySdError'; Parameters = @{ PassThru = $true; RemoveInheritedAuditRules = $true } }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'DisabledSecurity'
|
|
|
|
|
$missing = Join-Path -Path $sandbox -ChildPath ('MissingAudit-{0}' -f [guid]::NewGuid().ToString('N'))
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $path, $missing
|
|
|
|
|
if ($holdsSecurityForOperations) {
|
|
|
|
|
$privateData['EnablePrivileges'] = $true
|
|
|
|
|
Add-NTFSAudit -Path $path -Account 'S-1-1-0' -AccessRights Delete -AuditFlags Success -AppliesTo ThisFolderOnly
|
|
|
|
|
$saclBefore = (Get-NTFSSecurityDescriptor -Path $path).SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit')
|
|
|
|
|
}
|
|
|
|
|
$before = (Get-Acl -LiteralPath $path).Sddl
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
$null = [ProcessPrivileges.ProcessExtensions]::DisablePrivilege(
|
|
|
|
|
[Diagnostics.Process]::GetCurrentProcess(), [ProcessPrivileges.Privilege]::Security
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Security').PrivilegeState | Should -Not -Be 'Enabled'
|
|
|
|
|
|
|
|
|
|
$result = @(& $Command -Path $path, $missing @Parameters -ErrorVariable auditErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $path).Sddl | Should -BeExactly $before
|
|
|
|
|
if ($holdsSecurityForOperations) {
|
|
|
|
|
# AlphaFS temporarily enables a held Security privilege for SACL access, even with automatic privileges off.
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Security').PrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$auditErrors | Should -HaveCount 1
|
|
|
|
|
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
|
|
|
|
|
$auditErrors[0].CategoryInfo.Category | Should -Be 'OpenError'
|
|
|
|
|
$auditErrors[0].TargetObject | Should -BeExactly $missing
|
|
|
|
|
$written = Get-NTFSSecurityDescriptor -Path $path
|
|
|
|
|
$rules = @($written.SecurityDescriptor.GetAuditRules(
|
|
|
|
|
$true, $false, [System.Security.Principal.SecurityIdentifier]
|
|
|
|
|
))
|
|
|
|
|
switch ($Command) {
|
|
|
|
|
'Add-NTFSAudit' {
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object { $_.FullName | Should -BeExactly $path }
|
|
|
|
|
@($rules | Where-Object {
|
|
|
|
|
$_.IdentityReference.Value -eq 'S-1-1-0' -and $_.FileSystemRights.HasFlag(
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData
|
|
|
|
|
)
|
|
|
|
|
}).Count | Should -BeGreaterThan 0
|
|
|
|
|
}
|
|
|
|
|
'Disable-NTFSAuditInheritance' {
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].AuditInheritanceEnabled | Should -BeFalse
|
|
|
|
|
$rules | Should -HaveCount 1
|
|
|
|
|
$rules[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights]::Delete)
|
|
|
|
|
}
|
|
|
|
|
'Enable-NTFSAuditInheritance' {
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].AuditInheritanceEnabled | Should -BeTrue
|
|
|
|
|
$rules | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
default {
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$rules | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$written.SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -Not -BeExactly $saclBefore
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$auditErrors | Should -HaveCount 2
|
|
|
|
|
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
|
|
|
|
|
$auditErrors[0].CategoryInfo.Category | Should -Be 'WriteError'
|
|
|
|
|
$auditErrors[0].TargetObject | Should -BeExactly $path
|
|
|
|
|
$auditErrors[1].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
|
|
|
|
|
$auditErrors[1].CategoryInfo.Category | Should -Be 'OpenError'
|
|
|
|
|
$auditErrors[1].TargetObject | Should -BeExactly $missing
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Describe 'Clear-NTFSAudit descriptor inheritance' {
|
|
|
|
|
It 'Should clear and protect the descriptor SACL without writing the <Type>' -Skip:(-not $canReadAudit) -ForEach @(
|
|
|
|
|
@{ Type = 'file' }
|
|
|
|
|
@{ Type = 'folder' }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearAuditDescriptor' -Directory:($Type -eq 'folder')
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
|
|
|
|
|
Add-NTFSAudit -Path $path -Account 'S-1-1-0' -AccessRights Delete -AuditFlags Success -AppliesTo ThisFolderOnly
|
|
|
|
|
$before = Get-NTFSSecurityDescriptor -Path $path
|
|
|
|
|
$auditBefore = $before.SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit')
|
|
|
|
|
$daclBefore = (Get-Acl -LiteralPath $path).Sddl
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $path
|
|
|
|
|
|
|
|
|
|
Clear-NTFSAudit -SecurityDescriptor $sd -DisableInheritance -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$sd.SecurityDescriptor.AreAuditRulesProtected | Should -BeTrue
|
|
|
|
|
@($sd.SecurityDescriptor.GetAuditRules($true, $true, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
(Get-NTFSSecurityDescriptor -Path $path).SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') |
|
|
|
|
|
Should -BeExactly $auditBefore
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop
|
|
|
|
|
(Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -BeFalse
|
|
|
|
|
@(Get-NTFSAudit -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $path).Sddl | Should -BeExactly $daclBefore
|
|
|
|
|
}
|
|
|
|
|
}
|