From 5f23f58b55e0079eca9b0f2a1ee5cb2412526993 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 1 Oct 2024 06:27:07 -0700 Subject: [PATCH 1/4] fixed UserPath return to be localized in `Get-UserProfileLastUseTimeFromDat` --- .../Get-UserProfileLastUseTimeFromDat.ps1 | 3 +- ...et-UserProfileLastUseTimeFromDat.tests.ps1 | 50 ++++++++++++------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/source/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.ps1 b/source/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.ps1 index 0d0bf1c..ccee548 100644 --- a/source/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.ps1 +++ b/source/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.ps1 @@ -89,6 +89,7 @@ function Get-UserProfileLastUseTimeFromDat # Extract the user folder path (everything before 'AppData\Local\Microsoft\Windows') $userPath = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($datFilePath))))) + # Extract the user name based on the user folder path $userName = if ($isLocal) { @@ -106,7 +107,7 @@ function Get-UserProfileLastUseTimeFromDat ComputerName = $ComputerName Username = $userName LastLogon = $lastLogon - UserPath = $userPath + UserPath = (Get-DirectoryPath -BasePath $userPath -IsLocal $true -ComputerName $ComputerName) } } } diff --git a/tests/Unit/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.tests.ps1 b/tests/Unit/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.tests.ps1 index 18d7fab..d2d843c 100644 --- a/tests/Unit/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.tests.ps1 +++ b/tests/Unit/Private/ProfFolderProcessing/Get-UserProfileLastUseTimeFromDat.tests.ps1 @@ -19,25 +19,16 @@ AfterAll { Describe 'Get-UserProfileLastUseTimeFromDat Tests' -Tags "Private", "Unit", "ProfFolderProcessing" { - BeforeAll { - InModuleScope -Scriptblock { - - # Mocking external dependencies - Mock Get-DirectoryPath -MockWith { - "C:\Users\TestUser\AppData\Local\Microsoft\Windows\UsrClass.dat" - } - - Mock Get-ChildItem -MockWith { - @([pscustomobject]@{FullName = 'C:\Users\TestUser\AppData\Local\Microsoft\Windows\UsrClass.dat'; LastWriteTime = (Get-Date).AddDays(-1) }, - [pscustomobject]@{FullName = 'C:\Users\AdminUser\AppData\Local\Microsoft\Windows\UsrClass.dat'; LastWriteTime = (Get-Date).AddDays(-5) }) - } - } - } - Context 'Positive Tests' { It 'Should retrieve UsrClass.dat information from local computer' { InModuleScope -Scriptblock { - $result = Get-UserProfileLastUseTimeFromDat -ComputerName 'TestComputer' + + Mock Get-ChildItem -MockWith { + @([pscustomobject]@{FullName = 'C:\Users\TestUser\AppData\Local\Microsoft\Windows\UsrClass.dat'; LastWriteTime = (Get-Date).AddDays(-1) }, + [pscustomobject]@{FullName = 'C:\Users\AdminUser\AppData\Local\Microsoft\Windows\UsrClass.dat'; LastWriteTime = (Get-Date).AddDays(-5) }) + } + + $result = Get-UserProfileLastUseTimeFromDat $result | Should -Not -BeNullOrEmpty $result.UserPath | Should -Contain 'C:\Users\TestUser' @@ -48,6 +39,12 @@ Describe 'Get-UserProfileLastUseTimeFromDat Tests' -Tags "Private", "Unit", "Pro It 'Should retrieve UsrClass.dat information from remote computer' { InModuleScope -Scriptblock { + + Mock Get-ChildItem -MockWith { + @([pscustomobject]@{FullName = '\\RemoteComputer\C$\Users\TestUser\AppData\Local\Microsoft\Windows\UsrClass.dat'; LastWriteTime = (Get-Date).AddDays(-1) }, + [pscustomobject]@{FullName = '\\RemoteComputer\C$\Users\AdminUser\AppData\Local\Microsoft\Windows\UsrClass.dat'; LastWriteTime = (Get-Date).AddDays(-5) }) + } + $result = Get-UserProfileLastUseTimeFromDat -ComputerName 'RemoteComputer' $result | Should -Not -BeNullOrEmpty @@ -79,9 +76,13 @@ Describe 'Get-UserProfileLastUseTimeFromDat Tests' -Tags "Private", "Unit", "Pro InModuleScope -Scriptblock { Mock Get-ChildItem -MockWith { @() } + Mock Get-DirectoryPath -MockWith { + "C:\Users\TestUser\AppData\Local\Microsoft\Windows\UsrClass.dat" + } + mock Write-Warning - $result = Get-UserProfileLastUseTimeFromDat -ComputerName 'TestComputer' + $result = Get-UserProfileLastUseTimeFromDat $result | Should -Not -BeNullOrEmpty $result[0].Success | Should -Be $false $result[0].Message | Should -Be 'No UsrClass.dat files found.' @@ -126,6 +127,13 @@ Describe 'Get-UserProfileLastUseTimeFromDat Tests' -Tags "Private", "Unit", "Pro Context 'Verbose and Debug Logging Tests' { It 'Should log verbose messages when -Verbose is used' { InModuleScope -Scriptblock { + + Mock Get-DirectoryPath -MockWith { + "C:\Users\TestUser\AppData\Local\Microsoft\Windows\UsrClass.dat" + } + + mock Write-Warning + $VerbosePreference = 'Continue' Mock Write-Verbose @@ -146,6 +154,14 @@ Describe 'Get-UserProfileLastUseTimeFromDat Tests' -Tags "Private", "Unit", "Pro Context 'Performance Tests' { It 'Should complete within acceptable time' { InModuleScope -Scriptblock { + + Mock Get-DirectoryPath -MockWith { + "C:\Users\TestUser\AppData\Local\Microsoft\Windows\UsrClass.dat" + } + + Mock Write-Warning + + $elapsedTime = Measure-Command { Get-UserProfileLastUseTimeFromDat -ComputerName 'TestComputer' } $elapsedTime.TotalMilliseconds | Should -BeLessThan 1000 } From 305f737ea28b04699c514d53dc8eb8ac74bea7f9 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 1 Oct 2024 06:55:55 -0700 Subject: [PATCH 2/4] changed `Get-UserAccountFromSID` to resolve for locally or remotely --- .../Invoke-ProfileRegistryItemProcessing.ps1 | 2 +- .../Get-ProcessedUserProfilesFromFolders.ps1 | 4 +- .../Get-UserAccountFromSID.ps1 | 60 +++++++++++++--- .../Get-UserAccountFromSID.tests.ps1 | 70 +++++++++++++------ 4 files changed, 104 insertions(+), 32 deletions(-) diff --git a/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 b/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 index 1fa8723..cb249f0 100644 --- a/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 +++ b/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 @@ -139,7 +139,7 @@ function Invoke-ProfileRegistryItemProcessing $ParameterHash.IsSpecial = $IsSpecialResults.IsSpecial # Translate SID to user account information - $accountInfo = Get-UserAccountFromSID -SID $Sid + $accountInfo = Get-UserAccountFromSID -SID $Sid -ComputerName $ComputerName $ParameterHash.Domain = $accountInfo.Domain $ParameterHash.UserName = $accountInfo.Username diff --git a/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 b/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 index 8950f2c..3663887 100644 --- a/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 +++ b/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 @@ -81,7 +81,7 @@ function Get-ProcessedUserProfilesFromFolders # Try to resolve SID try { - $SID = Resolve-UsernamesToSIDs -ComputerName -Usernames $userName -WarningAction SilentlyContinue + $SID = Resolve-UsernamesToSIDs -Usernames $userName -WarningAction SilentlyContinue } catch { @@ -98,7 +98,7 @@ function Get-ProcessedUserProfilesFromFolders try { $TestSpecialParams.Add('SID', $SID) - $accountInfo = Get-UserAccountFromSID -SID $SID -WarningAction SilentlyContinue + $accountInfo = Get-UserAccountFromSID -SID $SID -ComputerName $ComputerName -WarningAction SilentlyContinue $domain = $accountInfo.Domain $userName = $accountInfo.Username } diff --git a/source/Private/ProfRegProcessing/Get-UserAccountFromSID.ps1 b/source/Private/ProfRegProcessing/Get-UserAccountFromSID.ps1 index 505a0ba..1539f57 100644 --- a/source/Private/ProfRegProcessing/Get-UserAccountFromSID.ps1 +++ b/source/Private/ProfRegProcessing/Get-UserAccountFromSID.ps1 @@ -1,15 +1,19 @@ <# .SYNOPSIS - Retrieves the domain and username associated with a given Security Identifier (SID). + Retrieves the domain and username associated with a given Security Identifier (SID), locally or remotely. .DESCRIPTION - The `Get-UserAccountFromSID` function takes a Security Identifier (SID) as input and translates it into a corresponding user account's domain and username. The function uses .NET's `System.Security.Principal.SecurityIdentifier` class to perform the translation and returns a custom object containing the SID, domain, and username. + The `Get-UserAccountFromSID` function takes a Security Identifier (SID) as input and translates it into a corresponding user account's domain and username. The function can be executed locally or remotely using PowerShell remoting. - If the SID cannot be translated, the function returns null for the domain and username and issues a warning. + The function uses .NET's `System.Security.Principal.SecurityIdentifier` class to perform the translation and returns a custom object containing the SID, domain, and username. If the SID cannot be translated, it returns null for the domain and username and issues a warning. .PARAMETER SID The Security Identifier (SID) to be translated. This is a required parameter and must not be null or empty. The function supports pipeline input for the SID. +.PARAMETER ComputerName + The name of the computer to perform the translation. If not specified, the function defaults to the local computer (`$env:COMPUTERNAME`). + When a remote computer is specified, the function uses `Invoke-Command` to run the translation remotely. + .OUTPUTS PSCustomObject - An object with the following properties: - SID: The input SID. @@ -29,7 +33,18 @@ S-1-5-21-1234567890-1234567890-1234567890-1001 DOMAIN User Description: - This example retrieves the domain and username associated with the given SID. + This example retrieves the domain and username associated with the given SID on the local computer. + +.EXAMPLE + Get-UserAccountFromSID -SID 'S-1-5-21-1234567890-1234567890-1234567890-1001' -ComputerName 'RemoteServer01' + + Output: + SID Domain Username + --- ------ -------- + S-1-5-21-1234567890-1234567890-1234567890-1001 DOMAIN User + + Description: + This example retrieves the domain and username associated with the given SID from the remote computer 'RemoteServer01'. .EXAMPLE 'S-1-5-21-1234567890-1234567890-1234567890-1001' | Get-UserAccountFromSID @@ -85,7 +100,10 @@ function Get-UserAccountFromSID throw "Invalid SID format: $_" } })] - [string]$SID + [string]$SID, + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME # Default to the local computer ) begin @@ -96,9 +114,35 @@ function Get-UserAccountFromSID { try { - $ntAccount = New-Object System.Security.Principal.SecurityIdentifier($SID) - $userAccount = $ntAccount.Translate([System.Security.Principal.NTAccount]) - $domain, $username = $userAccount.Value.Split('\', 2) + # Define the script block that performs the SID-to-account translation + $scriptBlock = { + param ($SID) + try + { + $ntAccount = New-Object System.Security.Principal.SecurityIdentifier($SID) + $userAccount = $ntAccount.Translate([System.Security.Principal.NTAccount]) + $domain, $username = $userAccount.Value.Split('\', 2) + return [pscustomobject]@{ + Domain = $domain + Username = $username + } + } + catch + { + Write-Warning "Failed to translate SID: $SID" + return [pscustomobject]@{ + Domain = $null + Username = $null + } + } + } + + # Invoke the command locally or remotely + $result = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $SID + + # Assign the returned result to variables + $domain = $result.Domain + $username = $result.Username } catch { diff --git a/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 b/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 index c6dfbd6..bf9c22d 100644 --- a/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 +++ b/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 @@ -17,13 +17,12 @@ AfterAll { Get-Module -Name $script:dscModuleName -All | Remove-Module -Force } -Describe 'Get-UserAccountFromSID Tests' -Tags "Private", "Unit", "ProfileRegProcessing" { +Describe 'Get-UserAccountFromSID Tests' -Tags "Public", "Unit", "ProfileRegProcessing", { BeforeAll { InModuleScope -Scriptblock { - # Mock the System.Security.Principal.SecurityIdentifier .NET class + # Mock the System.Security.Principal.SecurityIdentifier .NET class for both local and remote scenarios Mock -CommandName New-Object -MockWith { - # Mock object to simulate SecurityIdentifier and Translate behavior New-MockObject -Type 'System.Security.Principal.SecurityIdentifier' -Methods @{ Translate = { New-MockObject -Type 'System.Security.Principal.NTAccount' -Properties @{ @@ -32,11 +31,22 @@ Describe 'Get-UserAccountFromSID Tests' -Tags "Private", "Unit", "ProfileRegProc } } } + + # Mock Invoke-Command to handle local and remote execution + Mock -CommandName Invoke-Command -MockWith { + param ($ComputerName, $ScriptBlock, $ArgumentList) + + # Simulate the behavior of the remote execution by returning a mock object + return [pscustomobject]@{ + Domain = 'DOMAIN' + Username = 'User' + } + } } } Context 'Positive Tests' { - It 'Should return correct Domain and Username for a valid SID' { + It 'Should return correct Domain and Username for a valid SID (local execution)' { InModuleScope -Scriptblock { $result = Get-UserAccountFromSID -SID 'S-1-5-21-1234567890-1234567890-1234567890-1001' @@ -44,6 +54,27 @@ Describe 'Get-UserAccountFromSID Tests' -Tags "Private", "Unit", "ProfileRegProc $result.SID | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' $result.Domain | Should -Be 'DOMAIN' $result.Username | Should -Be 'User' + + # Ensure the mocked Invoke-Command was called for local execution + Assert-MockCalled -CommandName Invoke-Command -Exactly 1 -ParameterFilter { + $ComputerName -eq $env:COMPUTERNAME + } + } + } + + It 'Should return correct Domain and Username for a valid SID (remote execution)' { + InModuleScope -Scriptblock { + $result = Get-UserAccountFromSID -SID 'S-1-5-21-1234567890-1234567890-1234567890-1001' -ComputerName 'RemotePC' + + # Validate the returned PSCustomObject + $result.SID | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' + $result.Domain | Should -Be 'DOMAIN' + $result.Username | Should -Be 'User' + + # Ensure the mocked Invoke-Command was called for remote execution + Assert-MockCalled -CommandName Invoke-Command -Exactly 1 -ParameterFilter { + $ComputerName -eq 'RemotePC' + } } } @@ -64,24 +95,23 @@ Describe 'Get-UserAccountFromSID Tests' -Tags "Private", "Unit", "ProfileRegProc } Context 'Negative Tests' { - It 'Should return null for Domain and Username if Translate fails' { + It 'Should return null for Domain and Username if Invoke-Command fails' { InModuleScope -Scriptblock { - # Mock failure of Translate method to simulate error - Mock -CommandName New-Object -MockWith { - New-MockObject -Type 'System.Security.Principal.SecurityIdentifier' -Methods @{ - Translate = { throw "Translation failed" } - } + # Mock Invoke-Command to throw an error, simulating a failure + Mock -CommandName Invoke-Command -MockWith { + throw "Invoke-Command failed" } mock -CommandName Write-Warning $result = Get-UserAccountFromSID -SID 'S-1-5-21-1234567890-1234567890-1234567890-1001' - # Validate the returned PSCustomObject when translation fails + # Validate the returned PSCustomObject when Invoke-Command fails $result.SID | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' $result.Domain | Should -Be $null $result.Username | Should -Be $null + # Ensure that Write-Warning was called with the correct message Assert-MockCalled -CommandName Write-Warning -Scope It -Times 1 -ParameterFilter { $Message -eq 'Failed to translate SID: S-1-5-21-1234567890-1234567890-1234567890-1001' } @@ -90,13 +120,19 @@ Describe 'Get-UserAccountFromSID Tests' -Tags "Private", "Unit", "ProfileRegProc It 'Should throw an error for null SID input' { InModuleScope -Scriptblock { + { Get-UserAccountFromSID -SID $null } | Should -Throw } } - It 'Should throw an error for invliad SID input' { + It 'Should throw an error for invalid SID input' { InModuleScope -Scriptblock { - { Get-UserAccountFromSID -SID "Invaid-SID" } | Should -Throw + mock Write-Warning + { Get-UserAccountFromSID -SID "Invalid-SID" } | Should -Throw + + Assert-MockCalled -CommandName Write-Warning -Times 1 -ParameterFilter { + $Message -eq "Invalid SID format encountered: 'Invalid-SID'." + } } } @@ -128,12 +164,4 @@ Describe 'Get-UserAccountFromSID Tests' -Tags "Private", "Unit", "ProfileRegProc } } } - - Context 'Verbose Logging Tests' { - - } - - Context 'Cleanup Tests' { - - } } From 4be84d726890524c8a8796909a7a357f85ae2735 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 1 Oct 2024 07:32:47 -0700 Subject: [PATCH 3/4] changed `Get-SIDFromUsername` and `Resolve-UsernamesToSIDs` to work remotely --- CHANGELOG.md | 10 +- .../Private/Helpers/Get-SIDFromUsername.ps1 | 107 ++++++++++++++---- .../Resolve-UsernamesToSIDs.ps1 | 25 ++-- .../Remove-UserProfilesFromRegistry.ps1 | 2 +- .../Helpers/Get-SIDFromUsername.tests.ps1 | 96 +++++++++++----- .../Get-UserAccountFromSID.tests.ps1 | 2 +- 6 files changed, 174 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85bb223..4134d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,16 +76,9 @@ from the Windows registry based on SIDs, Usernames, or UserProfile objects. - If the user has administrator privileges, the function retrieves user profiles from the registry using `Get-SIDProfileInfo`. - - If the user lacks administrative privileges, the function falls back to the - `Get-SIDProfileInfoFallback` method, which retrieves user profiles using - CIM/WMI without requiring registry access. - - A warning is logged when the fallback method is used, indicating that special system accounts are excluded. -- Refactored `Process-RegistryProfiles` to better account for access denied errors - when testing profile paths with `Test-FolderExists`. - - Updated `UserProfile` object creation in `Test-OrphanedProfile` for `$AccessError` scenarios. @@ -96,6 +89,9 @@ from the Windows registry based on SIDs, Usernames, or UserProfile objects. (`System.Security.Principal.NTAccount` and `System.Security.Principal.SecurityIdentifier`) instead of relying on `Get-CimInstance` for SID resolution. +- `Get-UserAccountFromSID` and `Get-SIDFromUsername` now invoke locally / Remotely +to resolve without null values + ## [0.2.0] - 2024-09-12 ### Added diff --git a/source/Private/Helpers/Get-SIDFromUsername.ps1 b/source/Private/Helpers/Get-SIDFromUsername.ps1 index 4753ab0..afd74c7 100644 --- a/source/Private/Helpers/Get-SIDFromUsername.ps1 +++ b/source/Private/Helpers/Get-SIDFromUsername.ps1 @@ -1,55 +1,114 @@ <# .SYNOPSIS -Retrieves the Security Identifier (SID) for a given username. + Retrieves the Security Identifier (SID) for a given username either locally or remotely. .DESCRIPTION -The `Get-SIDFromUsername` function resolves the Security Identifier (SID) associated with a given username using the .NET `System.Security.Principal.NTAccount` class. The function translates the provided username into a SID by querying the local system. If the user exists and the SID can be resolved, it is returned. Otherwise, a warning is displayed, and the function returns `$null`. + The `Get-SIDFromUsername` function resolves the Security Identifier (SID) associated with a given username by using the .NET `System.Security.Principal.NTAccount` class. + The function allows execution on a local or remote computer by leveraging PowerShell's `Invoke-Command`. + If the user exists and the SID can be resolved, the SID is returned; otherwise, a warning is displayed, and the function returns `$null`. .PARAMETER Username -Specifies the username for which to retrieve the SID. This parameter is mandatory. + Specifies the username for which to retrieve the SID. This parameter is mandatory and must not be null or empty. + +.PARAMETER ComputerName + Specifies the computer from which to retrieve the SID. If this parameter is not provided, the function will default to the local computer. + When provided, the function will attempt to retrieve the SID from the specified remote computer. + +.OUTPUTS + String - The Security Identifier (SID) associated with the provided username. + If the SID cannot be resolved, the function returns `$null`. + +.NOTES + This function uses the .NET `System.Security.Principal.NTAccount` class to resolve the username into a SID. It can query either the local system or a remote system (via PowerShell remoting). + If PowerShell remoting is disabled or the specified remote computer is unreachable, the function will issue a warning and return `$null`. .EXAMPLE -Get-SIDFromUsername -Username 'JohnDoe' + Get-SIDFromUsername -Username 'JohnDoe' -Description: -This command retrieves the SID for the user 'JohnDoe' from the local computer. If the user exists and the SID is found, it is returned; otherwise, a warning will be displayed. + Description: + Retrieves the SID for the user 'JohnDoe' from the local computer. If the user exists and the SID is found, it will be returned. .EXAMPLE -Get-SIDFromUsername -Username 'LocalAdmin' + Get-SIDFromUsername -Username 'JohnDoe' -ComputerName 'Server01' -Description: -This command retrieves the SID for the user 'LocalAdmin' from the local computer. If the user exists and the SID is found, it is returned; otherwise, a warning will be displayed. + Description: + Retrieves the SID for the user 'JohnDoe' from the remote computer 'Server01'. If the user exists and the SID is found, it will be returned. -.NOTES -This function does not use WMI or CIM for querying user information, but rather the .NET `System.Security.Principal.NTAccount` class, which directly translates the username to a SID. As a result, this function works for both local and domain accounts if the appropriate access is available. -#> +.EXAMPLE + Get-SIDFromUsername -Username 'Administrator' + + Description: + Retrieves the SID for the 'Administrator' account from the local computer. This works for both local and domain accounts. + +.EXAMPLE + Get-SIDFromUsername -Username 'Administrator' -ComputerName 'Server01' + + Description: + Retrieves the SID for the 'Administrator' account from the remote computer 'Server01'. If the user account exists and the SID can be resolved, it will be returned. + +.EXAMPLE + $sids = @('User1', 'User2') | ForEach-Object { Get-SIDFromUsername -Username $_ } + Description: + Retrieves the SIDs for multiple users by passing the usernames through the pipeline and invoking the function for each user. + +.EXAMPLE + Get-SIDFromUsername -Username 'NonExistentUser' + + Warning: + Failed to retrieve SID for username: NonExistentUser + + Output: + $null + + Description: + Attempts to retrieve the SID for a user that does not exist. In this case, the function issues a warning and returns `$null`. + +#> function Get-SIDFromUsername { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [string]$Username + [string]$Username, + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME # Default to the local computer ) try { - # Query WMI to get the SID for the given username - $ntAccount = New-Object System.Security.Principal.NTAccount($Username) - - $SID = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]) + # Define the script block for translating Username to SID + $scriptBlock = { + param ($Username) + try + { + $ntAccount = New-Object System.Security.Principal.NTAccount($Username) + $SID = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]) - if ($Null -ne $SID -and $Null -ne $SID.Value) - { - return $SID.value - } - else - { - return $null + if ($null -ne $SID -and $null -ne $SID.Value) + { + return $SID.Value + } + else + { + return $null + } + } + catch + { + return $null + } } + + # Use Invoke-Command to run the script block locally or remotely + $result = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $Username + + return $result } catch { + Write-Warning "Failed to retrieve SID for username: $Username" return $null } } diff --git a/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 index 427acf5..f7d1d27 100644 --- a/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 +++ b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 @@ -3,27 +3,37 @@ Resolves a list of usernames to their corresponding Security Identifiers (SIDs). .DESCRIPTION -The `Resolve-UsernamesToSIDs` function resolves each provided username to its corresponding Security Identifier (SID) using the .NET `System.Security.Principal.NTAccount` class. For each username in the input array, the function attempts to resolve the username locally. If a username cannot be resolved, a warning is logged, and the function continues processing the next username. +The `Resolve-UsernamesToSIDs` function resolves each provided username to its corresponding Security Identifier (SID) on a specified computer or the local machine. It uses the `Get-SIDFromUsername` function, which can resolve usernames to SIDs either locally or remotely. For each username, the function attempts to resolve the username on the specified computer. If a username cannot be resolved, a warning is logged, and the function continues processing the next username. .PARAMETER Usernames Specifies an array of usernames to resolve to SIDs. This parameter is mandatory. +.PARAMETER ComputerName +Specifies the name of the computer on which to resolve the usernames to SIDs. If not provided, the function defaults to the local computer. + .EXAMPLE Resolve-UsernamesToSIDs -Usernames 'user1', 'user2' Description: Resolves the SIDs for 'user1' and 'user2' on the local computer. +.EXAMPLE +Resolve-UsernamesToSIDs -Usernames 'user1', 'user2' -ComputerName 'Server01' + +Description: +Resolves the SIDs for 'user1' and 'user2' on the remote computer 'Server01'. + .OUTPUTS -Array of SIDs corresponding to the provided usernames. If a username cannot be resolved, it will not be included in the output array, and a warning will be logged. +Array of custom objects containing the username and the corresponding SID. If a username cannot be resolved, the SID will be null, and a warning will be logged. .NOTES -This function uses the `Get-SIDFromUsername` function, which internally uses the .NET `System.Security.Principal.NTAccount` class for resolving SIDs. It does not support resolving SIDs from remote computers and works only on the local system. +This function supports resolving SIDs on remote computers using the `ComputerName` parameter. #> function Resolve-UsernamesToSIDs { param ( - [string[]]$Usernames + [string[]]$Usernames, + [string]$ComputerName = $env:COMPUTERNAME ) $SIDs = @() @@ -32,10 +42,12 @@ function Resolve-UsernamesToSIDs { try { - $SID = Get-SIDFromUsername -Username $Username + $SID = Get-SIDFromUsername -Username $Username -ComputerName $ComputerName } catch {} - if ($Null -ne $SID -and $Null -ne $SID) + + # Ensure $SID is not $null before adding to $SIDs array + if ($SID) { $SIDs += $SID } @@ -43,7 +55,6 @@ function Resolve-UsernamesToSIDs { Write-Warning "Could not resolve SID for username $Username." } - } return $SIDs diff --git a/source/Public/Remove-UserProfilesFromRegistry.ps1 b/source/Public/Remove-UserProfilesFromRegistry.ps1 index 8190357..4d64936 100644 --- a/source/Public/Remove-UserProfilesFromRegistry.ps1 +++ b/source/Public/Remove-UserProfilesFromRegistry.ps1 @@ -95,7 +95,7 @@ function Remove-UserProfilesFromRegistry # Resolve SIDs if Usernames are provided if ($PSCmdlet.ParameterSetName -eq 'UserNameSet') { - $SIDs = Resolve-UsernamesToSIDs -Usernames $Usernames + $SIDs = Resolve-UsernamesToSIDs -Usernames $Usernames -ComputerName $ComputerName # If no SIDs were resolved, stop execution by throwing a terminating error if (-not $SIDs) diff --git a/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 b/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 index 74183cf..35b8215 100644 --- a/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 +++ b/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 @@ -17,18 +17,17 @@ AfterAll { Get-Module -Name $script:dscModuleName -All | Remove-Module -Force } + Describe 'Get-SIDFromUsername' -Tags "Private", "Helpers" { Context 'When the username exists and has a valid SID' { - It 'should return the correct SID' { + It 'should return the correct SID for local execution' { InModuleScope -ScriptBlock { - # Mock NTAccount and SecurityIdentifier - Mock -CommandName New-Object -MockWith { - New-MockObject -Type 'System.Security.Principal.NTAccount' -Methods @{ - Translate = { New-MockObject -Type 'System.Security.Principal.SecurityIdentifier' -Properties @{ Value = 'S-1-5-21-1234567890-1234567890-1234567890-1001' } } - } + # Mock Invoke-Command for local execution + Mock -CommandName Invoke-Command -MockWith { + return 'S-1-5-21-1234567890-1234567890-1234567890-1001' } # Act: Call the function @@ -36,6 +35,31 @@ Describe 'Get-SIDFromUsername' -Tags "Private", "Helpers" { # Assert: Verify the result is the correct SID $result | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' + + # Ensure Invoke-Command was called once + Assert-MockCalled Invoke-Command -Exactly 1 + } + } + + It 'should return the correct SID for remote execution' { + + InModuleScope -ScriptBlock { + + # Mock Invoke-Command for remote execution + Mock -CommandName Invoke-Command -MockWith { + return 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + + # Act: Call the function with a remote ComputerName + $result = Get-SIDFromUsername -Username 'JohnDoe' -ComputerName 'RemoteComputer' + + # Assert: Verify the result is the correct SID + $result | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' + + # Ensure Invoke-Command was called once with remote ComputerName + Assert-MockCalled Invoke-Command -Exactly 1 -ParameterFilter { + $ComputerName -eq 'RemoteComputer' + } } } } @@ -45,9 +69,9 @@ Describe 'Get-SIDFromUsername' -Tags "Private", "Helpers" { InModuleScope -ScriptBlock { - # Mock NTAccount to throw an error (user not found) - Mock -CommandName New-Object -MockWith { - throw [System.Security.Principal.IdentityNotMappedException]::new("User not found") + # Mock Invoke-Command to simulate user not found + Mock -CommandName Invoke-Command -MockWith { + return $null } # Act: Call the function @@ -56,22 +80,24 @@ Describe 'Get-SIDFromUsername' -Tags "Private", "Helpers" { # Assert: The result should be null $result | Should -BeNullOrEmpty + # Ensure Invoke-Command was called once + Assert-MockCalled Invoke-Command -Exactly 1 } } } - Context 'When an error occurs while resolving the username' { - It 'should return null and display a warning with error information' { + Context 'When an error occurs during SID resolution' { + It 'should return null and display a warning' { InModuleScope -ScriptBlock { - # Mock NTAccount to throw a general exception - Mock -CommandName New-Object -MockWith { + # Mock Invoke-Command to simulate an error + Mock -CommandName Invoke-Command -MockWith { throw "An unexpected error occurred" } - # Mock Write-Warning to capture the warning message - #Mock -CommandName Write-Warning + # Mock Write-Warning to capture the warning + Mock Write-Warning # Act: Call the function $result = Get-SIDFromUsername -Username 'JohnDoe' @@ -79,35 +105,49 @@ Describe 'Get-SIDFromUsername' -Tags "Private", "Helpers" { # Assert: The result should be null $result | Should -BeNullOrEmpty - # Assert: Verify that the warning message was displayed - #Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + # Ensure Write-Warning was called once + Assert-MockCalled Write-Warning -Exactly 1 } } } - Context 'When the SID is missing for a user' { + Context 'When SID is missing for the user' { It 'should return null and display a warning' { InModuleScope -ScriptBlock { - # Mock NTAccount to return null for SID - Mock -CommandName New-Object -MockWith { - New-MockObject -Type 'System.Security.Principal.NTAccount' -Methods @{ - Translate = { throw } - } + # Mock Invoke-Command to simulate a user with no SID + Mock -CommandName Invoke-Command -MockWith { + return $null } - # Mock Write-Warning to capture the warning message - #Mock -CommandName Write-Warning - # Act: Call the function $result = Get-SIDFromUsername -Username 'JohnDoe' # Assert: The result should be null $result | Should -BeNullOrEmpty - # Assert: Verify that the warning message was displayed - #Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + # Ensure Invoke-Command was called once + Assert-MockCalled Invoke-Command -Exactly 1 + } + } + } + + Context 'When executing for performance' { + It 'should complete within the acceptable time frame' { + + InModuleScope -ScriptBlock { + + # Mock Invoke-Command for local execution + Mock -CommandName Invoke-Command -MockWith { + return 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + + # Act: Measure the time taken for function execution + $elapsedTime = Measure-Command { Get-SIDFromUsername -Username 'JohnDoe' } + + # Assert: Ensure execution time is less than 1000 milliseconds + $elapsedTime.TotalMilliseconds | Should -BeLessThan 1000 } } } diff --git a/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 b/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 index bf9c22d..44bc139 100644 --- a/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 +++ b/tests/Unit/Private/ProfRegProcessing/Get-UserAccountFromSID.tests.ps1 @@ -17,7 +17,7 @@ AfterAll { Get-Module -Name $script:dscModuleName -All | Remove-Module -Force } -Describe 'Get-UserAccountFromSID Tests' -Tags "Public", "Unit", "ProfileRegProcessing", { +Describe 'Get-UserAccountFromSID Tests' -Tags "Public", "Unit", "ProfileRegProcessing" { BeforeAll { InModuleScope -Scriptblock { From 086ac3baad1088b47c5b16c46768a528f1304d41 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 1 Oct 2024 12:17:29 -0700 Subject: [PATCH 4/4] changed `Write-Warning` preference on functions --- .../Invoke-ProfileRegistryItemProcessing.ps1 | 2 +- .../Get-ProcessedUserProfilesFromFolders.ps1 | 2 +- source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 | 2 +- source/Public/Get-UserProfilesFromFolders.ps1 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 b/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 index cb249f0..8c4bece 100644 --- a/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 +++ b/source/Private/Get-ProfileRegistryItems/Invoke-ProfileRegistryItemProcessing.ps1 @@ -139,7 +139,7 @@ function Invoke-ProfileRegistryItemProcessing $ParameterHash.IsSpecial = $IsSpecialResults.IsSpecial # Translate SID to user account information - $accountInfo = Get-UserAccountFromSID -SID $Sid -ComputerName $ComputerName + $accountInfo = Get-UserAccountFromSID -SID $Sid -ComputerName $ComputerName -WarningAction SilentlyContinue $ParameterHash.Domain = $accountInfo.Domain $ParameterHash.UserName = $accountInfo.Username diff --git a/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 b/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 index 3663887..bad5ad6 100644 --- a/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 +++ b/source/Private/ProfFolderProcessing/Get-ProcessedUserProfilesFromFolders.ps1 @@ -81,7 +81,7 @@ function Get-ProcessedUserProfilesFromFolders # Try to resolve SID try { - $SID = Resolve-UsernamesToSIDs -Usernames $userName -WarningAction SilentlyContinue + $SID = Resolve-UsernamesToSIDs -Usernames $userName -ComputerName $ComputerName } catch { diff --git a/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 index f7d1d27..d4f292a 100644 --- a/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 +++ b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 @@ -53,7 +53,7 @@ function Resolve-UsernamesToSIDs } else { - Write-Warning "Could not resolve SID for username $Username." + Write-Verbose "Could not resolve SID for username $Username." } } diff --git a/source/Public/Get-UserProfilesFromFolders.ps1 b/source/Public/Get-UserProfilesFromFolders.ps1 index 2fd16ac..ecb2e44 100644 --- a/source/Public/Get-UserProfilesFromFolders.ps1 +++ b/source/Public/Get-UserProfilesFromFolders.ps1 @@ -64,7 +64,7 @@ function Get-UserProfilesFromFolders return @() # Return an empty array } - Get-ProcessedUserProfilesFromFolders -UserFolders $UserFolders -ComputerName $ComputerName + Get-ProcessedUserProfilesFromFolders -UserFolders $UserFolders -ComputerName $ComputerName } catch