mirror of https://github.com/raandree/NTFSSecurity
committed by
GitHub
64 changed files with 3727 additions and 275 deletions
@ -0,0 +1,215 @@ |
|||
<# |
|||
.SYNOPSIS |
|||
Runs the Pester tests as a basic user, without the privileges and the Administrators group of the current account. |
|||
|
|||
.DESCRIPTION |
|||
Some tests need a session without the Security, Restore, or Create Symbolic Link privilege, and skip in an elevated |
|||
session such as that of a GitHub runner. This script derives a token of the SAFER level Normal User from the token |
|||
of the current process, as runas /trustlevel:0x20000 does: the Administrators group is deny-only, and only the |
|||
privilege to bypass traverse checking stays. It starts Invoke-Tests.ps1 in the same PowerShell edition with that |
|||
token, waits for it, prints its output, and fails if the tests failed. The job summary of GitHub Actions gets the |
|||
results through a file of this account, which the restricted token can write. |
|||
|
|||
.PARAMETER ResultPath |
|||
Specifies the path of the result file in the NUnit format. |
|||
|
|||
.PARAMETER Title |
|||
Specifies the heading of the test results in the job summary. It can't contain a double quote, a percent sign, or a |
|||
line break, which would change the command line of cmd.exe. |
|||
|
|||
.EXAMPLE |
|||
.\.github\scripts\Invoke-TestsAsBasicUser.ps1 -ResultPath TestResults\WindowsPowerShell-BasicUser.xml -Title 'Windows PowerShell 5.1 as a basic user' |
|||
|
|||
Runs the tests in Windows PowerShell 5.1 as a basic user. |
|||
#> |
|||
[CmdletBinding()] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[ValidateNotNullOrEmpty()] |
|||
[string] |
|||
$ResultPath, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[ValidateNotNullOrEmpty()] |
|||
# cmd.exe gets the title in a quoted argument, expands environment variables also inside quotes, and ends the |
|||
# command at a line break. \z, unlike $, doesn't match before a final line feed. |
|||
[ValidatePattern('\A[^"%\r\n]+\z')] |
|||
[string] |
|||
$Title |
|||
) |
|||
|
|||
$ErrorActionPreference = 'Stop' |
|||
|
|||
Add-Type -TypeDefinition @' |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
public static class NTFSSecurityBasicUserProcess |
|||
{ |
|||
private const uint SaferScopeIdUser = 2; |
|||
private const uint SaferLevelIdNormalUser = 0x20000; |
|||
private const uint SaferLevelOpen = 1; |
|||
private const uint CreateNoWindow = 0x08000000; |
|||
private const uint Infinite = 0xFFFFFFFF; |
|||
private const uint WaitObject0 = 0; |
|||
|
|||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] |
|||
private struct StartupInfo |
|||
{ |
|||
public int cb; |
|||
public string lpReserved; |
|||
public string lpDesktop; |
|||
public string lpTitle; |
|||
public int dwX; |
|||
public int dwY; |
|||
public int dwXSize; |
|||
public int dwYSize; |
|||
public int dwXCountChars; |
|||
public int dwYCountChars; |
|||
public int dwFillAttribute; |
|||
public int dwFlags; |
|||
public short wShowWindow; |
|||
public short cbReserved2; |
|||
public IntPtr lpReserved2; |
|||
public IntPtr hStdInput; |
|||
public IntPtr hStdOutput; |
|||
public IntPtr hStdError; |
|||
} |
|||
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
private struct ProcessInformation |
|||
{ |
|||
public IntPtr hProcess; |
|||
public IntPtr hThread; |
|||
public int dwProcessId; |
|||
public int dwThreadId; |
|||
} |
|||
|
|||
[DllImport("advapi32.dll", SetLastError = true)] |
|||
private static extern bool SaferCreateLevel(uint scopeId, uint levelId, uint openFlags, out IntPtr levelHandle, IntPtr reserved); |
|||
|
|||
[DllImport("advapi32.dll", SetLastError = true)] |
|||
private static extern bool SaferComputeTokenFromLevel(IntPtr levelHandle, IntPtr inAccessToken, out IntPtr outAccessToken, uint flags, IntPtr reserved); |
|||
|
|||
[DllImport("advapi32.dll", SetLastError = true)] |
|||
private static extern bool SaferCloseLevel(IntPtr levelHandle); |
|||
|
|||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] |
|||
private static extern bool CreateProcessAsUser(IntPtr token, string applicationName, string commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandles, uint creationFlags, IntPtr environment, string currentDirectory, ref StartupInfo startupInfo, out ProcessInformation processInformation); |
|||
|
|||
[DllImport("kernel32.dll", SetLastError = true)] |
|||
private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds); |
|||
|
|||
[DllImport("kernel32.dll", SetLastError = true)] |
|||
private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode); |
|||
|
|||
[DllImport("kernel32.dll", SetLastError = true)] |
|||
private static extern bool CloseHandle(IntPtr handle); |
|||
|
|||
// Starts the command line with a token of the SAFER level Normal User, waits for it, and returns its exit code. |
|||
public static int Run(string applicationName, string commandLine, string currentDirectory) |
|||
{ |
|||
IntPtr level; |
|||
if (!SaferCreateLevel(SaferScopeIdUser, SaferLevelIdNormalUser, SaferLevelOpen, out level, IntPtr.Zero)) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
|
|||
IntPtr token = IntPtr.Zero; |
|||
try |
|||
{ |
|||
if (!SaferComputeTokenFromLevel(level, IntPtr.Zero, out token, 0, IntPtr.Zero)) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
|
|||
var startupInfo = new StartupInfo(); |
|||
startupInfo.cb = Marshal.SizeOf(typeof(StartupInfo)); |
|||
ProcessInformation processInformation; |
|||
if (!CreateProcessAsUser(token, applicationName, commandLine, IntPtr.Zero, IntPtr.Zero, false, CreateNoWindow, IntPtr.Zero, currentDirectory, ref startupInfo, out processInformation)) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
if (WaitForSingleObject(processInformation.hProcess, Infinite) != WaitObject0) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
|
|||
uint exitCode; |
|||
if (!GetExitCodeProcess(processInformation.hProcess, out exitCode)) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
|
|||
return (int)exitCode; |
|||
} |
|||
finally |
|||
{ |
|||
CloseHandle(processInformation.hThread); |
|||
CloseHandle(processInformation.hProcess); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
if (token != IntPtr.Zero) |
|||
{ |
|||
CloseHandle(token); |
|||
} |
|||
|
|||
SaferCloseLevel(level); |
|||
} |
|||
} |
|||
} |
|||
'@ |
|||
|
|||
$repositoryPath = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..\..')).ProviderPath |
|||
$resultFullPath = [IO.Path]::GetFullPath((Join-Path -Path $repositoryPath -ChildPath $ResultPath)) |
|||
$resultFolder = Split-Path -Path $resultFullPath -Parent |
|||
if (-not (Test-Path -LiteralPath $resultFolder)) { |
|||
New-Item -ItemType Directory -Path $resultFolder | Out-Null |
|||
} |
|||
|
|||
$runFolder = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ('NTFSSecurity.BasicUser-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
New-Item -ItemType Directory -Path $runFolder | Out-Null |
|||
$logPath = Join-Path -Path $runFolder -ChildPath 'Output.log' |
|||
$summaryPath = Join-Path -Path $runFolder -ChildPath 'Summary.md' |
|||
# In the temp folder of the account, which the restricted token can write, unlike maybe the folder of the repository |
|||
$runResultPath = Join-Path -Path $runFolder -ChildPath 'Result.xml' |
|||
|
|||
$executable = (Get-Process -Id $PID).Path |
|||
$testScript = Join-Path -Path $PSScriptRoot -ChildPath 'Invoke-Tests.ps1' |
|||
# cmd redirects the output of the tests, which have no console of their own. |
|||
$commandLine = 'cmd.exe /d /s /c ""{0}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{1}" -ResultPath "{2}" -Title "{3}" > "{4}" 2>&1"' -f |
|||
$executable, $testScript, $runResultPath, $Title, $logPath |
|||
|
|||
$stepSummary = $env:GITHUB_STEP_SUMMARY |
|||
$env:GITHUB_STEP_SUMMARY = $summaryPath |
|||
try { |
|||
"Running the tests as a basic user: $commandLine" |
|||
$exitCode = [NTFSSecurityBasicUserProcess]::Run((Join-Path -Path $env:SystemRoot -ChildPath 'System32\cmd.exe'), $commandLine, $repositoryPath) |
|||
} |
|||
finally { |
|||
$env:GITHUB_STEP_SUMMARY = $stepSummary |
|||
} |
|||
|
|||
if (Test-Path -LiteralPath $logPath) { |
|||
Get-Content -LiteralPath $logPath |
|||
} |
|||
|
|||
if ($stepSummary -and (Test-Path -LiteralPath $summaryPath)) { |
|||
$encoding = New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList $false |
|||
[IO.File]::AppendAllText($stepSummary, [IO.File]::ReadAllText($summaryPath), $encoding) |
|||
} |
|||
|
|||
if (Test-Path -LiteralPath $runResultPath) { |
|||
Copy-Item -LiteralPath $runResultPath -Destination $resultFullPath -Force |
|||
} |
|||
|
|||
Remove-Item -LiteralPath $runFolder -Recurse -Force |
|||
if ($exitCode -ne 0) { |
|||
throw "The tests as a basic user failed with exit code $exitCode." |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
--- |
|||
status: accepted |
|||
date: 2026-10-08 |
|||
last-verified: 2026-10-08 |
|||
owner: shared |
|||
source: maintainer decision of 2026-10-08 |
|||
--- |
|||
|
|||
# Decision 21: A quality gate before 5.0.0 |
|||
|
|||
- Choice: 5.0.0 ships only at the highest quality, with everything tested |
|||
(maintainer, 2026-10-08). The gate has three phases: |
|||
1. Measure 5.0.0-rc5: done on 2026-10-08 (`progress.md`). |
|||
2. Add tests until every code path is tested or explained, live tests |
|||
for the remaining cmdlets, and test-first fixes of the known defects; |
|||
release them as 5.0.0-rc6. The maintainer approved it on 2026-10-08. |
|||
3. Run the live tests on more operating systems, such as a Windows 11 |
|||
client and Server 2019 and 2022 file servers, then release 5.0.0. |
|||
- Exit criteria for 5.0.0, as proposed on 2026-10-08: |
|||
- Every cmdlet and parameter set has behavior tests, error paths |
|||
included. |
|||
- No test is skipped in every configuration that runs. |
|||
- The C# coverage is measured, and every path that no test runs is |
|||
tested or explained. |
|||
- Every known defect is fixed, or accepted by the maintainer and listed |
|||
in the release notes. |
|||
- The published package passes the live tests on every operating system |
|||
of the matrix. |
|||
- Rationale: rc5 passed every test that ran, but the tests ran 55.9% of |
|||
the code lines and 37.4% of the branches; five cmdlets had no tests of |
|||
their own, and 19 cmdlets never ran over SMB. (That measurement counted |
|||
only one of the four test runs; with all four, rc5 runs 58.1% of the |
|||
lines and 38.0% of the branches, see `techContext.md`.) |
|||
- Open: behavior changes found on the way stay the maintainer's decision |
|||
(Decision 16); so do the 244 lines of classes that no cmdlet calls, the |
|||
operating systems of Phase 3, and how to cover file servers that aren't |
|||
Windows (#34). |
|||
@ -0,0 +1,149 @@ |
|||
# Lab acceptance of 5.0.0-rc6 |
|||
|
|||
Acceptance of the release candidate 5.0.0-rc6 in the lab, on 2026-10-08, |
|||
before the pull request. It follows the procedure in the |
|||
[README](README.md#acceptance-of-a-release-candidate). |
|||
|
|||
The candidate changed twice during the acceptance. The run of `acfe3af` |
|||
passed; the coverage report of the candidate then found three defects, |
|||
which `0df2482` and `1b9edbb` fix, and the run of `1b9edbb` passed as |
|||
well; a second review led to `7b0781f`. This record describes the run of |
|||
`7b0781f`, the last commit of the pull request that changes the module, |
|||
and names the results of the earlier runs. |
|||
|
|||
## Candidate |
|||
|
|||
- Branch `ai/release-5.0.0-rc6`, commit |
|||
`7b0781ff8bb2c1ee4087a16411acd4c7070ba1fa`, 31 commits on `fcb370e` |
|||
(5.0.0-rc5). |
|||
- Release build of that commit, packaged with |
|||
`.github\scripts\New-ModulePackage.ps1`. The live tests imported the |
|||
module from the extracted `NTFSSecurity.zip`. CI builds the packages that |
|||
the tag publishes again, so their hashes differ; the live tests run once |
|||
more against the published package. |
|||
|
|||
| SHA-256 | File | |
|||
| --- | --- | |
|||
| `E99B5123F45E4F56AC005C629C2241DC616B2AAC152C17EA57A67C0823FFCA10` | `NTFSSecurity.5.0.0-rc6.nupkg` | |
|||
| `53C020EAD59467A407ED755F3D9296E9C70AFFAB184CF9CDF27AF92117FBBEE0` | `NTFSSecurity.zip` | |
|||
| `F438D7FDB3F5A1D75F5CA48D7B610EED31215FF1BF9C185C6856AA365D195C8D` | `NTFSSecurity\NTFSSecurity.dll` | |
|||
| `EE1B0DF9619C998A4482F3F79DCB2191BBEAD859660D6D924D1F333DBE7CBBCE` | `NTFSSecurity\Security2.dll` | |
|||
| `902157ABBD2E0B76DA744A918BDD174D5226C3494908ABA75F9E5DE28AE6A008` | `NTFSSecurity\ProcessPrivileges.dll` | |
|||
| `E2077AFEB38703345AE7857C1266F8B26E167ED887BFFAC8C8169A8F267BE6E9` | `NTFSSecurity\PrivilegeControl.dll` | |
|||
| `A8DA47194AB0F71232C69D01955AD93BA73C7ECEB58D0DE800CA085D4A2E18D8` | `NTFSSecurity\AlphaFS.dll` | |
|||
| `75DA9F7A54DF7011968BACB3FDF5E30B33F4C078861D1DF87BC06F5E64A962D8` | `NTFSSecurity\NTFSSecurity.psd1` | |
|||
| `3F777E9D141EE0046119DA9D5ECF88A3BC7E023726098FE2FB150528E2FB59B8` | `NTFSSecurity\NTFSSecurity.psm1` | |
|||
| `59583423241951EBE0FC2D8237D0C28C3ECC8C7CD2C115D2660F8579888632FC` | `NTFSSecurity\NTFSSecurity.Init.ps1` | |
|||
| `FB0920CC37ED858F55AFD54998DC854E27FBBE6A0177CB059CB03CFE91361197` | `NTFSSecurity\NTFSSecurity.format.ps1xml` | |
|||
| `CB6882FF91E6716605D5599E7B464C3346E461216ACED07E847621738F04FB9B` | `NTFSSecurity\NTFSSecurity.types.ps1xml` | |
|||
| `5115D0D76CA2A06795CD754539AC7EC19A70591E8C5616E7BEB5AE66E6971E6D` | `NTFSSecurity\en-US\NTFSSecurity.dll-Help.xml` | |
|||
|
|||
## Tests without a lab |
|||
|
|||
The Pester suite of the commit, 684 tests, against the same build. No test |
|||
failed, and every test ran in at least one configuration. |
|||
|
|||
| Configuration | Passed | Failed | Skipped | |
|||
| --- | ---: | ---: | ---: | |
|||
| Windows PowerShell 5.1, elevated | 662 | 0 | 22 | |
|||
| PowerShell 7, elevated | 632 | 0 | 52 | |
|||
| Windows PowerShell 5.1, basic user | 590 | 0 | 94 | |
|||
| PowerShell 7, basic user | 560 | 0 | 124 | |
|||
|
|||
The C# coverage of the four configurations, measured with AltCover on |
|||
`1b9edbb`: 68.1% of the lines and 44.3% of the branches. |
|||
|
|||
## Lab |
|||
|
|||
`WindowsAccessControlLab` (AutomatedLab on Hyper-V). Every machine runs |
|||
Windows Server 2025 Datacenter (10.0.26100). |
|||
|
|||
| Machine | Domain | Role in the tests | |
|||
| --- | --- | --- | |
|||
| `F1ADC1` | `a.forest1.net` | Domain controller of the accounts | |
|||
| `F1AFile1` | `a.forest1.net` | Client that runs the tests | |
|||
| `F1AFile2` | `a.forest1.net` | File server with the share | |
|||
| `F1BDC1` | `b.forest1.net` | Account of another domain of the forest | |
|||
| `F2DC1` | `forest2.net` | Account of another forest | |
|||
| `F3DC1` | `forest3.net` | Account of another forest | |
|||
|
|||
Readiness, checked before each run: WinRM answered on all six machines. |
|||
The four domain controllers answered LDAP (RootDSE, synchronized) and |
|||
issued a Kerberos ticket for `krbtgt`. The client and the file server |
|||
found a domain controller, had a working secure channel, and got a service |
|||
ticket for each other. The clocks were 4.3 to 5.0 seconds ahead of the |
|||
host. |
|||
|
|||
Checkpoints (Production) of the six machines, taken before each run: |
|||
`ntfs-rc6-acfe3af-before-acceptance` (11:07 UTC), |
|||
`ntfs-rc6-1b9edbb-before-acceptance` (12:21 UTC), and |
|||
`ntfs-rc6-7b0781f-before-acceptance` (12:50 UTC). |
|||
|
|||
## Results |
|||
|
|||
`Invoke-NTFSSecurityLabTest.ps1 -ModulePath <extracted package>` in both |
|||
editions, 12:51 to 13:07 UTC. The module reported version 5.0.0-rc6 in |
|||
every role that loads it. |
|||
|
|||
| Edition | Role | Passed | Failed | Skipped | |
|||
| --- | --- | ---: | ---: | ---: | |
|||
| Windows PowerShell 5.1 | Delegate | 38 | 0 | 0 | |
|||
| Windows PowerShell 5.1 | ServerAdmin | 13 | 0 | 0 | |
|||
| Windows PowerShell 5.1 | Admin | 40 | 0 | 0 | |
|||
| Windows PowerShell 5.1 | Server | 72 | 0 | 1 | |
|||
| PowerShell 7 | Delegate | 38 | 0 | 0 | |
|||
| PowerShell 7 | ServerAdmin | 13 | 0 | 0 | |
|||
| PowerShell 7 | Admin | 40 | 0 | 0 | |
|||
| PowerShell 7 | Server | 72 | 0 | 1 | |
|||
|
|||
The role Server skips the check of the module version, because it doesn't |
|||
load the module. The accounts of the other domain and forests were |
|||
`B\NtfsLiveForeign`, `forest2\NtfsLiveForeign`, and |
|||
`forest3\NtfsLiveForeign`; their 13 tests passed in both editions. |
|||
|
|||
The earlier runs had the same counts and no failure: `acfe3af` from 11:08 |
|||
to 11:25 UTC, and `1b9edbb` from 12:23 to 12:39 UTC. |
|||
|
|||
## Baseline |
|||
|
|||
The published 5.0.0-rc5 from the PowerShell Gallery, whose hash the script |
|||
checks against the one the Gallery publishes, in Windows PowerShell 5.1, |
|||
11:26 to 11:34 UTC: Delegate 38 passed, ServerAdmin 13, Admin 38 passed and |
|||
2 failed, Server 72 passed and 1 skipped. The two failures are the defect |
|||
that 5.0.0-rc6 fixes: on the share, `Get-NTFSHardLink` and |
|||
`New-NTFSHardLink -PassThru` stopped with the terminating error "(50) The |
|||
request is not supported" instead of writing a `GetHardLinkError`. The new |
|||
cases find no other difference between the two versions; the local tests |
|||
cover the other fixes of 5.0.0-rc6. |
|||
|
|||
## Cleanup |
|||
|
|||
`Invoke-NTFSSecurityLabTest.ps1 -RemoveFixture` after each run: at 11:38 |
|||
UTC after the first run and the baseline, at 12:48 UTC after the second, |
|||
and at 13:09 UTC after the third. Each check compared the lab with the 10 |
|||
SIDs of the fixture's accounts and groups, read before the removal; the |
|||
same check had found the fixture before the first removal. After each |
|||
removal: |
|||
|
|||
- No domain has the organizational unit `NTFSSecurityLive` or an account |
|||
whose name starts with `NtfsLive`. |
|||
- The file server has no share `NTFSSecurityLive`, no folder |
|||
`C:\NTFSSecurityLive` or `C:\NTFSSecurityLab`, and no local group |
|||
`NtfsLiveLocal`. |
|||
- The client has no folder `C:\NTFSSecurityLab`. |
|||
- On both machines, Administrators, Access Control Assistance Operators, |
|||
and Remote Management Users have no member of the fixture, and no |
|||
profile of the fixture's accounts is left. |
|||
|
|||
The three checkpoints stay on the six machines until the maintainer |
|||
deletes them. |
|||
|
|||
## Not covered |
|||
|
|||
- Other operating systems than Windows Server 2025, such as a Windows 11 |
|||
client and Server 2019 or 2022 file servers: Phase 3 of the quality |
|||
gate. |
|||
- File servers that aren't Windows, such as the IBM ESS system of #34: |
|||
only the feedback of the reporter covers them. |
|||
- The package that CI publishes for the tag: the live tests run against it |
|||
after the release, with `-Version 5.0.0-rc6`. |
|||
@ -0,0 +1,232 @@ |
|||
<# |
|||
Tests the error handling that the cmdlets with -Path share, with the module built in NTFSSecurity\bin\Release on |
|||
files in a sandbox folder: a path that doesn't exist, an item whose owner may not read its permissions, and an item |
|||
whose owner may not change its permissions, which the cmdlets that write the DACL handle by taking ownership. Each |
|||
error belongs to its path only, and the cmdlet continues with the next one. The tests turn the module setting |
|||
EnablePrivileges off, so that an elevated session meets the same denials as a basic user, and restore it. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
|||
)] |
|||
param () |
|||
|
|||
BeforeDiscovery { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' |
|||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value |
|||
$readEntry = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' } |
|||
# An audit entry on a file has no inheritance flags. |
|||
$auditEntry = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = 'None'; PropagationFlags = 'None' } |
|||
|
|||
# Output: whether the cmdlet writes an object for the next path, an existing file. |
|||
$missingPathCases = @( |
|||
@{ Command = 'Get-NTFSAccess'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } |
|||
@{ Command = 'Add-NTFSAccess'; Parameters = $readEntry; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Remove-NTFSAccess'; Parameters = $readEntry; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Clear-NTFSAccess'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Get-NTFSEffectiveAccess'; Parameters = @{ Account = 'S-1-1-0' }; ErrorId = 'ReadFileError'; Output = $true } |
|||
@{ Command = 'Get-NTFSOrphanedAccess'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Get-NTFSInheritance'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } |
|||
@{ Command = 'Enable-NTFSAccessInheritance'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Disable-NTFSAccessInheritance'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Set-NTFSInheritance'; Parameters = @{ AccessInheritanceEnabled = $true }; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Get-NTFSOwner'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } |
|||
@{ Command = 'Set-NTFSOwner'; Parameters = @{ Account = $currentUser }; ErrorId = 'ReadFileError'; Output = $false } |
|||
@{ Command = 'Get-NTFSSecurityDescriptor'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } |
|||
@{ Command = 'Get-Item2'; Parameters = @{}; ErrorId = 'FileNotFound'; Output = $true } |
|||
@{ Command = 'Get-FileHash2'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } |
|||
@{ Command = 'Get-NTFSHardLink'; Parameters = @{}; ErrorId = 'FileNotFound'; Output = $true } |
|||
) |
|||
|
|||
# The audit cmdlets need the Security privilege for the next path. |
|||
$missingPathAuditCases = @( |
|||
@{ Command = 'Get-NTFSAudit'; Parameters = @{} } |
|||
@{ Command = 'Add-NTFSAudit'; Parameters = $auditEntry } |
|||
@{ Command = 'Remove-NTFSAudit'; Parameters = $auditEntry } |
|||
@{ Command = 'Clear-NTFSAudit'; Parameters = @{} } |
|||
@{ Command = 'Enable-NTFSAuditInheritance'; Parameters = @{} } |
|||
@{ Command = 'Disable-NTFSAuditInheritance'; Parameters = @{} } |
|||
) |
|||
|
|||
$deniedReadCases = @( |
|||
@{ Command = 'Get-NTFSAccess'; Parameters = @{}; Output = $true } |
|||
@{ Command = 'Get-NTFSEffectiveAccess'; Parameters = @{ Account = 'S-1-1-0' }; Output = $true } |
|||
@{ Command = 'Get-NTFSOrphanedAccess'; Parameters = @{}; Output = $false } |
|||
@{ Command = 'Get-NTFSInheritance'; Parameters = @{}; Output = $true } |
|||
@{ Command = 'Get-NTFSOwner'; Parameters = @{}; Output = $true } |
|||
@{ Command = 'Get-NTFSSecurityDescriptor'; Parameters = @{}; Output = $true } |
|||
) |
|||
} |
|||
|
|||
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 'PathErrors' |
|||
Push-Location -LiteralPath $sandbox |
|||
|
|||
$privateData = (Get-Module -Name NTFSSecurity).PrivateData |
|||
$enablePrivileges = $privateData['EnablePrivileges'] |
|||
$privateData['EnablePrivileges'] = $false |
|||
$sidType = [System.Security.Principal.SecurityIdentifier] |
|||
|
|||
function Get-TestMissingPath { |
|||
$path = Join-Path -Path $sandbox -ChildPath ('Missing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $path |
|||
$path |
|||
} |
|||
|
|||
function Get-TestAcl { |
|||
# .NET, because Get-Acl of an elevated Windows PowerShell reads also items that deny reading their permissions. |
|||
# .NET Core has the method as an extension method. |
|||
param ([string] $Path) |
|||
|
|||
$info = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList $Path |
|||
if ($PSVersionTable.PSEdition -eq 'Desktop') { |
|||
$info.GetAccessControl() |
|||
} |
|||
else { |
|||
[System.IO.FileSystemAclExtensions]::GetAccessControl($info) |
|||
} |
|||
} |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
Pop-Location |
|||
Remove-TestSandbox -Sandbox $sandbox |
|||
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
|||
} |
|||
|
|||
Describe 'A path that does not exist' { |
|||
It '<Command> should write a <ErrorId> for it and continue with the next path' -ForEach $missingPathCases { |
|||
$missing = Get-TestMissingPath |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Next' |
|||
|
|||
$output = @(& $Command -Path $missing, $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) |
|||
|
|||
$pathErrors | Should -HaveCount 1 |
|||
$pathErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" |
|||
$pathErrors[0].TargetObject | Should -Be $missing |
|||
if ($Output) { |
|||
$output | Should -Not -BeNullOrEmpty |
|||
} |
|||
} |
|||
|
|||
It '<Command> should write a ReadFileError for it and continue with the next path' -ForEach $missingPathAuditCases -Skip:(-not $holdsSecurityPrivilege) { |
|||
$privateData['EnablePrivileges'] = $true |
|||
try { |
|||
$missing = Get-TestMissingPath |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NextAudit' |
|||
|
|||
& $Command -Path $missing, $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue | Out-Null |
|||
} |
|||
finally { |
|||
$privateData['EnablePrivileges'] = $false |
|||
} |
|||
|
|||
$pathErrors | Should -HaveCount 1 |
|||
$pathErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$pathErrors[0].TargetObject | Should -Be $missing |
|||
} |
|||
} |
|||
|
|||
Describe 'An item whose owner may not read its permissions' { |
|||
# A deny entry for OWNER RIGHTS replaces the right of the owner to read the security descriptor. Taking ownership |
|||
# can't help: the cmdlet must read the owner first, which needs the same right. Before 5.0.0-rc6, |
|||
# Get-NTFSOrphanedAccess reported this as an AddAceError. |
|||
It '<Command> should write a ReadSecurityError for it and continue with the next path' -ForEach $deniedReadCases { |
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'Unreadable' |
|||
Block-TestReadPermission -Sandbox $sandbox -Path $blocked |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NextReadable' |
|||
|
|||
$output = @(& $Command -Path $blocked, $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) |
|||
|
|||
$pathErrors | Should -HaveCount 1 |
|||
$pathErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' |
|||
$pathErrors[0].TargetObject | Should -Be $blocked |
|||
if ($Output) { |
|||
$output | Should -Not -BeNullOrEmpty |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'An item whose owner may not change its permissions' { |
|||
# A deny entry for OWNER RIGHTS replaces the right of the owner to change the DACL. The cmdlets take ownership, |
|||
# which Windows answers by removing the OWNER RIGHTS entries, write the DACL, and set the previous owner back. |
|||
BeforeEach { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Unchangeable' |
|||
$acl = Get-TestAcl -Path $file |
|||
$owner = $acl.GetOwner($sidType).Value |
|||
} |
|||
|
|||
It 'Add-NTFSAccess should take ownership, add the entry, and set the owner back' { |
|||
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } |
|||
|
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorVariable changeErrors -ErrorAction SilentlyContinue |
|||
|
|||
$changeErrors | Should -BeNullOrEmpty |
|||
$acl = Get-TestAcl -Path $file |
|||
$acl.GetOwner($sidType).Value | Should -Be $owner |
|||
@($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1 |
|||
@($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-3-4' }) | Should -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Remove-NTFSAccess should take ownership, remove the entry, and set the owner back' { |
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData |
|||
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } |
|||
|
|||
Remove-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorVariable changeErrors -ErrorAction SilentlyContinue |
|||
|
|||
$changeErrors | Should -BeNullOrEmpty |
|||
$acl = Get-TestAcl -Path $file |
|||
$acl.GetOwner($sidType).Value | Should -Be $owner |
|||
@($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Clear-NTFSAccess should take ownership, remove the explicit entries, and set the owner back' { |
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData |
|||
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } |
|||
|
|||
Clear-NTFSAccess -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue |
|||
|
|||
$changeErrors | Should -BeNullOrEmpty |
|||
$acl = Get-TestAcl -Path $file |
|||
$acl.GetOwner($sidType).Value | Should -Be $owner |
|||
@($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Disable-NTFSAccessInheritance should take ownership, protect the DACL, and set the owner back' { |
|||
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } |
|||
|
|||
Disable-NTFSAccessInheritance -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue |
|||
|
|||
$changeErrors | Should -BeNullOrEmpty |
|||
$acl = Get-TestAcl -Path $file |
|||
$acl.GetOwner($sidType).Value | Should -Be $owner |
|||
$acl.AreAccessRulesProtected | Should -BeTrue |
|||
} |
|||
|
|||
It 'Enable-NTFSAccessInheritance should take ownership, let the DACL inherit, and set the owner back' { |
|||
Disable-NTFSAccessInheritance -Path $file |
|||
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } |
|||
|
|||
Enable-NTFSAccessInheritance -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue |
|||
|
|||
$changeErrors | Should -BeNullOrEmpty |
|||
$acl = Get-TestAcl -Path $file |
|||
$acl.GetOwner($sidType).Value | Should -Be $owner |
|||
$acl.AreAccessRulesProtected | Should -BeFalse |
|||
} |
|||
|
|||
It 'Set-NTFSInheritance should take ownership, protect the DACL, and set the owner back' { |
|||
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } |
|||
|
|||
Set-NTFSInheritance -Path $file -AccessInheritanceEnabled $false -ErrorVariable changeErrors -ErrorAction SilentlyContinue |
|||
|
|||
$changeErrors | Should -BeNullOrEmpty |
|||
$acl = Get-TestAcl -Path $file |
|||
$acl.GetOwner($sidType).Value | Should -Be $owner |
|||
$acl.AreAccessRulesProtected | Should -BeTrue |
|||
} |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
<# |
|||
Tests the SecurityDescriptor parameter sets that no other test file covers, with the module built in |
|||
NTFSSecurity\bin\Release on files in a sandbox folder. The cmdlets change a descriptor of Get-NTFSSecurityDescriptor |
|||
in memory only, and the item changes when Set-NTFSSecurityDescriptor writes the descriptor; the cmdlets that read |
|||
return what their Path parameter set returns. Tests of audit entries need the Security privilege and skip without |
|||
it. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
|||
)] |
|||
param () |
|||
|
|||
BeforeDiscovery { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' |
|||
} |
|||
|
|||
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 'SecurityDescriptorSets' |
|||
Push-Location -LiteralPath $sandbox |
|||
$sidType = [System.Security.Principal.SecurityIdentifier] |
|||
|
|||
function Get-ExplicitAccessCount { |
|||
param ([System.Security.AccessControl.FileSystemSecurity] $Acl) |
|||
|
|||
@($Acl.GetAccessRules($true, $false, $sidType)).Count |
|||
} |
|||
} |
|||
|
|||
AfterAll { |
|||
Pop-Location |
|||
Remove-TestSandbox -Sandbox $sandbox |
|||
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
|||
} |
|||
|
|||
Describe 'Cmdlets that change a security descriptor in memory' { |
|||
It 'Clear-NTFSAccess should remove the explicit access entries of the descriptor' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearAccess' |
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
|
|||
Clear-NTFSAccess -SecurityDescriptor $sd -ErrorAction Stop |
|||
|
|||
Get-ExplicitAccessCount -Acl $sd.SecurityDescriptor | Should -Be 0 |
|||
Get-ExplicitAccessCount -Acl (Get-Acl -LiteralPath $file) | Should -Be 1 |
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
Get-ExplicitAccessCount -Acl (Get-Acl -LiteralPath $file) | Should -Be 0 |
|||
} |
|||
|
|||
# Like the Path parameter set, the cmdlet doesn't copy the inherited entries, so the DACL ends up empty. |
|||
It 'Clear-NTFSAccess -DisableInheritance should leave the descriptor with an empty, protected DACL' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearAccessProtected' |
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData |
|||
$daclBefore = (Get-Acl -LiteralPath $file).GetSecurityDescriptorSddlForm('Access') |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
|
|||
Clear-NTFSAccess -SecurityDescriptor $sd -DisableInheritance -ErrorAction Stop |
|||
|
|||
$sd.SecurityDescriptor.AreAccessRulesProtected | Should -BeTrue |
|||
@($sd.SecurityDescriptor.GetAccessRules($true, $true, $sidType)) | Should -BeNullOrEmpty |
|||
(Get-Acl -LiteralPath $file).GetSecurityDescriptorSddlForm('Access') | Should -Be $daclBefore |
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
$acl = Get-Acl -LiteralPath $file |
|||
$acl.AreAccessRulesProtected | Should -BeTrue |
|||
@($acl.GetAccessRules($true, $true, $sidType)) | Should -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Disable-NTFSAccessInheritance should protect the DACL of the descriptor and keep the inherited entries' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'DisableAccess' |
|||
$inheritedCount = @((Get-Acl -LiteralPath $file).GetAccessRules($false, $true, $sidType)).Count |
|||
$inheritedCount | Should -BeGreaterThan 0 |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
|
|||
Disable-NTFSAccessInheritance -SecurityDescriptor $sd -ErrorAction Stop |
|||
|
|||
$sd.SecurityDescriptor.AreAccessRulesProtected | Should -BeTrue |
|||
(Get-Acl -LiteralPath $file).AreAccessRulesProtected | Should -BeFalse |
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
$acl = Get-Acl -LiteralPath $file |
|||
$acl.AreAccessRulesProtected | Should -BeTrue |
|||
Get-ExplicitAccessCount -Acl $acl | Should -Be $inheritedCount |
|||
} |
|||
|
|||
It 'Enable-NTFSAccessInheritance should let the DACL of the descriptor inherit' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'EnableAccess' |
|||
Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
|
|||
Enable-NTFSAccessInheritance -SecurityDescriptor $sd -ErrorAction Stop |
|||
|
|||
$sd.SecurityDescriptor.AreAccessRulesProtected | Should -BeFalse |
|||
(Get-Acl -LiteralPath $file).AreAccessRulesProtected | Should -BeTrue |
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
$acl = Get-Acl -LiteralPath $file |
|||
$acl.AreAccessRulesProtected | Should -BeFalse |
|||
@($acl.GetAccessRules($false, $true, $sidType)) | Should -Not -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Clear-NTFSAudit should remove the explicit audit entries of the descriptor' -Skip:(-not $holdsSecurityPrivilege) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearAudit' |
|||
Add-NTFSAudit -Path $file -Account 'S-1-1-0' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
|
|||
Clear-NTFSAudit -SecurityDescriptor $sd -ErrorAction Stop |
|||
|
|||
@($sd.SecurityDescriptor.GetAuditRules($true, $false, $sidType)) | Should -BeNullOrEmpty |
|||
@(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -HaveCount 1 |
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
@(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Enable-NTFSAuditInheritance should let the SACL of the descriptor inherit' -Skip:(-not $holdsSecurityPrivilege) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'EnableAudit' |
|||
Disable-NTFSAuditInheritance -Path $file |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
$sd.SecurityDescriptor.AreAuditRulesProtected | Should -BeTrue |
|||
|
|||
Enable-NTFSAuditInheritance -SecurityDescriptor $sd -ErrorAction Stop |
|||
|
|||
$sd.SecurityDescriptor.AreAuditRulesProtected | Should -BeFalse |
|||
(Get-NTFSInheritance -Path $file).AuditInheritanceEnabled | Should -BeFalse |
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
(Get-NTFSInheritance -Path $file).AuditInheritanceEnabled | Should -BeTrue |
|||
} |
|||
} |
|||
|
|||
Describe 'Cmdlets that read a security descriptor in memory' { |
|||
BeforeAll { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Read' |
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData |
|||
} |
|||
|
|||
It 'Get-NTFSAccess should return the entries that it returns for the path' { |
|||
$expected = @(Get-NTFSAccess -Path $file | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.IsInherited }) |
|||
|
|||
$result = @(Get-NTFSSecurityDescriptor -Path $file | Get-NTFSAccess -ErrorAction Stop) |
|||
|
|||
$result | Should -Not -BeNullOrEmpty |
|||
($result | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.IsInherited }) -join ';' | Should -Be ($expected -join ';') |
|||
$result | ForEach-Object -Process { $_.FullName | Should -Be $file } |
|||
} |
|||
|
|||
It 'Get-NTFSOwner should return the owner that it returns for the path' { |
|||
$expected = (Get-NTFSOwner -Path $file).Owner.Sid |
|||
|
|||
$result = @(Get-NTFSSecurityDescriptor -Path $file | Get-NTFSOwner -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0].Owner.Sid | Should -Be $expected |
|||
$result[0].FullName | Should -Be $file |
|||
} |
|||
|
|||
It 'Get-NTFSAudit should return the audit entries that it returns for the path' -Skip:(-not $holdsSecurityPrivilege) { |
|||
Add-NTFSAudit -Path $file -Account 'S-1-1-0' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None |
|||
$expected = @(Get-NTFSAudit -Path $file | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.AuditFlags }) |
|||
|
|||
$result = @(Get-NTFSSecurityDescriptor -Path $file | Get-NTFSAudit -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
($result | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.AuditFlags }) -join ';' | Should -Be ($expected -join ';') |
|||
} |
|||
} |
|||
Loading…
Reference in new issue