mirror of https://github.com/raandree/NTFSSecurity
Browse Source
Reproduce and fix public rule paths, simplified audit comparisons and ReadData conversion, and boxed privilege equality. Add behavior guards for descriptor inheritance, unresolved identities, audit capability and recursive denial. Freeze this source for Release matrix measurement; final gate evidence and independent review follow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant <ai@example.com>pull/118/head
12 changed files with 561 additions and 14 deletions
@ -0,0 +1,263 @@ |
|||||
|
<# |
||||
|
Tests the public object APIs used with cmdlet output, without changing an item's security descriptor. |
||||
|
#> |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
||||
|
)] |
||||
|
param () |
||||
|
|
||||
|
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 'ObjectApis' |
||||
|
$objectPath = Join-Path -Path $sandbox -ChildPath 'Rule.txt' |
||||
|
$identity = [Security2.IdentityReference2] 'S-1-1-0' |
||||
|
$sid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-1-0' |
||||
|
} |
||||
|
|
||||
|
AfterAll { |
||||
|
Remove-TestSandbox -Sandbox $sandbox |
||||
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
||||
|
} |
||||
|
|
||||
|
Describe 'Rule constructors with a path' { |
||||
|
It 'Should preserve the supplied path and name of an <Kind> rule' -ForEach @( |
||||
|
@{ Kind = 'access' } |
||||
|
@{ Kind = 'audit' } |
||||
|
) { |
||||
|
if ($Kind -eq 'access') { |
||||
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( |
||||
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData, |
||||
|
[System.Security.AccessControl.AccessControlType]::Allow |
||||
|
) |
||||
|
$rule = New-Object -TypeName 'Security2.FileSystemAccessRule2' -ArgumentList $raw, $objectPath |
||||
|
} |
||||
|
else { |
||||
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( |
||||
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData, |
||||
|
[System.Security.AccessControl.AuditFlags]::Success |
||||
|
) |
||||
|
$rule = New-Object -TypeName 'Security2.FileSystemAuditRule2' -ArgumentList $raw, $objectPath |
||||
|
} |
||||
|
|
||||
|
$rule.FullName | Should -BeExactly $objectPath |
||||
|
$rule.Name | Should -BeExactly 'Rule.txt' |
||||
|
$rule.InheritanceEnabled = $true |
||||
|
$rule.InheritedFrom = $sandbox |
||||
|
$rule.InheritanceEnabled | Should -BeTrue |
||||
|
$rule.InheritedFrom | Should -BeExactly $sandbox |
||||
|
$rule.GetHashCode() | Should -Be $raw.GetHashCode() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Describe 'Simplified audit entries' { |
||||
|
It 'Should reduce <Rights> to <Expected>' -ForEach @( |
||||
|
@{ Rights = 'None'; Expected = 'None' } |
||||
|
@{ Rights = 'ReadData'; Expected = 'Read' } |
||||
|
@{ Rights = 'Read'; Expected = 'Read' } |
||||
|
@{ Rights = 'CreateFiles'; Expected = 'Write' } |
||||
|
@{ Rights = 'AppendData'; Expected = 'Write' } |
||||
|
@{ Rights = 'ReadExtendedAttributes'; Expected = 'Read' } |
||||
|
@{ Rights = 'WriteExtendedAttributes'; Expected = 'Write' } |
||||
|
@{ Rights = 'ExecuteFile'; Expected = 'Read' } |
||||
|
@{ Rights = 'DeleteSubdirectoriesAndFiles'; Expected = 'Delete' } |
||||
|
@{ Rights = 'ReadAttributes'; Expected = 'Read' } |
||||
|
@{ Rights = 'WriteAttributes'; Expected = 'Write' } |
||||
|
@{ Rights = 'Delete'; Expected = 'Delete' } |
||||
|
@{ Rights = 'ReadPermissions'; Expected = 'Read' } |
||||
|
@{ Rights = 'ChangePermissions'; Expected = 'Write' } |
||||
|
@{ Rights = 'TakeOwnership'; Expected = 'Write' } |
||||
|
@{ Rights = 'Synchronize'; Expected = 'Read' } |
||||
|
@{ Rights = 'FullControl'; Expected = 'Read, Write, Delete' } |
||||
|
@{ Rights = 'GenericRead'; Expected = 'Read' } |
||||
|
@{ Rights = 'GenericWrite'; Expected = 'Write' } |
||||
|
@{ Rights = 'GenericExecute'; Expected = 'Read' } |
||||
|
@{ Rights = 'GenericAll'; Expected = 'Read, Write, Delete' } |
||||
|
) { |
||||
|
$rule = New-Object -TypeName 'Security2.SimpleFileSystemAuditRule' -ArgumentList ( |
||||
|
$objectPath, $identity, [Security2.FileSystemRights2] $Rights |
||||
|
) |
||||
|
|
||||
|
$rule.AccessRights | Should -Be ([Security2.SimpleFileSystemAccessRights] $Expected) |
||||
|
$rule.FullName | Should -BeExactly $objectPath |
||||
|
$rule.Name | Should -BeExactly 'Rule.txt' |
||||
|
$rule.Identity.Sid | Should -BeExactly 'S-1-1-0' |
||||
|
} |
||||
|
|
||||
|
It 'Should compare audit entries reflexively and symmetrically, never as access entries' { |
||||
|
$first = New-Object -TypeName 'Security2.SimpleFileSystemAuditRule' -ArgumentList ( |
||||
|
$objectPath, $identity, [Security2.FileSystemRights2]::Read |
||||
|
) |
||||
|
$second = New-Object -TypeName 'Security2.SimpleFileSystemAuditRule' -ArgumentList ( |
||||
|
$objectPath, $identity, [Security2.FileSystemRights2]::Read |
||||
|
) |
||||
|
$access = New-Object -TypeName 'Security2.SimpleFileSystemAccessRule' -ArgumentList ( |
||||
|
$objectPath, $identity, [Security2.FileSystemRights2]::Read, |
||||
|
[System.Security.AccessControl.AccessControlType]::Allow |
||||
|
) |
||||
|
|
||||
|
$first.Equals($first) | Should -BeTrue |
||||
|
$first.Equals($second) | Should -BeTrue |
||||
|
$second.Equals($first) | Should -BeTrue |
||||
|
$first.GetHashCode() | Should -Be $second.GetHashCode() |
||||
|
$first.Equals($access) | Should -BeFalse |
||||
|
$access.Equals($first) | Should -BeFalse |
||||
|
$first.Equals($null) | Should -BeFalse |
||||
|
$first.Equals('Read') | Should -BeFalse |
||||
|
$second.AccessControlType = 'Deny' |
||||
|
$first.Equals($second) | Should -BeFalse |
||||
|
} |
||||
|
|
||||
|
It 'Should preserve the path, account and ReadData when converting an audit entry' { |
||||
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( |
||||
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData, |
||||
|
[System.Security.AccessControl.AuditFlags]::Success |
||||
|
) |
||||
|
$wrapped = New-Object -TypeName 'Security2.FileSystemAuditRule2' -ArgumentList $raw, $objectPath |
||||
|
|
||||
|
$simple = $wrapped.ToSimpleFileSystemAuditRule2() |
||||
|
|
||||
|
$simple.FullName | Should -BeExactly $objectPath |
||||
|
$simple.Identity.Sid | Should -BeExactly 'S-1-1-0' |
||||
|
$simple.AccessRights | Should -Be ([Security2.SimpleFileSystemAccessRights]::Read) |
||||
|
$wrapped.ToString() | Should -BeExactly $raw.ToString() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Describe 'Identity comparisons and conversions' { |
||||
|
It 'Should compare <Value> with the identity by SID or resolved name' -ForEach @( |
||||
|
@{ Value = 'self'; Expected = $true } |
||||
|
@{ Value = 'same SID'; Expected = $true } |
||||
|
@{ Value = 'different SID'; Expected = $false } |
||||
|
@{ Value = 'SecurityIdentifier'; Expected = $true } |
||||
|
@{ Value = 'NTAccount'; Expected = $true } |
||||
|
@{ Value = 'SID string'; Expected = $true } |
||||
|
@{ Value = 'account name'; Expected = $true } |
||||
|
@{ Value = 'different string'; Expected = $false } |
||||
|
@{ Value = 'null'; Expected = $false } |
||||
|
@{ Value = 'other type'; Expected = $false } |
||||
|
) { |
||||
|
$other = switch ($Value) { |
||||
|
'self' { $identity } |
||||
|
'same SID' { [Security2.IdentityReference2] 'S-1-1-0' } |
||||
|
'different SID' { [Security2.IdentityReference2] 'S-1-5-32-546' } |
||||
|
'SecurityIdentifier' { $sid } |
||||
|
'NTAccount' { $sid.Translate([System.Security.Principal.NTAccount]) } |
||||
|
'SID string' { 'S-1-1-0' } |
||||
|
'account name' { $identity.AccountName.ToUpperInvariant() } |
||||
|
'different string' { 'NTFSSecurity-not-an-account' } |
||||
|
'null' { $null } |
||||
|
'other type' { 42 } |
||||
|
} |
||||
|
|
||||
|
$identity.Equals($other) | Should -Be $Expected |
||||
|
} |
||||
|
|
||||
|
It 'Should compare null operands and distinct instances through both operators' { |
||||
|
$same = [Security2.IdentityReference2] 'S-1-1-0' |
||||
|
$different = [Security2.IdentityReference2] 'S-1-5-32-546' |
||||
|
|
||||
|
[Security2.IdentityReference2]::op_Equality($null, $null) | Should -BeTrue |
||||
|
[Security2.IdentityReference2]::op_Equality($identity, $null) | Should -BeFalse |
||||
|
[Security2.IdentityReference2]::op_Equality($null, $identity) | Should -BeFalse |
||||
|
[Security2.IdentityReference2]::op_Equality($identity, $same) | Should -BeTrue |
||||
|
[Security2.IdentityReference2]::op_Inequality($identity, $identity) | Should -BeFalse |
||||
|
[Security2.IdentityReference2]::op_Inequality($identity, $null) | Should -BeTrue |
||||
|
[Security2.IdentityReference2]::op_Inequality($null, $identity) | Should -BeTrue |
||||
|
[Security2.IdentityReference2]::op_Inequality($identity, $different) | Should -BeTrue |
||||
|
[Security2.IdentityReference2]::op_Inequality($identity, $same) | Should -BeFalse |
||||
|
$identity.GetHashCode() | Should -Be $same.GetHashCode() |
||||
|
} |
||||
|
|
||||
|
It 'Should round-trip native identities and the binary SID without changing the account' { |
||||
|
$account = $sid.Translate([System.Security.Principal.NTAccount]) |
||||
|
$fromSid = [Security2.IdentityReference2]::op_Explicit($sid) |
||||
|
$fromAccount = [Security2.IdentityReference2]::op_Explicit($account) |
||||
|
|
||||
|
$fromSid.Sid | Should -BeExactly 'S-1-1-0' |
||||
|
$fromAccount.Sid | Should -BeExactly $fromSid.Sid |
||||
|
([System.Security.Principal.SecurityIdentifier] $fromSid).Value | Should -BeExactly 'S-1-1-0' |
||||
|
([System.Security.Principal.NTAccount] $fromAccount).Value | Should -BeExactly $account.Value |
||||
|
$binarySid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ( |
||||
|
$fromSid.GetBinaryForm(), 0 |
||||
|
) |
||||
|
$binarySid.Value | Should -BeExactly 'S-1-1-0' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Describe 'Unrepresentable inheritance flags' { |
||||
|
It 'Should reject propagation flags without inheritance instead of inventing an AppliesTo value' { |
||||
|
$failure = $null |
||||
|
try { |
||||
|
$null = [Security2.FileSystemSecurity2]::ConvertToApplyTo('None', 'InheritOnly') |
||||
|
} |
||||
|
catch { |
||||
|
$failure = $_.Exception.GetBaseException() |
||||
|
} |
||||
|
|
||||
|
$failure | Should -BeOfType [Security2.RightsConverionException] |
||||
|
$failure.Message | Should -BeExactly 'The combination of InheritanceFlags and PropagationFlags could not be translated' |
||||
|
} |
||||
|
} |
||||
|
Describe 'Privilege output comparisons and formatting' { |
||||
|
It 'Should compare boxed and typed privilege values consistently without accepting an attributes enum' { |
||||
|
$values = @(Get-Privileges) |
||||
|
$values.Count | Should -BeGreaterThan 0 |
||||
|
$first = $values[0].PSObject.BaseObject |
||||
|
$copy = $values[0].PSObject.BaseObject |
||||
|
# PowerShell prefers the typed overload; reflection selects the public boxed-object contract explicitly. |
||||
|
$equalsObject = [ProcessPrivileges.PrivilegeAndAttributes].GetMethod('Equals', [type[]] @([object])) |
||||
|
|
||||
|
$equalsObject.Invoke($first, [object[]] @($copy)) | Should -BeTrue |
||||
|
$first.Equals($copy) | Should -BeTrue |
||||
|
[ProcessPrivileges.PrivilegeAndAttributes]::op_Equality($first, $copy) | Should -BeTrue |
||||
|
[ProcessPrivileges.PrivilegeAndAttributes]::op_Inequality($first, $copy) | Should -BeFalse |
||||
|
$first.GetHashCode() | Should -Be $copy.GetHashCode() |
||||
|
$equalsObject.Invoke($first, [object[]] @($null)) | Should -BeFalse |
||||
|
$equalsObject.Invoke($first, [object[]] @('Backup')) | Should -BeFalse |
||||
|
$equalsObject.Invoke($first, [object[]] @([ProcessPrivileges.PrivilegeAttributes]::Disabled)) | Should -BeFalse |
||||
|
} |
||||
|
|
||||
|
It 'Should format the collection with one aligned privilege and attributes row per value' { |
||||
|
$control = New-Object -TypeName 'Security2.PrivilegeControl' |
||||
|
$values = $control.GetPrivileges() |
||||
|
$expectedWidth = ($values | ForEach-Object { $_.Privilege.ToString().Length } | Measure-Object -Maximum).Maximum |
||||
|
|
||||
|
$text = $values.ToString() |
||||
|
|
||||
|
$rows = @($text.TrimEnd("`r", "`n") -split '\r?\n') |
||||
|
$rows | Should -HaveCount $values.Count |
||||
|
for ($index = 0; $index -lt $values.Count; $index++) { |
||||
|
$value = $values[$index] |
||||
|
$rows[$index] | Should -BeExactly ('{0} => {1}' -f $value.Privilege.ToString().PadRight($expectedWidth), |
||||
|
$value.PrivilegeAttributes) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Describe 'Legacy effective-permission output objects' { |
||||
|
It 'Should retain a <Mask> mask and report the supplied path and identity without a native access check' -ForEach @( |
||||
|
@{ Mask = 0; ObjectName = 'Effective.txt' } |
||||
|
@{ Mask = 1; ObjectName = 'Effective.txt' } |
||||
|
@{ Mask = 3; ObjectName = $null } |
||||
|
) { |
||||
|
$path = if ($ObjectName) { Join-Path -Path $sandbox -ChildPath $ObjectName } else { $null } |
||||
|
$entry = New-Object -TypeName 'Security2.FileSystemEffectivePermissionEntry' -ArgumentList ( |
||||
|
$identity, [uint32] $Mask, $path |
||||
|
) |
||||
|
|
||||
|
$entry.Account.Sid | Should -BeExactly 'S-1-1-0' |
||||
|
$entry.AccessMask | Should -Be $Mask |
||||
|
[int] $entry.AccessRights | Should -Be $Mask |
||||
|
$entry.FullName | Should -BeExactly ([string] $path) |
||||
|
$entry.Name | Should -BeExactly $ObjectName |
||||
|
$entry.AccessAsString | Should -Not -BeNullOrEmpty |
||||
|
if ($Mask -eq 0) { |
||||
|
$entry.AccessAsString | Should -Be @('None') |
||||
|
} |
||||
|
else { |
||||
|
$entry.AccessAsString | Should -Not -Contain 'None' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue