PureInvoke.psm1

# Copyright WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

using namespace PureInvoke
using namespace PureInvoke.LsaLookup
using namespace PureInvoke.WinNT
using namespace System.ComponentModel
using namespace System.Runtime.InteropServices
using namespace System.Security.Principal
using namespace System.Text

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

# Functions should use $script:moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$script:moduleRoot = $PSScriptRoot


# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = Join-Path -Path $script:moduleRoot -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function Assert-NTStatusSuccess
{
    [CmdletBinding()]
    param(
        [UInt32] $Status,

        [String] $Message
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    # https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-ntstatus-values
    if ($Status -le 0x3FFFFFFF -or ($Status -ge 0x40000000 -and $Status -le 0x7FFFFFFF))
    {
        return $true
    }

    $win32Err = Invoke-AdvApiLsaNtStatusToWinError -Status $ntstatus
    Write-Win32Error -ErrorCode $win32Err -Message $Message
    return $false
}


function Assert-Win32Error
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int] $ErrorCode,

        [String] $Message
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($ErrorCode -eq 0x0)
    {
        return $true
    }

    Write-Win32Error -ErrorCode $ErrorCode -Message $Message
    return $false
}


function ConvertTo-IntPtr
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ParameterSetName='SecurityIdentifier')]
        [SecurityIdentifier] $Sid,

        [Parameter(Mandatory, ParameterSetName='LUID')]
        [LUID] $LUID
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($Sid)
    {
        $sidBytes = [byte[]]::New($Sid.BinaryLength)
        $sid.GetBinaryForm($sidBytes, 0);
        $sidPtr = [Marshal]::AllocHGlobal($sidBytes.Length)
        [Marshal]::Copy($sidBytes, 0, $sidPtr, $sidBytes.Length)
        return $sidPtr
    }

    if ($LUID)
    {
        $size = [Marshal]::SizeOf($LUID)
        $luidPtr = [Marshal]::AllocHGlobal($size)

        $lowBytes = [BitConverter]::GetBytes($LUID.LowPart)
        [Marshal]::Copy($lowBytes, 0, $luidPtr, $lowBytes.Length)

        $highBytes = [BitConverter]::GetBytes($LUID.HighPart)
        [Marshal]::Copy($highBytes, 0, [IntPtr]::Add($luidPtr, $lowBytes.Length), $highBytes.Length)

        return $luidPtr
    }
}


function ConvertTo-LsaUnicodeString
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $InputObject
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        [LSA_UNICODE_STRING]::New($InputObject) | Write-Output
    }
}


function Invoke-AdvApiLookupAccountName
{
    <#
    .SYNOPSIS
    Calls the Advanced Windows 32 Base API (advapi32.dll) `LookupAccountName` function.
 
    .DESCRIPTION
    The `Invoke-AdvApiLookupAccountName` function calls the advapi32.dll API's `LookupAccountName` function, which looks up
    an account name and returns its domain, SID, and use. Pass the account name to the `AccountName` parameter and the
    system name to the `SystemName` parameter, which are passed to `LookupAccountName` as the `lpAccountName` and
    `lpSystemName` arguments, respectively. The function returns an object with properties for each of the
    `LookupAccountName` function's out parameters: `DomainName`, `Sid`, and `Use`.
 
    .LINK
    https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupaccountnamea
 
    .EXAMPLE
    Invoke-AdvApiLookupAccountName -AccountName ([Environment]::UserName)
 
    Demonstrates how to call this function by passing a username to the `AccountName` parameter.
    #>

    [CmdletBinding()]
    param(
        # The account name to lookup.
        [Parameter(Mandatory)]
        [String] $AccountName,

        # The name of the system.
        [String] $SystemName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    [byte[]] $sid = [byte[]]::New(0);

    # cb = count of bytes
    [UInt32] $cbSid = 0;
    [StringBuilder] $sbDomainName = [StringBuilder]::New()
    # cch = count of chars
    [UInt32] $cchDomainName = $sbDomainName.Capacity;
    [SidNameUse] $sidNameUse = [SidNameUse]::Unknown;

    $result = [AdvApi32]::LookupAccountName($SystemName, $AccountName, $sid, [ref] $cbSid, $sbDomainName,
                                            [ref] $cchDomainName, [ref]$sidNameUse)
    $errCode = [Marshal]::GetLastWin32Error()

    if (-not $result)
    {
        if ($errCode -eq [ErrorCode]::InsufficientBuffer -or $errCode -eq [ErrorCode]::InvalidFlags)
        {
            $sid = [byte[]]::New($cbSid);
            [void]$sbDomainName.EnsureCapacity([int]$cchDomainName);
            $result = [AdvApi32]::LookupAccountName($SystemName, $AccountName, $sid, [ref] $cbSid, $sbDomainName,
                                                    [ref] $cchDomainName, [ref] $sidNameUse)
            $errCode = [Marshal]::GetLastWin32Error()
        }

        if (-not $result -and -not (Assert-Win32Error -ErrorCode $errCode))
        {
            return
        }
    }

    return [pscustomobject]@{
        DomainName = $sbDomainName.ToString();
        Sid = $sid
        Use = $sidNameUse
    }
}



function Invoke-AdvApiLookupAccountSid
{
    <#
    .SYNOPSIS
    Calls the Advanced Windows 32 Base API (advapi32.dll) `LookupAccountSid` function.
 
    .DESCRIPTION
    The `Invoke-AdvApiLookupAccountSid` function calls the advapi32.dll API's `LookupAccountSid` function, which looks up a
    SID and returns its account name, domain name, and use. Pass the SID as a byte array to the `Sid` parameter and the
    system name to the `SystemName` parameter, which are passed to `LookupAccountSid` as the `Sid` and `lpSystemName`
    arguments, respectively. The function returns an object with properties for each of the `LookupAccountSid`
    function's out parameters: `Name`, `ReferencedDomainName`, and `Use`.
 
    .LINK
    https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupaccountsida
 
    .EXAMPLE
    Invoke-AdvApiLookupAccountSid -Sid $sid
 
    Demonstrates how to call this function by passing a sid to the `Sid` parameter.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [byte[]] $Sid,

        [String] $SystemName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    [StringBuilder] $name = [StringBuilder]::New()
    # cch = count of chars
    [UInt32] $cchName = $name.Capacity;

    [StringBuilder] $domainName = [StringBuilder]::New()
    [UInt32] $cchDomainName = $domainName.Capacity;

    [SidNameUse] $sidNameUse = [SidNameUse]::Unknown;

    $result = [AdvApi32]::LookupAccountSid($SystemName, $sid, $name, [ref] $cchName, $domainName, [ref] $cchDomainName,
                                           [ref] $sidNameUse)
    $errCode = [Marshal]::GetLastWin32Error()

    if (-not $result)
    {
        if ($errCode -eq [PureInvoke.ErrorCode]::InsufficientBuffer)
        {
            [void]$name.EnsureCapacity($cchName);
            [void]$domainName.EnsureCapacity($cchName);
            $result = [AdvApi32]::LookupAccountSid($SystemName, $sid,  $name, [ref] $cchName, $domainName,
                                                   [ref] $cchDomainName, [ref] $sidNameUse)
            $errCode = [Marshal]::GetLastWin32Error()
        }

        if (-not $result -and -not (Assert-Win32Error -ErrorCode $errCode))
        {
            return
        }
    }

    return [pscustomobject]@{
        Name = $name.ToString();
        DomainName = $domainName.ToString();
        Use = $sidNameUse;
    }
}


function Invoke-AdvApiLookupPrivilegeName
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LookupPrivilegeName` function to lookup a privilege name from its local unique
    identifier.
 
    .DESCRIPTION
    The `Invoke-AdvApiLookupPrivilegeName` function calls the advapi32.dll library's `LookupPrivilegeName` function to
    lookup a privilege name from its local unique identifier. Pass the privilege's LUID to the `LUID` parameter. If the
    privilege exists, its name is returned. Otherwise nothing is returned and an error is written.
 
    .EXAMPLE
    Invoke-AdvapiLookupPrivilegeName -Luid $luid
 
    Demonstrates how to call this function.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [LUID] $LUID,

        [String] $SystemName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sbName = [StringBuilder]::New(1)
    $nameLength = $sbName.Capacity

    $ptrLuid = ConvertTo-IntPtr -LUID $LUID

    try
    {
        $result = [AdvApi32]::LookupPrivilegeName($SystemName, $ptrLuid, $sbName, [ref] $nameLength)
        $errCode = [Marshal]::GetLastWin32Error()

        if (-not $result)
        {
            if ($errCode -eq [ErrorCode]::InsufficientBuffer)
            {
                [void]$sbName.EnsureCapacity($nameLength)
                $result = [AdvApi32]::LookupPrivilegeName($SystemName, $ptrLuid, $sbName, [ref] $nameLength)
                $errCode = [Marshal]::GetLastWin32Error()
            }

            if (-not $result -and -not (Assert-Win32Error -ErrorCode $errCode))
            {
                return
            }
        }

        return $sbName.ToString()
    }
    finally
    {
        [Marshal]::FreeHGlobal($ptrLuid)
    }
}


function Invoke-AdvApiLookupPrivilegeValue
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LookupPrivilegeValue` function to lookup a privilege's local unique identifier.
 
    .DESCRIPTION
    The `Invoke-AdvApiLookupPrivilegeValue` function calls the advapi32.dll library's `LookupPrivilegeValue` function to
    lookup a privilege's LUID from its name. Pass the privilege's name to the `Name` parameter. If the privilege exists,
    its LUID is returned. Otherwise nothing is returned and an error is written.
 
    Privilege names are case-sensitive.
 
    Also, privilege names *do not* include account rights, even though the names look similar. The following [known
    account rights](https://learn.microsoft.com/en-us/windows/win32/secauthz/account-rights-constants) are not supported
    by `LookupPrivilegeValue`:
 
    * SeBatchLogonRight
    * SeDenyBatchLogonRight
    * SeDenyInteractiveLogonRight
    * SeDenyNetworkLogonRight
    * SeDenyRemoteInteractiveLogonRight
    * SeDenyServiceLogonRight
    * SeInteractiveLogonRight
    * SeNetworkLogonRight
    * SeRemoteInteractiveLogonRight
    * SeServiceLogonRight
    * SeUnsolicitedInputPrivilege
 
    .EXAMPLE
    Invoke-AdvapiLookupPrivilegeName -Name SeDebugPrivilege
 
    Demonstrates how to call this function.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [String] $Name,

        [String] $SystemName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    [LUID] $luid = [LUID]::New()
    $result = [AdvApi32]::LookupPrivilegeValue($SystemName, $Name, [ref] $luid)
    $errCode = [Marshal]::GetLastWin32Error()

    if (-not $result -and -not (Assert-Win32Error -ErrorCode $errCode))
    {
        return
    }

    return $luid
}


function Invoke-AdvApiLsaAddAccountRights
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LsaAddAccountRights` function.
 
    .DESCRIPTION
    The `Invoke-AdvApiLsaAddAccountRights` function calls the advapi32.dll `LsaAddAccountRights` function. Pass a policy
    handle to the `PolicyHandle` parameter (use `Invoke-AdvApiLsaOpenPolicy` to create a policy handle), the security
    identifier for the account receiving rights to the `Sid` parameter, and a list of privileges/rights to add to the
    `Privilege` parameter. The account is granted the given rights.
 
    If the call succeeds, returns `$true`. Otherwise, returns `$false` and an error is written.
 
    .EXAMPLE
    Invoke-AdvApiLsaAddAccountRights -PolicyHandle $handle -Sid $sid -Privilege 'SeBatchLogonRight'
 
    Demonstrates how to call `Invoke-AdvApiLsaAddAccountRights`.
    #>

    [CmdletBinding()]
    param(
        # A handle to the policy. Use `Invoke-AdvApiLsaOpenPolicy` to get a handle. When opening the handle to add
        # account rights, you must use `LookupNames` and `CreateAccount` to the desired access.
        [Parameter(Mandatory)]
        [IntPtr] $PolicyHandle,

        # The account security identifier receiving the rights/privileges.
        [Parameter(Mandatory)]
        [SecurityIdentifier] $Sid,

        # The list of privileges to add.
        [Parameter(Mandatory)]
        [String[]] $Privilege
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sidPtr = ConvertTo-IntPtr -Sid $Sid

    [LSA_UNICODE_STRING[]] $lsaPrivs = $Privilege | ConvertTo-LsaUnicodeString

    try
    {
        $ntstatus = [AdvApi32]::LsaAddAccountRights($PolicyHandle, $sidPtr, $lsaPrivs, $lsaPrivs.Length)

        Assert-NTStatusSuccess -Status $ntstatus -Message 'LsaAddAccountRights failed'
    }
    finally
    {
        [Marshal]::FreeHGlobal($sidPtr)
    }
}



function Invoke-AdvApiLsaClose
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LsaClose` method to close an LSA policy handle.
 
    .DESCRIPTION
    The `Invoke-AdvApiLsaClose` function calls the advapi32.dll library's `LsaClose` method to close an LSA policy
    handle that was created with `Invoke-AdvApiLsaOpenPolicy`. Pass the policy handle to the `PolicyHandle` parameter.
    The function closes the policy and returns `$true` if the close succeeded. If the close fails, returns `$false` and
    writes an error.
 
    Closing a handle more than once may result in a process crash. After closing a handle, it is recommended to set it
    to `[IntPtr]::Zero` as a precaution. This function will ignore a policy handle set to `[IntPtr]::Zero`.
 
    .EXAMPLE
    Invoke-AdvApiLsaClose -PolicyHandle $handle
 
    Demonstrates how to call `Invoke-AdvApiLsaClose`.
    #>

    [CmdletBinding()]
    param(
        # The policy handle to close. Use `Invoke-AdvApiLsaOpenPolicy` to create policy handles.
        [Parameter(Mandatory)]
        [IntPtr] $PolicyHandle
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($PolicyHandle -eq [IntPtr]::Zero)
    {
        return $true
    }

    $ntstatus = [PureInvoke.AdvApi32]::LsaClose($PolicyHandle)
    Assert-NTStatusSuccess -Status $ntstatus -Message 'Invoke-AdvApiLsaClose failed'
}



function Invoke-AdvApiLsaEnumerateAccountRights
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll assembly's `LsaEnumerateAccountRights` function to get the list of an account's
    rights/privileges.
 
    .DESCRIPTION
    The `Invoke-AdvApiLsaEnumerateAccountRights` function calls the advapi32.dll assembly's `LsaEnumerateAccountRights`
    function to get the list of an account's rights/privileges. Pass a handle to the LSA policy to the `PolicyHandle`
    parameter (use `Invoke-AdvApiLsaOpenPolicy` to create a policy handle). Pass the security identifier for the account
    to the `Sid` parameter. The account's rights are returned. If the account has no rights, then nothing is returned.
    If getting the account's rights fails, nothing is returned and the function writes an error.
 
    In order to read an account's rights, the policy must be opened with the `LookupNames` access right.
 
    .EXAMPLE
    Invoke-AdvApiLsaEnumerateAccountRights -PolicyHandle $handle -Sid $sid
 
    Demonstrates how to call `Invoke-AdvApiLsaEnumerateAccountRights`
    #>

    [CmdletBinding()]
    param(
        # A policy handle. Use `Invoke-AdvApiLsaOpenPolicy` to get a handle. When opening the handle to get account
        # rights, you must request `LookupNames` access.
        [Parameter(Mandatory)]
        [IntPtr] $PolicyHandle,

        # The security identifier of the account whose rights/privileges to get.
        [Parameter(Mandatory)]
        [SecurityIdentifier] $Sid
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sidPtr = ConvertTo-IntPtr -Sid $Sid

    [IntPtr] $rightsPtr = [IntPtr]::Zero

    try
    {
        [UInt32] $rightsCount = 0
        $ntstatus = [AdvApi32]::LsaEnumerateAccountRights($PolicyHandle, $sidPtr, [ref] $rightsPtr, [ref] $rightsCount)

        $win32Err = Invoke-AdvApiLsaNtStatusToWinError -Status $ntstatus
        if ($win32Err -eq [ErrorCode]::FileNotFound)
        {
            return
        }

        if (-not (Assert-NtStatusSuccess -Status $ntstatus -Message 'Invoke-AdvApiLsaEnumerateAccountRights failed'))
        {
            return
        }

        [LSA_UNICODE_STRING[]] $lsaPrivs = [LSA_UNICODE_STRING]::PtrToLsaUnicodeStrings($rightsPtr, $rightsCount)
        foreach ($lsaPriv in $lsaPrivs)
        {
            $lsaPrivLength = $lsaPriv.Length/[Text.UnicodeEncoding]::CharSize
            $cvt = [char[]]::New($lsaPrivLength)
            [Marshal]::Copy($lsaPriv.Buffer, $cvt, 0, $lsaPrivLength);
            [String]::New($cvt) | Write-Output
        }
    }
    finally
    {
        Invoke-AdvApiLsaFreeMemory -Handle $rightsPtr | Out-Null
        [Marshal]::FreeHGlobal($sidPtr)
    }
}


function Invoke-AdvApiLsaFreeMemory
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LsaFreeMemory` function.
 
    .DESCRIPTION
    The `Invoke-AdvApiLsaFreeMemory` function calls the advapi32.dll library's `LsaFreeMemory` function. Pass the
    pointer whose memory to free to the `Handle` parameter. If the operation succeeds, the function returns `$true`,
    otherwise it returns `$false` and writes an error.
 
    .EXAMPLE
    Invoke-AdvApiLsaFreeMemory -Handle $rightsPtr
 
    Demonstrates how to call `Invoke-AdvApiLsaFreeMemory`.
    #>

    [CmdletBinding()]
    param(
        # The handle whose memory should be freed.
        [Parameter(Mandatory)]
        [IntPtr] $Handle
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $ntstatus = [PureInvoke.AdvApi32]::LsaFreeMemory($Handle)
    Assert-NTStatusSuccess -Status $ntstatus -Message 'Invoke-AdvApiLsaFreeMemory failed'
}


function Invoke-AdvApiLsaNtStatusToWinError
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LsaNtStatusToWinError` function to convert an NTSTATUS error code into a Win32
    error code.
 
    .DESCRIPTION
    The `Invoke-AdvApiLsaNtStatusToWinError` function calls the advapi32.dll library's `LsaNtStatusToWinError` function
    to convert an NTSTATUS error code into a Win32 error code. Pass the NTSTATUS code to the `Status` parameter. The
    equivalent Win32 error code is returned.
 
    .EXAMPLE
    Invoke-AdvApiLsaNtStatusToWinError -Status $ntstatus
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [UInt32] $Status
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    return [AdvApi32]::LsaNtStatusToWinError($Status)
}


function Invoke-AdvApiLsaOpenPolicy
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LsaOpenPolicy` function to open a handle to a computer's LSA policy.
 
    .DESCRIPTION
    The `Invoke-AdvApiLsaOpenPolicy` function calls the advapi32.dll library's `LsaOpenPolicy` function to open a handle
    to a computer's LSA policy. Pass the desired access to the `DesiredAccess` parameter. The function returns a handle
    to the policy if opening succeeds or, if opening fails, returns nothing and writes an error.
 
    You can open the LSA policy on a different computer by passing the computer's name to the `ComputerName` parameter.
 
    .EXAMPLE
    Invoke-AdvApiLsaOpenPolicy -DesiredAccess LookupNames,CreateAccount
 
    Demonstrates how to open a policy handle that allows reading and setting privileges.
    #>

    [CmdletBinding()]
    param(
        # The desired access for the policy handle. See the documentation for the LSA function/method the policy will
        # be used with to discover what rights are needed.
        [Parameter(Mandatory)]
        [PolicyAccessRights] $DesiredAccess,

        # The optional computer name whose LSA policy to open. The default is the local computer.
        [String] $ComputerName,

        # The value of the `LsaOpenPolicy` method's `ObjectAttribute` parameter.
        [LSA_OBJECT_ATTRIBUTES] $ObjectAttribute
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $lsaSystemName = [LSA_UNICODE_STRING]::New([Environment]::MachineName)
    if ($ComputerName)
    {
        $lsaSystemName = [LSA_UNICODE_STRING]::New($ComputerName)
    }

    if (-not $ObjectAttribute)
    {
        $ObjectAttribute = [PureInvoke.LsaLookup.LSA_OBJECT_ATTRIBUTES]::New()
        $ObjectAttribute.Length = 0
        $ObjectAttribute.RootDirectory = [IntPtr]::Zero
        $ObjectAttribute.Attributes = 0
        $ObjectAttribute.SecurityDescriptor = [IntPtr]::Zero
        $ObjectAttribute.SecurityQualityOfService = [IntPtr]::Zero
    }

    $policyHandle = [IntPtr]::Zero

    $ntstatus =
        [PureInvoke.AdvApi32]::LsaOpenPolicy([ref] $lsaSystemName, [ref] $ObjectAttribute, $DesiredAccess, [ref] $policyHandle)

    if (-not (Assert-NtStatusSuccess -Status $ntstatus -Message "Invoke-AdvApiLsaOpenPolicy failed"))
    {
        return
    }

    return $policyHandle
}


function Invoke-AdvApiLsaRemoveAccountRights
{
    <#
    .SYNOPSIS
    Calls the advapi32.dll library's `LsaRemoveAccountRights` method which removes rights/privileges for an account.
 
    .DESCRIPTION
    The `Invoke-AdvApiLsaRemoveAccountRights` function calls the advapi32.dll library's `LsaRemoveAccountRights` method
    which removes rights/privileges for an account. Pass the LSA policy handle to the `PolicyHandle` parameter. Pass the
    security identifier for the account to the `Sid` parameter. To remove *all* of the account's rights, use the `All`
    switch. Otherwise, pass the specific rights to remove to the `Privilege` parameter. If the removal succeeds, the
    function returns `$true`, otherwise it returns $false and writes an error.
 
    In order to remove rights, the policy must be opened with the `LookupNames` access right.
 
    .EXAMPLE
    Invoke-AdvApiLsaRemoveAccountRights -PolicyHandle $handle -Sid $sid -All
 
    Demonstrates how to remove all of an account's privileges.
 
    .EXAMPLE
    Invoke-AdvApiLsaRemoveAccountRights -PolicyHandle $handle -Sid $sid -Privilege 'SeBatchLogonRight'
 
    Demonstrates how to remove a specific account's privilege.
    #>

    [CmdletBinding()]
    param(
        # A policy handle. Use `Invoke-AdvApiLsaOpenPolicy` to get a handle. When opening the handle to remove account
        # rights, you must request `LookupNames` access.
        [Parameter(Mandatory)]
        [IntPtr] $PolicyHandle,

        # The security identifier of the account whose rights/privileges to remove.
        [Parameter(Mandatory)]
        [SecurityIdentifier] $Sid,

        # If set, removes all of the account's privileges.
        [Parameter(Mandatory, ParameterSetName='All')]
        [switch] $All,

        # A list of the account's specific privileges to remove.
        [Parameter(Mandatory, ParameterSetName='Specific')]
        [String[]] $Privilege
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sidPtr = ConvertTo-IntPtr -Sid $Sid

    try
    {
        if ($All)
        {
            $ntstatus =
                [AdvApi32]::LsaRemoveAccountRights($PolicyHandle, $sidPtr, $true, [LSA_UNICODE_STRING[]]::New(0), 0)
        }
        else
        {
            [LSA_UNICODE_STRING[]] $lsaPrivs = $Privilege | ConvertTo-LsaUnicodeString
            $ntstatus =
                [AdvApi32]::LsaRemoveAccountRights($PolicyHandle, $sidPtr, $false, $lsaPrivs, $lsaPrivs.Length)
        }

        $winErr = Invoke-AdvApiLsaNtStatusToWinError -Status $ntstatus
        if ($winErr -eq [ErrorCode]::FileNotFound)
        {
            return $true
        }

        Assert-NTStatusSuccess -Status $ntstatus -Message 'LsaRemoveAccountRights failed'
    }
    finally
    {
        [Marshal]::FreeHGlobal($sidPtr)
    }
}


function Invoke-KernelFindFileName
{
    <#
    .SYNOPSIS
    Calls the Win32 `FindFirstFileNameW` and `FindNextFileNameW` functions to get the hardlinks to a file.
 
    .DESCRIPTION
    The `Invoke-KernelFindFileName` function finds all the hardlinks to a file. It calls the Win32 `FindFirstFileNameW`
    and `FindNextFileNameW` functions to get the paths. It returns the path to each hardlink, which includes the path
    to the file itself. The paths are returned *without* drive qualifiers at the beginning. Since hardlinks can't cross
    physical file systems, their drives will be the same as the source path.
 
    .LINK
    https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilenamew
 
    .LINK
    https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilenamew
 
    .EXAMPLE
    Invoke-KernelFindFileName -Path 'C:\link.txt'
 
    Demonstrates how to get the hardlinks to a file by passing its path to the `Invoke-KernelFindFileName` function's
    `Path` parameter.
    #>

    [CmdletBinding()]
    param(
        # The path to the file.
        [Parameter(Mandatory)]
        [String] $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    [PureInvoke.ErrorCode] $errCode = [PureInvoke.ErrorCode]::Ok

    # Loop over and collect all hard links as their full paths.
    [IntPtr]$findHandle = [IntPtr]::Zero

    [Text.StringBuilder] $sbLinkName = [Text.StringBuilder]::New()
    [UInt32] $cchLinkName = $sbLinkName.Capacity
    $findHandle = [PureInvoke.Kernel32]::FindFirstFileNameW($Path, 0, [ref]$cchLinkName, $sbLinkName)
    $errCode = [Marshal]::GetLastWin32Error()
    Write-Debug "[Kernel32]::FindFirstFileNameW(""${Path}"", 0, ${cchLinkName}, ""${sbLinkName}"") return ${findHandle} GetLastError() ${errCode}"
    if ([PureInvoke.Kernel32]::INVALID_HANDLE_VALUE -eq $findHandle)
    {
        if ($errCode -eq [PureInvoke.ErrorCode]::MoreData)
        {
            [void]$sbLinkName.EnsureCapacity($cchLinkName)
            $findHandle = [PureInvoke.Kernel32]::FindFirstFileNameW($Path, 0, [ref]$cchLinkName, $sbLinkName)
            $errCode = [Marshal]::GetLastWin32Error()
            Write-Debug "[Kernel32]::FindFirstFileNameW(""${Path}"", 0, ${cchLinkName}, ""${sbLinkName}"")) return ${findHandle} GetLastError() ${errCode}"
            if ([PureInvoke.Kernel32]::INVALID_HANDLE_VALUE -eq $findHandle)
            {
                Write-Win32Error -ErrorCode $errCode
                return
            }
        }
        else
        {
            Write-Win32Error -ErrorCode $errCode
            return
        }
    }

    $linkName = $sbLinkName.ToString()
    if (-not $linkName)
    {
        Write-Win32Error -ErrorCode $errCode
        return
    }

    $linkName | Write-Output

    try
    {
        do
        {
            [void]$sbLinkName.Clear()

            $result = [PureInvoke.Kernel32]::FindNextFileNameW($findHandle, [ref]$cchLinkName, $sbLinkName)
            $errCode = [Marshal]::GetLastWin32Error()
            Write-Debug "[Kernel32]::FindNextFileNameW(${findHandle}, ${cchLinkName}, ""${sbLinkName}"")) return ${result} GetLastError() ${errCode}"
            if (-not $result -and $errCode -eq [PureInvoke.ErrorCode]::MoreData)
            {
                [void]$sbLinkName.EnsureCapacity($cchLinkName)
                $result = [PureInvoke.Kernel32]::FindNextFileNameW($findHandle, [ref]$cchLinkName, $sbLinkName)
                $errCode = [Marshal]::GetLastWin32Error()
                Write-Debug "[Kernel32]::FindNextFileNameW(${findHandle}, ${cchLinkName}, ""${sbLinkName}"")) return ${result} GetLastError() ${errCode}"
            }

            if ($result)
            {
                $linkName = $sbLinkName.ToString()
                if (-not $linkName)
                {
                    Write-Win32Error -ErrorCode $errCode
                    return
                }

                $linkName | Write-Output
                continue
            }

            if ($errCode -eq [PureInvoke.ErrorCode]::HandleEof)
            {
                return
            }

            if($errCode -eq [PureInvoke.ErrorCode]::InvalidHandle)
            {
                $msg = 'No matching files found.'
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                return
            }

            Write-Win32Error -ErrorCode $errCode
            return
        }
        while ($true)
    }
    finally
    {
        [void][PureInvoke.Kernel32]::FindClose($findHandle);
    }
}



function Invoke-KernelGetVolumePathName
{
    <#
    .SYNOPSIS
    Calls the kernel32.dll libary's `GetVolumePathName` function.
 
    .DESCRIPTION
    The `Invoke-KernelGetVolumePathName` function calls the kernel32.dll libary's `GetVolumePathName` function which
    gets the volume mount point of the path. Pass the path to the `Path` parameter.
 
    .EXAMPLE
    Invoke-KernelGetVolumePathName -Path $path
 
    Demonstrates how to call this function.
    #>

    [CmdletBinding()]
    param(
        # The path whose volume mount point to get.
        [Parameter(Mandatory)]
        [String] $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    # Need to call GetFullPathName to get the size of the necessary buffer.
    $sbPath = [Text.StringBuilder]::New([PureInvoke.Kernel32]::MAX_PATH)
    $cchPath = [UInt32]$sbPath.Capacity; # in/out character-count variable for the WinAPI calls.
    $result = [PureInvoke.Kernel32]::GetVolumePathName($Path, $sbPath, $cchPath)
    # Get the volume (drive) part of the target file's full path (e.g., @"C:\")
    $errCode = [Marshal]::GetLastWin32Error()
    $msg = "[Kernel32]::GetVolumePathName(""${Path}"", [out] ""${sbPath}"", ${cchPath}) return ${result} " +
           "GetLastError() ${errCode}"
    Write-Debug $msg
    if (-not $result -and -not (Assert-Win32Error -ErrorCode $errCode))
    {
        return
    }

    return $sbPath.ToString()
}


function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common
    parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't
    get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the
    function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't
    have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that
    causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add
    explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get
    fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]`
        # attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the
        # `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken
    # from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }
}


function Write-Win32Error
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int] $ErrorCode,

        [String] $Message
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($Message)
    {
        $Message = $Message.TrimEnd('.')
        $Message = "${Message}: "
    }

    $win32Ex = [Win32Exception]::New($ErrorCode)

    $period = '.'
    if ($win32ex.Message.EndsWith('.'))
    {
        $period = ''
    }

    $msg = "${Message}$($win32Ex.Message)${period} (0x$($win32Ex.ErrorCode.ToString('x'))/$($win32Ex.NativeErrorCode))"
    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
}